Code

Merge branch 'master' of git://repo.or.cz/git-gui
authorJunio C Hamano <gitster@pobox.com>
Fri, 7 Dec 2007 07:51:15 +0000 (23:51 -0800)
committerJunio C Hamano <gitster@pobox.com>
Fri, 7 Dec 2007 07:51:15 +0000 (23:51 -0800)
* 'master' of git://repo.or.cz/git-gui:
  Update Hungarian translation. 100% completed.
  Update ja.po for git-gui
  git-gui: Improve the application icon on Windows.
  git-gui: install-sh from automake does not like -m755
  git-gui: Reorder msgfmt command-line arguments
  Update German translation. 100% completed.
  Update git-gui.pot with latest (few) string additions and changes.
  git-gui: update it.po and glossary/it.po
  git-gui: fix a typo in lib/commit.tcl

1202 files changed:
.gitignore
.mailmap [new file with mode: 0644]
COPYING [new file with mode: 0644]
Documentation/.gitignore [new file with mode: 0644]
Documentation/CodingGuidelines [new file with mode: 0644]
Documentation/Makefile [new file with mode: 0644]
Documentation/RelNotes-1.5.0.1.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.0.2.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.0.3.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.0.4.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.0.5.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.0.6.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.0.7.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.0.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.1.1.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.1.2.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.1.3.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.1.4.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.1.5.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.1.6.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.1.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.2.1.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.2.2.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.2.3.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.2.4.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.2.5.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.2.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.3.1.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.3.2.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.3.3.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.3.4.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.3.5.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.3.6.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.3.7.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.3.txt [new file with mode: 0644]
Documentation/RelNotes-1.5.4.txt [new file with mode: 0644]
Documentation/SubmittingPatches [new file with mode: 0644]
Documentation/asciidoc.conf [new file with mode: 0644]
Documentation/blame-options.txt [new file with mode: 0644]
Documentation/build-docdep.perl [new file with mode: 0755]
Documentation/callouts.xsl [new file with mode: 0644]
Documentation/cmd-list.perl [new file with mode: 0755]
Documentation/config.txt [new file with mode: 0644]
Documentation/core-tutorial.txt [new file with mode: 0644]
Documentation/cvs-migration.txt [new file with mode: 0644]
Documentation/diff-format.txt [new file with mode: 0644]
Documentation/diff-generate-patch.txt [new file with mode: 0644]
Documentation/diff-options.txt [new file with mode: 0644]
Documentation/diffcore.txt [new file with mode: 0644]
Documentation/docbook-xsl.css [new file with mode: 0644]
Documentation/docbook.xsl [new file with mode: 0644]
Documentation/everyday.txt [new file with mode: 0644]
Documentation/fetch-options.txt [new file with mode: 0644]
Documentation/fix-texi.perl [new file with mode: 0755]
Documentation/git-add.txt [new file with mode: 0644]
Documentation/git-am.txt [new file with mode: 0644]
Documentation/git-annotate.txt [new file with mode: 0644]
Documentation/git-apply.txt [new file with mode: 0644]
Documentation/git-archimport.txt [new file with mode: 0644]
Documentation/git-archive.txt [new file with mode: 0644]
Documentation/git-bisect.txt [new file with mode: 0644]
Documentation/git-blame.txt [new file with mode: 0644]
Documentation/git-branch.txt [new file with mode: 0644]
Documentation/git-bundle.txt [new file with mode: 0644]
Documentation/git-cat-file.txt [new file with mode: 0644]
Documentation/git-check-attr.txt [new file with mode: 0644]
Documentation/git-check-ref-format.txt [new file with mode: 0644]
Documentation/git-checkout-index.txt [new file with mode: 0644]
Documentation/git-checkout.txt [new file with mode: 0644]
Documentation/git-cherry-pick.txt [new file with mode: 0644]
Documentation/git-cherry.txt [new file with mode: 0644]
Documentation/git-citool.txt [new file with mode: 0644]
Documentation/git-clean.txt [new file with mode: 0644]
Documentation/git-clone.txt [new file with mode: 0644]
Documentation/git-commit-tree.txt [new file with mode: 0644]
Documentation/git-commit.txt [new file with mode: 0644]
Documentation/git-config.txt [new file with mode: 0644]
Documentation/git-count-objects.txt [new file with mode: 0644]
Documentation/git-cvsexportcommit.txt [new file with mode: 0644]
Documentation/git-cvsimport.txt [new file with mode: 0644]
Documentation/git-cvsserver.txt [new file with mode: 0644]
Documentation/git-daemon.txt [new file with mode: 0644]
Documentation/git-describe.txt [new file with mode: 0644]
Documentation/git-diff-files.txt [new file with mode: 0644]
Documentation/git-diff-index.txt [new file with mode: 0644]
Documentation/git-diff-tree.txt [new file with mode: 0644]
Documentation/git-diff.txt [new file with mode: 0644]
Documentation/git-fast-export.txt [new file with mode: 0644]
Documentation/git-fast-import.txt [new file with mode: 0644]
Documentation/git-fetch-pack.txt [new file with mode: 0644]
Documentation/git-fetch.txt [new file with mode: 0644]
Documentation/git-filter-branch.txt [new file with mode: 0644]
Documentation/git-fmt-merge-msg.txt [new file with mode: 0644]
Documentation/git-for-each-ref.txt [new file with mode: 0644]
Documentation/git-format-patch.txt [new file with mode: 0644]
Documentation/git-fsck-objects.txt [new file with mode: 0644]
Documentation/git-fsck.txt [new file with mode: 0644]
Documentation/git-gc.txt [new file with mode: 0644]
Documentation/git-get-tar-commit-id.txt [new file with mode: 0644]
Documentation/git-grep.txt [new file with mode: 0644]
Documentation/git-gui.txt [new file with mode: 0644]
Documentation/git-hash-object.txt [new file with mode: 0644]
Documentation/git-help.txt [new file with mode: 0644]
Documentation/git-http-fetch.txt [new file with mode: 0644]
Documentation/git-http-push.txt [new file with mode: 0644]
Documentation/git-imap-send.txt [new file with mode: 0644]
Documentation/git-index-pack.txt [new file with mode: 0644]
Documentation/git-init-db.txt [new file with mode: 0644]
Documentation/git-init.txt [new file with mode: 0644]
Documentation/git-instaweb.txt [new file with mode: 0644]
Documentation/git-log.txt [new file with mode: 0644]
Documentation/git-lost-found.txt [new file with mode: 0644]
Documentation/git-ls-files.txt [new file with mode: 0644]
Documentation/git-ls-remote.txt [new file with mode: 0644]
Documentation/git-ls-tree.txt [new file with mode: 0644]
Documentation/git-mailinfo.txt [new file with mode: 0644]
Documentation/git-mailsplit.txt [new file with mode: 0644]
Documentation/git-merge-base.txt [new file with mode: 0644]
Documentation/git-merge-file.txt [new file with mode: 0644]
Documentation/git-merge-index.txt [new file with mode: 0644]
Documentation/git-merge-one-file.txt [new file with mode: 0644]
Documentation/git-merge-tree.txt [new file with mode: 0644]
Documentation/git-merge.txt [new file with mode: 0644]
Documentation/git-mergetool.txt [new file with mode: 0644]
Documentation/git-mktag.txt [new file with mode: 0644]
Documentation/git-mktree.txt [new file with mode: 0644]
Documentation/git-mv.txt [new file with mode: 0644]
Documentation/git-name-rev.txt [new file with mode: 0644]
Documentation/git-pack-objects.txt [new file with mode: 0644]
Documentation/git-pack-redundant.txt [new file with mode: 0644]
Documentation/git-pack-refs.txt [new file with mode: 0644]
Documentation/git-parse-remote.txt [new file with mode: 0644]
Documentation/git-patch-id.txt [new file with mode: 0644]
Documentation/git-peek-remote.txt [new file with mode: 0644]
Documentation/git-prune-packed.txt [new file with mode: 0644]
Documentation/git-prune.txt [new file with mode: 0644]
Documentation/git-pull.txt [new file with mode: 0644]
Documentation/git-push.txt [new file with mode: 0644]
Documentation/git-quiltimport.txt [new file with mode: 0644]
Documentation/git-read-tree.txt [new file with mode: 0644]
Documentation/git-rebase.txt [new file with mode: 0644]
Documentation/git-receive-pack.txt [new file with mode: 0644]
Documentation/git-reflog.txt [new file with mode: 0644]
Documentation/git-relink.txt [new file with mode: 0644]
Documentation/git-remote.txt [new file with mode: 0644]
Documentation/git-repack.txt [new file with mode: 0644]
Documentation/git-repo-config.txt [new file with mode: 0644]
Documentation/git-request-pull.txt [new file with mode: 0644]
Documentation/git-rerere.txt [new file with mode: 0644]
Documentation/git-reset.txt [new file with mode: 0644]
Documentation/git-rev-list.txt [new file with mode: 0644]
Documentation/git-rev-parse.txt [new file with mode: 0644]
Documentation/git-revert.txt [new file with mode: 0644]
Documentation/git-rm.txt [new file with mode: 0644]
Documentation/git-runstatus.txt [new file with mode: 0644]
Documentation/git-send-email.txt [new file with mode: 0644]
Documentation/git-send-pack.txt [new file with mode: 0644]
Documentation/git-sh-setup.txt [new file with mode: 0644]
Documentation/git-shell.txt [new file with mode: 0644]
Documentation/git-shortlog.txt [new file with mode: 0644]
Documentation/git-show-branch.txt [new file with mode: 0644]
Documentation/git-show-index.txt [new file with mode: 0644]
Documentation/git-show-ref.txt [new file with mode: 0644]
Documentation/git-show.txt [new file with mode: 0644]
Documentation/git-stash.txt [new file with mode: 0644]
Documentation/git-status.txt [new file with mode: 0644]
Documentation/git-stripspace.txt [new file with mode: 0644]
Documentation/git-submodule.txt [new file with mode: 0644]
Documentation/git-svn.txt [new file with mode: 0644]
Documentation/git-symbolic-ref.txt [new file with mode: 0644]
Documentation/git-tag.txt [new file with mode: 0644]
Documentation/git-tar-tree.txt [new file with mode: 0644]
Documentation/git-tools.txt [new file with mode: 0644]
Documentation/git-unpack-file.txt [new file with mode: 0644]
Documentation/git-unpack-objects.txt [new file with mode: 0644]
Documentation/git-update-index.txt [new file with mode: 0644]
Documentation/git-update-ref.txt [new file with mode: 0644]
Documentation/git-update-server-info.txt [new file with mode: 0644]
Documentation/git-upload-archive.txt [new file with mode: 0644]
Documentation/git-upload-pack.txt [new file with mode: 0644]
Documentation/git-var.txt [new file with mode: 0644]
Documentation/git-verify-pack.txt [new file with mode: 0644]
Documentation/git-verify-tag.txt [new file with mode: 0644]
Documentation/git-whatchanged.txt [new file with mode: 0644]
Documentation/git-write-tree.txt [new file with mode: 0644]
Documentation/git.txt [new file with mode: 0644]
Documentation/gitattributes.txt [new file with mode: 0644]
Documentation/gitignore.txt [new file with mode: 0644]
Documentation/gitk.txt [new file with mode: 0644]
Documentation/gitmodules.txt [new file with mode: 0644]
Documentation/glossary.txt [new file with mode: 0644]
Documentation/hooks.txt [new file with mode: 0644]
Documentation/howto-index.sh [new file with mode: 0755]
Documentation/howto/maintain-git.txt [new file with mode: 0644]
Documentation/howto/rebase-and-edit.txt [new file with mode: 0644]
Documentation/howto/rebase-from-internal-branch.txt [new file with mode: 0644]
Documentation/howto/rebuild-from-update-hook.txt [new file with mode: 0644]
Documentation/howto/recover-corrupted-blob-object.txt [new file with mode: 0644]
Documentation/howto/revert-branch-rebase.txt [new file with mode: 0644]
Documentation/howto/separating-topic-branches.txt [new file with mode: 0644]
Documentation/howto/setup-git-server-over-http.txt [new file with mode: 0644]
Documentation/howto/update-hook-example.txt [new file with mode: 0644]
Documentation/howto/use-git-daemon.txt [new file with mode: 0644]
Documentation/i18n.txt [new file with mode: 0644]
Documentation/install-doc-quick.sh [new file with mode: 0755]
Documentation/install-webdoc.sh [new file with mode: 0755]
Documentation/merge-options.txt [new file with mode: 0644]
Documentation/merge-strategies.txt [new file with mode: 0644]
Documentation/pretty-formats.txt [new file with mode: 0644]
Documentation/pretty-options.txt [new file with mode: 0644]
Documentation/pull-fetch-param.txt [new file with mode: 0644]
Documentation/repository-layout.txt [new file with mode: 0644]
Documentation/technical/pack-format.txt [new file with mode: 0644]
Documentation/technical/pack-heuristics.txt [new file with mode: 0644]
Documentation/technical/pack-protocol.txt [new file with mode: 0644]
Documentation/technical/racy-git.txt [new file with mode: 0644]
Documentation/technical/send-pack-pipeline.txt [new file with mode: 0644]
Documentation/technical/shallow.txt [new file with mode: 0644]
Documentation/technical/trivial-merge.txt [new file with mode: 0644]
Documentation/tutorial-2.txt [new file with mode: 0644]
Documentation/tutorial.txt [new file with mode: 0644]
Documentation/urls-remotes.txt [new file with mode: 0644]
Documentation/urls.txt [new file with mode: 0644]
Documentation/user-manual.conf [new file with mode: 0644]
Documentation/user-manual.txt [new file with mode: 0644]
GIT-VERSION-GEN
INSTALL [new file with mode: 0644]
Makefile
README [new file with mode: 0644]
RelNotes [new symlink]
alloc.c [new file with mode: 0644]
archive-tar.c [new file with mode: 0644]
archive-zip.c [new file with mode: 0644]
archive.h [new file with mode: 0644]
arm/sha1.c [new file with mode: 0644]
arm/sha1.h [new file with mode: 0644]
arm/sha1_arm.S [new file with mode: 0644]
attr.c [new file with mode: 0644]
attr.h [new file with mode: 0644]
base85.c [new file with mode: 0644]
blob.c [new file with mode: 0644]
blob.h [new file with mode: 0644]
builtin-add.c [new file with mode: 0644]
builtin-annotate.c [new file with mode: 0644]
builtin-apply.c [new file with mode: 0644]
builtin-archive.c [new file with mode: 0644]
builtin-blame.c [new file with mode: 0644]
builtin-branch.c [new file with mode: 0644]
builtin-bundle.c [new file with mode: 0644]
builtin-cat-file.c [new file with mode: 0644]
builtin-check-attr.c [new file with mode: 0644]
builtin-check-ref-format.c [new file with mode: 0644]
builtin-checkout-index.c [new file with mode: 0644]
builtin-clean.c [new file with mode: 0644]
builtin-commit-tree.c [new file with mode: 0644]
builtin-commit.c [new file with mode: 0644]
builtin-config.c [new file with mode: 0644]
builtin-count-objects.c [new file with mode: 0644]
builtin-describe.c [new file with mode: 0644]
builtin-diff-files.c [new file with mode: 0644]
builtin-diff-index.c [new file with mode: 0644]
builtin-diff-tree.c [new file with mode: 0644]
builtin-diff.c [new file with mode: 0644]
builtin-fast-export.c [new file with mode: 0755]
builtin-fetch--tool.c [new file with mode: 0644]
builtin-fetch-pack.c [new file with mode: 0644]
builtin-fetch.c [new file with mode: 0644]
builtin-fmt-merge-msg.c [new file with mode: 0644]
builtin-for-each-ref.c [new file with mode: 0644]
builtin-fsck.c [new file with mode: 0644]
builtin-gc.c [new file with mode: 0644]
builtin-grep.c [new file with mode: 0644]
builtin-http-fetch.c [new file with mode: 0644]
builtin-init-db.c [new file with mode: 0644]
builtin-log.c [new file with mode: 0644]
builtin-ls-files.c [new file with mode: 0644]
builtin-ls-remote.c [new file with mode: 0644]
builtin-ls-tree.c [new file with mode: 0644]
builtin-mailinfo.c [new file with mode: 0644]
builtin-mailsplit.c [new file with mode: 0644]
builtin-merge-base.c [new file with mode: 0644]
builtin-merge-file.c [new file with mode: 0644]
builtin-merge-ours.c [new file with mode: 0644]
builtin-mv.c [new file with mode: 0644]
builtin-name-rev.c [new file with mode: 0644]
builtin-pack-objects.c [new file with mode: 0644]
builtin-pack-refs.c [new file with mode: 0644]
builtin-prune-packed.c [new file with mode: 0644]
builtin-prune.c [new file with mode: 0644]
builtin-push.c [new file with mode: 0644]
builtin-read-tree.c [new file with mode: 0644]
builtin-reflog.c [new file with mode: 0644]
builtin-rerere.c [new file with mode: 0644]
builtin-reset.c [new file with mode: 0644]
builtin-rev-list.c [new file with mode: 0644]
builtin-rev-parse.c [new file with mode: 0644]
builtin-revert.c [new file with mode: 0644]
builtin-rm.c [new file with mode: 0644]
builtin-runstatus.c [new file with mode: 0644]
builtin-send-pack.c [new file with mode: 0644]
builtin-shortlog.c [new file with mode: 0644]
builtin-show-branch.c [new file with mode: 0644]
builtin-show-ref.c [new file with mode: 0644]
builtin-stripspace.c [new file with mode: 0644]
builtin-symbolic-ref.c [new file with mode: 0644]
builtin-tag.c [new file with mode: 0644]
builtin-tar-tree.c [new file with mode: 0644]
builtin-unpack-objects.c [new file with mode: 0644]
builtin-update-index.c [new file with mode: 0644]
builtin-update-ref.c [new file with mode: 0644]
builtin-upload-archive.c [new file with mode: 0644]
builtin-verify-pack.c [new file with mode: 0644]
builtin-verify-tag.c [new file with mode: 0644]
builtin-write-tree.c [new file with mode: 0644]
builtin.h [new file with mode: 0644]
bundle.c [new file with mode: 0644]
bundle.h [new file with mode: 0644]
cache-tree.c [new file with mode: 0644]
cache-tree.h [new file with mode: 0644]
cache.h [new file with mode: 0644]
check-builtins.sh [new file with mode: 0755]
check-racy.c [new file with mode: 0644]
color.c [new file with mode: 0644]
color.h [new file with mode: 0644]
combine-diff.c [new file with mode: 0644]
command-list.txt [new file with mode: 0644]
commit.c [new file with mode: 0644]
commit.h [new file with mode: 0644]
compat/hstrerror.c [new file with mode: 0644]
compat/inet_ntop.c [new file with mode: 0644]
compat/inet_pton.c [new file with mode: 0644]
compat/memmem.c [new file with mode: 0644]
compat/mkdtemp.c [new file with mode: 0644]
compat/mmap.c [new file with mode: 0644]
compat/pread.c [new file with mode: 0644]
compat/setenv.c [new file with mode: 0644]
compat/strcasestr.c [new file with mode: 0644]
compat/strlcpy.c [new file with mode: 0644]
compat/strtoumax.c [new file with mode: 0644]
compat/unsetenv.c [new file with mode: 0644]
config.c [new file with mode: 0644]
config.mak.in [new file with mode: 0644]
configure.ac [new file with mode: 0644]
connect.c [new file with mode: 0644]
contrib/README [new file with mode: 0644]
contrib/blameview/README [new file with mode: 0644]
contrib/blameview/blameview.perl [new file with mode: 0755]
contrib/completion/git-completion.bash [new file with mode: 0755]
contrib/continuous/cidaemon [new file with mode: 0644]
contrib/continuous/post-receive-cinotify [new file with mode: 0644]
contrib/convert-objects/convert-objects.c [new file with mode: 0644]
contrib/convert-objects/git-convert-objects.txt [new file with mode: 0644]
contrib/emacs/.gitignore [new file with mode: 0644]
contrib/emacs/Makefile [new file with mode: 0644]
contrib/emacs/git-blame.el [new file with mode: 0644]
contrib/emacs/git.el [new file with mode: 0644]
contrib/emacs/vc-git.el [new file with mode: 0644]
contrib/examples/git-clean.sh [new file with mode: 0755]
contrib/examples/git-commit.sh [new file with mode: 0755]
contrib/examples/git-fetch.sh [new file with mode: 0755]
contrib/examples/git-gc.sh [new file with mode: 0755]
contrib/examples/git-ls-remote.sh [new file with mode: 0755]
contrib/examples/git-merge-ours.sh [new file with mode: 0755]
contrib/examples/git-reset.sh [new file with mode: 0755]
contrib/examples/git-resolve.sh [new file with mode: 0755]
contrib/examples/git-svnimport.perl [new file with mode: 0755]
contrib/examples/git-svnimport.txt [new file with mode: 0644]
contrib/examples/git-tag.sh [new file with mode: 0755]
contrib/examples/git-verify-tag.sh [new file with mode: 0755]
contrib/fast-import/git-import.perl [new file with mode: 0755]
contrib/fast-import/git-import.sh [new file with mode: 0755]
contrib/fast-import/git-p4 [new file with mode: 0755]
contrib/fast-import/git-p4.bat [new file with mode: 0644]
contrib/fast-import/git-p4.txt [new file with mode: 0644]
contrib/fast-import/import-tars.perl [new file with mode: 0755]
contrib/gitview/gitview [new file with mode: 0755]
contrib/gitview/gitview.txt [new file with mode: 0644]
contrib/hg-to-git/hg-to-git.py [new file with mode: 0755]
contrib/hg-to-git/hg-to-git.txt [new file with mode: 0644]
contrib/hooks/post-receive-email [new file with mode: 0644]
contrib/hooks/setgitperms.perl [new file with mode: 0644]
contrib/hooks/update-paranoid [new file with mode: 0644]
contrib/p4import/README [new file with mode: 0644]
contrib/p4import/git-p4import.py [new file with mode: 0644]
contrib/p4import/git-p4import.txt [new file with mode: 0644]
contrib/patches/docbook-xsl-manpages-charmap.patch [new file with mode: 0644]
contrib/remotes2config.sh [new file with mode: 0755]
contrib/stats/git-common-hash [new file with mode: 0755]
contrib/stats/mailmap.pl [new file with mode: 0755]
contrib/stats/packinfo.pl [new file with mode: 0755]
contrib/vim/README [new file with mode: 0644]
contrib/vim/syntax/gitcommit.vim [new file with mode: 0644]
contrib/workdir/git-new-workdir [new file with mode: 0755]
convert.c [new file with mode: 0644]
copy.c [new file with mode: 0644]
csum-file.c [new file with mode: 0644]
csum-file.h [new file with mode: 0644]
ctype.c [new file with mode: 0644]
daemon.c [new file with mode: 0644]
date.c [new file with mode: 0644]
decorate.c [new file with mode: 0644]
decorate.h [new file with mode: 0644]
delta.h [new file with mode: 0644]
diff-delta.c [new file with mode: 0644]
diff-lib.c [new file with mode: 0644]
diff.c [new file with mode: 0644]
diff.h [new file with mode: 0644]
diffcore-break.c [new file with mode: 0644]
diffcore-delta.c [new file with mode: 0644]
diffcore-order.c [new file with mode: 0644]
diffcore-pickaxe.c [new file with mode: 0644]
diffcore-rename.c [new file with mode: 0644]
diffcore.h [new file with mode: 0644]
dir.c [new file with mode: 0644]
dir.h [new file with mode: 0644]
dump-cache-tree.c [new file with mode: 0644]
entry.c [new file with mode: 0644]
environment.c [new file with mode: 0644]
exec_cmd.c [new file with mode: 0644]
exec_cmd.h [new file with mode: 0644]
fast-import.c [new file with mode: 0644]
fetch-pack.h [new file with mode: 0644]
fixup-builtins [new file with mode: 0755]
generate-cmdlist.sh [new file with mode: 0755]
git-add--interactive.perl [new file with mode: 0755]
git-am.sh [new file with mode: 0755]
git-archimport.perl [new file with mode: 0755]
git-bisect.sh [new file with mode: 0755]
git-checkout.sh [new file with mode: 0755]
git-clone.sh [new file with mode: 0755]
git-compat-util.h [new file with mode: 0644]
git-cvsexportcommit.perl [new file with mode: 0755]
git-cvsimport.perl [new file with mode: 0755]
git-cvsserver.perl [new file with mode: 0755]
git-filter-branch.sh [new file with mode: 0755]
git-gui.sh [deleted file]
git-gui/.gitignore [new file with mode: 0644]
git-gui/GIT-VERSION-GEN [new file with mode: 0755]
git-gui/Makefile [new file with mode: 0644]
git-gui/git-gui.sh [new file with mode: 0755]
git-gui/lib/about.tcl [new file with mode: 0644]
git-gui/lib/blame.tcl [new file with mode: 0644]
git-gui/lib/branch.tcl [new file with mode: 0644]
git-gui/lib/branch_checkout.tcl [new file with mode: 0644]
git-gui/lib/branch_create.tcl [new file with mode: 0644]
git-gui/lib/branch_delete.tcl [new file with mode: 0644]
git-gui/lib/branch_rename.tcl [new file with mode: 0644]
git-gui/lib/browser.tcl [new file with mode: 0644]
git-gui/lib/checkout_op.tcl [new file with mode: 0644]
git-gui/lib/choose_font.tcl [new file with mode: 0644]
git-gui/lib/choose_repository.tcl [new file with mode: 0644]
git-gui/lib/choose_rev.tcl [new file with mode: 0644]
git-gui/lib/class.tcl [new file with mode: 0644]
git-gui/lib/commit.tcl [new file with mode: 0644]
git-gui/lib/console.tcl [new file with mode: 0644]
git-gui/lib/database.tcl [new file with mode: 0644]
git-gui/lib/date.tcl [new file with mode: 0644]
git-gui/lib/diff.tcl [new file with mode: 0644]
git-gui/lib/encoding.tcl [new file with mode: 0644]
git-gui/lib/error.tcl [new file with mode: 0644]
git-gui/lib/git-gui.ico [new file with mode: 0644]
git-gui/lib/index.tcl [new file with mode: 0644]
git-gui/lib/logo.tcl [new file with mode: 0644]
git-gui/lib/merge.tcl [new file with mode: 0644]
git-gui/lib/option.tcl [new file with mode: 0644]
git-gui/lib/remote.tcl [new file with mode: 0644]
git-gui/lib/remote_branch_delete.tcl [new file with mode: 0644]
git-gui/lib/shortcut.tcl [new file with mode: 0644]
git-gui/lib/status_bar.tcl [new file with mode: 0644]
git-gui/lib/transport.tcl [new file with mode: 0644]
git-gui/lib/win32.tcl [new file with mode: 0644]
git-gui/lib/win32_shortcut.js [new file with mode: 0644]
git-gui/macosx/AppMain.tcl [new file with mode: 0644]
git-gui/macosx/Info.plist [new file with mode: 0644]
git-gui/macosx/git-gui.icns [new file with mode: 0644]
git-gui/po/.gitignore [new file with mode: 0644]
git-gui/po/README [new file with mode: 0644]
git-gui/po/de.po [new file with mode: 0644]
git-gui/po/git-gui.pot [new file with mode: 0644]
git-gui/po/glossary/Makefile [new file with mode: 0644]
git-gui/po/glossary/de.po [new file with mode: 0644]
git-gui/po/glossary/git-gui-glossary.pot [new file with mode: 0644]
git-gui/po/glossary/git-gui-glossary.txt [new file with mode: 0644]
git-gui/po/glossary/it.po [new file with mode: 0644]
git-gui/po/glossary/txt-to-pot.sh [new file with mode: 0755]
git-gui/po/glossary/zh_cn.po [new file with mode: 0644]
git-gui/po/hu.po [new file with mode: 0644]
git-gui/po/it.po [new file with mode: 0644]
git-gui/po/ja.po [new file with mode: 0644]
git-gui/po/po2msg.sh [new file with mode: 0644]
git-gui/po/ru.po [new file with mode: 0644]
git-gui/po/zh_cn.po [new file with mode: 0644]
git-gui/windows/git-gui.sh [new file with mode: 0644]
git-instaweb.sh [new file with mode: 0755]
git-lost-found.sh [new file with mode: 0755]
git-merge-octopus.sh [new file with mode: 0755]
git-merge-one-file.sh [new file with mode: 0755]
git-merge-resolve.sh [new file with mode: 0755]
git-merge-stupid.sh [new file with mode: 0755]
git-merge.sh [new file with mode: 0755]
git-mergetool.sh [new file with mode: 0755]
git-parse-remote.sh [new file with mode: 0755]
git-pull.sh [new file with mode: 0755]
git-quiltimport.sh [new file with mode: 0755]
git-rebase--interactive.sh [new file with mode: 0755]
git-rebase.sh [new file with mode: 0755]
git-relink.perl [new file with mode: 0755]
git-remote.perl [new file with mode: 0755]
git-repack.sh [new file with mode: 0755]
git-request-pull.sh [new file with mode: 0755]
git-send-email.perl [new file with mode: 0755]
git-sh-setup.sh [new file with mode: 0755]
git-stash.sh [new file with mode: 0755]
git-submodule.sh [new file with mode: 0755]
git-svn.perl [new file with mode: 0755]
git.c [new file with mode: 0644]
git.spec.in [new file with mode: 0644]
gitk-git/Makefile [new file with mode: 0644]
gitk-git/gitk [new file with mode: 0644]
gitweb/INSTALL [new file with mode: 0644]
gitweb/README [new file with mode: 0644]
gitweb/git-favicon.png [new file with mode: 0644]
gitweb/git-logo.png [new file with mode: 0644]
gitweb/gitweb.css [new file with mode: 0644]
gitweb/gitweb.perl [new file with mode: 0755]
gitweb/test/Märchen [new file with mode: 0644]
gitweb/test/file with spaces [new file with mode: 0644]
gitweb/test/file+plus+sign [new file with mode: 0644]
grep.c [new file with mode: 0644]
grep.h [new file with mode: 0644]
hash-object.c [new file with mode: 0644]
hash.c [new file with mode: 0644]
hash.h [new file with mode: 0644]
help.c [new file with mode: 0644]
http-push.c [new file with mode: 0644]
http-walker.c [new file with mode: 0644]
http.c [new file with mode: 0644]
http.h [new file with mode: 0644]
ident.c [new file with mode: 0644]
imap-send.c [new file with mode: 0644]
index-pack.c [new file with mode: 0644]
interpolate.c [new file with mode: 0644]
interpolate.h [new file with mode: 0644]
lib/about.tcl [deleted file]
lib/blame.tcl [deleted file]
lib/branch.tcl [deleted file]
lib/branch_checkout.tcl [deleted file]
lib/branch_create.tcl [deleted file]
lib/branch_delete.tcl [deleted file]
lib/branch_rename.tcl [deleted file]
lib/browser.tcl [deleted file]
lib/checkout_op.tcl [deleted file]
lib/choose_font.tcl [deleted file]
lib/choose_repository.tcl [deleted file]
lib/choose_rev.tcl [deleted file]
lib/class.tcl [deleted file]
lib/commit.tcl [deleted file]
lib/console.tcl [deleted file]
lib/database.tcl [deleted file]
lib/date.tcl [deleted file]
lib/diff.tcl [deleted file]
lib/encoding.tcl [deleted file]
lib/error.tcl [deleted file]
lib/git-gui.ico [deleted file]
lib/index.tcl [deleted file]
lib/logo.tcl [deleted file]
lib/merge.tcl [deleted file]
lib/option.tcl [deleted file]
lib/remote.tcl [deleted file]
lib/remote_branch_delete.tcl [deleted file]
lib/shortcut.tcl [deleted file]
lib/status_bar.tcl [deleted file]
lib/transport.tcl [deleted file]
lib/win32.tcl [deleted file]
lib/win32_shortcut.js [deleted file]
list-objects.c [new file with mode: 0644]
list-objects.h [new file with mode: 0644]
lockfile.c [new file with mode: 0644]
log-tree.c [new file with mode: 0644]
log-tree.h [new file with mode: 0644]
macosx/AppMain.tcl [deleted file]
macosx/Info.plist [deleted file]
macosx/git-gui.icns [deleted file]
mailmap.c [new file with mode: 0644]
mailmap.h [new file with mode: 0644]
match-trees.c [new file with mode: 0644]
merge-file.c [new file with mode: 0644]
merge-index.c [new file with mode: 0644]
merge-recursive.c [new file with mode: 0644]
merge-tree.c [new file with mode: 0644]
mktag.c [new file with mode: 0644]
mktree.c [new file with mode: 0644]
mozilla-sha1/sha1.c [new file with mode: 0644]
mozilla-sha1/sha1.h [new file with mode: 0644]
object-refs.c [new file with mode: 0644]
object.c [new file with mode: 0644]
object.h [new file with mode: 0644]
pack-check.c [new file with mode: 0644]
pack-redundant.c [new file with mode: 0644]
pack-write.c [new file with mode: 0644]
pack.h [new file with mode: 0644]
pager.c [new file with mode: 0644]
parse-options.c [new file with mode: 0644]
parse-options.h [new file with mode: 0644]
patch-delta.c [new file with mode: 0644]
patch-id.c [new file with mode: 0644]
patch-ids.c [new file with mode: 0644]
patch-ids.h [new file with mode: 0644]
path-list.c [new file with mode: 0644]
path-list.h [new file with mode: 0644]
path.c [new file with mode: 0644]
perl/.gitignore [new file with mode: 0644]
perl/Git.pm [new file with mode: 0644]
perl/Makefile [new file with mode: 0644]
perl/Makefile.PL [new file with mode: 0644]
perl/private-Error.pm [new file with mode: 0644]
pkt-line.c [new file with mode: 0644]
pkt-line.h [new file with mode: 0644]
po/.gitignore [deleted file]
po/README [deleted file]
po/de.po [deleted file]
po/git-gui.pot [deleted file]
po/glossary/Makefile [deleted file]
po/glossary/de.po [deleted file]
po/glossary/git-gui-glossary.pot [deleted file]
po/glossary/git-gui-glossary.txt [deleted file]
po/glossary/it.po [deleted file]
po/glossary/txt-to-pot.sh [deleted file]
po/glossary/zh_cn.po [deleted file]
po/hu.po [deleted file]
po/it.po [deleted file]
po/ja.po [deleted file]
po/po2msg.sh [deleted file]
po/ru.po [deleted file]
po/zh_cn.po [deleted file]
ppc/sha1.c [new file with mode: 0644]
ppc/sha1.h [new file with mode: 0644]
ppc/sha1ppc.S [new file with mode: 0644]
pretty.c [new file with mode: 0644]
progress.c [new file with mode: 0644]
progress.h [new file with mode: 0644]
quote.c [new file with mode: 0644]
quote.h [new file with mode: 0644]
reachable.c [new file with mode: 0644]
reachable.h [new file with mode: 0644]
read-cache.c [new file with mode: 0644]
receive-pack.c [new file with mode: 0644]
reflog-walk.c [new file with mode: 0644]
reflog-walk.h [new file with mode: 0644]
refs.c [new file with mode: 0644]
refs.h [new file with mode: 0644]
remote.c [new file with mode: 0644]
remote.h [new file with mode: 0644]
revision.c [new file with mode: 0644]
revision.h [new file with mode: 0644]
run-command.c [new file with mode: 0644]
run-command.h [new file with mode: 0644]
send-pack.h [new file with mode: 0644]
server-info.c [new file with mode: 0644]
setup.c [new file with mode: 0644]
sha1_file.c [new file with mode: 0644]
sha1_name.c [new file with mode: 0644]
shallow.c [new file with mode: 0644]
shell.c [new file with mode: 0644]
show-index.c [new file with mode: 0644]
sideband.c [new file with mode: 0644]
sideband.h [new file with mode: 0644]
strbuf.c [new file with mode: 0644]
strbuf.h [new file with mode: 0644]
symlinks.c [new file with mode: 0644]
t/.gitignore [new file with mode: 0644]
t/Makefile [new file with mode: 0644]
t/README [new file with mode: 0644]
t/annotate-tests.sh [new file with mode: 0644]
t/diff-lib.sh [new file with mode: 0644]
t/lib-git-svn.sh [new file with mode: 0644]
t/lib-read-tree-m-3way.sh [new file with mode: 0644]
t/t0000-basic.sh [new file with mode: 0755]
t/t0001-init.sh [new file with mode: 0755]
t/t0010-racy-git.sh [new file with mode: 0755]
t/t0020-crlf.sh [new file with mode: 0755]
t/t0021-conversion.sh [new file with mode: 0755]
t/t0022-crlf-rename.sh [new file with mode: 0755]
t/t0023-crlf-am.sh [new file with mode: 0755]
t/t0030-stripspace.sh [new file with mode: 0755]
t/t0040-parse-options.sh [new file with mode: 0755]
t/t1000-read-tree-m-3way.sh [new file with mode: 0755]
t/t1001-read-tree-m-2way.sh [new file with mode: 0755]
t/t1002-read-tree-m-u-2way.sh [new file with mode: 0755]
t/t1003-read-tree-prefix.sh [new file with mode: 0755]
t/t1004-read-tree-m-u-wf.sh [new file with mode: 0755]
t/t1020-subdirectory.sh [new file with mode: 0755]
t/t1100-commit-tree-options.sh [new file with mode: 0755]
t/t1200-tutorial.sh [new file with mode: 0755]
t/t1300-repo-config.sh [new file with mode: 0755]
t/t1301-shared-repo.sh [new file with mode: 0755]
t/t1302-repo-version.sh [new file with mode: 0755]
t/t1400-update-ref.sh [new file with mode: 0755]
t/t1410-reflog.sh [new file with mode: 0755]
t/t1420-lost-found.sh [new file with mode: 0755]
t/t1500-rev-parse.sh [new file with mode: 0755]
t/t1501-worktree.sh [new file with mode: 0755]
t/t2000-checkout-cache-clash.sh [new file with mode: 0755]
t/t2001-checkout-cache-clash.sh [new file with mode: 0755]
t/t2002-checkout-cache-u.sh [new file with mode: 0755]
t/t2003-checkout-cache-mkdir.sh [new file with mode: 0755]
t/t2004-checkout-cache-temp.sh [new file with mode: 0755]
t/t2005-checkout-index-symlinks.sh [new file with mode: 0755]
t/t2007-checkout-symlink.sh [new file with mode: 0755]
t/t2008-checkout-subdir.sh [new file with mode: 0755]
t/t2050-git-dir-relative.sh [new file with mode: 0755]
t/t2100-update-cache-badpath.sh [new file with mode: 0755]
t/t2101-update-index-reupdate.sh [new file with mode: 0755]
t/t2102-update-index-symlinks.sh [new file with mode: 0755]
t/t2200-add-update.sh [new file with mode: 0755]
t/t3000-ls-files-others.sh [new file with mode: 0755]
t/t3001-ls-files-others-exclude.sh [new file with mode: 0755]
t/t3002-ls-files-dashpath.sh [new file with mode: 0755]
t/t3010-ls-files-killed-modified.sh [new file with mode: 0755]
t/t3020-ls-files-error-unmatch.sh [new file with mode: 0755]
t/t3030-merge-recursive.sh [new file with mode: 0755]
t/t3040-subprojects-basic.sh [new file with mode: 0755]
t/t3050-subprojects-fetch.sh [new file with mode: 0755]
t/t3060-ls-files-with-tree.sh [new file with mode: 0755]
t/t3100-ls-tree-restrict.sh [new file with mode: 0755]
t/t3101-ls-tree-dirname.sh [new file with mode: 0755]
t/t3200-branch.sh [new file with mode: 0755]
t/t3201-branch-contains.sh [new file with mode: 0755]
t/t3210-pack-refs.sh [new file with mode: 0755]
t/t3300-funny-names.sh [new file with mode: 0755]
t/t3400-rebase.sh [new file with mode: 0755]
t/t3401-rebase-partial.sh [new file with mode: 0755]
t/t3402-rebase-merge.sh [new file with mode: 0755]
t/t3403-rebase-skip.sh [new file with mode: 0755]
t/t3404-rebase-interactive.sh [new file with mode: 0755]
t/t3405-rebase-malformed.sh [new file with mode: 0755]
t/t3406-rebase-message.sh [new file with mode: 0755]
t/t3500-cherry.sh [new file with mode: 0755]
t/t3501-revert-cherry-pick.sh [new file with mode: 0755]
t/t3502-cherry-pick-merge.sh [new file with mode: 0755]
t/t3600-rm.sh [new file with mode: 0755]
t/t3700-add.sh [new file with mode: 0755]
t/t3800-mktag.sh [new file with mode: 0755]
t/t3900-i18n-commit.sh [new file with mode: 0755]
t/t3900/1-UTF-8.txt [new file with mode: 0644]
t/t3900/2-UTF-8.txt [new file with mode: 0644]
t/t3900/EUCJP.txt [new file with mode: 0644]
t/t3900/ISO-2022-JP.txt [new file with mode: 0644]
t/t3900/ISO-8859-1.txt [new file with mode: 0644]
t/t3901-8859-1.txt [new file with mode: 0755]
t/t3901-i18n-patch.sh [new file with mode: 0755]
t/t3901-utf8.txt [new file with mode: 0755]
t/t3902-quoted.sh [new file with mode: 0755]
t/t3903-stash.sh [new file with mode: 0755]
t/t4000-diff-format.sh [new file with mode: 0755]
t/t4001-diff-rename.sh [new file with mode: 0755]
t/t4002-diff-basic.sh [new file with mode: 0755]
t/t4003-diff-rename-1.sh [new file with mode: 0755]
t/t4004-diff-rename-symlink.sh [new file with mode: 0755]
t/t4005-diff-rename-2.sh [new file with mode: 0755]
t/t4006-diff-mode.sh [new file with mode: 0755]
t/t4007-rename-3.sh [new file with mode: 0755]
t/t4008-diff-break-rewrite.sh [new file with mode: 0755]
t/t4009-diff-rename-4.sh [new file with mode: 0755]
t/t4010-diff-pathspec.sh [new file with mode: 0755]
t/t4011-diff-symlink.sh [new file with mode: 0755]
t/t4012-diff-binary.sh [new file with mode: 0755]
t/t4013-diff-various.sh [new file with mode: 0755]
t/t4013/diff.config_format.subjectprefix_DIFFERENT_PREFIX [new file with mode: 0644]
t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_master [new file with mode: 0644]
t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_side [new file with mode: 0644]
t/t4013/diff.diff-tree_--cc_--patch-with-stat_master [new file with mode: 0644]
t/t4013/diff.diff-tree_--cc_--stat_--summary_master [new file with mode: 0644]
t/t4013/diff.diff-tree_--cc_--stat_--summary_side [new file with mode: 0644]
t/t4013/diff.diff-tree_--cc_--stat_master [new file with mode: 0644]
t/t4013/diff.diff-tree_--cc_master [new file with mode: 0644]
t/t4013/diff.diff-tree_--patch-with-raw_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--patch-with-stat_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-raw_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-stat_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-raw_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-stat_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty=oneline_--root_-p_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty=oneline_--root_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty=oneline_-p_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty=oneline_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--patch-with-raw_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--patch-with-stat_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--patch-with-stat_side [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--root_--patch-with-raw_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--root_--patch-with-stat_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--root_--stat_--summary_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--root_--stat_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--root_--summary_-r_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--root_--summary_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--root_-p_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--root_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--stat_--summary_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--stat_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_--summary_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_-p_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_-p_side [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--pretty_side [new file with mode: 0644]
t/t4013/diff.diff-tree_--root_--abbrev_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--root_--patch-with-raw_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--root_--patch-with-stat_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--root_-p_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--root_-r_--abbrev=4_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--root_-r_--abbrev_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--root_-r_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_--root_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_-c_--abbrev_master [new file with mode: 0644]
t/t4013/diff.diff-tree_-c_--stat_--summary_master [new file with mode: 0644]
t/t4013/diff.diff-tree_-c_--stat_--summary_side [new file with mode: 0644]
t/t4013/diff.diff-tree_-c_--stat_master [new file with mode: 0644]
t/t4013/diff.diff-tree_-c_master [new file with mode: 0644]
t/t4013/diff.diff-tree_-p_-m_master [new file with mode: 0644]
t/t4013/diff.diff-tree_-p_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_-p_master [new file with mode: 0644]
t/t4013/diff.diff-tree_-r_--abbrev=4_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_-r_--abbrev_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_-r_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_initial [new file with mode: 0644]
t/t4013/diff.diff-tree_master [new file with mode: 0644]
t/t4013/diff.diff_--abbrev_initial..side [new file with mode: 0644]
t/t4013/diff.diff_--name-status_dir2_dir [new file with mode: 0644]
t/t4013/diff.diff_--patch-with-raw_-r_initial..side [new file with mode: 0644]
t/t4013/diff.diff_--patch-with-raw_initial..side [new file with mode: 0644]
t/t4013/diff.diff_--patch-with-stat_-r_initial..side [new file with mode: 0644]
t/t4013/diff.diff_--patch-with-stat_initial..side [new file with mode: 0644]
t/t4013/diff.diff_--stat_initial..side [new file with mode: 0644]
t/t4013/diff.diff_-r_--stat_initial..side [new file with mode: 0644]
t/t4013/diff.diff_-r_initial..side [new file with mode: 0644]
t/t4013/diff.diff_initial..side [new file with mode: 0644]
t/t4013/diff.format-patch_--attach_--stdout_initial..master [new file with mode: 0644]
t/t4013/diff.format-patch_--attach_--stdout_initial..master^ [new file with mode: 0644]
t/t4013/diff.format-patch_--attach_--stdout_initial..side [new file with mode: 0644]
t/t4013/diff.format-patch_--inline_--stdout_--subject-prefix=TESTCASE_initial..master [new file with mode: 0644]
t/t4013/diff.format-patch_--inline_--stdout_initial..master [new file with mode: 0644]
t/t4013/diff.format-patch_--inline_--stdout_initial..master^ [new file with mode: 0644]
t/t4013/diff.format-patch_--inline_--stdout_initial..master^^ [new file with mode: 0644]
t/t4013/diff.format-patch_--inline_--stdout_initial..side [new file with mode: 0644]
t/t4013/diff.format-patch_--stdout_initial..master [new file with mode: 0644]
t/t4013/diff.format-patch_--stdout_initial..master^ [new file with mode: 0644]
t/t4013/diff.format-patch_--stdout_initial..side [new file with mode: 0644]
t/t4013/diff.log_--patch-with-stat_--summary_master_--_dir_ [new file with mode: 0644]
t/t4013/diff.log_--patch-with-stat_master [new file with mode: 0644]
t/t4013/diff.log_--patch-with-stat_master_--_dir_ [new file with mode: 0644]
t/t4013/diff.log_--root_--cc_--patch-with-stat_--summary_master [new file with mode: 0644]
t/t4013/diff.log_--root_--patch-with-stat_--summary_master [new file with mode: 0644]
t/t4013/diff.log_--root_--patch-with-stat_master [new file with mode: 0644]
t/t4013/diff.log_--root_-c_--patch-with-stat_--summary_master [new file with mode: 0644]
t/t4013/diff.log_--root_-p_master [new file with mode: 0644]
t/t4013/diff.log_--root_master [new file with mode: 0644]
t/t4013/diff.log_-SF_-p_master [new file with mode: 0644]
t/t4013/diff.log_-SF_master [new file with mode: 0644]
t/t4013/diff.log_-p_master [new file with mode: 0644]
t/t4013/diff.log_master [new file with mode: 0644]
t/t4013/diff.show_--patch-with-raw_side [new file with mode: 0644]
t/t4013/diff.show_--patch-with-stat_--summary_side [new file with mode: 0644]
t/t4013/diff.show_--patch-with-stat_side [new file with mode: 0644]
t/t4013/diff.show_--root_initial [new file with mode: 0644]
t/t4013/diff.show_--stat_--summary_side [new file with mode: 0644]
t/t4013/diff.show_--stat_side [new file with mode: 0644]
t/t4013/diff.show_initial [new file with mode: 0644]
t/t4013/diff.show_master [new file with mode: 0644]
t/t4013/diff.show_side [new file with mode: 0644]
t/t4013/diff.whatchanged_--patch-with-stat_--summary_master_--_dir_ [new file with mode: 0644]
t/t4013/diff.whatchanged_--patch-with-stat_master [new file with mode: 0644]
t/t4013/diff.whatchanged_--patch-with-stat_master_--_dir_ [new file with mode: 0644]
t/t4013/diff.whatchanged_--root_--cc_--patch-with-stat_--summary_master [new file with mode: 0644]
t/t4013/diff.whatchanged_--root_--patch-with-stat_--summary_master [new file with mode: 0644]
t/t4013/diff.whatchanged_--root_--patch-with-stat_master [new file with mode: 0644]
t/t4013/diff.whatchanged_--root_-c_--patch-with-stat_--summary_master [new file with mode: 0644]
t/t4013/diff.whatchanged_--root_-p_master [new file with mode: 0644]
t/t4013/diff.whatchanged_--root_master [new file with mode: 0644]
t/t4013/diff.whatchanged_-SF_-p_master [new file with mode: 0644]
t/t4013/diff.whatchanged_-SF_master [new file with mode: 0644]
t/t4013/diff.whatchanged_-p_master [new file with mode: 0644]
t/t4013/diff.whatchanged_master [new file with mode: 0644]
t/t4014-format-patch.sh [new file with mode: 0755]
t/t4015-diff-whitespace.sh [new file with mode: 0755]
t/t4016-diff-quote.sh [new file with mode: 0755]
t/t4017-diff-retval.sh [new file with mode: 0755]
t/t4017-quiet.sh [new file with mode: 0755]
t/t4018-diff-funcname.sh [new file with mode: 0755]
t/t4020-diff-external.sh [new file with mode: 0755]
t/t4021-format-patch-numbered.sh [new file with mode: 0755]
t/t4021-format-patch-signer-mime.sh [new file with mode: 0755]
t/t4022-diff-rewrite.sh [new file with mode: 0755]
t/t4023-diff-rename-typechange.sh [new file with mode: 0755]
t/t4100-apply-stat.sh [new file with mode: 0755]
t/t4100/t-apply-1.expect [new file with mode: 0644]
t/t4100/t-apply-1.patch [new file with mode: 0644]
t/t4100/t-apply-2.expect [new file with mode: 0644]
t/t4100/t-apply-2.patch [new file with mode: 0644]
t/t4100/t-apply-3.expect [new file with mode: 0644]
t/t4100/t-apply-3.patch [new file with mode: 0644]
t/t4100/t-apply-4.expect [new file with mode: 0644]
t/t4100/t-apply-4.patch [new file with mode: 0644]
t/t4100/t-apply-5.expect [new file with mode: 0644]
t/t4100/t-apply-5.patch [new file with mode: 0644]
t/t4100/t-apply-6.expect [new file with mode: 0644]
t/t4100/t-apply-6.patch [new file with mode: 0644]
t/t4100/t-apply-7.expect [new file with mode: 0644]
t/t4100/t-apply-7.patch [new file with mode: 0644]
t/t4101-apply-nonl.sh [new file with mode: 0755]
t/t4101/diff.0-1 [new file with mode: 0644]
t/t4101/diff.0-2 [new file with mode: 0644]
t/t4101/diff.0-3 [new file with mode: 0644]
t/t4101/diff.1-0 [new file with mode: 0644]
t/t4101/diff.1-2 [new file with mode: 0644]
t/t4101/diff.1-3 [new file with mode: 0644]
t/t4101/diff.2-0 [new file with mode: 0644]
t/t4101/diff.2-1 [new file with mode: 0644]
t/t4101/diff.2-3 [new file with mode: 0644]
t/t4101/diff.3-0 [new file with mode: 0644]
t/t4101/diff.3-1 [new file with mode: 0644]
t/t4101/diff.3-2 [new file with mode: 0644]
t/t4102-apply-rename.sh [new file with mode: 0755]
t/t4103-apply-binary.sh [new file with mode: 0755]
t/t4104-apply-boundary.sh [new file with mode: 0755]
t/t4109-apply-multifrag.sh [new file with mode: 0755]
t/t4110-apply-scan.sh [new file with mode: 0755]
t/t4112-apply-renames.sh [new file with mode: 0755]
t/t4113-apply-ending.sh [new file with mode: 0755]
t/t4114-apply-typechange.sh [new file with mode: 0755]
t/t4115-apply-symlink.sh [new file with mode: 0755]
t/t4116-apply-reverse.sh [new file with mode: 0755]
t/t4117-apply-reject.sh [new file with mode: 0755]
t/t4118-apply-empty-context.sh [new file with mode: 0755]
t/t4119-apply-config.sh [new file with mode: 0755]
t/t4120-apply-popt.sh [new file with mode: 0755]
t/t4121-apply-diffs.sh [new file with mode: 0755]
t/t4122-apply-symlink-inside.sh [new file with mode: 0755]
t/t4123-apply-shrink.sh [new file with mode: 0755]
t/t4200-rerere.sh [new file with mode: 0755]
t/t4201-shortlog.sh [new file with mode: 0755]
t/t5000-tar-tree.sh [new file with mode: 0755]
t/t5100-mailinfo.sh [new file with mode: 0755]
t/t5100/info0001 [new file with mode: 0644]
t/t5100/info0002 [new file with mode: 0644]
t/t5100/info0003 [new file with mode: 0644]
t/t5100/info0004 [new file with mode: 0644]
t/t5100/info0005 [new file with mode: 0644]
t/t5100/info0006 [new file with mode: 0644]
t/t5100/info0007 [new file with mode: 0644]
t/t5100/info0008 [new file with mode: 0644]
t/t5100/msg0001 [new file with mode: 0644]
t/t5100/msg0002 [new file with mode: 0644]
t/t5100/msg0003 [new file with mode: 0644]
t/t5100/msg0004 [new file with mode: 0644]
t/t5100/msg0005 [new file with mode: 0644]
t/t5100/msg0006 [new file with mode: 0644]
t/t5100/msg0007 [new file with mode: 0644]
t/t5100/msg0008 [new file with mode: 0644]
t/t5100/patch0001 [new file with mode: 0644]
t/t5100/patch0002 [new file with mode: 0644]
t/t5100/patch0003 [new file with mode: 0644]
t/t5100/patch0004 [new file with mode: 0644]
t/t5100/patch0005 [new file with mode: 0644]
t/t5100/patch0006 [new file with mode: 0644]
t/t5100/patch0007 [new file with mode: 0644]
t/t5100/patch0008 [new file with mode: 0644]
t/t5100/sample.mbox [new file with mode: 0644]
t/t5300-pack-object.sh [new file with mode: 0755]
t/t5301-sliding-window.sh [new file with mode: 0755]
t/t5302-pack-index.sh [new file with mode: 0755]
t/t5400-send-pack.sh [new file with mode: 0755]
t/t5401-update-hooks.sh [new file with mode: 0755]
t/t5402-post-merge-hook.sh [new file with mode: 0755]
t/t5403-post-checkout-hook.sh [new file with mode: 0755]
t/t5404-tracking-branches.sh [new file with mode: 0755]
t/t5405-send-pack-rewind.sh [new file with mode: 0755]
t/t5406-remote-rejects.sh [new file with mode: 0755]
t/t5500-fetch-pack.sh [new file with mode: 0755]
t/t5502-quickfetch.sh [new file with mode: 0755]
t/t5505-remote.sh [new file with mode: 0755]
t/t5510-fetch.sh [new file with mode: 0755]
t/t5512-ls-remote.sh [new file with mode: 0755]
t/t5515-fetch-merge-logic.sh [new file with mode: 0755]
t/t5515/fetch.br-branches-default [new file with mode: 0644]
t/t5515/fetch.br-branches-default-merge [new file with mode: 0644]
t/t5515/fetch.br-branches-default-merge_branches-default [new file with mode: 0644]
t/t5515/fetch.br-branches-default-octopus [new file with mode: 0644]
t/t5515/fetch.br-branches-default-octopus_branches-default [new file with mode: 0644]
t/t5515/fetch.br-branches-default_branches-default [new file with mode: 0644]
t/t5515/fetch.br-branches-one [new file with mode: 0644]
t/t5515/fetch.br-branches-one-merge [new file with mode: 0644]
t/t5515/fetch.br-branches-one-merge_branches-one [new file with mode: 0644]
t/t5515/fetch.br-branches-one-octopus [new file with mode: 0644]
t/t5515/fetch.br-branches-one-octopus_branches-one [new file with mode: 0644]
t/t5515/fetch.br-branches-one_branches-one [new file with mode: 0644]
t/t5515/fetch.br-config-explicit [new file with mode: 0644]
t/t5515/fetch.br-config-explicit-merge [new file with mode: 0644]
t/t5515/fetch.br-config-explicit-merge_config-explicit [new file with mode: 0644]
t/t5515/fetch.br-config-explicit-octopus [new file with mode: 0644]
t/t5515/fetch.br-config-explicit-octopus_config-explicit [new file with mode: 0644]
t/t5515/fetch.br-config-explicit_config-explicit [new file with mode: 0644]
t/t5515/fetch.br-config-glob [new file with mode: 0644]
t/t5515/fetch.br-config-glob-merge [new file with mode: 0644]
t/t5515/fetch.br-config-glob-merge_config-glob [new file with mode: 0644]
t/t5515/fetch.br-config-glob-octopus [new file with mode: 0644]
t/t5515/fetch.br-config-glob-octopus_config-glob [new file with mode: 0644]
t/t5515/fetch.br-config-glob_config-glob [new file with mode: 0644]
t/t5515/fetch.br-remote-explicit [new file with mode: 0644]
t/t5515/fetch.br-remote-explicit-merge [new file with mode: 0644]
t/t5515/fetch.br-remote-explicit-merge_remote-explicit [new file with mode: 0644]
t/t5515/fetch.br-remote-explicit-octopus [new file with mode: 0644]
t/t5515/fetch.br-remote-explicit-octopus_remote-explicit [new file with mode: 0644]
t/t5515/fetch.br-remote-explicit_remote-explicit [new file with mode: 0644]
t/t5515/fetch.br-remote-glob [new file with mode: 0644]
t/t5515/fetch.br-remote-glob-merge [new file with mode: 0644]
t/t5515/fetch.br-remote-glob-merge_remote-glob [new file with mode: 0644]
t/t5515/fetch.br-remote-glob-octopus [new file with mode: 0644]
t/t5515/fetch.br-remote-glob-octopus_remote-glob [new file with mode: 0644]
t/t5515/fetch.br-remote-glob_remote-glob [new file with mode: 0644]
t/t5515/fetch.br-unconfig [new file with mode: 0644]
t/t5515/fetch.br-unconfig_--tags_.._.git [new file with mode: 0644]
t/t5515/fetch.br-unconfig_.._.git [new file with mode: 0644]
t/t5515/fetch.br-unconfig_.._.git_one [new file with mode: 0644]
t/t5515/fetch.br-unconfig_.._.git_one_tag_tag-one_tag_tag-three-file [new file with mode: 0644]
t/t5515/fetch.br-unconfig_.._.git_one_two [new file with mode: 0644]
t/t5515/fetch.br-unconfig_.._.git_tag_tag-one-tree_tag_tag-three-file [new file with mode: 0644]
t/t5515/fetch.br-unconfig_.._.git_tag_tag-one_tag_tag-three [new file with mode: 0644]
t/t5515/fetch.br-unconfig_branches-default [new file with mode: 0644]
t/t5515/fetch.br-unconfig_branches-one [new file with mode: 0644]
t/t5515/fetch.br-unconfig_config-explicit [new file with mode: 0644]
t/t5515/fetch.br-unconfig_config-glob [new file with mode: 0644]
t/t5515/fetch.br-unconfig_remote-explicit [new file with mode: 0644]
t/t5515/fetch.br-unconfig_remote-glob [new file with mode: 0644]
t/t5515/fetch.master [new file with mode: 0644]
t/t5515/fetch.master_--tags_.._.git [new file with mode: 0644]
t/t5515/fetch.master_.._.git [new file with mode: 0644]
t/t5515/fetch.master_.._.git_one [new file with mode: 0644]
t/t5515/fetch.master_.._.git_one_tag_tag-one_tag_tag-three-file [new file with mode: 0644]
t/t5515/fetch.master_.._.git_one_two [new file with mode: 0644]
t/t5515/fetch.master_.._.git_tag_tag-one-tree_tag_tag-three-file [new file with mode: 0644]
t/t5515/fetch.master_.._.git_tag_tag-one_tag_tag-three [new file with mode: 0644]
t/t5515/fetch.master_branches-default [new file with mode: 0644]
t/t5515/fetch.master_branches-one [new file with mode: 0644]
t/t5515/fetch.master_config-explicit [new file with mode: 0644]
t/t5515/fetch.master_config-glob [new file with mode: 0644]
t/t5515/fetch.master_remote-explicit [new file with mode: 0644]
t/t5515/fetch.master_remote-glob [new file with mode: 0644]
t/t5516-fetch-push.sh [new file with mode: 0755]
t/t5517-push-mirror.sh [new file with mode: 0755]
t/t5520-pull.sh [new file with mode: 0755]
t/t5530-upload-pack-error.sh [new file with mode: 0755]
t/t5600-clone-fail-cleanup.sh [new file with mode: 0755]
t/t5700-clone-reference.sh [new file with mode: 0755]
t/t5701-clone-local.sh [new file with mode: 0755]
t/t5710-info-alternate.sh [new file with mode: 0755]
t/t6000lib.sh [new file with mode: 0755]
t/t6001-rev-list-graft.sh [new file with mode: 0755]
t/t6002-rev-list-bisect.sh [new file with mode: 0755]
t/t6003-rev-list-topo-order.sh [new file with mode: 0755]
t/t6004-rev-list-path-optim.sh [new file with mode: 0755]
t/t6005-rev-list-count.sh [new file with mode: 0755]
t/t6006-rev-list-format.sh [new file with mode: 0755]
t/t6007-rev-list-cherry-pick-file.sh [new file with mode: 0755]
t/t6008-rev-list-submodule.sh [new file with mode: 0755]
t/t6010-merge-base.sh [new file with mode: 0755]
t/t6020-merge-df.sh [new file with mode: 0755]
t/t6021-merge-criss-cross.sh [new file with mode: 0755]
t/t6022-merge-rename.sh [new file with mode: 0755]
t/t6023-merge-file.sh [new file with mode: 0755]
t/t6023-merge-rename-nocruft.sh [new file with mode: 0755]
t/t6024-recursive-merge.sh [new file with mode: 0755]
t/t6025-merge-symlinks.sh [new file with mode: 0755]
t/t6026-merge-attr.sh [new file with mode: 0755]
t/t6027-merge-binary.sh [new file with mode: 0755]
t/t6028-merge-up-to-date.sh [new file with mode: 0755]
t/t6030-bisect-porcelain.sh [new file with mode: 0755]
t/t6101-rev-parse-parents.sh [new file with mode: 0755]
t/t6120-describe.sh [new file with mode: 0755]
t/t6200-fmt-merge-msg.sh [new file with mode: 0755]
t/t6300-for-each-ref.sh [new file with mode: 0755]
t/t7001-mv.sh [new file with mode: 0755]
t/t7002-grep.sh [new file with mode: 0755]
t/t7003-filter-branch.sh [new file with mode: 0755]
t/t7004-tag.sh [new file with mode: 0755]
t/t7004/pubring.gpg [new file with mode: 0644]
t/t7004/random_seed [new file with mode: 0644]
t/t7004/secring.gpg [new file with mode: 0644]
t/t7004/trustdb.gpg [new file with mode: 0644]
t/t7005-editor.sh [new file with mode: 0755]
t/t7101-reset.sh [new file with mode: 0755]
t/t7102-reset.sh [new file with mode: 0755]
t/t7201-co.sh [new file with mode: 0755]
t/t7300-clean.sh [new file with mode: 0755]
t/t7400-submodule-basic.sh [new file with mode: 0755]
t/t7500-commit.sh [new file with mode: 0755]
t/t7500/add-comments [new file with mode: 0755]
t/t7500/add-content [new file with mode: 0755]
t/t7500/add-signed-off [new file with mode: 0755]
t/t7501-commit.sh [new file with mode: 0755]
t/t7502-commit.sh [new file with mode: 0755]
t/t7502-status.sh [new file with mode: 0755]
t/t7600-merge.sh [new file with mode: 0755]
t/t8001-annotate.sh [new file with mode: 0755]
t/t8002-blame.sh [new file with mode: 0755]
t/t8003-blame.sh [new file with mode: 0755]
t/t8004-blame.sh [new file with mode: 0755]
t/t9001-send-email.sh [new file with mode: 0755]
t/t9100-git-svn-basic.sh [new file with mode: 0755]
t/t9101-git-svn-props.sh [new file with mode: 0755]
t/t9102-git-svn-deep-rmdir.sh [new file with mode: 0755]
t/t9104-git-svn-follow-parent.sh [new file with mode: 0755]
t/t9105-git-svn-commit-diff.sh [new file with mode: 0755]
t/t9106-git-svn-commit-diff-clobber.sh [new file with mode: 0755]
t/t9106-git-svn-dcommit-clobber-series.sh [new file with mode: 0755]
t/t9107-git-svn-migrate.sh [new file with mode: 0755]
t/t9108-git-svn-glob.sh [new file with mode: 0755]
t/t9110-git-svn-use-svm-props.sh [new file with mode: 0755]
t/t9110/svm.dump [new file with mode: 0644]
t/t9111-git-svn-use-svnsync-props.sh [new file with mode: 0755]
t/t9111/svnsync.dump [new file with mode: 0644]
t/t9112-git-svn-md5less-file.sh [new file with mode: 0755]
t/t9113-git-svn-dcommit-new-file.sh [new file with mode: 0755]
t/t9114-git-svn-dcommit-merge.sh [new file with mode: 0755]
t/t9115-git-svn-dcommit-funky-renames.sh [new file with mode: 0755]
t/t9115/funky-names.dump [new file with mode: 0644]
t/t9116-git-svn-log.sh [new file with mode: 0755]
t/t9117-git-svn-init-clone.sh [new file with mode: 0755]
t/t9118-git-svn-funky-branch-names.sh [new file with mode: 0755]
t/t9119-git-svn-info.sh [new file with mode: 0755]
t/t9200-git-cvsexportcommit.sh [new file with mode: 0755]
t/t9300-fast-import.sh [new file with mode: 0755]
t/t9301-fast-export.sh [new file with mode: 0755]
t/t9400-git-cvsserver-server.sh [new file with mode: 0755]
t/t9500-gitweb-standalone-no-errors.sh [new file with mode: 0755]
t/t9600-cvsimport.sh [new file with mode: 0755]
t/test-lib.sh [new file with mode: 0644]
t/test4012.png [new file with mode: 0644]
t/test9200a.png [new file with mode: 0644]
t/test9200b.png [new file with mode: 0644]
tag.c [new file with mode: 0644]
tag.h [new file with mode: 0644]
tar.h [new file with mode: 0644]
templates/.gitignore [new file with mode: 0644]
templates/Makefile [new file with mode: 0644]
templates/branches-- [new file with mode: 0644]
templates/hooks--applypatch-msg [new file with mode: 0644]
templates/hooks--commit-msg [new file with mode: 0644]
templates/hooks--post-commit [new file with mode: 0644]
templates/hooks--post-receive [new file with mode: 0644]
templates/hooks--post-update [new file with mode: 0644]
templates/hooks--pre-applypatch [new file with mode: 0644]
templates/hooks--pre-commit [new file with mode: 0644]
templates/hooks--pre-rebase [new file with mode: 0644]
templates/hooks--update [new file with mode: 0644]
templates/info--exclude [new file with mode: 0644]
templates/this--description [new file with mode: 0644]
test-absolute-path.c [new file with mode: 0644]
test-chmtime.c [new file with mode: 0644]
test-date.c [new file with mode: 0644]
test-delta.c [new file with mode: 0644]
test-genrandom.c [new file with mode: 0644]
test-match-trees.c [new file with mode: 0644]
test-parse-options.c [new file with mode: 0644]
test-sha1.c [new file with mode: 0644]
test-sha1.sh [new file with mode: 0755]
trace.c [new file with mode: 0644]
transport.c [new file with mode: 0644]
transport.h [new file with mode: 0644]
tree-diff.c [new file with mode: 0644]
tree-walk.c [new file with mode: 0644]
tree-walk.h [new file with mode: 0644]
tree.c [new file with mode: 0644]
tree.h [new file with mode: 0644]
unpack-file.c [new file with mode: 0644]
unpack-trees.c [new file with mode: 0644]
unpack-trees.h [new file with mode: 0644]
update-server-info.c [new file with mode: 0644]
upload-pack.c [new file with mode: 0644]
usage.c [new file with mode: 0644]
utf8.c [new file with mode: 0644]
utf8.h [new file with mode: 0644]
var.c [new file with mode: 0644]
walker.c [new file with mode: 0644]
walker.h [new file with mode: 0644]
windows/git-gui.sh [deleted file]
write_or_die.c [new file with mode: 0644]
wt-status.c [new file with mode: 0644]
wt-status.h [new file with mode: 0644]
xdiff-interface.c [new file with mode: 0644]
xdiff-interface.h [new file with mode: 0644]
xdiff/xdiff.h [new file with mode: 0644]
xdiff/xdiffi.c [new file with mode: 0644]
xdiff/xdiffi.h [new file with mode: 0644]
xdiff/xemit.c [new file with mode: 0644]
xdiff/xemit.h [new file with mode: 0644]
xdiff/xinclude.h [new file with mode: 0644]
xdiff/xmacros.h [new file with mode: 0644]
xdiff/xmerge.c [new file with mode: 0644]
xdiff/xprepare.c [new file with mode: 0644]
xdiff/xprepare.h [new file with mode: 0644]
xdiff/xtypes.h [new file with mode: 0644]
xdiff/xutils.c [new file with mode: 0644]
xdiff/xutils.h [new file with mode: 0644]

index 6483b21cbfc73601602d628a2c609d3ca84f9e53..bac60ce31ae3b8d74619df7dd93356c49f21d081 100644 (file)
@@ -1,8 +1,176 @@
-.DS_Store
-config.mak
-Git Gui.app*
-git-gui.tcl
-GIT-VERSION-FILE
+GIT-CFLAGS
 GIT-GUI-VARS
-git-gui
-lib/tclIndex
+GIT-VERSION-FILE
+git
+git-add
+git-add--interactive
+git-am
+git-annotate
+git-apply
+git-archimport
+git-archive
+git-bisect
+git-blame
+git-branch
+git-bundle
+git-cat-file
+git-check-attr
+git-check-ref-format
+git-checkout
+git-checkout-index
+git-cherry
+git-cherry-pick
+git-clean
+git-clone
+git-commit
+git-commit-tree
+git-config
+git-count-objects
+git-cvsexportcommit
+git-cvsimport
+git-cvsserver
+git-daemon
+git-diff
+git-diff-files
+git-diff-index
+git-diff-tree
+git-describe
+git-fast-export
+git-fast-import
+git-fetch
+git-fetch--tool
+git-fetch-pack
+git-filter-branch
+git-findtags
+git-fmt-merge-msg
+git-for-each-ref
+git-format-patch
+git-fsck
+git-fsck-objects
+git-gc
+git-get-tar-commit-id
+git-grep
+git-hash-object
+git-http-fetch
+git-http-push
+git-imap-send
+git-index-pack
+git-init
+git-init-db
+git-instaweb
+git-local-fetch
+git-log
+git-lost-found
+git-ls-files
+git-ls-remote
+git-ls-tree
+git-mailinfo
+git-mailsplit
+git-merge
+git-merge-base
+git-merge-index
+git-merge-file
+git-merge-tree
+git-merge-octopus
+git-merge-one-file
+git-merge-ours
+git-merge-recursive
+git-merge-resolve
+git-merge-stupid
+git-merge-subtree
+git-mergetool
+git-mktag
+git-mktree
+git-name-rev
+git-mv
+git-pack-redundant
+git-pack-objects
+git-pack-refs
+git-parse-remote
+git-patch-id
+git-peek-remote
+git-prune
+git-prune-packed
+git-pull
+git-push
+git-quiltimport
+git-read-tree
+git-rebase
+git-rebase--interactive
+git-receive-pack
+git-reflog
+git-relink
+git-remote
+git-repack
+git-repo-config
+git-request-pull
+git-rerere
+git-reset
+git-rev-list
+git-rev-parse
+git-revert
+git-rm
+git-send-email
+git-send-pack
+git-sh-setup
+git-shell
+git-shortlog
+git-show
+git-show-branch
+git-show-index
+git-show-ref
+git-ssh-fetch
+git-ssh-pull
+git-ssh-push
+git-ssh-upload
+git-stash
+git-status
+git-stripspace
+git-submodule
+git-svn
+git-symbolic-ref
+git-tag
+git-tar-tree
+git-unpack-file
+git-unpack-objects
+git-update-index
+git-update-ref
+git-update-server-info
+git-upload-archive
+git-upload-pack
+git-var
+git-verify-pack
+git-verify-tag
+git-whatchanged
+git-write-tree
+git-core-*/?*
+gitk-wish
+gitweb/gitweb.cgi
+test-absolute-path
+test-chmtime
+test-date
+test-delta
+test-dump-cache-tree
+test-genrandom
+test-match-trees
+test-parse-options
+test-sha1
+common-cmds.h
+*.tar.gz
+*.dsc
+*.deb
+git.spec
+*.exe
+*.[aos]
+*.py[co]
+config.mak
+autom4te.cache
+config.cache
+config.log
+config.status
+config.mak.autogen
+config.mak.append
+configure
+tags
+TAGS
+cscope*
diff --git a/.mailmap b/.mailmap
new file mode 100644 (file)
index 0000000..3b2ce57
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,51 @@
+#
+# This list is used by git-shortlog to fix a few botched name translations
+# in the git archive, either because the author's full name was messed up
+# and/or not always written the same way, making contributions from the
+# same person appearing not to be so.
+#
+
+Aneesh Kumar K.V <aneesh.kumar@gmail.com>
+Chris Shoemaker <c.shoemaker@cox.net>
+Dana L. How <danahow@gmail.com>
+Dana L. How <how@deathvalley.cswitch.com>
+Daniel Barkalow <barkalow@iabervon.org>
+David Kågedal <davidk@lysator.liu.se>
+Fredrik Kuivinen <freku045@student.liu.se>
+H. Peter Anvin <hpa@bonde.sc.orionmulti.com>
+H. Peter Anvin <hpa@tazenda.sc.orionmulti.com>
+H. Peter Anvin <hpa@trantor.hos.anvin.org>
+Horst H. von Brand <vonbrand@inf.utfsm.cl>
+Joachim Berdal Haga <cjhaga@fys.uio.no>
+Jon Loeliger <jdl@freescale.com>
+Jon Seymour <jon@blackcubes.dyndns.org>
+Junio C Hamano <junio@twinsun.com>
+Karl Hasselström <kha@treskal.com>
+Kent Engstrom <kent@lysator.liu.se>
+Lars Doelle <lars.doelle@on-line ! de>
+Lars Doelle <lars.doelle@on-line.de>
+Lukas Sandström <lukass@etek.chalmers.se>
+Martin Langhoff <martin@catalyst.net.nz>
+Michael Coleman <tutufan@gmail.com>
+Michele Ballabio <barra_cuda@katamail.com>
+Nanako Shiraishi <nanako3@bluebottle.com>
+Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
+Ramsay Allan Jones <ramsay@ramsay1.demon.co.uk>
+René Scharfe <rene.scharfe@lsrfire.ath.cx>
+Robert Fitzsimons <robfitz@273k.net>
+Sam Vilain <sam@vilain.net>
+Santi Béjar <sbejar@gmail.com>
+Sean Estabrooks <seanlkml@sympatico.ca>
+Shawn O. Pearce <spearce@spearce.org>
+Steven Grimm <koreth@midwinter.com>
+Theodore Ts'o <tytso@mit.edu>
+Tony Luck <tony.luck@intel.com>
+Uwe Kleine-König <Uwe_Zeisberger@digi.com>
+Uwe Kleine-König <ukleinek@informatik.uni-freiburg.de>
+Uwe Kleine-König <uzeisberger@io.fsforth.de>
+Uwe Kleine-König <zeisberg@informatik.uni-freiburg.de>
+Ville Skyttä <scop@xemacs.org>
+William Pursell <bill.pursell@gmail.com>
+YOSHIFUJI Hideaki <yoshfuji@linux-ipv6.org>
+anonymous <linux@horizon.com>
+anonymous <linux@horizon.net>
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..6ff87c4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,361 @@
+
+ Note that the only valid version of the GPL as far as this project
+ is concerned is _this_ particular version of the license (ie v2, not
+ v2.2 or v3.x or whatever), unless explicitly otherwise stated.
+
+ HOWEVER, in order to allow a migration to GPLv3 if that seems like
+ a good idea, I also ask that people involved with the project make
+ their preferences known. In particular, if you trust me to make that
+ decision, you might note so in your copyright message, ie something
+ like
+
+       This file is licensed under the GPL v2, or a later version
+       at the discretion of Linus.
+
+  might avoid issues. But we can also just decide to synchronize and
+  contact all copyright holders on record if/when the occasion arises.
+
+                       Linus Torvalds
+
+----------------------------------------
+
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/Documentation/.gitignore b/Documentation/.gitignore
new file mode 100644 (file)
index 0000000..a37b215
--- /dev/null
@@ -0,0 +1,7 @@
+*.xml
+*.html
+*.[1-8]
+*.made
+howto-index.txt
+doc.dep
+cmds-*.txt
diff --git a/Documentation/CodingGuidelines b/Documentation/CodingGuidelines
new file mode 100644 (file)
index 0000000..3b042db
--- /dev/null
@@ -0,0 +1,112 @@
+Like other projects, we also have some guidelines to keep to the
+code.  For git in general, three rough rules are:
+
+ - Most importantly, we never say "It's in POSIX; we'll happily
+   ignore your needs should your system not conform to it."
+   We live in the real world.
+
+ - However, we often say "Let's stay away from that construct,
+   it's not even in POSIX".
+
+ - In spite of the above two rules, we sometimes say "Although
+   this is not in POSIX, it (is so convenient | makes the code
+   much more readable | has other good characteristics) and
+   practically all the platforms we care about support it, so
+   let's use it".
+
+   Again, we live in the real world, and it is sometimes a
+   judgement call, the decision based more on real world
+   constraints people face than what the paper standard says.
+
+
+As for more concrete guidelines, just imitate the existing code
+(this is a good guideline, no matter which project you are
+contributing to).  But if you must have a list of rules,
+here they are.
+
+For shell scripts specifically (not exhaustive):
+
+ - We prefer $( ... ) for command substitution; unlike ``, it
+   properly nests.  It should have been the way Bourne spelled
+   it from day one, but unfortunately isn't.
+
+ - We use ${parameter-word} and its [-=?+] siblings, and their
+   colon'ed "unset or null" form.
+
+ - We use ${parameter#word} and its [#%] siblings, and their
+   doubled "longest matching" form.
+
+ - We use Arithmetic Expansion $(( ... )).
+
+ - No "Substring Expansion" ${parameter:offset:length}.
+
+ - No shell arrays.
+
+ - No strlen ${#parameter}.
+
+ - No regexp ${parameter/pattern/string}.
+
+ - We do not use Process Substitution <(list) or >(list).
+
+ - We prefer "test" over "[ ... ]".
+
+ - We do not write the noiseword "function" in front of shell
+   functions.
+
+For C programs:
+
+ - We use tabs to indent, and interpret tabs as taking up to
+   8 spaces.
+
+ - We try to keep to at most 80 characters per line.
+
+ - When declaring pointers, the star sides with the variable
+   name, i.e. "char *string", not "char* string" or
+   "char * string".  This makes it easier to understand code
+   like "char *string, c;".
+
+ - We avoid using braces unnecessarily.  I.e.
+
+       if (bla) {
+               x = 1;
+       }
+
+   is frowned upon.  A gray area is when the statement extends
+   over a few lines, and/or you have a lengthy comment atop of
+   it.  Also, like in the Linux kernel, if there is a long list
+   of "else if" statements, it can make sense to add braces to
+   single line blocks.
+
+ - Try to make your code understandable.  You may put comments
+   in, but comments invariably tend to stale out when the code
+   they were describing changes.  Often splitting a function
+   into two makes the intention of the code much clearer.
+
+ - Double negation is often harder to understand than no negation
+   at all.
+
+ - Some clever tricks, like using the !! operator with arithmetic
+   constructs, can be extremely confusing to others.  Avoid them,
+   unless there is a compelling reason to use them.
+
+ - Use the API.  No, really.  We have a strbuf (variable length
+   string), several arrays with the ALLOC_GROW() macro, a
+   path_list for sorted string lists, a hash map (mapping struct
+   objects) named "struct decorate", amongst other things.
+
+ - When you come up with an API, document it.
+
+ - The first #include in C files, except in platform specific
+   compat/ implementations, should be git-compat-util.h or another
+   header file that includes it, such as cache.h or builtin.h.
+
+ - If you are planning a new command, consider writing it in shell
+   or perl first, so that changes in semantics can be easily
+   changed and discussed.  Many git commands started out like
+   that, and a few are still scripts.
+
+ - Avoid introducing a new dependency into git. This means you
+   usually should stay away from scripting languages not already
+   used in the git core command set (unless your command is clearly
+   separate from it, such as an importer to convert random-scm-X
+   repositories to git).
diff --git a/Documentation/Makefile b/Documentation/Makefile
new file mode 100644 (file)
index 0000000..1b58024
--- /dev/null
@@ -0,0 +1,192 @@
+MAN1_TXT= \
+       $(filter-out $(addsuffix .txt, $(ARTICLES) $(SP_ARTICLES)), \
+               $(wildcard git-*.txt)) \
+       gitk.txt
+MAN5_TXT=gitattributes.txt gitignore.txt gitmodules.txt
+MAN7_TXT=git.txt
+
+DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT) $(MAN5_TXT) $(MAN7_TXT))
+
+ARTICLES = tutorial
+ARTICLES += tutorial-2
+ARTICLES += core-tutorial
+ARTICLES += cvs-migration
+ARTICLES += diffcore
+ARTICLES += howto-index
+ARTICLES += repository-layout
+ARTICLES += hooks
+ARTICLES += everyday
+ARTICLES += git-tools
+ARTICLES += glossary
+# with their own formatting rules.
+SP_ARTICLES = howto/revert-branch-rebase user-manual
+
+DOC_HTML += $(patsubst %,%.html,$(ARTICLES) $(SP_ARTICLES))
+
+DOC_MAN1=$(patsubst %.txt,%.1,$(MAN1_TXT))
+DOC_MAN5=$(patsubst %.txt,%.5,$(MAN5_TXT))
+DOC_MAN7=$(patsubst %.txt,%.7,$(MAN7_TXT))
+
+prefix?=$(HOME)
+bindir?=$(prefix)/bin
+mandir?=$(prefix)/share/man
+man1dir=$(mandir)/man1
+man5dir=$(mandir)/man5
+man7dir=$(mandir)/man7
+# DESTDIR=
+
+ASCIIDOC=asciidoc
+ASCIIDOC_EXTRA =
+INSTALL?=install
+RM ?= rm -f
+DOC_REF = origin/man
+
+infodir?=$(prefix)/share/info
+MAKEINFO=makeinfo
+INSTALL_INFO=install-info
+DOCBOOK2X_TEXI=docbook2x-texi
+ifndef PERL_PATH
+       PERL_PATH = /usr/bin/perl
+endif
+
+-include ../config.mak.autogen
+-include ../config.mak
+
+ifdef ASCIIDOC8
+ASCIIDOC_EXTRA += -a asciidoc7compatible
+endif
+ifdef DOCBOOK_XSL_172
+ASCIIDOC_EXTRA += -a docbook-xsl-172
+endif
+
+#
+# Please note that there is a minor bug in asciidoc.
+# The version after 6.0.3 _will_ include the patch found here:
+#   http://marc.theaimsgroup.com/?l=git&m=111558757202243&w=2
+#
+# Until that version is released you may have to apply the patch
+# yourself - yes, all 6 characters of it!
+#
+
+all: html man
+
+html: $(DOC_HTML)
+
+$(DOC_HTML) $(DOC_MAN1) $(DOC_MAN5) $(DOC_MAN7): asciidoc.conf
+
+man: man1 man5 man7
+man1: $(DOC_MAN1)
+man5: $(DOC_MAN5)
+man7: $(DOC_MAN7)
+
+info: git.info
+
+install: man
+       $(INSTALL) -d -m 755 $(DESTDIR)$(man1dir)
+       $(INSTALL) -d -m 755 $(DESTDIR)$(man5dir)
+       $(INSTALL) -d -m 755 $(DESTDIR)$(man7dir)
+       $(INSTALL) -m 644 $(DOC_MAN1) $(DESTDIR)$(man1dir)
+       $(INSTALL) -m 644 $(DOC_MAN5) $(DESTDIR)$(man5dir)
+       $(INSTALL) -m 644 $(DOC_MAN7) $(DESTDIR)$(man7dir)
+
+install-info: info
+       $(INSTALL) -d -m 755 $(DESTDIR)$(infodir)
+       $(INSTALL) -m 644 git.info $(DESTDIR)$(infodir)
+       if test -r $(DESTDIR)$(infodir)/dir; then \
+         $(INSTALL_INFO) --info-dir=$(DESTDIR)$(infodir) git.info ;\
+       else \
+         echo "No directory found in $(DESTDIR)$(infodir)" >&2 ; \
+       fi
+
+../GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
+       $(MAKE) -C ../ GIT-VERSION-FILE
+
+-include ../GIT-VERSION-FILE
+
+#
+# Determine "include::" file references in asciidoc files.
+#
+doc.dep : $(wildcard *.txt) build-docdep.perl
+       $(RM) $@+ $@
+       $(PERL_PATH) ./build-docdep.perl >$@+
+       mv $@+ $@
+
+-include doc.dep
+
+cmds_txt = cmds-ancillaryinterrogators.txt \
+       cmds-ancillarymanipulators.txt \
+       cmds-mainporcelain.txt \
+       cmds-plumbinginterrogators.txt \
+       cmds-plumbingmanipulators.txt \
+       cmds-synchingrepositories.txt \
+       cmds-synchelpers.txt \
+       cmds-purehelpers.txt \
+       cmds-foreignscminterface.txt
+
+$(cmds_txt): cmd-list.made
+
+cmd-list.made: cmd-list.perl ../command-list.txt $(MAN1_TXT)
+       $(RM) $@
+       $(PERL_PATH) ./cmd-list.perl ../command-list.txt
+       date >$@
+
+git.7 git.html: git.txt
+
+clean:
+       $(RM) *.xml *.xml+ *.html *.html+ *.1 *.5 *.7 *.texi *.texi+ howto-index.txt howto/*.html doc.dep
+       $(RM) $(cmds_txt) *.made
+
+%.html : %.txt
+       $(RM) $@+ $@
+       $(ASCIIDOC) -b xhtml11 -d manpage -f asciidoc.conf \
+               $(ASCIIDOC_EXTRA) -agit_version=$(GIT_VERSION) -o $@+ $<
+       mv $@+ $@
+
+%.1 %.5 %.7 : %.xml
+       $(RM) $@
+       xmlto -m callouts.xsl man $<
+
+%.xml : %.txt
+       $(RM) $@+ $@
+       $(ASCIIDOC) -b docbook -d manpage -f asciidoc.conf \
+               $(ASCIIDOC_EXTRA) -agit_version=$(GIT_VERSION) -o $@+ $<
+       mv $@+ $@
+
+user-manual.xml: user-manual.txt user-manual.conf
+       $(ASCIIDOC) -b docbook -d book $<
+
+XSLT = docbook.xsl
+XSLTOPTS = --xinclude --stringparam html.stylesheet docbook-xsl.css
+
+user-manual.html: user-manual.xml
+       xsltproc $(XSLTOPTS) -o $@ $(XSLT) $<
+
+git.info: user-manual.xml
+       $(RM) $@ $*.texi $*.texi+
+       $(DOCBOOK2X_TEXI) user-manual.xml --to-stdout >$*.texi+
+       $(PERL_PATH) fix-texi.perl <$*.texi+ >$*.texi
+       $(MAKEINFO) --no-split $*.texi
+       $(RM) $*.texi $*.texi+
+
+howto-index.txt: howto-index.sh $(wildcard howto/*.txt)
+       $(RM) $@+ $@
+       sh ./howto-index.sh $(wildcard howto/*.txt) >$@+
+       mv $@+ $@
+
+$(patsubst %,%.html,$(ARTICLES)) : %.html : %.txt
+       $(ASCIIDOC) -b xhtml11 $*.txt
+
+WEBDOC_DEST = /pub/software/scm/git/docs
+
+$(patsubst %.txt,%.html,$(wildcard howto/*.txt)): %.html : %.txt
+       $(RM) $@+ $@
+       sed -e '1,/^$$/d' $< | $(ASCIIDOC) -b xhtml11 - >$@+
+       mv $@+ $@
+
+install-webdoc : html
+       sh ./install-webdoc.sh $(WEBDOC_DEST)
+
+quick-install:
+       sh ./install-doc-quick.sh $(DOC_REF) $(mandir)
+
+.PHONY: .FORCE-GIT-VERSION-FILE
diff --git a/Documentation/RelNotes-1.5.0.1.txt b/Documentation/RelNotes-1.5.0.1.txt
new file mode 100644 (file)
index 0000000..fea3f99
--- /dev/null
@@ -0,0 +1,42 @@
+GIT v1.5.0.1 Release Notes
+==========================
+
+Fixes since v1.5.0
+------------------
+
+* Documentation updates
+
+  - Clarifications and corrections to 1.5.0 release notes.
+
+  - The main documentation did not link to git-remote documentation.
+
+  - Clarified introductory text of git-rebase documentation.
+
+  - Converted remaining mentions of update-index on Porcelain
+    documents to git-add/git-rm.
+
+  - Some i18n.* configuration variables were incorrectly
+    described as core.*; fixed.
+
+* Bugfixes
+
+  - git-add and git-update-index on a filesystem on which
+    executable bits are unreliable incorrectly reused st_mode
+    bits even when the path changed between symlink and regular
+    file.
+
+  - git-daemon marks the listening sockets with FD_CLOEXEC so
+    that it won't be leaked into the children.
+
+  - segfault from git-blame when the mandatory pathname
+    parameter was missing was fixed; usage() message is given
+    instead.
+
+  - git-rev-list did not read $GIT_DIR/config file, which means
+    that did not honor i18n.logoutputencoding correctly.
+
+* Tweaks
+
+  - sliding mmap() inefficiently mmaped the same region of a
+    packfile with an access pattern that used objects in the
+    reverse order.  This has been made more efficient.
diff --git a/Documentation/RelNotes-1.5.0.2.txt b/Documentation/RelNotes-1.5.0.2.txt
new file mode 100644 (file)
index 0000000..b061e50
--- /dev/null
@@ -0,0 +1,65 @@
+GIT v1.5.0.2 Release Notes
+==========================
+
+Fixes since v1.5.0.1
+--------------------
+
+* Bugfixes
+
+  - Automated merge conflict handling when changes to symbolic
+    links conflicted were completely broken.  The merge-resolve
+    strategy created a regular file with conflict markers in it
+    in place of the symbolic link.  The default strategy,
+    merge-recursive was even more broken.  It removed the path
+    that was pointed at by the symbolic link.  Both of these
+    problems have been fixed.
+
+  - 'git diff maint master next' did not correctly give combined
+    diff across three trees.
+
+  - 'git fast-import' portability fix for Solaris.
+
+  - 'git show-ref --verify' without arguments did not error out
+    but segfaulted.
+
+  - 'git diff :tracked-file `pwd`/an-untracked-file' gave an extra
+    slashes after a/ and b/.
+
+  - 'git format-patch' produced too long filenames if the commit
+    message had too long line at the beginning.
+
+  - Running 'make all' and then without changing anything
+    running 'make install' still rebuilt some files.  This
+    was inconvenient when building as yourself and then
+    installing as root (especially problematic when the source
+    directory is on NFS and root is mapped to nobody).
+
+  - 'git-rerere' failed to deal with two unconflicted paths that
+    sorted next to each other.
+
+  - 'git-rerere' attempted to open(2) a symlink and failed if
+    there was a conflict.  Since a conflicting change to a
+    symlink would not benefit from rerere anyway, the command
+    now ignores conflicting changes to symlinks.
+
+  - 'git-repack' did not like to pass more than 64 arguments
+    internally to underlying 'rev-list' logic, which made it
+    impossible to repack after accumulating many (small) packs
+    in the repository.
+
+  - 'git-diff' to review the combined diff during a conflicted
+    merge were not reading the working tree version correctly
+    when changes to a symbolic link conflicted.  It should have
+    read the data using readlink(2) but read from the regular
+    file the symbolic link pointed at.
+
+  - 'git-remote' did not like period in a remote's name.
+
+* Documentation updates
+
+  - added and clarified core.bare, core.legacyheaders configurations.
+
+  - updated "git-clone --depth" documentation.
+
+
+* Assorted git-gui fixes.
diff --git a/Documentation/RelNotes-1.5.0.3.txt b/Documentation/RelNotes-1.5.0.3.txt
new file mode 100644 (file)
index 0000000..cd500f9
--- /dev/null
@@ -0,0 +1,58 @@
+GIT v1.5.0.3 Release Notes
+==========================
+
+Fixes since v1.5.0.2
+--------------------
+
+* Bugfixes
+
+  - 'git.el' honors the commit coding system from the configuration.
+
+  - 'blameview' in contrib/ correctly digs deeper when a line is
+    clicked.
+
+  - 'http-push' correctly makes sure the remote side has leading
+    path.  Earlier it started in the middle of the path, and
+    incorrectly.
+
+  - 'git-merge' did not exit with non-zero status when the
+    working tree was dirty and cannot fast forward.  It does
+    now.
+
+  - 'cvsexportcommit' does not lose yet-to-be-used message file.
+
+  - int-vs-size_t typefix when running combined diff on files
+    over 2GB long.
+
+  - 'git apply --whitespace=strip' should not touch unmodified
+    lines.
+
+  - 'git-mailinfo' choke when a logical header line was too long.
+
+  - 'git show A..B' did not error out.  Negative ref ("not A" in
+    this example) does not make sense for the purpose of the
+    command, so now it errors out.
+
+  - 'git fmt-merge-msg --file' without file parameter did not
+    correctly error out.
+
+  - 'git archimport' barfed upon encountering a commit without
+    summary.
+
+  - 'git index-pack' did not protect itself from getting a short
+    read out of pread(2).
+
+  - 'git http-push' had a few buffer overruns.
+
+  - Build dependency fixes to rebuild fetch.o when other headers
+    change.
+
+* Documentation updates
+
+  - user-manual updates.
+
+  - Options to 'git remote add' were described insufficiently.
+
+  - Configuration format.suffix was not documented.
+
+  - Other formatting and spelling fixes.
diff --git a/Documentation/RelNotes-1.5.0.4.txt b/Documentation/RelNotes-1.5.0.4.txt
new file mode 100644 (file)
index 0000000..feefa5d
--- /dev/null
@@ -0,0 +1,22 @@
+GIT v1.5.0.4 Release Notes
+==========================
+
+Fixes since v1.5.0.3
+--------------------
+
+* Bugfixes
+
+  - git.el does not add duplicate sign-off lines.
+
+  - git-commit shows the full stat of the resulting commit, not
+    just about the files in the current directory, when run from
+    a subdirectory.
+
+  - "git-checkout -m '@{8 hours ago}'" had a funny failure from
+    eval; fixed.
+
+  - git-gui updates.
+
+* Documentation updates
+
+* User manual updates
diff --git a/Documentation/RelNotes-1.5.0.5.txt b/Documentation/RelNotes-1.5.0.5.txt
new file mode 100644 (file)
index 0000000..eeec3d7
--- /dev/null
@@ -0,0 +1,26 @@
+GIT v1.5.0.5 Release Notes
+==========================
+
+Fixes since v1.5.0.3
+--------------------
+
+* Bugfixes
+
+  - git-merge (hence git-pull) did not refuse fast-forwarding
+    when the working tree had local changes that would have
+    conflicted with it.
+
+  - git.el does not add duplicate sign-off lines.
+
+  - git-commit shows the full stat of the resulting commit, not
+    just about the files in the current directory, when run from
+    a subdirectory.
+
+  - "git-checkout -m '@{8 hours ago}'" had a funny failure from
+    eval; fixed.
+
+  - git-gui updates.
+
+* Documentation updates
+
+* User manual updates
diff --git a/Documentation/RelNotes-1.5.0.6.txt b/Documentation/RelNotes-1.5.0.6.txt
new file mode 100644 (file)
index 0000000..c02015a
--- /dev/null
@@ -0,0 +1,21 @@
+GIT v1.5.0.6 Release Notes
+==========================
+
+Fixes since v1.5.0.5
+--------------------
+
+* Bugfixes
+
+  - a handful small fixes to gitweb.
+
+  - build procedure for user-manual is fixed not to require locally
+    installed stylesheets.
+
+  - "git commit $paths" on paths whose earlier contents were
+    already updated in the index were failing out.
+
+* Documentation
+
+  - user-manual has better cross references.
+
+  - gitweb installation/deployment procedure is now documented.
diff --git a/Documentation/RelNotes-1.5.0.7.txt b/Documentation/RelNotes-1.5.0.7.txt
new file mode 100644 (file)
index 0000000..670ad32
--- /dev/null
@@ -0,0 +1,18 @@
+GIT v1.5.0.7 Release Notes
+==========================
+
+Fixes since v1.5.0.6
+--------------------
+
+* Bugfixes
+
+  - git-upload-pack failed to close unused pipe ends, resulting
+    in many zombies to hang around.
+
+  - git-rerere was recording the contents of earlier hunks
+    duplicated in later hunks.  This prevented resolving the same
+    conflict when performing the same merge the other way around.
+
+* Documentation
+
+  - a few documentation fixes from Debian package maintainer.
diff --git a/Documentation/RelNotes-1.5.0.txt b/Documentation/RelNotes-1.5.0.txt
new file mode 100644 (file)
index 0000000..daf4bdb
--- /dev/null
@@ -0,0 +1,469 @@
+GIT v1.5.0 Release Notes
+========================
+
+Old news
+--------
+
+This section is for people who are upgrading from ancient
+versions of git.  Although all of the changes in this section
+happened before the current v1.4.4 release, they are summarized
+here in the v1.5.0 release notes for people who skipped earlier
+versions.
+
+As of git v1.5.0 there are some optional features that changes
+the repository to allow data to be stored and transferred more
+efficiently.  These features are not enabled by default, as they
+will make the repository unusable with older versions of git.
+Specifically, the available options are:
+
+ - There is a configuration variable core.legacyheaders that
+   changes the format of loose objects so that they are more
+   efficient to pack and to send out of the repository over git
+   native protocol, since v1.4.2.  However, loose objects
+   written in the new format cannot be read by git older than
+   that version; people fetching from your repository using
+   older clients over dumb transports (e.g. http) using older
+   versions of git will also be affected.
+
+   To let git use the new loose object format, you have to
+   set core.legacyheaders to false.
+
+ - Since v1.4.3, configuration repack.usedeltabaseoffset allows
+   packfile to be created in more space efficient format, which
+   cannot be read by git older than that version.
+
+   To let git use the new format for packfiles, you have to
+   set repack.usedeltabaseoffset to true.
+
+The above two new features are not enabled by default and you
+have to explicitly ask for them, because they make repositories
+unreadable by older versions of git, and in v1.5.0 we still do
+not enable them by default for the same reason.  We will change
+this default probably 1 year after 1.4.2's release, when it is
+reasonable to expect everybody to have new enough version of
+git.
+
+ - 'git pack-refs' appeared in v1.4.4; this command allows tags
+   to be accessed much more efficiently than the traditional
+   'one-file-per-tag' format.  Older git-native clients can
+   still fetch from a repository that packed and pruned refs
+   (the server side needs to run the up-to-date version of git),
+   but older dumb transports cannot.  Packing of refs is done by
+   an explicit user action, either by use of "git pack-refs
+   --prune" command or by use of "git gc" command.
+
+ - 'git -p' to paginate anything -- many commands do pagination
+   by default on a tty.  Introduced between v1.4.1 and v1.4.2;
+   this may surprise old timers.
+
+ - 'git archive' superseded 'git tar-tree' in v1.4.3;
+
+ - 'git cvsserver' was new invention in v1.3.0;
+
+ - 'git repo-config', 'git grep', 'git rebase' and 'gitk' were
+   seriously enhanced during v1.4.0 timeperiod.
+
+ - 'gitweb' became part of git.git during v1.4.0 timeperiod and
+   seriously modified since then.
+
+ - reflog is an v1.4.0 invention.  This allows you to name a
+   revision that a branch used to be at (e.g. "git diff
+   master@{yesterday} master" allows you to see changes since
+   yesterday's tip of the branch).
+
+
+Updates in v1.5.0 since v1.4.4 series
+-------------------------------------
+
+* Index manipulation
+
+ - git-add is to add contents to the index (aka "staging area"
+   for the next commit), whether the file the contents happen to
+   be is an existing one or a newly created one.
+
+ - git-add without any argument does not add everything
+   anymore.  Use 'git-add .' instead.  Also you can add
+   otherwise ignored files with an -f option.
+
+ - git-add tries to be more friendly to users by offering an
+   interactive mode ("git-add -i").
+
+ - git-commit <path> used to refuse to commit if <path> was
+   different between HEAD and the index (i.e. update-index was
+   used on it earlier).  This check was removed.
+
+ - git-rm is much saner and safer.  It is used to remove paths
+   from both the index file and the working tree, and makes sure
+   you are not losing any local modification before doing so.
+
+ - git-reset <tree> <paths>... can be used to revert index
+   entries for selected paths.
+
+ - git-update-index is much less visible.  Many suggestions to
+   use the command in git output and documentation have now been
+   replaced by simpler commands such as "git add" or "git rm".
+
+
+* Repository layout and objects transfer
+
+ - The data for origin repository is stored in the configuration
+   file $GIT_DIR/config, not in $GIT_DIR/remotes/, for newly
+   created clones.  The latter is still supported and there is
+   no need to convert your existing repository if you are
+   already comfortable with your workflow with the layout.
+
+ - git-clone always uses what is known as "separate remote"
+   layout for a newly created repository with a working tree.
+
+   A repository with the separate remote layout starts with only
+   one default branch, 'master', to be used for your own
+   development.  Unlike the traditional layout that copied all
+   the upstream branches into your branch namespace (while
+   renaming their 'master' to your 'origin'), the new layout
+   puts upstream branches into local "remote-tracking branches"
+   with their own namespace. These can be referenced with names
+   such as "origin/$upstream_branch_name" and are stored in
+   .git/refs/remotes rather than .git/refs/heads where normal
+   branches are stored.
+
+   This layout keeps your own branch namespace less cluttered,
+   avoids name collision with your upstream, makes it possible
+   to automatically track new branches created at the remote
+   after you clone from it, and makes it easier to interact with
+   more than one remote repository (you can use "git remote" to
+   add other repositories to track).  There might be some
+   surprises:
+
+   * 'git branch' does not show the remote tracking branches.
+     It only lists your own branches.  Use '-r' option to view
+     the tracking branches.
+
+   * If you are forking off of a branch obtained from the
+     upstream, you would have done something like 'git branch
+     my-next next', because traditional layout dropped the
+     tracking branch 'next' into your own branch namespace.
+     With the separate remote layout, you say 'git branch next
+     origin/next', which allows you to use the matching name
+     'next' for your own branch.  It also allows you to track a
+     remote other than 'origin' (i.e. where you initially cloned
+     from) and fork off of a branch from there the same way
+     (e.g. "git branch mingw j6t/master").
+
+   Repositories initialized with the traditional layout continue
+   to work.
+
+ - New branches that appear on the origin side after a clone is
+   made are also tracked automatically.  This is done with an
+   wildcard refspec "refs/heads/*:refs/remotes/origin/*", which
+   older git does not understand, so if you clone with 1.5.0,
+   you would need to downgrade remote.*.fetch in the
+   configuration file to specify each branch you are interested
+   in individually if you plan to fetch into the repository with
+   older versions of git (but why would you?).
+
+ - Similarly, wildcard refspec "refs/heads/*:refs/remotes/me/*"
+   can be given to "git-push" command to update the tracking
+   branches that is used to track the repository you are pushing
+   from on the remote side.
+
+ - git-branch and git-show-branch know remote tracking branches
+   (use the command line switch "-r" to list only tracked branches).
+
+ - git-push can now be used to delete a remote branch or a tag.
+   This requires the updated git on the remote side (use "git
+   push <remote> :refs/heads/<branch>" to delete "branch").
+
+ - git-push more aggressively keeps the transferred objects
+   packed.  Earlier we recommended to monitor amount of loose
+   objects and repack regularly, but you should repack when you
+   accumulated too many small packs this way as well.  Updated
+   git-count-objects helps you with this.
+
+ - git-fetch also more aggressively keeps the transferred objects
+   packed.  This behavior of git-push and git-fetch can be
+   tweaked with a single configuration transfer.unpacklimit (but
+   usually there should not be any need for a user to tweak it).
+
+ - A new command, git-remote, can help you manage your remote
+   tracking branch definitions.
+
+ - You may need to specify explicit paths for upload-pack and/or
+   receive-pack due to your ssh daemon configuration on the
+   other end.  This can now be done via remote.*.uploadpack and
+   remote.*.receivepack configuration.
+
+
+* Bare repositories
+
+ - Certain commands change their behavior in a bare repository
+   (i.e. a repository without associated working tree).  We use
+   a fairly conservative heuristic (if $GIT_DIR is ".git", or
+   ends with "/.git", the repository is not bare) to decide if a
+   repository is bare, but "core.bare" configuration variable
+   can be used to override the heuristic when it misidentifies
+   your repository.
+
+ - git-fetch used to complain updating the current branch but
+   this is now allowed for a bare repository.  So is the use of
+   'git-branch -f' to update the current branch.
+
+ - Porcelain-ish commands that require a working tree refuses to
+   work in a bare repository.
+
+
+* Reflog
+
+ - Reflog records the history from the view point of the local
+   repository. In other words, regardless of the real history,
+   the reflog shows the history as seen by one particular
+   repository (this enables you to ask "what was the current
+   revision in _this_ repository, yesterday at 1pm?").  This
+   facility is enabled by default for repositories with working
+   trees, and can be accessed with the "branch@{time}" and
+   "branch@{Nth}" notation.
+
+ - "git show-branch" learned showing the reflog data with the
+   new -g option.  "git log" has -g option to view reflog
+   entries in a more verbose manner.
+
+ - git-branch knows how to rename branches and moves existing
+   reflog data from the old branch to the new one.
+
+ - In addition to the reflog support in v1.4.4 series, HEAD
+   reference maintains its own log.  "HEAD@{5.minutes.ago}"
+   means the commit you were at 5 minutes ago, which takes
+   branch switching into account.  If you want to know where the
+   tip of your current branch was at 5 minutes ago, you need to
+   explicitly say its name (e.g. "master@{5.minutes.ago}") or
+   omit the refname altogether i.e. "@{5.minutes.ago}".
+
+ - The commits referred to by reflog entries are now protected
+   against pruning.  The new command "git reflog expire" can be
+   used to truncate older reflog entries and entries that refer
+   to commits that have been pruned away previously with older
+   versions of git.
+
+   Existing repositories that have been using reflog may get
+   complaints from fsck-objects and may not be able to run
+   git-repack, if you had run git-prune from older git; please
+   run "git reflog expire --stale-fix --all" first to remove
+   reflog entries that refer to commits that are no longer in
+   the repository when that happens.
+
+
+* Crufts removal
+
+ - We used to say "old commits are retrievable using reflog and
+   'master@{yesterday}' syntax as long as you haven't run
+   git-prune".  We no longer have to say the latter half of the
+   above sentence, as git-prune does not remove things reachable
+   from reflog entries.
+
+ - There is a toplevel garbage collector script, 'git-gc', that
+   runs periodic cleanup functions, including 'git-repack -a -d',
+   'git-reflog expire', 'git-pack-refs --prune', and 'git-rerere
+   gc'.
+
+ - The output from fsck ("fsck-objects" is called just "fsck"
+   now, but the old name continues to work) was needlessly
+   alarming in that it warned missing objects that are reachable
+   only from dangling objects.  This has been corrected and the
+   output is much more useful.
+
+
+* Detached HEAD
+
+ - You can use 'git-checkout' to check out an arbitrary revision
+   or a tag as well, instead of named branches.  This will
+   dissociate your HEAD from the branch you are currently on.
+
+   A typical use of this feature is to "look around".  E.g.
+
+       $ git checkout v2.6.16
+       ... compile, test, etc.
+       $ git checkout v2.6.17
+       ... compile, test, etc.
+
+ - After detaching your HEAD, you can go back to an existing
+   branch with usual "git checkout $branch".  Also you can
+   start a new branch using "git checkout -b $newbranch" to
+   start a new branch at that commit.
+
+ - You can even pull from other repositories, make merges and
+   commits while your HEAD is detached.  Also you can use "git
+   reset" to jump to arbitrary commit, while still keeping your
+   HEAD detached.
+
+   Remember that a detached state is volatile, i.e. it will be forgotten
+   as soon as you move away from it with the checkout or reset command,
+   unless a branch is created from it as mentioned above.  It is also
+   possible to rescue a lost detached state from the HEAD reflog.
+
+
+* Packed refs
+
+ - Repositories with hundreds of tags have been paying large
+   overhead, both in storage and in runtime, due to the
+   traditional one-ref-per-file format.  A new command,
+   git-pack-refs, can be used to "pack" them in more efficient
+   representation (you can let git-gc do this for you).
+
+ - Clones and fetches over dumb transports are now aware of
+   packed refs and can download from repositories that use
+   them.
+
+
+* Configuration
+
+ - configuration related to color setting are consolidated under
+   color.* namespace (older diff.color.*, status.color.* are
+   still supported).
+
+ - 'git-repo-config' command is accessible as 'git-config' now.
+
+
+* Updated features
+
+ - git-describe uses better criteria to pick a base ref.  It
+   used to pick the one with the newest timestamp, but now it
+   picks the one that is topologically the closest (that is,
+   among ancestors of commit C, the ref T that has the shortest
+   output from "git-rev-list T..C" is chosen).
+
+ - git-describe gives the number of commits since the base ref
+   between the refname and the hash suffix.  E.g. the commit one
+   before v2.6.20-rc6 in the kernel repository is:
+
+       v2.6.20-rc5-306-ga21b069
+
+   which tells you that its object name begins with a21b069,
+   v2.6.20-rc5 is an ancestor of it (meaning, the commit
+   contains everything -rc5 has), and there are 306 commits
+   since v2.6.20-rc5.
+
+ - git-describe with --abbrev=0 can be used to show only the
+   name of the base ref.
+
+ - git-blame learned a new option, --incremental, that tells it
+   to output the blames as they are assigned.  A sample script
+   to use it is also included as contrib/blameview.
+
+ - git-blame starts annotating from the working tree by default.
+
+
+* Less external dependency
+
+ - We no longer require the "merge" program from the RCS suite.
+   All 3-way file-level merges are now done internally.
+
+ - The original implementation of git-merge-recursive which was
+   in Python has been removed; we have a C implementation of it
+   now.
+
+ - git-shortlog is no longer a Perl script.  It no longer
+   requires output piped from git-log; it can accept revision
+   parameters directly on the command line.
+
+
+* I18n
+
+ - We have always encouraged the commit message to be encoded in
+   UTF-8, but the users are allowed to use legacy encoding as
+   appropriate for their projects.  This will continue to be the
+   case.  However, a non UTF-8 commit encoding _must_ be
+   explicitly set with i18n.commitencoding in the repository
+   where a commit is made; otherwise git-commit-tree will
+   complain if the log message does not look like a valid UTF-8
+   string.
+
+ - The value of i18n.commitencoding in the originating
+   repository is recorded in the commit object on the "encoding"
+   header, if it is not UTF-8.  git-log and friends notice this,
+   and reencodes the message to the log output encoding when
+   displaying, if they are different.  The log output encoding
+   is determined by "git log --encoding=<encoding>",
+   i18n.logoutputencoding configuration, or i18n.commitencoding
+   configuration, in the decreasing order of preference, and
+   defaults to UTF-8.
+
+ - Tools for e-mailed patch application now default to -u
+   behavior; i.e. it always re-codes from the e-mailed encoding
+   to the encoding specified with i18n.commitencoding.  This
+   unfortunately forces projects that have happily been using a
+   legacy encoding without setting i18n.commitencoding to set
+   the configuration, but taken with other improvement, please
+   excuse us for this very minor one-time inconvenience.
+
+
+* e-mailed patches
+
+ - See the above I18n section.
+
+ - git-format-patch now enables --binary without being asked.
+   git-am does _not_ default to it, as sending binary patch via
+   e-mail is unusual and is harder to review than textual
+   patches and it is prudent to require the person who is
+   applying the patch to explicitly ask for it.
+
+ - The default suffix for git-format-patch output is now ".patch",
+   not ".txt".  This can be changed with --suffix=.txt option,
+   or setting the config variable "format.suffix" to ".txt".
+
+
+* Foreign SCM interfaces
+
+ - git-svn now requires the Perl SVN:: libraries, the
+   command-line backend was too slow and limited.
+
+ - the 'commit' subcommand of git-svn has been renamed to
+   'set-tree', and 'dcommit' is the recommended replacement for
+   day-to-day work.
+
+ - git fast-import backend.
+
+
+* User support
+
+ - Quite a lot of documentation updates.
+
+ - Bash completion scripts have been updated heavily.
+
+ - Better error messages for often used Porcelainish commands.
+
+ - Git GUI.  This is a simple Tk based graphical interface for
+   common Git operations.
+
+
+* Sliding mmap
+
+ - We used to assume that we can mmap the whole packfile while
+   in use, but with a large project this consumes huge virtual
+   memory space and truly huge ones would not fit in the
+   userland address space on 32-bit platforms.  We now mmap huge
+   packfile in pieces to avoid this problem.
+
+
+* Shallow clones
+
+ - There is a partial support for 'shallow' repositories that
+   keeps only recent history.  A 'shallow clone' is created by
+   specifying how deep that truncated history should be
+   (e.g. "git clone --depth 5 git://some.where/repo.git").
+
+   Currently a shallow repository has number of limitations:
+
+   - Cloning and fetching _from_ a shallow clone are not
+     supported (nor tested -- so they might work by accident but
+     they are not expected to).
+
+   - Pushing from nor into a shallow clone are not expected to
+     work.
+
+   - Merging inside a shallow repository would work as long as a
+     merge base is found in the recent history, but otherwise it
+     will be like merging unrelated histories and may result in
+     huge conflicts.
+
+   but this would be more than adequate for people who want to
+   look at near the tip of a big project with a deep history and
+   send patches in e-mail format.
diff --git a/Documentation/RelNotes-1.5.1.1.txt b/Documentation/RelNotes-1.5.1.1.txt
new file mode 100644 (file)
index 0000000..9147121
--- /dev/null
@@ -0,0 +1,65 @@
+GIT v1.5.1.1 Release Notes
+==========================
+
+Fixes since v1.5.1
+------------------
+
+* Documentation updates
+
+  - The --left-right option of rev-list and friends is documented.
+
+  - The documentation for cvsimport has been majorly improved.
+
+  - "git-show-ref --exclude-existing" was documented.
+
+* Bugfixes
+
+  - The implementation of -p option in "git cvsexportcommit" had
+    the meaning of -C (context reduction) option wrong, and
+    loosened the context requirements when it was told to be
+    strict.
+
+  - "git cvsserver" did not behave like the real cvsserver when
+    client side removed a file from the working tree without
+    doing anything else on the path.  In such a case, it should
+    restore it from the checked out revision.
+
+  - "git fsck" issued an alarming error message on detached
+    HEAD.  It is not an error since at least 1.5.0.
+
+  - "git send-email" produced of References header of unbounded length;
+    fixed this with line-folding.
+
+  - "git archive" to download from remote site should not
+    require you to be in a git repository, but it incorrectly
+    did.
+
+  - "git apply" ignored -p<n> for "diff --git" formatted
+    patches.
+
+  - "git rerere" recorded a conflict that had one side empty
+    (the other side adds) incorrectly; this made merging in the
+    other direction fail to use previously recorded resolution.
+
+  - t4200 test was broken where "wc -l" pads its output with
+    spaces.
+
+  - "git branch -m old new" to rename branch did not work
+    without a configuration file in ".git/config".
+
+  - The sample hook for notification e-mail was misnamed.
+
+  - gitweb did not show type-changing patch correctly in the
+    blobdiff view.
+
+  - git-svn did not error out with incorrect command line options.
+
+  - git-svn fell into an infinite loop when insanely long commit
+    message was found.
+
+  - git-svn dcommit and rebase was confused by patches that were
+    merged from another branch that is managed by git-svn.
+
+  - git-svn used to get confused when globbing remote branch/tag
+    spec (e.g. "branches = proj/branches/*:refs/remotes/origin/*")
+    is used and there was a plain file that matched the glob.
diff --git a/Documentation/RelNotes-1.5.1.2.txt b/Documentation/RelNotes-1.5.1.2.txt
new file mode 100644 (file)
index 0000000..d884563
--- /dev/null
@@ -0,0 +1,50 @@
+GIT v1.5.1.2 Release Notes
+==========================
+
+Fixes since v1.5.1.1
+--------------------
+
+* Bugfixes
+
+  - "git clone" over http from a repository that has lost the
+    loose refs by running "git pack-refs" were broken (a code to
+    deal with this was added to "git fetch" in v1.5.0, but it
+    was missing from "git clone").
+
+  - "git diff a/ b/" incorrectly fell in "diff between two
+    filesystem objects" codepath, when the user most likely
+    wanted to limit the extent of output to two tracked
+    directories.
+
+  - git-quiltimport had the same bug as we fixed for
+    git-applymbox in v1.5.1.1 -- it gave an alarming "did not
+    have any patch" message (but did not actually fail and was
+    harmless).
+
+  - various git-svn fixes.
+
+  - Sample update hook incorrectly always refused requests to
+    delete branches through push.
+
+  - git-blame on a very long working tree path had buffer
+    overrun problem.
+
+  - git-apply did not like to be fed two patches in a row that created
+    and then modified the same file.
+
+  - git-svn was confused when a non-project was stored directly under
+    trunk/, branches/ and tags/.
+
+  - git-svn wants the Error.pm module that was at least as new
+    as what we ship as part of git; install ours in our private
+    installation location if the one on the system is older.
+
+  - An earlier update to command line integer parameter parser was
+    botched and made 'update-index --cacheinfo' completely useless.
+
+
+* Documentation updates
+
+  - Various documentation updates from J. Bruce Fields, Frank
+    Lichtenheld, Alex Riesen and others.  Andrew Ruder started a
+    war on undocumented options.
diff --git a/Documentation/RelNotes-1.5.1.3.txt b/Documentation/RelNotes-1.5.1.3.txt
new file mode 100644 (file)
index 0000000..876408b
--- /dev/null
@@ -0,0 +1,45 @@
+GIT v1.5.1.3 Release Notes
+==========================
+
+Fixes since v1.5.1.2
+--------------------
+
+* Bugfixes
+
+  - git-add tried to optimize by finding common leading
+    directories across its arguments but botched, causing very
+    confused behaviour.
+
+  - unofficial rpm.spec file shipped with git was letting
+    ETC_GITCONFIG set to /usr/etc/gitconfig.  Tweak the official
+    Makefile to make it harder for distro people to make the
+    same mistake, by setting the variable to /etc/gitconfig if
+    prefix is set to /usr.
+
+  - git-svn inconsistently stripped away username from the URL
+    only when svnsync_props was in use.
+
+  - git-svn got confused when handling symlinks on Mac OS.
+
+  - git-send-email was not quoting recipient names that have
+    period '.' in them.  Also it did not allow overriding
+    envelope sender, which made it impossible to send patches to
+    certain subscriber-only lists.
+
+  - built-in write_tree() routine had a sequence that renamed a
+    file that is still open, which some systems did not like.
+
+  - when memory is very tight, sliding mmap code to read
+    packfiles incorrectly closed the fd that was still being
+    used to read the pack.
+
+  - import-tars contributed front-end for fastimport was passing
+    wrong directory modes without checking.
+
+  - git-fastimport trusted its input too much and allowed to
+    create corrupt tree objects with entries without a name.
+
+  - git-fetch needlessly barfed when too long reflog action
+    description was given by the caller.
+
+Also contains various documentation updates.
diff --git a/Documentation/RelNotes-1.5.1.4.txt b/Documentation/RelNotes-1.5.1.4.txt
new file mode 100644 (file)
index 0000000..df2f66c
--- /dev/null
@@ -0,0 +1,30 @@
+GIT v1.5.1.4 Release Notes
+==========================
+
+Fixes since v1.5.1.3
+--------------------
+
+* Bugfixes
+
+  - "git-http-fetch" did not work around a bug in libcurl
+    earlier than 7.16 (curl_multi_remove_handle() was broken).
+
+  - "git cvsserver" handles a file that was once removed and
+    then added again correctly.
+
+  - import-tars script (in contrib/) handles GNU tar archives
+    that contain pathnames longer than 100 bytes (long-link
+    extension) correctly.
+
+  - xdelta test program did not build correctly.
+
+  - gitweb sometimes tried incorrectly to apply function to
+    decode utf8 twice, resulting in corrupt output.
+
+  - "git blame -C" mishandled text at the end of a group of
+    lines.
+
+  - "git log/rev-list --boundary" did not produce output
+    correctly without --left-right option.
+
+  - Many documentation updates.
diff --git a/Documentation/RelNotes-1.5.1.5.txt b/Documentation/RelNotes-1.5.1.5.txt
new file mode 100644 (file)
index 0000000..b0ab8eb
--- /dev/null
@@ -0,0 +1,42 @@
+GIT v1.5.1.5 Release Notes
+==========================
+
+Fixes since v1.5.1.4
+--------------------
+
+* Bugfixes
+
+  - git-send-email did not understand aliases file for mutt, which
+    allows leading whitespaces.
+
+  - git-format-patch emitted Content-Type and Content-Transfer-Encoding
+    headers for non ASCII contents, but failed to add MIME-Version.
+
+  - git-name-rev had a buffer overrun with a deep history.
+
+  - contributed script import-tars did not get the directory in
+    tar archives interpreted correctly.
+
+  - git-svn was reported to segfault for many people on list and
+    #git; hopefully this has been fixed.
+
+  - "git-svn clone" does not try to minimize the URL
+    (i.e. connect to higher level hierarchy) by default, as this
+    can prevent clone to fail if only part of the repository
+    (e.g. 'trunk') is open to public.
+
+  - "git checkout branch^0" did not detach the head when you are
+    already on 'branch'; backported the fix from the 'master'.
+
+  - "git-config section.var" did not correctly work when
+    existing configuration file had both [section] and [section "name"]
+    next to each other.
+
+  - "git clone ../other-directory" was fooled if the current
+    directory $PWD points at is a symbolic link.
+
+  - (build) tree_entry_extract() function was both static inline
+    and extern, which caused trouble compiling with Forte12
+    compilers on Sun.
+
+  - Many many documentation fixes and updates.
diff --git a/Documentation/RelNotes-1.5.1.6.txt b/Documentation/RelNotes-1.5.1.6.txt
new file mode 100644 (file)
index 0000000..55f3ac1
--- /dev/null
@@ -0,0 +1,45 @@
+GIT v1.5.1.6 Release Notes
+==========================
+
+Fixes since v1.5.1.4
+--------------------
+
+* Bugfixes
+
+  - git-send-email did not understand aliases file for mutt, which
+    allows leading whitespaces.
+
+  - git-format-patch emitted Content-Type and Content-Transfer-Encoding
+    headers for non ASCII contents, but failed to add MIME-Version.
+
+  - git-name-rev had a buffer overrun with a deep history.
+
+  - contributed script import-tars did not get the directory in
+    tar archives interpreted correctly.
+
+  - git-svn was reported to segfault for many people on list and
+    #git; hopefully this has been fixed.
+
+  - git-svn also had a bug to crash svnserve by sending a bad
+    sequence of requests.
+
+  - "git-svn clone" does not try to minimize the URL
+    (i.e. connect to higher level hierarchy) by default, as this
+    can prevent clone to fail if only part of the repository
+    (e.g. 'trunk') is open to public.
+
+  - "git checkout branch^0" did not detach the head when you are
+    already on 'branch'; backported the fix from the 'master'.
+
+  - "git-config section.var" did not correctly work when
+    existing configuration file had both [section] and [section "name"]
+    next to each other.
+
+  - "git clone ../other-directory" was fooled if the current
+    directory $PWD points at is a symbolic link.
+
+  - (build) tree_entry_extract() function was both static inline
+    and extern, which caused trouble compiling with Forte12
+    compilers on Sun.
+
+  - Many many documentation fixes and updates.
diff --git a/Documentation/RelNotes-1.5.1.txt b/Documentation/RelNotes-1.5.1.txt
new file mode 100644 (file)
index 0000000..daed367
--- /dev/null
@@ -0,0 +1,371 @@
+GIT v1.5.1 Release Notes
+========================
+
+Updates since v1.5.0
+--------------------
+
+* Deprecated commands and options.
+
+  - git-diff-stages and git-resolve have been removed.
+
+* New commands and options.
+
+  - "git log" and friends take --reverse, which instructs them
+    to give their output in the order opposite from their usual.
+    They typically output from new to old, but with this option
+    their output would read from old to new.  "git shortlog"
+    usually lists older commits first, but with this option,
+    they are shown from new to old.
+
+  - "git log --pretty=format:<string>" to allow more flexible
+    custom log output.
+
+  - "git diff" learned --ignore-space-at-eol.  This is a weaker
+    form of --ignore-space-change.
+
+  - "git diff --no-index pathA pathB" can be used as diff
+    replacement with git specific enhancements.
+
+  - "git diff --no-index" can read from '-' (standard input).
+
+  - "git diff" also learned --exit-code to exit with non-zero
+    status when it found differences.  In the future we might
+    want to make this the default but that would be a rather big
+    backward incompatible change; it will stay as an option for
+    now.
+
+  - "git diff --quiet" is --exit-code with output turned off,
+    meant for scripted use to quickly determine if there is any
+    tree-level difference.
+
+  - Textual patch generation with "git diff" without -w/-b
+    option has been significantly optimized.  "git blame" got
+    faster because of the same change.
+
+  - "git log" and "git rev-list" has been optimized
+    significantly when they are used with pathspecs.
+
+  - "git branch --track" can be used to set up configuration
+    variables to help it easier to base your work on branches
+    you track from a remote site.
+
+  - "git format-patch --attach" now emits attachments.  Use
+    --inline to get an inlined multipart/mixed.
+
+  - "git name-rev" learned --refs=<pattern>, to limit the tags
+    used for naming the given revisions only to the ones
+    matching the given pattern.
+
+  - "git remote update" is to run "git fetch" for defined remotes
+    to update tracking branches.
+
+  - "git cvsimport" can now take '-d' to talk with a CVS
+    repository different from what are recorded in CVS/Root
+    (overriding it with environment CVSROOT does not work).
+
+  - "git bundle" can help sneaker-netting your changes between
+    repositories.
+
+  - "git mergetool" can help 3-way file-level conflict
+    resolution with your favorite graphical merge tools.
+
+  - A new configuration "core.symlinks" can be used to disable
+    symlinks on filesystems that do not support them; they are
+    checked out as regular files instead.
+
+  - You can name a commit object with its first line of the
+    message.  The syntax to use is ':/message text'.  E.g.
+
+    $ git show ":/object name: introduce ':/<oneline prefix>' notation"
+
+    means the same thing as:
+
+    $ git show 28a4d940443806412effa246ecc7768a21553ec7
+
+  - "git bisect" learned a new command "run" that takes a script
+    to run after each revision is checked out to determine if it
+    is good or bad, to automate the bisection process.
+
+  - "git log" family learned a new traversal option --first-parent,
+    which does what the name suggests.
+
+
+* Updated behavior of existing commands.
+
+  - "git-merge-recursive" used to barf when there are more than
+    one common ancestors for the merge, and merging them had a
+    rename/rename conflict.  This has been fixed.
+
+  - "git fsck" does not barf on corrupt loose objects.
+
+  - "git rm" does not remove newly added files without -f.
+
+  - "git archimport" allows remapping when coming up with git
+    branch names from arch names.
+
+  - git-svn got almost a rewrite.
+
+  - core.autocrlf configuration, when set to 'true', makes git
+    to convert CRLF at the end of lines in text files to LF when
+    reading from the filesystem, and convert in reverse when
+    writing to the filesystem.  The variable can be set to
+    'input', in which case the conversion happens only while
+    reading from the filesystem but files are written out with
+    LF at the end of lines.  Currently, which paths to consider
+    'text' (i.e. be subjected to the autocrlf mechanism) is
+    decided purely based on the contents, but the plan is to
+    allow users to explicitly override this heuristic based on
+    paths.
+
+  - The behavior of 'git-apply', when run in a subdirectory,
+    without --index nor --cached were inconsistent with that of
+    the command with these options.  This was fixed to match the
+    behavior with --index.  A patch that is meant to be applied
+    with -p1 from the toplevel of the project tree can be
+    applied with any custom -p<n> option.  A patch that is not
+    relative to the toplevel needs to be applied with -p<n>
+    option with or without --index (or --cached).
+
+  - "git diff" outputs a trailing HT when pathnames have embedded
+    SP on +++/--- header lines, in order to help "GNU patch" to
+    parse its output.  "git apply" was already updated to accept
+    this modified output format since ce74618d (Sep 22, 2006).
+
+  - "git cvsserver" runs hooks/update and honors its exit status.
+
+  - "git cvsserver" can be told to send everything with -kb.
+
+  - "git diff --check" also honors the --color output option.
+
+  - "git name-rev" used to stress the fact that a ref is a tag too
+    much, by saying something like "v1.2.3^0~22".  It now says
+    "v1.2.3~22" in such a case (it still says "v1.2.3^0" if it does
+    not talk about an ancestor of the commit that is tagged, which
+    makes sense).
+
+  - "git rev-list --boundary" now shows boundary markers for the
+    commits omitted by --max-age and --max-count condition.
+
+  - The configuration mechanism now reads $(prefix)/etc/gitconfig.
+
+  - "git apply --verbose" shows what preimage lines were wanted
+    when it couldn't find them.
+
+  - "git status" in a read-only repository got a bit saner.
+
+  - "git fetch" (hence "git clone" and "git pull") are less
+    noisy when the output does not go to tty.
+
+  - "git fetch" between repositories with many refs were slow
+    even when there are not many changes that needed
+    transferring.  This has been sped up by partially rewriting
+    the heaviest parts in C.
+
+  - "git mailinfo" which splits an e-mail into a patch and the
+    meta-information was rewritten, thanks to Don Zickus.  It
+    handles nested multipart better.  The command was broken for
+    a brief period on 'master' branch since 1.5.0 but the
+    breakage is fixed now.
+
+  - send-email learned configurable bcc and chain-reply-to.
+
+  - "git remote show $remote" also talks about branches that
+    would be pushed if you run "git push remote".
+
+  - Using objects from packs is now seriously optimized by clever
+    use of a cache.  This should be most noticeable in git-log
+    family of commands that involve reading many tree objects.
+    In addition, traversing revisions while filtering changes
+    with pathspecs is made faster by terminating the comparison
+    between the trees as early as possible.
+
+
+* Hooks
+
+  - The part to send out notification e-mails was removed from
+    the sample update hook, as it was not an appropriate place
+    to do so.  The proper place to do this is the new post-receive
+    hook.  An example hook has been added to contrib/hooks/.
+
+
+* Others
+
+  - git-revert, git-gc and git-cherry-pick are now built-ins.
+
+Fixes since v1.5.0
+------------------
+
+These are all in v1.5.0.x series.
+
+* Documentation updates
+
+  - Clarifications and corrections to 1.5.0 release notes.
+
+  - The main documentation did not link to git-remote documentation.
+
+  - Clarified introductory text of git-rebase documentation.
+
+  - Converted remaining mentions of update-index on Porcelain
+    documents to git-add/git-rm.
+
+  - Some i18n.* configuration variables were incorrectly
+    described as core.*; fixed.
+
+  - added and clarified core.bare, core.legacyheaders configurations.
+
+  - updated "git-clone --depth" documentation.
+
+  - user-manual updates.
+
+  - Options to 'git remote add' were described insufficiently.
+
+  - Configuration format.suffix was not documented.
+
+  - Other formatting and spelling fixes.
+
+  - user-manual has better cross references.
+
+  - gitweb installation/deployment procedure is now documented.
+
+
+* Bugfixes
+
+  - git-upload-pack closes unused pipe ends; earlier this caused
+    many zombies to hang around.
+
+  - git-rerere was recording the contents of earlier hunks
+    duplicated in later hunks.  This prevented resolving the same
+    conflict when performing the same merge the other way around.
+
+  - git-add and git-update-index on a filesystem on which
+    executable bits are unreliable incorrectly reused st_mode
+    bits even when the path changed between symlink and regular
+    file.
+
+  - git-daemon marks the listening sockets with FD_CLOEXEC so
+    that it won't be leaked into the children.
+
+  - segfault from git-blame when the mandatory pathname
+    parameter was missing was fixed; usage() message is given
+    instead.
+
+  - git-rev-list did not read $GIT_DIR/config file, which means
+    that did not honor i18n.logoutputencoding correctly.
+
+  - Automated merge conflict handling when changes to symbolic
+    links conflicted were completely broken.  The merge-resolve
+    strategy created a regular file with conflict markers in it
+    in place of the symbolic link.  The default strategy,
+    merge-recursive was even more broken.  It removed the path
+    that was pointed at by the symbolic link.  Both of these
+    problems have been fixed.
+
+  - 'git diff maint master next' did not correctly give combined
+    diff across three trees.
+
+  - 'git fast-import' portability fix for Solaris.
+
+  - 'git show-ref --verify' without arguments did not error out
+    but segfaulted.
+
+  - 'git diff :tracked-file `pwd`/an-untracked-file' gave an extra
+    slashes after a/ and b/.
+
+  - 'git format-patch' produced too long filenames if the commit
+    message had too long line at the beginning.
+
+  - Running 'make all' and then without changing anything
+    running 'make install' still rebuilt some files.  This
+    was inconvenient when building as yourself and then
+    installing as root (especially problematic when the source
+    directory is on NFS and root is mapped to nobody).
+
+  - 'git-rerere' failed to deal with two unconflicted paths that
+    sorted next to each other.
+
+  - 'git-rerere' attempted to open(2) a symlink and failed if
+    there was a conflict.  Since a conflicting change to a
+    symlink would not benefit from rerere anyway, the command
+    now ignores conflicting changes to symlinks.
+
+  - 'git-repack' did not like to pass more than 64 arguments
+    internally to underlying 'rev-list' logic, which made it
+    impossible to repack after accumulating many (small) packs
+    in the repository.
+
+  - 'git-diff' to review the combined diff during a conflicted
+    merge were not reading the working tree version correctly
+    when changes to a symbolic link conflicted.  It should have
+    read the data using readlink(2) but read from the regular
+    file the symbolic link pointed at.
+
+  - 'git-remote' did not like period in a remote's name.
+
+  - 'git.el' honors the commit coding system from the configuration.
+
+  - 'blameview' in contrib/ correctly digs deeper when a line is
+    clicked.
+
+  - 'http-push' correctly makes sure the remote side has leading
+    path.  Earlier it started in the middle of the path, and
+    incorrectly.
+
+  - 'git-merge' did not exit with non-zero status when the
+    working tree was dirty and cannot fast forward.  It does
+    now.
+
+  - 'cvsexportcommit' does not lose yet-to-be-used message file.
+
+  - int-vs-size_t typefix when running combined diff on files
+    over 2GB long.
+
+  - 'git apply --whitespace=strip' should not touch unmodified
+    lines.
+
+  - 'git-mailinfo' choke when a logical header line was too long.
+
+  - 'git show A..B' did not error out.  Negative ref ("not A" in
+    this example) does not make sense for the purpose of the
+    command, so now it errors out.
+
+  - 'git fmt-merge-msg --file' without file parameter did not
+    correctly error out.
+
+  - 'git archimport' barfed upon encountering a commit without
+    summary.
+
+  - 'git index-pack' did not protect itself from getting a short
+    read out of pread(2).
+
+  - 'git http-push' had a few buffer overruns.
+
+  - Build dependency fixes to rebuild fetch.o when other headers
+    change.
+
+  - git.el does not add duplicate sign-off lines.
+
+  - git-commit shows the full stat of the resulting commit, not
+    just about the files in the current directory, when run from
+    a subdirectory.
+
+  - "git-checkout -m '@{8 hours ago}'" had a funny failure from
+    eval; fixed.
+
+  - git-merge (hence git-pull) did not refuse fast-forwarding
+    when the working tree had local changes that would have
+    conflicted with it.
+
+  - a handful small fixes to gitweb.
+
+  - build procedure for user-manual is fixed not to require locally
+    installed stylesheets.
+
+  - "git commit $paths" on paths whose earlier contents were
+    already updated in the index were failing out.
+
+
+* Tweaks
+
+  - sliding mmap() inefficiently mmaped the same region of a
+    packfile with an access pattern that used objects in the
+    reverse order.  This has been made more efficient.
diff --git a/Documentation/RelNotes-1.5.2.1.txt b/Documentation/RelNotes-1.5.2.1.txt
new file mode 100644 (file)
index 0000000..ebf20e2
--- /dev/null
@@ -0,0 +1,53 @@
+GIT v1.5.2.1 Release Notes
+==========================
+
+Fixes since v1.5.2
+------------------
+
+* Bugfixes
+
+  - Temporary files that are used when invoking external diff
+    programs did not tolerate a long TMPDIR.
+
+  - git-daemon did not notice when it could not write into its
+    pid file.
+
+  - git-status did not honor core.excludesFile configuration like
+    git-add did.
+
+  - git-annotate did not work from a subdirectory while
+    git-blame did.
+
+  - git-cvsserver should have disabled access to a repository
+    with "gitcvs.pserver.enabled = false" set even when
+    "gitcvs.enabled = true" was set at the same time.  It
+    didn't.
+
+  - git-cvsimport did not work correctly in a repository with
+    its branch heads were packed with pack-refs.
+
+  - ident unexpansion to squash "$Id: xxx $" that is in the
+    repository copy removed incorrect number of bytes.
+
+  - git-svn misbehaved when the subversion repository did not
+    provide MD5 checksums for files.
+
+  - git rebase (and git am) misbehaved on commits that have '\n'
+    (literally backslash and en, not a linefeed) in the title.
+
+  - code to decode base85 used in binary patches had one error
+    return codepath wrong.
+
+  - RFC2047 Q encoding output by git-format-patch used '_' for a
+    space, which is not understood by some programs.  It uses =20
+    which is safer.
+
+  - git-fastimport --import-marks was broken; fixed.
+
+  - A lot of documentation updates, clarifications and fixes.
+
+--
+exec >/var/tmp/1
+O=v1.5.2-65-g996e2d6
+echo O=`git describe refs/heads/maint`
+git shortlog --no-merges $O..refs/heads/maint
diff --git a/Documentation/RelNotes-1.5.2.2.txt b/Documentation/RelNotes-1.5.2.2.txt
new file mode 100644 (file)
index 0000000..f6393f8
--- /dev/null
@@ -0,0 +1,61 @@
+GIT v1.5.2.2 Release Notes
+==========================
+
+Fixes since v1.5.2.1
+--------------------
+
+* Usability fix
+
+  - git-gui is shipped with its updated blame interface.  It is
+    rumored that the older one was not just unusable but was
+    active health hazard, but this one is actually pretty.
+    Please see for yourself.
+
+* Bugfixes
+
+  - "git checkout fubar" was utterly confused when there is a
+    branch fubar and a tag fubar at the same time.  It correctly
+    checks out the branch fubar now.
+
+  - "git clone /path/foo" to clone a local /path/foo.git
+    repository left an incorrect configuration.
+
+  - "git send-email" correctly unquotes RFC 2047 quoted names in
+    the patch-email before using their values.
+
+  - We did not accept number of seconds since epoch older than
+    year 2000 as a valid timestamp.  We now interpret positive
+    integers more than 8 digits as such, which allows us to
+    express timestamps more recent than March 1973.
+
+  - git-cvsimport did not work when you have GIT_DIR to point
+    your repository at a nonstandard location.
+
+  - Some systems (notably, Solaris) lack hstrerror() to make
+    h_errno human readable; prepare a replacement
+    implementation.
+
+  - .gitignore file listed git-core.spec but what we generate is
+    git.spec, and nobody noticed for a long time.
+
+  - "git-merge-recursive" does not try to run file level merge
+    on binary files.
+
+  - "git-branch --track" did not create tracking configuration
+    correctly when the branch name had slash in it.
+
+  - The email address of the user specified with user.email
+    configuration was overriden by EMAIL environment variable.
+
+  - The tree parser did not warn about tree entries with
+    nonsense file modes, and assumed they must be blobs.
+
+  - "git log -z" without any other request to generate diff still
+    invoked the diff machinery, wasting cycles.
+
+* Documentation
+
+  - Many updates to fix stale or missing documentation.
+
+  - Although our documentation was primarily meant to be formatted
+    with AsciiDoc7, formatting with AsciiDoc8 is supported better.
diff --git a/Documentation/RelNotes-1.5.2.3.txt b/Documentation/RelNotes-1.5.2.3.txt
new file mode 100644 (file)
index 0000000..addb229
--- /dev/null
@@ -0,0 +1,27 @@
+GIT v1.5.2.3 Release Notes
+==========================
+
+Fixes since v1.5.2.2
+--------------------
+
+ * Bugfixes
+
+   - Version 2 pack index format was introduced in version 1.5.2
+     to support pack files that has offset that cannot be
+     represented in 32-bit.  The runtime code to validate such
+     an index mishandled such an index for an empty pack.
+
+   - Commit walkers (most notably, fetch over http protocol)
+     tried to traverse commit objects contained in trees (aka
+     subproject); they shouldn't.
+
+   - A build option NO_R_TO_GCC_LINKER was not explained in Makefile
+     comment correctly.
+
+ * Documentation Fixes and Updates
+
+   - git-config --regexp was not documented properly.
+
+   - git-repack -a was not documented properly.
+
+   - git-remote -n was not documented properly.
diff --git a/Documentation/RelNotes-1.5.2.4.txt b/Documentation/RelNotes-1.5.2.4.txt
new file mode 100644 (file)
index 0000000..75cff47
--- /dev/null
@@ -0,0 +1,28 @@
+GIT v1.5.2.4 Release Notes
+==========================
+
+Fixes since v1.5.2.3
+--------------------
+
+ * Bugfixes
+
+   - "git-gui" bugfixes, including a handful fixes to run it
+     better on Cygwin/MSYS.
+
+   - "git checkout" failed to switch back and forth between
+     branches, one of which has "frotz -> xyzzy" symlink and
+     file "xyzzy/filfre", while the other one has a file
+     "frotz/filfre".
+
+   - "git prune" used to segfault upon seeing a commit that is
+     referred to by a tree object (aka "subproject").
+
+   - "git diff --name-status --no-index" mishandled an added file.
+
+   - "git apply --reverse --whitespace=warn" still complained
+     about whitespaces that a forward application would have
+     introduced.
+
+ * Documentation Fixes and Updates
+
+   - A handful documentation updates.
diff --git a/Documentation/RelNotes-1.5.2.5.txt b/Documentation/RelNotes-1.5.2.5.txt
new file mode 100644 (file)
index 0000000..e8281c7
--- /dev/null
@@ -0,0 +1,30 @@
+GIT v1.5.2.5 Release Notes
+==========================
+
+Fixes since v1.5.2.4
+--------------------
+
+ * Bugfixes
+
+   - "git add -u" had a serious data corruption problem in one
+     special case (when the changes to a subdirectory's files
+     consist only deletion of files).
+
+   - "git add -u <path>" did not work from a subdirectory.
+
+   - "git apply" left an empty directory after all its files are
+     renamed away.
+
+   - "git $anycmd foo/bar", when there is a file 'foo' in the
+     working tree, complained that "git $anycmd foo/bar --" form
+     should be used to disambiguate between revs and files,
+     which was completely bogus.
+
+   - "git checkout-index" and other commands that checks out
+     files to the work tree tried unlink(2) on directories,
+     which is a sane thing to do on sane systems, but not on
+     Solaris when you are root.
+
+ * Documentation Fixes and Updates
+
+   - A handful documentation fixes.
diff --git a/Documentation/RelNotes-1.5.2.txt b/Documentation/RelNotes-1.5.2.txt
new file mode 100644 (file)
index 0000000..6195715
--- /dev/null
@@ -0,0 +1,197 @@
+GIT v1.5.2 Release Notes
+========================
+
+Updates since v1.5.1
+--------------------
+
+* Plumbing level superproject support.
+
+  You can include a subdirectory that has an independent git
+  repository in your index and tree objects of your project
+  ("superproject").  This plumbing (i.e. "core") level
+  superproject support explicitly excludes recursive behaviour.
+
+  The "subproject" entries in the index and trees of a superproject
+  are incompatible with older versions of git.  Experimenting with
+  the plumbing level support is encouraged, but be warned that
+  unless everybody in your project updates to this release or
+  later, using this feature would make your project
+  inaccessible by people with older versions of git.
+
+* Plumbing level gitattributes support.
+
+  The gitattributes mechanism allows you to add 'attributes' to
+  paths in your project, and affect the way certain git
+  operations work.  Currently you can influence if a path is
+  considered a binary or text (the former would be treated by
+  'git diff' not to produce textual output; the latter can go
+  through the line endings conversion process in repositories
+  with core.autocrlf set), expand and unexpand '$Id$' keyword
+  with blob object name, specify a custom 3-way merge driver,
+  and specify a custom diff driver.  You can also apply
+  arbitrary filter to contents on check-in/check-out codepath
+  but this feature is an extremely sharp-edged razor and needs
+  to be handled with caution (do not use it unless you
+  understand the earlier mailing list discussion on keyword
+  expansion).  These conversions apply when checking files in
+  or out, and exporting via git-archive.
+
+* The packfile format now optionally suports 64-bit index.
+
+  This release supports the "version 2" format of the .idx
+  file.  This is automatically enabled when a huge packfile
+  needs more than 32-bit to express offsets of objects in the
+  pack.
+
+* Comes with an updated git-gui 0.7.1
+
+* Updated gitweb:
+
+  - can show combined diff for merges;
+  - uses font size of user's preference, not hardcoded in pixels;
+  - can now 'grep';
+
+* New commands and options.
+
+  - "git bisect start" can optionally take a single bad commit and
+    zero or more good commits on the command line.
+
+  - "git shortlog" can optionally be told to wrap its output.
+
+  - "subtree" merge strategy allows another project to be merged in as
+    your subdirectory.
+
+  - "git format-patch" learned a new --subject-prefix=<string>
+    option, to override the built-in "[PATCH]".
+
+  - "git add -u" is a quick way to do the first stage of "git
+    commit -a" (i.e. update the index to match the working
+    tree); it obviously does not make a commit.
+
+  - "git clean" honors a new configuration, "clean.requireforce".  When
+    set to true, this makes "git clean" a no-op, preventing you
+    from losing files by typing "git clean" when you meant to
+    say "make clean".  You can still say "git clean -f" to
+    override this.
+
+  - "git log" family of commands learned --date={local,relative,default}
+    option.  --date=relative is synonym to the --relative-date.
+    --date=local gives the timestamp in local timezone.
+
+* Updated behavior of existing commands.
+
+  - When $GIT_COMMITTER_EMAIL or $GIT_AUTHOR_EMAIL is not set
+    but $EMAIL is set, the latter is used as a substitute.
+
+  - "git diff --stat" shows size of preimage and postimage blobs
+    for binary contents.  Earlier it only said "Bin".
+
+  - "git lost-found" shows stuff that are unreachable except
+    from reflogs.
+
+  - "git checkout branch^0" now detaches HEAD at the tip commit
+    on the named branch, instead of just switching to the
+    branch (use "git checkout branch" to switch to the branch,
+    as before).
+
+  - "git bisect next" can be used after giving only a bad commit
+    without giving a good one (this starts bisection half-way to
+    the root commit).  We used to refuse to operate without a
+    good and a bad commit.
+
+  - "git push", when pushing into more than one repository, does
+    not stop at the first error.
+
+  - "git archive" does not insist you to give --format parameter
+    anymore; it defaults to "tar".
+
+  - "git cvsserver" can use backends other than sqlite.
+
+  - "gitview" (in contrib/ section) learned to better support
+    "git-annotate".
+
+  - "git diff $commit1:$path2 $commit2:$path2" can now report
+    mode changes between the two blobs.
+
+  - Local "git fetch" from a repository whose object store is
+    one of the alternates (e.g. fetching from the origin in a
+    repository created with "git clone -l -s") avoids
+    downloading objects unnecessarily.
+
+  - "git blame" uses .mailmap to canonicalize the author name
+    just like "git shortlog" does.
+
+  - "git pack-objects" pays attention to pack.depth
+    configuration variable.
+
+  - "git cherry-pick" and "git revert" does not use .msg file in
+    the working tree to prepare commit message; instead it uses
+    $GIT_DIR/MERGE_MSG as other commands do.
+
+* Builds
+
+  - git-p4import has never been installed; now there is an
+    installation option to do so.
+
+  - gitk and git-gui can be configured out.
+
+  - Generated documentation pages automatically get version
+    information from GIT_VERSION.
+
+  - Parallel build with "make -j" descending into subdirectory
+    was fixed.
+
+* Performance Tweaks
+
+  - Optimized "git-rev-list --bisect" (hence "git-bisect").
+
+  - Optimized "git-add $path" in a large directory, most of
+    whose contents are ignored.
+
+  - Optimized "git-diff-tree" for reduced memory footprint.
+
+  - The recursive merge strategy updated a worktree file that
+    was changed identically in two branches, when one of them
+    renamed it.  We do not do that when there is no rename, so
+    match that behaviour.  This avoids excessive rebuilds.
+
+  - The default pack depth has been increased to 50, as the
+    recent addition of delta_base_cache makes deeper delta chains
+    much less expensive to access.  Depending on the project, it was
+    reported that this reduces the resulting pack file by 10%
+    or so.
+
+
+Fixes since v1.5.1
+------------------
+
+All of the fixes in v1.5.1 maintenance series are included in
+this release, unless otherwise noted.
+
+* Bugfixes
+
+  - Switching branches with "git checkout" refused to work when
+    a path changes from a file to a directory between the
+    current branch and the new branch, in order not to lose
+    possible local changes in the directory that is being turned
+    into a file with the switch.  We now allow such a branch
+    switch after making sure that there is no locally modified
+    file nor un-ignored file in the directory.  This has not
+    been backported to 1.5.1.x series, as it is rather an
+    intrusive change.
+
+  - Merging branches that have a file in one and a directory in
+    another at the same path used to get quite confused.  We
+    handle such a case a bit more carefully, even though that is
+    still left as a conflict for the user to sort out.  This
+    will not be backported to 1.5.1.x series, as it is rather an
+    intrusive change.
+
+  - git-fetch had trouble with a remote with insanely large number
+    of refs.
+
+  - "git clean -d -X" now does not remove non-excluded directories.
+
+  - rebasing (without -m) a series that changes a symlink to a directory
+    in the middle of a path confused git-apply greatly and refused to
+    operate.
diff --git a/Documentation/RelNotes-1.5.3.1.txt b/Documentation/RelNotes-1.5.3.1.txt
new file mode 100644 (file)
index 0000000..7ff546c
--- /dev/null
@@ -0,0 +1,10 @@
+GIT v1.5.3.1 Release Notes
+==========================
+
+Fixes since v1.5.3
+------------------
+
+This is solely to fix the generated RPM's dependencies.  We used
+to have git-p4 package but we do not anymore.  As suggested on
+the mailing list, this release makes git-core "Obsolete" git-p4,
+so that yum update would not complain.
diff --git a/Documentation/RelNotes-1.5.3.2.txt b/Documentation/RelNotes-1.5.3.2.txt
new file mode 100644 (file)
index 0000000..4bbde3c
--- /dev/null
@@ -0,0 +1,58 @@
+GIT v1.5.3.2 Release Notes
+==========================
+
+Fixes since v1.5.3.1
+--------------------
+
+ * git-push sent thin packs by default, which was not good for
+   the public distribution server (no point in saving transfer
+   while pushing; no point in making the resulting pack less
+   optimum).
+
+ * git-svn sometimes terminated with "Malformed network data" when
+   talking over svn:// protocol.
+
+ * git-send-email re-issued the same message-id about 10% of the
+   time if you fired off 30 messages within a single second.
+
+ * git-stash was not terminating the log message of commits it
+   internally creates with LF.
+
+ * git-apply failed to check the size of the patch hunk when its
+   beginning part matched the remainder of the preimage exactly,
+   even though the preimage recorded in the hunk was much larger
+   (therefore the patch should not have applied), leading to a
+   segfault.
+
+ * "git rm foo && git commit foo" complained that 'foo' needs to
+   be added first, instead of committing the removal, which was a
+   nonsense.
+
+ * git grep -c said "/dev/null: 0".
+
+ * git-add -u failed to recognize a blob whose type changed
+   between the index and the work tree.
+
+ * The limit to rename detection has been tightened a lot to
+   reduce performance problems with a huge change.
+
+ * cvsimport and svnimport barfed when the input tried to move
+   a tag.
+
+ * "git apply -pN" did not chop the right number of directories.
+
+ * "git svnimport" did not like SVN tags with funny characters in them.
+
+ * git-gui 0.8.3, with assorted fixes, including:
+
+   - font-chooser on X11 was unusable with large number of fonts;
+   - a diff that contained a deleted symlink made it barf;
+   - an untracked symbolic link to a directory made it fart;
+   - a file with % in its name made it vomit;
+
+
+Documentation updates
+---------------------
+
+User manual has been somewhat restructured.  I think the new
+organization is much easier to read.
diff --git a/Documentation/RelNotes-1.5.3.3.txt b/Documentation/RelNotes-1.5.3.3.txt
new file mode 100644 (file)
index 0000000..2a7bfdd
--- /dev/null
@@ -0,0 +1,31 @@
+GIT v1.5.3.3 Release Notes
+==========================
+
+Fixes since v1.5.3.2
+--------------------
+
+ * git-quiltimport did not like it when a patch described in the
+   series file does not exist.
+
+ * p4 importer missed executable bit in some cases.
+
+ * The default shell on some FreeBSD did not execute the
+   argument parsing code correctly and made git unusable.
+
+ * git-svn incorrectly spawned pager even when the user user
+   explicitly asked not to.
+
+ * sample post-receive hook overquoted the envelope sender
+   value.
+
+ * git-am got confused when the patch contained a change that is
+   only about type and not contents.
+
+ * git-mergetool did not show our and their version of the
+   conflicted file when started from a subdirectory of the
+   project.
+
+ * git-mergetool did not pass correct options when invoking diff3.
+
+ * git-log sometimes invoked underlying "diff" machinery
+   unnecessarily.
diff --git a/Documentation/RelNotes-1.5.3.4.txt b/Documentation/RelNotes-1.5.3.4.txt
new file mode 100644 (file)
index 0000000..b04b3a4
--- /dev/null
@@ -0,0 +1,35 @@
+GIT v1.5.3.4 Release Notes
+==========================
+
+Fixes since v1.5.3.3
+--------------------
+
+ * Change to "git-ls-files" in v1.5.3.3 that was introduced to support
+   partial commit of removal better had a segfaulting bug, which was
+   diagnosed and fixed by Keith and Carl.
+
+ * Performance improvements for rename detection has been backported
+   from the 'master' branch.
+
+ * "git-for-each-ref --format='%(numparent)'" was not working
+   correctly at all, and --format='%(parent)' was not working for
+   merge commits.
+
+ * Sample "post-receive-hook" incorrectly sent out push
+   notification e-mails marked as "From: " the committer of the
+   commit that happened to be at the tip of the branch that was
+   pushed, not from the person who pushed.
+
+ * "git-remote" did not exit non-zero status upon error.
+
+ * "git-add -i" did not respond very well to EOF from tty nor
+   bogus input.
+
+ * "git-rebase -i" squash subcommand incorrectly made the
+   author of later commit the author of resulting commit,
+   instead of taking from the first one in the squashed series.
+
+ * "git-stash apply --index" was not documented.
+
+ * autoconfiguration learned that "ar" command is found as "gas" on
+   some systems.
diff --git a/Documentation/RelNotes-1.5.3.5.txt b/Documentation/RelNotes-1.5.3.5.txt
new file mode 100644 (file)
index 0000000..7ff1d5d
--- /dev/null
@@ -0,0 +1,94 @@
+GIT v1.5.3.5 Release Notes
+==========================
+
+Fixes since v1.5.3.4
+--------------------
+
+ * Comes with git-gui 0.8.4.
+
+ * "git-config" silently ignored options after --list; now it will
+   error out with a usage message.
+
+ * "git-config --file" failed if the argument used a relative path
+   as it changed directories before opening the file.
+
+ * "git-config --file" now displays a proper error message if it
+   cannot read the file specified on the command line.
+
+ * "git-config", "git-diff", "git-apply" failed if run from a
+   subdirectory with relative GIT_DIR and GIT_WORK_TREE set.
+
+ * "git-blame" crashed if run during a merge conflict.
+
+ * "git-add -i" did not handle single line hunks correctly.
+
+ * "git-rebase -i" and "git-stash apply" failed if external diff
+   drivers were used for one or more files in a commit.  They now
+   avoid calling the external diff drivers.
+
+ * "git-log --follow" did not work unless diff generation (e.g. -p)
+   was also requested.
+
+ * "git-log --follow -B" did not work at all.  Fixed.
+
+ * "git-log -M -B" did not correctly handle cases of very large files
+   being renamed and replaced by very small files in the same commit.
+
+ * "git-log" printed extra newlines between commits when a diff
+   was generated internally (e.g. -S or --follow) but not displayed.
+
+ * "git-push" error message is more helpful when pushing to a
+   repository with no matching refs and none specified.
+
+ * "git-push" now respects + (force push) on wildcard refspecs,
+   matching the behavior of git-fetch.
+
+ * "git-filter-branch" now updates the working directory when it
+   has finished filtering the current branch.
+
+ * "git-instaweb" no longer fails on Mac OS X.
+
+ * "git-cvsexportcommit" didn't always create new parent directories
+   before trying to create new child directories.  Fixed.
+
+ * "git-fetch" printed a scary (but bogus) error message while
+   fetching a tag that pointed to a tree or blob.  The error did
+   not impact correctness, only user perception.  The bogus error
+   is no longer printed.
+
+ * "git-ls-files --ignored" did not properly descend into non-ignored
+   directories that themselves contained ignored files if d_type
+   was not supported by the filesystem.  This bug impacted systems
+   such as AFS.  Fixed.
+
+ * Git segfaulted when reading an invalid .gitattributes file.  Fixed.
+
+ * post-receive-email example hook was fixed for non-fast-forward
+   updates.
+
+ * Documentation updates for supported (but previously undocumented)
+   options of "git-archive" and "git-reflog".
+
+ * "make clean" no longer deletes the configure script that ships
+   with the git tarball, making multiple architecture builds easier.
+
+ * "git-remote show origin" spewed a warning message from Perl
+   when no remote is defined for the current branch via
+   branch.<name>.remote configuration settings.
+
+ * Building with NO_PERL_MAKEMAKER excessively rebuilt contents
+   of perl/ subdirectory by rewriting perl.mak.
+
+ * http.sslVerify configuration settings were not used in scripted
+   Porcelains.
+
+ * "git-add" leaked a bit of memory while scanning for files to add.
+
+ * A few workarounds to squelch false warnings from recent gcc have
+   been added.
+
+ * "git-send-pack $remote frotz" segfaulted when there is nothing
+   named 'frotz' on the local end.
+
+ * "git-rebase --interactive" did not handle its "--strategy" option
+   properly.
diff --git a/Documentation/RelNotes-1.5.3.6.txt b/Documentation/RelNotes-1.5.3.6.txt
new file mode 100644 (file)
index 0000000..069a2b2
--- /dev/null
@@ -0,0 +1,48 @@
+GIT v1.5.3.6 Release Notes
+==========================
+
+Fixes since v1.5.3.5
+--------------------
+
+ * git-cvsexportcommit handles root commits better.
+
+ * git-svn dcommit used to clobber when sending a series of
+   patches.
+
+ * git-svn dcommit failed after attempting to rebase when
+   started with a dirty index; now it stops upfront.
+
+ * git-grep sometimes refused to work when your index was
+   unmerged.
+
+ * "git-grep -A1 -B2" acted as if it was told to run "git -A1 -B21".
+
+ * git-hash-object did not honor configuration variables, such as
+   core.compression.
+
+ * git-index-pack choked on a huge pack on 32-bit machines, even when
+   large file offsets are supported.
+
+ * atom feeds from git-web said "10" for the month of November.
+
+ * a memory leak in commit walker was plugged.
+
+ * When git-send-email inserted the original author's From:
+   address in body, it did not mark the message with
+   Content-type: as needed.
+
+ * git-revert and git-cherry-pick incorrectly refused to start
+   when the work tree was dirty.
+
+ * git-clean did not honor core.excludesfile configuration.
+
+ * git-add mishandled ".gitignore" files when applying them to
+   subdirectories.
+
+ * While importing a too branchy history, git-fastimport did not
+   honor delta depth limit properly.
+
+ * Support for zlib implementations that lack ZLIB_VERNUM and definition
+   of deflateBound() has been added.
+
+ * Quite a lot of documentation clarifications.
diff --git a/Documentation/RelNotes-1.5.3.7.txt b/Documentation/RelNotes-1.5.3.7.txt
new file mode 100644 (file)
index 0000000..2f69061
--- /dev/null
@@ -0,0 +1,45 @@
+GIT v1.5.3.7 Release Notes
+==========================
+
+Fixes since v1.5.3.6
+--------------------
+
+ * git-send-email added 8-bit contents to the payload without
+   marking it as 8-bit in a CTE header.
+
+ * "git-bundle create a.bndl HEAD" dereferenced the symref and
+   did not record the ref as 'HEAD'; this prevented a bundle
+   from being used as a normal source of git-clone.
+
+ * The code to reject nonsense command line of the form
+   "git-commit -a paths..." and "git-commit --interactive
+   paths..." were broken.
+
+ * Adding a signature that is not ASCII-only to an original
+   commit that is ASCII-only would make the result non-ASCII.
+   "git-format-patch -s" did not mark such a message correctly
+   with MIME encoding header.
+
+ * git-add sometimes did not mark the resulting index entry
+   stat-clean.  This affected only cases when adding the
+   contents with the same length as the previously staged
+   contents, and the previous staging made the index entry
+   "racily clean".
+
+ * git-commit did not honor GIT_INDEX_FILE the user had in the
+   environment.
+
+ * When checking out a revision, git-checkout did not report where the
+   updated HEAD is if you happened to have a file called HEAD in the
+   work tree.
+
+ * "git-rev-list --objects" mishandled a tree that points at a
+   submodule.
+
+ * "git cvsimport" was not ready for packed refs that "git gc" can
+   produce and gave incorrect results.
+
+ * Many scripted Porcelains were confused when you happened to have a
+   file called "HEAD" in your work tree.
+
+Also it contains updates to the user manual and documentation.
diff --git a/Documentation/RelNotes-1.5.3.txt b/Documentation/RelNotes-1.5.3.txt
new file mode 100644 (file)
index 0000000..d03894b
--- /dev/null
@@ -0,0 +1,366 @@
+GIT v1.5.3 Release Notes
+========================
+
+Updates since v1.5.2
+--------------------
+
+* The commit walkers other than http are officially deprecated,
+  but still supported for now.
+
+* The submodule support has Porcelain layer.
+
+  Note that the current submodule support is minimal and this is
+  deliberately so.  A design decision we made is that operations
+  at the supermodule level do not recurse into submodules by
+  default.  The expectation is that later we would add a
+  mechanism to tell git which submodules the user is interested
+  in, and this information might be used to determine the
+  recursive behaviour of certain commands (e.g. "git checkout"
+  and "git diff"), but currently we haven't agreed on what that
+  mechanism should look like.  Therefore, if you use submodules,
+  you would probably need "git submodule update" on the
+  submodules you care about after running a "git checkout" at
+  the supermodule level.
+
+* There are a handful pack-objects changes to help you cope better
+  with repositories with pathologically large blobs in them.
+
+* For people who need to import from Perforce, a front-end for
+  fast-import is in contrib/fast-import/.
+
+* Comes with git-gui 0.8.2.
+
+* Comes with updated gitk.
+
+* New commands and options.
+
+  - "git log --date=<format>" can use more formats: iso8601, rfc2822.
+
+  - The hunk header output from "git diff" family can be customized
+    with the attributes mechanism.  See gitattributes(5) for details.
+
+  - "git stash" allows you to quickly save away your work in
+    progress and replay it later on an updated state.
+
+  - "git rebase" learned an "interactive" mode that let you
+    pick and reorder which commits to rebuild.
+
+  - "git fsck" can save its findings in $GIT_DIR/lost-found, without a
+    separate invocation of "git lost-found" command.  The blobs stored by
+    lost-found are stored in plain format to allow you to grep in them.
+
+  - $GIT_WORK_TREE environment variable can be used together with
+    $GIT_DIR to work in a subdirectory of a working tree that is
+    not located at "$GIT_DIR/..".
+
+  - Giving "--file=<file>" option to "git config" is the same as
+    running the command with GIT_CONFIG=<file> environment.
+
+  - "git log" learned a new option "--follow", to follow
+    renaming history of a single file.
+
+  - "git filter-branch" lets you rewrite the revision history of
+    specified branches. You can specify a number of filters to
+    modify the commits, files and trees.
+
+  - "git cvsserver" learned new options (--base-path, --export-all,
+    --strict-paths) inspired by "git daemon".
+
+  - "git daemon --base-path-relaxed" can help migrating a repository URL
+    that did not use to use --base-path to use --base-path.
+
+  - "git commit" can use "-t templatefile" option and commit.template
+    configuration variable to prime the commit message given to you in the
+    editor.
+
+  - "git submodule" command helps you manage the projects from
+    the superproject that contain them.
+
+  - In addition to core.compression configuration option,
+    core.loosecompression and pack.compression options can
+    independently tweak zlib compression levels used for loose
+    and packed objects.
+
+  - "git ls-tree -l" shows size of blobs pointed at by the
+    tree entries, similar to "/bin/ls -l".
+
+  - "git rev-list" learned --regexp-ignore-case and
+    --extended-regexp options to tweak its matching logic used
+    for --grep fitering.
+
+  - "git describe --contains" is a handier way to call more
+    obscure command "git name-rev --tags".
+
+  - "git gc --aggressive" tells the command to spend more cycles
+    to optimize the repository harder.
+
+  - "git repack" learned a "window-memory" limit which
+    dynamically reduces the window size to stay within the
+    specified memory usage.
+
+  - "git repack" can be told to split resulting packs to avoid
+    exceeding limit specified with "--max-pack-size".
+
+  - "git fsck" gained --verbose option.  This is really really
+    verbose but it might help you identify exact commit that is
+    corrupt in your repository.
+
+  - "git format-patch" learned --numbered-files option.  This
+    may be useful for MH users.
+
+  - "git format-patch" learned format.subjectprefix configuration
+    variable, which serves the same purpose as "--subject-prefix"
+    option.
+
+  - "git tag -n -l" shows tag annotations while listing tags.
+
+  - "git cvsimport" can optionally use the separate-remote layout.
+
+  - "git blame" can be told to see through commits that change
+    whitespaces and indentation levels with "-w" option.
+
+  - "git send-email" can be told not to thread the messages when
+    sending out more than one patches.
+
+  - "git send-email" can also be told how to find whom to cc the
+    message to for each message via --cc-cmd.
+
+  - "git config" learned NUL terminated output format via -z to
+    help scripts.
+
+  - "git add" learned "--refresh <paths>..." option to selectively refresh
+    the cached stat information.
+
+  - "git init -q" makes the command quieter.
+
+  - "git -p command" now has a cousin of opposite sex, "git --no-pager
+    command".
+
+* Updated behavior of existing commands.
+
+  - "gitweb" can offer multiple snapshot formats.
+
+    ***NOTE*** Unfortunately, this changes the format of the
+    $feature{snapshot}{default} entry in the per-site
+    configuration file 'gitweb_config.perl'.  It used to be a
+    three-element tuple that describe a single format; with the
+    new configuration item format, you only have to say the name
+    of the format ('tgz', 'tbz2' or 'zip').  Please update the
+    your configuration file accordingly.
+
+  - "git clone" uses -l (hardlink files under .git) by default when
+    cloning locally.
+
+  - URL used for "git clone" and friends can specify nonstandard SSH port
+    by using ssh://host:port/path/to/repo syntax.
+
+  - "git bundle create" can now create a bundle without negative refs,
+    i.e. "everything since the beginning up to certain points".
+
+  - "git diff" (but not the plumbing level "git diff-tree") now
+    recursively descends into trees by default.
+
+  - "git diff" does not show differences that come only from
+    stat-dirtiness in the form of "diff --git" header anymore.
+    It runs "update-index --refresh" silently as needed.
+
+  - "git tag -l" used to match tags by globbing its parameter as if it
+    has wildcard '*' on both ends, which made "git tag -l gui" to match
+    tag 'gitgui-0.7.0'; this was very annoying.  You now have to add
+    asterisk on the sides you want to wildcard yourself.
+
+  - The editor to use with many interactive commands can be
+    overridden with GIT_EDITOR environment variable, or if it
+    does not exist, with core.editor configuration variable.  As
+    before, if you have neither, environment variables VISUAL
+    and EDITOR are consulted in this order, and then finally we
+    fall back on "vi".
+
+  - "git rm --cached" does not complain when removing a newly
+    added file from the index anymore.
+
+  - Options to "git log" to affect how --grep/--author options look for
+    given strings now have shorter abbreviations.  -i is for ignore case,
+    and -E is for extended regexp.
+
+  - "git log" learned --log-size to show the number of bytes in
+    the log message part of the output to help qgit.
+
+  - "git log --name-status" does not require you to give "-r" anymore.
+    As a general rule, Porcelain commands should recurse when showing
+    diff.
+
+  - "git format-patch --root A" can be used to format everything
+    since the beginning up to A.  This was supported with
+    "git format-patch --root A A" for a long time, but was not
+    properly documented.
+
+  - "git svn dcommit" retains local merge information.
+
+  - "git svnimport" allows an empty string to be specified as the
+    trunk/ directory.  This is necessary to suck data from a SVN
+    repository that doe not have trunk/ branches/ and tags/ organization
+    at all.
+
+  - "git config" to set values also honors type flags like --bool
+    and --int.
+
+  - core.quotepath configuration can be used to make textual git
+    output to emit most of the characters in the path literally.
+
+  - "git mergetool" chooses its backend more wisely, taking
+    notice of its environment such as use of X, Gnome/KDE, etc.
+
+  - "gitweb" shows merge commits a lot nicer than before.  The
+    default view uses more compact --cc format, while the UI
+    allows to choose normal diff with any parent.
+
+  - snapshot files "gitweb" creates from a repository at
+    $path/$project/.git are more useful.  We use $project part
+    in the filename, which we used to discard.
+
+  - "git cvsimport" creates lightweight tags; there is no
+    interesting information we can record in an annotated tag,
+    and the handcrafted ones the old code created was not
+    properly formed anyway.
+
+  - "git push" pretends that you immediately fetched back from
+    the remote by updating corresponding remote tracking
+    branches if you have any.
+
+  - The diffstat given after a merge (or a pull) honors the
+    color.diff configuration.
+
+  - "git commit --amend" is now compatible with various message source
+    options such as -m/-C/-c/-F.
+
+  - "git apply --whitespace=strip" removes blank lines added at
+    the end of the file.
+
+  - "git fetch" over git native protocols with "-v" option shows
+    connection status, and the IP address of the other end, to
+    help diagnosing problems.
+
+  - We used to have core.legacyheaders configuration, when
+    set to false, allowed git to write loose objects in a format
+    that mimicks the format used by objects stored in packs.  It
+    turns out that this was not so useful.  Although we will
+    continue to read objects written in that format, we do not
+    honor that configuration anymore and create loose objects in
+    the legacy/traditional format.
+
+  - "--find-copies-harder" option to diff family can now be
+    spelled as "-C -C" for brevity.
+
+  - "git mailsplit" (hence "git am") can read from Maildir
+    formatted mailboxes.
+
+  - "git cvsserver" does not barf upon seeing "cvs login"
+    request.
+
+  - "pack-objects" honors "delta" attribute set in
+    .gitattributes.  It does not attempt to deltify blobs that
+    come from paths with delta attribute set to false.
+
+  - "new-workdir" script (in contrib) can now be used with a
+    bare repository.
+
+  - "git mergetool" learned to use gvimdiff.
+
+  - "gitview" (in contrib) has a better blame interface.
+
+  - "git log" and friends did not handle a commit log message
+    that is larger than 16kB; they do now.
+
+  - "--pretty=oneline" output format for "git log" and friends
+    deals with "malformed" commit log messages that have more
+    than one lines in the first paragraph better.  We used to
+    show the first line, cutting the title at mid-sentence; we
+    concatenate them into a single line and treat the result as
+    "oneline".
+
+  - "git p4import" has been demoted to contrib status.  For
+    a superior option, checkout the "git p4" front end to
+    "git fast-import" (also in contrib).  The man page and p4
+    rpm have been removed as well.
+
+  - "git mailinfo" (hence "am") now tries to see if the message
+    is in utf-8 first, instead of assuming iso-8859-1, if
+    incoming e-mail does not say what encoding it is in.
+
+* Builds
+
+  - old-style function definitions (most notably, a function
+    without parameter defined with "func()", not "func(void)")
+    have been eradicated.
+
+  - "git tag" and "git verify-tag" have been rewritten in C.
+
+* Performance Tweaks
+
+  - "git pack-objects" avoids re-deltification cost by caching
+    small enough delta results it creates while looking for the
+    best delta candidates.
+
+  - "git pack-objects" learned a new heuristcs to prefer delta
+    that is shallower in depth over the smallest delta
+    possible.  This improves both overall packfile access
+    performance and packfile density.
+
+  - diff-delta code that is used for packing has been improved
+    to work better on big files.
+
+  - when there are more than one pack files in the repository,
+    the runtime used to try finding an object always from the
+    newest packfile; it now tries the same packfile as we found
+    the object requested the last time, which exploits the
+    locality of references.
+
+  - verifying pack contents done by "git fsck --full" got boost
+    by carefully choosing the order to verify objects in them.
+
+  - "git read-tree -m" to read into an already populated index
+    has been optimized vastly.  The effect of this can be seen
+    when switching branches that have differences in only a
+    handful paths.
+
+  - "git add paths..." and "git commit paths..." has also been
+    heavily optimized.
+
+Fixes since v1.5.2
+------------------
+
+All of the fixes in v1.5.2 maintenance series are included in
+this release, unless otherwise noted.
+
+* Bugfixes
+
+  - "gitweb" had trouble handling non UTF-8 text with older
+    Encode.pm Perl module.
+
+  - "git svn" misparsed the data from the commits in the repository when
+    the user had "color.diff = true" in the configuration.  This has been
+    fixed.
+
+  - There was a case where "git svn dcommit" clobbered changes made on the
+    SVN side while committing multiple changes.
+
+  - "git-write-tree" had a bad interaction with racy-git avoidance and
+    gitattributes mechanisms.
+
+  - "git --bare command" overrode existing GIT_DIR setting and always
+    made it treat the current working directory as GIT_DIR.
+
+  - "git ls-files --error-unmatch" does not complain if you give the
+    same path pattern twice by mistake.
+
+  - "git init" autodetected core.filemode but not core.symlinks, which
+    made a new directory created automatically by "git clone" cumbersome
+    to use on filesystems that require these configurations to be set.
+
+  - "git log" family of commands behaved differently when run as "git
+    log" (no pathspec) and as "git log --" (again, no pathspec).  This
+    inconsistency was introduced somewhere in v1.3.0 series but now has
+    been corrected.
+
+  - "git rebase -m" incorrectly displayed commits that were skipped.
diff --git a/Documentation/RelNotes-1.5.4.txt b/Documentation/RelNotes-1.5.4.txt
new file mode 100644 (file)
index 0000000..c40fd18
--- /dev/null
@@ -0,0 +1,256 @@
+GIT v1.5.4 Release Notes
+========================
+
+Removal
+-------
+
+ * "git svnimport" was removed in favor of "git svn".  It is still there
+   in the source tree (contrib/examples) but unsupported.
+
+
+Deprecation notices
+-------------------
+
+ * Next feature release of git (this change is scheduled for v1.5.5 but
+   it could slip) will by default install dashed form of commands
+   (e.g. "git-commit") outside of users' normal $PATH, and will install
+   only selected commands ("git" itself, and "gitk") in $PATH.  This
+   implies:
+
+   - Using dashed form of git commands (e.g. "git-commit") from the
+     command line has been informally deprecated since early 2006, but
+     now it officially is, and will be removed in the future.  Use
+     dashless form (e.g. "git commit") instead.
+
+   - Using dashed from from your scripts, without first prepending the
+     return value from "git --exec-path" to the scripts' PATH, has been
+     informally deprecated since early 2006, but now it officially is.
+
+   - Use of dashed form with "PATH=$(git --exec-path):$PATH; export
+     PATH" early in your script is not deprecated with this change.
+
+  Users are strongly encouraged to adjust their habits and scripts now
+  to prepare for this.
+
+ * The post-receive hook was introduced in March 2007 to supersede
+   post-update hook, primarily to overcome the command line length
+   limitation of the latter.  Use of post-update hook will be deprecated
+   in future versions of git, perhaps in v1.5.5.
+
+ * "git lost-found" was deprecated in favor of "git fsck"'s --lost-found
+   option, and will be removed in the future.
+
+ * "git peek-remote" is deprecated, as "git ls-remote" was written in C
+   and works for all transports, and will be removed in the future.
+
+
+Updates since v1.5.3
+--------------------
+
+ * Comes with much improved gitk.
+
+ * Comes with "git gui" 0.9.1 with i18n.
+
+ * gitk is now merged as a subdirectory of git.git project, in
+   preparation for its i18n.
+
+ * progress display from many commands are a lot nicer to the eye.
+   Transfer commands show throughput data.
+
+ * many commands that pay attention to per-directory .gitignore now do
+   so lazily, which makes the usual case go much faster.
+
+ * Output processing for '--pretty=format:<user format>' has been
+   optimized.
+
+ * Rename detection of diff family, while detecting exact matches, has
+   been greatly optimized.
+
+ * Rename detection of diff family tries to make more naturally looking
+   pairing.  Earlier if more than one identical rename sources were
+   found in the preimage, they were picked pretty much at random.
+
+ * Value "true" for color.diff and color.status configuration used to
+   mean "always" (even when the output is not going to a terminal).
+   This has been corrected to mean the same thing as "auto".
+
+ * HTTP proxy can be specified per remote repository using
+   remote.*.httpproxy configuration, or global http.proxy configuration
+   variable.
+
+ * Various Perforce importer updates.
+
+ * Example update and post-receive hooks have been improved.
+
+ * Any command that wants to take a commit object name can now use
+   ":/string" syntax to name a commit.
+
+ * "git reset" is now built-in and its output can be squelched with -q.
+
+ * "git send-email" can optionally talk over ssmtp and use SMTP-AUTH.
+
+ * "git rebase" learned --whitespace option.
+
+ * In "git rebase", when you decide not to replay a particular change
+   after the command stopped with a conflict, you can say "git rebase
+   --skip" without first running "git reset --hard", as the command now
+   runs it for you.
+
+ * "git rebase --interactive" mode can now work on detached HEAD.
+
+ * "git rebase" now detaches head during its operation, so after a
+   successful "git rebase" operation, the reflog entry branch@{1} for
+   the current branch points at the commit before the rebase was
+   started.
+
+ * "git rebase -i" also triggers rerere to help your repeated merges.
+
+ * "git merge" can call the "post-merge" hook.
+
+ * "git pack-objects" can optionally run deltification with multiple
+   threads.
+
+ * "git archive" can optionally substitute keywords in files marked with
+   export-subst attribute.
+
+ * "git cherry-pick" made a misguided attempt to repeat the original
+   command line in the generated log message, when told to cherry-pick a
+   commit by naming a tag that points at it.  It does not anymore.
+
+ * "git for-each-ref" learned %(xxxdate:<dateformat>) syntax to show the
+   various date fields in different formats.
+
+ * "git gc --auto" is a low-impact way to automatically run a variant of
+   "git repack" that does not lose unreferenced objects (read: safer
+   than the usual one) after the user accumulates too many loose
+   objects.
+
+ * "git clean" has been rewritten in C.
+
+ * You need to explicitly set clean.requireForce to "false" to allow
+   "git clean" without -f to do any damage (lack of the configuration
+   variable used to mean "do not require -f option to lose untracked
+   files", but we now use the safer default).
+
+ * "git push" learned --dry-run option to show what would happen if a
+   push is run.
+
+ * "git push" does not update a tracking ref on the local side when the
+   remote refused to update the corresponding ref.
+
+ * "git push" learned --mirror option.  This is to push the local refs
+   one-to-one to the remote, and deletes refs from the remote that do
+   not exist anymore in the repository on the pushing side.
+
+ * "git push" can remove a corrupt ref at the remote site with the usual
+   ":ref" refspec.
+
+ * "git remote" knows --mirror mode.  This is to set up configuration to
+   push into a remote repository to store local branch heads to the same
+   branch on the remote side, and remove branch heads locally removed
+   from local repository at the same time.  Suitable for pushing into a
+   back-up repository.
+
+ * "git remote" learned "rm" subcommand.
+
+ * "git cvsserver" can be run via "git shell".
+
+ * "git am" and "git rebase" are far less verbose.
+
+ * "git pull" learned to pass --[no-]ff option to underlying "git
+   merge".
+
+ * "git pull --rebase" is a different way to integrate what you fetched
+   into your current branch.
+
+ * "git fast-export" produces datastream that can be fed to fast-import
+   to reproduce the history recorded in a git repository.
+
+ * "git commit --allow-empty" allows you to create a single-parent
+   commit that records the same tree as its parent, overriding the usual
+   safety valve.
+
+ * "git commit --amend" can amend a merge that does not change the tree
+   from its first parent.
+
+ * "git stash random-text" does not create a new stash anymore.  It was
+   a UI mistake.  Use "git stash save random-text", or "git stash"
+   (without extra args) for that.
+
+ * "git prune --expire <time>" can exempt young loose objects from
+   getting pruned.
+
+ * "git branch --contains <commit>" can list branches that are
+   descendants of a given commit.
+
+ * "git log" learned --early-output option to help interactive GUI
+   implementations.
+
+ * "git bisect" learned "skip" action to mark untestable commits.
+
+ * "git format-patch" learned "format.numbered" configuration variable
+   to automatically turn --numbered option on when more than one commits
+   are formatted.
+
+ * "git ls-files" learned "--exclude-standard" to use the canned set of
+   exclude files.
+
+ * "git tag -a -f existing" begins the editor session using the existing
+   annotation message.
+
+ * "git tag -m one -m bar" (multiple -m options) behaves similarly to
+   "git commit"; the parameters to -m options are formatted as separate
+   paragraphs.
+
+ * "git cvsexportcommit" learned -w option to specify and switch to the
+   CVS working directory.
+
+ * "git checkout" from a subdirectory learned to use "../path" to allow
+   checking out a path outside the current directory without cd'ing up.
+
+ * "git send-email --dry-run" shows full headers for easier diagnosis.
+
+ * "git merge-ours" is now built-in.
+
+ * "git svn" learned "info" and "show-externals" subcommands.
+
+ * "git svn" run from a subdirectory failed to read settings from the
+   .git/config.
+
+ * "git svn" learned --use-log-author option, which picks up more
+   descriptive name from From: and Signed-off-by: lines in the commit
+   message.
+
+ * "git status" from a subdirectory now shows relative paths which makes
+   copy-and-pasting for git-checkout/git-add/git-rm easier.
+
+ * "git checkout" from and to detached HEAD leaves a bit more
+   information in the reflog.
+
+ * In addition there are quite a few internal clean-ups. Notably
+
+   - many fork/exec have been replaced with run-command API,
+     brought from the msysgit effort.
+
+   - introduction and more use of the option parser API.
+
+   - enhancement and more use of the strbuf API.
+
+
+Fixes since v1.5.3
+------------------
+
+All of the fixes in v1.5.3 maintenance series are included in
+this release, unless otherwise noted.
+
+These fixes are only in v1.5.4 and not backported to v1.5.3 maintenance
+series.
+
+ * "git svn" talking with the SVN over http will correctly quote branch
+   and project names.
+
+--
+exec >/var/tmp/1
+O=v1.5.3.7-1003-gf38ca7c
+echo O=`git describe refs/heads/master`
+git shortlog --no-merges $O..refs/heads/master ^refs/heads/maint
diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
new file mode 100644 (file)
index 0000000..de08d09
--- /dev/null
@@ -0,0 +1,404 @@
+Checklist (and a short version for the impatient):
+
+       Commits:
+
+       - make commits of logical units
+       - check for unnecessary whitespace with "git diff --check"
+         before committing
+       - do not check in commented out code or unneeded files
+       - provide a meaningful commit message
+       - the first line of the commit message should be a short
+         description and should skip the full stop
+       - if you want your work included in git.git, add a
+         "Signed-off-by: Your Name <you@example.com>" line to the
+         commit message (or just use the option "-s" when
+         committing) to confirm that you agree to the Developer's
+         Certificate of Origin
+       - make sure that you have tests for the bug you are fixing
+       - make sure that the test suite passes after your commit
+
+       Patch:
+
+       - use "git format-patch -M" to create the patch
+       - do not PGP sign your patch
+       - do not attach your patch, but read in the mail
+         body, unless you cannot teach your mailer to
+         leave the formatting of the patch alone.
+       - be careful doing cut & paste into your mailer, not to
+         corrupt whitespaces.
+       - provide additional information (which is unsuitable for
+         the commit message) between the "---" and the diffstat
+       - if you change, add, or remove a command line option or
+         make some other user interface change, the associated
+         documentation should be updated as well.
+       - if your name is not writable in ASCII, make sure that
+         you send off a message in the correct encoding.
+       - send the patch to the list (git@vger.kernel.org) and the
+         maintainer (gitster@pobox.com). If you use
+         git-send-email(1), please test it first by sending
+         email to yourself.
+
+Long version:
+
+I started reading over the SubmittingPatches document for Linux
+kernel, primarily because I wanted to have a document similar to
+it for the core GIT to make sure people understand what they are
+doing when they write "Signed-off-by" line.
+
+But the patch submission requirements are a lot more relaxed
+here on the technical/contents front, because the core GIT is
+thousand times smaller ;-).  So here is only the relevant bits.
+
+
+(1) Make separate commits for logically separate changes.
+
+Unless your patch is really trivial, you should not be sending
+out a patch that was generated between your working tree and
+your commit head.  Instead, always make a commit with complete
+commit message and generate a series of patches from your
+repository.  It is a good discipline.
+
+Describe the technical detail of the change(s).
+
+If your description starts to get too long, that's a sign that you
+probably need to split up your commit to finer grained pieces.
+
+Oh, another thing.  I am picky about whitespaces.  Make sure your
+changes do not trigger errors with the sample pre-commit hook shipped
+in templates/hooks--pre-commit.  To help ensure this does not happen,
+run git diff --check on your changes before you commit.
+
+
+(1a) Try to be nice to older C compilers
+
+We try to support wide range of C compilers to compile
+git with. That means that you should not use C99 initializers, even
+if a lot of compilers grok it.
+
+Also, variables have to be declared at the beginning of the block
+(you can check this with gcc, using the -Wdeclaration-after-statement
+option).
+
+Another thing: NULL pointers shall be written as NULL, not as 0.
+
+
+(2) Generate your patch using git tools out of your commits.
+
+git based diff tools (git, Cogito, and StGIT included) generate
+unidiff which is the preferred format.
+
+You do not have to be afraid to use -M option to "git diff" or
+"git format-patch", if your patch involves file renames.  The
+receiving end can handle them just fine.
+
+Please make sure your patch does not include any extra files
+which do not belong in a patch submission.  Make sure to review
+your patch after generating it, to ensure accuracy.  Before
+sending out, please make sure it cleanly applies to the "master"
+branch head.  If you are preparing a work based on "next" branch,
+that is fine, but please mark it as such.
+
+
+(3) Sending your patches.
+
+People on the git mailing list need to be able to read and
+comment on the changes you are submitting.  It is important for
+a developer to be able to "quote" your changes, using standard
+e-mail tools, so that they may comment on specific portions of
+your code.  For this reason, all patches should be submitted
+"inline".  WARNING: Be wary of your MUAs word-wrap
+corrupting your patch.  Do not cut-n-paste your patch; you can
+lose tabs that way if you are not careful.
+
+It is a common convention to prefix your subject line with
+[PATCH].  This lets people easily distinguish patches from other
+e-mail discussions.
+
+"git format-patch" command follows the best current practice to
+format the body of an e-mail message.  At the beginning of the
+patch should come your commit message, ending with the
+Signed-off-by: lines, and a line that consists of three dashes,
+followed by the diffstat information and the patch itself.  If
+you are forwarding a patch from somebody else, optionally, at
+the beginning of the e-mail message just before the commit
+message starts, you can put a "From: " line to name that person.
+
+You often want to add additional explanation about the patch,
+other than the commit message itself.  Place such "cover letter"
+material between the three dash lines and the diffstat.
+
+Do not attach the patch as a MIME attachment, compressed or not.
+Do not let your e-mail client send quoted-printable.  Do not let
+your e-mail client send format=flowed which would destroy
+whitespaces in your patches. Many
+popular e-mail applications will not always transmit a MIME
+attachment as plain text, making it impossible to comment on
+your code.  A MIME attachment also takes a bit more time to
+process.  This does not decrease the likelihood of your
+MIME-attached change being accepted, but it makes it more likely
+that it will be postponed.
+
+Exception:  If your mailer is mangling patches then someone may ask
+you to re-send them using MIME, that is OK.
+
+Do not PGP sign your patch, at least for now.  Most likely, your
+maintainer or other people on the list would not have your PGP
+key and would not bother obtaining it anyway.  Your patch is not
+judged by who you are; a good patch from an unknown origin has a
+far better chance of being accepted than a patch from a known,
+respected origin that is done poorly or does incorrect things.
+
+If you really really really really want to do a PGP signed
+patch, format it as "multipart/signed", not a text/plain message
+that starts with '-----BEGIN PGP SIGNED MESSAGE-----'.  That is
+not a text/plain, it's something else.
+
+Note that your maintainer does not necessarily read everything
+on the git mailing list.  If your patch is for discussion first,
+send it "To:" the mailing list, and optionally "cc:" him.  If it
+is trivially correct or after the list reached a consensus, send
+it "To:" the maintainer and optionally "cc:" the list.
+
+Also note that your maintainer does not actively involve himself in
+maintaining what are in contrib/ hierarchy.  When you send fixes and
+enhancements to them, do not forget to "cc: " the person who primarily
+worked on that hierarchy in contrib/.
+
+
+(4) Sign your work
+
+To improve tracking of who did what, we've borrowed the
+"sign-off" procedure from the Linux kernel project on patches
+that are being emailed around.  Although core GIT is a lot
+smaller project it is a good discipline to follow it.
+
+The sign-off is a simple line at the end of the explanation for
+the patch, which certifies that you wrote it or otherwise have
+the right to pass it on as a open-source patch.  The rules are
+pretty simple: if you can certify the below:
+
+        Developer's Certificate of Origin 1.1
+
+        By making a contribution to this project, I certify that:
+
+        (a) The contribution was created in whole or in part by me and I
+            have the right to submit it under the open source license
+            indicated in the file; or
+
+        (b) The contribution is based upon previous work that, to the best
+            of my knowledge, is covered under an appropriate open source
+            license and I have the right under that license to submit that
+            work with modifications, whether created in whole or in part
+            by me, under the same open source license (unless I am
+            permitted to submit under a different license), as indicated
+            in the file; or
+
+        (c) The contribution was provided directly to me by some other
+            person who certified (a), (b) or (c) and I have not modified
+            it.
+
+       (d) I understand and agree that this project and the contribution
+           are public and that a record of the contribution (including all
+           personal information I submit with it, including my sign-off) is
+           maintained indefinitely and may be redistributed consistent with
+           this project or the open source license(s) involved.
+
+then you just add a line saying
+
+       Signed-off-by: Random J Developer <random@developer.example.org>
+
+This line can be automatically added by git if you run the git-commit
+command with the -s option.
+
+Some people also put extra tags at the end.  They'll just be ignored for
+now, but you can do this to mark internal company procedures or just
+point out some special detail about the sign-off.
+
+
+------------------------------------------------
+MUA specific hints
+
+Some of patches I receive or pick up from the list share common
+patterns of breakage.  Please make sure your MUA is set up
+properly not to corrupt whitespaces.  Here are two common ones
+I have seen:
+
+* Empty context lines that do not have _any_ whitespace.
+
+* Non empty context lines that have one extra whitespace at the
+  beginning.
+
+One test you could do yourself if your MUA is set up correctly is:
+
+* Send the patch to yourself, exactly the way you would, except
+  To: and Cc: lines, which would not contain the list and
+  maintainer address.
+
+* Save that patch to a file in UNIX mailbox format.  Call it say
+  a.patch.
+
+* Try to apply to the tip of the "master" branch from the
+  git.git public repository:
+
+    $ git fetch http://kernel.org/pub/scm/git/git.git master:test-apply
+    $ git checkout test-apply
+    $ git reset --hard
+    $ git am a.patch
+
+If it does not apply correctly, there can be various reasons.
+
+* Your patch itself does not apply cleanly.  That is _bad_ but
+  does not have much to do with your MUA.  Please rebase the
+  patch appropriately.
+
+* Your MUA corrupted your patch; "am" would complain that
+  the patch does not apply.  Look at .dotest/ subdirectory and
+  see what 'patch' file contains and check for the common
+  corruption patterns mentioned above.
+
+* While you are at it, check what are in 'info' and
+  'final-commit' files as well.  If what is in 'final-commit' is
+  not exactly what you would want to see in the commit log
+  message, it is very likely that your maintainer would end up
+  hand editing the log message when he applies your patch.
+  Things like "Hi, this is my first patch.\n", if you really
+  want to put in the patch e-mail, should come after the
+  three-dash line that signals the end of the commit message.
+
+
+Pine
+----
+
+(Johannes Schindelin)
+
+I don't know how many people still use pine, but for those poor
+souls it may be good to mention that the quell-flowed-text is
+needed for recent versions.
+
+... the "no-strip-whitespace-before-send" option, too. AFAIK it
+was introduced in 4.60.
+
+(Linus Torvalds)
+
+And 4.58 needs at least this.
+
+---
+diff-tree 8326dd8350be64ac7fc805f6563a1d61ad10d32c (from e886a61f76edf5410573e92e38ce22974f9c40f1)
+Author: Linus Torvalds <torvalds@g5.osdl.org>
+Date:   Mon Aug 15 17:23:51 2005 -0700
+
+    Fix pine whitespace-corruption bug
+
+    There's no excuse for unconditionally removing whitespace from
+    the pico buffers on close.
+
+diff --git a/pico/pico.c b/pico/pico.c
+--- a/pico/pico.c
++++ b/pico/pico.c
+@@ -219,7 +219,9 @@ PICO *pm;
+           switch(pico_all_done){      /* prepare for/handle final events */
+             case COMP_EXIT :          /* already confirmed */
+               packheader();
++#if 0
+               stripwhitespace();
++#endif
+               c |= COMP_EXIT;
+               break;
+
+
+(Daniel Barkalow)
+
+> A patch to SubmittingPatches, MUA specific help section for
+> users of Pine 4.63 would be very much appreciated.
+
+Ah, it looks like a recent version changed the default behavior to do the
+right thing, and inverted the sense of the configuration option. (Either
+that or Gentoo did it.) So you need to set the
+"no-strip-whitespace-before-send" option, unless the option you have is
+"strip-whitespace-before-send", in which case you should avoid checking
+it.
+
+
+Thunderbird
+-----------
+
+(A Large Angry SCM)
+
+Here are some hints on how to successfully submit patches inline using
+Thunderbird.
+
+This recipe appears to work with the current [*1*] Thunderbird from Suse.
+
+The following Thunderbird extensions are needed:
+       AboutConfig 0.5
+               http://aboutconfig.mozdev.org/
+       External Editor 0.7.2
+               http://globs.org/articles.php?lng=en&pg=8
+
+1) Prepare the patch as a text file using your method of choice.
+
+2) Before opening a compose window, use Edit->Account Settings to
+uncheck the "Compose messages in HTML format" setting in the
+"Composition & Addressing" panel of the account to be used to send the
+patch. [*2*]
+
+3) In the main Thunderbird window, _before_ you open the compose window
+for the patch, use Tools->about:config to set the following to the
+indicated values:
+       mailnews.send_plaintext_flowed  => false
+       mailnews.wraplength             => 0
+
+4) Open a compose window and click the external editor icon.
+
+5) In the external editor window, read in the patch file and exit the
+editor normally.
+
+6) Back in the compose window: Add whatever other text you wish to the
+message, complete the addressing and subject fields, and press send.
+
+7) Optionally, undo the about:config/account settings changes made in
+steps 2 & 3.
+
+
+[Footnotes]
+*1* Version 1.0 (20041207) from the MozillaThunderbird-1.0-5 rpm of Suse
+9.3 professional updates.
+
+*2* It may be possible to do this with about:config and the following
+settings but I haven't tried, yet.
+       mail.html_compose                       => false
+       mail.identity.default.compose_html      => false
+       mail.identity.id?.compose_html          => false
+
+
+Gnus
+----
+
+'|' in the *Summary* buffer can be used to pipe the current
+message to an external program, and this is a handy way to drive
+"git am".  However, if the message is MIME encoded, what is
+piped into the program is the representation you see in your
+*Article* buffer after unwrapping MIME.  This is often not what
+you would want for two reasons.  It tends to screw up non ASCII
+characters (most notably in people's names), and also
+whitespaces (fatal in patches).  Running 'C-u g' to display the
+message in raw form before using '|' to run the pipe can work
+this problem around.
+
+
+KMail
+-----
+
+This should help you to submit patches inline using KMail.
+
+1) Prepare the patch as a text file.
+
+2) Click on New Mail.
+
+3) Go under "Options" in the Composer window and be sure that
+"Word wrap" is not set.
+
+4) Use Message -> Insert file... and insert the patch.
+
+5) Back in the compose window: add whatever other text you wish to the
+message, complete the addressing and subject fields, and press send.
diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf
new file mode 100644 (file)
index 0000000..99d8874
--- /dev/null
@@ -0,0 +1,66 @@
+## gitlink: macro
+#
+# Usage: gitlink:command[manpage-section]
+#
+# Note, {0} is the manpage section, while {target} is the command.
+#
+# Show GIT link as: <command>(<section>); if section is defined, else just show
+# the command.
+
+[attributes]
+plus=&#43;
+caret=&#94;
+startsb=&#91;
+endsb=&#93;
+tilde=&#126;
+
+ifdef::backend-docbook[]
+[gitlink-inlinemacro]
+{0%{target}}
+{0#<citerefentry>}
+{0#<refentrytitle>{target}</refentrytitle><manvolnum>{0}</manvolnum>}
+{0#</citerefentry>}
+endif::backend-docbook[]
+
+ifdef::backend-docbook[]
+ifndef::docbook-xsl-172[]
+# "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this.
+# v1.72 breaks with this because it replaces dots not in roff requests.
+[listingblock]
+<example><title>{title}</title>
+<literallayout>
+ifdef::doctype-manpage[]
+&#10;.ft C&#10;
+endif::doctype-manpage[]
+|
+ifdef::doctype-manpage[]
+&#10;.ft&#10;
+endif::doctype-manpage[]
+</literallayout>
+{title#}</example>
+endif::docbook-xsl-172[]
+endif::backend-docbook[]
+
+ifdef::doctype-manpage[]
+ifdef::backend-docbook[]
+[header]
+template::[header-declarations]
+<refentry>
+<refmeta>
+<refentrytitle>{mantitle}</refentrytitle>
+<manvolnum>{manvolnum}</manvolnum>
+<refmiscinfo class="source">Git</refmiscinfo>
+<refmiscinfo class="version">{git_version}</refmiscinfo>
+<refmiscinfo class="manual">Git Manual</refmiscinfo>
+</refmeta>
+<refnamediv>
+  <refname>{manname}</refname>
+  <refpurpose>{manpurpose}</refpurpose>
+</refnamediv>
+endif::backend-docbook[]
+endif::doctype-manpage[]
+
+ifdef::backend-xhtml11[]
+[gitlink-inlinemacro]
+<a href="{target}.html">{target}{0?({0})}</a>
+endif::backend-xhtml11[]
diff --git a/Documentation/blame-options.txt b/Documentation/blame-options.txt
new file mode 100644 (file)
index 0000000..17379f0
--- /dev/null
@@ -0,0 +1,87 @@
+-b::
+       Show blank SHA-1 for boundary commits.  This can also
+       be controlled via the `blame.blankboundary` config option.
+
+--root::
+       Do not treat root commits as boundaries.  This can also be
+       controlled via the `blame.showroot` config option.
+
+--show-stats::
+       Include additional statistics at the end of blame output.
+
+-L <start>,<end>::
+       Annotate only the given line range.  <start> and <end> can take
+       one of these forms:
+
+       - number
++
+If <start> or <end> is a number, it specifies an
+absolute line number (lines count from 1).
++
+
+- /regex/
++
+This form will use the first line matching the given
+POSIX regex.  If <end> is a regex, it will search
+starting at the line given by <start>.
++
+
+- +offset or -offset
++
+This is only valid for <end> and will specify a number
+of lines before or after the line given by <start>.
++
+
+-l::
+       Show long rev (Default: off).
+
+-t::
+       Show raw timestamp (Default: off).
+
+-S <revs-file>::
+       Use revs from revs-file instead of calling gitlink:git-rev-list[1].
+
+-p, --porcelain::
+       Show in a format designed for machine consumption.
+
+--incremental::
+       Show the result incrementally in a format designed for
+       machine consumption.
+
+--contents <file>::
+       When <rev> is not specified, the command annotates the
+       changes starting backwards from the working tree copy.
+       This flag makes the command pretend as if the working
+       tree copy has the contents of he named file (specify
+       `-` to make the command read from the standard input).
+
+-M|<num>|::
+       Detect moving lines in the file as well.  When a commit
+       moves a block of lines in a file (e.g. the original file
+       has A and then B, and the commit changes it to B and
+       then A), traditional 'blame' algorithm typically blames
+       the lines that were moved up (i.e. B) to the parent and
+       assigns blame to the lines that were moved down (i.e. A)
+       to the child commit.  With this option, both groups of lines
+       are blamed on the parent.
++
+<num> is optional but it is the lower bound on the number of
+alphanumeric characters that git must detect as moving
+within a file for it to associate those lines with the parent
+commit.
+
+-C|<num>|::
+       In addition to `-M`, detect lines copied from other
+       files that were modified in the same commit.  This is
+       useful when you reorganize your program and move code
+       around across files.  When this option is given twice,
+       the command looks for copies from all other files in the
+       parent for the commit that creates the file in addition.
++
+<num> is optional but it is the lower bound on the number of
+alphanumeric characters that git must detect as moving
+between files for it to associate those lines with the parent
+commit.
+
+-h, --help::
+       Show help message.
diff --git a/Documentation/build-docdep.perl b/Documentation/build-docdep.perl
new file mode 100755 (executable)
index 0000000..ba4205e
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/perl
+
+my %include = ();
+my %included = ();
+
+for my $text (<*.txt>) {
+    open I, '<', $text || die "cannot read: $text";
+    while (<I>) {
+       if (/^include::/) {
+           chomp;
+           s/^include::\s*//;
+           s/\[\]//;
+           $include{$text}{$_} = 1;
+           $included{$_} = 1;
+       }
+    }
+    close I;
+}
+
+# Do we care about chained includes???
+my $changed = 1;
+while ($changed) {
+    $changed = 0;
+    while (my ($text, $included) = each %include) {
+       for my $i (keys %$included) {
+           # $text has include::$i; if $i includes $j
+           # $text indirectly includes $j.
+           if (exists $include{$i}) {
+               for my $j (keys %{$include{$i}}) {
+                   if (!exists $include{$text}{$j}) {
+                       $include{$text}{$j} = 1;
+                       $included{$j} = 1;
+                       $changed = 1;
+                   }
+               }
+           }
+       }
+    }
+}
+
+while (my ($text, $included) = each %include) {
+    if (! exists $included{$text} &&
+       (my $base = $text) =~ s/\.txt$//) {
+       print "$base.html $base.xml : ", join(" ", keys %$included), "\n";
+    }
+}
diff --git a/Documentation/callouts.xsl b/Documentation/callouts.xsl
new file mode 100644 (file)
index 0000000..6a361a2
--- /dev/null
@@ -0,0 +1,30 @@
+<!-- callout.xsl: converts asciidoc callouts to man page format -->
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
+<xsl:template match="co">
+       <xsl:value-of select="concat('\fB(',substring-after(@id,'-'),')\fR')"/>
+</xsl:template>
+<xsl:template match="calloutlist">
+       <xsl:text>.sp&#10;</xsl:text>
+       <xsl:apply-templates/>
+       <xsl:text>&#10;</xsl:text>
+</xsl:template>
+<xsl:template match="callout">
+       <xsl:value-of select="concat('\fB',substring-after(@arearefs,'-'),'. \fR')"/>
+       <xsl:apply-templates/>
+       <xsl:text>.br&#10;</xsl:text>
+</xsl:template>
+
+<!-- sorry, this is not about callouts, but attempts to work around
+ spurious .sp at the tail of the line docbook stylesheets seem to add -->
+<xsl:template match="simpara">
+  <xsl:variable name="content">
+    <xsl:apply-templates/>
+  </xsl:variable>
+  <xsl:value-of select="normalize-space($content)"/>
+  <xsl:if test="not(ancestor::authorblurb) and
+                not(ancestor::personblurb)">
+    <xsl:text>&#10;&#10;</xsl:text>
+  </xsl:if>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/Documentation/cmd-list.perl b/Documentation/cmd-list.perl
new file mode 100755 (executable)
index 0000000..c2d55cd
--- /dev/null
@@ -0,0 +1,74 @@
+#!/usr/bin/perl -w
+
+use File::Compare qw(compare);
+
+sub format_one {
+       my ($out, $nameattr) = @_;
+       my ($name, $attr) = @$nameattr;
+       my ($state, $description);
+       $state = 0;
+       open I, '<', "$name.txt" or die "No such file $name.txt";
+       while (<I>) {
+               if (/^NAME$/) {
+                       $state = 1;
+                       next;
+               }
+               if ($state == 1 && /^----$/) {
+                       $state = 2;
+                       next;
+               }
+               next if ($state != 2);
+               chomp;
+               $description = $_;
+               last;
+       }
+       close I;
+       if (!defined $description) {
+               die "No description found in $name.txt";
+       }
+       if (my ($verify_name, $text) = ($description =~ /^($name) - (.*)/)) {
+               print $out "gitlink:$name\[1\]::\n\t";
+               if ($attr =~ / deprecated /) {
+                       print $out "(deprecated) ";
+               }
+               print $out "$text.\n\n";
+       }
+       else {
+               die "Description does not match $name: $description";
+       }
+}
+
+my %cmds = ();
+for (sort <>) {
+       next if /^#/;
+
+       chomp;
+       my ($name, $cat, $attr) = /^(\S+)\s+(.*?)(?:\s+(.*))?$/;
+       $attr = '' unless defined $attr;
+       push @{$cmds{$cat}}, [$name, " $attr "];
+}
+
+for my $cat (qw(ancillaryinterrogators
+               ancillarymanipulators
+               mainporcelain
+               plumbinginterrogators
+               plumbingmanipulators
+               synchingrepositories
+               foreignscminterface
+               purehelpers
+               synchelpers)) {
+       my $out = "cmds-$cat.txt";
+       open O, '>', "$out+" or die "Cannot open output file $out+";
+       for (@{$cmds{$cat}}) {
+               format_one(\*O, $_);
+       }
+       close O;
+
+       if (-f "$out" && compare("$out", "$out+") == 0) {
+               unlink "$out+";
+       }
+       else {
+               print STDERR "$out\n";
+               rename "$out+", "$out";
+       }
+}
diff --git a/Documentation/config.txt b/Documentation/config.txt
new file mode 100644 (file)
index 0000000..736fcd7
--- /dev/null
@@ -0,0 +1,814 @@
+CONFIGURATION FILE
+------------------
+
+The git configuration file contains a number of variables that affect
+the git command's behavior. `.git/config` file for each repository
+is used to store the information for that repository, and
+`$HOME/.gitconfig` is used to store per user information to give
+fallback values for `.git/config` file. The file `/etc/gitconfig`
+can be used to store system-wide defaults.
+
+They can be used by both the git plumbing
+and the porcelains. The variables are divided into sections, where
+in the fully qualified variable name the variable itself is the last
+dot-separated segment and the section name is everything before the last
+dot. The variable names are case-insensitive and only alphanumeric
+characters are allowed. Some variables may appear multiple times.
+
+Syntax
+~~~~~~
+
+The syntax is fairly flexible and permissive; whitespaces are mostly
+ignored.  The '#' and ';' characters begin comments to the end of line,
+blank lines are ignored.
+
+The file consists of sections and variables.  A section begins with
+the name of the section in square brackets and continues until the next
+section begins.  Section names are not case sensitive.  Only alphanumeric
+characters, '`-`' and '`.`' are allowed in section names.  Each variable
+must belong to some section, which means that there must be section
+header before first setting of a variable.
+
+Sections can be further divided into subsections.  To begin a subsection
+put its name in double quotes, separated by space from the section name,
+in the section header, like in example below:
+
+--------
+       [section "subsection"]
+
+--------
+
+Subsection names can contain any characters except newline (doublequote
+'`"`' and backslash have to be escaped as '`\"`' and '`\\`',
+respectively) and are case sensitive.  Section header cannot span multiple
+lines.  Variables may belong directly to a section or to a given subsection.
+You can have `[section]` if you have `[section "subsection"]`, but you
+don't need to.
+
+There is also (case insensitive) alternative `[section.subsection]` syntax.
+In this syntax subsection names follow the same restrictions as for section
+name.
+
+All the other lines are recognized as setting variables, in the form
+'name = value'.  If there is no equal sign on the line, the entire line
+is taken as 'name' and the variable is recognized as boolean "true".
+The variable names are case-insensitive and only alphanumeric
+characters and '`-`' are allowed.  There can be more than one value
+for a given variable; we say then that variable is multivalued.
+
+Leading and trailing whitespace in a variable value is discarded.
+Internal whitespace within a variable value is retained verbatim.
+
+The values following the equals sign in variable assign are all either
+a string, an integer, or a boolean.  Boolean values may be given as yes/no,
+0/1 or true/false.  Case is not significant in boolean values, when
+converting value to the canonical form using '--bool' type specifier;
+`git-config` will ensure that the output is "true" or "false".
+
+String values may be entirely or partially enclosed in double quotes.
+You need to enclose variable value in double quotes if you want to
+preserve leading or trailing whitespace, or if variable value contains
+beginning of comment characters (if it contains '#' or ';').
+Double quote '`"`' and backslash '`\`' characters in variable value must
+be escaped: use '`\"`' for '`"`' and '`\\`' for '`\`'.
+
+The following escape sequences (beside '`\"`' and '`\\`') are recognized:
+'`\n`' for newline character (NL), '`\t`' for horizontal tabulation (HT, TAB)
+and '`\b`' for backspace (BS).  No other char escape sequence, nor octal
+char sequences are valid.
+
+Variable value ending in a '`\`' is continued on the next line in the
+customary UNIX fashion.
+
+Some variables may require special value format.
+
+Example
+~~~~~~~
+
+       # Core variables
+       [core]
+               ; Don't trust file modes
+               filemode = false
+
+       # Our diff algorithm
+       [diff]
+               external = "/usr/local/bin/gnu-diff -u"
+               renames = true
+
+       [branch "devel"]
+               remote = origin
+               merge = refs/heads/devel
+
+       # Proxy settings
+       [core]
+               gitProxy="ssh" for "kernel.org"
+               gitProxy=default-proxy ; for the rest
+
+Variables
+~~~~~~~~~
+
+Note that this list is non-comprehensive and not necessarily complete.
+For command-specific variables, you will find a more detailed description
+in the appropriate manual page. You will find a description of non-core
+porcelain configuration variables in the respective porcelain documentation.
+
+core.fileMode::
+       If false, the executable bit differences between the index and
+       the working copy are ignored; useful on broken filesystems like FAT.
+       See gitlink:git-update-index[1]. True by default.
+
+core.quotepath::
+       The commands that output paths (e.g. `ls-files`,
+       `diff`), when not given the `-z` option, will quote
+       "unusual" characters in the pathname by enclosing the
+       pathname in a double-quote pair and with backslashes the
+       same way strings in C source code are quoted.  If this
+       variable is set to false, the bytes higher than 0x80 are
+       not quoted but output as verbatim.  Note that double
+       quote, backslash and control characters are always
+       quoted without `-z` regardless of the setting of this
+       variable.
+
+core.autocrlf::
+       If true, makes git convert `CRLF` at the end of lines in text files to
+       `LF` when reading from the filesystem, and convert in reverse when
+       writing to the filesystem.  The variable can be set to
+       'input', in which case the conversion happens only while
+       reading from the filesystem but files are written out with
+       `LF` at the end of lines.  Currently, which paths to consider
+       "text" (i.e. be subjected to the autocrlf mechanism) is
+       decided purely based on the contents.
+
+core.symlinks::
+       If false, symbolic links are checked out as small plain files that
+       contain the link text. gitlink:git-update-index[1] and
+       gitlink:git-add[1] will not change the recorded type to regular
+       file. Useful on filesystems like FAT that do not support
+       symbolic links. True by default.
+
+core.gitProxy::
+       A "proxy command" to execute (as 'command host port') instead
+       of establishing direct connection to the remote server when
+       using the git protocol for fetching. If the variable value is
+       in the "COMMAND for DOMAIN" format, the command is applied only
+       on hostnames ending with the specified domain string. This variable
+       may be set multiple times and is matched in the given order;
+       the first match wins.
++
+Can be overridden by the 'GIT_PROXY_COMMAND' environment variable
+(which always applies universally, without the special "for"
+handling).
+
+core.ignoreStat::
+       The working copy files are assumed to stay unchanged until you
+       mark them otherwise manually - Git will not detect the file changes
+       by lstat() calls. This is useful on systems where those are very
+       slow, such as Microsoft Windows.  See gitlink:git-update-index[1].
+       False by default.
+
+core.preferSymlinkRefs::
+       Instead of the default "symref" format for HEAD
+       and other symbolic reference files, use symbolic links.
+       This is sometimes needed to work with old scripts that
+       expect HEAD to be a symbolic link.
+
+core.bare::
+       If true this repository is assumed to be 'bare' and has no
+       working directory associated with it.  If this is the case a
+       number of commands that require a working directory will be
+       disabled, such as gitlink:git-add[1] or gitlink:git-merge[1].
++
+This setting is automatically guessed by gitlink:git-clone[1] or
+gitlink:git-init[1] when the repository was created.  By default a
+repository that ends in "/.git" is assumed to be not bare (bare =
+false), while all other repositories are assumed to be bare (bare
+= true).
+
+core.worktree::
+       Set the path to the working tree.  The value will not be
+       used in combination with repositories found automatically in
+       a .git directory (i.e. $GIT_DIR is not set).
+       This can be overridden by the GIT_WORK_TREE environment
+       variable and the '--work-tree' command line option.
+
+core.logAllRefUpdates::
+       Enable the reflog. Updates to a ref <ref> is logged to the file
+       "$GIT_DIR/logs/<ref>", by appending the new and old
+       SHA1, the date/time and the reason of the update, but
+       only when the file exists.  If this configuration
+       variable is set to true, missing "$GIT_DIR/logs/<ref>"
+       file is automatically created for branch heads.
++
+This information can be used to determine what commit
+was the tip of a branch "2 days ago".
++
+This value is true by default in a repository that has
+a working directory associated with it, and false by
+default in a bare repository.
+
+core.repositoryFormatVersion::
+       Internal variable identifying the repository format and layout
+       version.
+
+core.sharedRepository::
+       When 'group' (or 'true'), the repository is made shareable between
+       several users in a group (making sure all the files and objects are
+       group-writable). When 'all' (or 'world' or 'everybody'), the
+       repository will be readable by all users, additionally to being
+       group-shareable. When 'umask' (or 'false'), git will use permissions
+       reported by umask(2). See gitlink:git-init[1]. False by default.
+
+core.warnAmbiguousRefs::
+       If true, git will warn you if the ref name you passed it is ambiguous
+       and might match multiple refs in the .git/refs/ tree. True by default.
+
+core.compression::
+       An integer -1..9, indicating a default compression level.
+       -1 is the zlib default. 0 means no compression,
+       and 1..9 are various speed/size tradeoffs, 9 being slowest.
+       If set, this provides a default to other compression variables,
+       such as 'core.loosecompression' and 'pack.compression'.
+
+core.loosecompression::
+       An integer -1..9, indicating the compression level for objects that
+       are not in a pack file. -1 is the zlib default. 0 means no
+       compression, and 1..9 are various speed/size tradeoffs, 9 being
+       slowest.  If not set,  defaults to core.compression.  If that is
+       not set,  defaults to 1 (best speed).
+
+core.packedGitWindowSize::
+       Number of bytes of a pack file to map into memory in a
+       single mapping operation.  Larger window sizes may allow
+       your system to process a smaller number of large pack files
+       more quickly.  Smaller window sizes will negatively affect
+       performance due to increased calls to the operating system's
+       memory manager, but may improve performance when accessing
+       a large number of large pack files.
++
+Default is 1 MiB if NO_MMAP was set at compile time, otherwise 32
+MiB on 32 bit platforms and 1 GiB on 64 bit platforms.  This should
+be reasonable for all users/operating systems.  You probably do
+not need to adjust this value.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
+core.packedGitLimit::
+       Maximum number of bytes to map simultaneously into memory
+       from pack files.  If Git needs to access more than this many
+       bytes at once to complete an operation it will unmap existing
+       regions to reclaim virtual address space within the process.
++
+Default is 256 MiB on 32 bit platforms and 8 GiB on 64 bit platforms.
+This should be reasonable for all users/operating systems, except on
+the largest projects.  You probably do not need to adjust this value.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
+core.deltaBaseCacheLimit::
+       Maximum number of bytes to reserve for caching base objects
+       that multiple deltafied objects reference.  By storing the
+       entire decompressed base objects in a cache Git is able
+       to avoid unpacking and decompressing frequently used base
+       objects multiple times.
++
+Default is 16 MiB on all platforms.  This should be reasonable
+for all users/operating systems, except on the largest projects.
+You probably do not need to adjust this value.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
+core.excludesfile::
+       In addition to '.gitignore' (per-directory) and
+       '.git/info/exclude', git looks into this file for patterns
+       of files which are not meant to be tracked.  See
+       gitlink:gitignore[5].
+
+core.editor::
+       Commands such as `commit` and `tag` that lets you edit
+       messages by launching an editor uses the value of this
+       variable when it is set, and the environment variable
+       `GIT_EDITOR` is not set.  The order of preference is
+       `GIT_EDITOR` environment, `core.editor`, `VISUAL` and
+       `EDITOR` environment variables and then finally `vi`.
+
+core.pager::
+       The command that git will use to paginate output.  Can be overridden
+       with the `GIT_PAGER` environment variable.
+
+alias.*::
+       Command aliases for the gitlink:git[1] command wrapper - e.g.
+       after defining "alias.last = cat-file commit HEAD", the invocation
+       "git last" is equivalent to "git cat-file commit HEAD". To avoid
+       confusion and troubles with script usage, aliases that
+       hide existing git commands are ignored. Arguments are split by
+       spaces, the usual shell quoting and escaping is supported.
+       quote pair and a backslash can be used to quote them.
++
+If the alias expansion is prefixed with an exclamation point,
+it will be treated as a shell command.  For example, defining
+"alias.new = !gitk --all --not ORIG_HEAD", the invocation
+"git new" is equivalent to running the shell command
+"gitk --all --not ORIG_HEAD".
+
+apply.whitespace::
+       Tells `git-apply` how to handle whitespaces, in the same way
+       as the '--whitespace' option. See gitlink:git-apply[1].
+
+branch.autosetupmerge::
+       Tells `git-branch` and `git-checkout` to setup new branches
+       so that gitlink:git-pull[1] will appropriately merge from that
+       remote branch.  Note that even if this option is not set,
+       this behavior can be chosen per-branch using the `--track`
+       and `--no-track` options.  This option defaults to false.
+
+branch.<name>.remote::
+       When in branch <name>, it tells `git fetch` which remote to fetch.
+       If this option is not given, `git fetch` defaults to remote "origin".
+
+branch.<name>.merge::
+       When in branch <name>, it tells `git fetch` the default
+       refspec to be marked for merging in FETCH_HEAD. The value is
+       handled like the remote part of a refspec, and must match a
+       ref which is fetched from the remote given by
+       "branch.<name>.remote".
+       The merge information is used by `git pull` (which at first calls
+       `git fetch`) to lookup the default branch for merging. Without
+       this option, `git pull` defaults to merge the first refspec fetched.
+       Specify multiple values to get an octopus merge.
+       If you wish to setup `git pull` so that it merges into <name> from
+       another branch in the local repository, you can point
+       branch.<name>.merge to the desired branch, and use the special setting
+       `.` (a period) for branch.<name>.remote.
+
+branch.<name>.mergeoptions::
+       Sets default options for merging into branch <name>. The syntax and
+       supported options are equal to that of gitlink:git-merge[1], but
+       option values containing whitespace characters are currently not
+       supported.
+
+branch.<name>.rebase::
+       When true, rebase the branch <name> on top of the fetched branch,
+       instead of merging the default branch from the default remote.
+       *NOTE*: this is a possibly dangerous operation; do *not* use
+       it unless you understand the implications (see gitlink:git-rebase[1]
+       for details).
+
+clean.requireForce::
+       A boolean to make git-clean do nothing unless given -f
+       or -n.   Defaults to true.
+
+color.branch::
+       A boolean to enable/disable color in the output of
+       gitlink:git-branch[1]. May be set to `always`,
+       `false` (or `never`) or `auto` (or `true`), in which case colors are used
+       only when the output is to a terminal. Defaults to false.
+
+color.branch.<slot>::
+       Use customized color for branch coloration. `<slot>` is one of
+       `current` (the current branch), `local` (a local branch),
+       `remote` (a tracking branch in refs/remotes/), `plain` (other
+       refs).
++
+The value for these configuration variables is a list of colors (at most
+two) and attributes (at most one), separated by spaces.  The colors
+accepted are `normal`, `black`, `red`, `green`, `yellow`, `blue`,
+`magenta`, `cyan` and `white`; the attributes are `bold`, `dim`, `ul`,
+`blink` and `reverse`.  The first color given is the foreground; the
+second is the background.  The position of the attribute, if any,
+doesn't matter.
+
+color.diff::
+       When set to `always`, always use colors in patch.
+       When false (or `never`), never.  When set to `true` or `auto`, use
+       colors only when the output is to the terminal. Defaults to false.
+
+color.diff.<slot>::
+       Use customized color for diff colorization.  `<slot>` specifies
+       which part of the patch to use the specified color, and is one
+       of `plain` (context text), `meta` (metainformation), `frag`
+       (hunk header), `old` (removed lines), `new` (added lines),
+       `commit` (commit headers), or `whitespace` (highlighting dubious
+       whitespace).  The values of these variables may be specified as
+       in color.branch.<slot>.
+
+color.interactive::
+       When set to `always`, always use colors in `git add --interactive`.
+       When false (or `never`), never.  When set to `true` or `auto`, use
+       colors only when the output is to the terminal. Defaults to false.
+
+color.interactive.<slot>::
+       Use customized color for `git add --interactive`
+       output. `<slot>` may be `prompt`, `header`, or `help`, for
+       three distinct types of normal output from interactive
+       programs.  The values of these variables may be specified as
+       in color.branch.<slot>.
+
+color.pager::
+       A boolean to enable/disable colored output when the pager is in
+       use (default is true).
+
+color.status::
+       A boolean to enable/disable color in the output of
+       gitlink:git-status[1]. May be set to `always`,
+       `false` (or `never`) or `auto` (or `true`), in which case colors are used
+       only when the output is to a terminal. Defaults to false.
+
+color.status.<slot>::
+       Use customized color for status colorization. `<slot>` is
+       one of `header` (the header text of the status message),
+       `added` or `updated` (files which are added but not committed),
+       `changed` (files which are changed but not added in the index),
+       or `untracked` (files which are not tracked by git). The values of
+       these variables may be specified as in color.branch.<slot>.
+
+commit.template::
+       Specify a file to use as the template for new commit messages.
+
+diff.autorefreshindex::
+       When using `git diff` to compare with work tree
+       files, do not consider stat-only change as changed.
+       Instead, silently run `git update-index --refresh` to
+       update the cached stat information for paths whose
+       contents in the work tree match the contents in the
+       index.  This option defaults to true.  Note that this
+       affects only `git diff` Porcelain, and not lower level
+       `diff` commands, such as `git diff-files`.
+
+diff.renameLimit::
+       The number of files to consider when performing the copy/rename
+       detection; equivalent to the git diff option '-l'.
+
+diff.renames::
+       Tells git to detect renames.  If set to any boolean value, it
+       will enable basic rename detection.  If set to "copies" or
+       "copy", it will detect copies, as well.
+
+fetch.unpackLimit::
+       If the number of objects fetched over the git native
+       transfer is below this
+       limit, then the objects will be unpacked into loose object
+       files. However if the number of received objects equals or
+       exceeds this limit then the received pack will be stored as
+       a pack, after adding any missing delta bases.  Storing the
+       pack from a push can make the push operation complete faster,
+       especially on slow filesystems.
+
+format.numbered::
+       A boolean which can enable sequence numbers in patch subjects.
+       Seting this option to "auto" will enable it only if there is
+       more than one patch.  See --numbered option in
+       gitlink:git-format-patch[1].
+
+format.headers::
+       Additional email headers to include in a patch to be submitted
+       by mail.  See gitlink:git-format-patch[1].
+
+format.suffix::
+       The default for format-patch is to output files with the suffix
+       `.patch`. Use this variable to change that suffix (make sure to
+       include the dot if you want it).
+
+gc.aggressiveWindow::
+       The window size parameter used in the delta compression
+       algorithm used by 'git gc --aggressive'.  This defaults
+       to 10.
+
+gc.auto::
+       When there are approximately more than this many loose
+       objects in the repository, `git gc --auto` will pack them.
+       Some Porcelain commands use this command to perform a
+       light-weight garbage collection from time to time.  Setting
+       this to 0 disables it.
+
+gc.autopacklimit::
+       When there are more than this many packs that are not
+       marked with `*.keep` file in the repository, `git gc
+       --auto` consolidates them into one larger pack.  Setting
+       this to 0 disables this.
+
+gc.packrefs::
+       `git gc` does not run `git pack-refs` in a bare repository by
+       default so that older dumb-transport clients can still fetch
+       from the repository.  Setting this to `true` lets `git
+       gc` to run `git pack-refs`.  Setting this to `false` tells
+       `git gc` never to run `git pack-refs`. The default setting is
+       `notbare`. Enable it only when you know you do not have to
+       support such clients.  The default setting will change to `true`
+       at some stage, and setting this to `false` will continue to
+       prevent `git pack-refs` from being run from `git gc`.
+
+gc.reflogexpire::
+       `git reflog expire` removes reflog entries older than
+       this time; defaults to 90 days.
+
+gc.reflogexpireunreachable::
+       `git reflog expire` removes reflog entries older than
+       this time and are not reachable from the current tip;
+       defaults to 30 days.
+
+gc.rerereresolved::
+       Records of conflicted merge you resolved earlier are
+       kept for this many days when `git rerere gc` is run.
+       The default is 60 days.  See gitlink:git-rerere[1].
+
+gc.rerereunresolved::
+       Records of conflicted merge you have not resolved are
+       kept for this many days when `git rerere gc` is run.
+       The default is 15 days.  See gitlink:git-rerere[1].
+
+rerere.enabled::
+       Activate recording of resolved conflicts, so that identical
+       conflict hunks can be resolved automatically, should they
+       be encountered again.  gitlink:git-rerere[1] command is by
+       default enabled, but can be disabled by setting this option to
+       false.
+
+gitcvs.enabled::
+       Whether the CVS server interface is enabled for this repository.
+       See gitlink:git-cvsserver[1].
+
+gitcvs.logfile::
+       Path to a log file where the CVS server interface well... logs
+       various stuff. See gitlink:git-cvsserver[1].
+
+gitcvs.allbinary::
+       If true, all files are sent to the client in mode '-kb'. This
+       causes the client to treat all files as binary files which suppresses
+       any newline munging it otherwise might do. A work-around for the
+       fact that there is no way yet to set single files to mode '-kb'.
+
+gitcvs.dbname::
+       Database used by git-cvsserver to cache revision information
+       derived from the git repository. The exact meaning depends on the
+       used database driver, for SQLite (which is the default driver) this
+       is a filename. Supports variable substitution (see
+       gitlink:git-cvsserver[1] for details). May not contain semicolons (`;`).
+       Default: '%Ggitcvs.%m.sqlite'
+
+gitcvs.dbdriver::
+       Used Perl DBI driver. You can specify any available driver
+        for this here, but it might not work. git-cvsserver is tested
+       with 'DBD::SQLite', reported to work with 'DBD::Pg', and
+       reported *not* to work with 'DBD::mysql'. Experimental feature.
+       May not contain double colons (`:`). Default: 'SQLite'.
+       See gitlink:git-cvsserver[1].
+
+gitcvs.dbuser, gitcvs.dbpass::
+       Database user and password. Only useful if setting 'gitcvs.dbdriver',
+       since SQLite has no concept of database users and/or passwords.
+       'gitcvs.dbuser' supports variable substitution (see
+       gitlink:git-cvsserver[1] for details).
+
+All gitcvs variables except for 'gitcvs.allbinary' can also be
+specified as 'gitcvs.<access_method>.<varname>' (where 'access_method'
+is one of "ext" and "pserver") to make them apply only for the given
+access method.
+
+http.proxy::
+       Override the HTTP proxy, normally configured using the 'http_proxy'
+       environment variable (see gitlink:curl[1]).  This can be overridden
+       on a per-remote basis; see remote.<name>.proxy
+
+http.sslVerify::
+       Whether to verify the SSL certificate when fetching or pushing
+       over HTTPS. Can be overridden by the 'GIT_SSL_NO_VERIFY' environment
+       variable.
+
+http.sslCert::
+       File containing the SSL certificate when fetching or pushing
+       over HTTPS. Can be overridden by the 'GIT_SSL_CERT' environment
+       variable.
+
+http.sslKey::
+       File containing the SSL private key when fetching or pushing
+       over HTTPS. Can be overridden by the 'GIT_SSL_KEY' environment
+       variable.
+
+http.sslCAInfo::
+       File containing the certificates to verify the peer with when
+       fetching or pushing over HTTPS. Can be overridden by the
+       'GIT_SSL_CAINFO' environment variable.
+
+http.sslCAPath::
+       Path containing files with the CA certificates to verify the peer
+       with when fetching or pushing over HTTPS. Can be overridden
+       by the 'GIT_SSL_CAPATH' environment variable.
+
+http.maxRequests::
+       How many HTTP requests to launch in parallel. Can be overridden
+       by the 'GIT_HTTP_MAX_REQUESTS' environment variable. Default is 5.
+
+http.lowSpeedLimit, http.lowSpeedTime::
+       If the HTTP transfer speed is less than 'http.lowSpeedLimit'
+       for longer than 'http.lowSpeedTime' seconds, the transfer is aborted.
+       Can be overridden by the 'GIT_HTTP_LOW_SPEED_LIMIT' and
+       'GIT_HTTP_LOW_SPEED_TIME' environment variables.
+
+http.noEPSV::
+       A boolean which disables using of EPSV ftp command by curl.
+       This can helpful with some "poor" ftp servers which don't
+       support EPSV mode. Can be overridden by the 'GIT_CURL_FTP_NO_EPSV'
+       environment variable. Default is false (curl will use EPSV).
+
+i18n.commitEncoding::
+       Character encoding the commit messages are stored in; git itself
+       does not care per se, but this information is necessary e.g. when
+       importing commits from emails or in the gitk graphical history
+       browser (and possibly at other places in the future or in other
+       porcelains). See e.g. gitlink:git-mailinfo[1]. Defaults to 'utf-8'.
+
+i18n.logOutputEncoding::
+       Character encoding the commit messages are converted to when
+       running `git-log` and friends.
+
+log.showroot::
+       If true, the initial commit will be shown as a big creation event.
+       This is equivalent to a diff against an empty tree.
+       Tools like gitlink:git-log[1] or gitlink:git-whatchanged[1], which
+       normally hide the root commit will now show it. True by default.
+
+merge.summary::
+       Whether to include summaries of merged commits in newly created
+       merge commit messages. False by default.
+
+merge.tool::
+       Controls which merge resolution program is used by
+       gitlink:git-mergetool[1].  Valid values are: "kdiff3", "tkdiff",
+       "meld", "xxdiff", "emerge", "vimdiff", "gvimdiff", and "opendiff".
+
+merge.verbosity::
+       Controls the amount of output shown by the recursive merge
+       strategy.  Level 0 outputs nothing except a final error
+       message if conflicts were detected. Level 1 outputs only
+       conflicts, 2 outputs conflicts and file changes.  Level 5 and
+       above outputs debugging information.  The default is level 2.
+       Can be overridden by 'GIT_MERGE_VERBOSITY' environment variable.
+
+merge.<driver>.name::
+       Defines a human readable name for a custom low-level
+       merge driver.  See gitlink:gitattributes[5] for details.
+
+merge.<driver>.driver::
+       Defines the command that implements a custom low-level
+       merge driver.  See gitlink:gitattributes[5] for details.
+
+merge.<driver>.recursive::
+       Names a low-level merge driver to be used when
+       performing an internal merge between common ancestors.
+       See gitlink:gitattributes[5] for details.
+
+pack.window::
+       The size of the window used by gitlink:git-pack-objects[1] when no
+       window size is given on the command line. Defaults to 10.
+
+pack.depth::
+       The maximum delta depth used by gitlink:git-pack-objects[1] when no
+       maximum depth is given on the command line. Defaults to 50.
+
+pack.windowMemory::
+       The window memory size limit used by gitlink:git-pack-objects[1]
+       when no limit is given on the command line.  The value can be
+       suffixed with "k", "m", or "g".  Defaults to 0, meaning no
+       limit.
+
+pack.compression::
+       An integer -1..9, indicating the compression level for objects
+       in a pack file. -1 is the zlib default. 0 means no
+       compression, and 1..9 are various speed/size tradeoffs, 9 being
+       slowest.  If not set,  defaults to core.compression.  If that is
+       not set,  defaults to -1, the zlib default, which is "a default
+       compromise between speed and compression (currently equivalent
+       to level 6)."
+
+pack.deltaCacheSize::
+       The maximum memory in bytes used for caching deltas in
+       gitlink:git-pack-objects[1].
+       A value of 0 means no limit. Defaults to 0.
+
+pack.deltaCacheLimit::
+       The maximum size of a delta, that is cached in
+       gitlink:git-pack-objects[1]. Defaults to 1000.
+
+pack.threads::
+       Specifies the number of threads to spawn when searching for best
+       delta matches.  This requires that gitlink:git-pack-objects[1]
+       be compiled with pthreads otherwise this option is ignored with a
+       warning. This is meant to reduce packing time on multiprocessor
+       machines. The required amount of memory for the delta search window
+       is however multiplied by the number of threads.
+
+pack.indexVersion::
+       Specify the default pack index version.  Valid values are 1 for
+       legacy pack index used by Git versions prior to 1.5.2, and 2 for
+       the new pack index with capabilities for packs larger than 4 GB
+       as well as proper protection against the repacking of corrupted
+       packs.  Version 2 is selected and this config option ignored
+       whenever the corresponding pack is larger than 2 GB.  Otherwise
+       the default is 1.
+
+pull.octopus::
+       The default merge strategy to use when pulling multiple branches
+       at once.
+
+pull.twohead::
+       The default merge strategy to use when pulling a single branch.
+
+remote.<name>.url::
+       The URL of a remote repository.  See gitlink:git-fetch[1] or
+       gitlink:git-push[1].
+
+remote.<name>.proxy::
+       For remotes that require curl (http, https and ftp), the URL to
+       the proxy to use for that remote.  Set to the empty string to
+       disable proxying for that remote.
+
+remote.<name>.fetch::
+       The default set of "refspec" for gitlink:git-fetch[1]. See
+       gitlink:git-fetch[1].
+
+remote.<name>.push::
+       The default set of "refspec" for gitlink:git-push[1]. See
+       gitlink:git-push[1].
+
+remote.<name>.skipDefaultUpdate::
+       If true, this remote will be skipped by default when updating
+       using the remote subcommand of gitlink:git-remote[1].
+
+remote.<name>.receivepack::
+       The default program to execute on the remote side when pushing.  See
+       option \--exec of gitlink:git-push[1].
+
+remote.<name>.uploadpack::
+       The default program to execute on the remote side when fetching.  See
+       option \--exec of gitlink:git-fetch-pack[1].
+
+remote.<name>.tagopt::
+       Setting this value to --no-tags disables automatic tag following when fetching
+       from remote <name>
+
+remotes.<group>::
+       The list of remotes which are fetched by "git remote update
+       <group>".  See gitlink:git-remote[1].
+
+repack.usedeltabaseoffset::
+       Allow gitlink:git-repack[1] to create packs that uses
+       delta-base offset.  Defaults to false.
+
+show.difftree::
+       The default gitlink:git-diff-tree[1] arguments to be used
+       for gitlink:git-show[1].
+
+showbranch.default::
+       The default set of branches for gitlink:git-show-branch[1].
+       See gitlink:git-show-branch[1].
+
+tar.umask::
+       This variable can be used to restrict the permission bits of
+       tar archive entries.  The default is 0002, which turns off the
+       world write bit.  The special value "user" indicates that the
+       archiving user's umask will be used instead.  See umask(2) and
+       gitlink:git-archive[1].
+
+user.email::
+       Your email address to be recorded in any newly created commits.
+       Can be overridden by the 'GIT_AUTHOR_EMAIL', 'GIT_COMMITTER_EMAIL', and
+       'EMAIL' environment variables.  See gitlink:git-commit-tree[1].
+
+user.name::
+       Your full name to be recorded in any newly created commits.
+       Can be overridden by the 'GIT_AUTHOR_NAME' and 'GIT_COMMITTER_NAME'
+       environment variables.  See gitlink:git-commit-tree[1].
+
+user.signingkey::
+       If gitlink:git-tag[1] is not selecting the key you want it to
+       automatically when creating a signed tag, you can override the
+       default selection with this variable.  This option is passed
+       unchanged to gpg's --local-user parameter, so you may specify a key
+       using any method that gpg supports.
+
+whatchanged.difftree::
+       The default gitlink:git-diff-tree[1] arguments to be used
+       for gitlink:git-whatchanged[1].
+
+imap::
+       The configuration variables in the 'imap' section are described
+       in gitlink:git-imap-send[1].
+
+receive.unpackLimit::
+       If the number of objects received in a push is below this
+       limit then the objects will be unpacked into loose object
+       files. However if the number of received objects equals or
+       exceeds this limit then the received pack will be stored as
+       a pack, after adding any missing delta bases.  Storing the
+       pack from a push can make the push operation complete faster,
+       especially on slow filesystems.
+
+receive.denyNonFastForwards::
+       If set to true, git-receive-pack will deny a ref update which is
+       not a fast forward. Use this to prevent such an update via a push,
+       even if that push is forced. This configuration variable is
+       set when initializing a shared repository.
+
+transfer.unpackLimit::
+       When `fetch.unpackLimit` or `receive.unpackLimit` are
+       not set, the value of this variable is used instead.
diff --git a/Documentation/core-tutorial.txt b/Documentation/core-tutorial.txt
new file mode 100644 (file)
index 0000000..bd6cd41
--- /dev/null
@@ -0,0 +1,1681 @@
+A git core tutorial for developers
+==================================
+
+Introduction
+------------
+
+This tutorial explains how to use the "core" git programs to set up and
+work with a git repository.
+
+If you just need to use git as a revision control system you may prefer
+to start with link:tutorial.html[a tutorial introduction to git] or
+link:user-manual.html[the git user manual].
+
+However, an understanding of these low-level tools can be helpful if
+you want to understand git's internals.
+
+The core git is often called "plumbing", with the prettier user
+interfaces on top of it called "porcelain". You may not want to use the
+plumbing directly very often, but it can be good to know what the
+plumbing does for when the porcelain isn't flushing.
+
+[NOTE]
+Deeper technical details are often marked as Notes, which you can
+skip on your first reading.
+
+
+Creating a git repository
+-------------------------
+
+Creating a new git repository couldn't be easier: all git repositories start
+out empty, and the only thing you need to do is find yourself a
+subdirectory that you want to use as a working tree - either an empty
+one for a totally new project, or an existing working tree that you want
+to import into git.
+
+For our first example, we're going to start a totally new repository from
+scratch, with no pre-existing files, and we'll call it `git-tutorial`.
+To start up, create a subdirectory for it, change into that
+subdirectory, and initialize the git infrastructure with `git-init`:
+
+------------------------------------------------
+$ mkdir git-tutorial
+$ cd git-tutorial
+$ git-init
+------------------------------------------------
+
+to which git will reply
+
+----------------
+Initialized empty Git repository in .git/
+----------------
+
+which is just git's way of saying that you haven't been doing anything
+strange, and that it will have created a local `.git` directory setup for
+your new project. You will now have a `.git` directory, and you can
+inspect that with `ls`. For your new empty project, it should show you
+three entries, among other things:
+
+ - a file called `HEAD`, that has `ref: refs/heads/master` in it.
+   This is similar to a symbolic link and points at
+   `refs/heads/master` relative to the `HEAD` file.
++
+Don't worry about the fact that the file that the `HEAD` link points to
+doesn't even exist yet -- you haven't created the commit that will
+start your `HEAD` development branch yet.
+
+ - a subdirectory called `objects`, which will contain all the
+   objects of your project. You should never have any real reason to
+   look at the objects directly, but you might want to know that these
+   objects are what contains all the real 'data' in your repository.
+
+ - a subdirectory called `refs`, which contains references to objects.
+
+In particular, the `refs` subdirectory will contain two other
+subdirectories, named `heads` and `tags` respectively. They do
+exactly what their names imply: they contain references to any number
+of different 'heads' of development (aka 'branches'), and to any
+'tags' that you have created to name specific versions in your
+repository.
+
+One note: the special `master` head is the default branch, which is
+why the `.git/HEAD` file was created points to it even if it
+doesn't yet exist. Basically, the `HEAD` link is supposed to always
+point to the branch you are working on right now, and you always
+start out expecting to work on the `master` branch.
+
+However, this is only a convention, and you can name your branches
+anything you want, and don't have to ever even 'have' a `master`
+branch. A number of the git tools will assume that `.git/HEAD` is
+valid, though.
+
+[NOTE]
+An 'object' is identified by its 160-bit SHA1 hash, aka 'object name',
+and a reference to an object is always the 40-byte hex
+representation of that SHA1 name. The files in the `refs`
+subdirectory are expected to contain these hex references
+(usually with a final `\'\n\'` at the end), and you should thus
+expect to see a number of 41-byte files containing these
+references in these `refs` subdirectories when you actually start
+populating your tree.
+
+[NOTE]
+An advanced user may want to take a look at the
+link:repository-layout.html[repository layout] document
+after finishing this tutorial.
+
+You have now created your first git repository. Of course, since it's
+empty, that's not very useful, so let's start populating it with data.
+
+
+Populating a git repository
+---------------------------
+
+We'll keep this simple and stupid, so we'll start off with populating a
+few trivial files just to get a feel for it.
+
+Start off with just creating any random files that you want to maintain
+in your git repository. We'll start off with a few bad examples, just to
+get a feel for how this works:
+
+------------------------------------------------
+$ echo "Hello World" >hello
+$ echo "Silly example" >example
+------------------------------------------------
+
+you have now created two files in your working tree (aka 'working directory'),
+but to actually check in your hard work, you will have to go through two steps:
+
+ - fill in the 'index' file (aka 'cache') with the information about your
+   working tree state.
+
+ - commit that index file as an object.
+
+The first step is trivial: when you want to tell git about any changes
+to your working tree, you use the `git-update-index` program. That
+program normally just takes a list of filenames you want to update, but
+to avoid trivial mistakes, it refuses to add new entries to the index
+(or remove existing ones) unless you explicitly tell it that you're
+adding a new entry with the `\--add` flag (or removing an entry with the
+`\--remove`) flag.
+
+So to populate the index with the two files you just created, you can do
+
+------------------------------------------------
+$ git-update-index --add hello example
+------------------------------------------------
+
+and you have now told git to track those two files.
+
+In fact, as you did that, if you now look into your object directory,
+you'll notice that git will have added two new objects to the object
+database. If you did exactly the steps above, you should now be able to do
+
+
+----------------
+$ ls .git/objects/??/*
+----------------
+
+and see two files:
+
+----------------
+.git/objects/55/7db03de997c86a4a028e1ebd3a1ceb225be238
+.git/objects/f2/4c74a2e500f5ee1332c86b94199f52b1d1d962
+----------------
+
+which correspond with the objects with names of `557db...` and
+`f24c7...` respectively.
+
+If you want to, you can use `git-cat-file` to look at those objects, but
+you'll have to use the object name, not the filename of the object:
+
+----------------
+$ git-cat-file -t 557db03de997c86a4a028e1ebd3a1ceb225be238
+----------------
+
+where the `-t` tells `git-cat-file` to tell you what the "type" of the
+object is. git will tell you that you have a "blob" object (i.e., just a
+regular file), and you can see the contents with
+
+----------------
+$ git-cat-file "blob" 557db03
+----------------
+
+which will print out "Hello World". The object `557db03` is nothing
+more than the contents of your file `hello`.
+
+[NOTE]
+Don't confuse that object with the file `hello` itself. The
+object is literally just those specific *contents* of the file, and
+however much you later change the contents in file `hello`, the object
+we just looked at will never change. Objects are immutable.
+
+[NOTE]
+The second example demonstrates that you can
+abbreviate the object name to only the first several
+hexadecimal digits in most places.
+
+Anyway, as we mentioned previously, you normally never actually take a
+look at the objects themselves, and typing long 40-character hex
+names is not something you'd normally want to do. The above digression
+was just to show that `git-update-index` did something magical, and
+actually saved away the contents of your files into the git object
+database.
+
+Updating the index did something else too: it created a `.git/index`
+file. This is the index that describes your current working tree, and
+something you should be very aware of. Again, you normally never worry
+about the index file itself, but you should be aware of the fact that
+you have not actually really "checked in" your files into git so far,
+you've only *told* git about them.
+
+However, since git knows about them, you can now start using some of the
+most basic git commands to manipulate the files or look at their status.
+
+In particular, let's not even check in the two files into git yet, we'll
+start off by adding another line to `hello` first:
+
+------------------------------------------------
+$ echo "It's a new day for git" >>hello
+------------------------------------------------
+
+and you can now, since you told git about the previous state of `hello`, ask
+git what has changed in the tree compared to your old index, using the
+`git-diff-files` command:
+
+------------
+$ git-diff-files
+------------
+
+Oops. That wasn't very readable. It just spit out its own internal
+version of a `diff`, but that internal version really just tells you
+that it has noticed that "hello" has been modified, and that the old object
+contents it had have been replaced with something else.
+
+To make it readable, we can tell git-diff-files to output the
+differences as a patch, using the `-p` flag:
+
+------------
+$ git-diff-files -p
+diff --git a/hello b/hello
+index 557db03..263414f 100644
+--- a/hello
++++ b/hello
+@@ -1 +1,2 @@
+ Hello World
++It's a new day for git
+----
+
+i.e. the diff of the change we caused by adding another line to `hello`.
+
+In other words, `git-diff-files` always shows us the difference between
+what is recorded in the index, and what is currently in the working
+tree. That's very useful.
+
+A common shorthand for `git-diff-files -p` is to just write `git
+diff`, which will do the same thing.
+
+------------
+$ git diff
+diff --git a/hello b/hello
+index 557db03..263414f 100644
+--- a/hello
++++ b/hello
+@@ -1 +1,2 @@
+ Hello World
++It's a new day for git
+------------
+
+
+Committing git state
+--------------------
+
+Now, we want to go to the next stage in git, which is to take the files
+that git knows about in the index, and commit them as a real tree. We do
+that in two phases: creating a 'tree' object, and committing that 'tree'
+object as a 'commit' object together with an explanation of what the
+tree was all about, along with information of how we came to that state.
+
+Creating a tree object is trivial, and is done with `git-write-tree`.
+There are no options or other input: git-write-tree will take the
+current index state, and write an object that describes that whole
+index. In other words, we're now tying together all the different
+filenames with their contents (and their permissions), and we're
+creating the equivalent of a git "directory" object:
+
+------------------------------------------------
+$ git-write-tree
+------------------------------------------------
+
+and this will just output the name of the resulting tree, in this case
+(if you have done exactly as I've described) it should be
+
+----------------
+8988da15d077d4829fc51d8544c097def6644dbb
+----------------
+
+which is another incomprehensible object name. Again, if you want to,
+you can use `git-cat-file -t 8988d\...` to see that this time the object
+is not a "blob" object, but a "tree" object (you can also use
+`git-cat-file` to actually output the raw object contents, but you'll see
+mainly a binary mess, so that's less interesting).
+
+However -- normally you'd never use `git-write-tree` on its own, because
+normally you always commit a tree into a commit object using the
+`git-commit-tree` command. In fact, it's easier to not actually use
+`git-write-tree` on its own at all, but to just pass its result in as an
+argument to `git-commit-tree`.
+
+`git-commit-tree` normally takes several arguments -- it wants to know
+what the 'parent' of a commit was, but since this is the first commit
+ever in this new repository, and it has no parents, we only need to pass in
+the object name of the tree. However, `git-commit-tree` also wants to get a
+commit message on its standard input, and it will write out the resulting
+object name for the commit to its standard output.
+
+And this is where we create the `.git/refs/heads/master` file
+which is pointed at by `HEAD`. This file is supposed to contain
+the reference to the top-of-tree of the master branch, and since
+that's exactly what `git-commit-tree` spits out, we can do this
+all with a sequence of simple shell commands:
+
+------------------------------------------------
+$ tree=$(git-write-tree)
+$ commit=$(echo 'Initial commit' | git-commit-tree $tree)
+$ git-update-ref HEAD $commit
+------------------------------------------------
+
+In this case this creates a totally new commit that is not related to
+anything else. Normally you do this only *once* for a project ever, and
+all later commits will be parented on top of an earlier commit.
+
+Again, normally you'd never actually do this by hand. There is a
+helpful script called `git commit` that will do all of this for you. So
+you could have just written `git commit`
+instead, and it would have done the above magic scripting for you.
+
+
+Making a change
+---------------
+
+Remember how we did the `git-update-index` on file `hello` and then we
+changed `hello` afterward, and could compare the new state of `hello` with the
+state we saved in the index file?
+
+Further, remember how I said that `git-write-tree` writes the contents
+of the *index* file to the tree, and thus what we just committed was in
+fact the *original* contents of the file `hello`, not the new ones. We did
+that on purpose, to show the difference between the index state, and the
+state in the working tree, and how they don't have to match, even
+when we commit things.
+
+As before, if we do `git-diff-files -p` in our git-tutorial project,
+we'll still see the same difference we saw last time: the index file
+hasn't changed by the act of committing anything. However, now that we
+have committed something, we can also learn to use a new command:
+`git-diff-index`.
+
+Unlike `git-diff-files`, which showed the difference between the index
+file and the working tree, `git-diff-index` shows the differences
+between a committed *tree* and either the index file or the working
+tree. In other words, `git-diff-index` wants a tree to be diffed
+against, and before we did the commit, we couldn't do that, because we
+didn't have anything to diff against.
+
+But now we can do
+
+----------------
+$ git-diff-index -p HEAD
+----------------
+
+(where `-p` has the same meaning as it did in `git-diff-files`), and it
+will show us the same difference, but for a totally different reason.
+Now we're comparing the working tree not against the index file,
+but against the tree we just wrote. It just so happens that those two
+are obviously the same, so we get the same result.
+
+Again, because this is a common operation, you can also just shorthand
+it with
+
+----------------
+$ git diff HEAD
+----------------
+
+which ends up doing the above for you.
+
+In other words, `git-diff-index` normally compares a tree against the
+working tree, but when given the `\--cached` flag, it is told to
+instead compare against just the index cache contents, and ignore the
+current working tree state entirely. Since we just wrote the index
+file to HEAD, doing `git-diff-index \--cached -p HEAD` should thus return
+an empty set of differences, and that's exactly what it does.
+
+[NOTE]
+================
+`git-diff-index` really always uses the index for its
+comparisons, and saying that it compares a tree against the working
+tree is thus not strictly accurate. In particular, the list of
+files to compare (the "meta-data") *always* comes from the index file,
+regardless of whether the `\--cached` flag is used or not. The `\--cached`
+flag really only determines whether the file *contents* to be compared
+come from the working tree or not.
+
+This is not hard to understand, as soon as you realize that git simply
+never knows (or cares) about files that it is not told about
+explicitly. git will never go *looking* for files to compare, it
+expects you to tell it what the files are, and that's what the index
+is there for.
+================
+
+However, our next step is to commit the *change* we did, and again, to
+understand what's going on, keep in mind the difference between "working
+tree contents", "index file" and "committed tree". We have changes
+in the working tree that we want to commit, and we always have to
+work through the index file, so the first thing we need to do is to
+update the index cache:
+
+------------------------------------------------
+$ git-update-index hello
+------------------------------------------------
+
+(note how we didn't need the `\--add` flag this time, since git knew
+about the file already).
+
+Note what happens to the different `git-diff-\*` versions here. After
+we've updated `hello` in the index, `git-diff-files -p` now shows no
+differences, but `git-diff-index -p HEAD` still *does* show that the
+current state is different from the state we committed. In fact, now
+`git-diff-index` shows the same difference whether we use the `--cached`
+flag or not, since now the index is coherent with the working tree.
+
+Now, since we've updated `hello` in the index, we can commit the new
+version. We could do it by writing the tree by hand again, and
+committing the tree (this time we'd have to use the `-p HEAD` flag to
+tell commit that the HEAD was the *parent* of the new commit, and that
+this wasn't an initial commit any more), but you've done that once
+already, so let's just use the helpful script this time:
+
+------------------------------------------------
+$ git commit
+------------------------------------------------
+
+which starts an editor for you to write the commit message and tells you
+a bit about what you have done.
+
+Write whatever message you want, and all the lines that start with '#'
+will be pruned out, and the rest will be used as the commit message for
+the change. If you decide you don't want to commit anything after all at
+this point (you can continue to edit things and update the index), you
+can just leave an empty message. Otherwise `git commit` will commit
+the change for you.
+
+You've now made your first real git commit. And if you're interested in
+looking at what `git commit` really does, feel free to investigate:
+it's a few very simple shell scripts to generate the helpful (?) commit
+message headers, and a few one-liners that actually do the
+commit itself (`git-commit`).
+
+
+Inspecting Changes
+------------------
+
+While creating changes is useful, it's even more useful if you can tell
+later what changed. The most useful command for this is another of the
+`diff` family, namely `git-diff-tree`.
+
+`git-diff-tree` can be given two arbitrary trees, and it will tell you the
+differences between them. Perhaps even more commonly, though, you can
+give it just a single commit object, and it will figure out the parent
+of that commit itself, and show the difference directly. Thus, to get
+the same diff that we've already seen several times, we can now do
+
+----------------
+$ git-diff-tree -p HEAD
+----------------
+
+(again, `-p` means to show the difference as a human-readable patch),
+and it will show what the last commit (in `HEAD`) actually changed.
+
+[NOTE]
+============
+Here is an ASCII art by Jon Loeliger that illustrates how
+various diff-\* commands compare things.
+
+                      diff-tree
+                       +----+
+                       |    |
+                       |    |
+                       V    V
+                    +-----------+
+                    | Object DB |
+                    |  Backing  |
+                    |   Store   |
+                    +-----------+
+                      ^    ^
+                      |    |
+                      |    |  diff-index --cached
+                      |    |
+          diff-index  |    V
+                      |  +-----------+
+                      |  |   Index   |
+                      |  |  "cache"  |
+                      |  +-----------+
+                      |    ^
+                      |    |
+                      |    |  diff-files
+                      |    |
+                      V    V
+                    +-----------+
+                    |  Working  |
+                    | Directory |
+                    +-----------+
+============
+
+More interestingly, you can also give `git-diff-tree` the `--pretty` flag,
+which tells it to also show the commit message and author and date of the
+commit, and you can tell it to show a whole series of diffs.
+Alternatively, you can tell it to be "silent", and not show the diffs at
+all, but just show the actual commit message.
+
+In fact, together with the `git-rev-list` program (which generates a
+list of revisions), `git-diff-tree` ends up being a veritable fount of
+changes. A trivial (but very useful) script called `git-whatchanged` is
+included with git which does exactly this, and shows a log of recent
+activities.
+
+To see the whole history of our pitiful little git-tutorial project, you
+can do
+
+----------------
+$ git log
+----------------
+
+which shows just the log messages, or if we want to see the log together
+with the associated patches use the more complex (and much more
+powerful)
+
+----------------
+$ git-whatchanged -p --root
+----------------
+
+and you will see exactly what has changed in the repository over its
+short history.
+
+[NOTE]
+The `\--root` flag is a flag to `git-diff-tree` to tell it to
+show the initial aka 'root' commit too. Normally you'd probably not
+want to see the initial import diff, but since the tutorial project
+was started from scratch and is so small, we use it to make the result
+a bit more interesting.
+
+With that, you should now be having some inkling of what git does, and
+can explore on your own.
+
+[NOTE]
+Most likely, you are not directly using the core
+git Plumbing commands, but using Porcelain such as `git-add`, `git-rm'
+and `git-commit'.
+
+
+Tagging a version
+-----------------
+
+In git, there are two kinds of tags, a "light" one, and an "annotated tag".
+
+A "light" tag is technically nothing more than a branch, except we put
+it in the `.git/refs/tags/` subdirectory instead of calling it a `head`.
+So the simplest form of tag involves nothing more than
+
+------------------------------------------------
+$ git tag my-first-tag
+------------------------------------------------
+
+which just writes the current `HEAD` into the `.git/refs/tags/my-first-tag`
+file, after which point you can then use this symbolic name for that
+particular state. You can, for example, do
+
+----------------
+$ git diff my-first-tag
+----------------
+
+to diff your current state against that tag (which at this point will
+obviously be an empty diff, but if you continue to develop and commit
+stuff, you can use your tag as an "anchor-point" to see what has changed
+since you tagged it.
+
+An "annotated tag" is actually a real git object, and contains not only a
+pointer to the state you want to tag, but also a small tag name and
+message, along with optionally a PGP signature that says that yes,
+you really did
+that tag. You create these annotated tags with either the `-a` or
+`-s` flag to `git tag`:
+
+----------------
+$ git tag -s <tagname>
+----------------
+
+which will sign the current `HEAD` (but you can also give it another
+argument that specifies the thing to tag, i.e., you could have tagged the
+current `mybranch` point by using `git tag <tagname> mybranch`).
+
+You normally only do signed tags for major releases or things
+like that, while the light-weight tags are useful for any marking you
+want to do -- any time you decide that you want to remember a certain
+point, just create a private tag for it, and you have a nice symbolic
+name for the state at that point.
+
+
+Copying repositories
+--------------------
+
+git repositories are normally totally self-sufficient and relocatable.
+Unlike CVS, for example, there is no separate notion of
+"repository" and "working tree". A git repository normally *is* the
+working tree, with the local git information hidden in the `.git`
+subdirectory. There is nothing else. What you see is what you got.
+
+[NOTE]
+You can tell git to split the git internal information from
+the directory that it tracks, but we'll ignore that for now: it's not
+how normal projects work, and it's really only meant for special uses.
+So the mental model of "the git information is always tied directly to
+the working tree that it describes" may not be technically 100%
+accurate, but it's a good model for all normal use.
+
+This has two implications:
+
+ - if you grow bored with the tutorial repository you created (or you've
+   made a mistake and want to start all over), you can just do simple
++
+----------------
+$ rm -rf git-tutorial
+----------------
++
+and it will be gone. There's no external repository, and there's no
+history outside the project you created.
+
+ - if you want to move or duplicate a git repository, you can do so. There
+   is `git clone` command, but if all you want to do is just to
+   create a copy of your repository (with all the full history that
+   went along with it), you can do so with a regular
+   `cp -a git-tutorial new-git-tutorial`.
++
+Note that when you've moved or copied a git repository, your git index
+file (which caches various information, notably some of the "stat"
+information for the files involved) will likely need to be refreshed.
+So after you do a `cp -a` to create a new copy, you'll want to do
++
+----------------
+$ git-update-index --refresh
+----------------
++
+in the new repository to make sure that the index file is up-to-date.
+
+Note that the second point is true even across machines. You can
+duplicate a remote git repository with *any* regular copy mechanism, be it
+`scp`, `rsync` or `wget`.
+
+When copying a remote repository, you'll want to at a minimum update the
+index cache when you do this, and especially with other peoples'
+repositories you often want to make sure that the index cache is in some
+known state (you don't know *what* they've done and not yet checked in),
+so usually you'll precede the `git-update-index` with a
+
+----------------
+$ git-read-tree --reset HEAD
+$ git-update-index --refresh
+----------------
+
+which will force a total index re-build from the tree pointed to by `HEAD`.
+It resets the index contents to `HEAD`, and then the `git-update-index`
+makes sure to match up all index entries with the checked-out files.
+If the original repository had uncommitted changes in its
+working tree, `git-update-index --refresh` notices them and
+tells you they need to be updated.
+
+The above can also be written as simply
+
+----------------
+$ git reset
+----------------
+
+and in fact a lot of the common git command combinations can be scripted
+with the `git xyz` interfaces.  You can learn things by just looking
+at what the various git scripts do.  For example, `git reset` used to be
+the above two lines implemented in `git-reset`, but some things like
+`git status` and `git commit` are slightly more complex scripts around
+the basic git commands.
+
+Many (most?) public remote repositories will not contain any of
+the checked out files or even an index file, and will *only* contain the
+actual core git files. Such a repository usually doesn't even have the
+`.git` subdirectory, but has all the git files directly in the
+repository.
+
+To create your own local live copy of such a "raw" git repository, you'd
+first create your own subdirectory for the project, and then copy the
+raw repository contents into the `.git` directory. For example, to
+create your own copy of the git repository, you'd do the following
+
+----------------
+$ mkdir my-git
+$ cd my-git
+$ rsync -rL rsync://rsync.kernel.org/pub/scm/git/git.git/ .git
+----------------
+
+followed by
+
+----------------
+$ git-read-tree HEAD
+----------------
+
+to populate the index. However, now you have populated the index, and
+you have all the git internal files, but you will notice that you don't
+actually have any of the working tree files to work on. To get
+those, you'd check them out with
+
+----------------
+$ git-checkout-index -u -a
+----------------
+
+where the `-u` flag means that you want the checkout to keep the index
+up-to-date (so that you don't have to refresh it afterward), and the
+`-a` flag means "check out all files" (if you have a stale copy or an
+older version of a checked out tree you may also need to add the `-f`
+flag first, to tell git-checkout-index to *force* overwriting of any old
+files).
+
+Again, this can all be simplified with
+
+----------------
+$ git clone rsync://rsync.kernel.org/pub/scm/git/git.git/ my-git
+$ cd my-git
+$ git checkout
+----------------
+
+which will end up doing all of the above for you.
+
+You have now successfully copied somebody else's (mine) remote
+repository, and checked it out.
+
+
+Creating a new branch
+---------------------
+
+Branches in git are really nothing more than pointers into the git
+object database from within the `.git/refs/` subdirectory, and as we
+already discussed, the `HEAD` branch is nothing but a symlink to one of
+these object pointers.
+
+You can at any time create a new branch by just picking an arbitrary
+point in the project history, and just writing the SHA1 name of that
+object into a file under `.git/refs/heads/`. You can use any filename you
+want (and indeed, subdirectories), but the convention is that the
+"normal" branch is called `master`. That's just a convention, though,
+and nothing enforces it.
+
+To show that as an example, let's go back to the git-tutorial repository we
+used earlier, and create a branch in it. You do that by simply just
+saying that you want to check out a new branch:
+
+------------
+$ git checkout -b mybranch
+------------
+
+will create a new branch based at the current `HEAD` position, and switch
+to it.
+
+[NOTE]
+================================================
+If you make the decision to start your new branch at some
+other point in the history than the current `HEAD`, you can do so by
+just telling `git checkout` what the base of the checkout would be.
+In other words, if you have an earlier tag or branch, you'd just do
+
+------------
+$ git checkout -b mybranch earlier-commit
+------------
+
+and it would create the new branch `mybranch` at the earlier commit,
+and check out the state at that time.
+================================================
+
+You can always just jump back to your original `master` branch by doing
+
+------------
+$ git checkout master
+------------
+
+(or any other branch-name, for that matter) and if you forget which
+branch you happen to be on, a simple
+
+------------
+$ cat .git/HEAD
+------------
+
+will tell you where it's pointing.  To get the list of branches
+you have, you can say
+
+------------
+$ git branch
+------------
+
+which used to be nothing more than a simple script around `ls .git/refs/heads`.
+There will be an asterisk in front of the branch you are currently on.
+
+Sometimes you may wish to create a new branch _without_ actually
+checking it out and switching to it. If so, just use the command
+
+------------
+$ git branch <branchname> [startingpoint]
+------------
+
+which will simply _create_ the branch, but will not do anything further.
+You can then later -- once you decide that you want to actually develop
+on that branch -- switch to that branch with a regular `git checkout`
+with the branchname as the argument.
+
+
+Merging two branches
+--------------------
+
+One of the ideas of having a branch is that you do some (possibly
+experimental) work in it, and eventually merge it back to the main
+branch. So assuming you created the above `mybranch` that started out
+being the same as the original `master` branch, let's make sure we're in
+that branch, and do some work there.
+
+------------------------------------------------
+$ git checkout mybranch
+$ echo "Work, work, work" >>hello
+$ git commit -m "Some work." -i hello
+------------------------------------------------
+
+Here, we just added another line to `hello`, and we used a shorthand for
+doing both `git-update-index hello` and `git commit` by just giving the
+filename directly to `git commit`, with an `-i` flag (it tells
+git to 'include' that file in addition to what you have done to
+the index file so far when making the commit).  The `-m` flag is to give the
+commit log message from the command line.
+
+Now, to make it a bit more interesting, let's assume that somebody else
+does some work in the original branch, and simulate that by going back
+to the master branch, and editing the same file differently there:
+
+------------
+$ git checkout master
+------------
+
+Here, take a moment to look at the contents of `hello`, and notice how they
+don't contain the work we just did in `mybranch` -- because that work
+hasn't happened in the `master` branch at all. Then do
+
+------------
+$ echo "Play, play, play" >>hello
+$ echo "Lots of fun" >>example
+$ git commit -m "Some fun." -i hello example
+------------
+
+since the master branch is obviously in a much better mood.
+
+Now, you've got two branches, and you decide that you want to merge the
+work done. Before we do that, let's introduce a cool graphical tool that
+helps you view what's going on:
+
+----------------
+$ gitk --all
+----------------
+
+will show you graphically both of your branches (that's what the `\--all`
+means: normally it will just show you your current `HEAD`) and their
+histories. You can also see exactly how they came to be from a common
+source.
+
+Anyway, let's exit `gitk` (`^Q` or the File menu), and decide that we want
+to merge the work we did on the `mybranch` branch into the `master`
+branch (which is currently our `HEAD` too). To do that, there's a nice
+script called `git merge`, which wants to know which branches you want
+to resolve and what the merge is all about:
+
+------------
+$ git merge -m "Merge work in mybranch" mybranch
+------------
+
+where the first argument is going to be used as the commit message if
+the merge can be resolved automatically.
+
+Now, in this case we've intentionally created a situation where the
+merge will need to be fixed up by hand, though, so git will do as much
+of it as it can automatically (which in this case is just merge the `example`
+file, which had no differences in the `mybranch` branch), and say:
+
+----------------
+       Auto-merging hello
+       CONFLICT (content): Merge conflict in hello
+       Automatic merge failed; fix up by hand
+----------------
+
+It tells you that it did an "Automatic merge", which
+failed due to conflicts in `hello`.
+
+Not to worry. It left the (trivial) conflict in `hello` in the same form you
+should already be well used to if you've ever used CVS, so let's just
+open `hello` in our editor (whatever that may be), and fix it up somehow.
+I'd suggest just making it so that `hello` contains all four lines:
+
+------------
+Hello World
+It's a new day for git
+Play, play, play
+Work, work, work
+------------
+
+and once you're happy with your manual merge, just do a
+
+------------
+$ git commit -i hello
+------------
+
+which will very loudly warn you that you're now committing a merge
+(which is correct, so never mind), and you can write a small merge
+message about your adventures in git-merge-land.
+
+After you're done, start up `gitk \--all` to see graphically what the
+history looks like. Notice that `mybranch` still exists, and you can
+switch to it, and continue to work with it if you want to. The
+`mybranch` branch will not contain the merge, but next time you merge it
+from the `master` branch, git will know how you merged it, so you'll not
+have to do _that_ merge again.
+
+Another useful tool, especially if you do not always work in X-Window
+environment, is `git show-branch`.
+
+------------------------------------------------
+$ git-show-branch --topo-order --more=1 master mybranch
+* [master] Merge work in mybranch
+ ! [mybranch] Some work.
+--
+-  [master] Merge work in mybranch
+*+ [mybranch] Some work.
+*  [master^] Some fun.
+------------------------------------------------
+
+The first two lines indicate that it is showing the two branches
+and the first line of the commit log message from their
+top-of-the-tree commits, you are currently on `master` branch
+(notice the asterisk `\*` character), and the first column for
+the later output lines is used to show commits contained in the
+`master` branch, and the second column for the `mybranch`
+branch. Three commits are shown along with their log messages.
+All of them have non blank characters in the first column (`*`
+shows an ordinary commit on the current branch, `-` is a merge commit), which
+means they are now part of the `master` branch. Only the "Some
+work" commit has the plus `+` character in the second column,
+because `mybranch` has not been merged to incorporate these
+commits from the master branch.  The string inside brackets
+before the commit log message is a short name you can use to
+name the commit.  In the above example, 'master' and 'mybranch'
+are branch heads.  'master^' is the first parent of 'master'
+branch head.  Please see 'git-rev-parse' documentation if you
+see more complex cases.
+
+[NOTE]
+Without the '--more=1' option, 'git-show-branch' would not output the
+'[master^]' commit, as '[mybranch]' commit is a common ancestor of
+both 'master' and 'mybranch' tips.  Please see 'git-show-branch'
+documentation for details.
+
+[NOTE]
+If there were more commits on the 'master' branch after the merge, the
+merge commit itself would not be shown by 'git-show-branch' by
+default.  You would need to provide '--sparse' option to make the
+merge commit visible in this case.
+
+Now, let's pretend you are the one who did all the work in
+`mybranch`, and the fruit of your hard work has finally been merged
+to the `master` branch. Let's go back to `mybranch`, and run
+`git merge` to get the "upstream changes" back to your branch.
+
+------------
+$ git checkout mybranch
+$ git merge -m "Merge upstream changes." master
+------------
+
+This outputs something like this (the actual commit object names
+would be different)
+
+----------------
+Updating from ae3a2da... to a80b4aa....
+Fast forward
+ example |    1 +
+ hello   |    1 +
+ 2 files changed, 2 insertions(+), 0 deletions(-)
+----------------
+
+Because your branch did not contain anything more than what are
+already merged into the `master` branch, the merge operation did
+not actually do a merge. Instead, it just updated the top of
+the tree of your branch to that of the `master` branch. This is
+often called 'fast forward' merge.
+
+You can run `gitk \--all` again to see how the commit ancestry
+looks like, or run `show-branch`, which tells you this.
+
+------------------------------------------------
+$ git show-branch master mybranch
+! [master] Merge work in mybranch
+ * [mybranch] Merge work in mybranch
+--
+-- [master] Merge work in mybranch
+------------------------------------------------
+
+
+Merging external work
+---------------------
+
+It's usually much more common that you merge with somebody else than
+merging with your own branches, so it's worth pointing out that git
+makes that very easy too, and in fact, it's not that different from
+doing a `git merge`. In fact, a remote merge ends up being nothing
+more than "fetch the work from a remote repository into a temporary tag"
+followed by a `git merge`.
+
+Fetching from a remote repository is done by, unsurprisingly,
+`git fetch`:
+
+----------------
+$ git fetch <remote-repository>
+----------------
+
+One of the following transports can be used to name the
+repository to download from:
+
+Rsync::
+       `rsync://remote.machine/path/to/repo.git/`
++
+Rsync transport is usable for both uploading and downloading,
+but is completely unaware of what git does, and can produce
+unexpected results when you download from the public repository
+while the repository owner is uploading into it via `rsync`
+transport.  Most notably, it could update the files under
+`refs/` which holds the object name of the topmost commits
+before uploading the files in `objects/` -- the downloader would
+obtain head commit object name while that object itself is still
+not available in the repository.  For this reason, it is
+considered deprecated.
+
+SSH::
+       `remote.machine:/path/to/repo.git/` or
++
+`ssh://remote.machine/path/to/repo.git/`
++
+This transport can be used for both uploading and downloading,
+and requires you to have a log-in privilege over `ssh` to the
+remote machine.  It finds out the set of objects the other side
+lacks by exchanging the head commits both ends have and
+transfers (close to) minimum set of objects.  It is by far the
+most efficient way to exchange git objects between repositories.
+
+Local directory::
+       `/path/to/repo.git/`
++
+This transport is the same as SSH transport but uses `sh` to run
+both ends on the local machine instead of running other end on
+the remote machine via `ssh`.
+
+git Native::
+       `git://remote.machine/path/to/repo.git/`
++
+This transport was designed for anonymous downloading.  Like SSH
+transport, it finds out the set of objects the downstream side
+lacks and transfers (close to) minimum set of objects.
+
+HTTP(S)::
+       `http://remote.machine/path/to/repo.git/`
++
+Downloader from http and https URL
+first obtains the topmost commit object name from the remote site
+by looking at the specified refname under `repo.git/refs/` directory,
+and then tries to obtain the
+commit object by downloading from `repo.git/objects/xx/xxx\...`
+using the object name of that commit object.  Then it reads the
+commit object to find out its parent commits and the associate
+tree object; it repeats this process until it gets all the
+necessary objects.  Because of this behavior, they are
+sometimes also called 'commit walkers'.
++
+The 'commit walkers' are sometimes also called 'dumb
+transports', because they do not require any git aware smart
+server like git Native transport does.  Any stock HTTP server
+that does not even support directory index would suffice.  But
+you must prepare your repository with `git-update-server-info`
+to help dumb transport downloaders.
+
+Once you fetch from the remote repository, you `merge` that
+with your current branch.
+
+However -- it's such a common thing to `fetch` and then
+immediately `merge`, that it's called `git pull`, and you can
+simply do
+
+----------------
+$ git pull <remote-repository>
+----------------
+
+and optionally give a branch-name for the remote end as a second
+argument.
+
+[NOTE]
+You could do without using any branches at all, by
+keeping as many local repositories as you would like to have
+branches, and merging between them with `git pull`, just like
+you merge between branches. The advantage of this approach is
+that it lets you keep a set of files for each `branch` checked
+out and you may find it easier to switch back and forth if you
+juggle multiple lines of development simultaneously. Of
+course, you will pay the price of more disk usage to hold
+multiple working trees, but disk space is cheap these days.
+
+It is likely that you will be pulling from the same remote
+repository from time to time. As a short hand, you can store
+the remote repository URL in the local repository's config file
+like this:
+
+------------------------------------------------
+$ git config remote.linus.url http://www.kernel.org/pub/scm/git/git.git/
+------------------------------------------------
+
+and use the "linus" keyword with `git pull` instead of the full URL.
+
+Examples.
+
+. `git pull linus`
+. `git pull linus tag v0.99.1`
+
+the above are equivalent to:
+
+. `git pull http://www.kernel.org/pub/scm/git/git.git/ HEAD`
+. `git pull http://www.kernel.org/pub/scm/git/git.git/ tag v0.99.1`
+
+
+How does the merge work?
+------------------------
+
+We said this tutorial shows what plumbing does to help you cope
+with the porcelain that isn't flushing, but we so far did not
+talk about how the merge really works.  If you are following
+this tutorial the first time, I'd suggest to skip to "Publishing
+your work" section and come back here later.
+
+OK, still with me?  To give us an example to look at, let's go
+back to the earlier repository with "hello" and "example" file,
+and bring ourselves back to the pre-merge state:
+
+------------
+$ git show-branch --more=2 master mybranch
+! [master] Merge work in mybranch
+ * [mybranch] Merge work in mybranch
+--
+-- [master] Merge work in mybranch
++* [master^2] Some work.
++* [master^] Some fun.
+------------
+
+Remember, before running `git merge`, our `master` head was at
+"Some fun." commit, while our `mybranch` head was at "Some
+work." commit.
+
+------------
+$ git checkout mybranch
+$ git reset --hard master^2
+$ git checkout master
+$ git reset --hard master^
+------------
+
+After rewinding, the commit structure should look like this:
+
+------------
+$ git show-branch
+* [master] Some fun.
+ ! [mybranch] Some work.
+--
+ + [mybranch] Some work.
+*  [master] Some fun.
+*+ [mybranch^] New day.
+------------
+
+Now we are ready to experiment with the merge by hand.
+
+`git merge` command, when merging two branches, uses 3-way merge
+algorithm.  First, it finds the common ancestor between them.
+The command it uses is `git-merge-base`:
+
+------------
+$ mb=$(git-merge-base HEAD mybranch)
+------------
+
+The command writes the commit object name of the common ancestor
+to the standard output, so we captured its output to a variable,
+because we will be using it in the next step.  By the way, the common
+ancestor commit is the "New day." commit in this case.  You can
+tell it by:
+
+------------
+$ git-name-rev $mb
+my-first-tag
+------------
+
+After finding out a common ancestor commit, the second step is
+this:
+
+------------
+$ git-read-tree -m -u $mb HEAD mybranch
+------------
+
+This is the same `git-read-tree` command we have already seen,
+but it takes three trees, unlike previous examples.  This reads
+the contents of each tree into different 'stage' in the index
+file (the first tree goes to stage 1, the second to stage 2,
+etc.).  After reading three trees into three stages, the paths
+that are the same in all three stages are 'collapsed' into stage
+0.  Also paths that are the same in two of three stages are
+collapsed into stage 0, taking the SHA1 from either stage 2 or
+stage 3, whichever is different from stage 1 (i.e. only one side
+changed from the common ancestor).
+
+After 'collapsing' operation, paths that are different in three
+trees are left in non-zero stages.  At this point, you can
+inspect the index file with this command:
+
+------------
+$ git-ls-files --stage
+100644 7f8b141b65fdcee47321e399a2598a235a032422 0      example
+100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1      hello
+100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2      hello
+100644 cc44c73eb783565da5831b4d820c962954019b69 3      hello
+------------
+
+In our example of only two files, we did not have unchanged
+files so only 'example' resulted in collapsing, but in real-life
+large projects, only small number of files change in one commit,
+and this 'collapsing' tends to trivially merge most of the paths
+fairly quickly, leaving only a handful the real changes in non-zero
+stages.
+
+To look at only non-zero stages, use `\--unmerged` flag:
+
+------------
+$ git-ls-files --unmerged
+100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1      hello
+100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2      hello
+100644 cc44c73eb783565da5831b4d820c962954019b69 3      hello
+------------
+
+The next step of merging is to merge these three versions of the
+file, using 3-way merge.  This is done by giving
+`git-merge-one-file` command as one of the arguments to
+`git-merge-index` command:
+
+------------
+$ git-merge-index git-merge-one-file hello
+Auto-merging hello.
+merge: warning: conflicts during merge
+ERROR: Merge conflict in hello.
+fatal: merge program failed
+------------
+
+`git-merge-one-file` script is called with parameters to
+describe those three versions, and is responsible to leave the
+merge results in the working tree.
+It is a fairly straightforward shell script, and
+eventually calls `merge` program from RCS suite to perform a
+file-level 3-way merge.  In this case, `merge` detects
+conflicts, and the merge result with conflict marks is left in
+the working tree..  This can be seen if you run `ls-files
+--stage` again at this point:
+
+------------
+$ git-ls-files --stage
+100644 7f8b141b65fdcee47321e399a2598a235a032422 0      example
+100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1      hello
+100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2      hello
+100644 cc44c73eb783565da5831b4d820c962954019b69 3      hello
+------------
+
+This is the state of the index file and the working file after
+`git merge` returns control back to you, leaving the conflicting
+merge for you to resolve.  Notice that the path `hello` is still
+unmerged, and what you see with `git diff` at this point is
+differences since stage 2 (i.e. your version).
+
+
+Publishing your work
+--------------------
+
+So, we can use somebody else's work from a remote repository, but
+how can *you* prepare a repository to let other people pull from
+it?
+
+You do your real work in your working tree that has your
+primary repository hanging under it as its `.git` subdirectory.
+You *could* make that repository accessible remotely and ask
+people to pull from it, but in practice that is not the way
+things are usually done. A recommended way is to have a public
+repository, make it reachable by other people, and when the
+changes you made in your primary working tree are in good shape,
+update the public repository from it. This is often called
+'pushing'.
+
+[NOTE]
+This public repository could further be mirrored, and that is
+how git repositories at `kernel.org` are managed.
+
+Publishing the changes from your local (private) repository to
+your remote (public) repository requires a write privilege on
+the remote machine. You need to have an SSH account there to
+run a single command, `git-receive-pack`.
+
+First, you need to create an empty repository on the remote
+machine that will house your public repository. This empty
+repository will be populated and be kept up-to-date by pushing
+into it later. Obviously, this repository creation needs to be
+done only once.
+
+[NOTE]
+`git push` uses a pair of programs,
+`git-send-pack` on your local machine, and `git-receive-pack`
+on the remote machine. The communication between the two over
+the network internally uses an SSH connection.
+
+Your private repository's git directory is usually `.git`, but
+your public repository is often named after the project name,
+i.e. `<project>.git`. Let's create such a public repository for
+project `my-git`. After logging into the remote machine, create
+an empty directory:
+
+------------
+$ mkdir my-git.git
+------------
+
+Then, make that directory into a git repository by running
+`git init`, but this time, since its name is not the usual
+`.git`, we do things slightly differently:
+
+------------
+$ GIT_DIR=my-git.git git-init
+------------
+
+Make sure this directory is available for others you want your
+changes to be pulled by via the transport of your choice. Also
+you need to make sure that you have the `git-receive-pack`
+program on the `$PATH`.
+
+[NOTE]
+Many installations of sshd do not invoke your shell as the login
+shell when you directly run programs; what this means is that if
+your login shell is `bash`, only `.bashrc` is read and not
+`.bash_profile`. As a workaround, make sure `.bashrc` sets up
+`$PATH` so that you can run `git-receive-pack` program.
+
+[NOTE]
+If you plan to publish this repository to be accessed over http,
+you should do `chmod +x my-git.git/hooks/post-update` at this
+point.  This makes sure that every time you push into this
+repository, `git-update-server-info` is run.
+
+Your "public repository" is now ready to accept your changes.
+Come back to the machine you have your private repository. From
+there, run this command:
+
+------------
+$ git push <public-host>:/path/to/my-git.git master
+------------
+
+This synchronizes your public repository to match the named
+branch head (i.e. `master` in this case) and objects reachable
+from them in your current repository.
+
+As a real example, this is how I update my public git
+repository. Kernel.org mirror network takes care of the
+propagation to other publicly visible machines:
+
+------------
+$ git push master.kernel.org:/pub/scm/git/git.git/
+------------
+
+
+Packing your repository
+-----------------------
+
+Earlier, we saw that one file under `.git/objects/??/` directory
+is stored for each git object you create. This representation
+is efficient to create atomically and safely, but
+not so convenient to transport over the network. Since git objects are
+immutable once they are created, there is a way to optimize the
+storage by "packing them together". The command
+
+------------
+$ git repack
+------------
+
+will do it for you. If you followed the tutorial examples, you
+would have accumulated about 17 objects in `.git/objects/??/`
+directories by now. `git repack` tells you how many objects it
+packed, and stores the packed file in `.git/objects/pack`
+directory.
+
+[NOTE]
+You will see two files, `pack-\*.pack` and `pack-\*.idx`,
+in `.git/objects/pack` directory. They are closely related to
+each other, and if you ever copy them by hand to a different
+repository for whatever reason, you should make sure you copy
+them together. The former holds all the data from the objects
+in the pack, and the latter holds the index for random
+access.
+
+If you are paranoid, running `git-verify-pack` command would
+detect if you have a corrupt pack, but do not worry too much.
+Our programs are always perfect ;-).
+
+Once you have packed objects, you do not need to leave the
+unpacked objects that are contained in the pack file anymore.
+
+------------
+$ git prune-packed
+------------
+
+would remove them for you.
+
+You can try running `find .git/objects -type f` before and after
+you run `git prune-packed` if you are curious.  Also `git
+count-objects` would tell you how many unpacked objects are in
+your repository and how much space they are consuming.
+
+[NOTE]
+`git pull` is slightly cumbersome for HTTP transport, as a
+packed repository may contain relatively few objects in a
+relatively large pack. If you expect many HTTP pulls from your
+public repository you might want to repack & prune often, or
+never.
+
+If you run `git repack` again at this point, it will say
+"Nothing to pack". Once you continue your development and
+accumulate the changes, running `git repack` again will create a
+new pack, that contains objects created since you packed your
+repository the last time. We recommend that you pack your project
+soon after the initial import (unless you are starting your
+project from scratch), and then run `git repack` every once in a
+while, depending on how active your project is.
+
+When a repository is synchronized via `git push` and `git pull`
+objects packed in the source repository are usually stored
+unpacked in the destination, unless rsync transport is used.
+While this allows you to use different packing strategies on
+both ends, it also means you may need to repack both
+repositories every once in a while.
+
+
+Working with Others
+-------------------
+
+Although git is a truly distributed system, it is often
+convenient to organize your project with an informal hierarchy
+of developers. Linux kernel development is run this way. There
+is a nice illustration (page 17, "Merges to Mainline") in
+link:http://www.xenotime.net/linux/mentor/linux-mentoring-2006.pdf[Randy Dunlap's presentation].
+
+It should be stressed that this hierarchy is purely *informal*.
+There is nothing fundamental in git that enforces the "chain of
+patch flow" this hierarchy implies. You do not have to pull
+from only one remote repository.
+
+A recommended workflow for a "project lead" goes like this:
+
+1. Prepare your primary repository on your local machine. Your
+   work is done there.
+
+2. Prepare a public repository accessible to others.
++
+If other people are pulling from your repository over dumb
+transport protocols (HTTP), you need to keep this repository
+'dumb transport friendly'.  After `git init`,
+`$GIT_DIR/hooks/post-update` copied from the standard templates
+would contain a call to `git-update-server-info` but the
+`post-update` hook itself is disabled by default -- enable it
+with `chmod +x post-update`.  This makes sure `git-update-server-info`
+keeps the necessary files up-to-date.
+
+3. Push into the public repository from your primary
+   repository.
+
+4. `git repack` the public repository. This establishes a big
+   pack that contains the initial set of objects as the
+   baseline, and possibly `git prune` if the transport
+   used for pulling from your repository supports packed
+   repositories.
+
+5. Keep working in your primary repository. Your changes
+   include modifications of your own, patches you receive via
+   e-mails, and merges resulting from pulling the "public"
+   repositories of your "subsystem maintainers".
++
+You can repack this private repository whenever you feel like.
+
+6. Push your changes to the public repository, and announce it
+   to the public.
+
+7. Every once in a while, "git repack" the public repository.
+   Go back to step 5. and continue working.
+
+
+A recommended work cycle for a "subsystem maintainer" who works
+on that project and has an own "public repository" goes like this:
+
+1. Prepare your work repository, by `git clone` the public
+   repository of the "project lead". The URL used for the
+   initial cloning is stored in the remote.origin.url
+   configuration variable.
+
+2. Prepare a public repository accessible to others, just like
+   the "project lead" person does.
+
+3. Copy over the packed files from "project lead" public
+   repository to your public repository, unless the "project
+   lead" repository lives on the same machine as yours.  In the
+   latter case, you can use `objects/info/alternates` file to
+   point at the repository you are borrowing from.
+
+4. Push into the public repository from your primary
+   repository. Run `git repack`, and possibly `git prune` if the
+   transport used for pulling from your repository supports
+   packed repositories.
+
+5. Keep working in your primary repository. Your changes
+   include modifications of your own, patches you receive via
+   e-mails, and merges resulting from pulling the "public"
+   repositories of your "project lead" and possibly your
+   "sub-subsystem maintainers".
++
+You can repack this private repository whenever you feel
+like.
+
+6. Push your changes to your public repository, and ask your
+   "project lead" and possibly your "sub-subsystem
+   maintainers" to pull from it.
+
+7. Every once in a while, `git repack` the public repository.
+   Go back to step 5. and continue working.
+
+
+A recommended work cycle for an "individual developer" who does
+not have a "public" repository is somewhat different. It goes
+like this:
+
+1. Prepare your work repository, by `git clone` the public
+   repository of the "project lead" (or a "subsystem
+   maintainer", if you work on a subsystem). The URL used for
+   the initial cloning is stored in the remote.origin.url
+   configuration variable.
+
+2. Do your work in your repository on 'master' branch.
+
+3. Run `git fetch origin` from the public repository of your
+   upstream every once in a while. This does only the first
+   half of `git pull` but does not merge. The head of the
+   public repository is stored in `.git/refs/remotes/origin/master`.
+
+4. Use `git cherry origin` to see which ones of your patches
+   were accepted, and/or use `git rebase origin` to port your
+   unmerged changes forward to the updated upstream.
+
+5. Use `git format-patch origin` to prepare patches for e-mail
+   submission to your upstream and send it out. Go back to
+   step 2. and continue.
+
+
+Working with Others, Shared Repository Style
+--------------------------------------------
+
+If you are coming from CVS background, the style of cooperation
+suggested in the previous section may be new to you. You do not
+have to worry. git supports "shared public repository" style of
+cooperation you are probably more familiar with as well.
+
+See link:cvs-migration.html[git for CVS users] for the details.
+
+Bundling your work together
+---------------------------
+
+It is likely that you will be working on more than one thing at
+a time.  It is easy to manage those more-or-less independent tasks
+using branches with git.
+
+We have already seen how branches work previously,
+with "fun and work" example using two branches.  The idea is the
+same if there are more than two branches.  Let's say you started
+out from "master" head, and have some new code in the "master"
+branch, and two independent fixes in the "commit-fix" and
+"diff-fix" branches:
+
+------------
+$ git show-branch
+! [commit-fix] Fix commit message normalization.
+ ! [diff-fix] Fix rename detection.
+  * [master] Release candidate #1
+---
+ +  [diff-fix] Fix rename detection.
+ +  [diff-fix~1] Better common substring algorithm.
++   [commit-fix] Fix commit message normalization.
+  * [master] Release candidate #1
+++* [diff-fix~2] Pretty-print messages.
+------------
+
+Both fixes are tested well, and at this point, you want to merge
+in both of them.  You could merge in 'diff-fix' first and then
+'commit-fix' next, like this:
+
+------------
+$ git merge -m "Merge fix in diff-fix" diff-fix
+$ git merge -m "Merge fix in commit-fix" commit-fix
+------------
+
+Which would result in:
+
+------------
+$ git show-branch
+! [commit-fix] Fix commit message normalization.
+ ! [diff-fix] Fix rename detection.
+  * [master] Merge fix in commit-fix
+---
+  - [master] Merge fix in commit-fix
++ * [commit-fix] Fix commit message normalization.
+  - [master~1] Merge fix in diff-fix
+ +* [diff-fix] Fix rename detection.
+ +* [diff-fix~1] Better common substring algorithm.
+  * [master~2] Release candidate #1
+++* [master~3] Pretty-print messages.
+------------
+
+However, there is no particular reason to merge in one branch
+first and the other next, when what you have are a set of truly
+independent changes (if the order mattered, then they are not
+independent by definition).  You could instead merge those two
+branches into the current branch at once.  First let's undo what
+we just did and start over.  We would want to get the master
+branch before these two merges by resetting it to 'master~2':
+
+------------
+$ git reset --hard master~2
+------------
+
+You can make sure 'git show-branch' matches the state before
+those two 'git merge' you just did.  Then, instead of running
+two 'git merge' commands in a row, you would merge these two
+branch heads (this is known as 'making an Octopus'):
+
+------------
+$ git merge commit-fix diff-fix
+$ git show-branch
+! [commit-fix] Fix commit message normalization.
+ ! [diff-fix] Fix rename detection.
+  * [master] Octopus merge of branches 'diff-fix' and 'commit-fix'
+---
+  - [master] Octopus merge of branches 'diff-fix' and 'commit-fix'
++ * [commit-fix] Fix commit message normalization.
+ +* [diff-fix] Fix rename detection.
+ +* [diff-fix~1] Better common substring algorithm.
+  * [master~1] Release candidate #1
+++* [master~2] Pretty-print messages.
+------------
+
+Note that you should not do Octopus because you can.  An octopus
+is a valid thing to do and often makes it easier to view the
+commit history if you are merging more than two independent
+changes at the same time.  However, if you have merge conflicts
+with any of the branches you are merging in and need to hand
+resolve, that is an indication that the development happened in
+those branches were not independent after all, and you should
+merge two at a time, documenting how you resolved the conflicts,
+and the reason why you preferred changes made in one side over
+the other.  Otherwise it would make the project history harder
+to follow, not easier.
diff --git a/Documentation/cvs-migration.txt b/Documentation/cvs-migration.txt
new file mode 100644 (file)
index 0000000..3b6b494
--- /dev/null
@@ -0,0 +1,171 @@
+git for CVS users
+=================
+
+Git differs from CVS in that every working tree contains a repository with
+a full copy of the project history, and no repository is inherently more
+important than any other.  However, you can emulate the CVS model by
+designating a single shared repository which people can synchronize with;
+this document explains how to do that.
+
+Some basic familiarity with git is required.  This
+link:tutorial.html[tutorial introduction to git] should be sufficient.
+
+Developing against a shared repository
+--------------------------------------
+
+Suppose a shared repository is set up in /pub/repo.git on the host
+foo.com.  Then as an individual committer you can clone the shared
+repository over ssh with:
+
+------------------------------------------------
+$ git clone foo.com:/pub/repo.git/ my-project
+$ cd my-project
+------------------------------------------------
+
+and hack away.  The equivalent of `cvs update` is
+
+------------------------------------------------
+$ git pull origin
+------------------------------------------------
+
+which merges in any work that others might have done since the clone
+operation.  If there are uncommitted changes in your working tree, commit
+them first before running git pull.
+
+[NOTE]
+================================
+The `pull` command knows where to get updates from because of certain
+configuration variables that were set by the first `git clone`
+command; see `git config -l` and the gitlink:git-config[1] man
+page for details.
+================================
+
+You can update the shared repository with your changes by first committing
+your changes, and then using the gitlink:git-push[1] command:
+
+------------------------------------------------
+$ git push origin master
+------------------------------------------------
+
+to "push" those commits to the shared repository.  If someone else has
+updated the repository more recently, `git push`, like `cvs commit`, will
+complain, in which case you must pull any changes before attempting the
+push again.
+
+In the `git push` command above we specify the name of the remote branch
+to update (`master`).  If we leave that out, `git push` tries to update
+any branches in the remote repository that have the same name as a branch
+in the local repository.  So the last `push` can be done with either of:
+
+------------
+$ git push origin
+$ git push foo.com:/pub/project.git/
+------------
+
+as long as the shared repository does not have any branches
+other than `master`.
+
+Setting Up a Shared Repository
+------------------------------
+
+We assume you have already created a git repository for your project,
+possibly created from scratch or from a tarball (see the
+link:tutorial.html[tutorial]), or imported from an already existing CVS
+repository (see the next section).
+
+Assume your existing repo is at /home/alice/myproject.  Create a new "bare"
+repository (a repository without a working tree) and fetch your project into
+it:
+
+------------------------------------------------
+$ mkdir /pub/my-repo.git
+$ cd /pub/my-repo.git
+$ git --bare init --shared
+$ git --bare fetch /home/alice/myproject master:master
+------------------------------------------------
+
+Next, give every team member read/write access to this repository.  One
+easy way to do this is to give all the team members ssh access to the
+machine where the repository is hosted.  If you don't want to give them a
+full shell on the machine, there is a restricted shell which only allows
+users to do git pushes and pulls; see gitlink:git-shell[1].
+
+Put all the committers in the same group, and make the repository
+writable by that group:
+
+------------------------------------------------
+$ chgrp -R $group /pub/my-repo.git
+------------------------------------------------
+
+Make sure committers have a umask of at most 027, so that the directories
+they create are writable and searchable by other group members.
+
+Importing a CVS archive
+-----------------------
+
+First, install version 2.1 or higher of cvsps from
+link:http://www.cobite.com/cvsps/[http://www.cobite.com/cvsps/] and make
+sure it is in your path.  Then cd to a checked out CVS working directory
+of the project you are interested in and run gitlink:git-cvsimport[1]:
+
+-------------------------------------------
+$ git cvsimport -C <destination> <module>
+-------------------------------------------
+
+This puts a git archive of the named CVS module in the directory
+<destination>, which will be created if necessary.
+
+The import checks out from CVS every revision of every file.  Reportedly
+cvsimport can average some twenty revisions per second, so for a
+medium-sized project this should not take more than a couple of minutes.
+Larger projects or remote repositories may take longer.
+
+The main trunk is stored in the git branch named `origin`, and additional
+CVS branches are stored in git branches with the same names.  The most
+recent version of the main trunk is also left checked out on the `master`
+branch, so you can start adding your own changes right away.
+
+The import is incremental, so if you call it again next month it will
+fetch any CVS updates that have been made in the meantime.  For this to
+work, you must not modify the imported branches; instead, create new
+branches for your own changes, and merge in the imported branches as
+necessary.
+
+Advanced Shared Repository Management
+-------------------------------------
+
+Git allows you to specify scripts called "hooks" to be run at certain
+points.  You can use these, for example, to send all commits to the shared
+repository to a mailing list.  See link:hooks.html[Hooks used by git].
+
+You can enforce finer grained permissions using update hooks.  See
+link:howto/update-hook-example.txt[Controlling access to branches using
+update hooks].
+
+Providing CVS Access to a git Repository
+----------------------------------------
+
+It is also possible to provide true CVS access to a git repository, so
+that developers can still use CVS; see gitlink:git-cvsserver[1] for
+details.
+
+Alternative Development Models
+------------------------------
+
+CVS users are accustomed to giving a group of developers commit access to
+a common repository.  As we've seen, this is also possible with git.
+However, the distributed nature of git allows other development models,
+and you may want to first consider whether one of them might be a better
+fit for your project.
+
+For example, you can choose a single person to maintain the project's
+primary public repository.  Other developers then clone this repository
+and each work in their own clone.  When they have a series of changes that
+they're happy with, they ask the maintainer to pull from the branch
+containing the changes.  The maintainer reviews their changes and pulls
+them into the primary repository, which other developers pull from as
+necessary to stay coordinated.  The Linux kernel and other projects use
+variants of this model.
+
+With a small group, developers may just pull changes from each other's
+repositories without the need for a central maintainer.
diff --git a/Documentation/diff-format.txt b/Documentation/diff-format.txt
new file mode 100644 (file)
index 0000000..2c3a4c4
--- /dev/null
@@ -0,0 +1,86 @@
+The output format from "git-diff-index", "git-diff-tree",
+"git-diff-files" and "git diff --raw" are very similar.
+
+These commands all compare two sets of things; what is
+compared differs:
+
+git-diff-index <tree-ish>::
+        compares the <tree-ish> and the files on the filesystem.
+
+git-diff-index --cached <tree-ish>::
+        compares the <tree-ish> and the index.
+
+git-diff-tree [-r] <tree-ish-1> <tree-ish-2> [<pattern>...]::
+        compares the trees named by the two arguments.
+
+git-diff-files [<pattern>...]::
+        compares the index and the files on the filesystem.
+
+
+An output line is formatted this way:
+
+------------------------------------------------
+in-place edit  :100644 100644 bcd1234... 0123456... M file0
+copy-edit      :100644 100644 abcd123... 1234567... C68 file1 file2
+rename-edit    :100644 100644 abcd123... 1234567... R86 file1 file3
+create         :000000 100644 0000000... 1234567... A file4
+delete         :100644 000000 1234567... 0000000... D file5
+unmerged       :000000 000000 0000000... 0000000... U file6
+------------------------------------------------
+
+That is, from the left to the right:
+
+. a colon.
+. mode for "src"; 000000 if creation or unmerged.
+. a space.
+. mode for "dst"; 000000 if deletion or unmerged.
+. a space.
+. sha1 for "src"; 0\{40\} if creation or unmerged.
+. a space.
+. sha1 for "dst"; 0\{40\} if creation, unmerged or "look at work tree".
+. a space.
+. status, followed by optional "score" number.
+. a tab or a NUL when '-z' option is used.
+. path for "src"
+. a tab or a NUL when '-z' option is used; only exists for C or R.
+. path for "dst"; only exists for C or R.
+. an LF or a NUL when '-z' option is used, to terminate the record.
+
+<sha1> is shown as all 0's if a file is new on the filesystem
+and it is out of sync with the index.
+
+Example:
+
+------------------------------------------------
+:100644 100644 5be4a4...... 000000...... M file.c
+------------------------------------------------
+
+When `-z` option is not used, TAB, LF, and backslash characters
+in pathnames are represented as `\t`, `\n`, and `\\`,
+respectively.
+
+diff format for merges
+----------------------
+
+"git-diff-tree", "git-diff-files" and "git-diff --raw"
+can take '-c' or '--cc' option
+to generate diff output also for merge commits.  The output differs
+from the format described above in the following way:
+
+. there is a colon for each parent
+. there are more "src" modes and "src" sha1
+. status is concatenated status characters for each parent
+. no optional "score" number
+. single path, only for "dst"
+
+Example:
+
+------------------------------------------------
+::100644 100644 100644 fabadb8... cc95eb0... 4866510... MM     describe.c
+------------------------------------------------
+
+Note that 'combined diff' lists only files which were modified from
+all parents.
+
+
+include::diff-generate-patch.txt[]
diff --git a/Documentation/diff-generate-patch.txt b/Documentation/diff-generate-patch.txt
new file mode 100644 (file)
index 0000000..029c5f2
--- /dev/null
@@ -0,0 +1,161 @@
+Generating patches with -p
+--------------------------
+
+When "git-diff-index", "git-diff-tree", or "git-diff-files" are run
+with a '-p' option, "git diff" without the '--raw' option, or
+"git log" with the "-p" option, they
+do not produce the output described above; instead they produce a
+patch file.  You can customize the creation of such patches via the
+GIT_EXTERNAL_DIFF and the GIT_DIFF_OPTS environment variables.
+
+What the -p option produces is slightly different from the traditional
+diff format.
+
+1.   It is preceded with a "git diff" header, that looks like
+     this:
+
+       diff --git a/file1 b/file2
++
+The `a/` and `b/` filenames are the same unless rename/copy is
+involved.  Especially, even for a creation or a deletion,
+`/dev/null` is _not_ used in place of `a/` or `b/` filenames.
++
+When rename/copy is involved, `file1` and `file2` show the
+name of the source file of the rename/copy and the name of
+the file that rename/copy produces, respectively.
+
+2.   It is followed by one or more extended header lines:
+
+       old mode <mode>
+       new mode <mode>
+       deleted file mode <mode>
+       new file mode <mode>
+       copy from <path>
+       copy to <path>
+       rename from <path>
+       rename to <path>
+       similarity index <number>
+       dissimilarity index <number>
+       index <hash>..<hash> <mode>
+
+3.  TAB, LF, double quote and backslash characters in pathnames
+    are represented as `\t`, `\n`, `\"` and `\\`, respectively.
+    If there is need for such substitution then the whole
+    pathname is put in double quotes.
+
+The similarity index is the percentage of unchanged lines, and
+the dissimilarity index is the percentage of changed lines.  It
+is a rounded down integer, followed by a percent sign.  The
+similarity index value of 100% is thus reserved for two equal
+files, while 100% dissimilarity means that no line from the old
+file made it into the new one.
+
+
+combined diff format
+--------------------
+
+"git-diff-tree", "git-diff-files" and "git-diff" can take '-c' or
+'--cc' option to produce 'combined diff'.  For showing a merge commit
+with "git log -p", this is the default format.
+A 'combined diff' format looks like this:
+
+------------
+diff --combined describe.c
+index fabadb8,cc95eb0..4866510
+--- a/describe.c
++++ b/describe.c
+@@@ -98,20 -98,12 +98,20 @@@
+       return (a_date > b_date) ? -1 : (a_date == b_date) ? 0 : 1;
+  }
+
+- static void describe(char *arg)
+ -static void describe(struct commit *cmit, int last_one)
+++static void describe(char *arg, int last_one)
+  {
+ +     unsigned char sha1[20];
+ +     struct commit *cmit;
+       struct commit_list *list;
+       static int initialized = 0;
+       struct commit_name *n;
+
+ +     if (get_sha1(arg, sha1) < 0)
+ +             usage(describe_usage);
+ +     cmit = lookup_commit_reference(sha1);
+ +     if (!cmit)
+ +             usage(describe_usage);
+ +
+       if (!initialized) {
+               initialized = 1;
+               for_each_ref(get_name);
+------------
+
+1.   It is preceded with a "git diff" header, that looks like
+     this (when '-c' option is used):
+
+       diff --combined file
++
+or like this (when '--cc' option is used):
+
+       diff --c file
+
+2.   It is followed by one or more extended header lines
+     (this example shows a merge with two parents):
+
+       index <hash>,<hash>..<hash>
+       mode <mode>,<mode>..<mode>
+       new file mode <mode>
+       deleted file mode <mode>,<mode>
++
+The `mode <mode>,<mode>..<mode>` line appears only if at least one of
+the <mode> is different from the rest. Extended headers with
+information about detected contents movement (renames and
+copying detection) are designed to work with diff of two
+<tree-ish> and are not used by combined diff format.
+
+3.   It is followed by two-line from-file/to-file header
+
+       --- a/file
+       +++ b/file
++
+Similar to two-line header for traditional 'unified' diff
+format, `/dev/null` is used to signal created or deleted
+files.
+
+4.   Chunk header format is modified to prevent people from
+     accidentally feeding it to `patch -p1`. Combined diff format
+     was created for review of merge commit changes, and was not
+     meant for apply. The change is similar to the change in the
+     extended 'index' header:
+
+       @@@ <from-file-range> <from-file-range> <to-file-range> @@@
++
+There are (number of parents + 1) `@` characters in the chunk
+header for combined diff format.
+
+Unlike the traditional 'unified' diff format, which shows two
+files A and B with a single column that has `-` (minus --
+appears in A but removed in B), `+` (plus -- missing in A but
+added to B), or `" "` (space -- unchanged) prefix, this format
+compares two or more files file1, file2,... with one file X, and
+shows how X differs from each of fileN.  One column for each of
+fileN is prepended to the output line to note how X's line is
+different from it.
+
+A `-` character in the column N means that the line appears in
+fileN but it does not appear in the result.  A `+` character
+in the column N means that the line appears in the last file,
+and fileN does not have that line (in other words, the line was
+added, from the point of view of that parent).
+
+In the above example output, the function signature was changed
+from both files (hence two `-` removals from both file1 and
+file2, plus `++` to mean one line that was added does not appear
+in either file1 nor file2).  Also two other lines are the same
+from file1 but do not appear in file2 (hence prefixed with ` +`).
+
+When shown by `git diff-tree -c`, it compares the parents of a
+merge commit with the merge result (i.e. file1..fileN are the
+parents).  When shown by `git diff-files -c`, it compares the
+two unresolved merge parents with the working tree file
+(i.e. file1 is stage 2 aka "our version", file2 is stage 3 aka
+"their version").
diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt
new file mode 100644 (file)
index 0000000..d0154bb
--- /dev/null
@@ -0,0 +1,213 @@
+// Please don't remove this comment as asciidoc behaves badly when
+// the first non-empty line is ifdef/ifndef. The symptom is that
+// without this comment the <git-diff-core> attribute conditionally
+// defined below ends up being defined unconditionally.
+// Last checked with asciidoc 7.0.2.
+
+ifndef::git-format-patch[]
+ifndef::git-diff[]
+ifndef::git-log[]
+:git-diff-core: 1
+endif::git-log[]
+endif::git-diff[]
+endif::git-format-patch[]
+
+ifdef::git-format-patch[]
+-p::
+       Generate patches without diffstat.
+endif::git-format-patch[]
+
+ifndef::git-format-patch[]
+-p::
+       Generate patch (see section on generating patches).
+       {git-diff? This is the default.}
+endif::git-format-patch[]
+
+-u::
+       Synonym for "-p".
+
+-U<n>::
+       Shorthand for "--unified=<n>".
+
+--unified=<n>::
+       Generate diffs with <n> lines of context instead of
+       the usual three. Implies "-p".
+
+--raw::
+       Generate the raw format.
+       {git-diff-core? This is the default.}
+
+--patch-with-raw::
+       Synonym for "-p --raw".
+
+--stat[=width[,name-width]]::
+       Generate a diffstat.  You can override the default
+       output width for 80-column terminal by "--stat=width".
+       The width of the filename part can be controlled by
+       giving another width to it separated by a comma.
+
+--numstat::
+       Similar to \--stat, but shows number of added and
+       deleted lines in decimal notation and pathname without
+       abbreviation, to make it more machine friendly.  For
+       binary files, outputs two `-` instead of saying
+       `0 0`.
+
+--shortstat::
+       Output only the last line of the --stat format containing total
+       number of modified files, as well as number of added and deleted
+       lines.
+
+--summary::
+       Output a condensed summary of extended header information
+       such as creations, renames and mode changes.
+
+--patch-with-stat::
+       Synonym for "-p --stat".
+       {git-format-patch? This is the default.}
+
+-z::
+       NUL-line termination on output.  This affects the --raw
+       output field terminator.  Also output from commands such
+       as "git-log" will be delimited with NUL between commits.
+
+--name-only::
+       Show only names of changed files.
+
+--name-status::
+       Show only names and status of changed files.
+
+--color::
+       Show colored diff.
+
+--no-color::
+       Turn off colored diff, even when the configuration file
+       gives the default to color output.
+
+--color-words::
+       Show colored word diff, i.e. color words which have changed.
+
+--no-renames::
+       Turn off rename detection, even when the configuration
+       file gives the default to do so.
+
+--check::
+       Warn if changes introduce trailing whitespace
+       or an indent that uses a space before a tab.
+
+--full-index::
+       Instead of the first handful characters, show full
+       object name of pre- and post-image blob on the "index"
+       line when generating a patch format output.
+
+--binary::
+       In addition to --full-index, output "binary diff" that
+       can be applied with "git apply".
+
+--abbrev[=<n>]::
+       Instead of showing the full 40-byte hexadecimal object
+       name in diff-raw format output and diff-tree header
+       lines, show only handful hexdigits prefix.  This is
+       independent of --full-index option above, which controls
+       the diff-patch output format.  Non default number of
+       digits can be specified with --abbrev=<n>.
+
+-B::
+       Break complete rewrite changes into pairs of delete and create.
+
+-M::
+       Detect renames.
+
+-C::
+       Detect copies as well as renames.  See also `--find-copies-harder`.
+
+--diff-filter=[ACDMRTUXB*]::
+       Select only files that are Added (`A`), Copied (`C`),
+       Deleted (`D`), Modified (`M`), Renamed (`R`), have their
+       type (mode) changed (`T`), are Unmerged (`U`), are
+       Unknown (`X`), or have had their pairing Broken (`B`).
+       Any combination of the filter characters may be used.
+       When `*` (All-or-none) is added to the combination, all
+       paths are selected if there is any file that matches
+       other criteria in the comparison; if there is no file
+       that matches other criteria, nothing is selected.
+
+--find-copies-harder::
+       For performance reasons, by default, `-C` option finds copies only
+       if the original file of the copy was modified in the same
+       changeset.  This flag makes the command
+       inspect unmodified files as candidates for the source of
+       copy.  This is a very expensive operation for large
+       projects, so use it with caution.  Giving more than one
+       `-C` option has the same effect.
+
+-l<num>::
+       -M and -C options require O(n^2) processing time where n
+       is the number of potential rename/copy targets.  This
+       option prevents rename/copy detection from running if
+       the number of rename/copy targets exceeds the specified
+       number.
+
+-S<string>::
+       Look for differences that contain the change in <string>.
+
+--pickaxe-all::
+       When -S finds a change, show all the changes in that
+       changeset, not just the files that contain the change
+       in <string>.
+
+--pickaxe-regex::
+       Make the <string> not a plain string but an extended POSIX
+       regex to match.
+
+-O<orderfile>::
+       Output the patch in the order specified in the
+       <orderfile>, which has one shell glob pattern per line.
+
+-R::
+       Swap two inputs; that is, show differences from index or
+       on-disk file to tree contents.
+
+--text::
+       Treat all files as text.
+
+-a::
+       Shorthand for "--text".
+
+--ignore-space-at-eol::
+       Ignore changes in white spaces at EOL.
+
+--ignore-space-change::
+       Ignore changes in amount of white space.  This ignores white
+       space at line end, and consider all other sequences of one or
+       more white space characters to be equivalent.
+
+-b::
+       Shorthand for "--ignore-space-change".
+
+--ignore-all-space::
+       Ignore white space when comparing lines.  This ignores
+       difference even if one line has white space where the other
+       line has none.
+
+-w::
+       Shorthand for "--ignore-all-space".
+
+--exit-code::
+       Make the program exit with codes similar to diff(1).
+       That is, it exits with 1 if there were differences and
+       0 means no differences.
+
+--quiet::
+       Disable all output of the program. Implies --exit-code.
+
+--ext-diff::
+       Allow an external diff helper to be executed. If you set an
+       external diff driver with gitlink:gitattributes[5], you need
+       to use this option with gitlink:git-log[1] and friends.
+
+--no-ext-diff::
+       Disallow external diff drivers.
+
+For more detailed explanation on these common options, see also
+link:diffcore.html[diffcore documentation].
diff --git a/Documentation/diffcore.txt b/Documentation/diffcore.txt
new file mode 100644 (file)
index 0000000..c6a983a
--- /dev/null
@@ -0,0 +1,271 @@
+Tweaking diff output
+====================
+June 2005
+
+
+Introduction
+------------
+
+The diff commands git-diff-index, git-diff-files, and git-diff-tree
+can be told to manipulate differences they find in
+unconventional ways before showing diff(1) output.  The manipulation
+is collectively called "diffcore transformation".  This short note
+describes what they are and how to use them to produce diff outputs
+that are easier to understand than the conventional kind.
+
+
+The chain of operation
+----------------------
+
+The git-diff-* family works by first comparing two sets of
+files:
+
+ - git-diff-index compares contents of a "tree" object and the
+   working directory (when '\--cached' flag is not used) or a
+   "tree" object and the index file (when '\--cached' flag is
+   used);
+
+ - git-diff-files compares contents of the index file and the
+   working directory;
+
+ - git-diff-tree compares contents of two "tree" objects;
+
+In all of these cases, the commands themselves compare
+corresponding paths in the two sets of files.  The result of
+comparison is passed from these commands to what is internally
+called "diffcore", in a format similar to what is output when
+the -p option is not used.  E.g.
+
+------------------------------------------------
+in-place edit  :100644 100644 bcd1234... 0123456... M file0
+create         :000000 100644 0000000... 1234567... A file4
+delete         :100644 000000 1234567... 0000000... D file5
+unmerged       :000000 000000 0000000... 0000000... U file6
+------------------------------------------------
+
+The diffcore mechanism is fed a list of such comparison results
+(each of which is called "filepair", although at this point each
+of them talks about a single file), and transforms such a list
+into another list.  There are currently 6 such transformations:
+
+- diffcore-pathspec
+- diffcore-break
+- diffcore-rename
+- diffcore-merge-broken
+- diffcore-pickaxe
+- diffcore-order
+
+These are applied in sequence.  The set of filepairs git-diff-\*
+commands find are used as the input to diffcore-pathspec, and
+the output from diffcore-pathspec is used as the input to the
+next transformation.  The final result is then passed to the
+output routine and generates either diff-raw format (see Output
+format sections of the manual for git-diff-\* commands) or
+diff-patch format.
+
+
+diffcore-pathspec: For Ignoring Files Outside Our Consideration
+---------------------------------------------------------------
+
+The first transformation in the chain is diffcore-pathspec, and
+is controlled by giving the pathname parameters to the
+git-diff-* commands on the command line.  The pathspec is used
+to limit the world diff operates in.  It removes the filepairs
+outside the specified set of pathnames.  E.g. If the input set
+of filepairs included:
+
+------------------------------------------------
+:100644 100644 bcd1234... 0123456... M junkfile
+------------------------------------------------
+
+but the command invocation was "git-diff-files myfile", then the
+junkfile entry would be removed from the list because only "myfile"
+is under consideration.
+
+Implementation note.  For performance reasons, git-diff-tree
+uses the pathname parameters on the command line to cull set of
+filepairs it feeds the diffcore mechanism itself, and does not
+use diffcore-pathspec, but the end result is the same.
+
+
+diffcore-break: For Splitting Up "Complete Rewrites"
+----------------------------------------------------
+
+The second transformation in the chain is diffcore-break, and is
+controlled by the -B option to the git-diff-* commands.  This is
+used to detect a filepair that represents "complete rewrite" and
+break such filepair into two filepairs that represent delete and
+create.  E.g.  If the input contained this filepair:
+
+------------------------------------------------
+:100644 100644 bcd1234... 0123456... M file0
+------------------------------------------------
+
+and if it detects that the file "file0" is completely rewritten,
+it changes it to:
+
+------------------------------------------------
+:100644 000000 bcd1234... 0000000... D file0
+:000000 100644 0000000... 0123456... A file0
+------------------------------------------------
+
+For the purpose of breaking a filepair, diffcore-break examines
+the extent of changes between the contents of the files before
+and after modification (i.e. the contents that have "bcd1234..."
+and "0123456..." as their SHA1 content ID, in the above
+example).  The amount of deletion of original contents and
+insertion of new material are added together, and if it exceeds
+the "break score", the filepair is broken into two.  The break
+score defaults to 50% of the size of the smaller of the original
+and the result (i.e. if the edit shrinks the file, the size of
+the result is used; if the edit lengthens the file, the size of
+the original is used), and can be customized by giving a number
+after "-B" option (e.g. "-B75" to tell it to use 75%).
+
+
+diffcore-rename: For Detection Renames and Copies
+-------------------------------------------------
+
+This transformation is used to detect renames and copies, and is
+controlled by the -M option (to detect renames) and the -C option
+(to detect copies as well) to the git-diff-* commands.  If the
+input contained these filepairs:
+
+------------------------------------------------
+:100644 000000 0123456... 0000000... D fileX
+:000000 100644 0000000... 0123456... A file0
+------------------------------------------------
+
+and the contents of the deleted file fileX is similar enough to
+the contents of the created file file0, then rename detection
+merges these filepairs and creates:
+
+------------------------------------------------
+:100644 100644 0123456... 0123456... R100 fileX file0
+------------------------------------------------
+
+When the "-C" option is used, the original contents of modified files,
+and deleted files (and also unmodified files, if the
+"\--find-copies-harder" option is used) are considered as candidates
+of the source files in rename/copy operation.  If the input were like
+these filepairs, that talk about a modified file fileY and a newly
+created file file0:
+
+------------------------------------------------
+:100644 100644 0123456... 1234567... M fileY
+:000000 100644 0000000... bcd3456... A file0
+------------------------------------------------
+
+the original contents of fileY and the resulting contents of
+file0 are compared, and if they are similar enough, they are
+changed to:
+
+------------------------------------------------
+:100644 100644 0123456... 1234567... M fileY
+:100644 100644 0123456... bcd3456... C100 fileY file0
+------------------------------------------------
+
+In both rename and copy detection, the same "extent of changes"
+algorithm used in diffcore-break is used to determine if two
+files are "similar enough", and can be customized to use
+a similarity score different from the default of 50% by giving a
+number after the "-M" or "-C" option (e.g. "-M8" to tell it to use
+8/10 = 80%).
+
+Note.  When the "-C" option is used with `\--find-copies-harder`
+option, git-diff-\* commands feed unmodified filepairs to
+diffcore mechanism as well as modified ones.  This lets the copy
+detector consider unmodified files as copy source candidates at
+the expense of making it slower.  Without `\--find-copies-harder`,
+git-diff-\* commands can detect copies only if the file that was
+copied happened to have been modified in the same changeset.
+
+
+diffcore-merge-broken: For Putting "Complete Rewrites" Back Together
+--------------------------------------------------------------------
+
+This transformation is used to merge filepairs broken by
+diffcore-break, and not transformed into rename/copy by
+diffcore-rename, back into a single modification.  This always
+runs when diffcore-break is used.
+
+For the purpose of merging broken filepairs back, it uses a
+different "extent of changes" computation from the ones used by
+diffcore-break and diffcore-rename.  It counts only the deletion
+from the original, and does not count insertion.  If you removed
+only 10 lines from a 100-line document, even if you added 910
+new lines to make a new 1000-line document, you did not do a
+complete rewrite.  diffcore-break breaks such a case in order to
+help diffcore-rename to consider such filepairs as candidate of
+rename/copy detection, but if filepairs broken that way were not
+matched with other filepairs to create rename/copy, then this
+transformation merges them back into the original
+"modification".
+
+The "extent of changes" parameter can be tweaked from the
+default 80% (that is, unless more than 80% of the original
+material is deleted, the broken pairs are merged back into a
+single modification) by giving a second number to -B option,
+like these:
+
+* -B50/60 (give 50% "break score" to diffcore-break, use 60%
+  for diffcore-merge-broken).
+
+* -B/60 (the same as above, since diffcore-break defaults to 50%).
+
+Note that earlier implementation left a broken pair as a separate
+creation and deletion patches.  This was an unnecessary hack and
+the latest implementation always merges all the broken pairs
+back into modifications, but the resulting patch output is
+formatted differently for easier review in case of such
+a complete rewrite by showing the entire contents of old version
+prefixed with '-', followed by the entire contents of new
+version prefixed with '+'.
+
+
+diffcore-pickaxe: For Detecting Addition/Deletion of Specified String
+---------------------------------------------------------------------
+
+This transformation is used to find filepairs that represent
+changes that touch a specified string, and is controlled by the
+-S option and the `\--pickaxe-all` option to the git-diff-*
+commands.
+
+When diffcore-pickaxe is in use, it checks if there are
+filepairs whose "original" side has the specified string and
+whose "result" side does not.  Such a filepair represents "the
+string appeared in this changeset".  It also checks for the
+opposite case that loses the specified string.
+
+When `\--pickaxe-all` is not in effect, diffcore-pickaxe leaves
+only such filepairs that touch the specified string in its
+output.  When `\--pickaxe-all` is used, diffcore-pickaxe leaves all
+filepairs intact if there is such a filepair, or makes the
+output empty otherwise.  The latter behaviour is designed to
+make reviewing of the changes in the context of the whole
+changeset easier.
+
+
+diffcore-order: For Sorting the Output Based on Filenames
+---------------------------------------------------------
+
+This is used to reorder the filepairs according to the user's
+(or project's) taste, and is controlled by the -O option to the
+git-diff-* commands.
+
+This takes a text file each of whose lines is a shell glob
+pattern.  Filepairs that match a glob pattern on an earlier line
+in the file are output before ones that match a later line, and
+filepairs that do not match any glob pattern are output last.
+
+As an example, a typical orderfile for the core git probably
+would look like this:
+
+------------------------------------------------
+README
+Makefile
+Documentation
+*.h
+*.c
+t
+------------------------------------------------
diff --git a/Documentation/docbook-xsl.css b/Documentation/docbook-xsl.css
new file mode 100644 (file)
index 0000000..b878b38
--- /dev/null
@@ -0,0 +1,286 @@
+/*
+  CSS stylesheet for XHTML produced by DocBook XSL stylesheets.
+  Tested with XSL stylesheets 1.61.2, 1.67.2
+*/
+
+span.strong {
+  font-weight: bold;
+}
+
+body blockquote {
+  margin-top: .75em;
+  line-height: 1.5;
+  margin-bottom: .75em;
+}
+
+html body {
+  margin: 1em 5% 1em 5%;
+  line-height: 1.2;
+}
+
+body div {
+  margin: 0;
+}
+
+h1, h2, h3, h4, h5, h6,
+div.toc p b,
+div.list-of-figures p b,
+div.list-of-tables p b,
+div.abstract p.title
+{
+  color: #527bbd;
+  font-family: tahoma, verdana, sans-serif;
+}
+
+div.toc p:first-child,
+div.list-of-figures p:first-child,
+div.list-of-tables p:first-child,
+div.example p.title
+{
+  margin-bottom: 0.2em;
+}
+
+body h1 {
+  margin: .0em 0 0 -4%;
+  line-height: 1.3;
+  border-bottom: 2px solid silver;
+}
+
+body h2 {
+  margin: 0.5em 0 0 -4%;
+  line-height: 1.3;
+  border-bottom: 2px solid silver;
+}
+
+body h3 {
+  margin: .8em 0 0 -3%;
+  line-height: 1.3;
+}
+
+body h4 {
+  margin: .8em 0 0 -3%;
+  line-height: 1.3;
+}
+
+body h5 {
+  margin: .8em 0 0 -2%;
+  line-height: 1.3;
+}
+
+body h6 {
+  margin: .8em 0 0 -1%;
+  line-height: 1.3;
+}
+
+body hr {
+  border: none; /* Broken on IE6 */
+}
+div.footnotes hr {
+  border: 1px solid silver;
+}
+
+div.navheader th, div.navheader td, div.navfooter td {
+  font-family: sans-serif;
+  font-size: 0.9em;
+  font-weight: bold;
+  color: #527bbd;
+}
+div.navheader img, div.navfooter img {
+  border-style: none;
+}
+div.navheader a, div.navfooter a {
+  font-weight: normal;
+}
+div.navfooter hr {
+  border: 1px solid silver;
+}
+
+body td {
+  line-height: 1.2
+}
+
+body th {
+  line-height: 1.2;
+}
+
+ol {
+  line-height: 1.2;
+}
+
+ul, body dir, body menu {
+  line-height: 1.2;
+}
+
+html {
+  margin: 0;
+  padding: 0;
+}
+
+body h1, body h2, body h3, body h4, body h5, body h6 {
+  margin-left: 0
+}
+
+body pre {
+  margin: 0.5em 10% 0.5em 1em;
+  line-height: 1.0;
+  color: navy;
+}
+
+tt.literal, code.literal {
+  color: navy;
+}
+
+div.literallayout p {
+  padding: 0em;
+  margin: 0em;
+}
+
+div.literallayout {
+  font-family: monospace;
+#  margin: 0.5em 10% 0.5em 1em;
+  margin: 0em;
+  color: navy;
+  border: 1px solid silver;
+  background: #f4f4f4;
+  padding: 0.5em;
+}
+
+.programlisting, .screen {
+  border: 1px solid silver;
+  background: #f4f4f4;
+  margin: 0.5em 10% 0.5em 0;
+  padding: 0.5em 1em;
+}
+
+div.sidebar {
+  background: #ffffee;
+  margin: 1.0em 10% 0.5em 0;
+  padding: 0.5em 1em;
+  border: 1px solid silver;
+}
+div.sidebar * { padding: 0; }
+div.sidebar div { margin: 0; }
+div.sidebar p.title {
+  font-family: sans-serif;
+  margin-top: 0.5em;
+  margin-bottom: 0.2em;
+}
+
+div.bibliomixed {
+  margin: 0.5em 5% 0.5em 1em;
+}
+
+div.glossary dt {
+  font-weight: bold;
+}
+div.glossary dd p {
+  margin-top: 0.2em;
+}
+
+dl {
+  margin: .8em 0;
+  line-height: 1.2;
+}
+
+dt {
+  margin-top: 0.5em;
+}
+
+dt span.term {
+  font-style: italic;
+}
+
+div.variablelist dd p {
+  margin-top: 0;
+}
+
+div.itemizedlist li, div.orderedlist li {
+  margin-left: -0.8em;
+  margin-top: 0.5em;
+}
+
+ul, ol {
+    list-style-position: outside;
+}
+
+div.sidebar ul, div.sidebar ol {
+    margin-left: 2.8em;
+}
+
+div.itemizedlist p.title,
+div.orderedlist p.title,
+div.variablelist p.title
+{
+  margin-bottom: -0.8em;
+}
+
+div.revhistory table {
+  border-collapse: collapse;
+  border: none;
+}
+div.revhistory th {
+  border: none;
+  color: #527bbd;
+  font-family: tahoma, verdana, sans-serif;
+}
+div.revhistory td {
+  border: 1px solid silver;
+}
+
+/* Keep TOC and index lines close together. */
+div.toc dl, div.toc dt,
+div.list-of-figures dl, div.list-of-figures dt,
+div.list-of-tables dl, div.list-of-tables dt,
+div.indexdiv dl, div.indexdiv dt
+{
+  line-height: normal;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+/*
+  Table styling does not work because of overriding attributes in
+  generated HTML.
+*/
+div.table table,
+div.informaltable table
+{
+    margin-left: 0;
+    margin-right: 5%;
+    margin-bottom: 0.8em;
+}
+div.informaltable table
+{
+    margin-top: 0.4em
+}
+div.table thead,
+div.table tfoot,
+div.table tbody,
+div.informaltable thead,
+div.informaltable tfoot,
+div.informaltable tbody
+{
+    /* No effect in IE6. */
+    border-top: 2px solid #527bbd;
+    border-bottom: 2px solid #527bbd;
+}
+div.table thead, div.table tfoot,
+div.informaltable thead, div.informaltable tfoot
+{
+    font-weight: bold;
+}
+
+div.mediaobject img {
+    border: 1px solid silver;
+    margin-bottom: 0.8em;
+}
+div.figure p.title,
+div.table p.title
+{
+  margin-top: 1em;
+  margin-bottom: 0.4em;
+}
+
+@media print {
+  div.navheader, div.navfooter { display: none; }
+}
diff --git a/Documentation/docbook.xsl b/Documentation/docbook.xsl
new file mode 100644 (file)
index 0000000..9a6912c
--- /dev/null
@@ -0,0 +1,5 @@
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+               version='1.0'>
+ <xsl:import href="http://docbook.sourceforge.net/release/xsl/current/html/docbook.xsl"/>
+ <xsl:output method="html" encoding="UTF-8" indent="no" />
+</xsl:stylesheet>
diff --git a/Documentation/everyday.txt b/Documentation/everyday.txt
new file mode 100644 (file)
index 0000000..ce7c170
--- /dev/null
@@ -0,0 +1,465 @@
+Everyday GIT With 20 Commands Or So
+===================================
+
+<<Basic Repository>> commands are needed by people who have a
+repository --- that is everybody, because every working tree of
+git is a repository.
+
+In addition, <<Individual Developer (Standalone)>> commands are
+essential for anybody who makes a commit, even for somebody who
+works alone.
+
+If you work with other people, you will need commands listed in
+the <<Individual Developer (Participant)>> section as well.
+
+People who play the <<Integrator>> role need to learn some more
+commands in addition to the above.
+
+<<Repository Administration>> commands are for system
+administrators who are responsible for the care and feeding
+of git repositories.
+
+
+Basic Repository[[Basic Repository]]
+------------------------------------
+
+Everybody uses these commands to maintain git repositories.
+
+  * gitlink:git-init[1] or gitlink:git-clone[1] to create a
+    new repository.
+
+  * gitlink:git-fsck[1] to check the repository for errors.
+
+  * gitlink:git-prune[1] to remove unused objects in the repository.
+
+  * gitlink:git-repack[1] to pack loose objects for efficiency.
+
+  * gitlink:git-gc[1] to do common housekeeping tasks such as
+    repack and prune.
+
+Examples
+~~~~~~~~
+
+Check health and remove cruft.::
++
+------------
+$ git fsck <1>
+$ git count-objects <2>
+$ git repack <3>
+$ git gc <4>
+------------
++
+<1> running without `\--full` is usually cheap and assures the
+repository health reasonably well.
+<2> check how many loose objects there are and how much
+disk space is wasted by not repacking.
+<3> without `-a` repacks incrementally.  repacking every 4-5MB
+of loose objects accumulation may be a good rule of thumb.
+<4> it is easier to use `git gc` than individual housekeeping commands
+such as `prune` and `repack`.  This runs `repack -a -d`.
+
+Repack a small project into single pack.::
++
+------------
+$ git repack -a -d <1>
+$ git prune
+------------
++
+<1> pack all the objects reachable from the refs into one pack,
+then remove the other packs.
+
+
+Individual Developer (Standalone)[[Individual Developer (Standalone)]]
+----------------------------------------------------------------------
+
+A standalone individual developer does not exchange patches with
+other people, and works alone in a single repository, using the
+following commands.
+
+  * gitlink:git-show-branch[1] to see where you are.
+
+  * gitlink:git-log[1] to see what happened.
+
+  * gitlink:git-checkout[1] and gitlink:git-branch[1] to switch
+    branches.
+
+  * gitlink:git-add[1] to manage the index file.
+
+  * gitlink:git-diff[1] and gitlink:git-status[1] to see what
+    you are in the middle of doing.
+
+  * gitlink:git-commit[1] to advance the current branch.
+
+  * gitlink:git-reset[1] and gitlink:git-checkout[1] (with
+    pathname parameters) to undo changes.
+
+  * gitlink:git-merge[1] to merge between local branches.
+
+  * gitlink:git-rebase[1] to maintain topic branches.
+
+  * gitlink:git-tag[1] to mark known point.
+
+Examples
+~~~~~~~~
+
+Use a tarball as a starting point for a new repository.::
++
+------------
+$ tar zxf frotz.tar.gz
+$ cd frotz
+$ git-init
+$ git add . <1>
+$ git commit -m "import of frotz source tree."
+$ git tag v2.43 <2>
+------------
++
+<1> add everything under the current directory.
+<2> make a lightweight, unannotated tag.
+
+Create a topic branch and develop.::
++
+------------
+$ git checkout -b alsa-audio <1>
+$ edit/compile/test
+$ git checkout -- curses/ux_audio_oss.c <2>
+$ git add curses/ux_audio_alsa.c <3>
+$ edit/compile/test
+$ git diff HEAD <4>
+$ git commit -a -s <5>
+$ edit/compile/test
+$ git reset --soft HEAD^ <6>
+$ edit/compile/test
+$ git diff ORIG_HEAD <7>
+$ git commit -a -c ORIG_HEAD <8>
+$ git checkout master <9>
+$ git merge alsa-audio <10>
+$ git log --since='3 days ago' <11>
+$ git log v2.43.. curses/ <12>
+------------
++
+<1> create a new topic branch.
+<2> revert your botched changes in `curses/ux_audio_oss.c`.
+<3> you need to tell git if you added a new file; removal and
+modification will be caught if you do `git commit -a` later.
+<4> to see what changes you are committing.
+<5> commit everything as you have tested, with your sign-off.
+<6> take the last commit back, keeping what is in the working tree.
+<7> look at the changes since the premature commit we took back.
+<8> redo the commit undone in the previous step, using the message
+you originally wrote.
+<9> switch to the master branch.
+<10> merge a topic branch into your master branch.
+<11> review commit logs; other forms to limit output can be
+combined and include `\--max-count=10` (show 10 commits),
+`\--until=2005-12-10`, etc.
+<12> view only the changes that touch what's in `curses/`
+directory, since `v2.43` tag.
+
+
+Individual Developer (Participant)[[Individual Developer (Participant)]]
+------------------------------------------------------------------------
+
+A developer working as a participant in a group project needs to
+learn how to communicate with others, and uses these commands in
+addition to the ones needed by a standalone developer.
+
+  * gitlink:git-clone[1] from the upstream to prime your local
+    repository.
+
+  * gitlink:git-pull[1] and gitlink:git-fetch[1] from "origin"
+    to keep up-to-date with the upstream.
+
+  * gitlink:git-push[1] to shared repository, if you adopt CVS
+    style shared repository workflow.
+
+  * gitlink:git-format-patch[1] to prepare e-mail submission, if
+    you adopt Linux kernel-style public forum workflow.
+
+Examples
+~~~~~~~~
+
+Clone the upstream and work on it.  Feed changes to upstream.::
++
+------------
+$ git clone git://git.kernel.org/pub/scm/.../torvalds/linux-2.6 my2.6
+$ cd my2.6
+$ edit/compile/test; git commit -a -s <1>
+$ git format-patch origin <2>
+$ git pull <3>
+$ git log -p ORIG_HEAD.. arch/i386 include/asm-i386 <4>
+$ git pull git://git.kernel.org/pub/.../jgarzik/libata-dev.git ALL <5>
+$ git reset --hard ORIG_HEAD <6>
+$ git prune <7>
+$ git fetch --tags <8>
+------------
++
+<1> repeat as needed.
+<2> extract patches from your branch for e-mail submission.
+<3> `git pull` fetches from `origin` by default and merges into the
+current branch.
+<4> immediately after pulling, look at the changes done upstream
+since last time we checked, only in the
+area we are interested in.
+<5> fetch from a specific branch from a specific repository and merge.
+<6> revert the pull.
+<7> garbage collect leftover objects from reverted pull.
+<8> from time to time, obtain official tags from the `origin`
+and store them under `.git/refs/tags/`.
+
+
+Push into another repository.::
++
+------------
+satellite$ git clone mothership:frotz frotz <1>
+satellite$ cd frotz
+satellite$ git config --get-regexp '^(remote|branch)\.' <2>
+remote.origin.url mothership:frotz
+remote.origin.fetch refs/heads/*:refs/remotes/origin/*
+branch.master.remote origin
+branch.master.merge refs/heads/master
+satellite$ git config remote.origin.push \
+           master:refs/remotes/satellite/master <3>
+satellite$ edit/compile/test/commit
+satellite$ git push origin <4>
+
+mothership$ cd frotz
+mothership$ git checkout master
+mothership$ git merge satellite/master <5>
+------------
++
+<1> mothership machine has a frotz repository under your home
+directory; clone from it to start a repository on the satellite
+machine.
+<2> clone sets these configuration variables by default.
+It arranges `git pull` to fetch and store the branches of mothership
+machine to local `remotes/origin/*` tracking branches.
+<3> arrange `git push` to push local `master` branch to
+`remotes/satellite/master` branch of the mothership machine.
+<4> push will stash our work away on `remotes/satellite/master`
+tracking branch on the mothership machine.  You could use this as
+a back-up method.
+<5> on mothership machine, merge the work done on the satellite
+machine into the master branch.
+
+Branch off of a specific tag.::
++
+------------
+$ git checkout -b private2.6.14 v2.6.14 <1>
+$ edit/compile/test; git commit -a
+$ git checkout master
+$ git format-patch -k -m --stdout v2.6.14..private2.6.14 |
+  git am -3 -k <2>
+------------
++
+<1> create a private branch based on a well known (but somewhat behind)
+tag.
+<2> forward port all changes in `private2.6.14` branch to `master` branch
+without a formal "merging".
+
+
+Integrator[[Integrator]]
+------------------------
+
+A fairly central person acting as the integrator in a group
+project receives changes made by others, reviews and integrates
+them and publishes the result for others to use, using these
+commands in addition to the ones needed by participants.
+
+  * gitlink:git-am[1] to apply patches e-mailed in from your
+    contributors.
+
+  * gitlink:git-pull[1] to merge from your trusted lieutenants.
+
+  * gitlink:git-format-patch[1] to prepare and send suggested
+    alternative to contributors.
+
+  * gitlink:git-revert[1] to undo botched commits.
+
+  * gitlink:git-push[1] to publish the bleeding edge.
+
+
+Examples
+~~~~~~~~
+
+My typical GIT day.::
++
+------------
+$ git status <1>
+$ git show-branch <2>
+$ mailx <3>
+& s 2 3 4 5 ./+to-apply
+& s 7 8 ./+hold-linus
+& q
+$ git checkout -b topic/one master
+$ git am -3 -i -s -u ./+to-apply <4>
+$ compile/test
+$ git checkout -b hold/linus && git am -3 -i -s -u ./+hold-linus <5>
+$ git checkout topic/one && git rebase master <6>
+$ git checkout pu && git reset --hard next <7>
+$ git merge topic/one topic/two && git merge hold/linus <8>
+$ git checkout maint
+$ git cherry-pick master~4 <9>
+$ compile/test
+$ git tag -s -m "GIT 0.99.9x" v0.99.9x <10>
+$ git fetch ko && git show-branch master maint 'tags/ko-*' <11>
+$ git push ko <12>
+$ git push ko v0.99.9x <13>
+------------
++
+<1> see what I was in the middle of doing, if any.
+<2> see what topic branches I have and think about how ready
+they are.
+<3> read mails, save ones that are applicable, and save others
+that are not quite ready.
+<4> apply them, interactively, with my sign-offs.
+<5> create topic branch as needed and apply, again with my
+sign-offs.
+<6> rebase internal topic branch that has not been merged to the
+master, nor exposed as a part of a stable branch.
+<7> restart `pu` every time from the next.
+<8> and bundle topic branches still cooking.
+<9> backport a critical fix.
+<10> create a signed tag.
+<11> make sure I did not accidentally rewind master beyond what I
+already pushed out.  `ko` shorthand points at the repository I have
+at kernel.org, and looks like this:
++
+------------
+$ cat .git/remotes/ko
+URL: kernel.org:/pub/scm/git/git.git
+Pull: master:refs/tags/ko-master
+Pull: next:refs/tags/ko-next
+Pull: maint:refs/tags/ko-maint
+Push: master
+Push: next
+Push: +pu
+Push: maint
+------------
++
+In the output from `git show-branch`, `master` should have
+everything `ko-master` has, and `next` should have
+everything `ko-next` has.
+
+<12> push out the bleeding edge.
+<13> push the tag out, too.
+
+
+Repository Administration[[Repository Administration]]
+------------------------------------------------------
+
+A repository administrator uses the following tools to set up
+and maintain access to the repository by developers.
+
+  * gitlink:git-daemon[1] to allow anonymous download from
+    repository.
+
+  * gitlink:git-shell[1] can be used as a 'restricted login shell'
+    for shared central repository users.
+
+link:howto/update-hook-example.txt[update hook howto] has a good
+example of managing a shared central repository.
+
+
+Examples
+~~~~~~~~
+We assume the following in /etc/services::
++
+------------
+$ grep 9418 /etc/services
+git            9418/tcp                # Git Version Control System
+------------
+
+Run git-daemon to serve /pub/scm from inetd.::
++
+------------
+$ grep git /etc/inetd.conf
+git    stream  tcp     nowait  nobody \
+  /usr/bin/git-daemon git-daemon --inetd --export-all /pub/scm
+------------
++
+The actual configuration line should be on one line.
+
+Run git-daemon to serve /pub/scm from xinetd.::
++
+------------
+$ cat /etc/xinetd.d/git-daemon
+# default: off
+# description: The git server offers access to git repositories
+service git
+{
+        disable = no
+        type            = UNLISTED
+        port            = 9418
+        socket_type     = stream
+        wait            = no
+        user            = nobody
+        server          = /usr/bin/git-daemon
+        server_args     = --inetd --export-all --base-path=/pub/scm
+        log_on_failure  += USERID
+}
+------------
++
+Check your xinetd(8) documentation and setup, this is from a Fedora system.
+Others might be different.
+
+Give push/pull only access to developers.::
++
+------------
+$ grep git /etc/passwd <1>
+alice:x:1000:1000::/home/alice:/usr/bin/git-shell
+bob:x:1001:1001::/home/bob:/usr/bin/git-shell
+cindy:x:1002:1002::/home/cindy:/usr/bin/git-shell
+david:x:1003:1003::/home/david:/usr/bin/git-shell
+$ grep git /etc/shells <2>
+/usr/bin/git-shell
+------------
++
+<1> log-in shell is set to /usr/bin/git-shell, which does not
+allow anything but `git push` and `git pull`.  The users should
+get an ssh access to the machine.
+<2> in many distributions /etc/shells needs to list what is used
+as the login shell.
+
+CVS-style shared repository.::
++
+------------
+$ grep git /etc/group <1>
+git:x:9418:alice,bob,cindy,david
+$ cd /home/devo.git
+$ ls -l <2>
+  lrwxrwxrwx   1 david git    17 Dec  4 22:40 HEAD -> refs/heads/master
+  drwxrwsr-x   2 david git  4096 Dec  4 22:40 branches
+  -rw-rw-r--   1 david git    84 Dec  4 22:40 config
+  -rw-rw-r--   1 david git    58 Dec  4 22:40 description
+  drwxrwsr-x   2 david git  4096 Dec  4 22:40 hooks
+  -rw-rw-r--   1 david git 37504 Dec  4 22:40 index
+  drwxrwsr-x   2 david git  4096 Dec  4 22:40 info
+  drwxrwsr-x   4 david git  4096 Dec  4 22:40 objects
+  drwxrwsr-x   4 david git  4096 Nov  7 14:58 refs
+  drwxrwsr-x   2 david git  4096 Dec  4 22:40 remotes
+$ ls -l hooks/update <3>
+  -r-xr-xr-x   1 david git  3536 Dec  4 22:40 update
+$ cat info/allowed-users <4>
+refs/heads/master      alice\|cindy
+refs/heads/doc-update  bob
+refs/tags/v[0-9]*      david
+------------
++
+<1> place the developers into the same git group.
+<2> and make the shared repository writable by the group.
+<3> use update-hook example by Carl from Documentation/howto/
+for branch policy control.
+<4> alice and cindy can push into master, only bob can push into doc-update.
+david is the release manager and is the only person who can
+create and push version tags.
+
+HTTP server to support dumb protocol transfer.::
++
+------------
+dev$ git update-server-info <1>
+dev$ ftp user@isp.example.com <2>
+ftp> cp -r .git /home/user/myproject.git
+------------
++
+<1> make sure your info/refs and objects/info/packs are up-to-date
+<2> upload to public HTTP server hosted by your ISP.
diff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt
new file mode 100644 (file)
index 0000000..da03422
--- /dev/null
@@ -0,0 +1,54 @@
+-q, \--quiet::
+       Pass --quiet to git-fetch-pack and silence any other internally
+       used programs.
+
+-v, \--verbose::
+       Be verbose.
+
+-a, \--append::
+       Append ref names and object names of fetched refs to the
+       existing contents of `.git/FETCH_HEAD`.  Without this
+       option old data in `.git/FETCH_HEAD` will be overwritten.
+
+\--upload-pack <upload-pack>::
+       When given, and the repository to fetch from is handled
+       by 'git-fetch-pack', '--exec=<upload-pack>' is passed to
+       the command to specify non-default path for the command
+       run on the other end.
+
+-f, \--force::
+       When `git-fetch` is used with `<rbranch>:<lbranch>`
+       refspec, it refuses to update the local branch
+       `<lbranch>` unless the remote branch `<rbranch>` it
+       fetches is a descendant of `<lbranch>`.  This option
+       overrides that check.
+
+-n, \--no-tags::
+       By default, `git-fetch` fetches tags that point at
+       objects that are downloaded from the remote repository
+       and stores them locally.  This option disables this
+       automatic tag following.
+
+-t, \--tags::
+       Most of the tags are fetched automatically as branch
+       heads are downloaded, but tags that do not point at
+       objects reachable from the branch heads that are being
+       tracked will not be fetched by this mechanism.  This
+       flag lets all tags and their associated objects be
+       downloaded.
+
+-k, \--keep::
+       Keep downloaded pack.
+
+-u, \--update-head-ok::
+       By default `git-fetch` refuses to update the head which
+       corresponds to the current branch.  This flag disables the
+       check.  This is purely for the internal use for `git-pull`
+       to communicate with `git-fetch`, and unless you are
+       implementing your own Porcelain you are not supposed to
+       use it.
+
+\--depth=<depth>::
+       Deepen the history of a 'shallow' repository created by
+       `git clone` with `--depth=<depth>` option (see gitlink:git-clone[1])
+       by the specified number of commits.
diff --git a/Documentation/fix-texi.perl b/Documentation/fix-texi.perl
new file mode 100755 (executable)
index 0000000..ff7d78f
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/perl -w
+
+while (<>) {
+       if (/^\@setfilename/) {
+               $_ = "\@setfilename git.info\n";
+       } elsif (/^\@direntry/) {
+               print '@dircategory Development
+@direntry
+* Git: (git).           A fast distributed revision control system
+@end direntry
+';     }
+       unless (/^\@direntry/../^\@end direntry/) {
+               print;
+       }
+}
diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt
new file mode 100644 (file)
index 0000000..bf94cd4
--- /dev/null
@@ -0,0 +1,251 @@
+git-add(1)
+==========
+
+NAME
+----
+git-add - Add file contents to the index
+
+SYNOPSIS
+--------
+[verse]
+'git-add' [-n] [-v] [-f] [--interactive | -i] [--patch | -p] [-u] [--refresh]
+          [--] <filepattern>...
+
+DESCRIPTION
+-----------
+This command adds the current content of new or modified files to the
+index, thus staging that content for inclusion in the next commit.
+
+The "index" holds a snapshot of the content of the working tree, and it
+is this snapshot that is taken as the contents of the next commit.  Thus
+after making any changes to the working directory, and before running
+the commit command, you must use the 'add' command to add any new or
+modified files to the index.
+
+This command can be performed multiple times before a commit.  It only
+adds the content of the specified file(s) at the time the add command is
+run; if you want subsequent changes included in the next commit, then
+you must run 'git add' again to add the new content to the index.
+
+The 'git status' command can be used to obtain a summary of which
+files have changes that are staged for the next commit.
+
+The 'git add' command will not add ignored files by default.  If any
+ignored files were explicitly specified on the command line, 'git add'
+will fail with a list of ignored files.  Ignored files reached by
+directory recursion or filename globbing performed by Git (quote your
+globs before the shell) will be silently ignored.  The 'add' command can
+be used to add ignored files with the `-f` (force) option.
+
+Please see gitlink:git-commit[1] for alternative ways to add content to a
+commit.
+
+
+OPTIONS
+-------
+<filepattern>...::
+       Files to add content from.  Fileglobs (e.g. `*.c`) can
+       be given to add all matching files.  Also a
+       leading directory name (e.g. `dir` to add `dir/file1`
+       and `dir/file2`) can be given to add all files in the
+       directory, recursively.
+
+-n, \--dry-run::
+        Don't actually add the file(s), just show if they exist.
+
+-v, \--verbose::
+        Be verbose.
+
+-f::
+       Allow adding otherwise ignored files.
+
+-i, \--interactive::
+       Add modified contents in the working tree interactively to
+       the index. Optional path arguments may be supplied to limit
+       operation to a subset of the working tree. See ``Interactive
+       mode'' for details.
+
+-p, \--patch:
+       Similar to Interactive mode but the initial command loop is
+       bypassed and the 'patch' subcommand is invoked using each of
+       the specified filepatterns before exiting.
+
+-u::
+       Update only files that git already knows about. This is similar
+       to what "git commit -a" does in preparation for making a commit,
+       except that the update is limited to paths specified on the
+       command line. If no paths are specified, all tracked files are
+       updated.
+
+\--refresh::
+       Don't add the file(s), but only refresh their stat()
+       information in the index.
+
+\--::
+       This option can be used to separate command-line options from
+       the list of files, (useful when filenames might be mistaken
+       for command-line options).
+
+
+Configuration
+-------------
+
+The optional configuration variable 'core.excludesfile' indicates a path to a
+file containing patterns of file names to exclude from git-add, similar to
+$GIT_DIR/info/exclude.  Patterns in the exclude file are used in addition to
+those in info/exclude.  See link:repository-layout.html[repository layout].
+
+
+EXAMPLES
+--------
+git-add Documentation/\\*.txt::
+
+       Adds content from all `\*.txt` files under `Documentation`
+       directory and its subdirectories.
++
+Note that the asterisk `\*` is quoted from the shell in this
+example; this lets the command to include the files from
+subdirectories of `Documentation/` directory.
+
+git-add git-*.sh::
+
+       Considers adding content from all git-*.sh scripts.
+       Because this example lets shell expand the asterisk
+       (i.e. you are listing the files explicitly), it does not
+       consider `subdir/git-foo.sh`.
+
+Interactive mode
+----------------
+When the command enters the interactive mode, it shows the
+output of the 'status' subcommand, and then goes into its
+interactive command loop.
+
+The command loop shows the list of subcommands available, and
+gives a prompt "What now> ".  In general, when the prompt ends
+with a single '>', you can pick only one of the choices given
+and type return, like this:
+
+------------
+    *** Commands ***
+      1: status       2: update       3: revert       4: add untracked
+      5: patch        6: diff         7: quit         8: help
+    What now> 1
+------------
+
+You also could say "s" or "sta" or "status" above as long as the
+choice is unique.
+
+The main command loop has 6 subcommands (plus help and quit).
+
+status::
+
+   This shows the change between HEAD and index (i.e. what will be
+   committed if you say "git commit"), and between index and
+   working tree files (i.e. what you could stage further before
+   "git commit" using "git-add") for each path.  A sample output
+   looks like this:
++
+------------
+              staged     unstaged path
+     1:       binary      nothing foo.png
+     2:     +403/-35        +1/-1 git-add--interactive.perl
+------------
++
+It shows that foo.png has differences from HEAD (but that is
+binary so line count cannot be shown) and there is no
+difference between indexed copy and the working tree
+version (if the working tree version were also different,
+'binary' would have been shown in place of 'nothing').  The
+other file, git-add--interactive.perl, has 403 lines added
+and 35 lines deleted if you commit what is in the index, but
+working tree file has further modifications (one addition and
+one deletion).
+
+update::
+
+   This shows the status information and gives prompt
+   "Update>>".  When the prompt ends with double '>>', you can
+   make more than one selection, concatenated with whitespace or
+   comma.  Also you can say ranges.  E.g. "2-5 7,9" to choose
+   2,3,4,5,7,9 from the list.  You can say '*' to choose
+   everything.
++
+What you chose are then highlighted with '*',
+like this:
++
+------------
+           staged     unstaged path
+  1:       binary      nothing foo.png
+* 2:     +403/-35        +1/-1 git-add--interactive.perl
+------------
++
+To remove selection, prefix the input with `-`
+like this:
++
+------------
+Update>> -2
+------------
++
+After making the selection, answer with an empty line to stage the
+contents of working tree files for selected paths in the index.
+
+revert::
+
+  This has a very similar UI to 'update', and the staged
+  information for selected paths are reverted to that of the
+  HEAD version.  Reverting new paths makes them untracked.
+
+add untracked::
+
+  This has a very similar UI to 'update' and
+  'revert', and lets you add untracked paths to the index.
+
+patch::
+
+  This lets you choose one path out of 'status' like selection.
+  After choosing the path, it presents diff between the index
+  and the working tree file and asks you if you want to stage
+  the change of each hunk.  You can say:
+
+       y - add the change from that hunk to index
+       n - do not add the change from that hunk to index
+       a - add the change from that hunk and all the rest to index
+       d - do not the change from that hunk nor any of the rest to index
+       j - do not decide on this hunk now, and view the next
+           undecided hunk
+       J - do not decide on this hunk now, and view the next hunk
+       k - do not decide on this hunk now, and view the previous
+           undecided hunk
+       K - do not decide on this hunk now, and view the previous hunk
+       s - split the current hunk into smaller hunks
+       ? - print help
++
+After deciding the fate for all hunks, if there is any hunk
+that was chosen, the index is updated with the selected hunks.
+
+diff::
+
+  This lets you review what will be committed (i.e. between
+  HEAD and index).
+
+
+See Also
+--------
+gitlink:git-status[1]
+gitlink:git-rm[1]
+gitlink:git-reset[1]
+gitlink:git-mv[1]
+gitlink:git-commit[1]
+gitlink:git-update-index[1]
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt
new file mode 100644 (file)
index 0000000..e4a6b3a
--- /dev/null
@@ -0,0 +1,160 @@
+git-am(1)
+=========
+
+NAME
+----
+git-am - Apply a series of patches from a mailbox
+
+
+SYNOPSIS
+--------
+[verse]
+'git-am' [--signoff] [--dotest=<dir>] [--keep] [--utf8 | --no-utf8]
+         [--3way] [--interactive] [--binary]
+         [--whitespace=<option>] [-C<n>] [-p<n>]
+         <mbox>|<Maildir>...
+'git-am' [--skip | --resolved]
+
+DESCRIPTION
+-----------
+Splits mail messages in a mailbox into commit log message,
+authorship information and patches, and applies them to the
+current branch.
+
+OPTIONS
+-------
+<mbox>|<Maildir>...::
+       The list of mailbox files to read patches from. If you do not
+       supply this argument, reads from the standard input. If you supply
+       directories, they'll be treated as Maildirs.
+
+-s, --signoff::
+       Add `Signed-off-by:` line to the commit message, using
+       the committer identity of yourself.
+
+-d=<dir>, --dotest=<dir>::
+       Instead of `.dotest` directory, use <dir> as a working
+       area to store extracted patches.
+
+-k, --keep::
+       Pass `-k` flag to `git-mailinfo` (see gitlink:git-mailinfo[1]).
+
+-u, --utf8::
+       Pass `-u` flag to `git-mailinfo` (see gitlink:git-mailinfo[1]).
+       The proposed commit log message taken from the e-mail
+       is re-coded into UTF-8 encoding (configuration variable
+       `i18n.commitencoding` can be used to specify project's
+       preferred encoding if it is not UTF-8).
++
+This was optional in prior versions of git, but now it is the
+default.   You could use `--no-utf8` to override this.
+
+--no-utf8::
+       Pass `-n` flag to `git-mailinfo` (see
+       gitlink:git-mailinfo[1]).
+
+-3, --3way::
+       When the patch does not apply cleanly, fall back on
+       3-way merge, if the patch records the identity of blobs
+       it is supposed to apply to, and we have those blobs
+       available locally.
+
+-b, --binary::
+       Pass `--allow-binary-replacement` flag to `git-apply`
+       (see gitlink:git-apply[1]).
+
+--whitespace=<option>::
+       This flag is passed to the `git-apply` (see gitlink:git-apply[1])
+       program that applies
+       the patch.
+
+-C<n>, -p<n>::
+       These flags are passed to the `git-apply` (see gitlink:git-apply[1])
+       program that applies
+       the patch.
+
+-i, --interactive::
+       Run interactively.
+
+--skip::
+       Skip the current patch.  This is only meaningful when
+       restarting an aborted patch.
+
+-r, --resolved::
+       After a patch failure (e.g. attempting to apply
+       conflicting patch), the user has applied it by hand and
+       the index file stores the result of the application.
+       Make a commit using the authorship and commit log
+       extracted from the e-mail message and the current index
+       file, and continue.
+
+--resolvemsg=<msg>::
+       When a patch failure occurs, <msg> will be printed
+       to the screen before exiting.  This overrides the
+       standard message informing you to use `--resolved`
+       or `--skip` to handle the failure.  This is solely
+       for internal use between `git-rebase` and `git-am`.
+
+DISCUSSION
+----------
+
+The commit author name is taken from the "From: " line of the
+message, and commit author time is taken from the "Date: " line
+of the message.  The "Subject: " line is used as the title of
+the commit, after stripping common prefix "[PATCH <anything>]".
+It is supposed to describe what the commit is about concisely as
+a one line text.
+
+The body of the message (iow, after a blank line that terminates
+RFC2822 headers) can begin with "Subject: " and "From: " lines
+that are different from those of the mail header, to override
+the values of these fields.
+
+The commit message is formed by the title taken from the
+"Subject: ", a blank line and the body of the message up to
+where the patch begins.  Excess whitespaces at the end of the
+lines are automatically stripped.
+
+The patch is expected to be inline, directly following the
+message.  Any line that is of form:
+
+* three-dashes and end-of-line, or
+* a line that begins with "diff -", or
+* a line that begins with "Index: "
+
+is taken as the beginning of a patch, and the commit log message
+is terminated before the first occurrence of such a line.
+
+When initially invoking it, you give it names of the mailboxes
+to crunch.  Upon seeing the first patch that does not apply, it
+aborts in the middle,.  You can recover from this in one of two ways:
+
+. skip the current patch by re-running the command with '--skip'
+  option.
+
+. hand resolve the conflict in the working directory, and update
+  the index file to bring it in a state that the patch should
+  have produced.  Then run the command with '--resolved' option.
+
+The command refuses to process new mailboxes while `.dotest`
+directory exists, so if you decide to start over from scratch,
+run `rm -f .dotest` before running the command with mailbox
+names.
+
+
+SEE ALSO
+--------
+gitlink:git-apply[1].
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Petr Baudis, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-annotate.txt b/Documentation/git-annotate.txt
new file mode 100644 (file)
index 0000000..02dc474
--- /dev/null
@@ -0,0 +1,31 @@
+git-annotate(1)
+===============
+
+NAME
+----
+git-annotate - Annotate file lines with commit info
+
+SYNOPSIS
+--------
+git-annotate [options] file [revision]
+
+DESCRIPTION
+-----------
+Annotates each line in the given file with information from the commit
+which introduced the line. Optionally annotate from a given revision.
+
+OPTIONS
+-------
+include::blame-options.txt[]
+
+SEE ALSO
+--------
+gitlink:git-blame[1]
+
+AUTHOR
+------
+Written by Ryan Anderson <ryan@michonline.com>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-apply.txt b/Documentation/git-apply.txt
new file mode 100644 (file)
index 0000000..c1c54bf
--- /dev/null
@@ -0,0 +1,202 @@
+git-apply(1)
+============
+
+NAME
+----
+git-apply - Apply a patch on a git index file and a working tree
+
+
+SYNOPSIS
+--------
+[verse]
+'git-apply' [--stat] [--numstat] [--summary] [--check] [--index]
+         [--apply] [--no-add] [--build-fake-ancestor <file>] [-R | --reverse]
+         [--allow-binary-replacement | --binary] [--reject] [-z]
+         [-pNUM] [-CNUM] [--inaccurate-eof] [--cached]
+         [--whitespace=<nowarn|warn|error|error-all|strip>]
+         [--exclude=PATH] [--verbose] [<patch>...]
+
+DESCRIPTION
+-----------
+Reads supplied diff output and applies it on a git index file
+and a work tree.
+
+OPTIONS
+-------
+<patch>...::
+       The files to read patch from.  '-' can be used to read
+       from the standard input.
+
+--stat::
+       Instead of applying the patch, output diffstat for the
+       input.  Turns off "apply".
+
+--numstat::
+       Similar to \--stat, but shows number of added and
+       deleted lines in decimal notation and pathname without
+       abbreviation, to make it more machine friendly.  For
+       binary files, outputs two `-` instead of saying
+       `0 0`.  Turns off "apply".
+
+--summary::
+       Instead of applying the patch, output a condensed
+       summary of information obtained from git diff extended
+       headers, such as creations, renames and mode changes.
+       Turns off "apply".
+
+--check::
+       Instead of applying the patch, see if the patch is
+       applicable to the current work tree and/or the index
+       file and detects errors.  Turns off "apply".
+
+--index::
+       When --check is in effect, or when applying the patch
+       (which is the default when none of the options that
+       disables it is in effect), make sure the patch is
+       applicable to what the current index file records.  If
+       the file to be patched in the work tree is not
+       up-to-date, it is flagged as an error.  This flag also
+       causes the index file to be updated.
+
+--cached::
+       Apply a patch without touching the working tree. Instead, take the
+       cached data, apply the patch, and store the result in the index,
+       without using the working tree. This implies '--index'.
+
+--build-fake-ancestor <file>::
+       Newer git-diff output has embedded 'index information'
+       for each blob to help identify the original version that
+       the patch applies to.  When this flag is given, and if
+       the original versions of the blobs is available locally,
+       builds a temporary index containing those blobs.
++
+When a pure mode change is encountered (which has no index information),
+the information is read from the current index instead.
+
+-R, --reverse::
+       Apply the patch in reverse.
+
+--reject::
+       For atomicity, gitlink:git-apply[1] by default fails the whole patch and
+       does not touch the working tree when some of the hunks
+       do not apply.  This option makes it apply
+       the parts of the patch that are applicable, and leave the
+       rejected hunks in corresponding *.rej files.
+
+-z::
+       When showing the index information, do not munge paths,
+       but use NUL terminated machine readable format.  Without
+       this flag, the pathnames output will have TAB, LF, and
+       backslash characters replaced with `\t`, `\n`, and `\\`,
+       respectively.
+
+-p<n>::
+       Remove <n> leading slashes from traditional diff paths. The
+       default is 1.
+
+-C<n>::
+       Ensure at least <n> lines of surrounding context match before
+       and after each change.  When fewer lines of surrounding
+       context exist they all must match.  By default no context is
+       ever ignored.
+
+--unidiff-zero::
+       By default, gitlink:git-apply[1] expects that the patch being
+       applied is a unified diff with at least one line of context.
+       This provides good safety measures, but breaks down when
+       applying a diff generated with --unified=0. To bypass these
+       checks use '--unidiff-zero'.
++
+Note, for the reasons stated above usage of context-free patches are
+discouraged.
+
+--apply::
+       If you use any of the options marked "Turns off
+       'apply'" above, gitlink:git-apply[1] reads and outputs the
+       information you asked without actually applying the
+       patch.  Give this flag after those flags to also apply
+       the patch.
+
+--no-add::
+       When applying a patch, ignore additions made by the
+       patch.  This can be used to extract common part between
+       two files by first running `diff` on them and applying
+       the result with this option, which would apply the
+       deletion part but not addition part.
+
+--allow-binary-replacement, --binary::
+       Historically we did not allow binary patch applied
+       without an explicit permission from the user, and this
+       flag was the way to do so.  Currently we always allow binary
+       patch application, so this is a no-op.
+
+--exclude=<path-pattern>::
+       Don't apply changes to files matching the given path pattern. This can
+       be useful when importing patchsets, where you want to exclude certain
+       files or directories.
+
+--whitespace=<option>::
+       When applying a patch, detect a new or modified line
+       that ends with trailing whitespaces (this includes a
+       line that solely consists of whitespaces).  By default,
+       the command outputs warning messages and applies the
+       patch.
+       When gitlink:git-apply[1] is used for statistics and not applying a
+       patch, it defaults to `nowarn`.
+       You can use different `<option>` to control this
+       behavior:
++
+* `nowarn` turns off the trailing whitespace warning.
+* `warn` outputs warnings for a few such errors, but applies the
+  patch (default).
+* `error` outputs warnings for a few such errors, and refuses
+  to apply the patch.
+* `error-all` is similar to `error` but shows all errors.
+* `strip` outputs warnings for a few such errors, strips out the
+  trailing whitespaces and applies the patch.
+
+--inaccurate-eof::
+       Under certain circumstances, some versions of diff do not correctly
+       detect a missing new-line at the end of the file. As a result, patches
+       created by such diff programs do not record incomplete lines
+       correctly. This option adds support for applying such patches by
+       working around this bug.
+
+-v, --verbose::
+       Report progress to stderr. By default, only a message about the
+       current patch being applied will be printed. This option will cause
+       additional information to be reported.
+
+Configuration
+-------------
+
+apply.whitespace::
+       When no `--whitespace` flag is given from the command
+       line, this configuration item is used as the default.
+
+Submodules
+----------
+If the patch contains any changes to submodules then gitlink:git-apply[1]
+treats these changes as follows.
+
+If --index is specified (explicitly or implicitly), then the submodule
+commits must match the index exactly for the patch to apply.  If any
+of the submodules are checked-out, then these check-outs are completely
+ignored, i.e., they are not required to be up-to-date or clean and they
+are not updated.
+
+If --index is not specified, then the submodule commits in the patch
+are ignored and only the absence of presence of the corresponding
+subdirectory is checked and (if possible) updated.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-archimport.txt b/Documentation/git-archimport.txt
new file mode 100644 (file)
index 0000000..7091b8d
--- /dev/null
@@ -0,0 +1,120 @@
+git-archimport(1)
+=================
+
+NAME
+----
+git-archimport - Import an Arch repository into git
+
+
+SYNOPSIS
+--------
+[verse]
+'git-archimport' [-h] [-v] [-o] [-a] [-f] [-T] [-D depth] [-t tempdir]
+               <archive/branch>[:<git-branch>] ...
+
+DESCRIPTION
+-----------
+Imports a project from one or more Arch repositories. It will follow branches
+and repositories within the namespaces defined by the <archive/branch>
+parameters supplied. If it cannot find the remote branch a merge comes from
+it will just import it as a regular commit. If it can find it, it will mark it
+as a merge whenever possible (see discussion below).
+
+The script expects you to provide the key roots where it can start the import
+from an 'initial import' or 'tag' type of Arch commit. It will follow and
+import new branches within the provided roots.
+
+It expects to be dealing with one project only. If it sees
+branches that have different roots, it will refuse to run. In that case,
+edit your <archive/branch> parameters to define clearly the scope of the
+import.
+
+`git-archimport` uses `tla` extensively in the background to access the
+Arch repository.
+Make sure you have a recent version of `tla` available in the path. `tla` must
+know about the repositories you pass to `git-archimport`.
+
+For the initial import `git-archimport` expects to find itself in an empty
+directory. To follow the development of a project that uses Arch, rerun
+`git-archimport` with the same parameters as the initial import to perform
+incremental imports.
+
+While git-archimport will try to create sensible branch names for the
+archives that it imports, it is also possible to specify git branch names
+manually.  To do so, write a git branch name after each <archive/branch>
+parameter, separated by a colon.  This way, you can shorten the Arch
+branch names and convert Arch jargon to git jargon, for example mapping a
+"PROJECT--devo--VERSION" branch to "master".
+
+Associating multiple Arch branches to one git branch is possible; the
+result will make the most sense only if no commits are made to the first
+branch, after the second branch is created.  Still, this is useful to
+convert Arch repositories that had been rotated periodically.
+
+
+MERGES
+------
+Patch merge data from Arch is used to mark merges in git as well. git
+does not care much about tracking patches, and only considers a merge when a
+branch incorporates all the commits since the point they forked. The end result
+is that git will have a good idea of how far branches have diverged. So the
+import process does lose some patch-trading metadata.
+
+Fortunately, when you try and merge branches imported from Arch,
+git will find a good merge base, and it has a good chance of identifying
+patches that have been traded out-of-sequence between the branches.
+
+OPTIONS
+-------
+
+-h::
+       Display usage.
+
+-v::
+       Verbose output.
+
+-T::
+       Many tags. Will create a tag for every commit, reflecting the commit
+       name in the Arch repository.
+
+-f::
+       Use the fast patchset import strategy.  This can be significantly
+       faster for large trees, but cannot handle directory renames or
+       permissions changes.  The default strategy is slow and safe.
+
+-o::
+       Use this for compatibility with old-style branch names used by
+       earlier versions of git-archimport.  Old-style branch names
+       were category--branch, whereas new-style branch names are
+       archive,category--branch--version.  In both cases, names given
+       on the command-line will override the automatically-generated
+       ones.
+
+-D <depth>::
+       Follow merge ancestry and attempt to import trees that have been
+       merged from.  Specify a depth greater than 1 if patch logs have been
+       pruned.
+
+-a::
+       Attempt to auto-register archives at http://mirrors.sourcecontrol.net
+       This is particularly useful with the -D option.
+
+-t <tmpdir>::
+       Override the default tempdir.
+
+
+<archive/branch>::
+       Archive/branch identifier in a format that `tla log` understands.
+
+
+Author
+------
+Written by Martin Langhoff <martin@catalyst.net.nz>.
+
+Documentation
+--------------
+Documentation by Junio C Hamano, Martin Langhoff and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-archive.txt b/Documentation/git-archive.txt
new file mode 100644 (file)
index 0000000..7cd6526
--- /dev/null
@@ -0,0 +1,121 @@
+git-archive(1)
+==============
+
+NAME
+----
+git-archive - Create an archive of files from a named tree
+
+
+SYNOPSIS
+--------
+[verse]
+'git-archive' --format=<fmt> [--list] [--prefix=<prefix>/] [<extra>]
+             [--remote=<repo> [--exec=<git-upload-archive>]] <tree-ish>
+             [path...]
+
+DESCRIPTION
+-----------
+Creates an archive of the specified format containing the tree
+structure for the named tree, and writes it out to the standard
+output.  If <prefix> is specified it is
+prepended to the filenames in the archive.
+
+'git-archive' behaves differently when given a tree ID versus when
+given a commit ID or tag ID.  In the first case the current time is
+used as modification time of each file in the archive.  In the latter
+case the commit time as recorded in the referenced commit object is
+used instead.  Additionally the commit ID is stored in a global
+extended pax header if the tar format is used; it can be extracted
+using 'git-get-tar-commit-id'. In ZIP files it is stored as a file
+comment.
+
+OPTIONS
+-------
+
+--format=<fmt>::
+       Format of the resulting archive: 'tar' or 'zip'.  The default
+       is 'tar'.
+
+--list, -l::
+       Show all available formats.
+
+--verbose, -v::
+       Report progress to stderr.
+
+--prefix=<prefix>/::
+       Prepend <prefix>/ to each filename in the archive.
+
+<extra>::
+       This can be any options that the archiver backend understand.
+       See next section.
+
+--remote=<repo>::
+       Instead of making a tar archive from local repository,
+       retrieve a tar archive from a remote repository.
+
+--exec=<git-upload-archive>::
+       Used with --remote to specify the path to the
+       git-upload-archive executable on the remote side.
+
+<tree-ish>::
+       The tree or commit to produce an archive for.
+
+path::
+       If one or more paths are specified, include only these in the
+       archive, otherwise include all files and subdirectories.
+
+BACKEND EXTRA OPTIONS
+---------------------
+
+zip
+~~~
+-0::
+       Store the files instead of deflating them.
+-9::
+       Highest and slowest compression level.  You can specify any
+       number from 1 to 9 to adjust compression speed and ratio.
+
+
+CONFIGURATION
+-------------
+
+tar.umask::
+       This variable can be used to restrict the permission bits of
+       tar archive entries.  The default is 0002, which turns off the
+       world write bit.  The special value "user" indicates that the
+       archiving user's umask will be used instead.  See umask(2) for
+       details.
+
+EXAMPLES
+--------
+git archive --format=tar --prefix=junk/ HEAD | (cd /var/tmp/ && tar xf -)::
+
+       Create a tar archive that contains the contents of the
+       latest commit on the current branch, and extracts it in
+       `/var/tmp/junk` directory.
+
+git archive --format=tar --prefix=git-1.4.0/ v1.4.0 | gzip >git-1.4.0.tar.gz::
+
+       Create a compressed tarball for v1.4.0 release.
+
+git archive --format=tar --prefix=git-1.4.0/ v1.4.0{caret}\{tree\} | gzip >git-1.4.0.tar.gz::
+
+       Create a compressed tarball for v1.4.0 release, but without a
+       global extended pax header.
+
+git archive --format=zip --prefix=git-docs/ HEAD:Documentation/ > git-1.4.0-docs.zip::
+
+       Put everything in the current head's Documentation/ directory
+       into 'git-1.4.0-docs.zip', with the prefix 'git-docs/'.
+
+Author
+------
+Written by Franck Bui-Huu and Rene Scharfe.
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-bisect.txt b/Documentation/git-bisect.txt
new file mode 100644 (file)
index 0000000..4795349
--- /dev/null
@@ -0,0 +1,221 @@
+git-bisect(1)
+=============
+
+NAME
+----
+git-bisect - Find the change that introduced a bug by binary search
+
+
+SYNOPSIS
+--------
+'git bisect' <subcommand> <options>
+
+DESCRIPTION
+-----------
+The command takes various subcommands, and different options depending
+on the subcommand:
+
+ git bisect start [<bad> [<good>...]] [--] [<paths>...]
+ git bisect bad [<rev>]
+ git bisect good [<rev>...]
+ git bisect skip [<rev>...]
+ git bisect reset [<branch>]
+ git bisect visualize
+ git bisect replay <logfile>
+ git bisect log
+ git bisect run <cmd>...
+
+This command uses 'git-rev-list --bisect' option to help drive the
+binary search process to find which change introduced a bug, given an
+old "good" commit object name and a later "bad" commit object name.
+
+Basic bisect commands: start, bad, good
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The way you use it is:
+
+------------------------------------------------
+$ git bisect start
+$ git bisect bad                 # Current version is bad
+$ git bisect good v2.6.13-rc2    # v2.6.13-rc2 was the last version
+                                 # tested that was good
+------------------------------------------------
+
+When you give at least one bad and one good versions, it will bisect
+the revision tree and say something like:
+
+------------------------------------------------
+Bisecting: 675 revisions left to test after this
+------------------------------------------------
+
+and check out the state in the middle. Now, compile that kernel, and
+boot it. Now, let's say that this booted kernel works fine, then just
+do
+
+------------------------------------------------
+$ git bisect good                      # this one is good
+------------------------------------------------
+
+which will now say
+
+------------------------------------------------
+Bisecting: 337 revisions left to test after this
+------------------------------------------------
+
+and you continue along, compiling that one, testing it, and depending
+on whether it is good or bad, you say "git bisect good" or "git bisect
+bad", and ask for the next bisection.
+
+Until you have no more left, and you'll have been left with the first
+bad kernel rev in "refs/bisect/bad".
+
+Bisect reset
+~~~~~~~~~~~~
+
+Oh, and then after you want to reset to the original head, do a
+
+------------------------------------------------
+$ git bisect reset
+------------------------------------------------
+
+to get back to the master branch, instead of being in one of the
+bisection branches ("git bisect start" will do that for you too,
+actually: it will reset the bisection state, and before it does that
+it checks that you're not using some old bisection branch).
+
+Bisect visualize
+~~~~~~~~~~~~~~~~
+
+During the bisection process, you can say
+
+------------
+$ git bisect visualize
+------------
+
+to see the currently remaining suspects in `gitk`.
+
+Bisect log and bisect replay
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The good/bad input is logged, and
+
+------------
+$ git bisect log
+------------
+
+shows what you have done so far. You can truncate its output somewhere
+and save it in a file, and run
+
+------------
+$ git bisect replay that-file
+------------
+
+if you find later you made a mistake telling good/bad about a
+revision.
+
+Avoiding to test a commit
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If in a middle of bisect session, you know what the bisect suggested
+to try next is not a good one to test (e.g. the change the commit
+introduces is known not to work in your environment and you know it
+does not have anything to do with the bug you are chasing), you may
+want to find a near-by commit and try that instead.
+
+It goes something like this:
+
+------------
+$ git bisect good/bad                  # previous round was good/bad.
+Bisecting: 337 revisions left to test after this
+$ git bisect visualize                 # oops, that is uninteresting.
+$ git reset --hard HEAD~3              # try 3 revs before what
+                                       # was suggested
+------------
+
+Then compile and test the one you chose to try. After that, tell
+bisect what the result was as usual.
+
+Bisect skip
+~~~~~~~~~~~~
+
+Instead of choosing by yourself a nearby commit, you may just want git
+to do it for you using:
+
+------------
+$ git bisect skip                 # Current version cannot be tested
+------------
+
+But computing the commit to test may be slower afterwards and git may
+eventually not be able to tell the first bad among a bad and one or
+more "skip"ped commits.
+
+Cutting down bisection by giving more parameters to bisect start
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can further cut down the number of trials if you know what part of
+the tree is involved in the problem you are tracking down, by giving
+paths parameters when you say `bisect start`, like this:
+
+------------
+$ git bisect start -- arch/i386 include/asm-i386
+------------
+
+If you know beforehand more than one good commits, you can narrow the
+bisect space down without doing the whole tree checkout every time you
+give good commits. You give the bad revision immediately after `start`
+and then you give all the good revisions you have:
+
+------------
+$ git bisect start v2.6.20-rc6 v2.6.20-rc4 v2.6.20-rc1 --
+                   # v2.6.20-rc6 is bad
+                   # v2.6.20-rc4 and v2.6.20-rc1 are good
+------------
+
+Bisect run
+~~~~~~~~~~
+
+If you have a script that can tell if the current source code is good
+or bad, you can automatically bisect using:
+
+------------
+$ git bisect run my_script
+------------
+
+Note that the "run" script (`my_script` in the above example) should
+exit with code 0 in case the current source code is good.  Exit with a
+code between 1 and 127 (inclusive), except 125, if the current
+source code is bad.
+
+Any other exit code will abort the automatic bisect process. (A
+program that does "exit(-1)" leaves $? = 255, see exit(3) manual page,
+the value is chopped with "& 0377".)
+
+The special exit code 125 should be used when the current source code
+cannot be tested. If the "run" script exits with this code, the current
+revision will be skipped, see `git bisect skip` above.
+
+You may often find that during bisect you want to have near-constant
+tweaks (e.g., s/#define DEBUG 0/#define DEBUG 1/ in a header file, or
+"revision that does not have this commit needs this patch applied to
+work around other problem this bisection is not interested in")
+applied to the revision being tested.
+
+To cope with such a situation, after the inner git-bisect finds the
+next revision to test, with the "run" script, you can apply that tweak
+before compiling, run the real test, and after the test decides if the
+revision (possibly with the needed tweaks) passed the test, rewind the
+tree to the pristine state.  Finally the "run" script can exit with
+the status of the real test to let "git bisect run" command loop to
+know the outcome.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+-------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-blame.txt b/Documentation/git-blame.txt
new file mode 100644 (file)
index 0000000..66f1203
--- /dev/null
@@ -0,0 +1,195 @@
+git-blame(1)
+============
+
+NAME
+----
+git-blame - Show what revision and author last modified each line of a file
+
+SYNOPSIS
+--------
+[verse]
+'git-blame' [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-p] [-w] [--incremental] [-L n,m]
+            [-S <revs-file>] [-M] [-C] [-C] [--since=<date>]
+            [<rev> | --contents <file>] [--] <file>
+
+DESCRIPTION
+-----------
+
+Annotates each line in the given file with information from the revision which
+last modified the line. Optionally, start annotating from the given revision.
+
+Also it can limit the range of lines annotated.
+
+This report doesn't tell you anything about lines which have been deleted or
+replaced; you need to use a tool such as gitlink:git-diff[1] or the "pickaxe"
+interface briefly mentioned in the following paragraph.
+
+Apart from supporting file annotation, git also supports searching the
+development history for when a code snippet occurred in a change. This makes it
+possible to track when a code snippet was added to a file, moved or copied
+between files, and eventually deleted or replaced. It works by searching for
+a text string in the diff. A small example:
+
+-----------------------------------------------------------------------------
+$ git log --pretty=oneline -S'blame_usage'
+5040f17eba15504bad66b14a645bddd9b015ebb7 blame -S <ancestry-file>
+ea4c7f9bf69e781dd0cd88d2bccb2bf5cc15c9a7 git-blame: Make the output
+-----------------------------------------------------------------------------
+
+OPTIONS
+-------
+include::blame-options.txt[]
+
+-c::
+       Use the same output mode as gitlink:git-annotate[1] (Default: off).
+
+--score-debug::
+       Include debugging information related to the movement of
+       lines between files (see `-C`) and lines moved within a
+       file (see `-M`).  The first number listed is the score.
+       This is the number of alphanumeric characters detected
+       to be moved between or within files.  This must be above
+       a certain threshold for git-blame to consider those lines
+       of code to have been moved.
+
+-f, --show-name::
+       Show filename in the original commit.  By default
+       filename is shown if there is any line that came from a
+       file with different name, due to rename detection.
+
+-n, --show-number::
+       Show line number in the original commit (Default: off).
+
+-s::
+       Suppress author name and timestamp from the output.
+
+-w::
+       Ignore whitespace when comparing parent's version and
+       child's to find where the lines came from.
+
+
+THE PORCELAIN FORMAT
+--------------------
+
+In this format, each line is output after a header; the
+header at the minimum has the first line which has:
+
+- 40-byte SHA-1 of the commit the line is attributed to;
+- the line number of the line in the original file;
+- the line number of the line in the final file;
+- on a line that starts a group of line from a different
+  commit than the previous one, the number of lines in this
+  group.  On subsequent lines this field is absent.
+
+This header line is followed by the following information
+at least once for each commit:
+
+- author name ("author"), email ("author-mail"), time
+  ("author-time"), and timezone ("author-tz"); similarly
+  for committer.
+- filename in the commit the line is attributed to.
+- the first line of the commit log message ("summary").
+
+The contents of the actual line is output after the above
+header, prefixed by a TAB. This is to allow adding more
+header elements later.
+
+
+SPECIFYING RANGES
+-----------------
+
+Unlike `git-blame` and `git-annotate` in older git, the extent
+of annotation can be limited to both line ranges and revision
+ranges.  When you are interested in finding the origin for
+ll. 40-60 for file `foo`, you can use `-L` option like these
+(they mean the same thing -- both ask for 21 lines starting at
+line 40):
+
+       git blame -L 40,60 foo
+       git blame -L 40,+21 foo
+
+Also you can use regular expression to specify the line range.
+
+       git blame -L '/^sub hello {/,/^}$/' foo
+
+would limit the annotation to the body of `hello` subroutine.
+
+When you are not interested in changes older than the version
+v2.6.18, or changes older than 3 weeks, you can use revision
+range specifiers  similar to `git-rev-list`:
+
+       git blame v2.6.18.. -- foo
+       git blame --since=3.weeks -- foo
+
+When revision range specifiers are used to limit the annotation,
+lines that have not changed since the range boundary (either the
+commit v2.6.18 or the most recent commit that is more than 3
+weeks old in the above example) are blamed for that range
+boundary commit.
+
+A particularly useful way is to see if an added file have lines
+created by copy-and-paste from existing files.  Sometimes this
+indicates that the developer was being sloppy and did not
+refactor the code properly.  You can first find the commit that
+introduced the file with:
+
+       git log --diff-filter=A --pretty=short -- foo
+
+and then annotate the change between the commit and its
+parents, using `commit{caret}!` notation:
+
+       git blame -C -C -f $commit^! -- foo
+
+
+INCREMENTAL OUTPUT
+------------------
+
+When called with `--incremental` option, the command outputs the
+result as it is built.  The output generally will talk about
+lines touched by more recent commits first (i.e. the lines will
+be annotated out of order) and is meant to be used by
+interactive viewers.
+
+The output format is similar to the Porcelain format, but it
+does not contain the actual lines from the file that is being
+annotated.
+
+. Each blame entry always starts with a line of:
+
+       <40-byte hex sha1> <sourceline> <resultline> <num_lines>
++
+Line numbers count from 1.
+
+. The first time that commit shows up in the stream, it has various
+  other information about it printed out with a one-word tag at the
+  beginning of each line about that "extended commit info" (author,
+  email, committer, dates, summary etc).
+
+. Unlike Porcelain format, the filename information is always
+  given and terminates the entry:
+
+       "filename" <whitespace-quoted-filename-goes-here>
++
+and thus it's really quite easy to parse for some line- and word-oriented
+parser (which should be quite natural for most scripting languages).
++
+[NOTE]
+For people who do parsing: to make it more robust, just ignore any
+lines in between the first and last one ("<sha1>" and "filename" lines)
+where you don't recognize the tag-words (or care about that particular
+one) at the beginning of the "extended information" lines. That way, if
+there is ever added information (like the commit encoding or extended
+commit commentary), a blame viewer won't ever care.
+
+
+SEE ALSO
+--------
+gitlink:git-annotate[1]
+
+AUTHOR
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt
new file mode 100644 (file)
index 0000000..d3f21c7
--- /dev/null
@@ -0,0 +1,190 @@
+git-branch(1)
+=============
+
+NAME
+----
+git-branch - List, create, or delete branches
+
+SYNOPSIS
+--------
+[verse]
+'git-branch' [--color | --no-color] [-r | -a]
+          [-v [--abbrev=<length> | --no-abbrev]]
+          [--contains <commit>]
+'git-branch' [--track | --no-track] [-l] [-f] <branchname> [<start-point>]
+'git-branch' (-m | -M) [<oldbranch>] <newbranch>
+'git-branch' (-d | -D) [-r] <branchname>...
+
+DESCRIPTION
+-----------
+With no arguments given a list of existing branches
+will be shown, the current branch will be highlighted with an asterisk.
+Option `-r` causes the remote-tracking branches to be listed,
+and option `-a` shows both.
+With `--contains <commit>`, shows only the branches that
+contains the named commit (in other words, the branches whose
+tip commits are descendant of the named commit).
+
+In its second form, a new branch named <branchname> will be created.
+It will start out with a head equal to the one given as <start-point>.
+If no <start-point> is given, the branch will be created with a head
+equal to that of the currently checked out branch.
+
+Note that this will create the new branch, but it will not switch the
+working tree to it; use "git checkout <newbranch>" to switch to the
+new branch.
+
+When a local branch is started off a remote branch, git can setup the
+branch so that gitlink:git-pull[1] will appropriately merge from that
+remote branch.  If this behavior is desired, it is possible to make it
+the default using the global `branch.autosetupmerge` configuration
+flag.  Otherwise, it can be chosen per-branch using the `--track`
+and `--no-track` options.
+
+With a '-m' or '-M' option, <oldbranch> will be renamed to <newbranch>.
+If <oldbranch> had a corresponding reflog, it is renamed to match
+<newbranch>, and a reflog entry is created to remember the branch
+renaming. If <newbranch> exists, -M must be used to force the rename
+to happen.
+
+With a `-d` or `-D` option, `<branchname>` will be deleted.  You may
+specify more than one branch for deletion.  If the branch currently
+has a reflog then the reflog will also be deleted.
+
+Use -r together with -d to delete remote-tracking branches. Note, that it
+only makes sense to delete remote-tracking branches if they no longer exist
+in remote repository or if gitlink:git-fetch[1] was configured not to fetch
+them again. See also 'prune' subcommand of gitlink:git-remote[1] for way to
+clean up all obsolete remote-tracking branches.
+
+
+OPTIONS
+-------
+-d::
+       Delete a branch. The branch must be fully merged in HEAD.
+
+-D::
+       Delete a branch irrespective of its merged status.
+
+-l::
+       Create the branch's reflog.  This activates recording of
+       all changes made to the branch ref, enabling use of date
+       based sha1 expressions such as "<branchname>@\{yesterday}".
+
+-f::
+       Force the creation of a new branch even if it means deleting
+       a branch that already exists with the same name.
+
+-m::
+       Move/rename a branch and the corresponding reflog.
+
+-M::
+       Move/rename a branch even if the new branchname already exists.
+
+--color::
+       Color branches to highlight current, local, and remote branches.
+
+--no-color::
+       Turn off branch colors, even when the configuration file gives the
+       default to color output.
+
+-r::
+       List or delete (if used with -d) the remote-tracking branches.
+
+-a::
+       List both remote-tracking branches and local branches.
+
+-v, --verbose::
+       Show sha1 and commit subject line for each head.
+
+--abbrev=<length>::
+       Alter minimum display length for sha1 in output listing,
+       default value is 7.
+
+--no-abbrev::
+       Display the full sha1s in output listing rather than abbreviating them.
+
+--track::
+       Set up configuration so that git-pull will automatically
+       retrieve data from the remote branch.  Use this if you always
+       pull from the same remote branch into the new branch, or if you
+       don't want to use "git pull <repository> <refspec>" explicitly.  Set the
+       branch.autosetupmerge configuration variable to true if you
+       want git-checkout and git-branch to always behave as if
+       '--track' were given.
+
+--no-track::
+       When a branch is created off a remote branch,
+       set up configuration so that git-pull will not retrieve data
+       from the remote branch, ignoring the branch.autosetupmerge
+       configuration variable.
+
+<branchname>::
+       The name of the branch to create or delete.
+       The new branch name must pass all checks defined by
+       gitlink:git-check-ref-format[1].  Some of these checks
+       may restrict the characters allowed in a branch name.
+
+<start-point>::
+       The new branch will be created with a HEAD equal to this.  It may
+       be given as a branch name, a commit-id, or a tag.  If this option
+       is omitted, the current branch is assumed.
+
+<oldbranch>::
+       The name of an existing branch to rename.
+
+<newbranch>::
+       The new name for an existing branch. The same restrictions as for
+       <branchname> applies.
+
+
+Examples
+--------
+
+Start development off of a known tag::
++
+------------
+$ git clone git://git.kernel.org/pub/scm/.../linux-2.6 my2.6
+$ cd my2.6
+$ git branch my2.6.14 v2.6.14   <1>
+$ git checkout my2.6.14
+------------
++
+<1> This step and the next one could be combined into a single step with
+"checkout -b my2.6.14 v2.6.14".
+
+Delete unneeded branch::
++
+------------
+$ git clone git://git.kernel.org/.../git.git my.git
+$ cd my.git
+$ git branch -d -r origin/todo origin/html origin/man   <1>
+$ git branch -D test                                    <2>
+------------
++
+<1> Delete remote-tracking branches "todo", "html", "man". Next 'fetch' or
+'pull' will create them again unless you configure them not to. See
+gitlink:git-fetch[1].
+<2> Delete "test" branch even if the "master" branch (or whichever branch is
+currently checked out) does not have all commits from test branch.
+
+
+Notes
+-----
+
+If you are creating a branch that you want to immediately checkout, it's
+easier to use the git checkout command with its `-b` option to create
+a branch and check it out with a single command.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org> and Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-bundle.txt b/Documentation/git-bundle.txt
new file mode 100644 (file)
index 0000000..0cc6511
--- /dev/null
@@ -0,0 +1,148 @@
+git-bundle(1)
+=============
+
+NAME
+----
+git-bundle - Move objects and refs by archive
+
+
+SYNOPSIS
+--------
+[verse]
+'git-bundle' create <file> [git-rev-list args]
+'git-bundle' verify <file>
+'git-bundle' list-heads <file> [refname...]
+'git-bundle' unbundle <file> [refname...]
+
+DESCRIPTION
+-----------
+
+Some workflows require that one or more branches of development on one
+machine be replicated on another machine, but the two machines cannot
+be directly connected so the interactive git protocols (git, ssh,
+rsync, http) cannot be used.  This command provides support for
+git-fetch and git-pull to operate by packaging objects and references
+in an archive at the originating machine, then importing those into
+another repository using gitlink:git-fetch[1] and gitlink:git-pull[1]
+after moving the archive by some means (i.e., by sneakernet).  As no
+direct connection between repositories exists, the user must specify a
+basis for the bundle that is held by the destination repository: the
+bundle assumes that all objects in the basis are already in the
+destination repository.
+
+OPTIONS
+-------
+
+create <file>::
+       Used to create a bundle named 'file'.  This requires the
+       git-rev-list arguments to define the bundle contents.
+
+verify <file>::
+       Used to check that a bundle file is valid and will apply
+       cleanly to the current repository.  This includes checks on the
+       bundle format itself as well as checking that the prerequisite
+       commits exist and are fully linked in the current repository.
+       git-bundle prints a list of missing commits, if any, and exits
+       with non-zero status.
+
+list-heads <file>::
+       Lists the references defined in the bundle.  If followed by a
+       list of references, only references matching those given are
+       printed out.
+
+unbundle <file>::
+       Passes the objects in the bundle to gitlink:git-index-pack[1]
+       for storage in the repository, then prints the names of all
+       defined references. If a reflist is given, only references
+       matching those in the given list are printed. This command is
+       really plumbing, intended to be called only by
+       gitlink:git-fetch[1].
+
+[git-rev-list-args...]::
+       A list of arguments, acceptable to git-rev-parse and
+       git-rev-list, that specify the specific objects and references
+       to transport.  For example, "master~10..master" causes the
+       current master reference to be packaged along with all objects
+       added since its 10th ancestor commit.  There is no explicit
+       limit to the number of references and objects that may be
+       packaged.
+
+
+[refname...]::
+       A list of references used to limit the references reported as
+       available. This is principally of use to git-fetch, which
+       expects to receive only those references asked for and not
+       necessarily everything in the pack (in this case, git-bundle is
+       acting like gitlink:git-fetch-pack[1]).
+
+SPECIFYING REFERENCES
+---------------------
+
+git-bundle will only package references that are shown by
+git-show-ref: this includes heads, tags, and remote heads.  References
+such as master~1 cannot be packaged, but are perfectly suitable for
+defining the basis.  More than one reference may be packaged, and more
+than one basis can be specified.  The objects packaged are those not
+contained in the union of the given bases.  Each basis can be
+specified explicitly (e.g., ^master~10), or implicitly (e.g.,
+master~10..master, master --since=10.days.ago).
+
+It is very important that the basis used be held by the destination.
+It is okay to err on the side of conservatism, causing the bundle file
+to contain objects already in the destination as these are ignored
+when unpacking at the destination.
+
+EXAMPLE
+-------
+
+Assume two repositories exist as R1 on machine A, and R2 on machine B.
+For whatever reason, direct connection between A and B is not allowed,
+but we can move data from A to B via some mechanism (CD, email, etc).
+We want to update R2 with developments made on branch master in R1.
+We set a tag in R1 (lastR2bundle) after the previous such transport,
+and move it afterwards to help build the bundle.
+
+in R1 on A:
+
+------------
+$ git-bundle create mybundle master ^lastR2bundle
+$ git tag -f lastR2bundle master
+------------
+
+(move mybundle from A to B by some mechanism)
+
+in R2 on B:
+
+------------
+$ git-bundle verify mybundle
+$ git-fetch mybundle  refspec
+------------
+
+where refspec is refInBundle:localRef
+
+
+Also, with something like this in your config:
+
+[remote "bundle"]
+    url = /home/me/tmp/file.bdl
+    fetch = refs/heads/*:refs/remotes/origin/*
+
+You can first sneakernet the bundle file to ~/tmp/file.bdl and
+then these commands:
+
+------------
+$ git ls-remote bundle
+$ git fetch bundle
+$ git pull bundle
+------------
+
+would treat it as if it is talking with a remote side over the
+network.
+
+Author
+------
+Written by Mark Levedahl <mdl123@verizon.net>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-cat-file.txt b/Documentation/git-cat-file.txt
new file mode 100644 (file)
index 0000000..afa095c
--- /dev/null
@@ -0,0 +1,73 @@
+git-cat-file(1)
+===============
+
+NAME
+----
+git-cat-file - Provide content or type/size information for repository objects
+
+
+SYNOPSIS
+--------
+'git-cat-file' [-t | -s | -e | -p | <type>] <object>
+
+DESCRIPTION
+-----------
+Provides content or type of objects in the repository. The type
+is required unless '-t' or '-p' is used to find the object type,
+or '-s' is used to find the object size.
+
+OPTIONS
+-------
+<object>::
+       The name of the object to show.
+       For a more complete list of ways to spell object names, see
+       "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1].
+
+-t::
+       Instead of the content, show the object type identified by
+       <object>.
+
+-s::
+       Instead of the content, show the object size identified by
+       <object>.
+
+-e::
+       Suppress all output; instead exit with zero status if <object>
+       exists and is a valid object.
+
+-p::
+       Pretty-print the contents of <object> based on its type.
+
+<type>::
+       Typically this matches the real type of <object> but asking
+       for a type that can trivially be dereferenced from the given
+       <object> is also permitted.  An example is to ask for a
+       "tree" with <object> being a commit object that contains it,
+       or to ask for a "blob" with <object> being a tag object that
+       points at it.
+
+OUTPUT
+------
+If '-t' is specified, one of the <type>.
+
+If '-s' is specified, the size of the <object> in bytes.
+
+If '-e' is specified, no output.
+
+If '-p' is specified, the contents of <object> are pretty-printed.
+
+Otherwise the raw (though uncompressed) contents of the <object> will
+be returned.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-check-attr.txt b/Documentation/git-check-attr.txt
new file mode 100644 (file)
index 0000000..856d2af
--- /dev/null
@@ -0,0 +1,36 @@
+git-check-attr(1)
+=================
+
+NAME
+----
+git-check-attr - Display gitattributes information.
+
+
+SYNOPSIS
+--------
+'git-check-attr' attr... [--] pathname...
+
+DESCRIPTION
+-----------
+For every pathname, this command will list if each attr is 'unspecified',
+'set', or 'unset' as a gitattribute on that pathname.
+
+OPTIONS
+-------
+\--::
+       Interpret all preceding arguments as attributes, and all following
+       arguments as path names. If not supplied, only the first argument will
+       be treated as an attribute.
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by James Bowes.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-check-ref-format.txt b/Documentation/git-check-ref-format.txt
new file mode 100644 (file)
index 0000000..13a5f43
--- /dev/null
@@ -0,0 +1,55 @@
+git-check-ref-format(1)
+=======================
+
+NAME
+----
+git-check-ref-format - Make sure ref name is well formed
+
+SYNOPSIS
+--------
+'git-check-ref-format' <refname>
+
+DESCRIPTION
+-----------
+Checks if a given 'refname' is acceptable, and exits non-zero if
+it is not.
+
+A reference is used in git to specify branches and tags.  A
+branch head is stored under `$GIT_DIR/refs/heads` directory, and
+a tag is stored under `$GIT_DIR/refs/tags` directory.  git
+imposes the following rules on how refs are named:
+
+. It can include slash `/` for hierarchical (directory)
+  grouping, but no slash-separated component can begin with a
+  dot `.`;
+
+. It cannot have two consecutive dots `..` anywhere;
+
+. It cannot have ASCII control character (i.e. bytes whose
+  values are lower than \040, or \177 `DEL`), space, tilde `~`,
+  caret `{caret}`, colon `:`, question-mark `?`, asterisk `*`,
+  or open bracket `[` anywhere;
+
+. It cannot end with a slash `/`.
+
+These rules makes it easy for shell script based tools to parse
+refnames, pathname expansion by the shell when a refname is used
+unquoted (by mistake), and also avoids ambiguities in certain
+refname expressions (see gitlink:git-rev-parse[1]).  Namely:
+
+. double-dot `..` are often used as in `ref1..ref2`, and in some
+  context this notation means `{caret}ref1 ref2` (i.e. not in
+  ref1 and in ref2).
+
+. tilde `~` and caret `{caret}` are used to introduce postfix
+  'nth parent' and 'peel onion' operation.
+
+. colon `:` is used as in `srcref:dstref` to mean "use srcref\'s
+  value and store it in dstref" in fetch and push operations.
+  It may also be used to select a specific object such as with
+  gitlink:git-cat-file[1] "git-cat-file blob v1.3.3:refs.c".
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-checkout-index.txt b/Documentation/git-checkout-index.txt
new file mode 100644 (file)
index 0000000..b1a8ce1
--- /dev/null
@@ -0,0 +1,184 @@
+git-checkout-index(1)
+=====================
+
+NAME
+----
+git-checkout-index - Copy files from the index to the working tree
+
+
+SYNOPSIS
+--------
+[verse]
+'git-checkout-index' [-u] [-q] [-a] [-f] [-n] [--prefix=<string>]
+                  [--stage=<number>|all]
+                  [--temp]
+                  [-z] [--stdin]
+                  [--] [<file>]\*
+
+DESCRIPTION
+-----------
+Will copy all files listed from the index to the working directory
+(not overwriting existing files).
+
+OPTIONS
+-------
+-u|--index::
+       update stat information for the checked out entries in
+       the index file.
+
+-q|--quiet::
+       be quiet if files exist or are not in the index
+
+-f|--force::
+       forces overwrite of existing files
+
+-a|--all::
+       checks out all files in the index.  Cannot be used
+       together with explicit filenames.
+
+-n|--no-create::
+       Don't checkout new files, only refresh files already checked
+       out.
+
+--prefix=<string>::
+       When creating files, prepend <string> (usually a directory
+       including a trailing /)
+
+--stage=<number>|all::
+       Instead of checking out unmerged entries, copy out the
+       files from named stage.  <number> must be between 1 and 3.
+       Note: --stage=all automatically implies --temp.
+
+--temp::
+       Instead of copying the files to the working directory
+       write the content to temporary files.  The temporary name
+       associations will be written to stdout.
+
+--stdin::
+       Instead of taking list of paths from the command line,
+       read list of paths from the standard input.  Paths are
+       separated by LF (i.e. one path per line) by default.
+
+-z::
+       Only meaningful with `--stdin`; paths are separated with
+       NUL character instead of LF.
+
+\--::
+       Do not interpret any more arguments as options.
+
+The order of the flags used to matter, but not anymore.
+
+Just doing `git-checkout-index` does nothing. You probably meant
+`git-checkout-index -a`. And if you want to force it, you want
+`git-checkout-index -f -a`.
+
+Intuitiveness is not the goal here. Repeatability is. The reason for
+the "no arguments means no work" behavior is that from scripts you are
+supposed to be able to do:
+
+----------------
+$ find . -name '*.h' -print0 | xargs -0 git-checkout-index -f --
+----------------
+
+which will force all existing `*.h` files to be replaced with their
+cached copies. If an empty command line implied "all", then this would
+force-refresh everything in the index, which was not the point.  But
+since git-checkout-index accepts --stdin it would be faster to use:
+
+----------------
+$ find . -name '*.h' -print0 | git-checkout-index -f -z --stdin
+----------------
+
+The `--` is just a good idea when you know the rest will be filenames;
+it will prevent problems with a filename of, for example,  `-a`.
+Using `--` is probably a good policy in scripts.
+
+
+Using --temp or --stage=all
+---------------------------
+When `--temp` is used (or implied by `--stage=all`)
+`git-checkout-index` will create a temporary file for each index
+entry being checked out.  The index will not be updated with stat
+information.  These options can be useful if the caller needs all
+stages of all unmerged entries so that the unmerged files can be
+processed by an external merge tool.
+
+A listing will be written to stdout providing the association of
+temporary file names to tracked path names.  The listing format
+has two variations:
+
+    . tempname TAB path RS
++
+The first format is what gets used when `--stage` is omitted or
+is not `--stage=all`. The field tempname is the temporary file
+name holding the file content and path is the tracked path name in
+the index.  Only the requested entries are output.
+
+    . stage1temp SP stage2temp SP stage3tmp TAB path RS
++
+The second format is what gets used when `--stage=all`.  The three
+stage temporary fields (stage1temp, stage2temp, stage3temp) list the
+name of the temporary file if there is a stage entry in the index
+or `.` if there is no stage entry.  Paths which only have a stage 0
+entry will always be omitted from the output.
+
+In both formats RS (the record separator) is newline by default
+but will be the null byte if -z was passed on the command line.
+The temporary file names are always safe strings; they will never
+contain directory separators or whitespace characters.  The path
+field is always relative to the current directory and the temporary
+file names are always relative to the top level directory.
+
+If the object being copied out to a temporary file is a symbolic
+link the content of the link will be written to a normal file.  It is
+up to the end-user or the Porcelain to make use of this information.
+
+
+EXAMPLES
+--------
+To update and refresh only the files already checked out::
++
+----------------
+$ git-checkout-index -n -f -a && git-update-index --ignore-missing --refresh
+----------------
+
+Using `git-checkout-index` to "export an entire tree"::
+       The prefix ability basically makes it trivial to use
+       `git-checkout-index` as an "export as tree" function.
+       Just read the desired tree into the index, and do:
++
+----------------
+$ git-checkout-index --prefix=git-export-dir/ -a
+----------------
++
+`git-checkout-index` will "export" the index into the specified
+directory.
++
+The final "/" is important. The exported name is literally just
+prefixed with the specified string.  Contrast this with the
+following example.
+
+Export files with a prefix::
++
+----------------
+$ git-checkout-index --prefix=.merged- Makefile
+----------------
++
+This will check out the currently cached copy of `Makefile`
+into the file `.merged-Makefile`.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+
+Documentation
+--------------
+Documentation by David Greaves,
+Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
new file mode 100644 (file)
index 0000000..2e58481
--- /dev/null
@@ -0,0 +1,219 @@
+git-checkout(1)
+===============
+
+NAME
+----
+git-checkout - Checkout and switch to a branch
+
+SYNOPSIS
+--------
+[verse]
+'git-checkout' [-q] [-f] [[--track | --no-track] -b <new_branch> [-l]] [-m] [<branch>]
+'git-checkout' [<tree-ish>] <paths>...
+
+DESCRIPTION
+-----------
+
+When <paths> are not given, this command switches branches by
+updating the index and working tree to reflect the specified
+branch, <branch>, and updating HEAD to be <branch> or, if
+specified, <new_branch>.  Using -b will cause <new_branch> to
+be created; in this case you can use the --track or --no-track
+options, which will be passed to `git branch`.
+
+When <paths> are given, this command does *not* switch
+branches.  It updates the named paths in the working tree from
+the index file (i.e. it runs `git-checkout-index -f -u`), or
+from a named commit.  In
+this case, the `-f` and `-b` options are meaningless and giving
+either of them results in an error.  <tree-ish> argument can be
+used to specify a specific tree-ish (i.e. commit, tag or tree)
+to update the index for the given paths before updating the
+working tree.
+
+
+OPTIONS
+-------
+-q::
+       Quiet, suppress feedback messages.
+
+-f::
+       Proceed even if the index or the working tree differs
+       from HEAD.  This is used to throw away local changes.
+
+-b::
+       Create a new branch named <new_branch> and start it at
+       <branch>.  The new branch name must pass all checks defined
+       by gitlink:git-check-ref-format[1].  Some of these checks
+       may restrict the characters allowed in a branch name.
+
+--track::
+       When -b is given and a branch is created off a remote branch,
+       set up configuration so that git-pull will automatically
+       retrieve data from the remote branch.  Use this if you always
+       pull from the same remote branch into the new branch, or if you
+       don't want to use "git pull <repository> <refspec>" explicitly.  Set the
+       branch.autosetupmerge configuration variable to true if you
+       want git-checkout and git-branch to always behave as if
+       '--track' were given.
+
+--no-track::
+       When -b is given and a branch is created off a remote branch,
+       set up configuration so that git-pull will not retrieve data
+       from the remote branch, ignoring the branch.autosetupmerge
+       configuration variable.
+
+-l::
+       Create the new branch's reflog.  This activates recording of
+       all changes made to the branch ref, enabling use of date
+       based sha1 expressions such as "<branchname>@\{yesterday}".
+
+-m::
+       If you have local modifications to one or more files that
+       are different between the current branch and the branch to
+       which you are switching, the command refuses to switch
+       branches in order to preserve your modifications in context.
+       However, with this option, a three-way merge between the current
+       branch, your working tree contents, and the new branch
+       is done, and you will be on the new branch.
++
+When a merge conflict happens, the index entries for conflicting
+paths are left unmerged, and you need to resolve the conflicts
+and mark the resolved paths with `git add` (or `git rm` if the merge
+should result in deletion of the path).
+
+<new_branch>::
+       Name for the new branch.
+
+<branch>::
+       Branch to checkout; may be any object ID that resolves to a
+       commit.  Defaults to HEAD.
++
+When this parameter names a non-branch (but still a valid commit object),
+your HEAD becomes 'detached'.
+
+
+Detached HEAD
+-------------
+
+It is sometimes useful to be able to 'checkout' a commit that is
+not at the tip of one of your branches.  The most obvious
+example is to check out the commit at a tagged official release
+point, like this:
+
+------------
+$ git checkout v2.6.18
+------------
+
+Earlier versions of git did not allow this and asked you to
+create a temporary branch using `-b` option, but starting from
+version 1.5.0, the above command 'detaches' your HEAD from the
+current branch and directly point at the commit named by the tag
+(`v2.6.18` in the above example).
+
+You can use usual git commands while in this state.  You can use
+`git-reset --hard $othercommit` to further move around, for
+example.  You can make changes and create a new commit on top of
+a detached HEAD.  You can even create a merge by using `git
+merge $othercommit`.
+
+The state you are in while your HEAD is detached is not recorded
+by any branch (which is natural --- you are not on any branch).
+What this means is that you can discard your temporary commits
+and merges by switching back to an existing branch (e.g. `git
+checkout master`), and a later `git prune` or `git gc` would
+garbage-collect them.  If you did this by mistake, you can ask
+the reflog for HEAD where you were, e.g.
+
+------------
+$ git log -g -2 HEAD
+------------
+
+
+EXAMPLES
+--------
+
+. The following sequence checks out the `master` branch, reverts
+the `Makefile` to two revisions back, deletes hello.c by
+mistake, and gets it back from the index.
++
+------------
+$ git checkout master             <1>
+$ git checkout master~2 Makefile  <2>
+$ rm -f hello.c
+$ git checkout hello.c            <3>
+------------
++
+<1> switch branch
+<2> take out a file out of other commit
+<3> restore hello.c from HEAD of current branch
++
+If you have an unfortunate branch that is named `hello.c`, this
+step would be confused as an instruction to switch to that branch.
+You should instead write:
++
+------------
+$ git checkout -- hello.c
+------------
+
+. After working in a wrong branch, switching to the correct
+branch would be done using:
++
+------------
+$ git checkout mytopic
+------------
++
+However, your "wrong" branch and correct "mytopic" branch may
+differ in files that you have locally modified, in which case,
+the above checkout would fail like this:
++
+------------
+$ git checkout mytopic
+fatal: Entry 'frotz' not uptodate. Cannot merge.
+------------
++
+You can give the `-m` flag to the command, which would try a
+three-way merge:
++
+------------
+$ git checkout -m mytopic
+Auto-merging frotz
+------------
++
+After this three-way merge, the local modifications are _not_
+registered in your index file, so `git diff` would show you what
+changes you made since the tip of the new branch.
+
+. When a merge conflict happens during switching branches with
+the `-m` option, you would see something like this:
++
+------------
+$ git checkout -m mytopic
+Auto-merging frotz
+merge: warning: conflicts during merge
+ERROR: Merge conflict in frotz
+fatal: merge program failed
+------------
++
+At this point, `git diff` shows the changes cleanly merged as in
+the previous example, as well as the changes in the conflicted
+files.  Edit and resolve the conflict and mark it resolved with
+`git add` as usual:
++
+------------
+$ edit frotz
+$ git add frotz
+------------
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-cherry-pick.txt b/Documentation/git-cherry-pick.txt
new file mode 100644 (file)
index 0000000..937c4a7
--- /dev/null
@@ -0,0 +1,78 @@
+git-cherry-pick(1)
+==================
+
+NAME
+----
+git-cherry-pick - Apply the change introduced by an existing commit
+
+SYNOPSIS
+--------
+'git-cherry-pick' [--edit] [-n] [-m parent-number] [-x] <commit>
+
+DESCRIPTION
+-----------
+Given one existing commit, apply the change the patch introduces, and record a
+new commit that records it.  This requires your working tree to be clean (no
+modifications from the HEAD commit).
+
+OPTIONS
+-------
+<commit>::
+       Commit to cherry-pick.
+       For a more complete list of ways to spell commits, see
+       "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1].
+
+-e|--edit::
+       With this option, `git-cherry-pick` will let you edit the commit
+       message prior committing.
+
+-x::
+       When recording the commit, append to the original commit
+       message a note that indicates which commit this change
+       was cherry-picked from.  Append the note only for cherry
+       picks without conflicts.  Do not use this option if
+       you are cherry-picking from your private branch because
+       the information is useless to the recipient.  If on the
+       other hand you are cherry-picking between two publicly
+       visible branches (e.g. backporting a fix to a
+       maintenance branch for an older release from a
+       development branch), adding this information can be
+       useful.
+
+-r::
+       It used to be that the command defaulted to do `-x`
+       described above, and `-r` was to disable it.  Now the
+       default is not to do `-x` so this option is a no-op.
+
+-m parent-number|--mainline parent-number::
+       Usually you cannot revert a merge because you do not know which
+       side of the merge should be considered the mainline.  This
+       option specifies the parent number (starting from 1) of
+       the mainline and allows cherry-pick to replay the change
+       relative to the specified parent.
+
+-n|--no-commit::
+       Usually the command automatically creates a commit with
+       a commit log message stating which commit was
+       cherry-picked.  This flag applies the change necessary
+       to cherry-pick the named commit to your working tree,
+       but does not make the commit.  In addition, when this
+       option is used, your working tree does not have to match
+       the HEAD commit.  The cherry-pick is done against the
+       beginning state of your working tree.
++
+This is useful when cherry-picking more than one commits'
+effect to your working tree in a row.
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-cherry.txt b/Documentation/git-cherry.txt
new file mode 100644 (file)
index 0000000..e694382
--- /dev/null
@@ -0,0 +1,69 @@
+git-cherry(1)
+=============
+
+NAME
+----
+git-cherry - Find commits not merged upstream
+
+SYNOPSIS
+--------
+'git-cherry' [-v] <upstream> [<head>] [<limit>]
+
+DESCRIPTION
+-----------
+The changeset (or "diff") of each commit between the fork-point and <head>
+is compared against each commit between the fork-point and <upstream>.
+
+Every commit that doesn't exist in the <upstream> branch
+has its id (sha1) reported, prefixed by a symbol.  The ones that have
+equivalent change already
+in the <upstream> branch are prefixed with a minus (-) sign, and those
+that only exist in the <head> branch are prefixed with a plus (+) symbol:
+
+               __*__*__*__*__> <upstream>
+              /
+    fork-point
+              \__+__+__-__+__+__-__+__> <head>
+
+
+If a <limit> has been given then the commits along the <head> branch up
+to and including <limit> are not reported:
+
+               __*__*__*__*__> <upstream>
+              /
+    fork-point
+              \__*__*__<limit>__-__+__> <head>
+
+
+Because git-cherry compares the changeset rather than the commit id
+(sha1), you can use git-cherry to find out if a commit you made locally
+has been applied <upstream> under a different commit id.  For example,
+this will happen if you're feeding patches <upstream> via email rather
+than pushing or pulling commits directly.
+
+
+OPTIONS
+-------
+-v::
+       Verbose.
+
+<upstream>::
+       Upstream branch to compare against.
+
+<head>::
+       Working branch; defaults to HEAD.
+
+<limit>::
+       Do not report commits up to (and including) limit.
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-citool.txt b/Documentation/git-citool.txt
new file mode 100644 (file)
index 0000000..5217ab2
--- /dev/null
@@ -0,0 +1,32 @@
+git-citool(1)
+=============
+
+NAME
+----
+git-citool - Graphical alternative to git-commit
+
+SYNOPSIS
+--------
+'git citool'
+
+DESCRIPTION
+-----------
+A Tcl/Tk based graphical interface to review modified files, stage
+them into the index, enter a commit message and record the new
+commit onto the current branch.  This interface is an alternative
+to the less interactive gitlink:git-commit[1] program.
+
+git-citool is actually a standard alias for 'git gui citool'.
+See gitlink:git-gui[1] for more details.
+
+Author
+------
+Written by Shawn O. Pearce <spearce@spearce.org>.
+
+Documentation
+--------------
+Documentation by Shawn O. Pearce <spearce@spearce.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-clean.txt b/Documentation/git-clean.txt
new file mode 100644 (file)
index 0000000..e3252d5
--- /dev/null
@@ -0,0 +1,57 @@
+git-clean(1)
+============
+
+NAME
+----
+git-clean - Remove untracked files from the working tree
+
+SYNOPSIS
+--------
+[verse]
+'git-clean' [-d] [-f] [-n] [-q] [-x | -X] [--] <paths>...
+
+DESCRIPTION
+-----------
+Removes files unknown to git.  This allows to clean the working tree
+from files that are not under version control.  If the '-x' option is
+specified, ignored files are also removed, allowing to remove all
+build products.
+When optional `<paths>...` arguments are given, the paths
+affected are further limited to those that match them.
+
+
+OPTIONS
+-------
+-d::
+       Remove untracked directories in addition to untracked files.
+
+-f::
+       If the git configuration specifies clean.requireForce as true,
+       git-clean will refuse to run unless given -f or -n.
+
+-n::
+       Don't actually remove anything, just show what would be done.
+
+-q::
+       Be quiet, only report errors, but not the files that are
+       successfully removed.
+
+-x::
+       Don't use the ignore rules.  This allows removing all untracked
+       files, including build products.  This can be used (possibly in
+       conjunction with gitlink:git-reset[1]) to create a pristine
+       working directory to test a clean build.
+
+-X::
+       Remove only files ignored by git.  This may be useful to rebuild
+       everything from scratch, but keep manually created files.
+
+
+Author
+------
+Written by Pavel Roskin <proski@gnu.org>
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt
new file mode 100644 (file)
index 0000000..c90bcec
--- /dev/null
@@ -0,0 +1,194 @@
+git-clone(1)
+============
+
+NAME
+----
+git-clone - Clone a repository into a new directory
+
+
+SYNOPSIS
+--------
+[verse]
+'git-clone' [--template=<template_directory>]
+         [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare]
+         [-o <name>] [-u <upload-pack>] [--reference <repository>]
+         [--depth <depth>] [--] <repository> [<directory>]
+
+DESCRIPTION
+-----------
+
+Clones a repository into a newly created directory, creates
+remote-tracking branches for each branch in the cloned repository
+(visible using `git branch -r`), and creates and checks out an initial
+branch equal to the cloned repository's currently active branch.
+
+After the clone, a plain `git fetch` without arguments will update
+all the remote-tracking branches, and a `git pull` without
+arguments will in addition merge the remote master branch into the
+current master branch, if any.
+
+This default configuration is achieved by creating references to
+the remote branch heads under `$GIT_DIR/refs/remotes/origin` and
+by initializing `remote.origin.url` and `remote.origin.fetch`
+configuration variables.
+
+
+OPTIONS
+-------
+--local::
+-l::
+       When the repository to clone from is on a local machine,
+       this flag bypasses normal "git aware" transport
+       mechanism and clones the repository by making a copy of
+       HEAD and everything under objects and refs directories.
+       The files under `.git/objects/` directory are hardlinked
+       to save space when possible.  This is now the default when
+       the source repository is specified with `/path/to/repo`
+       syntax, so it essentially is a no-op option.  To force
+       copying instead of hardlinking (which may be desirable
+       if you are trying to make a back-up of your repository),
+       but still avoid the usual "git aware" transport
+       mechanism, `--no-hardlinks` can be used.
+
+--no-hardlinks::
+       Optimize the cloning process from a repository on a
+       local filesystem by copying files under `.git/objects`
+       directory.
+
+--shared::
+-s::
+       When the repository to clone is on the local machine,
+       instead of using hard links, automatically setup
+       .git/objects/info/alternates to share the objects
+       with the source repository.  The resulting repository
+       starts out without any object of its own.
+
+--reference <repository>::
+       If the reference repository is on the local machine
+       automatically setup .git/objects/info/alternates to
+       obtain objects from the reference repository.  Using
+       an already existing repository as an alternate will
+       require fewer objects to be copied from the repository
+       being cloned, reducing network and local storage costs.
+
+--quiet::
+-q::
+       Operate quietly.  This flag is passed to "rsync" and
+       "git-fetch-pack" commands when given.
+
+--no-checkout::
+-n::
+       No checkout of HEAD is performed after the clone is complete.
+
+--bare::
+       Make a 'bare' GIT repository.  That is, instead of
+       creating `<directory>` and placing the administrative
+       files in `<directory>/.git`, make the `<directory>`
+       itself the `$GIT_DIR`. This obviously implies the `-n`
+       because there is nowhere to check out the working tree.
+       Also the branch heads at the remote are copied directly
+       to corresponding local branch heads, without mapping
+       them to `refs/remotes/origin/`.  When this option is
+       used, neither remote-tracking branches nor the related
+       configuration variables are created.
+
+--origin <name>::
+-o <name>::
+       Instead of using the remote name 'origin' to keep track
+       of the upstream repository, use <name> instead.
+
+--upload-pack <upload-pack>::
+-u <upload-pack>::
+       When given, and the repository to clone from is handled
+       by 'git-fetch-pack', '--exec=<upload-pack>' is passed to
+       the command to specify non-default path for the command
+       run on the other end.
+
+--template=<template_directory>::
+       Specify the directory from which templates will be used;
+       if unset the templates are taken from the installation
+       defined default, typically `/usr/share/git-core/templates`.
+
+--depth <depth>::
+       Create a 'shallow' clone with a history truncated to the
+       specified number of revisions.  A shallow repository has a
+       number of limitations (you cannot clone or fetch from
+       it, nor push from nor into it), but is adequate if you
+       are only interested in the recent history of a large project
+       with a long history, and would want to send in fixes
+       as patches.
+
+<repository>::
+       The (possibly remote) repository to clone from.  See the
+       <<URLS,URLS>> section below for more information on specifying
+       repositories.
+
+<directory>::
+       The name of a new directory to clone into.  The "humanish"
+       part of the source repository is used if no directory is
+       explicitly given ("repo" for "/path/to/repo.git" and "foo"
+       for "host.xz:foo/.git").  Cloning into an existing directory
+       is not allowed.
+
+:git-clone: 1
+include::urls.txt[]
+
+Examples
+--------
+
+Clone from upstream::
++
+------------
+$ git clone git://git.kernel.org/pub/scm/.../linux-2.6 my2.6
+$ cd my2.6
+$ make
+------------
+
+
+Make a local clone that borrows from the current directory, without checking things out::
++
+------------
+$ git clone -l -s -n . ../copy
+$ cd ../copy
+$ git show-branch
+------------
+
+
+Clone from upstream while borrowing from an existing local directory::
++
+------------
+$ git clone --reference my2.6 \
+       git://git.kernel.org/pub/scm/.../linux-2.7 \
+       my2.7
+$ cd my2.7
+------------
+
+
+Create a bare repository to publish your changes to the public::
++
+------------
+$ git clone --bare -l /home/proj/.git /pub/scm/proj.git
+------------
+
+
+Create a repository on the kernel.org machine that borrows from Linus::
++
+------------
+$ git clone --bare -l -s /pub/scm/.../torvalds/linux-2.6.git \
+    /pub/scm/.../me/subsys-2.6.git
+------------
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-commit-tree.txt b/Documentation/git-commit-tree.txt
new file mode 100644 (file)
index 0000000..a2537e1
--- /dev/null
@@ -0,0 +1,106 @@
+git-commit-tree(1)
+==================
+
+NAME
+----
+git-commit-tree - Create a new commit object
+
+
+SYNOPSIS
+--------
+'git-commit-tree' <tree> [-p <parent commit>]\* < changelog
+
+DESCRIPTION
+-----------
+This is usually not what an end user wants to run directly.  See
+gitlink:git-commit[1] instead.
+
+Creates a new commit object based on the provided tree object and
+emits the new commit object id on stdout. If no parent is given then
+it is considered to be an initial tree.
+
+A commit object usually has 1 parent (a commit after a change) or up
+to 16 parents.  More than one parent represents a merge of branches
+that led to them.
+
+While a tree represents a particular directory state of a working
+directory, a commit represents that state in "time", and explains how
+to get there.
+
+Normally a commit would identify a new "HEAD" state, and while git
+doesn't care where you save the note about that state, in practice we
+tend to just write the result to the file that is pointed at by
+`.git/HEAD`, so that we can always see what the last committed
+state was.
+
+OPTIONS
+-------
+<tree>::
+       An existing tree object
+
+-p <parent commit>::
+       Each '-p' indicates the id of a parent commit object.
+
+
+Commit Information
+------------------
+
+A commit encapsulates:
+
+- all parent object ids
+- author name, email and date
+- committer name and email and the commit time.
+
+While parent object ids are provided on the command line, author and
+committer information is taken from the following environment variables,
+if set:
+
+       GIT_AUTHOR_NAME
+       GIT_AUTHOR_EMAIL
+       GIT_AUTHOR_DATE
+       GIT_COMMITTER_NAME
+       GIT_COMMITTER_EMAIL
+       GIT_COMMITTER_DATE
+       EMAIL
+
+(nb "<", ">" and "\n"s are stripped)
+
+In case (some of) these environment variables are not set, the information
+is taken from the configuration items user.name and user.email, or, if not
+present, system user name and fully qualified hostname.
+
+A commit comment is read from stdin. If a changelog
+entry is not provided via "<" redirection, "git-commit-tree" will just wait
+for one to be entered and terminated with ^D.
+
+
+Diagnostics
+-----------
+You don't exist. Go away!::
+    The passwd(5) gecos field couldn't be read
+Your parents must have hated you!::
+    The password(5) gecos field is longer than a giant static buffer.
+Your sysadmin must hate you!::
+    The password(5) name field is longer than a giant static buffer.
+
+Discussion
+----------
+
+include::i18n.txt[]
+
+See Also
+--------
+gitlink:git-write-tree[1]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-commit.txt b/Documentation/git-commit.txt
new file mode 100644 (file)
index 0000000..4bb2791
--- /dev/null
@@ -0,0 +1,294 @@
+git-commit(1)
+=============
+
+NAME
+----
+git-commit - Record changes to the repository
+
+SYNOPSIS
+--------
+[verse]
+'git-commit' [-a | --interactive] [-s] [-v] [-u]
+          [(-c | -C) <commit> | -F <file> | -m <msg> | --amend]
+          [--allow-empty] [--no-verify] [-e] [--author <author>]
+          [--] [[-i | -o ]<file>...]
+
+DESCRIPTION
+-----------
+Use 'git commit' to store the current contents of the index in a new
+commit along with a log message describing the changes you have made.
+
+The content to be added can be specified in several ways:
+
+1. by using gitlink:git-add[1] to incrementally "add" changes to the
+   index before using the 'commit' command (Note: even modified
+   files must be "added");
+
+2. by using gitlink:git-rm[1] to remove files from the working tree
+   and the index, again before using the 'commit' command;
+
+3. by listing files as arguments to the 'commit' command, in which
+   case the commit will ignore changes staged in the index, and instead
+   record the current content of the listed files;
+
+4. by using the -a switch with the 'commit' command to automatically
+   "add" changes from all known files (i.e. all files that are already
+   listed in the index) and to automatically "rm" files in the index
+   that have been removed from the working tree, and then perform the
+   actual commit;
+
+5. by using the --interactive switch with the 'commit' command to decide one
+   by one which files should be part of the commit, before finalizing the
+   operation.  Currently, this is done by invoking `git-add --interactive`.
+
+The gitlink:git-status[1] command can be used to obtain a
+summary of what is included by any of the above for the next
+commit by giving the same set of parameters you would give to
+this command.
+
+If you make a commit and then found a mistake immediately after
+that, you can recover from it with gitlink:git-reset[1].
+
+
+OPTIONS
+-------
+-a|--all::
+       Tell the command to automatically stage files that have
+       been modified and deleted, but new files you have not
+       told git about are not affected.
+
+-c or -C <commit>::
+       Take existing commit object, and reuse the log message
+       and the authorship information (including the timestamp)
+       when creating the commit.  With '-C', the editor is not
+       invoked; with '-c' the user can further edit the commit
+       message.
+
+-F <file>::
+       Take the commit message from the given file.  Use '-' to
+       read the message from the standard input.
+
+--author <author>::
+       Override the author name used in the commit.  Use
+       `A U Thor <author@example.com>` format.
+
+-m <msg>|--message=<msg>::
+       Use the given <msg> as the commit message.
+
+-t <file>|--template=<file>::
+       Use the contents of the given file as the initial version
+       of the commit message. The editor is invoked and you can
+       make subsequent changes. If a message is specified using
+       the `-m` or `-F` options, this option has no effect. This
+       overrides the `commit.template` configuration variable.
+
+-s|--signoff::
+       Add Signed-off-by line at the end of the commit message.
+
+--no-verify::
+       This option bypasses the pre-commit hook.
+       See also link:hooks.html[hooks].
+
+--allow-empty::
+       Usually recording a commit that has the exact same tree as its
+       sole parent commit is a mistake, and the command prevents you
+       from making such a commit.  This option bypasses the safety, and
+       is primarily for use by foreign scm interface scripts.
+
+-e|--edit::
+       The message taken from file with `-F`, command line with
+       `-m`, and from file with `-C` are usually used as the
+       commit log message unmodified.  This option lets you
+       further edit the message taken from these sources.
+
+--amend::
+
+       Used to amend the tip of the current branch. Prepare the tree
+       object you would want to replace the latest commit as usual
+       (this includes the usual -i/-o and explicit paths), and the
+       commit log editor is seeded with the commit message from the
+       tip of the current branch. The commit you create replaces the
+       current tip -- if it was a merge, it will have the parents of
+       the current tip as parents -- so the current top commit is
+       discarded.
++
+--
+It is a rough equivalent for:
+------
+       $ git reset --soft HEAD^
+       $ ... do something else to come up with the right tree ...
+       $ git commit -c ORIG_HEAD
+
+------
+but can be used to amend a merge commit.
+--
+
+-i|--include::
+       Before making a commit out of staged contents so far,
+       stage the contents of paths given on the command line
+       as well.  This is usually not what you want unless you
+       are concluding a conflicted merge.
+
+-u|--untracked-files::
+       Show all untracked files, also those in uninteresting
+       directories, in the "Untracked files:" section of commit
+       message template.  Without this option only its name and
+       a trailing slash are displayed for each untracked
+       directory.
+
+-v|--verbose::
+       Show unified diff between the HEAD commit and what
+       would be committed at the bottom of the commit message
+       template.  Note that this diff output doesn't have its
+       lines prefixed with '#'.
+
+-q|--quiet::
+       Suppress commit summary message.
+
+\--::
+       Do not interpret any more arguments as options.
+
+<file>...::
+       When files are given on the command line, the command
+       commits the contents of the named files, without
+       recording the changes already staged.  The contents of
+       these files are also staged for the next commit on top
+       of what have been staged before.
+
+
+EXAMPLES
+--------
+When recording your own work, the contents of modified files in
+your working tree are temporarily stored to a staging area
+called the "index" with gitlink:git-add[1].  A file can be
+reverted back, only in the index but not in the working tree,
+to that of the last commit with `git-reset HEAD -- <file>`,
+which effectively reverts `git-add` and prevents the changes to
+this file from participating in the next commit.  After building
+the state to be committed incrementally with these commands,
+`git commit` (without any pathname parameter) is used to record what
+has been staged so far.  This is the most basic form of the
+command.  An example:
+
+------------
+$ edit hello.c
+$ git rm goodbye.c
+$ git add hello.c
+$ git commit
+------------
+
+Instead of staging files after each individual change, you can
+tell `git commit` to notice the changes to the files whose
+contents are tracked in
+your working tree and do corresponding `git add` and `git rm`
+for you.  That is, this example does the same as the earlier
+example if there is no other change in your working tree:
+
+------------
+$ edit hello.c
+$ rm goodbye.c
+$ git commit -a
+------------
+
+The command `git commit -a` first looks at your working tree,
+notices that you have modified hello.c and removed goodbye.c,
+and performs necessary `git add` and `git rm` for you.
+
+After staging changes to many files, you can alter the order the
+changes are recorded in, by giving pathnames to `git commit`.
+When pathnames are given, the command makes a commit that
+only records the changes made to the named paths:
+
+------------
+$ edit hello.c hello.h
+$ git add hello.c hello.h
+$ edit Makefile
+$ git commit Makefile
+------------
+
+This makes a commit that records the modification to `Makefile`.
+The changes staged for `hello.c` and `hello.h` are not included
+in the resulting commit.  However, their changes are not lost --
+they are still staged and merely held back.  After the above
+sequence, if you do:
+
+------------
+$ git commit
+------------
+
+this second commit would record the changes to `hello.c` and
+`hello.h` as expected.
+
+After a merge (initiated by either gitlink:git-merge[1] or
+gitlink:git-pull[1]) stops because of conflicts, cleanly merged
+paths are already staged to be committed for you, and paths that
+conflicted are left in unmerged state.  You would have to first
+check which paths are conflicting with gitlink:git-status[1]
+and after fixing them manually in your working tree, you would
+stage the result as usual with gitlink:git-add[1]:
+
+------------
+$ git status | grep unmerged
+unmerged: hello.c
+$ edit hello.c
+$ git add hello.c
+------------
+
+After resolving conflicts and staging the result, `git ls-files -u`
+would stop mentioning the conflicted path.  When you are done,
+run `git commit` to finally record the merge:
+
+------------
+$ git commit
+------------
+
+As with the case to record your own changes, you can use `-a`
+option to save typing.  One difference is that during a merge
+resolution, you cannot use `git commit` with pathnames to
+alter the order the changes are committed, because the merge
+should be recorded as a single commit.  In fact, the command
+refuses to run when given pathnames (but see `-i` option).
+
+
+DISCUSSION
+----------
+
+Though not required, it's a good idea to begin the commit message
+with a single short (less than 50 character) line summarizing the
+change, followed by a blank line and then a more thorough description.
+Tools that turn commits into email, for example, use the first line
+on the Subject: line and the rest of the commit in the body.
+
+include::i18n.txt[]
+
+ENVIRONMENT AND CONFIGURATION VARIABLES
+---------------------------------------
+The editor used to edit the commit log message will be chosen from the
+GIT_EDITOR environment variable, the core.editor configuration variable, the
+VISUAL environment variable, or the EDITOR environment variable (in that
+order).
+
+HOOKS
+-----
+This command can run `commit-msg`, `pre-commit`, and
+`post-commit` hooks.  See link:hooks.html[hooks] for more
+information.
+
+
+SEE ALSO
+--------
+gitlink:git-add[1],
+gitlink:git-rm[1],
+gitlink:git-mv[1],
+gitlink:git-merge[1],
+gitlink:git-commit-tree[1]
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org> and
+Junio C Hamano <junkio@cox.net>
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
new file mode 100644 (file)
index 0000000..98509b4
--- /dev/null
@@ -0,0 +1,335 @@
+git-config(1)
+=============
+
+NAME
+----
+git-config - Get and set repository or global options
+
+
+SYNOPSIS
+--------
+[verse]
+'git-config' [<file-option>] [type] [-z|--null] name [value [value_regex]]
+'git-config' [<file-option>] [type] --add name value
+'git-config' [<file-option>] [type] --replace-all name [value [value_regex]]
+'git-config' [<file-option>] [type] [-z|--null] --get name [value_regex]
+'git-config' [<file-option>] [type] [-z|--null] --get-all name [value_regex]
+'git-config' [<file-option>] [type] [-z|--null] --get-regexp name_regex [value_regex]
+'git-config' [<file-option>] --unset name [value_regex]
+'git-config' [<file-option>] --unset-all name [value_regex]
+'git-config' [<file-option>] --rename-section old_name new_name
+'git-config' [<file-option>] --remove-section name
+'git-config' [<file-option>] [-z|--null] -l | --list
+'git-config' [<file-option>] --get-color name [default]
+'git-config' [<file-option>] --get-colorbool name [stdout-is-tty]
+
+DESCRIPTION
+-----------
+You can query/set/replace/unset options with this command. The name is
+actually the section and the key separated by a dot, and the value will be
+escaped.
+
+Multiple lines can be added to an option by using the '--add' option.
+If you want to update or unset an option which can occur on multiple
+lines, a POSIX regexp `value_regex` needs to be given.  Only the
+existing values that match the regexp are updated or unset.  If
+you want to handle the lines that do *not* match the regex, just
+prepend a single exclamation mark in front (see also <<EXAMPLES>>).
+
+The type specifier can be either '--int' or '--bool', which will make
+'git-config' ensure that the variable(s) are of the given type and
+convert the value to the canonical form (simple decimal number for int,
+a "true" or "false" string for bool).  If no type specifier is passed,
+no checks or transformations are performed on the value.
+
+The file-option can be one of '--system', '--global' or '--file'
+which specify where the values will be read from or written to.
+The default is to assume the config file of the current repository,
+.git/config unless defined otherwise with GIT_DIR and GIT_CONFIG
+(see <<FILES>>).
+
+This command will fail if:
+
+. The config file is invalid,
+. Can not write to the config file,
+. no section was provided,
+. the section or key is invalid,
+. you try to unset an option which does not exist,
+. you try to unset/set an option for which multiple lines match, or
+. you use '--global' option without $HOME being properly set.
+
+
+OPTIONS
+-------
+
+--replace-all::
+       Default behavior is to replace at most one line. This replaces
+       all lines matching the key (and optionally the value_regex).
+
+--add::
+       Adds a new line to the option without altering any existing
+       values.  This is the same as providing '^$' as the value_regex.
+
+--get::
+       Get the value for a given key (optionally filtered by a regex
+       matching the value). Returns error code 1 if the key was not
+       found and error code 2 if multiple key values were found.
+
+--get-all::
+       Like get, but does not fail if the number of values for the key
+       is not exactly one.
+
+--get-regexp::
+       Like --get-all, but interprets the name as a regular expression.
+       Also outputs the key names.
+
+--global::
+       For writing options: write to global ~/.gitconfig file rather than
+       the repository .git/config.
++
+For reading options: read only from global ~/.gitconfig rather than
+from all available files.
++
+See also <<FILES>>.
+
+--system::
+       For writing options: write to system-wide $(prefix)/etc/gitconfig
+       rather than the repository .git/config.
++
+For reading options: read only from system-wide $(prefix)/etc/gitconfig
+rather than from all available files.
++
+See also <<FILES>>.
+
+-f config-file, --file config-file::
+       Use the given config file instead of the one specified by GIT_CONFIG.
+
+--remove-section::
+       Remove the given section from the configuration file.
+
+--rename-section::
+       Rename the given section to a new name.
+
+--unset::
+       Remove the line matching the key from config file.
+
+--unset-all::
+       Remove all lines matching the key from config file.
+
+-l, --list::
+       List all variables set in config file.
+
+--bool::
+       git-config will ensure that the output is "true" or "false"
+
+--int::
+       git-config will ensure that the output is a simple
+       decimal number.  An optional value suffix of 'k', 'm', or 'g'
+       in the config file will cause the value to be multiplied
+       by 1024, 1048576, or 1073741824 prior to output.
+
+-z, --null::
+       For all options that output values and/or keys, always
+       end values with with the null character (instead of a
+       newline). Use newline instead as a delimiter between
+       key and value. This allows for secure parsing of the
+       output without getting confused e.g. by values that
+       contain line breaks.
+
+--get-colorbool name [stdout-is-tty]::
+
+       Find the color setting for `name` (e.g. `color.diff`) and output
+       "true" or "false".  `stdout-is-tty` should be either "true" or
+       "false", and is taken into account when configuration says
+       "auto".  If `stdout-is-tty` is missing, then checks the standard
+       output of the command itself, and exits with status 0 if color
+       is to be used, or exits with status 1 otherwise.
+
+--get-color name default::
+
+       Find the color configured for `name` (e.g. `color.diff.new`) and
+       output it as the ANSI color escape sequence to the standard
+       output.  The optional `default` parameter is used instead, if
+       there is no color configured for `name`.
+
+[[FILES]]
+FILES
+-----
+
+If not set explicitly with '--file', there are three files where
+git-config will search for configuration options:
+
+$GIT_DIR/config::
+       Repository specific configuration file. (The filename is
+       of course relative to the repository root, not the working
+       directory.)
+
+~/.gitconfig::
+       User-specific configuration file. Also called "global"
+       configuration file.
+
+$(prefix)/etc/gitconfig::
+       System-wide configuration file.
+
+If no further options are given, all reading options will read all of these
+files that are available. If the global or the system-wide configuration
+file are not available they will be ignored. If the repository configuration
+file is not available or readable, git-config will exit with a non-zero
+error code. However, in neither case will an error message be issued.
+
+All writing options will per default write to the repository specific
+configuration file. Note that this also affects options like '--replace-all'
+and '--unset'. *git-config will only ever change one file at a time*.
+
+You can override these rules either by command line options or by environment
+variables. The '--global' and the '--system' options will limit the file used
+to the global or system-wide file respectively. The GIT_CONFIG environment
+variable has a similar effect, but you can specify any filename you want.
+
+The GIT_CONFIG_LOCAL environment variable on the other hand only changes
+the name used instead of the repository configuration file. The global and
+the system-wide configuration files will still be read. (For writing options
+this will obviously result in the same behavior as using GIT_CONFIG.)
+
+
+ENVIRONMENT
+-----------
+
+GIT_CONFIG::
+       Take the configuration from the given file instead of .git/config.
+       Using the "--global" option forces this to ~/.gitconfig. Using the
+       "--system" option forces this to $(prefix)/etc/gitconfig.
+
+GIT_CONFIG_LOCAL::
+       Take the configuration from the given file instead if .git/config.
+       Still read the global and the system-wide configuration files, though.
+
+See also <<FILES>>.
+
+
+[[EXAMPLES]]
+EXAMPLES
+--------
+
+Given a .git/config like this:
+
+       #
+       # This is the config file, and
+       # a '#' or ';' character indicates
+       # a comment
+       #
+
+       ; core variables
+       [core]
+               ; Don't trust file modes
+               filemode = false
+
+       ; Our diff algorithm
+       [diff]
+               external = "/usr/local/bin/gnu-diff -u"
+               renames = true
+
+       ; Proxy settings
+       [core]
+               gitproxy="proxy-command" for kernel.org
+               gitproxy=default-proxy ; for all the rest
+
+you can set the filemode to true with
+
+------------
+% git config core.filemode true
+------------
+
+The hypothetical proxy command entries actually have a postfix to discern
+what URL they apply to. Here is how to change the entry for kernel.org
+to "ssh".
+
+------------
+% git config core.gitproxy '"ssh" for kernel.org' 'for kernel.org$'
+------------
+
+This makes sure that only the key/value pair for kernel.org is replaced.
+
+To delete the entry for renames, do
+
+------------
+% git config --unset diff.renames
+------------
+
+If you want to delete an entry for a multivar (like core.gitproxy above),
+you have to provide a regex matching the value of exactly one line.
+
+To query the value for a given key, do
+
+------------
+% git config --get core.filemode
+------------
+
+or
+
+------------
+% git config core.filemode
+------------
+
+or, to query a multivar:
+
+------------
+% git config --get core.gitproxy "for kernel.org$"
+------------
+
+If you want to know all the values for a multivar, do:
+
+------------
+% git config --get-all core.gitproxy
+------------
+
+If you like to live dangerous, you can replace *all* core.gitproxy by a
+new one with
+
+------------
+% git config --replace-all core.gitproxy ssh
+------------
+
+However, if you really only want to replace the line for the default proxy,
+i.e. the one without a "for ..." postfix, do something like this:
+
+------------
+% git config core.gitproxy ssh '! for '
+------------
+
+To actually match only values with an exclamation mark, you have to
+
+------------
+% git config section.key value '[!]'
+------------
+
+To add a new proxy, without altering any of the existing ones, use
+
+------------
+% git config core.gitproxy '"proxy-command" for example.com'
+------------
+
+An example to use customized color from the configuration in your
+script:
+
+------------
+#!/bin/sh
+WS=$(git config --get-color color.diff.whitespace "blue reverse")
+RESET=$(git config --get-color "" "reset")
+echo "${WS}your whitespace color or blue reverse${RESET}"
+------------
+
+include::config.txt[]
+
+
+Author
+------
+Written by Johannes Schindelin <Johannes.Schindelin@gmx.de>
+
+Documentation
+--------------
+Documentation by Johannes Schindelin, Petr Baudis and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-count-objects.txt b/Documentation/git-count-objects.txt
new file mode 100644 (file)
index 0000000..8161411
--- /dev/null
@@ -0,0 +1,37 @@
+git-count-objects(1)
+====================
+
+NAME
+----
+git-count-objects - Count unpacked number of objects and their disk consumption
+
+SYNOPSIS
+--------
+'git-count-objects' [-v]
+
+DESCRIPTION
+-----------
+This counts the number of unpacked object files and disk space consumed by
+them, to help you decide when it is a good time to repack.
+
+
+OPTIONS
+-------
+-v::
+       In addition to the number of loose objects and disk
+       space consumed, it reports the number of in-pack
+       objects, number of packs, and number of objects that can be
+       removed by running `git-prune-packed`.
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-cvsexportcommit.txt b/Documentation/git-cvsexportcommit.txt
new file mode 100644 (file)
index 0000000..3f9d229
--- /dev/null
@@ -0,0 +1,109 @@
+git-cvsexportcommit(1)
+======================
+
+NAME
+----
+git-cvsexportcommit - Export a single commit to a CVS checkout
+
+
+SYNOPSIS
+--------
+'git-cvsexportcommit' [-h] [-u] [-v] [-c] [-P] [-p] [-a] [-d cvsroot] [-w cvsworkdir] [-f] [-m msgprefix] [PARENTCOMMIT] COMMITID
+
+
+DESCRIPTION
+-----------
+Exports a commit from GIT to a CVS checkout, making it easier
+to merge patches from a git repository into a CVS repository.
+
+Specify the name of a CVS checkout using the -w switch or execute it
+from the root of the CVS working copy. In the latter case GIT_DIR must
+be defined. See examples below.
+
+It does its best to do the safe thing, it will check that the files are
+unchanged and up to date in the CVS checkout, and it will not autocommit
+by default.
+
+Supports file additions, removals, and commits that affect binary files.
+
+If the commit is a merge commit, you must tell git-cvsexportcommit what parent
+should the changeset be done against.
+
+OPTIONS
+-------
+
+-c::
+       Commit automatically if the patch applied cleanly. It will not
+       commit if any hunks fail to apply or there were other problems.
+
+-p::
+       Be pedantic (paranoid) when applying patches. Invokes patch with
+       --fuzz=0
+
+-a::
+       Add authorship information. Adds Author line, and Committer (if
+       different from Author) to the message.
+
+-d::
+       Set an alternative CVSROOT to use.  This corresponds to the CVS
+       -d parameter.  Usually users will not want to set this, except
+       if using CVS in an asymmetric fashion.
+
+-f::
+       Force the merge even if the files are not up to date.
+
+-P::
+       Force the parent commit, even if it is not a direct parent.
+
+-m::
+       Prepend the commit message with the provided prefix.
+       Useful for patch series and the like.
+
+-u::
+       Update affected files from CVS repository before attempting export.
+
+-w::
+       Specify the location of the CVS checkout to use for the export. This
+       option does not require GIT_DIR to be set before execution if the
+       current directory is within a git repository.
+
+-v::
+       Verbose.
+
+EXAMPLES
+--------
+
+Merge one patch into CVS::
++
+------------
+$ export GIT_DIR=~/project/.git
+$ cd ~/project_cvs_checkout
+$ git-cvsexportcommit -v <commit-sha1>
+$ cvs commit -F .msg <files>
+------------
+
+Merge one patch into CVS (-c and -w options). The working directory is within the Git Repo::
++
+------------
+       $ git-cvsexportcommit -v -c -w ~/project_cvs_checkout <commit-sha1>
+------------
+
+Merge pending patches into CVS automatically -- only if you really know what you are doing::
++
+------------
+$ export GIT_DIR=~/project/.git
+$ cd ~/project_cvs_checkout
+$ git-cherry cvshead myhead | sed -n 's/^+ //p' | xargs -l1 git-cvsexportcommit -c -p -v
+------------
+
+Author
+------
+Written by Martin Langhoff <martin@catalyst.net.nz> and others.
+
+Documentation
+--------------
+Documentation by Martin Langhoff <martin@catalyst.net.nz> and others.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-cvsimport.txt b/Documentation/git-cvsimport.txt
new file mode 100644 (file)
index 0000000..fdd7ec7
--- /dev/null
@@ -0,0 +1,169 @@
+git-cvsimport(1)
+================
+
+NAME
+----
+git-cvsimport - Salvage your data out of another SCM people love to hate
+
+
+SYNOPSIS
+--------
+[verse]
+'git-cvsimport' [-o <branch-for-HEAD>] [-h] [-v] [-d <CVSROOT>]
+             [-A <author-conv-file>] [-p <options-for-cvsps>] [-P <file>]
+             [-C <git_repository>] [-z <fuzz>] [-i] [-k] [-u] [-s <subst>]
+             [-a] [-m] [-M <regex>] [-S <regex>] [-L <commitlimit>]
+             [-r <remote>] [<CVS_module>]
+
+
+DESCRIPTION
+-----------
+Imports a CVS repository into git. It will either create a new
+repository, or incrementally import into an existing one.
+
+Splitting the CVS log into patch sets is done by 'cvsps'.
+At least version 2.1 is required.
+
+You should *never* do any work of your own on the branches that are
+created by git-cvsimport.  By default initial import will create and populate a
+"master" branch from the CVS repository's main branch which you're free
+to work with; after that, you need to 'git merge' incremental imports, or
+any CVS branches, yourself.  It is advisable to specify a named remote via
+-r to separate and protect the incoming branches.
+
+
+OPTIONS
+-------
+-v::
+       Verbosity: let 'cvsimport' report what it is doing.
+
+-d <CVSROOT>::
+       The root of the CVS archive. May be local (a simple path) or remote;
+       currently, only the :local:, :ext: and :pserver: access methods
+       are supported. If not given, git-cvsimport will try to read it
+       from `CVS/Root`. If no such file exists, it checks for the
+       `CVSROOT` environment variable.
+
+<CVS_module>::
+       The CVS module you want to import. Relative to <CVSROOT>.
+       If not given, git-cvsimport tries to read it from
+       `CVS/Repository`.
+
+-C <target-dir>::
+        The git repository to import to.  If the directory doesn't
+        exist, it will be created.  Default is the current directory.
+
+-r <remote>::
+       The git remote to import this CVS repository into.
+       Moves all CVS branches into remotes/<remote>/<branch>
+       akin to the git-clone --use-separate-remote option.
+
+-o <branch-for-HEAD>::
+       When no remote is specified (via -r) the 'HEAD' branch
+       from CVS is imported to the 'origin' branch within the git
+       repository, as 'HEAD' already has a special meaning for git.
+       When a remote is specified the 'HEAD' branch is named
+       remotes/<remote>/master mirroring git-clone behaviour.
+       Use this option if you want to import into a different
+       branch.
++
+Use '-o master' for continuing an import that was initially done by
+the old cvs2git tool.
+
+-i::
+       Import-only: don't perform a checkout after importing.  This option
+       ensures the working directory and index remain untouched and will
+       not create them if they do not exist.
+
+-k::
+       Kill keywords: will extract files with '-kk' from the CVS archive
+       to avoid noisy changesets. Highly recommended, but off by default
+       to preserve compatibility with early imported trees.
+
+-u::
+       Convert underscores in tag and branch names to dots.
+
+-s <subst>::
+       Substitute the character "/" in branch names with <subst>
+
+-p <options-for-cvsps>::
+       Additional options for cvsps.
+       The options '-u' and '-A' are implicit and should not be used here.
++
+If you need to pass multiple options, separate them with a comma.
+
+-z <fuzz>::
+       Pass the timestamp fuzz factor to cvsps, in seconds. If unset,
+       cvsps defaults to 300s.
+
+-P <cvsps-output-file>::
+       Instead of calling cvsps, read the provided cvsps output file. Useful
+       for debugging or when cvsps is being handled outside cvsimport.
+
+-m::
+       Attempt to detect merges based on the commit message. This option
+       will enable default regexes that try to capture the name source
+       branch name from the commit message.
+
+-M <regex>::
+       Attempt to detect merges based on the commit message with a custom
+       regex. It can be used with '-m' to also see the default regexes.
+       You must escape forward slashes.
+
+-S <regex>::
+       Skip paths matching the regex.
+
+-a::
+       Import all commits, including recent ones. cvsimport by default
+       skips commits that have a timestamp less than 10 minutes ago.
+
+-L <limit>::
+       Limit the number of commits imported. Workaround for cases where
+       cvsimport leaks memory.
+
+-A <author-conv-file>::
+       CVS by default uses the Unix username when writing its
+       commit logs. Using this option and an author-conv-file
+       in this format
++
+---------
+       exon=Andreas Ericsson <ae@op5.se>
+       spawn=Simon Pawn <spawn@frog-pond.org>
+
+---------
++
+git-cvsimport will make it appear as those authors had
+their GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL set properly
+all along.
++
+For convenience, this data is saved to `$GIT_DIR/cvs-authors`
+each time the '-A' option is provided and read from that same
+file each time git-cvsimport is run.
++
+It is not recommended to use this feature if you intend to
+export changes back to CVS again later with
+gitlink:git-cvsexportcommit[1].
+
+-h::
+       Print a short usage message and exit.
+
+OUTPUT
+------
+If '-v' is specified, the script reports what it is doing.
+
+Otherwise, success is indicated the Unix way, i.e. by simply exiting with
+a zero exit status.
+
+
+Author
+------
+Written by Matthias Urlichs <smurf@smurf.noris.de>, with help from
+various participants of the git-list <git@vger.kernel.org>.
+
+Documentation
+--------------
+Documentation by Matthias Urlichs <smurf@smurf.noris.de>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-cvsserver.txt b/Documentation/git-cvsserver.txt
new file mode 100644 (file)
index 0000000..258a62f
--- /dev/null
@@ -0,0 +1,322 @@
+git-cvsserver(1)
+================
+
+NAME
+----
+git-cvsserver - A CVS server emulator for git
+
+SYNOPSIS
+--------
+
+SSH:
+
+[verse]
+export CVS_SERVER=git-cvsserver
+'cvs' -d :ext:user@server/path/repo.git co <HEAD_name>
+
+pserver (/etc/inetd.conf):
+
+[verse]
+cvspserver stream tcp nowait nobody /usr/bin/git-cvsserver git-cvsserver pserver
+
+Usage:
+
+[verse]
+'git-cvsserver' [options] [pserver|server] [<directory> ...]
+
+OPTIONS
+-------
+
+All these options obviously only make sense if enforced by the server side.
+They have been implemented to resemble the gitlink:git-daemon[1] options as
+closely as possible.
+
+--base-path <path>::
+Prepend 'path' to requested CVSROOT
+
+--strict-paths::
+Don't allow recursing into subdirectories
+
+--export-all::
+Don't check for `gitcvs.enabled` in config. You also have to specify a list
+of allowed directories (see below) if you want to use this option.
+
+--version, -V::
+Print version information and exit
+
+--help, -h, -H::
+Print usage information and exit
+
+<directory>::
+You can specify a list of allowed directories. If no directories
+are given, all are allowed. This is an additional restriction, gitcvs
+access still needs to be enabled by the `gitcvs.enabled` config option
+unless '--export-all' was given, too.
+
+
+DESCRIPTION
+-----------
+
+This application is a CVS emulation layer for git.
+
+It is highly functional. However, not all methods are implemented,
+and for those methods that are implemented,
+not all switches are implemented.
+
+Testing has been done using both the CLI CVS client, and the Eclipse CVS
+plugin. Most functionality works fine with both of these clients.
+
+LIMITATIONS
+-----------
+
+Currently cvsserver works over SSH connections for read/write clients, and
+over pserver for anonymous CVS access.
+
+CVS clients cannot tag, branch or perform GIT merges.
+
+git-cvsserver maps GIT branches to CVS modules. This is very different
+from what most CVS users would expect since in CVS modules usually represent
+one or more directories.
+
+INSTALLATION
+------------
+
+1. If you are going to offer anonymous CVS access via pserver, add a line in
+   /etc/inetd.conf like
++
+--
+------
+   cvspserver stream tcp nowait nobody git-cvsserver pserver
+
+------
+Note: Some inetd servers let you specify the name of the executable
+independently of the value of argv[0] (i.e. the name the program assumes
+it was executed with). In this case the correct line in /etc/inetd.conf
+looks like
+
+------
+   cvspserver stream tcp nowait nobody /usr/bin/git-cvsserver git-cvsserver pserver
+
+------
+No special setup is needed for SSH access, other than having GIT tools
+in the PATH. If you have clients that do not accept the CVS_SERVER
+environment variable, you can rename git-cvsserver to cvs.
+
+Note: Newer CVS versions (>= 1.12.11) also support specifying
+CVS_SERVER directly in CVSROOT like
+
+------
+cvs -d ":ext;CVS_SERVER=git-cvsserver:user@server/path/repo.git" co <HEAD_name>
+------
+This has the advantage that it will be saved in your 'CVS/Root' files and
+you don't need to worry about always setting the correct environment
+variable.
+--
+2. For each repo that you want accessible from CVS you need to edit config in
+   the repo and add the following section.
++
+--
+------
+   [gitcvs]
+        enabled=1
+        # optional for debugging
+        logfile=/path/to/logfile
+
+------
+Note: you need to ensure each user that is going to invoke git-cvsserver has
+write access to the log file and to the database (see
+<<dbbackend,Database Backend>>. If you want to offer write access over
+SSH, the users of course also need write access to the git repository itself.
+
+[[configaccessmethod]]
+All configuration variables can also be overridden for a specific method of
+access. Valid method names are "ext" (for SSH access) and "pserver". The
+following example configuration would disable pserver access while still
+allowing access over SSH.
+------
+   [gitcvs]
+        enabled=0
+
+   [gitcvs "ext"]
+        enabled=1
+------
+--
+3. On the client machine you need to set the following variables.
+   CVSROOT should be set as per normal, but the directory should point at the
+   appropriate git repo. For example:
++
+--
+For SSH access, CVS_SERVER should be set to git-cvsserver
+
+Example:
+
+------
+     export CVSROOT=:ext:user@server:/var/git/project.git
+     export CVS_SERVER=git-cvsserver
+------
+--
+4. For SSH clients that will make commits, make sure their .bashrc file
+   sets the GIT_AUTHOR and GIT_COMMITTER variables.
+
+5. Clients should now be able to check out the project. Use the CVS 'module'
+   name to indicate what GIT 'head' you want to check out. Example:
++
+------
+     cvs co -d project-master master
+------
+
+[[dbbackend]]
+Database Backend
+----------------
+
+git-cvsserver uses one database per git head (i.e. CVS module) to
+store information about the repository for faster access. The
+database doesn't contain any persistent data and can be completely
+regenerated from the git repository at any time. The database
+needs to be updated (i.e. written to) after every commit.
+
+If the commit is done directly by using git (as opposed to
+using git-cvsserver) the update will need to happen on the
+next repository access by git-cvsserver, independent of
+access method and requested operation.
+
+That means that even if you offer only read access (e.g. by using
+the pserver method), git-cvsserver should have write access to
+the database to work reliably (otherwise you need to make sure
+that the database if up-to-date all the time git-cvsserver is run).
+
+By default it uses SQLite databases in the git directory, named
+`gitcvs.<module_name>.sqlite`. Note that the SQLite backend creates
+temporary files in the same directory as the database file on
+write so it might not be enough to grant the users using
+git-cvsserver write access to the database file without granting
+them write access to the directory, too.
+
+You can configure the database backend with the following
+configuration variables:
+
+Configuring database backend
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+git-cvsserver uses the Perl DBI module. Please also read
+its documentation if changing these variables, especially
+about `DBI->connect()`.
+
+gitcvs.dbname::
+       Database name. The exact meaning depends on the
+       used database driver, for SQLite this is a filename.
+       Supports variable substitution (see below). May
+       not contain semicolons (`;`).
+       Default: '%Ggitcvs.%m.sqlite'
+
+gitcvs.dbdriver::
+       Used DBI driver. You can specify any available driver
+       for this here, but it might not work. cvsserver is tested
+       with 'DBD::SQLite', reported to work with
+       'DBD::Pg', and reported *not* to work with 'DBD::mysql'.
+       Please regard this as an experimental feature. May not
+       contain double colons (`:`).
+       Default: 'SQLite'
+
+gitcvs.dbuser::
+       Database user. Only useful if setting `dbdriver`, since
+       SQLite has no concept of database users. Supports variable
+       substitution (see below).
+
+gitcvs.dbpass::
+       Database password.  Only useful if setting `dbdriver`, since
+       SQLite has no concept of database passwords.
+
+All variables can also be set per access method, see <<configaccessmethod,above>>.
+
+Variable substitution
+^^^^^^^^^^^^^^^^^^^^^
+In `dbdriver` and `dbuser` you can use the following variables:
+
+%G::
+       git directory name
+%g::
+       git directory name, where all characters except for
+       alpha-numeric ones, `.`, and `-` are replaced with
+       `_` (this should make it easier to use the directory
+       name in a filename if wanted)
+%m::
+       CVS module/git head name
+%a::
+       access method (one of "ext" or "pserver")
+%u::
+       Name of the user running git-cvsserver.
+       If no name can be determined, the
+       numeric uid is used.
+
+Eclipse CVS Client Notes
+------------------------
+
+To get a checkout with the Eclipse CVS client:
+
+1. Select "Create a new project -> From CVS checkout"
+2. Create a new location. See the notes below for details on how to choose the
+   right protocol.
+3. Browse the 'modules' available. It will give you a list of the heads in
+   the repository. You will not be able to browse the tree from there. Only
+   the heads.
+4. Pick 'HEAD' when it asks what branch/tag to check out. Untick the
+   "launch commit wizard" to avoid committing the .project file.
+
+Protocol notes: If you are using anonymous access via pserver, just select that.
+Those using SSH access should choose the 'ext' protocol, and configure 'ext'
+access on the Preferences->Team->CVS->ExtConnection pane. Set CVS_SERVER to
+'git-cvsserver'. Note that password support is not good when using 'ext',
+you will definitely want to have SSH keys setup.
+
+Alternatively, you can just use the non-standard extssh protocol that Eclipse
+offer. In that case CVS_SERVER is ignored, and you will have to replace
+the cvs utility on the server with git-cvsserver or manipulate your `.bashrc`
+so that calling 'cvs' effectively calls git-cvsserver.
+
+Clients known to work
+---------------------
+
+- CVS 1.12.9 on Debian
+- CVS 1.11.17 on MacOSX (from Fink package)
+- Eclipse 3.0, 3.1.2 on MacOSX (see Eclipse CVS Client Notes)
+- TortoiseCVS
+
+Operations supported
+--------------------
+
+All the operations required for normal use are supported, including
+checkout, diff, status, update, log, add, remove, commit.
+Legacy monitoring operations are not supported (edit, watch and related).
+Exports and tagging (tags and branches) are not supported at this stage.
+
+The server should set the '-k' mode to binary when relevant, however,
+this is not really implemented yet. For now, you can force the server
+to set '-kb' for all files by setting the `gitcvs.allbinary` config
+variable. In proper GIT tradition, the contents of the files are
+always respected. No keyword expansion or newline munging is supported.
+
+Dependencies
+------------
+
+git-cvsserver depends on DBD::SQLite.
+
+Copyright and Authors
+---------------------
+
+This program is copyright The Open University UK - 2006.
+
+Authors:
+
+- Martyn Smith    <martyn@catalyst.net.nz>
+- Martin Langhoff <martin@catalyst.net.nz>
+
+with ideas and patches from participants of the git-list <git@vger.kernel.org>.
+
+Documentation
+--------------
+Documentation by Martyn Smith <martyn@catalyst.net.nz>, Martin Langhoff <martin@catalyst.net.nz>, and Matthias Urlichs <smurf@smurf.noris.de>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-daemon.txt b/Documentation/git-daemon.txt
new file mode 100644 (file)
index 0000000..99e47c9
--- /dev/null
@@ -0,0 +1,275 @@
+git-daemon(1)
+=============
+
+NAME
+----
+git-daemon - A really simple server for git repositories
+
+SYNOPSIS
+--------
+[verse]
+'git-daemon' [--verbose] [--syslog] [--export-all]
+             [--timeout=n] [--init-timeout=n] [--strict-paths]
+             [--base-path=path] [--user-path | --user-path=path]
+             [--interpolated-path=pathtemplate]
+             [--reuseaddr] [--detach] [--pid-file=file]
+             [--enable=service] [--disable=service]
+            [--allow-override=service] [--forbid-override=service]
+            [--inetd | [--listen=host_or_ipaddr] [--port=n] [--user=user [--group=group]]
+            [directory...]
+
+DESCRIPTION
+-----------
+A really simple TCP git daemon that normally listens on port "DEFAULT_GIT_PORT"
+aka 9418.  It waits for a connection asking for a service, and will serve
+that service if it is enabled.
+
+It verifies that the directory has the magic file "git-daemon-export-ok", and
+it will refuse to export any git directory that hasn't explicitly been marked
+for export this way (unless the '--export-all' parameter is specified). If you
+pass some directory paths as 'git-daemon' arguments, you can further restrict
+the offers to a whitelist comprising of those.
+
+By default, only `upload-pack` service is enabled, which serves
+`git-fetch-pack` and `git-peek-remote` clients that are invoked
+from `git-fetch`, `git-ls-remote`, and `git-clone`.
+
+This is ideally suited for read-only updates, i.e., pulling from
+git repositories.
+
+An `upload-archive` also exists to serve `git-archive`.
+
+OPTIONS
+-------
+--strict-paths::
+       Match paths exactly (i.e. don't allow "/foo/repo" when the real path is
+       "/foo/repo.git" or "/foo/repo/.git") and don't do user-relative paths.
+       git-daemon will refuse to start when this option is enabled and no
+       whitelist is specified.
+
+--base-path::
+       Remap all the path requests as relative to the given path.
+       This is sort of "GIT root" - if you run git-daemon with
+       '--base-path=/srv/git' on example.com, then if you later try to pull
+       'git://example.com/hello.git', `git-daemon` will interpret the path
+       as '/srv/git/hello.git'.
+
+--base-path-relaxed::
+       If --base-path is enabled and repo lookup fails, with this option
+       `git-daemon` will attempt to lookup without prefixing the base path.
+       This is useful for switching to --base-path usage, while still
+       allowing the old paths.
+
+--interpolated-path=pathtemplate::
+       To support virtual hosting, an interpolated path template can be
+       used to dynamically construct alternate paths.  The template
+       supports %H for the target hostname as supplied by the client but
+       converted to all lowercase, %CH for the canonical hostname,
+       %IP for the server's IP address, %P for the port number,
+       and %D for the absolute path of the named repository.
+       After interpolation, the path is validated against the directory
+       whitelist.
+
+--export-all::
+       Allow pulling from all directories that look like GIT repositories
+       (have the 'objects' and 'refs' subdirectories), even if they
+       do not have the 'git-daemon-export-ok' file.
+
+--inetd::
+       Have the server run as an inetd service. Implies --syslog.
+       Incompatible with --port, --listen, --user and --group options.
+
+--listen=host_or_ipaddr::
+       Listen on an a specific IP address or hostname.  IP addresses can
+       be either an IPv4 address or an IPV6 address if supported.  If IPv6
+       is not supported, then --listen=hostname is also not supported and
+       --listen must be given an IPv4 address.
+       Incompatible with '--inetd' option.
+
+--port=n::
+       Listen on an alternative port.  Incompatible with '--inetd' option.
+
+--init-timeout::
+       Timeout between the moment the connection is established and the
+       client request is received (typically a rather low value, since
+       that should be basically immediate).
+
+--timeout::
+       Timeout for specific client sub-requests. This includes the time
+       it takes for the server to process the sub-request and time spent
+       waiting for next client's request.
+
+--syslog::
+       Log to syslog instead of stderr. Note that this option does not imply
+       --verbose, thus by default only error conditions will be logged.
+
+--user-path, --user-path=path::
+       Allow ~user notation to be used in requests.  When
+       specified with no parameter, requests to
+       git://host/~alice/foo is taken as a request to access
+       'foo' repository in the home directory of user `alice`.
+       If `--user-path=path` is specified, the same request is
+       taken as a request to access `path/foo` repository in
+       the home directory of user `alice`.
+
+--verbose::
+       Log details about the incoming connections and requested files.
+
+--reuseaddr::
+       Use SO_REUSEADDR when binding the listening socket.
+       This allows the server to restart without waiting for
+       old connections to time out.
+
+--detach::
+       Detach from the shell. Implies --syslog.
+
+--pid-file=file::
+       Save the process id in 'file'.  Ignored when the daemon
+       is run under `--inetd`.
+
+--user=user, --group=group::
+       Change daemon's uid and gid before entering the service loop.
+       When only `--user` is given without `--group`, the
+       primary group ID for the user is used.  The values of
+       the option are given to `getpwnam(3)` and `getgrnam(3)`
+       and numeric IDs are not supported.
++
+Giving these options is an error when used with `--inetd`; use
+the facility of inet daemon to achieve the same before spawning
+`git-daemon` if needed.
+
+--enable=service, --disable=service::
+       Enable/disable the service site-wide per default.  Note
+       that a service disabled site-wide can still be enabled
+       per repository if it is marked overridable and the
+       repository enables the service with an configuration
+       item.
+
+--allow-override=service, --forbid-override=service::
+       Allow/forbid overriding the site-wide default with per
+       repository configuration.  By default, all the services
+       are overridable.
+
+<directory>::
+       A directory to add to the whitelist of allowed directories. Unless
+       --strict-paths is specified this will also include subdirectories
+       of each named directory.
+
+SERVICES
+--------
+
+These services can be globally enabled/disabled using the
+command line options of this command.  If a finer-grained
+control is desired (e.g. to allow `git-archive` to be run
+against only in a few selected repositories the daemon serves),
+the per-repository configuration file can be used to enable or
+disable them.
+
+upload-pack::
+       This serves `git-fetch-pack` and `git-peek-remote`
+       clients.  It is enabled by default, but a repository can
+       disable it by setting `daemon.uploadpack` configuration
+       item to `false`.
+
+upload-archive::
+       This serves `git-archive --remote`.  It is disabled by
+       default, but a repository can enable it by setting
+       `daemon.uploadarchive` configuration item to `true`.
+
+receive-pack::
+       This serves `git-send-pack` clients, allowing anonymous
+       push.  It is disabled by default, as there is _no_
+       authentication in the protocol (in other words, anybody
+       can push anything into the repository, including removal
+       of refs).  This is solely meant for a closed LAN setting
+       where everybody is friendly.  This service can be
+       enabled by `daemon.receivepack` configuration item to
+       `true`.
+
+EXAMPLES
+--------
+We assume the following in /etc/services::
++
+------------
+$ grep 9418 /etc/services
+git            9418/tcp                # Git Version Control System
+------------
+
+git-daemon as inetd server::
+       To set up `git-daemon` as an inetd service that handles any
+       repository under the whitelisted set of directories, /pub/foo
+       and /pub/bar, place an entry like the following into
+       /etc/inetd all on one line:
++
+------------------------------------------------
+       git stream tcp nowait nobody  /usr/bin/git-daemon
+               git-daemon --inetd --verbose --export-all
+               /pub/foo /pub/bar
+------------------------------------------------
+
+
+git-daemon as inetd server for virtual hosts::
+       To set up `git-daemon` as an inetd service that handles
+       repositories for different virtual hosts, `www.example.com`
+       and `www.example.org`, place an entry like the following into
+       `/etc/inetd` all on one line:
++
+------------------------------------------------
+       git stream tcp nowait nobody /usr/bin/git-daemon
+               git-daemon --inetd --verbose --export-all
+               --interpolated-path=/pub/%H%D
+               /pub/www.example.org/software
+               /pub/www.example.com/software
+               /software
+------------------------------------------------
++
+In this example, the root-level directory `/pub` will contain
+a subdirectory for each virtual host name supported.
+Further, both hosts advertise repositories simply as
+`git://www.example.com/software/repo.git`.  For pre-1.4.0
+clients, a symlink from `/software` into the appropriate
+default repository could be made as well.
+
+
+git-daemon as regular daemon for virtual hosts::
+       To set up `git-daemon` as a regular, non-inetd service that
+       handles repositories for multiple virtual hosts based on
+       their IP addresses, start the daemon like this:
++
+------------------------------------------------
+       git-daemon --verbose --export-all
+               --interpolated-path=/pub/%IP/%D
+               /pub/192.168.1.200/software
+               /pub/10.10.220.23/software
+------------------------------------------------
++
+In this example, the root-level directory `/pub` will contain
+a subdirectory for each virtual host IP address supported.
+Repositories can still be accessed by hostname though, assuming
+they correspond to these IP addresses.
+
+selectively enable/disable services per repository::
+       To enable `git-archive --remote` and disable `git-fetch` against
+       a repository, have the following in the configuration file in the
+       repository (that is the file 'config' next to 'HEAD', 'refs' and
+       'objects').
++
+----------------------------------------------------------------
+       [daemon]
+               uploadpack = false
+               uploadarchive = true
+----------------------------------------------------------------
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>, YOSHIFUJI Hideaki
+<yoshfuji@linux-ipv6.org> and the git-list <git@vger.kernel.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-describe.txt b/Documentation/git-describe.txt
new file mode 100644 (file)
index 0000000..ac23e28
--- /dev/null
@@ -0,0 +1,126 @@
+git-describe(1)
+===============
+
+NAME
+----
+git-describe - Show the most recent tag that is reachable from a commit
+
+
+SYNOPSIS
+--------
+'git-describe' [--all] [--tags] [--contains] [--abbrev=<n>] <committish>...
+
+DESCRIPTION
+-----------
+The command finds the most recent tag that is reachable from a
+commit, and if the commit itself is pointed at by the tag, shows
+the tag.  Otherwise, it suffixes the tag name with the number of
+additional commits and the abbreviated object name of the commit.
+
+
+OPTIONS
+-------
+<committish>::
+       The object name of the committish.
+
+--all::
+       Instead of using only the annotated tags, use any ref
+       found in `.git/refs/`.
+
+--tags::
+       Instead of using only the annotated tags, use any tag
+       found in `.git/refs/tags`.
+
+--contains::
+       Instead of finding the tag that predates the commit, find
+       the tag that comes after the commit, and thus contains it.
+       Automatically implies --tags.
+
+--abbrev=<n>::
+       Instead of using the default 8 hexadecimal digits as the
+       abbreviated object name, use <n> digits.
+
+--candidates=<n>::
+       Instead of considering only the 10 most recent tags as
+       candidates to describe the input committish consider
+       up to <n> candidates.  Increasing <n> above 10 will take
+       slightly longer but may produce a more accurate result.
+
+--debug::
+       Verbosely display information about the searching strategy
+       being employed to standard error.  The tag name will still
+       be printed to standard out.
+
+EXAMPLES
+--------
+
+With something like git.git current tree, I get:
+
+       [torvalds@g5 git]$ git-describe parent
+       v1.0.4-14-g2414721
+
+i.e. the current head of my "parent" branch is based on v1.0.4,
+but since it has a handful commits on top of that,
+describe has added the number of additional commits ("14") and
+an abbreviated object name for the commit itself ("2414721")
+at the end.
+
+The number of additional commits is the number
+of commits which would be displayed by "git log v1.0.4..parent".
+The hash suffix is "-g" + 7-char abbreviation for the tip commit
+of parent (which was `2414721b194453f058079d897d13c4e377f92dc6`).
+
+Doing a "git-describe" on a tag-name will just show the tag name:
+
+       [torvalds@g5 git]$ git-describe v1.0.4
+       v1.0.4
+
+With --all, the command can use branch heads as references, so
+the output shows the reference path as well:
+
+       [torvalds@g5 git]$ git describe --all --abbrev=4 v1.0.5^2
+       tags/v1.0.0-21-g975b
+
+       [torvalds@g5 git]$ git describe --all HEAD^
+       heads/lt/describe-7-g975b
+
+With --abbrev set to 0, the command can be used to find the
+closest tagname without any suffix:
+
+       [torvalds@g5 git]$ git describe --abbrev=0 v1.0.5^2
+       tags/v1.0.0
+
+SEARCH STRATEGY
+---------------
+
+For each committish supplied "git describe" will first look for
+a tag which tags exactly that commit.  Annotated tags will always
+be preferred over lightweight tags, and tags with newer dates will
+always be preferred over tags with older dates.  If an exact match
+is found, its name will be output and searching will stop.
+
+If an exact match was not found "git describe" will walk back
+through the commit history to locate an ancestor commit which
+has been tagged.  The ancestor's tag will be output along with an
+abbreviation of the input committish's SHA1.
+
+If multiple tags were found during the walk then the tag which
+has the fewest commits different from the input committish will be
+selected and output.  Here fewest commits different is defined as
+the number of commits which would be shown by "git log tag..input"
+will be the smallest number of commits possible.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>, but somewhat
+butchered by Junio C Hamano <junkio@cox.net>.  Later significantly
+updated by Shawn Pearce <spearce@spearce.org>.
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-diff-files.txt b/Documentation/git-diff-files.txt
new file mode 100644 (file)
index 0000000..d8a0a86
--- /dev/null
@@ -0,0 +1,60 @@
+git-diff-files(1)
+=================
+
+NAME
+----
+git-diff-files - Compares files in the working tree and the index
+
+
+SYNOPSIS
+--------
+'git-diff-files' [-q] [-0|-1|-2|-3|-c|--cc|--no-index] [<common diff options>] [<path>...]
+
+DESCRIPTION
+-----------
+Compares the files in the working tree and the index.  When paths
+are specified, compares only those named paths.  Otherwise all
+entries in the index are compared.  The output format is the
+same as "git-diff-index" and "git-diff-tree".
+
+OPTIONS
+-------
+include::diff-options.txt[]
+
+-1 -2 -3 or --base --ours --theirs, and -0::
+       Diff against the "base" version, "our branch" or "their
+       branch" respectively.  With these options, diffs for
+       merged entries are not shown.
++
+The default is to diff against our branch (-2) and the
+cleanly resolved paths.  The option -0 can be given to
+omit diff output for unmerged entries and just show "Unmerged".
+
+-c,--cc::
+       This compares stage 2 (our branch), stage 3 (their
+       branch) and the working tree file and outputs a combined
+       diff, similar to the way 'diff-tree' shows a merge
+       commit with these flags.
+
+--no-index::
+       Compare the two given files / directories.
+
+-q::
+       Remain silent even on nonexistent files
+
+Output format
+-------------
+include::diff-format.txt[]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-diff-index.txt b/Documentation/git-diff-index.txt
new file mode 100644 (file)
index 0000000..7bd262c
--- /dev/null
@@ -0,0 +1,132 @@
+git-diff-index(1)
+=================
+
+NAME
+----
+git-diff-index - Compares content and mode of blobs between the index and repository
+
+
+SYNOPSIS
+--------
+'git-diff-index' [-m] [--cached] [<common diff options>] <tree-ish> [<path>...]
+
+DESCRIPTION
+-----------
+Compares the content and mode of the blobs found via a tree
+object with the content of the current index and, optionally
+ignoring the stat state of the file on disk.  When paths are
+specified, compares only those named paths.  Otherwise all
+entries in the index are compared.
+
+OPTIONS
+-------
+include::diff-options.txt[]
+
+<tree-ish>::
+       The id of a tree object to diff against.
+
+--cached::
+       do not consider the on-disk file at all
+
+-m::
+       By default, files recorded in the index but not checked
+       out are reported as deleted.  This flag makes
+       "git-diff-index" say that all non-checked-out files are up
+       to date.
+
+Output format
+-------------
+include::diff-format.txt[]
+
+Operating Modes
+---------------
+You can choose whether you want to trust the index file entirely
+(using the '--cached' flag) or ask the diff logic to show any files
+that don't match the stat state as being "tentatively changed".  Both
+of these operations are very useful indeed.
+
+Cached Mode
+-----------
+If '--cached' is specified, it allows you to ask:
+
+       show me the differences between HEAD and the current index
+       contents (the ones I'd write with a "git-write-tree")
+
+For example, let's say that you have worked on your working directory, updated
+some files in the index and are ready to commit. You want to see exactly
+*what* you are going to commit, without having to write a new tree
+object and compare it that way, and to do that, you just do
+
+       git-diff-index --cached HEAD
+
+Example: let's say I had renamed `commit.c` to `git-commit.c`, and I had
+done an "git-update-index" to make that effective in the index file.
+"git-diff-files" wouldn't show anything at all, since the index file
+matches my working directory. But doing a "git-diff-index" does:
+
+  torvalds@ppc970:~/git> git-diff-index --cached HEAD
+  -100644 blob    4161aecc6700a2eb579e842af0b7f22b98443f74        commit.c
+  +100644 blob    4161aecc6700a2eb579e842af0b7f22b98443f74        git-commit.c
+
+You can see easily that the above is a rename.
+
+In fact, "git-diff-index --cached" *should* always be entirely equivalent to
+actually doing a "git-write-tree" and comparing that. Except this one is much
+nicer for the case where you just want to check where you are.
+
+So doing a "git-diff-index --cached" is basically very useful when you are
+asking yourself "what have I already marked for being committed, and
+what's the difference to a previous tree".
+
+Non-cached Mode
+---------------
+The "non-cached" mode takes a different approach, and is potentially
+the more useful of the two in that what it does can't be emulated with
+a "git-write-tree" + "git-diff-tree". Thus that's the default mode.
+The non-cached version asks the question:
+
+  show me the differences between HEAD and the currently checked out
+  tree - index contents _and_ files that aren't up-to-date
+
+which is obviously a very useful question too, since that tells you what
+you *could* commit. Again, the output matches the "git-diff-tree -r"
+output to a tee, but with a twist.
+
+The twist is that if some file doesn't match the index, we don't have
+a backing store thing for it, and we use the magic "all-zero" sha1 to
+show that. So let's say that you have edited `kernel/sched.c`, but
+have not actually done a "git-update-index" on it yet - there is no
+"object" associated with the new state, and you get:
+
+  torvalds@ppc970:~/v2.6/linux> git-diff-index HEAD
+  *100644->100664 blob    7476bb......->000000......      kernel/sched.c
+
+i.e., it shows that the tree has changed, and that `kernel/sched.c` has is
+not up-to-date and may contain new stuff. The all-zero sha1 means that to
+get the real diff, you need to look at the object in the working directory
+directly rather than do an object-to-object diff.
+
+NOTE: As with other commands of this type, "git-diff-index" does not
+actually look at the contents of the file at all. So maybe
+`kernel/sched.c` hasn't actually changed, and it's just that you
+touched it. In either case, it's a note that you need to
+"git-update-index" it to make the index be in sync.
+
+NOTE: You can have a mixture of files show up as "has been updated"
+and "is still dirty in the working directory" together. You can always
+tell which file is in which state, since the "has been updated" ones
+show a valid sha1, and the "not in sync with the index" ones will
+always have the special all-zero sha1.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-diff-tree.txt b/Documentation/git-diff-tree.txt
new file mode 100644 (file)
index 0000000..6b3f74e
--- /dev/null
@@ -0,0 +1,168 @@
+git-diff-tree(1)
+================
+
+NAME
+----
+git-diff-tree - Compares the content and mode of blobs found via two tree objects
+
+
+SYNOPSIS
+--------
+[verse]
+'git-diff-tree' [--stdin] [-m] [-s] [-v] [--no-commit-id] [--pretty]
+             [-t] [-r] [-c | --cc] [--root] [<common diff options>]
+             <tree-ish> [<tree-ish>] [<path>...]
+
+DESCRIPTION
+-----------
+Compares the content and mode of the blobs found via two tree objects.
+
+If there is only one <tree-ish> given, the commit is compared with its parents
+(see --stdin below).
+
+Note that "git-diff-tree" can use the tree encapsulated in a commit object.
+
+OPTIONS
+-------
+include::diff-options.txt[]
+
+<tree-ish>::
+       The id of a tree object.
+
+<path>...::
+       If provided, the results are limited to a subset of files
+       matching one of these prefix strings.
+       i.e., file matches `/^<pattern1>|<pattern2>|.../`
+       Note that this parameter does not provide any wildcard or regexp
+       features.
+
+-r::
+        recurse into sub-trees
+
+-t::
+       show tree entry itself as well as subtrees.  Implies -r.
+
+--root::
+       When '--root' is specified the initial commit will be showed as a big
+       creation event. This is equivalent to a diff against the NULL tree.
+
+--stdin::
+       When '--stdin' is specified, the command does not take
+       <tree-ish> arguments from the command line.  Instead, it
+       reads either one <commit> or a pair of <tree-ish>
+       separated with a single space from its standard input.
++
+When a single commit is given on one line of such input, it compares
+the commit with its parents.  The following flags further affects its
+behavior.  This does not apply to the case where two <tree-ish>
+separated with a single space are given.
+
+-m::
+       By default, "git-diff-tree --stdin" does not show
+       differences for merge commits.  With this flag, it shows
+       differences to that commit from all of its parents. See
+       also '-c'.
+
+-s::
+       By default, "git-diff-tree --stdin" shows differences,
+       either in machine-readable form (without '-p') or in patch
+       form (with '-p').  This output can be suppressed.  It is
+       only useful with '-v' flag.
+
+-v::
+       This flag causes "git-diff-tree --stdin" to also show
+       the commit message before the differences.
+
+include::pretty-options.txt[]
+
+--no-commit-id::
+       git-diff-tree outputs a line with the commit ID when
+       applicable.  This flag suppressed the commit ID output.
+
+-c::
+       This flag changes the way a merge commit is displayed
+       (which means it is useful only when the command is given
+       one <tree-ish>, or '--stdin').  It shows the differences
+       from each of the parents to the merge result simultaneously
+       instead of showing pairwise diff between a parent and the
+       result one at a time (which is what the '-m' option does).
+       Furthermore, it lists only files which were modified
+       from all parents.
+
+--cc::
+       This flag changes the way a merge commit patch is displayed,
+       in a similar way to the '-c' option. It implies the '-c'
+       and '-p' options and further compresses the patch output
+       by omitting hunks that show differences from only one
+       parent, or show the same change from all but one parent
+       for an Octopus merge.  When this optimization makes all
+       hunks disappear, the commit itself and the commit log
+       message is not shown, just like in any other "empty diff" case.
+
+--always::
+       Show the commit itself and the commit log message even
+       if the diff itself is empty.
+
+
+include::pretty-formats.txt[]
+
+
+Limiting Output
+---------------
+If you're only interested in differences in a subset of files, for
+example some architecture-specific files, you might do:
+
+       git-diff-tree -r <tree-ish> <tree-ish> arch/ia64 include/asm-ia64
+
+and it will only show you what changed in those two directories.
+
+Or if you are searching for what changed in just `kernel/sched.c`, just do
+
+       git-diff-tree -r <tree-ish> <tree-ish> kernel/sched.c
+
+and it will ignore all differences to other files.
+
+The pattern is always the prefix, and is matched exactly.  There are no
+wildcards.  Even stricter, it has to match a complete path component.
+I.e. "foo" does not pick up `foobar.h`.  "foo" does match `foo/bar.h`
+so it can be used to name subdirectories.
+
+An example of normal usage is:
+
+  torvalds@ppc970:~/git> git-diff-tree 5319e4......
+  *100664->100664 blob    ac348b.......->a01513.......      git-fsck-objects.c
+
+which tells you that the last commit changed just one file (it's from
+this one:
+
+-----------------------------------------------------------------------------
+commit 3c6f7ca19ad4043e9e72fa94106f352897e651a8
+tree 5319e4d609cdd282069cc4dce33c1db559539b03
+parent b4e628ea30d5ab3606119d2ea5caeab141d38df7
+author Linus Torvalds <torvalds@ppc970.osdl.org> Sat Apr 9 12:02:30 2005
+committer Linus Torvalds <torvalds@ppc970.osdl.org> Sat Apr 9 12:02:30 2005
+
+Make "git-fsck-objects" print out all the root commits it finds.
+
+Once I do the reference tracking, I'll also make it print out all the
+HEAD commits it finds, which is even more interesting.
+-----------------------------------------------------------------------------
+
+in case you care).
+
+Output format
+-------------
+include::diff-format.txt[]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-diff.txt b/Documentation/git-diff.txt
new file mode 100644 (file)
index 0000000..2808a5e
--- /dev/null
@@ -0,0 +1,171 @@
+git-diff(1)
+===========
+
+NAME
+----
+git-diff - Show changes between commits, commit and working tree, etc
+
+
+SYNOPSIS
+--------
+'git-diff' [<common diff options>] <commit>{0,2} [--] [<path>...]
+
+DESCRIPTION
+-----------
+Show changes between two trees, a tree and the working tree, a
+tree and the index file, or the index file and the working tree.
+
+'git-diff' [--options] [--] [<path>...]::
+
+       This form is to view the changes you made relative to
+       the index (staging area for the next commit).  In other
+       words, the differences are what you _could_ tell git to
+       further add to the index but you still haven't.  You can
+       stage these changes by using gitlink:git-add[1].
++
+If exactly two paths are given, and at least one is untracked,
+compare the two files / directories. This behavior can be
+forced by --no-index.
+
+'git-diff' [--options] --cached [<commit>] [--] [<path>...]::
+
+       This form is to view the changes you staged for the next
+       commit relative to the named <commit>.  Typically you
+       would want comparison with the latest commit, so if you
+       do not give <commit>, it defaults to HEAD.
+
+'git-diff' [--options] <commit> [--] [<path>...]::
+
+       This form is to view the changes you have in your
+       working tree relative to the named <commit>.  You can
+       use HEAD to compare it with the latest commit, or a
+       branch name to compare with the tip of a different
+       branch.
+
+'git-diff' [--options] <commit> <commit> [--] [<path>...]::
+
+       This is to view the changes between two arbitrary
+       <commit>.
+
+'git-diff' [--options] <commit>..<commit> [--] [<path>...]::
+
+       This is synonymous to the previous form.  If <commit> on
+       one side is omitted, it will have the same effect as
+       using HEAD instead.
+
+'git-diff' [--options] <commit>\...<commit> [--] [<path>...]::
+
+       This form is to view the changes on the branch containing
+       and up to the second <commit>, starting at a common ancestor
+       of both <commit>.  "git-diff A\...B" is equivalent to
+       "git-diff $(git-merge-base A B) B".  You can omit any one
+       of <commit>, which has the same effect as using HEAD instead.
+
+Just in case if you are doing something exotic, it should be
+noted that all of the <commit> in the above description, except
+for the last two forms that use ".." notations, can be any
+<tree-ish>.
+
+For a more complete list of ways to spell <commit>, see
+"SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1].
+However, "diff" is about comparing two _endpoints_, not ranges,
+and the range notations ("<commit>..<commit>" and
+"<commit>\...<commit>") do not mean a range as defined in the
+"SPECIFYING RANGES" section in gitlink:git-rev-parse[1].
+
+OPTIONS
+-------
+:git-diff: 1
+include::diff-options.txt[]
+
+<path>...::
+       The <paths> parameters, when given, are used to limit
+       the diff to the named paths (you can give directory
+       names and get diff for all files under them).
+
+Output format
+-------------
+include::diff-format.txt[]
+
+EXAMPLES
+--------
+
+Various ways to check your working tree::
++
+------------
+$ git diff            <1>
+$ git diff --cached   <2>
+$ git diff HEAD       <3>
+------------
++
+<1> Changes in the working tree not yet staged for the next commit.
+<2> Changes between the index and your last commit; what you
+would be committing if you run "git commit" without "-a" option.
+<3> Changes in the working tree since your last commit; what you
+would be committing if you run "git commit -a"
+
+Comparing with arbitrary commits::
++
+------------
+$ git diff test            <1>
+$ git diff HEAD -- ./test  <2>
+$ git diff HEAD^ HEAD      <3>
+------------
++
+<1> Instead of using the tip of the current branch, compare with the
+tip of "test" branch.
+<2> Instead of comparing with the tip of "test" branch, compare with
+the tip of the current branch, but limit the comparison to the
+file "test".
+<3> Compare the version before the last commit and the last commit.
+
+Comparing branches::
++
+------------
+$ git diff topic master    <1>
+$ git diff topic..master   <2>
+$ git diff topic...master  <3>
+------------
++
+<1> Changes between the tips of the topic and the master branches.
+<2> Same as above.
+<3> Changes that occurred on the master branch since when the topic
+branch was started off it.
+
+Limiting the diff output::
++
+------------
+$ git diff --diff-filter=MRC            <1>
+$ git diff --name-status                <2>
+$ git diff arch/i386 include/asm-i386   <3>
+------------
++
+<1> Show only modification, rename and copy, but not addition
+nor deletion.
+<2> Show only names and the nature of change, but not actual
+diff output.
+<3> Limit diff output to named subtrees.
+
+Munging the diff output::
++
+------------
+$ git diff --find-copies-harder -B -C  <1>
+$ git diff -R                          <2>
+------------
++
+<1> Spend extra cycles to find renames, copies and complete
+rewrites (very expensive).
+<2> Output diff in reverse.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-fast-export.txt b/Documentation/git-fast-export.txt
new file mode 100644 (file)
index 0000000..fd3d571
--- /dev/null
@@ -0,0 +1,83 @@
+git-fast-export(1)
+==================
+
+NAME
+----
+git-fast-export - Git data exporter
+
+
+SYNOPSIS
+--------
+'git-fast-export [options]' | 'git-fast-import'
+
+DESCRIPTION
+-----------
+This program dumps the given revisions in a form suitable to be piped
+into gitlink:git-fast-import[1].
+
+You can use it as a human readable bundle replacement (see
+gitlink:git-bundle[1]), or as a kind of an interactive
+gitlink:git-filter-branch[1].
+
+
+OPTIONS
+-------
+--progress=<n>::
+       Insert 'progress' statements every <n> objects, to be shown by
+       gitlink:git-fast-import[1] during import.
+
+--signed-tags=(verbatim|warn|strip|abort)::
+       Specify how to handle signed tags.  Since any transformation
+       after the export can change the tag names (which can also happen
+       when excluding revisions) the signatures will not match.
++
+When asking to 'abort' (which is the default), this program will die
+when encountering a signed tag.  With 'strip', the tags will be made
+unsigned, with 'verbatim', they will be silently exported
+and with 'warn', they will be exported, but you will see a warning.
+
+
+EXAMPLES
+--------
+
+-------------------------------------------------------------------
+$ git fast-export --all | (cd /empty/repository && git fast-import)
+-------------------------------------------------------------------
+
+This will export the whole repository and import it into the existing
+empty repository.  Except for reencoding commits that are not in
+UTF-8, it would be a one-to-one mirror.
+
+-----------------------------------------------------
+$ git fast-export master~5..master |
+       sed "s|refs/heads/master|refs/heads/other|" |
+       git fast-import
+-----------------------------------------------------
+
+This makes a new branch called 'other' from 'master~5..master'
+(i.e. if 'master' has linear history, it will take the last 5 commits).
+
+Note that this assumes that none of the blobs and commit messages
+referenced by that revision range contains the string
+'refs/heads/master'.
+
+
+Limitations
+-----------
+
+Since gitlink:git-fast-import[1] cannot tag trees, you will not be
+able to export the linux-2.6.git repository completely, as it contains
+a tag referencing a tree instead of a commit.
+
+
+Author
+------
+Written by Johannes E. Schindelin <johannes.schindelin@gmx.de>.
+
+Documentation
+--------------
+Documentation by Johannes E. Schindelin <johannes.schindelin@gmx.de>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-fast-import.txt b/Documentation/git-fast-import.txt
new file mode 100644 (file)
index 0000000..d511967
--- /dev/null
@@ -0,0 +1,1030 @@
+git-fast-import(1)
+==================
+
+NAME
+----
+git-fast-import - Backend for fast Git data importers
+
+
+SYNOPSIS
+--------
+frontend | 'git-fast-import' [options]
+
+DESCRIPTION
+-----------
+This program is usually not what the end user wants to run directly.
+Most end users want to use one of the existing frontend programs,
+which parses a specific type of foreign source and feeds the contents
+stored there to git-fast-import.
+
+fast-import reads a mixed command/data stream from standard input and
+writes one or more packfiles directly into the current repository.
+When EOF is received on standard input, fast import writes out
+updated branch and tag refs, fully updating the current repository
+with the newly imported data.
+
+The fast-import backend itself can import into an empty repository (one that
+has already been initialized by gitlink:git-init[1]) or incrementally
+update an existing populated repository.  Whether or not incremental
+imports are supported from a particular foreign source depends on
+the frontend program in use.
+
+
+OPTIONS
+-------
+--date-format=<fmt>::
+       Specify the type of dates the frontend will supply to
+       fast-import within `author`, `committer` and `tagger` commands.
+       See ``Date Formats'' below for details about which formats
+       are supported, and their syntax.
+
+--force::
+       Force updating modified existing branches, even if doing
+       so would cause commits to be lost (as the new commit does
+       not contain the old commit).
+
+--max-pack-size=<n>::
+       Maximum size of each output packfile, expressed in MiB.
+       The default is 4096 (4 GiB) as that is the maximum allowed
+       packfile size (due to file format limitations). Some
+       importers may wish to lower this, such as to ensure the
+       resulting packfiles fit on CDs.
+
+--depth=<n>::
+       Maximum delta depth, for blob and tree deltification.
+       Default is 10.
+
+--active-branches=<n>::
+       Maximum number of branches to maintain active at once.
+       See ``Memory Utilization'' below for details.  Default is 5.
+
+--export-marks=<file>::
+       Dumps the internal marks table to <file> when complete.
+       Marks are written one per line as `:markid SHA-1`.
+       Frontends can use this file to validate imports after they
+       have been completed, or to save the marks table across
+       incremental runs.  As <file> is only opened and truncated
+       at checkpoint (or completion) the same path can also be
+       safely given to \--import-marks.
+
+--import-marks=<file>::
+       Before processing any input, load the marks specified in
+       <file>.  The input file must exist, must be readable, and
+       must use the same format as produced by \--export-marks.
+       Multiple options may be supplied to import more than one
+       set of marks.  If a mark is defined to different values,
+       the last file wins.
+
+--export-pack-edges=<file>::
+       After creating a packfile, print a line of data to
+       <file> listing the filename of the packfile and the last
+       commit on each branch that was written to that packfile.
+       This information may be useful after importing projects
+       whose total object set exceeds the 4 GiB packfile limit,
+       as these commits can be used as edge points during calls
+       to gitlink:git-pack-objects[1].
+
+--quiet::
+       Disable all non-fatal output, making fast-import silent when it
+       is successful.  This option disables the output shown by
+       \--stats.
+
+--stats::
+       Display some basic statistics about the objects fast-import has
+       created, the packfiles they were stored into, and the
+       memory used by fast-import during this run.  Showing this output
+       is currently the default, but can be disabled with \--quiet.
+
+
+Performance
+-----------
+The design of fast-import allows it to import large projects in a minimum
+amount of memory usage and processing time.  Assuming the frontend
+is able to keep up with fast-import and feed it a constant stream of data,
+import times for projects holding 10+ years of history and containing
+100,000+ individual commits are generally completed in just 1-2
+hours on quite modest (~$2,000 USD) hardware.
+
+Most bottlenecks appear to be in foreign source data access (the
+source just cannot extract revisions fast enough) or disk IO (fast-import
+writes as fast as the disk will take the data).  Imports will run
+faster if the source data is stored on a different drive than the
+destination Git repository (due to less IO contention).
+
+
+Development Cost
+----------------
+A typical frontend for fast-import tends to weigh in at approximately 200
+lines of Perl/Python/Ruby code.  Most developers have been able to
+create working importers in just a couple of hours, even though it
+is their first exposure to fast-import, and sometimes even to Git.  This is
+an ideal situation, given that most conversion tools are throw-away
+(use once, and never look back).
+
+
+Parallel Operation
+------------------
+Like `git-push` or `git-fetch`, imports handled by fast-import are safe to
+run alongside parallel `git repack -a -d` or `git gc` invocations,
+or any other Git operation (including `git prune`, as loose objects
+are never used by fast-import).
+
+fast-import does not lock the branch or tag refs it is actively importing.
+After the import, during its ref update phase, fast-import tests each
+existing branch ref to verify the update will be a fast-forward
+update (the commit stored in the ref is contained in the new
+history of the commit to be written).  If the update is not a
+fast-forward update, fast-import will skip updating that ref and instead
+prints a warning message.  fast-import will always attempt to update all
+branch refs, and does not stop on the first failure.
+
+Branch updates can be forced with \--force, but its recommended that
+this only be used on an otherwise quiet repository.  Using \--force
+is not necessary for an initial import into an empty repository.
+
+
+Technical Discussion
+--------------------
+fast-import tracks a set of branches in memory.  Any branch can be created
+or modified at any point during the import process by sending a
+`commit` command on the input stream.  This design allows a frontend
+program to process an unlimited number of branches simultaneously,
+generating commits in the order they are available from the source
+data.  It also simplifies the frontend programs considerably.
+
+fast-import does not use or alter the current working directory, or any
+file within it.  (It does however update the current Git repository,
+as referenced by `GIT_DIR`.)  Therefore an import frontend may use
+the working directory for its own purposes, such as extracting file
+revisions from the foreign source.  This ignorance of the working
+directory also allows fast-import to run very quickly, as it does not
+need to perform any costly file update operations when switching
+between branches.
+
+Input Format
+------------
+With the exception of raw file data (which Git does not interpret)
+the fast-import input format is text (ASCII) based.  This text based
+format simplifies development and debugging of frontend programs,
+especially when a higher level language such as Perl, Python or
+Ruby is being used.
+
+fast-import is very strict about its input.  Where we say SP below we mean
+*exactly* one space.  Likewise LF means one (and only one) linefeed.
+Supplying additional whitespace characters will cause unexpected
+results, such as branch names or file names with leading or trailing
+spaces in their name, or early termination of fast-import when it encounters
+unexpected input.
+
+Stream Comments
+~~~~~~~~~~~~~~~
+To aid in debugging frontends fast-import ignores any line that
+begins with `#` (ASCII pound/hash) up to and including the line
+ending `LF`.  A comment line may contain any sequence of bytes
+that does not contain an LF and therefore may be used to include
+any detailed debugging information that might be specific to the
+frontend and useful when inspecting a fast-import data stream.
+
+Date Formats
+~~~~~~~~~~~~
+The following date formats are supported.  A frontend should select
+the format it will use for this import by passing the format name
+in the \--date-format=<fmt> command line option.
+
+`raw`::
+       This is the Git native format and is `<time> SP <offutc>`.
+       It is also fast-import's default format, if \--date-format was
+       not specified.
++
+The time of the event is specified by `<time>` as the number of
+seconds since the UNIX epoch (midnight, Jan 1, 1970, UTC) and is
+written as an ASCII decimal integer.
++
+The local offset is specified by `<offutc>` as a positive or negative
+offset from UTC.  For example EST (which is 5 hours behind UTC)
+would be expressed in `<tz>` by ``-0500'' while UTC is ``+0000''.
+The local offset does not affect `<time>`; it is used only as an
+advisement to help formatting routines display the timestamp.
++
+If the local offset is not available in the source material, use
+``+0000'', or the most common local offset.  For example many
+organizations have a CVS repository which has only ever been accessed
+by users who are located in the same location and timezone.  In this
+case a reasonable offset from UTC could be assumed.
++
+Unlike the `rfc2822` format, this format is very strict.  Any
+variation in formatting will cause fast-import to reject the value.
+
+`rfc2822`::
+       This is the standard email format as described by RFC 2822.
++
+An example value is ``Tue Feb 6 11:22:18 2007 -0500''.  The Git
+parser is accurate, but a little on the lenient side.  It is the
+same parser used by gitlink:git-am[1] when applying patches
+received from email.
++
+Some malformed strings may be accepted as valid dates.  In some of
+these cases Git will still be able to obtain the correct date from
+the malformed string.  There are also some types of malformed
+strings which Git will parse wrong, and yet consider valid.
+Seriously malformed strings will be rejected.
++
+Unlike the `raw` format above, the timezone/UTC offset information
+contained in an RFC 2822 date string is used to adjust the date
+value to UTC prior to storage.  Therefore it is important that
+this information be as accurate as possible.
++
+If the source material uses RFC 2822 style dates,
+the frontend should let fast-import handle the parsing and conversion
+(rather than attempting to do it itself) as the Git parser has
+been well tested in the wild.
++
+Frontends should prefer the `raw` format if the source material
+already uses UNIX-epoch format, can be coaxed to give dates in that
+format, or its format is easily convertible to it, as there is no
+ambiguity in parsing.
+
+`now`::
+       Always use the current time and timezone.  The literal
+       `now` must always be supplied for `<when>`.
++
+This is a toy format.  The current time and timezone of this system
+is always copied into the identity string at the time it is being
+created by fast-import.  There is no way to specify a different time or
+timezone.
++
+This particular format is supplied as its short to implement and
+may be useful to a process that wants to create a new commit
+right now, without needing to use a working directory or
+gitlink:git-update-index[1].
++
+If separate `author` and `committer` commands are used in a `commit`
+the timestamps may not match, as the system clock will be polled
+twice (once for each command).  The only way to ensure that both
+author and committer identity information has the same timestamp
+is to omit `author` (thus copying from `committer`) or to use a
+date format other than `now`.
+
+Commands
+~~~~~~~~
+fast-import accepts several commands to update the current repository
+and control the current import process.  More detailed discussion
+(with examples) of each command follows later.
+
+`commit`::
+       Creates a new branch or updates an existing branch by
+       creating a new commit and updating the branch to point at
+       the newly created commit.
+
+`tag`::
+       Creates an annotated tag object from an existing commit or
+       branch.  Lightweight tags are not supported by this command,
+       as they are not recommended for recording meaningful points
+       in time.
+
+`reset`::
+       Reset an existing branch (or a new branch) to a specific
+       revision.  This command must be used to change a branch to
+       a specific revision without making a commit on it.
+
+`blob`::
+       Convert raw file data into a blob, for future use in a
+       `commit` command.  This command is optional and is not
+       needed to perform an import.
+
+`checkpoint`::
+       Forces fast-import to close the current packfile, generate its
+       unique SHA-1 checksum and index, and start a new packfile.
+       This command is optional and is not needed to perform
+       an import.
+
+`progress`::
+       Causes fast-import to echo the entire line to its own
+       standard output.  This command is optional and is not needed
+       to perform an import.
+
+`commit`
+~~~~~~~~
+Create or update a branch with a new commit, recording one logical
+change to the project.
+
+....
+       'commit' SP <ref> LF
+       mark?
+       ('author' SP <name> SP LT <email> GT SP <when> LF)?
+       'committer' SP <name> SP LT <email> GT SP <when> LF
+       data
+       ('from' SP <committish> LF)?
+       ('merge' SP <committish> LF)?
+       (filemodify | filedelete | filecopy | filerename | filedeleteall)*
+       LF?
+....
+
+where `<ref>` is the name of the branch to make the commit on.
+Typically branch names are prefixed with `refs/heads/` in
+Git, so importing the CVS branch symbol `RELENG-1_0` would use
+`refs/heads/RELENG-1_0` for the value of `<ref>`.  The value of
+`<ref>` must be a valid refname in Git.  As `LF` is not valid in
+a Git refname, no quoting or escaping syntax is supported here.
+
+A `mark` command may optionally appear, requesting fast-import to save a
+reference to the newly created commit for future use by the frontend
+(see below for format).  It is very common for frontends to mark
+every commit they create, thereby allowing future branch creation
+from any imported commit.
+
+The `data` command following `committer` must supply the commit
+message (see below for `data` command syntax).  To import an empty
+commit message use a 0 length data.  Commit messages are free-form
+and are not interpreted by Git.  Currently they must be encoded in
+UTF-8, as fast-import does not permit other encodings to be specified.
+
+Zero or more `filemodify`, `filedelete`, `filecopy`, `filerename`
+and `filedeleteall` commands
+may be included to update the contents of the branch prior to
+creating the commit.  These commands may be supplied in any order.
+However it is recommended that a `filedeleteall` command precede
+all `filemodify`, `filecopy` and `filerename` commands in the same
+commit, as `filedeleteall`
+wipes the branch clean (see below).
+
+The `LF` after the command is optional (it used to be required).
+
+`author`
+^^^^^^^^
+An `author` command may optionally appear, if the author information
+might differ from the committer information.  If `author` is omitted
+then fast-import will automatically use the committer's information for
+the author portion of the commit.  See below for a description of
+the fields in `author`, as they are identical to `committer`.
+
+`committer`
+^^^^^^^^^^^
+The `committer` command indicates who made this commit, and when
+they made it.
+
+Here `<name>` is the person's display name (for example
+``Com M Itter'') and `<email>` is the person's email address
+(``cm@example.com'').  `LT` and `GT` are the literal less-than (\x3c)
+and greater-than (\x3e) symbols.  These are required to delimit
+the email address from the other fields in the line.  Note that
+`<name>` is free-form and may contain any sequence of bytes, except
+`LT` and `LF`.  It is typically UTF-8 encoded.
+
+The time of the change is specified by `<when>` using the date format
+that was selected by the \--date-format=<fmt> command line option.
+See ``Date Formats'' above for the set of supported formats, and
+their syntax.
+
+`from`
+^^^^^^
+The `from` command is used to specify the commit to initialize
+this branch from.  This revision will be the first ancestor of the
+new commit.
+
+Omitting the `from` command in the first commit of a new branch
+will cause fast-import to create that commit with no ancestor. This
+tends to be desired only for the initial commit of a project.
+Omitting the `from` command on existing branches is usually desired,
+as the current commit on that branch is automatically assumed to
+be the first ancestor of the new commit.
+
+As `LF` is not valid in a Git refname or SHA-1 expression, no
+quoting or escaping syntax is supported within `<committish>`.
+
+Here `<committish>` is any of the following:
+
+* The name of an existing branch already in fast-import's internal branch
+  table.  If fast-import doesn't know the name, its treated as a SHA-1
+  expression.
+
+* A mark reference, `:<idnum>`, where `<idnum>` is the mark number.
++
+The reason fast-import uses `:` to denote a mark reference is this character
+is not legal in a Git branch name.  The leading `:` makes it easy
+to distinguish between the mark 42 (`:42`) and the branch 42 (`42`
+or `refs/heads/42`), or an abbreviated SHA-1 which happened to
+consist only of base-10 digits.
++
+Marks must be declared (via `mark`) before they can be used.
+
+* A complete 40 byte or abbreviated commit SHA-1 in hex.
+
+* Any valid Git SHA-1 expression that resolves to a commit.  See
+  ``SPECIFYING REVISIONS'' in gitlink:git-rev-parse[1] for details.
+
+The special case of restarting an incremental import from the
+current branch value should be written as:
+----
+       from refs/heads/branch^0
+----
+The `{caret}0` suffix is necessary as fast-import does not permit a branch to
+start from itself, and the branch is created in memory before the
+`from` command is even read from the input.  Adding `{caret}0` will force
+fast-import to resolve the commit through Git's revision parsing library,
+rather than its internal branch table, thereby loading in the
+existing value of the branch.
+
+`merge`
+^^^^^^^
+Includes one additional ancestor commit, and makes the current
+commit a merge commit.  An unlimited number of `merge` commands per
+commit are permitted by fast-import, thereby establishing an n-way merge.
+However Git's other tools never create commits with more than 15
+additional ancestors (forming a 16-way merge).  For this reason
+it is suggested that frontends do not use more than 15 `merge`
+commands per commit.
+
+Here `<committish>` is any of the commit specification expressions
+also accepted by `from` (see above).
+
+`filemodify`
+^^^^^^^^^^^^
+Included in a `commit` command to add a new file or change the
+content of an existing file.  This command has two different means
+of specifying the content of the file.
+
+External data format::
+       The data content for the file was already supplied by a prior
+       `blob` command.  The frontend just needs to connect it.
++
+....
+       'M' SP <mode> SP <dataref> SP <path> LF
+....
++
+Here `<dataref>` can be either a mark reference (`:<idnum>`)
+set by a prior `blob` command, or a full 40-byte SHA-1 of an
+existing Git blob object.
+
+Inline data format::
+       The data content for the file has not been supplied yet.
+       The frontend wants to supply it as part of this modify
+       command.
++
+....
+       'M' SP <mode> SP 'inline' SP <path> LF
+       data
+....
++
+See below for a detailed description of the `data` command.
+
+In both formats `<mode>` is the type of file entry, specified
+in octal.  Git only supports the following modes:
+
+* `100644` or `644`: A normal (not-executable) file.  The majority
+  of files in most projects use this mode.  If in doubt, this is
+  what you want.
+* `100755` or `755`: A normal, but executable, file.
+* `120000`: A symlink, the content of the file will be the link target.
+
+In both formats `<path>` is the complete path of the file to be added
+(if not already existing) or modified (if already existing).
+
+A `<path>` string must use UNIX-style directory separators (forward
+slash `/`), may contain any byte other than `LF`, and must not
+start with double quote (`"`).
+
+If an `LF` or double quote must be encoded into `<path>` shell-style
+quoting should be used, e.g. `"path/with\n and \" in it"`.
+
+The value of `<path>` must be in canonical form. That is it must not:
+
+* contain an empty directory component (e.g. `foo//bar` is invalid),
+* end with a directory separator (e.g. `foo/` is invalid),
+* start with a directory separator (e.g. `/foo` is invalid),
+* contain the special component `.` or `..` (e.g. `foo/./bar` and
+  `foo/../bar` are invalid).
+
+It is recommended that `<path>` always be encoded using UTF-8.
+
+`filedelete`
+^^^^^^^^^^^^
+Included in a `commit` command to remove a file or recursively
+delete an entire directory from the branch.  If the file or directory
+removal makes its parent directory empty, the parent directory will
+be automatically removed too.  This cascades up the tree until the
+first non-empty directory or the root is reached.
+
+....
+       'D' SP <path> LF
+....
+
+here `<path>` is the complete path of the file or subdirectory to
+be removed from the branch.
+See `filemodify` above for a detailed description of `<path>`.
+
+`filecopy`
+^^^^^^^^^^^^
+Recursively copies an existing file or subdirectory to a different
+location within the branch.  The existing file or directory must
+exist.  If the destination exists it will be completely replaced
+by the content copied from the source.
+
+....
+       'C' SP <path> SP <path> LF
+....
+
+here the first `<path>` is the source location and the second
+`<path>` is the destination.  See `filemodify` above for a detailed
+description of what `<path>` may look like.  To use a source path
+that contains SP the path must be quoted.
+
+A `filecopy` command takes effect immediately.  Once the source
+location has been copied to the destination any future commands
+applied to the source location will not impact the destination of
+the copy.
+
+`filerename`
+^^^^^^^^^^^^
+Renames an existing file or subdirectory to a different location
+within the branch.  The existing file or directory must exist. If
+the destination exists it will be replaced by the source directory.
+
+....
+       'R' SP <path> SP <path> LF
+....
+
+here the first `<path>` is the source location and the second
+`<path>` is the destination.  See `filemodify` above for a detailed
+description of what `<path>` may look like.  To use a source path
+that contains SP the path must be quoted.
+
+A `filerename` command takes effect immediately.  Once the source
+location has been renamed to the destination any future commands
+applied to the source location will create new files there and not
+impact the destination of the rename.
+
+Note that a `filerename` is the same as a `filecopy` followed by a
+`filedelete` of the source location.  There is a slight performance
+advantage to using `filerename`, but the advantage is so small
+that it is never worth trying to convert a delete/add pair in
+source material into a rename for fast-import.  This `filerename`
+command is provided just to simplify frontends that already have
+rename information and don't want bother with decomposing it into a
+`filecopy` followed by a `filedelete`.
+
+`filedeleteall`
+^^^^^^^^^^^^^^^
+Included in a `commit` command to remove all files (and also all
+directories) from the branch.  This command resets the internal
+branch structure to have no files in it, allowing the frontend
+to subsequently add all interesting files from scratch.
+
+....
+       'deleteall' LF
+....
+
+This command is extremely useful if the frontend does not know
+(or does not care to know) what files are currently on the branch,
+and therefore cannot generate the proper `filedelete` commands to
+update the content.
+
+Issuing a `filedeleteall` followed by the needed `filemodify`
+commands to set the correct content will produce the same results
+as sending only the needed `filemodify` and `filedelete` commands.
+The `filedeleteall` approach may however require fast-import to use slightly
+more memory per active branch (less than 1 MiB for even most large
+projects); so frontends that can easily obtain only the affected
+paths for a commit are encouraged to do so.
+
+`mark`
+~~~~~~
+Arranges for fast-import to save a reference to the current object, allowing
+the frontend to recall this object at a future point in time, without
+knowing its SHA-1.  Here the current object is the object creation
+command the `mark` command appears within.  This can be `commit`,
+`tag`, and `blob`, but `commit` is the most common usage.
+
+....
+       'mark' SP ':' <idnum> LF
+....
+
+where `<idnum>` is the number assigned by the frontend to this mark.
+The value of `<idnum>` is expressed as an ASCII decimal integer.
+The value 0 is reserved and cannot be used as
+a mark.  Only values greater than or equal to 1 may be used as marks.
+
+New marks are created automatically.  Existing marks can be moved
+to another object simply by reusing the same `<idnum>` in another
+`mark` command.
+
+`tag`
+~~~~~
+Creates an annotated tag referring to a specific commit.  To create
+lightweight (non-annotated) tags see the `reset` command below.
+
+....
+       'tag' SP <name> LF
+       'from' SP <committish> LF
+       'tagger' SP <name> SP LT <email> GT SP <when> LF
+       data
+....
+
+where `<name>` is the name of the tag to create.
+
+Tag names are automatically prefixed with `refs/tags/` when stored
+in Git, so importing the CVS branch symbol `RELENG-1_0-FINAL` would
+use just `RELENG-1_0-FINAL` for `<name>`, and fast-import will write the
+corresponding ref as `refs/tags/RELENG-1_0-FINAL`.
+
+The value of `<name>` must be a valid refname in Git and therefore
+may contain forward slashes.  As `LF` is not valid in a Git refname,
+no quoting or escaping syntax is supported here.
+
+The `from` command is the same as in the `commit` command; see
+above for details.
+
+The `tagger` command uses the same format as `committer` within
+`commit`; again see above for details.
+
+The `data` command following `tagger` must supply the annotated tag
+message (see below for `data` command syntax).  To import an empty
+tag message use a 0 length data.  Tag messages are free-form and are
+not interpreted by Git.  Currently they must be encoded in UTF-8,
+as fast-import does not permit other encodings to be specified.
+
+Signing annotated tags during import from within fast-import is not
+supported.  Trying to include your own PGP/GPG signature is not
+recommended, as the frontend does not (easily) have access to the
+complete set of bytes which normally goes into such a signature.
+If signing is required, create lightweight tags from within fast-import with
+`reset`, then create the annotated versions of those tags offline
+with the standard gitlink:git-tag[1] process.
+
+`reset`
+~~~~~~~
+Creates (or recreates) the named branch, optionally starting from
+a specific revision.  The reset command allows a frontend to issue
+a new `from` command for an existing branch, or to create a new
+branch from an existing commit without creating a new commit.
+
+....
+       'reset' SP <ref> LF
+       ('from' SP <committish> LF)?
+       LF?
+....
+
+For a detailed description of `<ref>` and `<committish>` see above
+under `commit` and `from`.
+
+The `LF` after the command is optional (it used to be required).
+
+The `reset` command can also be used to create lightweight
+(non-annotated) tags.  For example:
+
+====
+       reset refs/tags/938
+       from :938
+====
+
+would create the lightweight tag `refs/tags/938` referring to
+whatever commit mark `:938` references.
+
+`blob`
+~~~~~~
+Requests writing one file revision to the packfile.  The revision
+is not connected to any commit; this connection must be formed in
+a subsequent `commit` command by referencing the blob through an
+assigned mark.
+
+....
+       'blob' LF
+       mark?
+       data
+....
+
+The mark command is optional here as some frontends have chosen
+to generate the Git SHA-1 for the blob on their own, and feed that
+directly to `commit`.  This is typically more work than its worth
+however, as marks are inexpensive to store and easy to use.
+
+`data`
+~~~~~~
+Supplies raw data (for use as blob/file content, commit messages, or
+annotated tag messages) to fast-import.  Data can be supplied using an exact
+byte count or delimited with a terminating line.  Real frontends
+intended for production-quality conversions should always use the
+exact byte count format, as it is more robust and performs better.
+The delimited format is intended primarily for testing fast-import.
+
+Comment lines appearing within the `<raw>` part of `data` commands
+are always taken to be part of the body of the data and are therefore
+never ignored by fast-import.  This makes it safe to import any
+file/message content whose lines might start with `#`.
+
+Exact byte count format::
+       The frontend must specify the number of bytes of data.
++
+....
+       'data' SP <count> LF
+       <raw> LF?
+....
++
+where `<count>` is the exact number of bytes appearing within
+`<raw>`.  The value of `<count>` is expressed as an ASCII decimal
+integer.  The `LF` on either side of `<raw>` is not
+included in `<count>` and will not be included in the imported data.
++
+The `LF` after `<raw>` is optional (it used to be required) but
+recommended.  Always including it makes debugging a fast-import
+stream easier as the next command always starts in column 0
+of the next line, even if `<raw>` did not end with an `LF`.
+
+Delimited format::
+       A delimiter string is used to mark the end of the data.
+       fast-import will compute the length by searching for the delimiter.
+       This format is primarily useful for testing and is not
+       recommended for real data.
++
+....
+       'data' SP '<<' <delim> LF
+       <raw> LF
+       <delim> LF
+       LF?
+....
++
+where `<delim>` is the chosen delimiter string.  The string `<delim>`
+must not appear on a line by itself within `<raw>`, as otherwise
+fast-import will think the data ends earlier than it really does.  The `LF`
+immediately trailing `<raw>` is part of `<raw>`.  This is one of
+the limitations of the delimited format, it is impossible to supply
+a data chunk which does not have an LF as its last byte.
++
+The `LF` after `<delim> LF` is optional (it used to be required).
+
+`checkpoint`
+~~~~~~~~~~~~
+Forces fast-import to close the current packfile, start a new one, and to
+save out all current branch refs, tags and marks.
+
+....
+       'checkpoint' LF
+       LF?
+....
+
+Note that fast-import automatically switches packfiles when the current
+packfile reaches \--max-pack-size, or 4 GiB, whichever limit is
+smaller.  During an automatic packfile switch fast-import does not update
+the branch refs, tags or marks.
+
+As a `checkpoint` can require a significant amount of CPU time and
+disk IO (to compute the overall pack SHA-1 checksum, generate the
+corresponding index file, and update the refs) it can easily take
+several minutes for a single `checkpoint` command to complete.
+
+Frontends may choose to issue checkpoints during extremely large
+and long running imports, or when they need to allow another Git
+process access to a branch.  However given that a 30 GiB Subversion
+repository can be loaded into Git through fast-import in about 3 hours,
+explicit checkpointing may not be necessary.
+
+The `LF` after the command is optional (it used to be required).
+
+`progress`
+~~~~~~~~~~
+Causes fast-import to print the entire `progress` line unmodified to
+its standard output channel (file descriptor 1) when the command is
+processed from the input stream.  The command otherwise has no impact
+on the current import, or on any of fast-import's internal state.
+
+....
+       'progress' SP <any> LF
+       LF?
+....
+
+The `<any>` part of the command may contain any sequence of bytes
+that does not contain `LF`.  The `LF` after the command is optional.
+Callers may wish to process the output through a tool such as sed to
+remove the leading part of the line, for example:
+
+====
+       frontend | git-fast-import | sed 's/^progress //'
+====
+
+Placing a `progress` command immediately after a `checkpoint` will
+inform the reader when the `checkpoint` has been completed and it
+can safely access the refs that fast-import updated.
+
+Tips and Tricks
+---------------
+The following tips and tricks have been collected from various
+users of fast-import, and are offered here as suggestions.
+
+Use One Mark Per Commit
+~~~~~~~~~~~~~~~~~~~~~~~
+When doing a repository conversion, use a unique mark per commit
+(`mark :<n>`) and supply the \--export-marks option on the command
+line.  fast-import will dump a file which lists every mark and the Git
+object SHA-1 that corresponds to it.  If the frontend can tie
+the marks back to the source repository, it is easy to verify the
+accuracy and completeness of the import by comparing each Git
+commit to the corresponding source revision.
+
+Coming from a system such as Perforce or Subversion this should be
+quite simple, as the fast-import mark can also be the Perforce changeset
+number or the Subversion revision number.
+
+Freely Skip Around Branches
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Don't bother trying to optimize the frontend to stick to one branch
+at a time during an import.  Although doing so might be slightly
+faster for fast-import, it tends to increase the complexity of the frontend
+code considerably.
+
+The branch LRU builtin to fast-import tends to behave very well, and the
+cost of activating an inactive branch is so low that bouncing around
+between branches has virtually no impact on import performance.
+
+Handling Renames
+~~~~~~~~~~~~~~~~
+When importing a renamed file or directory, simply delete the old
+name(s) and modify the new name(s) during the corresponding commit.
+Git performs rename detection after-the-fact, rather than explicitly
+during a commit.
+
+Use Tag Fixup Branches
+~~~~~~~~~~~~~~~~~~~~~~
+Some other SCM systems let the user create a tag from multiple
+files which are not from the same commit/changeset.  Or to create
+tags which are a subset of the files available in the repository.
+
+Importing these tags as-is in Git is impossible without making at
+least one commit which ``fixes up'' the files to match the content
+of the tag.  Use fast-import's `reset` command to reset a dummy branch
+outside of your normal branch space to the base commit for the tag,
+then commit one or more file fixup commits, and finally tag the
+dummy branch.
+
+For example since all normal branches are stored under `refs/heads/`
+name the tag fixup branch `TAG_FIXUP`.  This way it is impossible for
+the fixup branch used by the importer to have namespace conflicts
+with real branches imported from the source (the name `TAG_FIXUP`
+is not `refs/heads/TAG_FIXUP`).
+
+When committing fixups, consider using `merge` to connect the
+commit(s) which are supplying file revisions to the fixup branch.
+Doing so will allow tools such as gitlink:git-blame[1] to track
+through the real commit history and properly annotate the source
+files.
+
+After fast-import terminates the frontend will need to do `rm .git/TAG_FIXUP`
+to remove the dummy branch.
+
+Import Now, Repack Later
+~~~~~~~~~~~~~~~~~~~~~~~~
+As soon as fast-import completes the Git repository is completely valid
+and ready for use.  Typically this takes only a very short time,
+even for considerably large projects (100,000+ commits).
+
+However repacking the repository is necessary to improve data
+locality and access performance.  It can also take hours on extremely
+large projects (especially if -f and a large \--window parameter is
+used).  Since repacking is safe to run alongside readers and writers,
+run the repack in the background and let it finish when it finishes.
+There is no reason to wait to explore your new Git project!
+
+If you choose to wait for the repack, don't try to run benchmarks
+or performance tests until repacking is completed.  fast-import outputs
+suboptimal packfiles that are simply never seen in real use
+situations.
+
+Repacking Historical Data
+~~~~~~~~~~~~~~~~~~~~~~~~~
+If you are repacking very old imported data (e.g. older than the
+last year), consider expending some extra CPU time and supplying
+\--window=50 (or higher) when you run gitlink:git-repack[1].
+This will take longer, but will also produce a smaller packfile.
+You only need to expend the effort once, and everyone using your
+project will benefit from the smaller repository.
+
+Include Some Progress Messages
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Every once in a while have your frontend emit a `progress` message
+to fast-import.  The contents of the messages are entirely free-form,
+so one suggestion would be to output the current month and year
+each time the current commit date moves into the next month.
+Your users will feel better knowing how much of the data stream
+has been processed.
+
+
+Packfile Optimization
+---------------------
+When packing a blob fast-import always attempts to deltify against the last
+blob written.  Unless specifically arranged for by the frontend,
+this will probably not be a prior version of the same file, so the
+generated delta will not be the smallest possible.  The resulting
+packfile will be compressed, but will not be optimal.
+
+Frontends which have efficient access to all revisions of a
+single file (for example reading an RCS/CVS ,v file) can choose
+to supply all revisions of that file as a sequence of consecutive
+`blob` commands.  This allows fast-import to deltify the different file
+revisions against each other, saving space in the final packfile.
+Marks can be used to later identify individual file revisions during
+a sequence of `commit` commands.
+
+The packfile(s) created by fast-import do not encourage good disk access
+patterns.  This is caused by fast-import writing the data in the order
+it is received on standard input, while Git typically organizes
+data within packfiles to make the most recent (current tip) data
+appear before historical data.  Git also clusters commits together,
+speeding up revision traversal through better cache locality.
+
+For this reason it is strongly recommended that users repack the
+repository with `git repack -a -d` after fast-import completes, allowing
+Git to reorganize the packfiles for faster data access.  If blob
+deltas are suboptimal (see above) then also adding the `-f` option
+to force recomputation of all deltas can significantly reduce the
+final packfile size (30-50% smaller can be quite typical).
+
+
+Memory Utilization
+------------------
+There are a number of factors which affect how much memory fast-import
+requires to perform an import.  Like critical sections of core
+Git, fast-import uses its own memory allocators to amortize any overheads
+associated with malloc.  In practice fast-import tends to amortize any
+malloc overheads to 0, due to its use of large block allocations.
+
+per object
+~~~~~~~~~~
+fast-import maintains an in-memory structure for every object written in
+this execution.  On a 32 bit system the structure is 32 bytes,
+on a 64 bit system the structure is 40 bytes (due to the larger
+pointer sizes).  Objects in the table are not deallocated until
+fast-import terminates.  Importing 2 million objects on a 32 bit system
+will require approximately 64 MiB of memory.
+
+The object table is actually a hashtable keyed on the object name
+(the unique SHA-1).  This storage configuration allows fast-import to reuse
+an existing or already written object and avoid writing duplicates
+to the output packfile.  Duplicate blobs are surprisingly common
+in an import, typically due to branch merges in the source.
+
+per mark
+~~~~~~~~
+Marks are stored in a sparse array, using 1 pointer (4 bytes or 8
+bytes, depending on pointer size) per mark.  Although the array
+is sparse, frontends are still strongly encouraged to use marks
+between 1 and n, where n is the total number of marks required for
+this import.
+
+per branch
+~~~~~~~~~~
+Branches are classified as active and inactive.  The memory usage
+of the two classes is significantly different.
+
+Inactive branches are stored in a structure which uses 96 or 120
+bytes (32 bit or 64 bit systems, respectively), plus the length of
+the branch name (typically under 200 bytes), per branch.  fast-import will
+easily handle as many as 10,000 inactive branches in under 2 MiB
+of memory.
+
+Active branches have the same overhead as inactive branches, but
+also contain copies of every tree that has been recently modified on
+that branch.  If subtree `include` has not been modified since the
+branch became active, its contents will not be loaded into memory,
+but if subtree `src` has been modified by a commit since the branch
+became active, then its contents will be loaded in memory.
+
+As active branches store metadata about the files contained on that
+branch, their in-memory storage size can grow to a considerable size
+(see below).
+
+fast-import automatically moves active branches to inactive status based on
+a simple least-recently-used algorithm.  The LRU chain is updated on
+each `commit` command.  The maximum number of active branches can be
+increased or decreased on the command line with \--active-branches=.
+
+per active tree
+~~~~~~~~~~~~~~~
+Trees (aka directories) use just 12 bytes of memory on top of the
+memory required for their entries (see ``per active file'' below).
+The cost of a tree is virtually 0, as its overhead amortizes out
+over the individual file entries.
+
+per active file entry
+~~~~~~~~~~~~~~~~~~~~~
+Files (and pointers to subtrees) within active trees require 52 or 64
+bytes (32/64 bit platforms) per entry.  To conserve space, file and
+tree names are pooled in a common string table, allowing the filename
+``Makefile'' to use just 16 bytes (after including the string header
+overhead) no matter how many times it occurs within the project.
+
+The active branch LRU, when coupled with the filename string pool
+and lazy loading of subtrees, allows fast-import to efficiently import
+projects with 2,000+ branches and 45,114+ files in a very limited
+memory footprint (less than 2.7 MiB per active branch).
+
+
+Author
+------
+Written by Shawn O. Pearce <spearce@spearce.org>.
+
+Documentation
+--------------
+Documentation by Shawn O. Pearce <spearce@spearce.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-fetch-pack.txt b/Documentation/git-fetch-pack.txt
new file mode 100644 (file)
index 0000000..a99a5b3
--- /dev/null
@@ -0,0 +1,96 @@
+git-fetch-pack(1)
+=================
+
+NAME
+----
+git-fetch-pack - Receive missing objects from another repository
+
+
+SYNOPSIS
+--------
+'git-fetch-pack' [--all] [--quiet|-q] [--keep|-k] [--thin] [--upload-pack=<git-upload-pack>] [--depth=<n>] [--no-progress] [-v] [<host>:]<directory> [<refs>...]
+
+DESCRIPTION
+-----------
+Usually you would want to use gitlink:git-fetch[1] which is a
+higher level wrapper of this command instead.
+
+Invokes 'git-upload-pack' on a potentially remote repository,
+and asks it to send objects missing from this repository, to
+update the named heads.  The list of commits available locally
+is found out by scanning local $GIT_DIR/refs/ and sent to
+'git-upload-pack' running on the other end.
+
+This command degenerates to download everything to complete the
+asked refs from the remote side when the local side does not
+have a common ancestor commit.
+
+
+OPTIONS
+-------
+\--all::
+       Fetch all remote refs.
+
+\--quiet, \-q::
+       Pass '-q' flag to 'git-unpack-objects'; this makes the
+       cloning process less verbose.
+
+\--keep, \-k::
+       Do not invoke 'git-unpack-objects' on received data, but
+       create a single packfile out of it instead, and store it
+       in the object database. If provided twice then the pack is
+       locked against repacking.
+
+\--thin::
+       Spend extra cycles to minimize the number of objects to be sent.
+       Use it on slower connection.
+
+\--upload-pack=<git-upload-pack>::
+       Use this to specify the path to 'git-upload-pack' on the
+       remote side, if is not found on your $PATH.
+       Installations of sshd ignores the user's environment
+       setup scripts for login shells (e.g. .bash_profile) and
+       your privately installed git may not be found on the system
+       default $PATH.  Another workaround suggested is to set
+       up your $PATH in ".bashrc", but this flag is for people
+       who do not want to pay the overhead for non-interactive
+       shells by having a lean .bashrc file (they set most of
+       the things up in .bash_profile).
+
+\--exec=<git-upload-pack>::
+       Same as \--upload-pack=<git-upload-pack>.
+
+\--depth=<n>::
+       Limit fetching to ancestor-chains not longer than n.
+
+\--no-progress::
+       Do not show the progress.
+
+\-v::
+       Run verbosely.
+
+<host>::
+       A remote host that houses the repository.  When this
+       part is specified, 'git-upload-pack' is invoked via
+       ssh.
+
+<directory>::
+       The repository to sync from.
+
+<refs>...::
+       The remote heads to update from. This is relative to
+       $GIT_DIR (e.g. "HEAD", "refs/heads/master").  When
+       unspecified, update from all heads the remote side has.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt
new file mode 100644 (file)
index 0000000..9003473
--- /dev/null
@@ -0,0 +1,56 @@
+git-fetch(1)
+============
+
+NAME
+----
+git-fetch - Download objects and refs from another repository
+
+
+SYNOPSIS
+--------
+'git-fetch' <options> <repository> <refspec>...
+
+
+DESCRIPTION
+-----------
+Fetches named heads or tags from another repository, along with
+the objects necessary to complete them.
+
+The ref names and their object names of fetched refs are stored
+in `.git/FETCH_HEAD`.  This information is left for a later merge
+operation done by "git merge".
+
+When <refspec> stores the fetched result in tracking branches,
+the tags that point at these branches are automatically
+followed.  This is done by first fetching from the remote using
+the given <refspec>s, and if the repository has objects that are
+pointed by remote tags that it does not yet have, then fetch
+those missing tags.  If the other end has tags that point at
+branches you are not interested in, you will not get them.
+
+
+OPTIONS
+-------
+include::fetch-options.txt[]
+
+include::pull-fetch-param.txt[]
+
+include::urls-remotes.txt[]
+
+SEE ALSO
+--------
+gitlink:git-pull[1]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org> and
+Junio C Hamano <junkio@cox.net>
+
+Documentation
+-------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-filter-branch.txt b/Documentation/git-filter-branch.txt
new file mode 100644 (file)
index 0000000..895d750
--- /dev/null
@@ -0,0 +1,298 @@
+git-filter-branch(1)
+====================
+
+NAME
+----
+git-filter-branch - Rewrite branches
+
+SYNOPSIS
+--------
+[verse]
+'git-filter-branch' [--env-filter <command>] [--tree-filter <command>]
+       [--index-filter <command>] [--parent-filter <command>]
+       [--msg-filter <command>] [--commit-filter <command>]
+       [--tag-name-filter <command>] [--subdirectory-filter <directory>]
+       [--original <namespace>] [-d <directory>] [-f | --force]
+       [<rev-list options>...]
+
+DESCRIPTION
+-----------
+Lets you rewrite git revision history by rewriting the branches mentioned
+in the <rev-list options>, applying custom filters on each revision.
+Those filters can modify each tree (e.g. removing a file or running
+a perl rewrite on all files) or information about each commit.
+Otherwise, all information (including original commit times or merge
+information) will be preserved.
+
+The command will only rewrite the _positive_ refs mentioned in the
+command line (i.e. if you pass 'a..b', only 'b' will be rewritten).
+If you specify no filters, the commits will be recommitted without any
+changes, which would normally have no effect.  Nevertheless, this may be
+useful in the future for compensating for some git bugs or such,
+therefore such a usage is permitted.
+
+*WARNING*! The rewritten history will have different object names for all
+the objects and will not converge with the original branch.  You will not
+be able to easily push and distribute the rewritten branch on top of the
+original branch.  Please do not use this command if you do not know the
+full implications, and avoid using it anyway, if a simple single commit
+would suffice to fix your problem.
+
+Always verify that the rewritten version is correct: The original refs,
+if different from the rewritten ones, will be stored in the namespace
+'refs/original/'.
+
+Note that since this operation is extensively I/O expensive, it might
+be a good idea to redirect the temporary directory off-disk with the
+'-d' option, e.g. on tmpfs.  Reportedly the speedup is very noticeable.
+
+
+Filters
+~~~~~~~
+
+The filters are applied in the order as listed below.  The <command>
+argument is always evaluated in shell using the 'eval' command (with the
+notable exception of the commit filter, for technical reasons).
+Prior to that, the $GIT_COMMIT environment variable will be set to contain
+the id of the commit being rewritten.  Also, GIT_AUTHOR_NAME,
+GIT_AUTHOR_EMAIL, GIT_AUTHOR_DATE, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL,
+and GIT_COMMITTER_DATE are set according to the current commit.
+
+A 'map' function is available that takes an "original sha1 id" argument
+and outputs a "rewritten sha1 id" if the commit has been already
+rewritten, and "original sha1 id" otherwise; the 'map' function can
+return several ids on separate lines if your commit filter emitted
+multiple commits.
+
+
+OPTIONS
+-------
+
+--env-filter <command>::
+       This is the filter for modifying the environment in which
+       the commit will be performed.  Specifically, you might want
+       to rewrite the author/committer name/email/time environment
+       variables (see gitlink:git-commit[1] for details).  Do not forget
+       to re-export the variables.
+
+--tree-filter <command>::
+       This is the filter for rewriting the tree and its contents.
+       The argument is evaluated in shell with the working
+       directory set to the root of the checked out tree.  The new tree
+       is then used as-is (new files are auto-added, disappeared files
+       are auto-removed - neither .gitignore files nor any other ignore
+       rules *HAVE ANY EFFECT*!).
+
+--index-filter <command>::
+       This is the filter for rewriting the index.  It is similar to the
+       tree filter but does not check out the tree, which makes it much
+       faster.  For hairy cases, see gitlink:git-update-index[1].
+
+--parent-filter <command>::
+       This is the filter for rewriting the commit's parent list.
+       It will receive the parent string on stdin and shall output
+       the new parent string on stdout.  The parent string is in
+       a format accepted by gitlink:git-commit-tree[1]: empty for
+       the initial commit, "-p parent" for a normal commit and
+       "-p parent1 -p parent2 -p parent3 ..." for a merge commit.
+
+--msg-filter <command>::
+       This is the filter for rewriting the commit messages.
+       The argument is evaluated in the shell with the original
+       commit message on standard input; its standard output is
+       used as the new commit message.
+
+--commit-filter <command>::
+       This is the filter for performing the commit.
+       If this filter is specified, it will be called instead of the
+       gitlink:git-commit-tree[1] command, with arguments of the form
+       "<TREE_ID> [-p <PARENT_COMMIT_ID>]..." and the log message on
+       stdin.  The commit id is expected on stdout.
++
+As a special extension, the commit filter may emit multiple
+commit ids; in that case, ancestors of the original commit will
+have all of them as parents.
++
+You can use the 'map' convenience function in this filter, and other
+convenience functions, too.  For example, calling 'skip_commit "$@"'
+will leave out the current commit (but not its changes! If you want
+that, use gitlink:git-rebase[1] instead).
+
+--tag-name-filter <command>::
+       This is the filter for rewriting tag names. When passed,
+       it will be called for every tag ref that points to a rewritten
+       object (or to a tag object which points to a rewritten object).
+       The original tag name is passed via standard input, and the new
+       tag name is expected on standard output.
++
+The original tags are not deleted, but can be overwritten;
+use "--tag-name-filter cat" to simply update the tags.  In this
+case, be very careful and make sure you have the old tags
+backed up in case the conversion has run afoul.
++
+Note that there is currently no support for proper rewriting of
+tag objects; in layman terms, if the tag has a message or signature
+attached, the rewritten tag won't have it.  Sorry.  (It is by
+definition impossible to preserve signatures at any rate.)
+
+--subdirectory-filter <directory>::
+       Only look at the history which touches the given subdirectory.
+       The result will contain that directory (and only that) as its
+       project root.
+
+--original <namespace>::
+       Use this option to set the namespace where the original commits
+       will be stored. The default value is 'refs/original'.
+
+-d <directory>::
+       Use this option to set the path to the temporary directory used for
+       rewriting.  When applying a tree filter, the command needs to
+       temporary checkout the tree to some directory, which may consume
+       considerable space in case of large projects.  By default it
+       does this in the '.git-rewrite/' directory but you can override
+       that choice by this parameter.
+
+-f|--force::
+       `git filter-branch` refuses to start with an existing temporary
+       directory or when there are already refs starting with
+       'refs/original/', unless forced.
+
+<rev-list-options>::
+       When options are given after the new branch name, they will
+       be passed to gitlink:git-rev-list[1].  Only commits in the resulting
+       output will be filtered, although the filtered commits can still
+       reference parents which are outside of that set.
+
+
+Examples
+--------
+
+Suppose you want to remove a file (containing confidential information
+or copyright violation) from all commits:
+
+-------------------------------------------------------
+git filter-branch --tree-filter 'rm filename' HEAD
+-------------------------------------------------------
+
+A significantly faster version:
+
+--------------------------------------------------------------------------
+git filter-branch --index-filter 'git update-index --remove filename' HEAD
+--------------------------------------------------------------------------
+
+Now, you will get the rewritten history saved in HEAD.
+
+To set a commit (which typically is at the tip of another
+history) to be the parent of the current initial commit, in
+order to paste the other history behind the current history:
+
+-------------------------------------------------------------------
+git filter-branch --parent-filter 'sed "s/^\$/-p <graft-id>/"' HEAD
+-------------------------------------------------------------------
+
+(if the parent string is empty - which happens when we are dealing with
+the initial commit - add graftcommit as a parent).  Note that this assumes
+history with a single root (that is, no merge without common ancestors
+happened).  If this is not the case, use:
+
+--------------------------------------------------------------------------
+git filter-branch --parent-filter \
+       'cat; test $GIT_COMMIT = <commit-id> && echo "-p <graft-id>"' HEAD
+--------------------------------------------------------------------------
+
+or even simpler:
+
+-----------------------------------------------
+echo "$commit-id $graft-id" >> .git/info/grafts
+git filter-branch $graft-id..HEAD
+-----------------------------------------------
+
+To remove commits authored by "Darl McBribe" from the history:
+
+------------------------------------------------------------------------------
+git filter-branch --commit-filter '
+       if [ "$GIT_AUTHOR_NAME" = "Darl McBribe" ];
+       then
+               skip_commit "$@";
+       else
+               git commit-tree "$@";
+       fi' HEAD
+------------------------------------------------------------------------------
+
+The function 'skip_commit' is defined as follows:
+
+--------------------------
+skip_commit()
+{
+       shift;
+       while [ -n "$1" ];
+       do
+               shift;
+               map "$1";
+               shift;
+       done;
+}
+--------------------------
+
+The shift magic first throws away the tree id and then the -p
+parameters.  Note that this handles merges properly! In case Darl
+committed a merge between P1 and P2, it will be propagated properly
+and all children of the merge will become merge commits with P1,P2
+as their parents instead of the merge commit.
+
+
+To restrict rewriting to only part of the history, specify a revision
+range in addition to the new branch name.  The new branch name will
+point to the top-most revision that a 'git rev-list' of this range
+will print.
+
+*NOTE* the changes introduced by the commits, and which are not reverted
+by subsequent commits, will still be in the rewritten branch. If you want
+to throw out _changes_ together with the commits, you should use the
+interactive mode of gitlink:git-rebase[1].
+
+
+Consider this history:
+
+------------------
+     D--E--F--G--H
+    /     /
+A--B-----C
+------------------
+
+To rewrite only commits D,E,F,G,H, but leave A, B and C alone, use:
+
+--------------------------------
+git filter-branch ... C..H
+--------------------------------
+
+To rewrite commits E,F,G,H, use one of these:
+
+----------------------------------------
+git filter-branch ... C..H --not D
+git filter-branch ... D..H --not C
+----------------------------------------
+
+To move the whole tree into a subdirectory, or remove it from there:
+
+---------------------------------------------------------------
+git filter-branch --index-filter \
+       'git ls-files -s | sed "s-\t-&newsubdir/-" |
+               GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
+                       git update-index --index-info &&
+        mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE' HEAD
+---------------------------------------------------------------
+
+
+Author
+------
+Written by Petr "Pasky" Baudis <pasky@suse.cz>,
+and the git list <git@vger.kernel.org>
+
+Documentation
+--------------
+Documentation by Petr Baudis and the git list.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-fmt-merge-msg.txt b/Documentation/git-fmt-merge-msg.txt
new file mode 100644 (file)
index 0000000..7088ed4
--- /dev/null
@@ -0,0 +1,62 @@
+git-fmt-merge-msg(1)
+====================
+
+NAME
+----
+git-fmt-merge-msg - Produce a merge commit message
+
+
+SYNOPSIS
+--------
+[verse]
+git-fmt-merge-msg [--summary | --no-summary] <$GIT_DIR/FETCH_HEAD
+git-fmt-merge-msg [--summary | --no-summary] -F <file>
+
+DESCRIPTION
+-----------
+Takes the list of merged objects on stdin and produces a suitable
+commit message to be used for the merge commit, usually to be
+passed as the '<merge-message>' argument of `git-merge`.
+
+This script is intended mostly for internal use by scripts
+automatically invoking `git-merge`.
+
+OPTIONS
+-------
+
+--summary::
+       In addition to branch names, populate the log message with
+       one-line descriptions from the actual commits that are being
+       merged.
+
+--no-summary::
+       Do not list one-line descriptions from the actual commits being
+       merged.
+
+--file <file>, -F <file>::
+       Take the list of merged objects from <file> instead of
+       stdin.
+
+CONFIGURATION
+-------------
+
+merge.summary::
+       Whether to include summaries of merged commits in newly
+       merge commit messages. False by default.
+
+SEE ALSO
+--------
+gitlink:git-merge[1]
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Petr Baudis, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt
new file mode 100644 (file)
index 0000000..f1f90cc
--- /dev/null
@@ -0,0 +1,193 @@
+git-for-each-ref(1)
+===================
+
+NAME
+----
+git-for-each-ref - Output information on each ref
+
+SYNOPSIS
+--------
+[verse]
+'git-for-each-ref' [--count=<count>]\*
+                   [--shell|--perl|--python|--tcl]
+                   [--sort=<key>]\* [--format=<format>] [<pattern>]
+
+DESCRIPTION
+-----------
+
+Iterate over all refs that match `<pattern>` and show them
+according to the given `<format>`, after sorting them according
+to the given set of `<key>`.  If `<max>` is given, stop after
+showing that many refs.  The interpolated values in `<format>`
+can optionally be quoted as string literals in the specified
+host language allowing their direct evaluation in that language.
+
+OPTIONS
+-------
+<count>::
+       By default the command shows all refs that match
+       `<pattern>`.  This option makes it stop after showing
+       that many refs.
+
+<key>::
+       A field name to sort on.  Prefix `-` to sort in
+       descending order of the value.  When unspecified,
+       `refname` is used.  More than one sort keys can be
+       given.
+
+<format>::
+       A string that interpolates `%(fieldname)` from the
+       object pointed at by a ref being shown.  If `fieldname`
+       is prefixed with an asterisk (`*`) and the ref points
+       at a tag object, the value for the field in the object
+       tag refers is used.  When unspecified, defaults to
+       `%(objectname) SPC %(objecttype) TAB %(refname)`.
+       It also interpolates `%%` to `%`, and `%xx` where `xx`
+       are hex digits interpolates to character with hex code
+       `xx`; for example `%00` interpolates to `\0` (NUL),
+       `%09` to `\t` (TAB) and `%0a` to `\n` (LF).
+
+<pattern>::
+       If given, the name of the ref is matched against this
+       using fnmatch(3).  Refs that do not match the pattern
+       are not shown.
+
+--shell, --perl, --python, --tcl::
+       If given, strings that substitute `%(fieldname)`
+       placeholders are quoted as string literals suitable for
+       the specified host language.  This is meant to produce
+       a scriptlet that can directly be `eval`ed.
+
+
+FIELD NAMES
+-----------
+
+Various values from structured fields in referenced objects can
+be used to interpolate into the resulting output, or as sort
+keys.
+
+For all objects, the following names can be used:
+
+refname::
+       The name of the ref (the part after $GIT_DIR/).
+
+objecttype::
+       The type of the object (`blob`, `tree`, `commit`, `tag`).
+
+objectsize::
+       The size of the object (the same as `git-cat-file -s` reports).
+
+objectname::
+       The object name (aka SHA-1).
+
+In addition to the above, for commit and tag objects, the header
+field names (`tree`, `parent`, `object`, `type`, and `tag`) can
+be used to specify the value in the header field.
+
+Fields that have name-email-date tuple as its value (`author`,
+`committer`, and `tagger`) can be suffixed with `name`, `email`,
+and `date` to extract the named component.
+
+The first line of the message in a commit and tag object is
+`subject`, the remaining lines are `body`.  The whole message
+is `contents`.
+
+For sorting purposes, fields with numeric values sort in numeric
+order (`objectsize`, `authordate`, `committerdate`, `taggerdate`).
+All other fields are used to sort in their byte-value order.
+
+In any case, a field name that refers to a field inapplicable to
+the object referred by the ref does not cause an error.  It
+returns an empty string instead.
+
+As a special case for the date-type fields, you may specify a format for
+the date by adding one of `:default`, `:relative`, `:short`, `:local`,
+`:iso8601` or `:rfc2822` to the end of the fieldname; e.g.
+`%(taggerdate:relative)`.
+
+
+EXAMPLES
+--------
+
+An example directly producing formatted text.  Show the most recent
+3 tagged commits::
+
+------------
+#!/bin/sh
+
+git-for-each-ref --count=3 --sort='-*authordate' \
+--format='From: %(*authorname) %(*authoremail)
+Subject: %(*subject)
+Date: %(*authordate)
+Ref: %(*refname)
+
+%(*body)
+' 'refs/tags'
+------------
+
+
+A simple example showing the use of shell eval on the output,
+demonstrating the use of --shell.  List the prefixes of all heads::
+------------
+#!/bin/sh
+
+git-for-each-ref --shell --format="ref=%(refname)" refs/heads | \
+while read entry
+do
+       eval "$entry"
+       echo `dirname $ref`
+done
+------------
+
+
+A bit more elaborate report on tags, demonstrating that the format
+may be an entire script::
+------------
+#!/bin/sh
+
+fmt='
+       r=%(refname)
+       t=%(*objecttype)
+       T=${r#refs/tags/}
+
+       o=%(*objectname)
+       n=%(*authorname)
+       e=%(*authoremail)
+       s=%(*subject)
+       d=%(*authordate)
+       b=%(*body)
+
+       kind=Tag
+       if test "z$t" = z
+       then
+               # could be a lightweight tag
+               t=%(objecttype)
+               kind="Lightweight tag"
+               o=%(objectname)
+               n=%(authorname)
+               e=%(authoremail)
+               s=%(subject)
+               d=%(authordate)
+               b=%(body)
+       fi
+       echo "$kind $T points at a $t object $o"
+       if test "z$t" = zcommit
+       then
+               echo "The commit was authored by $n $e
+at $d, and titled
+
+    $s
+
+Its message reads as:
+"
+               echo "$b" | sed -e "s/^/    /"
+               echo
+       fi
+'
+
+eval=`git-for-each-ref --shell --format="$fmt" \
+       --sort='*objecttype' \
+       --sort=-taggerdate \
+       refs/tags`
+eval "$eval"
+------------
diff --git a/Documentation/git-format-patch.txt b/Documentation/git-format-patch.txt
new file mode 100644 (file)
index 0000000..6fb9429
--- /dev/null
@@ -0,0 +1,208 @@
+git-format-patch(1)
+===================
+
+NAME
+----
+git-format-patch - Prepare patches for e-mail submission
+
+
+SYNOPSIS
+--------
+[verse]
+'git-format-patch' [-k] [-o <dir> | --stdout] [--thread]
+                   [--attach[=<boundary>] | --inline[=<boundary>]]
+                   [-s | --signoff] [<common diff options>]
+                   [-n | --numbered | -N | --no-numbered]
+                   [--start-number <n>] [--numbered-files]
+                   [--in-reply-to=Message-Id] [--suffix=.<sfx>]
+                   [--ignore-if-in-upstream]
+                   [--subject-prefix=Subject-Prefix]
+                  [ <since> | <revision range> ]
+
+DESCRIPTION
+-----------
+
+Prepare each commit with its patch in
+one file per commit, formatted to resemble UNIX mailbox format.
+The output of this command is convenient for e-mail submission or
+for use with gitlink:git-am[1].
+
+There are two ways to specify which commits to operate on.
+
+1. A single commit, <since>, specifies that the commits leading
+   to the tip of the current branch that are not in the history
+   that leads to the <since> to be output.
+
+2. Generic <revision range> expression (see "SPECIFYING
+   REVISIONS" section in gitlink:git-rev-parse[1]) means the
+   commits in the specified range.
+
+A single commit, when interpreted as a <revision range>
+expression, means "everything that leads to that commit", but
+if you write 'git format-patch <commit>', the previous rule
+applies to that command line and you do not get "everything
+since the beginning of the time".  If you want to format
+everything since project inception to one commit, say "git
+format-patch \--root <commit>" to make it clear that it is the
+latter case.
+
+By default, each output file is numbered sequentially from 1, and uses the
+first line of the commit message (massaged for pathname safety) as
+the filename. With the --numbered-files option, the output file names
+will only be numbers, without the first line of the commit appended.
+The names of the output files are printed to standard
+output, unless the --stdout option is specified.
+
+If -o is specified, output files are created in <dir>.  Otherwise
+they are created in the current working directory.
+
+If -n is specified, instead of "[PATCH] Subject", the first line
+is formatted as "[PATCH n/m] Subject".
+
+If given --thread, git-format-patch will generate In-Reply-To and
+References headers to make the second and subsequent patch mails appear
+as replies to the first mail; this also generates a Message-Id header to
+reference.
+
+OPTIONS
+-------
+:git-format-patch: 1
+include::diff-options.txt[]
+
+-<n>::
+       Limits the number of patches to prepare.
+
+-o|--output-directory <dir>::
+       Use <dir> to store the resulting files, instead of the
+       current working directory.
+
+-n|--numbered::
+       Name output in '[PATCH n/m]' format.
+
+-N|--no-numbered::
+       Name output in '[PATCH]' format.
+
+--start-number <n>::
+       Start numbering the patches at <n> instead of 1.
+
+--numbered-files::
+       Output file names will be a simple number sequence
+       without the default first line of the commit appended.
+       Mutually exclusive with the --stdout option.
+
+-k|--keep-subject::
+       Do not strip/add '[PATCH]' from the first line of the
+       commit log message.
+
+-s|--signoff::
+       Add `Signed-off-by:` line to the commit message, using
+       the committer identity of yourself.
+
+--stdout::
+       Print all commits to the standard output in mbox format,
+       instead of creating a file for each one.
+
+--attach[=<boundary>]::
+       Create multipart/mixed attachment, the first part of
+       which is the commit message and the patch itself in the
+       second part, with "Content-Disposition: attachment".
+
+--inline[=<boundary>]::
+       Create multipart/mixed attachment, the first part of
+       which is the commit message and the patch itself in the
+       second part, with "Content-Disposition: inline".
+
+--thread::
+       Add In-Reply-To and References headers to make the second and
+       subsequent mails appear as replies to the first.  Also generates
+       the Message-Id header to reference.
+
+--in-reply-to=Message-Id::
+       Make the first mail (or all the mails with --no-thread) appear as a
+       reply to the given Message-Id, which avoids breaking threads to
+       provide a new patch series.
+
+--ignore-if-in-upstream::
+       Do not include a patch that matches a commit in
+       <until>..<since>.  This will examine all patches reachable
+       from <since> but not from <until> and compare them with the
+       patches being generated, and any patch that matches is
+       ignored.
+
+--subject-prefix=<Subject-Prefix>::
+       Instead of the standard '[PATCH]' prefix in the subject
+       line, instead use '[<Subject-Prefix>]'. This
+       allows for useful naming of a patch series, and can be
+       combined with the --numbered option.
+
+--suffix=.<sfx>::
+       Instead of using `.patch` as the suffix for generated
+       filenames, use specified suffix.  A common alternative is
+       `--suffix=.txt`.
++
+Note that you would need to include the leading dot `.` if you
+want a filename like `0001-description-of-my-change.patch`, and
+the first letter does not have to be a dot.  Leaving it empty would
+not add any suffix.
+
+CONFIGURATION
+-------------
+You can specify extra mail header lines to be added to each message
+in the repository configuration, new defaults for the subject prefix
+and file suffix, and number patches when outputting more than one.
+
+------------
+[format]
+        headers = "Organization: git-foo\n"
+        subjectprefix = CHANGE
+        suffix = .txt
+        numbered = auto
+------------
+
+
+EXAMPLES
+--------
+
+git-format-patch -k --stdout R1..R2 | git-am -3 -k::
+       Extract commits between revisions R1 and R2, and apply
+       them on top of the current branch using `git-am` to
+       cherry-pick them.
+
+git-format-patch origin::
+       Extract all commits which are in the current branch but
+       not in the origin branch.  For each commit a separate file
+       is created in the current directory.
+
+git-format-patch \--root origin::
+       Extract all commits that lead to 'origin' since the
+       inception of the project.
+
+git-format-patch -M -B origin::
+       The same as the previous one.  Additionally, it detects
+       and handles renames and complete rewrites intelligently to
+       produce a renaming patch.  A renaming patch reduces the
+       amount of text output, and generally makes it easier to
+       review it.  Note that the "patch" program does not
+       understand renaming patches, so use it only when you know
+       the recipient uses git to apply your patch.
+
+git-format-patch -3::
+       Extract three topmost commits from the current branch
+       and format them as e-mailable patches.
+
+See Also
+--------
+gitlink:git-am[1], gitlink:git-send-email[1]
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-fsck-objects.txt b/Documentation/git-fsck-objects.txt
new file mode 100644 (file)
index 0000000..f21061e
--- /dev/null
@@ -0,0 +1,17 @@
+git-fsck-objects(1)
+===================
+
+NAME
+----
+git-fsck-objects - Verifies the connectivity and validity of the objects in the database
+
+
+SYNOPSIS
+--------
+'git-fsck-objects' ...
+
+DESCRIPTION
+-----------
+
+This is a synonym for gitlink:git-fsck[1].  Please refer to the
+documentation of that command.
diff --git a/Documentation/git-fsck.txt b/Documentation/git-fsck.txt
new file mode 100644 (file)
index 0000000..45c0bee
--- /dev/null
@@ -0,0 +1,153 @@
+git-fsck(1)
+===========
+
+NAME
+----
+git-fsck - Verifies the connectivity and validity of the objects in the database
+
+
+SYNOPSIS
+--------
+[verse]
+'git-fsck' [--tags] [--root] [--unreachable] [--cache] [--no-reflogs]
+        [--full] [--strict] [--verbose] [--lost-found] [<object>*]
+
+DESCRIPTION
+-----------
+Verifies the connectivity and validity of the objects in the database.
+
+OPTIONS
+-------
+<object>::
+       An object to treat as the head of an unreachability trace.
++
+If no objects are given, git-fsck defaults to using the
+index file and all SHA1 references in .git/refs/* as heads.
+
+--unreachable::
+       Print out objects that exist but that aren't readable from any
+       of the reference nodes.
+
+--root::
+       Report root nodes.
+
+--tags::
+       Report tags.
+
+--cache::
+       Consider any object recorded in the index also as a head node for
+       an unreachability trace.
+
+--no-reflogs::
+       Do not consider commits that are referenced only by an
+       entry in a reflog to be reachable.  This option is meant
+       only to search for commits that used to be in a ref, but
+       now aren't, but are still in that corresponding reflog.
+
+--full::
+       Check not just objects in GIT_OBJECT_DIRECTORY
+       ($GIT_DIR/objects), but also the ones found in alternate
+       object pools listed in GIT_ALTERNATE_OBJECT_DIRECTORIES
+       or $GIT_DIR/objects/info/alternates,
+       and in packed git archives found in $GIT_DIR/objects/pack
+       and corresponding pack subdirectories in alternate
+       object pools.
+
+--strict::
+       Enable more strict checking, namely to catch a file mode
+       recorded with g+w bit set, which was created by older
+       versions of git.  Existing repositories, including the
+       Linux kernel, git itself, and sparse repository have old
+       objects that triggers this check, but it is recommended
+       to check new projects with this flag.
+
+--verbose::
+       Be chatty.
+
+--lost-found::
+       Write dangling objects into .git/lost-found/commit/ or
+       .git/lost-found/other/, depending on type.  If the object is
+       a blob, the contents are written into the file, rather than
+       its object name.
+
+It tests SHA1 and general object sanity, and it does full tracking of
+the resulting reachability and everything else. It prints out any
+corruption it finds (missing or bad objects), and if you use the
+'--unreachable' flag it will also print out objects that exist but
+that aren't readable from any of the specified head nodes.
+
+So for example
+
+       git-fsck --unreachable HEAD $(cat .git/refs/heads/*)
+
+will do quite a _lot_ of verification on the tree. There are a few
+extra validity tests to be added (make sure that tree objects are
+sorted properly etc), but on the whole if "git-fsck" is happy, you
+do have a valid tree.
+
+Any corrupt objects you will have to find in backups or other archives
+(i.e., you can just remove them and do an "rsync" with some other site in
+the hopes that somebody else has the object you have corrupted).
+
+Of course, "valid tree" doesn't mean that it wasn't generated by some
+evil person, and the end result might be crap. git is a revision
+tracking system, not a quality assurance system ;)
+
+Extracted Diagnostics
+---------------------
+
+expect dangling commits - potential heads - due to lack of head information::
+       You haven't specified any nodes as heads so it won't be
+       possible to differentiate between un-parented commits and
+       root nodes.
+
+missing sha1 directory '<dir>'::
+       The directory holding the sha1 objects is missing.
+
+unreachable <type> <object>::
+       The <type> object <object>, isn't actually referred to directly
+       or indirectly in any of the trees or commits seen. This can
+       mean that there's another root node that you're not specifying
+       or that the tree is corrupt. If you haven't missed a root node
+       then you might as well delete unreachable nodes since they
+       can't be used.
+
+missing <type> <object>::
+       The <type> object <object>, is referred to but isn't present in
+       the database.
+
+dangling <type> <object>::
+       The <type> object <object>, is present in the database but never
+       'directly' used. A dangling commit could be a root node.
+
+warning: git-fsck: tree <tree> has full pathnames in it::
+       And it shouldn't...
+
+sha1 mismatch <object>::
+       The database has an object who's sha1 doesn't match the
+       database value.
+       This indicates a serious data integrity problem.
+
+Environment Variables
+---------------------
+
+GIT_OBJECT_DIRECTORY::
+       used to specify the object database root (usually $GIT_DIR/objects)
+
+GIT_INDEX_FILE::
+       used to specify the index file of the index
+
+GIT_ALTERNATE_OBJECT_DIRECTORIES::
+       used to specify additional object database roots (usually unset)
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-gc.txt b/Documentation/git-gc.txt
new file mode 100644 (file)
index 0000000..872056e
--- /dev/null
@@ -0,0 +1,117 @@
+git-gc(1)
+=========
+
+NAME
+----
+git-gc - Cleanup unnecessary files and optimize the local repository
+
+
+SYNOPSIS
+--------
+'git-gc' [--prune] [--aggressive] [--auto]
+
+DESCRIPTION
+-----------
+Runs a number of housekeeping tasks within the current repository,
+such as compressing file revisions (to reduce disk space and increase
+performance) and removing unreachable objects which may have been
+created from prior invocations of gitlink:git-add[1].
+
+Users are encouraged to run this task on a regular basis within
+each repository to maintain good disk space utilization and good
+operating performance. Some git commands may automatically run
+`git-gc`; see the `--auto` flag below for details.
+
+OPTIONS
+-------
+
+--prune::
+       Usually `git-gc` packs refs, expires old reflog entries,
+       packs loose objects,
+       and removes old 'rerere' records.  Removal
+       of unreferenced loose objects is an unsafe operation
+       while other git operations are in progress, so it is not
+       done by default.  Pass this option if you want it, and only
+       when you know nobody else is creating new objects in the
+       repository at the same time (e.g. never use this option
+       in a cron script).
+
+--aggressive::
+       Usually 'git-gc' runs very quickly while providing good disk
+       space utilization and performance.  This option will cause
+       git-gc to more aggressively optimize the repository at the expense
+       of taking much more time.  The effects of this optimization are
+       persistent, so this option only needs to be used occasionally; every
+       few hundred changesets or so.
+
+--auto::
+       With this option, `git gc` checks whether any housekeeping is
+       required; if not, it exits without performing any work.
+       Some git commands run `git gc --auto` after performing
+       operations that could create many loose objects.
++
+Housekeeping is required if there are too many loose objects or
+too many packs in the repository. If the number of loose objects
+exceeds the value of the `gc.auto` configuration variable, then
+all loose objects are combined into a single pack using
+`git-repack -d -l`.  Setting the value of `gc.auto` to 0
+disables automatic packing of loose objects.
++
+If the number of packs exceeds the value of `gc.autopacklimit`,
+then existing packs (except those marked with a `.keep` file)
+are consolidated into a single pack by using the `-A` option of
+`git-repack`. Setting `gc.autopacklimit` to 0 disables
+automatic consolidation of packs.
+
+Configuration
+-------------
+
+The optional configuration variable 'gc.reflogExpire' can be
+set to indicate how long historical entries within each branch's
+reflog should remain available in this repository.  The setting is
+expressed as a length of time, for example '90 days' or '3 months'.
+It defaults to '90 days'.
+
+The optional configuration variable 'gc.reflogExpireUnreachable'
+can be set to indicate how long historical reflog entries which
+are not part of the current branch should remain available in
+this repository.  These types of entries are generally created as
+a result of using `git commit \--amend` or `git rebase` and are the
+commits prior to the amend or rebase occurring.  Since these changes
+are not part of the current project most users will want to expire
+them sooner.  This option defaults to '30 days'.
+
+The optional configuration variable 'gc.rerereresolved' indicates
+how long records of conflicted merge you resolved earlier are
+kept.  This defaults to 60 days.
+
+The optional configuration variable 'gc.rerereunresolved' indicates
+how long records of conflicted merge you have not resolved are
+kept.  This defaults to 15 days.
+
+The optional configuration variable 'gc.packrefs' determines if
+`git gc` runs `git-pack-refs`.  Without the configuration, `git-pack-refs`
+is not run in bare repositories by default, to allow older dumb-transport
+clients fetch from the repository,  but this will change in the future.
+
+The optional configuration variable 'gc.aggressiveWindow' controls how
+much time is spent optimizing the delta compression of the objects in
+the repository when the --aggressive option is specified.  The larger
+the value, the more time is spent optimizing the delta compression.  See
+the documentation for the --window' option in gitlink:git-repack[1] for
+more details.  This defaults to 10.
+
+See Also
+--------
+gitlink:git-prune[1]
+gitlink:git-reflog[1]
+gitlink:git-repack[1]
+gitlink:git-rerere[1]
+
+Author
+------
+Written by Shawn O. Pearce <spearce@spearce.org>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-get-tar-commit-id.txt b/Documentation/git-get-tar-commit-id.txt
new file mode 100644 (file)
index 0000000..60d1c52
--- /dev/null
@@ -0,0 +1,36 @@
+git-get-tar-commit-id(1)
+========================
+
+NAME
+----
+git-get-tar-commit-id - Extract commit ID from an archive created using git-archive
+
+
+SYNOPSIS
+--------
+'git-get-tar-commit-id' < <tarfile>
+
+
+DESCRIPTION
+-----------
+Acts as a filter, extracting the commit ID stored in archives created by
+gitlink:git-archive[1].  It reads only the first 1024 bytes of input, thus its
+runtime is not influenced by the size of <tarfile> very much.
+
+If no commit ID is found, git-get-tar-commit-id quietly exists with a
+return code of 1.  This can happen if <tarfile> had not been created
+using git-archive or if the first parameter of git-archive had been
+a tree ID instead of a commit ID or tag.
+
+
+Author
+------
+Written by Rene Scharfe <rene.scharfe@lsrfire.ath.cx>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-grep.txt b/Documentation/git-grep.txt
new file mode 100644 (file)
index 0000000..97faaa1
--- /dev/null
@@ -0,0 +1,146 @@
+git-grep(1)
+===========
+
+NAME
+----
+git-grep - Print lines matching a pattern
+
+
+SYNOPSIS
+--------
+[verse]
+'git-grep' [--cached]
+          [-a | --text] [-I] [-i | --ignore-case] [-w | --word-regexp]
+          [-v | --invert-match] [-h|-H] [--full-name]
+          [-E | --extended-regexp] [-G | --basic-regexp]
+          [-F | --fixed-strings] [-n]
+          [-l | --files-with-matches] [-L | --files-without-match]
+          [-c | --count] [--all-match]
+          [-A <post-context>] [-B <pre-context>] [-C <context>]
+          [-f <file>] [-e] <pattern>
+          [--and|--or|--not|(|)|-e <pattern>...] [<tree>...]
+          [--] [<path>...]
+
+DESCRIPTION
+-----------
+Look for specified patterns in the working tree files, blobs
+registered in the index file, or given tree objects.
+
+
+OPTIONS
+-------
+--cached::
+       Instead of searching in the working tree files, check
+       the blobs registered in the index file.
+
+-a | --text::
+       Process binary files as if they were text.
+
+-i | --ignore-case::
+       Ignore case differences between the patterns and the
+       files.
+
+-I::
+       Don't match the pattern in binary files.
+
+-w | --word-regexp::
+       Match the pattern only at word boundary (either begin at the
+       beginning of a line, or preceded by a non-word character; end at
+       the end of a line or followed by a non-word character).
+
+-v | --invert-match::
+       Select non-matching lines.
+
+-h | -H::
+       By default, the command shows the filename for each
+       match.  `-h` option is used to suppress this output.
+       `-H` is there for completeness and does not do anything
+       except it overrides `-h` given earlier on the command
+       line.
+
+--full-name::
+       When run from a subdirectory, the command usually
+       outputs paths relative to the current directory.  This
+       option forces paths to be output relative to the project
+       top directory.
+
+-E | --extended-regexp | -G | --basic-regexp::
+       Use POSIX extended/basic regexp for patterns.  Default
+       is to use basic regexp.
+
+-F | --fixed-strings::
+       Use fixed strings for patterns (don't interpret pattern
+       as a regex).
+
+-n::
+       Prefix the line number to matching lines.
+
+-l | --files-with-matches | -L | --files-without-match::
+       Instead of showing every matched line, show only the
+       names of files that contain (or do not contain) matches.
+
+-c | --count::
+       Instead of showing every matched line, show the number of
+       lines that match.
+
+-[ABC] <context>::
+       Show `context` trailing (`A` -- after), or leading (`B`
+       -- before), or both (`C` -- context) lines, and place a
+       line containing `--` between contiguous groups of
+       matches.
+
+-<num>::
+       A shortcut for specifying -C<num>.
+
+-f <file>::
+       Read patterns from <file>, one per line.
+
+-e::
+       The next parameter is the pattern. This option has to be
+       used for patterns starting with - and should be used in
+       scripts passing user input to grep.  Multiple patterns are
+       combined by 'or'.
+
+--and | --or | --not | ( | )::
+       Specify how multiple patterns are combined using Boolean
+       expressions.  `--or` is the default operator.  `--and` has
+       higher precedence than `--or`.  `-e` has to be used for all
+       patterns.
+
+--all-match::
+       When giving multiple pattern expressions combined with `--or`,
+       this flag is specified to limit the match to files that
+       have lines to match all of them.
+
+`<tree>...`::
+       Search blobs in the trees for specified patterns.
+
+\--::
+       Signals the end of options; the rest of the parameters
+       are <path> limiters.
+
+
+Example
+-------
+
+git grep -e \'#define\' --and \( -e MAX_PATH -e PATH_MAX \)::
+       Looks for a line that has `#define` and either `MAX_PATH` or
+       `PATH_MAX`.
+
+git grep --all-match -e NODE -e Unexpected::
+       Looks for a line that has `NODE` or `Unexpected` in
+       files that have lines that match both.
+
+Author
+------
+Originally written by Linus Torvalds <torvalds@osdl.org>, later
+revamped by Junio C Hamano.
+
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-gui.txt b/Documentation/git-gui.txt
new file mode 100644 (file)
index 0000000..13252a1
--- /dev/null
@@ -0,0 +1,115 @@
+git-gui(1)
+==========
+
+NAME
+----
+git-gui - A portable graphical interface to Git
+
+SYNOPSIS
+--------
+'git gui' [<command>] [arguments]
+
+DESCRIPTION
+-----------
+A Tcl/Tk based graphical user interface to Git.  git-gui focuses
+on allowing users to make changes to their repository by making
+new commits, amending existing ones, creating branches, performing
+local merges, and fetching/pushing to remote repositories.
+
+Unlike gitlink:gitk[1], git-gui focuses on commit generation
+and single file annotation, and does not show project history.
+It does however supply menu actions to start a gitk session from
+within git-gui.
+
+git-gui is known to work on all popular UNIX systems, Mac OS X,
+and Windows (under both Cygwin and MSYS).  To the extent possible
+OS specific user interface guidelines are followed, making git-gui
+a fairly native interface for users.
+
+COMMANDS
+--------
+blame::
+       Start a blame viewer on the specified file on the given
+       version (or working directory if not specified).
+
+browser::
+       Start a tree browser showing all files in the specified
+       commit (or 'HEAD' by default).  Files selected through the
+       browser are opened in the blame viewer.
+
+citool::
+       Start git-gui and arrange to make exactly one commit before
+       exiting and returning to the shell.  The interface is limited
+       to only commit actions, slightly reducing the application's
+       startup time and simplifying the menubar.
+
+version::
+       Display the currently running version of git-gui.
+
+
+Examples
+--------
+git gui blame Makefile::
+
+       Show the contents of the file 'Makefile' in the current
+       working directory, and provide annotations for both the
+       original author of each line, and who moved the line to its
+       current location.  The uncommitted file is annotated, and
+       uncommitted changes (if any) are explicitly attributed to
+       'Not Yet Committed'.
+
+git gui blame v0.99.8 Makefile::
+
+       Show the contents of 'Makefile' in revision 'v0.99.8'
+       and provide annotations for each line.  Unlike the above
+       example the file is read from the object database and not
+       the working directory.
+
+git gui citool::
+
+       Make one commit and return to the shell when it is complete.
+
+git citool::
+
+       Same as 'git gui citool' (above).
+
+git gui browser maint::
+
+       Show a browser for the tree of the 'maint' branch.  Files
+       selected in the browser can be viewed with the internal
+       blame viewer.
+
+See Also
+--------
+'gitk(1)'::
+       The git repository browser.  Shows branches, commit history
+       and file differences.  gitk is the utility started by
+       git-gui's Repository Visualize actions.
+
+Other
+-----
+git-gui is actually maintained as an independent project, but stable
+versions are distributed as part of the Git suite for the convenience
+of end users.
+
+A git-gui development repository can be obtained from:
+
+  git clone git://repo.or.cz/git-gui.git
+
+or
+
+  git clone http://repo.or.cz/r/git-gui.git
+
+or browsed online at http://repo.or.cz/w/git-gui.git/[].
+
+Author
+------
+Written by Shawn O. Pearce <spearce@spearce.org>.
+
+Documentation
+--------------
+Documentation by Shawn O. Pearce <spearce@spearce.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-hash-object.txt b/Documentation/git-hash-object.txt
new file mode 100644 (file)
index 0000000..616f196
--- /dev/null
@@ -0,0 +1,45 @@
+git-hash-object(1)
+==================
+
+NAME
+----
+git-hash-object - Compute object ID and optionally creates a blob from a file
+
+
+SYNOPSIS
+--------
+'git-hash-object' [-t <type>] [-w] [--stdin] [--] <file>...
+
+DESCRIPTION
+-----------
+Computes the object ID value for an object with specified type
+with the contents of the named file (which can be outside of the
+work tree), and optionally writes the resulting object into the
+object database.  Reports its object ID to its standard output.
+This is used by "git-cvsimport" to update the index
+without modifying files in the work tree.  When <type> is not
+specified, it defaults to "blob".
+
+OPTIONS
+-------
+
+-t <type>::
+       Specify the type (default: "blob").
+
+-w::
+       Actually write the object into the object database.
+
+--stdin::
+       Read the object from standard input instead of from a file.
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-help.txt b/Documentation/git-help.txt
new file mode 100644 (file)
index 0000000..7ddbf46
--- /dev/null
@@ -0,0 +1,48 @@
+git-help(1)
+===========
+
+NAME
+----
+git-help - display help information about git
+
+SYNOPSIS
+--------
+'git help' [-a|--all] [COMMAND]
+
+DESCRIPTION
+-----------
+
+With no options and no COMMAND given, the synopsis of the 'git'
+command and a list of the most commonly used git commands are printed
+on the standard output.
+
+If the option '--all' or '-a' is given, then all available commands are
+printed on the standard output.
+
+If a git command is named, a manual page for that command is brought
+up. The 'man' program is used by default for this purpose.
+
+Note that 'git --help ...' is identical as 'git help ...' because the
+former is internally converted into the latter.
+
+OPTIONS
+-------
+-a|--all::
+
+       Prints all the available commands on the standard output. This
+       option superseeds any other option.
+
+Author
+------
+Written by Junio C Hamano <gitster@pobox.com> and the git-list
+<git@vger.kernel.org>.
+
+Documentation
+-------------
+Initial documentation was part of the gitlink:git[7] man page.
+Christian Couder <chriscool@tuxfamily.org> extracted and rewrote it a
+little. Maintenance is done by the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-http-fetch.txt b/Documentation/git-http-fetch.txt
new file mode 100644 (file)
index 0000000..389c6ed
--- /dev/null
@@ -0,0 +1,56 @@
+git-http-fetch(1)
+=================
+
+NAME
+----
+git-http-fetch - Download from a remote git repository via HTTP
+
+
+SYNOPSIS
+--------
+'git-http-fetch' [-c] [-t] [-a] [-d] [-v] [-w filename] [--recover] [--stdin] <commit> <url>
+
+DESCRIPTION
+-----------
+Downloads a remote git repository via HTTP.
+
+OPTIONS
+-------
+commit-id::
+        Either the hash or the filename under [URL]/refs/ to
+        pull.
+
+-c::
+       Get the commit objects.
+-t::
+       Get trees associated with the commit objects.
+-a::
+       Get all the objects.
+-v::
+       Report what is downloaded.
+
+-w <filename>::
+        Writes the commit-id into the filename under $GIT_DIR/refs/<filename> on
+        the local end after the transfer is complete.
+
+--stdin::
+       Instead of a commit id on the command line (which is not expected in this
+       case), 'git-http-fetch' expects lines on stdin in the format
+
+               <commit-id>['\t'<filename-as-in--w>]
+
+--recover::
+       Verify that everything reachable from target is fetched.  Used after
+       an earlier fetch is interrupted.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-http-push.txt b/Documentation/git-http-push.txt
new file mode 100644 (file)
index 0000000..3a69b71
--- /dev/null
@@ -0,0 +1,101 @@
+git-http-push(1)
+================
+
+NAME
+----
+git-http-push - Push objects over HTTP/DAV to another repository
+
+
+SYNOPSIS
+--------
+'git-http-push' [--all] [--dry-run] [--force] [--verbose] <url> <ref> [<ref>...]
+
+DESCRIPTION
+-----------
+Sends missing objects to remote repository, and updates the
+remote branch.
+
+
+OPTIONS
+-------
+--all::
+       Do not assume that the remote repository is complete in its
+       current state, and verify all objects in the entire local
+       ref's history exist in the remote repository.
+
+--force::
+       Usually, the command refuses to update a remote ref that
+       is not an ancestor of the local ref used to overwrite it.
+       This flag disables the check.  What this means is that
+       the remote repository can lose commits; use it with
+       care.
+
+--dry-run::
+       Do everything except actually send the updates.
+
+--verbose::
+       Report the list of objects being walked locally and the
+       list of objects successfully sent to the remote repository.
+
+-d, -D::
+       Remove <ref> from remote repository.  The specified branch
+       cannot be the remote HEAD.  If -d is specified the following
+       other conditions must also be met:
+
+       - Remote HEAD must resolve to an object that exists locally
+       - Specified branch resolves to an object that exists locally
+       - Specified branch is an ancestor of the remote HEAD
+
+<ref>...::
+       The remote refs to update.
+
+
+Specifying the Refs
+-------------------
+
+A '<ref>' specification can be either a single pattern, or a pair
+of such patterns separated by a colon ":" (this means that a ref name
+cannot have a colon in it).  A single pattern '<name>' is just a
+shorthand for '<name>:<name>'.
+
+Each pattern pair consists of the source side (before the colon)
+and the destination side (after the colon).  The ref to be
+pushed is determined by finding a match that matches the source
+side, and where it is pushed is determined by using the
+destination side.
+
+ - It is an error if <src> does not match exactly one of the
+   local refs.
+
+ - If <dst> does not match any remote ref, either
+
+   * it has to start with "refs/"; <dst> is used as the
+     destination literally in this case.
+
+   * <src> == <dst> and the ref that matched the <src> must not
+     exist in the set of remote refs; the ref matched <src>
+     locally is used as the name of the destination.
+
+Without '--force', the <src> ref is stored at the remote only if
+<dst> does not exist, or <dst> is a proper subset (i.e. an
+ancestor) of <src>.  This check, known as "fast forward check",
+is performed in order to avoid accidentally overwriting the
+remote ref and lose other peoples' commits from there.
+
+With '--force', the fast forward check is disabled for all refs.
+
+Optionally, a <ref> parameter can be prefixed with a plus '+' sign
+to disable the fast-forward check only on that ref.
+
+
+Author
+------
+Written by Nick Hengeveld <nickh@reactrix.com>
+
+Documentation
+--------------
+Documentation by Nick Hengeveld
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-imap-send.txt b/Documentation/git-imap-send.txt
new file mode 100644 (file)
index 0000000..eca9e9c
--- /dev/null
@@ -0,0 +1,62 @@
+git-imap-send(1)
+================
+
+NAME
+----
+git-imap-send - Dump a mailbox from stdin into an imap folder
+
+
+SYNOPSIS
+--------
+'git-imap-send'
+
+
+DESCRIPTION
+-----------
+This command uploads a mailbox generated with git-format-patch
+into an imap drafts folder.  This allows patches to be sent as
+other email is sent with mail clients that cannot read mailbox
+files directly.
+
+Typical usage is something like:
+
+git-format-patch --signoff --stdout --attach origin | git-imap-send
+
+
+CONFIGURATION
+-------------
+
+git-imap-send requires the following values in the repository
+configuration file (shown with examples):
+
+..........................
+[imap]
+    Folder = "INBOX.Drafts"
+
+[imap]
+    Tunnel = "ssh -q user@server.com /usr/bin/imapd ./Maildir 2> /dev/null"
+
+[imap]
+    Host = imap.server.com
+    User = bob
+    Pass = pwd
+    Port = 143
+..........................
+
+
+BUGS
+----
+Doesn't handle lines starting with "From " in the message body.
+
+
+Author
+------
+Derived from isync 1.0.1 by Mike McCormack.
+
+Documentation
+--------------
+Documentation by Mike McCormack
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-index-pack.txt b/Documentation/git-index-pack.txt
new file mode 100644 (file)
index 0000000..bf5c2bd
--- /dev/null
@@ -0,0 +1,100 @@
+git-index-pack(1)
+=================
+
+NAME
+----
+git-index-pack - Build pack index file for an existing packed archive
+
+
+SYNOPSIS
+--------
+[verse]
+'git-index-pack' [-v] [-o <index-file>] <pack-file>
+'git-index-pack' --stdin [--fix-thin] [--keep] [-v] [-o <index-file>]
+                 [<pack-file>]
+
+
+DESCRIPTION
+-----------
+Reads a packed archive (.pack) from the specified file, and
+builds a pack index file (.idx) for it.  The packed archive
+together with the pack index can then be placed in the
+objects/pack/ directory of a git repository.
+
+
+OPTIONS
+-------
+-v::
+       Be verbose about what is going on, including progress status.
+
+-o <index-file>::
+       Write the generated pack index into the specified
+       file.  Without this option the name of pack index
+       file is constructed from the name of packed archive
+       file by replacing .pack with .idx (and the program
+       fails if the name of packed archive does not end
+       with .pack).
+
+--stdin::
+       When this flag is provided, the pack is read from stdin
+       instead and a copy is then written to <pack-file>. If
+       <pack-file> is not specified, the pack is written to
+       objects/pack/ directory of the current git repository with
+       a default name determined from the pack content.  If
+       <pack-file> is not specified consider using --keep to
+       prevent a race condition between this process and
+       gitlink::git-repack[1].
+
+--fix-thin::
+       It is possible for gitlink:git-pack-objects[1] to build
+       "thin" pack, which records objects in deltified form based on
+       objects not included in the pack to reduce network traffic.
+       Those objects are expected to be present on the receiving end
+       and they must be included in the pack for that pack to be self
+       contained and indexable. Without this option any attempt to
+       index a thin pack will fail. This option only makes sense in
+       conjunction with --stdin.
+
+--keep::
+       Before moving the index into its final destination
+       create an empty .keep file for the associated pack file.
+       This option is usually necessary with --stdin to prevent a
+       simultaneous gitlink:git-repack[1] process from deleting
+       the newly constructed pack and index before refs can be
+       updated to use objects contained in the pack.
+
+--keep='why'::
+       Like --keep create a .keep file before moving the index into
+       its final destination, but rather than creating an empty file
+       place 'why' followed by an LF into the .keep file.  The 'why'
+       message can later be searched for within all .keep files to
+       locate any which have outlived their usefulness.
+
+--index-version=<version>[,<offset>]::
+       This is intended to be used by the test suite only. It allows
+       to force the version for the generated pack index, and to force
+       64-bit index entries on objects located above the given offset.
+
+
+Note
+----
+
+Once the index has been created, the list of object names is sorted
+and the SHA1 hash of that list is printed to stdout. If --stdin was
+also used then this is prefixed by either "pack\t", or "keep\t" if a
+new .keep file was successfully created. This is useful to remove a
+.keep file used as a lock to prevent the race with gitlink:git-repack[1]
+mentioned above.
+
+
+Author
+------
+Written by Sergey Vlasov <vsu@altlinux.ru>
+
+Documentation
+-------------
+Documentation by Sergey Vlasov
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-init-db.txt b/Documentation/git-init-db.txt
new file mode 100644 (file)
index 0000000..d4e01cb
--- /dev/null
@@ -0,0 +1,18 @@
+git-init-db(1)
+==============
+
+NAME
+----
+git-init-db - Creates an empty git repository
+
+
+SYNOPSIS
+--------
+'git-init-db' [-q | --quiet] [--template=<template_directory>] [--shared[=<permissions>]]
+
+
+DESCRIPTION
+-----------
+
+This is a synonym for gitlink:git-init[1].  Please refer to the
+documentation of that command.
diff --git a/Documentation/git-init.txt b/Documentation/git-init.txt
new file mode 100644 (file)
index 0000000..07484a4
--- /dev/null
@@ -0,0 +1,114 @@
+git-init(1)
+===========
+
+NAME
+----
+git-init - Create an empty git repository or reinitialize an existing one
+
+
+SYNOPSIS
+--------
+'git-init' [-q | --quiet] [--template=<template_directory>] [--shared[=<permissions>]]
+
+
+OPTIONS
+-------
+
+--
+
+-q, \--quiet::
+
+Only print error and warning messages, all other output will be suppressed.
+
+--template=<template_directory>::
+
+Provide the directory from which templates will be used.  The default template
+directory is `/usr/share/git-core/templates`.
+
+When specified, `<template_directory>` is used as the source of the template
+files rather than the default.  The template files include some directory
+structure, some suggested "exclude patterns", and copies of non-executing
+"hook" files.  The suggested patterns and hook files are all modifiable and
+extensible.
+
+--shared[={false|true|umask|group|all|world|everybody}]::
+
+Specify that the git repository is to be shared amongst several users.  This
+allows users belonging to the same group to push into that
+repository.  When specified, the config variable "core.sharedRepository" is
+set so that files and directories under `$GIT_DIR` are created with the
+requested permissions.  When not specified, git will use permissions reported
+by umask(2).
+
+The option can have the following values, defaulting to 'group' if no value
+is given:
+
+ - 'umask' (or 'false'): Use permissions reported by umask(2). The default,
+   when `--shared` is not specified.
+
+ - 'group' (or 'true'): Make the repository group-writable, (and g+sx, since
+   the git group may be not the primary group of all users).
+
+ - 'all' (or 'world' or 'everybody'): Same as 'group', but make the repository
+   readable by all users.
+
+By default, the configuration flag receive.denyNonFastforward is enabled
+in shared repositories, so that you cannot force a non fast-forwarding push
+into it.
+
+--
+
+
+DESCRIPTION
+-----------
+This command creates an empty git repository - basically a `.git` directory
+with subdirectories for `objects`, `refs/heads`, `refs/tags`, and
+template files.
+An initial `HEAD` file that references the HEAD of the master branch
+is also created.
+
+If the `$GIT_DIR` environment variable is set then it specifies a path
+to use instead of `./.git` for the base of the repository.
+
+If the object storage directory is specified via the `$GIT_OBJECT_DIRECTORY`
+environment variable then the sha1 directories are created underneath -
+otherwise the default `$GIT_DIR/objects` directory is used.
+
+Running `git-init` in an existing repository is safe. It will not overwrite
+things that are already there. The primary reason for rerunning `git-init`
+is to pick up newly added templates.
+
+Note that `git-init` is the same as `git-init-db`.  The command
+was primarily meant to initialize the object database, but over
+time it has become responsible for setting up the other aspects
+of the repository, such as installing the default hooks and
+setting the configuration variables.  The old name is retained
+for backward compatibility reasons.
+
+
+EXAMPLES
+--------
+
+Start a new git repository for an existing code base::
++
+----------------
+$ cd /path/to/my/codebase
+$ git-init      <1>
+$ git-add .     <2>
+----------------
++
+<1> prepare /path/to/my/codebase/.git directory
+<2> add all existing file to the index
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-instaweb.txt b/Documentation/git-instaweb.txt
new file mode 100644 (file)
index 0000000..735008c
--- /dev/null
@@ -0,0 +1,84 @@
+git-instaweb(1)
+===============
+
+NAME
+----
+git-instaweb - Instantly browse your working repository in gitweb
+
+SYNOPSIS
+--------
+[verse]
+'git-instaweb' [--local] [--httpd=<httpd>] [--port=<port>]
+               [--browser=<browser>]
+'git-instaweb' [--start] [--stop] [--restart]
+
+DESCRIPTION
+-----------
+A simple script to setup gitweb and a web server for browsing the local
+repository.
+
+OPTIONS
+-------
+
+-l|--local::
+       Only bind the web server to the local IP (127.0.0.1).
+
+-d|--httpd::
+       The HTTP daemon command-line that will be executed.
+       Command-line options may be specified here, and the
+       configuration file will be added at the end of the command-line.
+       Currently lighttpd, apache2 and webrick are supported.
+       (Default: lighttpd)
+
+-m|--module-path::
+       The module path (only needed if httpd is Apache).
+       (Default: /usr/lib/apache2/modules)
+
+-p|--port::
+       The port number to bind the httpd to.  (Default: 1234)
+
+-b|--browser::
+
+       The web browser command-line to execute to view the gitweb page.
+       If blank, the URL of the gitweb instance will be printed to
+       stdout.  (Default: 'firefox')
+
+--start::
+       Start the httpd instance and exit.  This does not generate
+       any of the configuration files for spawning a new instance.
+
+--stop::
+       Stop the httpd instance and exit.  This does not generate
+       any of the configuration files for spawning a new instance,
+       nor does it close the browser.
+
+--restart::
+       Restart the httpd instance and exit.  This does not generate
+       any of the configuration files for spawning a new instance.
+
+CONFIGURATION
+-------------
+
+You may specify configuration in your .git/config
+
+-----------------------------------------------------------------------
+[instaweb]
+       local = true
+       httpd = apache2 -f
+       port = 4321
+       browser = konqueror
+       modulepath = /usr/lib/apache2/modules
+
+-----------------------------------------------------------------------
+
+Author
+------
+Written by Eric Wong <normalperson@yhbt.net>
+
+Documentation
+--------------
+Documentation by Eric Wong <normalperson@yhbt.net>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-log.txt b/Documentation/git-log.txt
new file mode 100644 (file)
index 0000000..5920d17
--- /dev/null
@@ -0,0 +1,128 @@
+git-log(1)
+==========
+
+NAME
+----
+git-log - Show commit logs
+
+
+SYNOPSIS
+--------
+'git-log' <option>...
+
+DESCRIPTION
+-----------
+Shows the commit logs.
+
+The command takes options applicable to the gitlink:git-rev-list[1]
+command to control what is shown and how, and options applicable to
+the gitlink:git-diff-tree[1] commands to control how the changes
+each commit introduces are shown.
+
+This manual page describes only the most frequently used options.
+
+
+OPTIONS
+-------
+
+include::pretty-options.txt[]
+
+:git-log: 1
+include::diff-options.txt[]
+
+-<n>::
+       Limits the number of commits to show.
+
+<since>..<until>::
+       Show only commits between the named two commits.  When
+       either <since> or <until> is omitted, it defaults to
+       `HEAD`, i.e. the tip of the current branch.
+       For a more complete list of ways to spell <since>
+       and <until>, see "SPECIFYING REVISIONS" section in
+       gitlink:git-rev-parse[1].
+
+--first-parent::
+       Follow only the first parent commit upon seeing a merge
+       commit.  This  option gives a better overview of the
+       evolution of a particular branch.
+
+-g, \--walk-reflogs::
+       Show commits as they were recorded in the reflog. The log contains
+       a record about how the tip of a reference was changed.
+       Cannot be combined with --reverse.
+       See also gitlink:git-reflog[1].
+
+--decorate::
+       Print out the ref names of any commits that are shown.
+
+--full-diff::
+       Without this flag, "git log -p <paths>..." shows commits that
+       touch the specified paths, and diffs about the same specified
+       paths.  With this, the full diff is shown for commits that touch
+       the specified paths; this means that "<paths>..." limits only
+       commits, and doesn't limit diff for those commits.
+
+--follow::
+       Continue listing the history of a file beyond renames.
+
+--log-size::
+       Before the log message print out its size in bytes. Intended
+       mainly for porcelain tools consumption. If git is unable to
+       produce a valid value size is set to zero.
+       Note that only message is considered, if also a diff is shown
+       its size is not included.
+
+<paths>...::
+       Show only commits that affect the specified paths.
+
+
+include::pretty-formats.txt[]
+
+include::diff-generate-patch.txt[]
+
+Examples
+--------
+git log --no-merges::
+
+       Show the whole commit history, but skip any merges
+
+git log v2.6.12.. include/scsi drivers/scsi::
+
+       Show all commits since version 'v2.6.12' that changed any file
+       in the include/scsi or drivers/scsi subdirectories
+
+git log --since="2 weeks ago" \-- gitk::
+
+       Show the changes during the last two weeks to the file 'gitk'.
+       The "--" is necessary to avoid confusion with the *branch* named
+       'gitk'
+
+git log --name-status release..test::
+
+       Show the commits that are in the "test" branch but not yet
+       in the "release" branch, along with the list of paths
+       each commit modifies.
+
+git log --follow builtin-rev-list.c::
+
+       Shows the commits that changed builtin-rev-list.c, including
+       those commits that occurred before the file was given its
+       present name.
+
+Discussion
+----------
+
+include::i18n.txt[]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-lost-found.txt b/Documentation/git-lost-found.txt
new file mode 100644 (file)
index 0000000..7f808fc
--- /dev/null
@@ -0,0 +1,81 @@
+git-lost-found(1)
+=================
+
+NAME
+----
+git-lost-found - Recover lost refs that luckily have not yet been pruned
+
+SYNOPSIS
+--------
+'git-lost-found'
+
+DESCRIPTION
+-----------
+
+*NOTE*: this command is deprecated.  Use gitlink:git-fsck[1] with
+the option '--lost-found' instead.
+
+Finds dangling commits and tags from the object database, and
+creates refs to them in the .git/lost-found/ directory.  Commits and
+tags that dereference to commits are stored in .git/lost-found/commit,
+and other objects are stored in .git/lost-found/other.
+
+
+OUTPUT
+------
+Prints to standard output the object names and one-line descriptions
+of any commits or tags found.
+
+EXAMPLE
+-------
+
+Suppose you run 'git tag -f' and mistype the tag to overwrite.
+The ref to your tag is overwritten, but until you run 'git
+prune', the tag itself is still there.
+
+------------
+$ git lost-found
+[1ef2b196d909eed523d4f3c9bf54b78cdd6843c6] GIT 0.99.9c
+...
+------------
+
+Also you can use gitk to browse how any tags found relate to each
+other.
+
+------------
+$ gitk $(cd .git/lost-found/commit && echo ??*)
+------------
+
+After making sure you know which the object is the tag you are looking
+for, you can reconnect it to your regular .git/refs hierarchy.
+
+------------
+$ git cat-file -t 1ef2b196
+tag
+$ git cat-file tag 1ef2b196
+object fa41bbce8e38c67a218415de6cfa510c7e50032a
+type commit
+tag v0.99.9c
+tagger Junio C Hamano <junkio@cox.net> 1131059594 -0800
+
+GIT 0.99.9c
+
+This contains the following changes from the "master" branch, since
+...
+$ git update-ref refs/tags/not-lost-anymore 1ef2b196
+$ git rev-parse not-lost-anymore
+1ef2b196d909eed523d4f3c9bf54b78cdd6843c6
+------------
+
+Author
+------
+Written by Junio C Hamano <gitster@pobox.com>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
new file mode 100644 (file)
index 0000000..2ec0c0d
--- /dev/null
@@ -0,0 +1,194 @@
+git-ls-files(1)
+===============
+
+NAME
+----
+git-ls-files - Show information about files in the index and the working tree
+
+
+SYNOPSIS
+--------
+[verse]
+'git-ls-files' [-z] [-t] [-v]
+               (--[cached|deleted|others|ignored|stage|unmerged|killed|modified])\*
+               (-[c|d|o|i|s|u|k|m])\*
+               [-x <pattern>|--exclude=<pattern>]
+               [-X <file>|--exclude-from=<file>]
+               [--exclude-per-directory=<file>]
+               [--exclude-standard]
+               [--error-unmatch] [--with-tree=<tree-ish>]
+               [--full-name] [--abbrev] [--] [<file>]\*
+
+DESCRIPTION
+-----------
+This merges the file listing in the directory cache index with the
+actual working directory list, and shows different combinations of the
+two.
+
+One or more of the options below may be used to determine the files
+shown:
+
+OPTIONS
+-------
+-c|--cached::
+       Show cached files in the output (default)
+
+-d|--deleted::
+       Show deleted files in the output
+
+-m|--modified::
+       Show modified files in the output
+
+-o|--others::
+       Show other files in the output
+
+-i|--ignored::
+       Show ignored files in the output.
+       Note that this also reverses any exclude list present.
+
+-s|--stage::
+       Show stage files in the output
+
+--directory::
+       If a whole directory is classified as "other", show just its
+       name (with a trailing slash) and not its whole contents.
+
+--no-empty-directory::
+       Do not list empty directories. Has no effect without --directory.
+
+-u|--unmerged::
+       Show unmerged files in the output (forces --stage)
+
+-k|--killed::
+       Show files on the filesystem that need to be removed due
+       to file/directory conflicts for checkout-index to
+       succeed.
+
+-z::
+       \0 line termination on output.
+
+-x|--exclude=<pattern>::
+       Skips files matching pattern.
+       Note that pattern is a shell wildcard pattern.
+
+-X|--exclude-from=<file>::
+       exclude patterns are read from <file>; 1 per line.
+
+--exclude-per-directory=<file>::
+       read additional exclude patterns that apply only to the
+       directory and its subdirectories in <file>.
+
+--exclude-standard::
+       Add the standard git exclusions: .git/info/exclude, .gitignore
+       in each directory, and the user's global exclusion file.
+
+--error-unmatch::
+       If any <file> does not appear in the index, treat this as an
+       error (return 1).
+
+--with-tree=<tree-ish>::
+       When using --error-unmatch to expand the user supplied
+       <file> (i.e. path pattern) arguments to paths, pretend
+       that paths which were removed in the index since the
+       named <tree-ish> are still present.  Using this option
+       with `-s` or `-u` options does not make any sense.
+
+-t::
+       Identify the file status with the following tags (followed by
+       a space) at the start of each line:
+       H::     cached
+       M::     unmerged
+       R::     removed/deleted
+       C::     modified/changed
+       K::     to be killed
+       ?::     other
+
+-v::
+       Similar to `-t`, but use lowercase letters for files
+       that are marked as 'always matching index'.
+
+--full-name::
+       When run from a subdirectory, the command usually
+       outputs paths relative to the current directory.  This
+       option forces paths to be output relative to the project
+       top directory.
+
+--abbrev[=<n>]::
+       Instead of showing the full 40-byte hexadecimal object
+       lines, show only handful hexdigits prefix.
+       Non default number of digits can be specified with --abbrev=<n>.
+
+\--::
+       Do not interpret any more arguments as options.
+
+<file>::
+       Files to show. If no files are given all files which match the other
+       specified criteria are shown.
+
+Output
+------
+show files just outputs the filename unless '--stage' is specified in
+which case it outputs:
+
+        [<tag> ]<mode> <object> <stage> <file>
+
+"git-ls-files --unmerged" and "git-ls-files --stage" can be used to examine
+detailed information on unmerged paths.
+
+For an unmerged path, instead of recording a single mode/SHA1 pair,
+the index records up to three such pairs; one from tree O in stage
+1, A in stage 2, and B in stage 3.  This information can be used by
+the user (or the porcelain) to see what should eventually be recorded at the
+path. (see git-read-tree for more information on state)
+
+When `-z` option is not used, TAB, LF, and backslash characters
+in pathnames are represented as `\t`, `\n`, and `\\`,
+respectively.
+
+
+Exclude Patterns
+----------------
+
+'git-ls-files' can use a list of "exclude patterns" when
+traversing the directory tree and finding files to show when the
+flags --others or --ignored are specified.  gitlink:gitignore[5]
+specifies the format of exclude patterns.
+
+These exclude patterns come from these places, in order:
+
+  1. The command line flag --exclude=<pattern> specifies a
+     single pattern.  Patterns are ordered in the same order
+     they appear in the command line.
+
+  2. The command line flag --exclude-from=<file> specifies a
+     file containing a list of patterns.  Patterns are ordered
+     in the same order they appear in the file.
+
+  3. command line flag --exclude-per-directory=<name> specifies
+     a name of the file in each directory 'git-ls-files'
+     examines, normally `.gitignore`.  Files in deeper
+     directories take precedence.  Patterns are ordered in the
+     same order they appear in the files.
+
+A pattern specified on the command line with --exclude or read
+from the file specified with --exclude-from is relative to the
+top of the directory tree.  A pattern read from a file specified
+by --exclude-per-directory is relative to the directory that the
+pattern file appears in.
+
+See Also
+--------
+gitlink:git-read-tree[1], gitlink:gitignore[5]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano, Josh Triplett, and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-ls-remote.txt b/Documentation/git-ls-remote.txt
new file mode 100644 (file)
index 0000000..93e9a60
--- /dev/null
@@ -0,0 +1,72 @@
+git-ls-remote(1)
+================
+
+NAME
+----
+git-ls-remote - List references in a remote repository
+
+
+SYNOPSIS
+--------
+[verse]
+'git-ls-remote' [--heads] [--tags]  [-u <exec> | --upload-pack <exec>]
+             <repository> <refs>...
+
+DESCRIPTION
+-----------
+Displays references available in a remote repository along with the associated
+commit IDs.
+
+
+OPTIONS
+-------
+-h|--heads, -t|--tags::
+       Limit to only refs/heads and refs/tags, respectively.
+       These options are _not_ mutually exclusive; when given
+       both, references stored in refs/heads and refs/tags are
+       displayed.
+
+-u <exec>, --upload-pack=<exec>::
+       Specify the full path of gitlink:git-upload-pack[1] on the remote
+       host. This allows listing references from repositories accessed via
+       SSH and where the SSH daemon does not use the PATH configured by the
+       user. Also see the '--exec' option for gitlink:git-peek-remote[1].
+
+<repository>::
+       Location of the repository.  The shorthand defined in
+       $GIT_DIR/branches/ can be used. Use "." (dot) to list references in
+       the local repository.
+
+<refs>...::
+       When unspecified, all references, after filtering done
+       with --heads and --tags, are shown.  When <refs>... are
+       specified, only references matching the given patterns
+       are displayed.
+
+EXAMPLES
+--------
+
+       $ git ls-remote --tags ./.
+       d6602ec5194c87b0fc87103ca4d67251c76f233a        refs/tags/v0.99
+       f25a265a342aed6041ab0cc484224d9ca54b6f41        refs/tags/v0.99.1
+       7ceca275d047c90c0c7d5afb13ab97efdf51bd6e        refs/tags/v0.99.3
+       c5db5456ae3b0873fc659c19fafdde22313cc441        refs/tags/v0.99.2
+       0918385dbd9656cab0d1d81ba7453d49bbc16250        refs/tags/junio-gpg-pub
+       $ git ls-remote http://www.kernel.org/pub/scm/git/git.git master pu rc
+       5fe978a5381f1fbad26a80e682ddd2a401966740        refs/heads/master
+       c781a84b5204fb294c9ccc79f8b3baceeb32c061        refs/heads/pu
+       b1d096f2926c4e37c9c0b6a7bf2119bedaa277cb        refs/heads/rc
+       $ echo http://www.kernel.org/pub/scm/git/git.git >.git/branches/public
+       $ git ls-remote --tags public v\*
+       d6602ec5194c87b0fc87103ca4d67251c76f233a        refs/tags/v0.99
+       f25a265a342aed6041ab0cc484224d9ca54b6f41        refs/tags/v0.99.1
+       c5db5456ae3b0873fc659c19fafdde22313cc441        refs/tags/v0.99.2
+       7ceca275d047c90c0c7d5afb13ab97efdf51bd6e        refs/tags/v0.99.3
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-ls-tree.txt b/Documentation/git-ls-tree.txt
new file mode 100644 (file)
index 0000000..7b78599
--- /dev/null
@@ -0,0 +1,94 @@
+git-ls-tree(1)
+==============
+
+NAME
+----
+git-ls-tree - List the contents of a tree object
+
+
+SYNOPSIS
+--------
+[verse]
+'git-ls-tree' [-d] [-r] [-t] [-l] [-z]
+           [--name-only] [--name-status] [--full-name] [--abbrev=[<n>]]
+           <tree-ish> [paths...]
+
+DESCRIPTION
+-----------
+Lists the contents of a given tree object, like what "/bin/ls -a" does
+in the current working directory. Note that the usage is subtly different,
+though - 'paths' denote just a list of patterns to match, e.g. so specifying
+directory name (without '-r') will behave differently, and order of the
+arguments does not matter.
+
+OPTIONS
+-------
+<tree-ish>::
+       Id of a tree-ish.
+
+-d::
+       Show only the named tree entry itself, not its children.
+
+-r::
+       Recurse into sub-trees.
+
+-t::
+       Show tree entries even when going to recurse them. Has no effect
+       if '-r' was not passed. '-d' implies '-t'.
+
+-l::
+--long::
+       Show object size of blob (file) entries.
+
+-z::
+       \0 line termination on output.
+
+--name-only::
+--name-status::
+       List only filenames (instead of the "long" output), one per line.
+
+--abbrev[=<n>]::
+       Instead of showing the full 40-byte hexadecimal object
+       lines, show only handful hexdigits prefix.
+       Non default number of digits can be specified with --abbrev=<n>.
+
+--full-name::
+       Instead of showing the path names relative to the current working
+       directory, show the full path names.
+
+paths::
+       When paths are given, show them (note that this isn't really raw
+       pathnames, but rather a list of patterns to match).  Otherwise
+       implicitly uses the root level of the tree as the sole path argument.
+
+
+Output Format
+-------------
+        <mode> SP <type> SP <object> TAB <file>
+
+When the `-z` option is not used, TAB, LF, and backslash characters
+in pathnames are represented as `\t`, `\n`, and `\\`, respectively.
+
+When the `-l` option is used, format changes to
+
+        <mode> SP <type> SP <object> SP <object size> TAB <file>
+
+Object size identified by <object> is given in bytes, and right-justified
+with minimum width of 7 characters.  Object size is given only for blobs
+(file) entries; for other entries `-` character is used in place of size.
+
+
+Author
+------
+Written by Petr Baudis <pasky@suse.cz>
+Completely rewritten from scratch by Junio C Hamano <junkio@cox.net>,
+another major rewrite by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list
+<git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-mailinfo.txt b/Documentation/git-mailinfo.txt
new file mode 100644 (file)
index 0000000..64aa6a1
--- /dev/null
@@ -0,0 +1,69 @@
+git-mailinfo(1)
+===============
+
+NAME
+----
+git-mailinfo - Extracts patch and authorship from a single e-mail message
+
+
+SYNOPSIS
+--------
+'git-mailinfo' [-k] [-u | --encoding=<encoding>] <msg> <patch>
+
+
+DESCRIPTION
+-----------
+Reading a single e-mail message from the standard input, and
+writes the commit log message in <msg> file, and the patches in
+<patch> file.  The author name, e-mail and e-mail subject are
+written out to the standard output to be used by git-am
+to create a commit.  It is usually not necessary to use this
+command directly.  See gitlink:git-am[1] instead.
+
+
+OPTIONS
+-------
+-k::
+       Usually the program 'cleans up' the Subject: header line
+       to extract the title line for the commit log message,
+       among which (1) remove 'Re:' or 're:', (2) leading
+       whitespaces, (3) '[' up to ']', typically '[PATCH]', and
+       then prepends "[PATCH] ".  This flag forbids this
+       munging, and is most useful when used to read back 'git
+       format-patch -k' output.
+
+-u::
+       The commit log message, author name and author email are
+       taken from the e-mail, and after minimally decoding MIME
+       transfer encoding, re-coded in UTF-8 by transliterating
+       them.  This used to be optional but now it is the default.
++
+Note that the patch is always used as-is without charset
+conversion, even with this flag.
+
+--encoding=<encoding>::
+       Similar to -u but if the local convention is different
+       from what is specified by i18n.commitencoding, this flag
+       can be used to override it.
+
+<msg>::
+       The commit log message extracted from e-mail, usually
+       except the title line which comes from e-mail Subject.
+
+<patch>::
+       The patch extracted from e-mail.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org> and
+Junio C Hamano <junkio@cox.net>
+
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-mailsplit.txt b/Documentation/git-mailsplit.txt
new file mode 100644 (file)
index 0000000..c4f4cab
--- /dev/null
@@ -0,0 +1,58 @@
+git-mailsplit(1)
+================
+
+NAME
+----
+git-mailsplit - Simple UNIX mbox splitter program
+
+SYNOPSIS
+--------
+'git-mailsplit' [-b] [-f<nn>] [-d<prec>] -o<directory> [--] [<mbox>|<Maildir>...]
+
+DESCRIPTION
+-----------
+Splits a mbox file or a Maildir into a list of files: "0001" "0002" ..  in the
+specified directory so you can process them further from there.
+
+IMPORTANT: Maildir splitting relies upon filenames being sorted to output
+patches in the correct order.
+
+OPTIONS
+-------
+<mbox>::
+       Mbox file to split.  If not given, the mbox is read from
+       the standard input.
+
+<Maildir>::
+       Root of the Maildir to split. This directory should contain the cur, tmp
+       and new subdirectories.
+
+<directory>::
+       Directory in which to place the individual messages.
+
+-b::
+       If any file doesn't begin with a From line, assume it is a
+       single mail message instead of signaling error.
+
+-d<prec>::
+       Instead of the default 4 digits with leading zeros,
+       different precision can be specified for the generated
+       filenames.
+
+-f<nn>::
+       Skip the first <nn> numbers, for example if -f3 is specified,
+       start the numbering with 0004.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+and Junio C Hamano <junkio@cox.net>
+
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-merge-base.txt b/Documentation/git-merge-base.txt
new file mode 100644 (file)
index 0000000..6b71880
--- /dev/null
@@ -0,0 +1,42 @@
+git-merge-base(1)
+=================
+
+NAME
+----
+git-merge-base - Find as good common ancestors as possible for a merge
+
+
+SYNOPSIS
+--------
+'git-merge-base' [--all] <commit> <commit>
+
+DESCRIPTION
+-----------
+
+"git-merge-base" finds as good a common ancestor as possible between
+the two commits. That is, given two commits A and B 'git-merge-base A
+B' will output a commit which is reachable from both A and B through
+the parent relationship.
+
+Given a selection of equally good common ancestors it should not be
+relied on to decide in any particular way.
+
+The "git-merge-base" algorithm is still in flux - use the source...
+
+OPTIONS
+-------
+--all::
+       Output all common ancestors for the two commits instead of
+       just one.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-merge-file.txt b/Documentation/git-merge-file.txt
new file mode 100644 (file)
index 0000000..31882ab
--- /dev/null
@@ -0,0 +1,92 @@
+git-merge-file(1)
+=================
+
+NAME
+----
+git-merge-file - Run a three-way file merge
+
+
+SYNOPSIS
+--------
+[verse]
+'git-merge-file' [-L <current-name> [-L <base-name> [-L <other-name>]]]
+       [-p|--stdout] [-q|--quiet] <current-file> <base-file> <other-file>
+
+
+DESCRIPTION
+-----------
+git-file-merge incorporates all changes that lead from the `<base-file>`
+to `<other-file>` into `<current-file>`. The result ordinarily goes into
+`<current-file>`. git-merge-file is useful for combining separate changes
+to an original. Suppose `<base-file>` is the original, and both
+`<current-file>` and `<other-file>` are modifications of `<base-file>`.
+Then git-merge-file combines both changes.
+
+A conflict occurs if both `<current-file>` and `<other-file>` have changes
+in a common segment of lines. If a conflict is found, git-merge-file
+normally outputs a warning and brackets the conflict with <<<<<<< and
+>>>>>>> lines. A typical conflict will look like this:
+
+       <<<<<<< A
+       lines in file A
+       =======
+       lines in file B
+       >>>>>>> B
+
+If there are conflicts, the user should edit the result and delete one of
+the alternatives.
+
+The exit value of this program is negative on error, and the number of
+conflicts otherwise. If the merge was clean, the exit value is 0.
+
+git-merge-file is designed to be a minimal clone of RCS merge, that is, it
+implements all of RCS merge's functionality which is needed by
+gitlink:git[1].
+
+
+OPTIONS
+-------
+
+-L <label>::
+       This option may be given up to three times, and
+       specifies labels to be used in place of the
+       corresponding file names in conflict reports. That is,
+       `git-merge-file -L x -L y -L z a b c` generates output that
+       looks like it came from files x, y and z instead of
+       from files a, b and c.
+
+-p::
+       Send results to standard output instead of overwriting
+       `<current-file>`.
+
+-q::
+       Quiet;  do  not  warn about conflicts.
+
+
+EXAMPLES
+--------
+
+git merge-file README.my README README.upstream::
+
+       combines the changes of README.my and README.upstream since README,
+       tries to merge them and writes the result into README.my.
+
+git merge-file -L a -L b -L c tmp/a123 tmp/b234 tmp/c345::
+
+       merges tmp/a123 and tmp/c345 with the base tmp/b234, but uses labels
+       `a` and `c` instead of `tmp/a123` and `tmp/c345`.
+
+
+Author
+------
+Written by Johannes Schindelin <johannes.schindelin@gmx.de>
+
+
+Documentation
+--------------
+Documentation by Johannes Schindelin and the git-list <git@vger.kernel.org>,
+with parts copied from the original documentation of RCS merge.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-merge-index.txt b/Documentation/git-merge-index.txt
new file mode 100644 (file)
index 0000000..b726ddf
--- /dev/null
@@ -0,0 +1,87 @@
+git-merge-index(1)
+==================
+
+NAME
+----
+git-merge-index - Run a merge for files needing merging
+
+
+SYNOPSIS
+--------
+'git-merge-index' [-o] [-q] <merge-program> (-a | \-- | <file>\*)
+
+DESCRIPTION
+-----------
+This looks up the <file>(s) in the index and, if there are any merge
+entries, passes the SHA1 hash for those files as arguments 1, 2, 3 (empty
+argument if no file), and <file> as argument 4.  File modes for the three
+files are passed as arguments 5, 6 and 7.
+
+OPTIONS
+-------
+\--::
+       Do not interpret any more arguments as options.
+
+-a::
+       Run merge against all files in the index that need merging.
+
+-o::
+       Instead of stopping at the first failed merge, do all of them
+       in one shot - continue with merging even when previous merges
+       returned errors, and only return the error code after all the
+       merges are over.
+
+-q::
+       Do not complain about failed merge program (the merge program
+       failure usually indicates conflicts during merge). This is for
+       porcelains which might want to emit custom messages.
+
+If "git-merge-index" is called with multiple <file>s (or -a) then it
+processes them in turn only stopping if merge returns a non-zero exit
+code.
+
+Typically this is run with a script calling git's imitation of
+the merge command from the RCS package.
+
+A sample script called "git-merge-one-file" is included in the
+distribution.
+
+ALERT ALERT ALERT! The git "merge object order" is different from the
+RCS "merge" program merge object order. In the above ordering, the
+original is first. But the argument order to the 3-way merge program
+"merge" is to have the original in the middle. Don't ask me why.
+
+Examples:
+
+  torvalds@ppc970:~/merge-test> git-merge-index cat MM
+  This is MM from the original tree.                   # original
+  This is modified MM in the branch A.                 # merge1
+  This is modified MM in the branch B.                 # merge2
+  This is modified MM in the branch B.                 # current contents
+
+or
+
+  torvalds@ppc970:~/merge-test> git-merge-index cat AA MM
+  cat: : No such file or directory
+  This is added AA in the branch A.
+  This is added AA in the branch B.
+  This is added AA in the branch B.
+  fatal: merge program failed
+
+where the latter example shows how "git-merge-index" will stop trying to
+merge once anything has returned an error (i.e., "cat" returned an error
+for the AA file, because it didn't exist in the original, and thus
+"git-merge-index" didn't even try to merge the MM thing).
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+One-shot merge by Petr Baudis <pasky@ucw.cz>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-merge-one-file.txt b/Documentation/git-merge-one-file.txt
new file mode 100644 (file)
index 0000000..f35d0e1
--- /dev/null
@@ -0,0 +1,29 @@
+git-merge-one-file(1)
+=====================
+
+NAME
+----
+git-merge-one-file - The standard helper program to use with git-merge-index
+
+
+SYNOPSIS
+--------
+'git-merge-one-file'
+
+DESCRIPTION
+-----------
+This is the standard helper program to use with "git-merge-index"
+to resolve a merge after the trivial merge done with "git-read-tree -m".
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>,
+Junio C Hamano <junkio@cox.net> and Petr Baudis <pasky@suse.cz>.
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-merge-tree.txt b/Documentation/git-merge-tree.txt
new file mode 100644 (file)
index 0000000..6892fda
--- /dev/null
@@ -0,0 +1,36 @@
+git-merge-tree(1)
+=================
+
+NAME
+----
+git-merge-tree - Show three-way merge without touching index
+
+
+SYNOPSIS
+--------
+'git-merge-tree' <base-tree> <branch1> <branch2>
+
+DESCRIPTION
+-----------
+Reads three treeish, and output trivial merge results and
+conflicting stages to the standard output.  This is similar to
+what three-way read-tree -m does, but instead of storing the
+results in the index, the command outputs the entries to the
+standard output.
+
+This is meant to be used by higher level scripts to compute
+merge results outside index, and stuff the results back into the
+index.  For this reason, the output from the command omits
+entries that match <branch1> tree.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt
new file mode 100644 (file)
index 0000000..eabd7ef
--- /dev/null
@@ -0,0 +1,180 @@
+git-merge(1)
+============
+
+NAME
+----
+git-merge - Join two or more development histories together
+
+
+SYNOPSIS
+--------
+[verse]
+'git-merge' [-n] [--summary] [--no-commit] [--squash] [-s <strategy>]...
+       [-m <msg>] <remote> <remote>...
+'git-merge' <msg> HEAD <remote>...
+
+DESCRIPTION
+-----------
+This is the top-level interface to the merge machinery
+which drives multiple merge strategy scripts.
+
+The second syntax (<msg> `HEAD` <remote>) is supported for
+historical reasons.  Do not use it from the command line or in
+new scripts.  It is the same as `git merge -m <msg> <remote>`.
+
+
+OPTIONS
+-------
+include::merge-options.txt[]
+
+-m <msg>::
+       The commit message to be used for the merge commit (in case
+       it is created). The `git-fmt-merge-msg` script can be used
+       to give a good default for automated `git-merge` invocations.
+
+<remote>::
+       Other branch head merged into our branch.  You need at
+       least one <remote>.  Specifying more than one <remote>
+       obviously means you are trying an Octopus.
+
+include::merge-strategies.txt[]
+
+
+If you tried a merge which resulted in a complex conflicts and
+would want to start over, you can recover with
+gitlink:git-reset[1].
+
+CONFIGURATION
+-------------
+
+merge.summary::
+       Whether to include summaries of merged commits in newly
+       created merge commit. False by default.
+
+merge.verbosity::
+       Controls the amount of output shown by the recursive merge
+       strategy.  Level 0 outputs nothing except a final error
+       message if conflicts were detected. Level 1 outputs only
+       conflicts, 2 outputs conflicts and file changes.  Level 5 and
+       above outputs debugging information.  The default is level 2.
+       Can be overridden by 'GIT_MERGE_VERBOSITY' environment variable.
+
+branch.<name>.mergeoptions::
+       Sets default options for merging into branch <name>. The syntax and
+       supported options are equal to that of git-merge, but option values
+       containing whitespace characters are currently not supported.
+
+HOW MERGE WORKS
+---------------
+
+A merge is always between the current `HEAD` and one or more
+remote branch heads, and the index file must exactly match the
+tree of `HEAD` commit (i.e. the contents of the last commit) when
+it happens.  In other words, `git-diff --cached HEAD` must
+report no changes.
+
+[NOTE]
+This is a bit of lie.  In certain special cases, your index are
+allowed to be different from the tree of `HEAD` commit.  The most
+notable case is when your `HEAD` commit is already ahead of what
+is being merged, in which case your index can have arbitrary
+difference from your `HEAD` commit.  Otherwise, your index entries
+are allowed have differences from your `HEAD` commit that match
+the result of trivial merge (e.g. you received the same patch
+from external source to produce the same result as what you are
+merging).  For example, if a path did not exist in the common
+ancestor and your head commit but exists in the tree you are
+merging into your repository, and if you already happen to have
+that path exactly in your index, the merge does not have to
+fail.
+
+Otherwise, merge will refuse to do any harm to your repository
+(that is, it may fetch the objects from remote, and it may even
+update the local branch used to keep track of the remote branch
+with `git pull remote rbranch:lbranch`, but your working tree,
+`.git/HEAD` pointer and index file are left intact).
+
+You may have local modifications in the working tree files.  In
+other words, `git-diff` is allowed to report changes.
+However, the merge uses your working tree as the working area,
+and in order to prevent the merge operation from losing such
+changes, it makes sure that they do not interfere with the
+merge. Those complex tables in read-tree documentation define
+what it means for a path to "interfere with the merge".  And if
+your local modifications interfere with the merge, again, it
+stops before touching anything.
+
+So in the above two "failed merge" case, you do not have to
+worry about loss of data --- you simply were not ready to do
+a merge, so no merge happened at all.  You may want to finish
+whatever you were in the middle of doing, and retry the same
+pull after you are done and ready.
+
+When things cleanly merge, these things happen:
+
+1. The results are updated both in the index file and in your
+   working tree;
+2. Index file is written out as a tree;
+3. The tree gets committed; and
+4. The `HEAD` pointer gets advanced.
+
+Because of 2., we require that the original state of the index
+file to match exactly the current `HEAD` commit; otherwise we
+will write out your local changes already registered in your
+index file along with the merge result, which is not good.
+Because 1. involves only the paths different between your
+branch and the remote branch you are pulling from during the
+merge (which is typically a fraction of the whole tree), you can
+have local modifications in your working tree as long as they do
+not overlap with what the merge updates.
+
+When there are conflicts, these things happen:
+
+1. `HEAD` stays the same.
+
+2. Cleanly merged paths are updated both in the index file and
+   in your working tree.
+
+3. For conflicting paths, the index file records up to three
+   versions; stage1 stores the version from the common ancestor,
+   stage2 from `HEAD`, and stage3 from the remote branch (you
+   can inspect the stages with `git-ls-files -u`).  The working
+   tree files have the result of "merge" program; i.e. 3-way
+   merge result with familiar conflict markers `<<< === >>>`.
+
+4. No other changes are done.  In particular, the local
+   modifications you had before you started merge will stay the
+   same and the index entries for them stay as they were,
+   i.e. matching `HEAD`.
+
+After seeing a conflict, you can do two things:
+
+ * Decide not to merge.  The only clean-up you need are to reset
+   the index file to the `HEAD` commit to reverse 2. and to clean
+   up working tree changes made by 2. and 3.; `git-reset` can
+   be used for this.
+
+ * Resolve the conflicts.  `git-diff` would report only the
+   conflicting paths because of the above 2. and 3..  Edit the
+   working tree files into a desirable shape, `git-add` or `git-rm`
+   them, to make the index file contain what the merge result
+   should be, and run `git-commit` to commit the result.
+
+
+SEE ALSO
+--------
+gitlink:git-fmt-merge-msg[1], gitlink:git-pull[1]
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-mergetool.txt b/Documentation/git-mergetool.txt
new file mode 100644 (file)
index 0000000..a26c260
--- /dev/null
@@ -0,0 +1,51 @@
+git-mergetool(1)
+================
+
+NAME
+----
+git-mergetool - Run merge conflict resolution tools to resolve merge conflicts
+
+SYNOPSIS
+--------
+'git-mergetool' [--tool=<tool>] [<file>]...
+
+DESCRIPTION
+-----------
+
+Use 'git mergetool' to run one of several merge utilities to resolve
+merge conflicts.  It is typically run after gitlink:git-merge[1].
+
+If one or more <file> parameters are given, the merge tool program will
+be run to resolve differences on each file.  If no <file> names are
+specified, 'git mergetool' will run the merge tool program on every file
+with merge conflicts.
+
+OPTIONS
+-------
+-t or --tool=<tool>::
+       Use the merge resolution program specified by <tool>.
+       Valid merge tools are:
+       kdiff3, tkdiff, meld, xxdiff, emerge, vimdiff, gvimdiff, ecmerge, and opendiff
++
+If a merge resolution program is not specified, 'git mergetool'
+will use the configuration variable merge.tool.  If the
+configuration variable merge.tool is not set, 'git mergetool'
+will pick a suitable default.
++
+You can explicitly provide a full path to the tool by setting the
+configuration variable mergetool.<tool>.path. For example, you
+can configure the absolute path to kdiff3 by setting
+mergetool.kdiff3.path. Otherwise, 'git mergetool' assumes the tool
+is available in PATH.
+
+Author
+------
+Written by Theodore Y Ts'o <tytso@mit.edu>
+
+Documentation
+--------------
+Documentation by Theodore Y Ts'o.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-mktag.txt b/Documentation/git-mktag.txt
new file mode 100644 (file)
index 0000000..ea7a752
--- /dev/null
@@ -0,0 +1,46 @@
+git-mktag(1)
+============
+
+NAME
+----
+git-mktag - Creates a tag object
+
+
+SYNOPSIS
+--------
+'git-mktag' < signature_file
+
+DESCRIPTION
+-----------
+Reads a tag contents on standard input and creates a tag object
+that can also be used to sign other objects.
+
+The output is the new tag's <object> identifier.
+
+Tag Format
+----------
+A tag signature file has a very simple fixed format: four lines of
+
+  object <sha1>
+  type <typename>
+  tag <tagname>
+  tagger <tagger>
+
+followed by some 'optional' free-form message (some tags created
+by older git may not have `tagger` line).  The message, when
+exists, is separated by a blank line from the header.  The
+message part may contain a signature that git itself doesn't
+care about, but that can be verified with gpg.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-mktree.txt b/Documentation/git-mktree.txt
new file mode 100644 (file)
index 0000000..638abc7
--- /dev/null
@@ -0,0 +1,34 @@
+git-mktree(1)
+=============
+
+NAME
+----
+git-mktree - Build a tree-object from ls-tree formatted text
+
+
+SYNOPSIS
+--------
+'git-mktree' [-z]
+
+DESCRIPTION
+-----------
+Reads standard input in non-recursive `ls-tree` output format,
+and creates a tree object.  The object name of the tree object
+built is written to the standard output.
+
+OPTIONS
+-------
+-z::
+       Read the NUL-terminated `ls-tree -z` output instead.
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-mv.txt b/Documentation/git-mv.txt
new file mode 100644 (file)
index 0000000..3b8ca76
--- /dev/null
@@ -0,0 +1,53 @@
+git-mv(1)
+=========
+
+NAME
+----
+git-mv - Move or rename a file, a directory, or a symlink
+
+
+SYNOPSIS
+--------
+'git-mv' <options>... <args>...
+
+DESCRIPTION
+-----------
+This script is used to move or rename a file, directory or symlink.
+
+ git-mv [-f] [-n] <source> <destination>
+ git-mv [-f] [-n] [-k] <source> ... <destination directory>
+
+In the first form, it renames <source>, which must exist and be either
+a file, symlink or directory, to <destination>.
+In the second form, the last argument has to be an existing
+directory; the given sources will be moved into this directory.
+
+The index is updated after successful completion, but the change must still be
+committed.
+
+OPTIONS
+-------
+-f::
+       Force renaming or moving of a file even if the target exists
+-k::
+        Skip move or rename actions which would lead to an error
+       condition. An error happens when a source is neither existing nor
+        controlled by GIT, or when it would overwrite an existing
+        file unless '-f' is given.
+-n, \--dry-run::
+       Do nothing; only show what would happen
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+Rewritten by Ryan Anderson <ryan@michonline.com>
+Move functionality added by Josef Weidendorfer <Josef.Weidendorfer@gmx.de>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-name-rev.txt b/Documentation/git-name-rev.txt
new file mode 100644 (file)
index 0000000..306e1a4
--- /dev/null
@@ -0,0 +1,78 @@
+git-name-rev(1)
+===============
+
+NAME
+----
+git-name-rev - Find symbolic names for given revs
+
+
+SYNOPSIS
+--------
+[verse]
+'git-name-rev' [--tags] [--refs=<pattern>]
+              ( --all | --stdin | <committish>... )
+
+DESCRIPTION
+-----------
+Finds symbolic names suitable for human digestion for revisions given in any
+format parsable by git-rev-parse.
+
+
+OPTIONS
+-------
+
+--tags::
+       Do not use branch names, but only tags to name the commits
+
+--refs=<pattern>::
+       Only use refs whose names match a given shell pattern.
+
+--all::
+       List all commits reachable from all refs
+
+--stdin::
+       Read from stdin, append "(<rev_name>)" to all sha1's of nameable
+       commits, and pass to stdout
+
+--name-only::
+       Instead of printing both the SHA-1 and the name, print only
+       the name.  If given with --tags the usual tag prefix of
+       "tags/" is also omitted from the name, matching the output
+       of gitlink::git-describe[1] more closely.  This option
+       cannot be combined with --stdin.
+
+EXAMPLE
+-------
+
+Given a commit, find out where it is relative to the local refs. Say somebody
+wrote you about that fantastic commit 33db5f4d9027a10e477ccf054b2c1ab94f74c85a.
+Of course, you look into the commit, but that only tells you what happened, but
+not the context.
+
+Enter git-name-rev:
+
+------------
+% git name-rev 33db5f4d9027a10e477ccf054b2c1ab94f74c85a
+33db5f4d9027a10e477ccf054b2c1ab94f74c85a tags/v0.99^0~940
+------------
+
+Now you are wiser, because you know that it happened 940 revisions before v0.99.
+
+Another nice thing you can do is:
+
+------------
+% git log | git name-rev --stdin
+------------
+
+
+Author
+------
+Written by Johannes Schindelin <Johannes.Schindelin@gmx.de>
+
+Documentation
+--------------
+Documentation by Johannes Schindelin.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt
new file mode 100644 (file)
index 0000000..5237ab0
--- /dev/null
@@ -0,0 +1,202 @@
+git-pack-objects(1)
+===================
+
+NAME
+----
+git-pack-objects - Create a packed archive of objects
+
+
+SYNOPSIS
+--------
+[verse]
+'git-pack-objects' [-q] [--no-reuse-delta] [--delta-base-offset] [--non-empty]
+       [--local] [--incremental] [--window=N] [--depth=N] [--all-progress]
+       [--revs [--unpacked | --all]*] [--stdout | base-name] < object-list
+
+
+DESCRIPTION
+-----------
+Reads list of objects from the standard input, and writes a packed
+archive with specified base-name, or to the standard output.
+
+A packed archive is an efficient way to transfer set of objects
+between two repositories, and also is an archival format which
+is efficient to access.  The packed archive format (.pack) is
+designed to be unpackable without having anything else, but for
+random access, accompanied with the pack index file (.idx).
+
+Placing both in the pack/ subdirectory of $GIT_OBJECT_DIRECTORY (or
+any of the directories on $GIT_ALTERNATE_OBJECT_DIRECTORIES)
+enables git to read from such an archive.
+
+'git-unpack-objects' command can read the packed archive and
+expand the objects contained in the pack into "one-file
+one-object" format; this is typically done by the smart-pull
+commands when a pack is created on-the-fly for efficient network
+transport by their peers.
+
+In a packed archive, an object is either stored as a compressed
+whole, or as a difference from some other object.  The latter is
+often called a delta.
+
+
+OPTIONS
+-------
+base-name::
+       Write into a pair of files (.pack and .idx), using
+       <base-name> to determine the name of the created file.
+       When this option is used, the two files are written in
+       <base-name>-<SHA1>.{pack,idx} files.  <SHA1> is a hash
+       of the sorted object names to make the resulting filename
+       based on the pack content, and written to the standard
+       output of the command.
+
+--stdout::
+       Write the pack contents (what would have been written to
+       .pack file) out to the standard output.
+
+--revs::
+       Read the revision arguments from the standard input, instead of
+       individual object names.  The revision arguments are processed
+       the same way as gitlink:git-rev-list[1] with `--objects` flag
+       uses its `commit` arguments to build the list of objects it
+       outputs.  The objects on the resulting list are packed.
+
+--unpacked::
+       This implies `--revs`.  When processing the list of
+       revision arguments read from the standard input, limit
+       the objects packed to those that are not already packed.
+
+--all::
+       This implies `--revs`.  In addition to the list of
+       revision arguments read from the standard input, pretend
+       as if all refs under `$GIT_DIR/refs` are specified to be
+       included.
+
+--window=[N], --depth=[N]::
+       These two options affect how the objects contained in
+       the pack are stored using delta compression.  The
+       objects are first internally sorted by type, size and
+       optionally names and compared against the other objects
+       within --window to see if using delta compression saves
+       space.  --depth limits the maximum delta depth; making
+       it too deep affects the performance on the unpacker
+       side, because delta data needs to be applied that many
+       times to get to the necessary object.
+       The default value for --window is 10 and --depth is 50.
+
+--window-memory=[N]::
+       This option provides an additional limit on top of `--window`;
+       the window size will dynamically scale down so as to not take
+       up more than N bytes in memory.  This is useful in
+       repositories with a mix of large and small objects to not run
+       out of memory with a large window, but still be able to take
+       advantage of the large window for the smaller objects.  The
+       size can be suffixed with "k", "m", or "g".
+       `--window-memory=0` makes memory usage unlimited, which is the
+       default.
+
+--max-pack-size=<n>::
+       Maximum size of each output packfile, expressed in MiB.
+       If specified,  multiple packfiles may be created.
+       The default is unlimited.
+
+--incremental::
+       This flag causes an object already in a pack ignored
+       even if it appears in the standard input.
+
+--local::
+       This flag is similar to `--incremental`; instead of
+       ignoring all packed objects, it only ignores objects
+       that are packed and not in the local object store
+       (i.e. borrowed from an alternate).
+
+--non-empty::
+        Only create a packed archive if it would contain at
+        least one object.
+
+--progress::
+       Progress status is reported on the standard error stream
+       by default when it is attached to a terminal, unless -q
+       is specified. This flag forces progress status even if
+       the standard error stream is not directed to a terminal.
+
+--all-progress::
+       When --stdout is specified then progress report is
+       displayed during the object count and deltification phases
+       but inhibited during the write-out phase. The reason is
+       that in some cases the output stream is directly linked
+       to another command which may wish to display progress
+       status of its own as it processes incoming pack data.
+       This flag is like --progress except that it forces progress
+       report for the write-out phase as well even if --stdout is
+       used.
+
+-q::
+       This flag makes the command not to report its progress
+       on the standard error stream.
+
+--no-reuse-delta::
+       When creating a packed archive in a repository that
+       has existing packs, the command reuses existing deltas.
+       This sometimes results in a slightly suboptimal pack.
+       This flag tells the command not to reuse existing deltas
+       but compute them from scratch.
+
+--no-reuse-object::
+       This flag tells the command not to reuse existing object data at all,
+       including non deltified object, forcing recompression of everything.
+       This implies --no-reuse-delta. Useful only in the obscure case where
+       wholesale enforcement of a different compression level on the
+       packed data is desired.
+
+--compression=[N]::
+       Specifies compression level for newly-compressed data in the
+       generated pack.  If not specified,  pack compression level is
+       determined first by pack.compression,  then by core.compression,
+       and defaults to -1,  the zlib default,  if neither is set.
+       Add \--no-reuse-object if you want to force a uniform compression
+       level on all data no matter the source.
+
+--delta-base-offset::
+       A packed archive can express base object of a delta as
+       either 20-byte object name or as an offset in the
+       stream, but older version of git does not understand the
+       latter.  By default, git-pack-objects only uses the
+       former format for better compatibility.  This option
+       allows the command to use the latter format for
+       compactness.  Depending on the average delta chain
+       length, this option typically shrinks the resulting
+       packfile by 3-5 per-cent.
+
+--threads=<n>::
+       Specifies the number of threads to spawn when searching for best
+       delta matches.  This requires that pack-objects be compiled with
+       pthreads otherwise this option is ignored with a warning.
+       This is meant to reduce packing time on multiprocessor machines.
+       The required amount of memory for the delta search window is
+       however multiplied by the number of threads.
+
+--index-version=<version>[,<offset>]::
+       This is intended to be used by the test suite only. It allows
+       to force the version for the generated pack index, and to force
+       64-bit index entries on objects located above the given offset.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+-------------
+Documentation by Junio C Hamano
+
+See Also
+--------
+gitlink:git-rev-list[1]
+gitlink:git-repack[1]
+gitlink:git-prune-packed[1]
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-pack-redundant.txt b/Documentation/git-pack-redundant.txt
new file mode 100644 (file)
index 0000000..f2ceeba
--- /dev/null
@@ -0,0 +1,57 @@
+git-pack-redundant(1)
+=====================
+
+NAME
+----
+git-pack-redundant - Find redundant pack files
+
+
+SYNOPSIS
+--------
+'git-pack-redundant' [ --verbose ] [ --alt-odb ] < --all | .pack filename ... >
+
+DESCRIPTION
+-----------
+This program computes which packs in your repository
+are redundant. The output is suitable for piping to
+'xargs rm' if you are in the root of the repository.
+
+git-pack-redundant accepts a list of objects on standard input. Any objects
+given will be ignored when checking which packs are required. This makes the
+following command useful when wanting to remove packs which contain unreachable
+objects.
+
+git-fsck --full --unreachable | cut -d ' ' -f3 | \
+git-pack-redundant --all | xargs rm
+
+OPTIONS
+-------
+
+
+--all::
+       Processes all packs. Any filenames on the command line are ignored.
+
+--alt-odb::
+       Don't require objects present in packs from alternate object
+       directories to be present in local packs.
+
+--verbose::
+       Outputs some statistics to stderr. Has a small performance penalty.
+
+Author
+------
+Written by Lukas Sandström <lukass@etek.chalmers.se>
+
+Documentation
+--------------
+Documentation by Lukas Sandström <lukass@etek.chalmers.se>
+
+See Also
+--------
+gitlink:git-pack-objects[1]
+gitlink:git-repack[1]
+gitlink:git-prune-packed[1]
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-pack-refs.txt b/Documentation/git-pack-refs.txt
new file mode 100644 (file)
index 0000000..a20fc7d
--- /dev/null
@@ -0,0 +1,66 @@
+git-pack-refs(1)
+================
+
+NAME
+----
+git-pack-refs - Pack heads and tags for efficient repository access
+
+SYNOPSIS
+--------
+'git-pack-refs' [--all] [--no-prune]
+
+DESCRIPTION
+-----------
+
+Traditionally, tips of branches and tags (collectively known as
+'refs') were stored one file per ref under `$GIT_DIR/refs`
+directory.  While many branch tips tend to be updated often,
+most tags and some branch tips are never updated.  When a
+repository has hundreds or thousands of tags, this
+one-file-per-ref format both wastes storage and hurts
+performance.
+
+This command is used to solve the storage and performance
+problem by stashing the refs in a single file,
+`$GIT_DIR/packed-refs`.  When a ref is missing from the
+traditional `$GIT_DIR/refs` hierarchy, it is looked up in this
+file and used if found.
+
+Subsequent updates to branches always creates new file under
+`$GIT_DIR/refs` hierarchy.
+
+A recommended practice to deal with a repository with too many
+refs is to pack its refs with `--all --prune` once, and
+occasionally run `git-pack-refs \--prune`.  Tags are by
+definition stationary and are not expected to change.  Branch
+heads will be packed with the initial `pack-refs --all`, but
+only the currently active branch heads will become unpacked,
+and next `pack-refs` (without `--all`) will leave them
+unpacked.
+
+
+OPTIONS
+-------
+
+\--all::
+
+The command by default packs all tags and refs that are already
+packed, and leaves other refs
+alone.  This is because branches are expected to be actively
+developed and packing their tips does not help performance.
+This option causes branch tips to be packed as well.  Useful for
+a repository with many branches of historical interests.
+
+\--no-prune::
+
+The command usually removes loose refs under `$GIT_DIR/refs`
+hierarchy after packing them.  This option tells it not to.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-parse-remote.txt b/Documentation/git-parse-remote.txt
new file mode 100644 (file)
index 0000000..11b1f4d
--- /dev/null
@@ -0,0 +1,50 @@
+git-parse-remote(1)
+===================
+
+NAME
+----
+git-parse-remote - Routines to help parsing remote repository access parameters
+
+
+SYNOPSIS
+--------
+'. git-parse-remote'
+
+DESCRIPTION
+-----------
+This script is included in various scripts to supply
+routines to parse files under $GIT_DIR/remotes/ and
+$GIT_DIR/branches/ and configuration variables that are related
+to fetching, pulling and pushing.
+
+The primary entry points are:
+
+get_remote_refs_for_fetch::
+       Given the list of user-supplied `<repo> <refspec>...`,
+       return the list of refs to fetch after canonicalizing
+       them into `$GIT_DIR` relative paths
+       (e.g. `refs/heads/foo`).  When `<refspec>...` is empty
+       the returned list of refs consists of the defaults
+       for the given `<repo>`, if specified in
+       `$GIT_DIR/remotes/`, `$GIT_DIR/branches/`, or `remote.*.fetch`
+       configuration.
+
+get_remote_refs_for_push::
+       Given the list of user-supplied `<repo> <refspec>...`,
+       return the list of refs to push in a form suitable to be
+       fed to the `git-send-pack` command.  When `<refspec>...`
+       is empty the returned list of refs consists of the
+       defaults for the given `<repo>`, if specified in
+       `$GIT_DIR/remotes/`.
+
+Author
+------
+Written by Junio C Hamano.
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-patch-id.txt b/Documentation/git-patch-id.txt
new file mode 100644 (file)
index 0000000..ad528a9
--- /dev/null
@@ -0,0 +1,42 @@
+git-patch-id(1)
+===============
+
+NAME
+----
+git-patch-id - Compute unique ID for a patch
+
+SYNOPSIS
+--------
+'git-patch-id' < <patch>
+
+DESCRIPTION
+-----------
+A "patch ID" is nothing but a SHA1 of the diff associated with a patch, with
+whitespace and line numbers ignored.  As such, it's "reasonably stable", but at
+the same time also reasonably unique, i.e., two patches that have the same "patch
+ID" are almost guaranteed to be the same thing.
+
+IOW, you can use this thing to look for likely duplicate commits.
+
+When dealing with git-diff-tree output, it takes advantage of
+the fact that the patch is prefixed with the object name of the
+commit, and outputs two 40-byte hexadecimal string.  The first
+string is the patch ID, and the second string is the commit ID.
+This can be used to make a mapping from patch ID to commit ID.
+
+OPTIONS
+-------
+<patch>::
+       The diff to create the ID of.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-peek-remote.txt b/Documentation/git-peek-remote.txt
new file mode 100644 (file)
index 0000000..38a5325
--- /dev/null
@@ -0,0 +1,53 @@
+git-peek-remote(1)
+==================
+
+NAME
+----
+git-peek-remote - List the references in a remote repository
+
+
+SYNOPSIS
+--------
+'git-peek-remote' [--upload-pack=<git-upload-pack>] [<host>:]<directory>
+
+DESCRIPTION
+-----------
+This command is deprecated; use `git-ls-remote` instead.
+
+OPTIONS
+-------
+\--upload-pack=<git-upload-pack>::
+       Use this to specify the path to 'git-upload-pack' on the
+       remote side, if it is not found on your $PATH. Some
+       installations of sshd ignores the user's environment
+       setup scripts for login shells (e.g. .bash_profile) and
+       your privately installed git may not be found on the system
+       default $PATH.  Another workaround suggested is to set
+       up your $PATH in ".bashrc", but this flag is for people
+       who do not want to pay the overhead for non-interactive
+       shells, but prefer having a lean .bashrc file (they set most of
+       the things up in .bash_profile).
+
+\--exec=<git-upload-pack>::
+       Same \--upload-pack=<git-upload-pack>.
+
+<host>::
+       A remote host that houses the repository.  When this
+       part is specified, 'git-upload-pack' is invoked via
+       ssh.
+
+<directory>::
+       The repository to sync from.
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-prune-packed.txt b/Documentation/git-prune-packed.txt
new file mode 100644 (file)
index 0000000..9f85f38
--- /dev/null
@@ -0,0 +1,52 @@
+git-prune-packed(1)
+=====================
+
+NAME
+----
+git-prune-packed - Remove extra objects that are already in pack files
+
+
+SYNOPSIS
+--------
+'git-prune-packed' [-n] [-q]
+
+
+DESCRIPTION
+-----------
+This program searches the `$GIT_OBJECT_DIR` for all objects that currently
+exist in a pack file as well as the independent object directories.
+
+All such extra objects are removed.
+
+A pack is a collection of objects, individually compressed, with delta
+compression applied, stored in a single file, with an associated index file.
+
+Packs are used to reduce the load on mirror systems, backup engines,
+disk storage, etc.
+
+
+OPTIONS
+-------
+-n::
+        Don't actually remove any objects, only show those that would have been
+        removed.
+
+-q::
+       Squelch the progress indicator.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Ryan Anderson <ryan@michonline.com>
+
+See Also
+--------
+gitlink:git-pack-objects[1]
+gitlink:git-repack[1]
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
new file mode 100644 (file)
index 0000000..9835bdb
--- /dev/null
@@ -0,0 +1,63 @@
+git-prune(1)
+============
+
+NAME
+----
+git-prune - Prune all unreachable objects from the object database
+
+
+SYNOPSIS
+--------
+'git-prune' [-n] [--expire <expire>] [--] [<head>...]
+
+DESCRIPTION
+-----------
+
+This runs `git-fsck --unreachable` using all the refs
+available in `$GIT_DIR/refs`, optionally with additional set of
+objects specified on the command line, and prunes all
+objects unreachable from any of these head objects from the object database.
+In addition, it
+prunes the unpacked objects that are also found in packs by
+running `git prune-packed`.
+
+OPTIONS
+-------
+
+-n::
+       Do not remove anything; just report what it would
+       remove.
+
+\--::
+       Do not interpret any more arguments as options.
+
+\--expire <time>::
+       Only expire loose objects older than <time>.
+
+<head>...::
+       In addition to objects
+       reachable from any of our references, keep objects
+       reachable from listed <head>s.
+
+EXAMPLE
+-------
+
+To prune objects not used by your repository nor another that
+borrows from your repository via its
+`.git/objects/info/alternates`:
+
+------------
+$ git prune $(cd ../another && $(git-rev-parse --all))
+------------
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt
new file mode 100644 (file)
index 0000000..d4d26af
--- /dev/null
@@ -0,0 +1,177 @@
+git-pull(1)
+===========
+
+NAME
+----
+git-pull - Fetch from and merge with another repository or a local branch
+
+
+SYNOPSIS
+--------
+'git-pull' <options> <repository> <refspec>...
+
+
+DESCRIPTION
+-----------
+Runs `git-fetch` with the given parameters, and calls `git-merge`
+to merge the retrieved head(s) into the current branch.
+
+Note that you can use `.` (current directory) as the
+<repository> to pull from the local repository -- this is useful
+when merging local branches into the current branch.
+
+
+OPTIONS
+-------
+include::merge-options.txt[]
+
+include::fetch-options.txt[]
+
+include::pull-fetch-param.txt[]
+
+include::urls-remotes.txt[]
+
+include::merge-strategies.txt[]
+
+\--rebase::
+       Instead of a merge, perform a rebase after fetching.
+       *NOTE:* This is a potentially _dangerous_ mode of operation.
+       It rewrites history, which does not bode well when you
+       published that history already.  Do *not* use this option
+       unless you have read gitlink:git-rebase[1] carefully.
+
+\--no-rebase::
+       Override earlier \--rebase.
+
+DEFAULT BEHAVIOUR
+-----------------
+
+Often people use `git pull` without giving any parameter.
+Traditionally, this has been equivalent to saying `git pull
+origin`.  However, when configuration `branch.<name>.remote` is
+present while on branch `<name>`, that value is used instead of
+`origin`.
+
+In order to determine what URL to use to fetch from, the value
+of the configuration `remote.<origin>.url` is consulted
+and if there is not any such variable, the value on `URL: ` line
+in `$GIT_DIR/remotes/<origin>` file is used.
+
+In order to determine what remote branches to fetch (and
+optionally store in the tracking branches) when the command is
+run without any refspec parameters on the command line, values
+of the configuration variable `remote.<origin>.fetch` are
+consulted, and if there aren't any, `$GIT_DIR/remotes/<origin>`
+file is consulted and its `Pull: ` lines are used.
+In addition to the refspec formats described in the OPTIONS
+section, you can have a globbing refspec that looks like this:
+
+------------
+refs/heads/*:refs/remotes/origin/*
+------------
+
+A globbing refspec must have a non-empty RHS (i.e. must store
+what were fetched in tracking branches), and its LHS and RHS
+must end with `/*`.  The above specifies that all remote
+branches are tracked using tracking branches in
+`refs/remotes/origin/` hierarchy under the same name.
+
+The rule to determine which remote branch to merge after
+fetching is a bit involved, in order not to break backward
+compatibility.
+
+If explicit refspecs were given on the command
+line of `git pull`, they are all merged.
+
+When no refspec was given on the command line, then `git pull`
+uses the refspec from the configuration or
+`$GIT_DIR/remotes/<origin>`.  In such cases, the following
+rules apply:
+
+. If `branch.<name>.merge` configuration for the current
+  branch `<name>` exists, that is the name of the branch at the
+  remote site that is merged.
+
+. If the refspec is a globbing one, nothing is merged.
+
+. Otherwise the remote branch of the first refspec is merged.
+
+
+EXAMPLES
+--------
+
+git pull, git pull origin::
+       Update the remote-tracking branches for the repository
+       you cloned from, then merge one of them into your
+       current branch.  Normally the branch merged in is
+       the HEAD of the remote repository, but the choice is
+       determined by the branch.<name>.remote and
+       branch.<name>.merge options; see gitlink:git-config[1]
+       for details.
+
+git pull origin next::
+       Merge into the current branch the remote branch `next`;
+       leaves a copy of `next` temporarily in FETCH_HEAD, but
+       does not update any remote-tracking branches.
+
+git pull . fixes enhancements::
+       Bundle local branch `fixes` and `enhancements` on top of
+       the current branch, making an Octopus merge.  This `git pull .`
+       syntax is equivalent to `git merge`.
+
+git pull -s ours . obsolete::
+       Merge local branch `obsolete` into the current branch,
+       using `ours` merge strategy.
+
+git pull --no-commit . maint::
+       Merge local branch `maint` into the current branch, but
+       do not make a commit automatically.  This can be used
+       when you want to include further changes to the merge,
+       or want to write your own merge commit message.
++
+You should refrain from abusing this option to sneak substantial
+changes into a merge commit.  Small fixups like bumping
+release/version name would be acceptable.
+
+Command line pull of multiple branches from one repository::
++
+------------------------------------------------
+$ git checkout master
+$ git fetch origin +pu:pu maint:tmp
+$ git pull . tmp
+------------------------------------------------
++
+This updates (or creates, as necessary) branches `pu` and `tmp`
+in the local repository by fetching from the branches
+(respectively) `pu` and `maint` from the remote repository.
++
+The `pu` branch will be updated even if it is does not
+fast-forward; the others will not be.
++
+The final command then merges the newly fetched `tmp` into master.
+
+
+If you tried a pull which resulted in a complex conflicts and
+would want to start over, you can recover with
+gitlink:git-reset[1].
+
+
+SEE ALSO
+--------
+gitlink:git-fetch[1], gitlink:git-merge[1], gitlink:git-config[1]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+and Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Jon Loeliger,
+David Greaves,
+Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
new file mode 100644 (file)
index 0000000..b8003c6
--- /dev/null
@@ -0,0 +1,148 @@
+git-push(1)
+===========
+
+NAME
+----
+git-push - Update remote refs along with associated objects
+
+
+SYNOPSIS
+--------
+[verse]
+'git-push' [--all] [--dry-run] [--tags] [--receive-pack=<git-receive-pack>]
+           [--repo=all] [-f | --force] [-v | --verbose] [<repository> <refspec>...]
+
+DESCRIPTION
+-----------
+
+Updates remote refs using local refs, while sending objects
+necessary to complete the given refs.
+
+You can make interesting things happen to a repository
+every time you push into it, by setting up 'hooks' there.  See
+documentation for gitlink:git-receive-pack[1].
+
+
+OPTIONS
+-------
+<repository>::
+       The "remote" repository that is destination of a push
+       operation.  See the section <<URLS,GIT URLS>> below.
+
+<refspec>::
+       The canonical format of a <refspec> parameter is
+       `+?<src>:<dst>`; that is, an optional plus `+`, followed
+       by the source ref, followed by a colon `:`, followed by
+       the destination ref.
++
+The <src> side can be an
+arbitrary "SHA1 expression" that can be used as an
+argument to `git-cat-file -t`.  E.g. `master~4` (push
+four parents before the current master head).
++
+The local ref that matches <src> is used
+to fast forward the remote ref that matches <dst>.  If
+the optional plus `+` is used, the remote ref is updated
+even if it does not result in a fast forward update.
++
+Note: If no explicit refspec is found, (that is neither
+on the command line nor in any Push line of the
+corresponding remotes file---see below), then all the
+heads that exist both on the local side and on the remote
+side are updated.
++
+`tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`.
++
+A parameter <ref> without a colon pushes the <ref> from the source
+repository to the destination repository under the same name.
++
+Pushing an empty <src> allows you to delete the <dst> ref from
+the remote repository.
+
+\--all::
+       Instead of naming each ref to push, specifies that all
+       refs under `$GIT_DIR/refs/heads/` be pushed.
+
+\--mirror::
+       Instead of naming each ref to push, specifies that all
+       refs under `$GIT_DIR/refs/heads/` and `$GIT_DIR/refs/tags/`
+       be mirrored to the remote repository.  Newly created local
+       refs will be pushed to the remote end, locally updated refs
+       will be force updated on the remote end, and deleted refs
+       will be removed from the remote end.
+
+\--dry-run::
+       Do everything except actually send the updates.
+
+\--tags::
+       All refs under `$GIT_DIR/refs/tags` are pushed, in
+       addition to refspecs explicitly listed on the command
+       line.
+
+\--receive-pack=<git-receive-pack>::
+       Path to the 'git-receive-pack' program on the remote
+       end.  Sometimes useful when pushing to a remote
+       repository over ssh, and you do not have the program in
+       a directory on the default $PATH.
+
+\--exec=<git-receive-pack>::
+       Same as \--receive-pack=<git-receive-pack>.
+
+-f, \--force::
+       Usually, the command refuses to update a remote ref that is
+       not an ancestor of the local ref used to overwrite it.
+       This flag disables the check.  This can cause the
+       remote repository to lose commits; use it with care.
+
+\--repo=<repo>::
+       When no repository is specified the command defaults to
+       "origin"; this overrides it.
+
+\--thin, \--no-thin::
+       These options are passed to `git-send-pack`.  Thin
+       transfer spends extra cycles to minimize the number of
+       objects to be sent and meant to be used on slower connection.
+
+-v, \--verbose::
+       Run verbosely.
+
+include::urls-remotes.txt[]
+
+
+Examples
+--------
+
+git push origin master::
+       Find a ref that matches `master` in the source repository
+       (most likely, it would find `refs/heads/master`), and update
+       the same ref (e.g. `refs/heads/master`) in `origin` repository
+       with it.
+
+git push origin :experimental::
+       Find a ref that matches `experimental` in the `origin` repository
+       (e.g. `refs/heads/experimental`), and delete it.
+
+git push origin master:satellite/master::
+       Find a ref that matches `master` in the source repository
+       (most likely, it would find `refs/heads/master`), and update
+       the ref that matches `satellite/master` (most likely, it would
+       be `refs/remotes/satellite/master`) in `origin` repository with it.
+
+git push origin master:refs/heads/experimental::
+       Create the branch `experimental` in the `origin` repository
+       by copying the current `master` branch.  This form is usually
+       needed to create a new branch in the remote repository as
+       there is no `experimental` branch to match.
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>, later rewritten in C
+by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-quiltimport.txt b/Documentation/git-quiltimport.txt
new file mode 100644 (file)
index 0000000..1c3ef4c
--- /dev/null
@@ -0,0 +1,60 @@
+git-quiltimport(1)
+================
+
+NAME
+----
+git-quiltimport - Applies a quilt patchset onto the current branch
+
+
+SYNOPSIS
+--------
+[verse]
+'git-quiltimport' [--dry-run] [--author <author>] [--patches <dir>]
+
+
+DESCRIPTION
+-----------
+Applies a quilt patchset onto the current git branch, preserving
+the patch boundaries, patch order, and patch descriptions present
+in the quilt patchset.
+
+For each patch the code attempts to extract the author from the
+patch description.  If that fails it falls back to the author
+specified with --author.  If the --author flag was not given
+the patch description is displayed and the user is asked to
+interactively enter the author of the patch.
+
+If a subject is not found in the patch description the patch name is
+preserved as the 1 line subject in the git description.
+
+OPTIONS
+-------
+--dry-run::
+       Walk through the patches in the series and warn
+       if we cannot find all of the necessary information to commit
+       a patch.  At the time of this writing only missing author
+       information is warned about.
+
+--author Author Name <Author Email>::
+       The author name and email address to use when no author
+       information can be found in the patch description.
+
+--patches <dir>::
+       The directory to find the quilt patches and the
+       quilt series file.
++
+The default for the patch directory is patches
+or the value of the $QUILT_PATCHES environment
+variable.
+
+Author
+------
+Written by Eric Biederman <ebiederm@lnxi.com>
+
+Documentation
+--------------
+Documentation by Eric Biederman <ebiederm@lnxi.com>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-read-tree.txt b/Documentation/git-read-tree.txt
new file mode 100644 (file)
index 0000000..74c5478
--- /dev/null
@@ -0,0 +1,364 @@
+git-read-tree(1)
+================
+
+NAME
+----
+git-read-tree - Reads tree information into the index
+
+
+SYNOPSIS
+--------
+'git-read-tree' (<tree-ish> | [[-m [--trivial] [--aggressive] | --reset | --prefix=<prefix>] [-u | -i]] [--exclude-per-directory=<gitignore>] [--index-output=<file>] <tree-ish1> [<tree-ish2> [<tree-ish3>]])
+
+
+DESCRIPTION
+-----------
+Reads the tree information given by <tree-ish> into the index,
+but does not actually *update* any of the files it "caches". (see:
+gitlink:git-checkout-index[1])
+
+Optionally, it can merge a tree into the index, perform a
+fast-forward (i.e. 2-way) merge, or a 3-way merge, with the `-m`
+flag.  When used with `-m`, the `-u` flag causes it to also update
+the files in the work tree with the result of the merge.
+
+Trivial merges are done by `git-read-tree` itself.  Only conflicting paths
+will be in unmerged state when `git-read-tree` returns.
+
+OPTIONS
+-------
+-m::
+       Perform a merge, not just a read.  The command will
+       refuse to run if your index file has unmerged entries,
+       indicating that you have not finished previous merge you
+       started.
+
+--reset::
+        Same as -m, except that unmerged entries are discarded
+        instead of failing.
+
+-u::
+       After a successful merge, update the files in the work
+       tree with the result of the merge.
+
+-i::
+       Usually a merge requires the index file as well as the
+       files in the working tree are up to date with the
+       current head commit, in order not to lose local
+       changes.  This flag disables the check with the working
+       tree and is meant to be used when creating a merge of
+       trees that are not directly related to the current
+       working tree status into a temporary index file.
+
+--trivial::
+       Restrict three-way merge by `git-read-tree` to happen
+       only if there is no file-level merging required, instead
+       of resolving merge for trivial cases and leaving
+       conflicting files unresolved in the index.
+
+--aggressive::
+       Usually a three-way merge by `git-read-tree` resolves
+       the merge for really trivial cases and leaves other
+       cases unresolved in the index, so that Porcelains can
+       implement different merge policies.  This flag makes the
+       command to resolve a few more cases internally:
++
+* when one side removes a path and the other side leaves the path
+  unmodified.  The resolution is to remove that path.
+* when both sides remove a path.  The resolution is to remove that path.
+* when both sides adds a path identically.  The resolution
+  is to add that path.
+
+--prefix=<prefix>/::
+       Keep the current index contents, and read the contents
+       of named tree-ish under directory at `<prefix>`.  The
+       original index file cannot have anything at the path
+       `<prefix>` itself, and have nothing in `<prefix>/`
+       directory.  Note that the `<prefix>/` value must end
+       with a slash.
+
+--exclude-per-directory=<gitignore>::
+       When running the command with `-u` and `-m` options, the
+       merge result may need to overwrite paths that are not
+       tracked in the current branch.  The command usually
+       refuses to proceed with the merge to avoid losing such a
+       path.  However this safety valve sometimes gets in the
+       way.  For example, it often happens that the other
+       branch added a file that used to be a generated file in
+       your branch, and the safety valve triggers when you try
+       to switch to that branch after you ran `make` but before
+       running `make clean` to remove the generated file.  This
+       option tells the command to read per-directory exclude
+       file (usually '.gitignore') and allows such an untracked
+       but explicitly ignored file to be overwritten.
+
+--index-output=<file>::
+       Instead of writing the results out to `$GIT_INDEX_FILE`,
+       write the resulting index in the named file.  While the
+       command is operating, the original index file is locked
+       with the same mechanism as usual.  The file must allow
+       to be rename(2)ed into from a temporary file that is
+       created next to the usual index file; typically this
+       means it needs to be on the same filesystem as the index
+       file itself, and you need write permission to the
+       directories the index file and index output file are
+       located in.
+
+<tree-ish#>::
+       The id of the tree object(s) to be read/merged.
+
+
+Merging
+-------
+If `-m` is specified, `git-read-tree` can perform 3 kinds of
+merge, a single tree merge if only 1 tree is given, a
+fast-forward merge with 2 trees, or a 3-way merge if 3 trees are
+provided.
+
+
+Single Tree Merge
+~~~~~~~~~~~~~~~~~
+If only 1 tree is specified, git-read-tree operates as if the user did not
+specify `-m`, except that if the original index has an entry for a
+given pathname, and the contents of the path matches with the tree
+being read, the stat info from the index is used. (In other words, the
+index's stat()s take precedence over the merged tree's).
+
+That means that if you do a `git-read-tree -m <newtree>` followed by a
+`git-checkout-index -f -u -a`, the `git-checkout-index` only checks out
+the stuff that really changed.
+
+This is used to avoid unnecessary false hits when `git-diff-files` is
+run after `git-read-tree`.
+
+
+Two Tree Merge
+~~~~~~~~~~~~~~
+
+Typically, this is invoked as `git-read-tree -m $H $M`, where $H
+is the head commit of the current repository, and $M is the head
+of a foreign tree, which is simply ahead of $H (i.e. we are in a
+fast forward situation).
+
+When two trees are specified, the user is telling git-read-tree
+the following:
+
+     1. The current index and work tree is derived from $H, but
+        the user may have local changes in them since $H;
+
+     2. The user wants to fast-forward to $M.
+
+In this case, the `git-read-tree -m $H $M` command makes sure
+that no local change is lost as the result of this "merge".
+Here are the "carry forward" rules:
+
+        I (index)           H        M        Result
+       -------------------------------------------------------
+      0 nothing             nothing  nothing  (does not happen)
+      1 nothing             nothing  exists   use M
+      2 nothing             exists   nothing  remove path from index
+      3 nothing             exists   exists   use M
+
+        clean I==H  I==M
+       ------------------
+      4 yes   N/A   N/A     nothing  nothing  keep index
+      5 no    N/A   N/A     nothing  nothing  keep index
+
+      6 yes   N/A   yes     nothing  exists   keep index
+      7 no    N/A   yes     nothing  exists   keep index
+      8 yes   N/A   no      nothing  exists   fail
+      9 no    N/A   no      nothing  exists   fail
+
+     10 yes   yes   N/A     exists   nothing  remove path from index
+     11 no    yes   N/A     exists   nothing  fail
+     12 yes   no    N/A     exists   nothing  fail
+     13 no    no    N/A     exists   nothing  fail
+
+        clean (H=M)
+       ------
+     14 yes                 exists   exists   keep index
+     15 no                  exists   exists   keep index
+
+        clean I==H  I==M (H!=M)
+       ------------------
+     16 yes   no    no      exists   exists   fail
+     17 no    no    no      exists   exists   fail
+     18 yes   no    yes     exists   exists   keep index
+     19 no    no    yes     exists   exists   keep index
+     20 yes   yes   no      exists   exists   use M
+     21 no    yes   no      exists   exists   fail
+
+In all "keep index" cases, the index entry stays as in the
+original index file.  If the entry were not up to date,
+git-read-tree keeps the copy in the work tree intact when
+operating under the -u flag.
+
+When this form of git-read-tree returns successfully, you can
+see what "local changes" you made are carried forward by running
+`git-diff-index --cached $M`.  Note that this does not
+necessarily match `git-diff-index --cached $H` would have
+produced before such a two tree merge.  This is because of cases
+18 and 19 --- if you already had the changes in $M (e.g. maybe
+you picked it up via e-mail in a patch form), `git-diff-index
+--cached $H` would have told you about the change before this
+merge, but it would not show in `git-diff-index --cached $M`
+output after two-tree merge.
+
+
+3-Way Merge
+~~~~~~~~~~~
+Each "index" entry has two bits worth of "stage" state. stage 0 is the
+normal one, and is the only one you'd see in any kind of normal use.
+
+However, when you do `git-read-tree` with three trees, the "stage"
+starts out at 1.
+
+This means that you can do
+
+----------------
+$ git-read-tree -m <tree1> <tree2> <tree3>
+----------------
+
+and you will end up with an index with all of the <tree1> entries in
+"stage1", all of the <tree2> entries in "stage2" and all of the
+<tree3> entries in "stage3".  When performing a merge of another
+branch into the current branch, we use the common ancestor tree
+as <tree1>, the current branch head as <tree2>, and the other
+branch head as <tree3>.
+
+Furthermore, `git-read-tree` has special-case logic that says: if you see
+a file that matches in all respects in the following states, it
+"collapses" back to "stage0":
+
+   - stage 2 and 3 are the same; take one or the other (it makes no
+     difference - the same work has been done on our branch in
+     stage 2 and their branch in stage 3)
+
+   - stage 1 and stage 2 are the same and stage 3 is different; take
+     stage 3 (our branch in stage 2 did not do anything since the
+     ancestor in stage 1 while their branch in stage 3 worked on
+     it)
+
+   - stage 1 and stage 3 are the same and stage 2 is different take
+     stage 2 (we did something while they did nothing)
+
+The `git-write-tree` command refuses to write a nonsensical tree, and it
+will complain about unmerged entries if it sees a single entry that is not
+stage 0.
+
+OK, this all sounds like a collection of totally nonsensical rules,
+but it's actually exactly what you want in order to do a fast
+merge. The different stages represent the "result tree" (stage 0, aka
+"merged"), the original tree (stage 1, aka "orig"), and the two trees
+you are trying to merge (stage 2 and 3 respectively).
+
+The order of stages 1, 2 and 3 (hence the order of three
+<tree-ish> command line arguments) are significant when you
+start a 3-way merge with an index file that is already
+populated.  Here is an outline of how the algorithm works:
+
+- if a file exists in identical format in all three trees, it will
+  automatically collapse to "merged" state by git-read-tree.
+
+- a file that has _any_ difference what-so-ever in the three trees
+  will stay as separate entries in the index. It's up to "porcelain
+  policy" to determine how to remove the non-0 stages, and insert a
+  merged version.
+
+- the index file saves and restores with all this information, so you
+  can merge things incrementally, but as long as it has entries in
+  stages 1/2/3 (i.e., "unmerged entries") you can't write the result. So
+  now the merge algorithm ends up being really simple:
+
+  * you walk the index in order, and ignore all entries of stage 0,
+    since they've already been done.
+
+  * if you find a "stage1", but no matching "stage2" or "stage3", you
+    know it's been removed from both trees (it only existed in the
+    original tree), and you remove that entry.
+
+  * if you find a matching "stage2" and "stage3" tree, you remove one
+    of them, and turn the other into a "stage0" entry. Remove any
+    matching "stage1" entry if it exists too.  .. all the normal
+    trivial rules ..
+
+You would normally use `git-merge-index` with supplied
+`git-merge-one-file` to do this last step.  The script updates
+the files in the working tree as it merges each path and at the
+end of a successful merge.
+
+When you start a 3-way merge with an index file that is already
+populated, it is assumed that it represents the state of the
+files in your work tree, and you can even have files with
+changes unrecorded in the index file.  It is further assumed
+that this state is "derived" from the stage 2 tree.  The 3-way
+merge refuses to run if it finds an entry in the original index
+file that does not match stage 2.
+
+This is done to prevent you from losing your work-in-progress
+changes, and mixing your random changes in an unrelated merge
+commit.  To illustrate, suppose you start from what has been
+committed last to your repository:
+
+----------------
+$ JC=`git-rev-parse --verify "HEAD^0"`
+$ git-checkout-index -f -u -a $JC
+----------------
+
+You do random edits, without running git-update-index.  And then
+you notice that the tip of your "upstream" tree has advanced
+since you pulled from him:
+
+----------------
+$ git-fetch git://.... linus
+$ LT=`cat .git/FETCH_HEAD`
+----------------
+
+Your work tree is still based on your HEAD ($JC), but you have
+some edits since.  Three-way merge makes sure that you have not
+added or modified index entries since $JC, and if you haven't,
+then does the right thing.  So with the following sequence:
+
+----------------
+$ git-read-tree -m -u `git-merge-base $JC $LT` $JC $LT
+$ git-merge-index git-merge-one-file -a
+$ echo "Merge with Linus" | \
+  git-commit-tree `git-write-tree` -p $JC -p $LT
+----------------
+
+what you would commit is a pure merge between $JC and $LT without
+your work-in-progress changes, and your work tree would be
+updated to the result of the merge.
+
+However, if you have local changes in the working tree that
+would be overwritten by this merge,`git-read-tree` will refuse
+to run to prevent your changes from being lost.
+
+In other words, there is no need to worry about what exists only
+in the working tree.  When you have local changes in a part of
+the project that is not involved in the merge, your changes do
+not interfere with the merge, and are kept intact.  When they
+*do* interfere, the merge does not even start (`git-read-tree`
+complains loudly and fails without modifying anything).  In such
+a case, you can simply continue doing what you were in the
+middle of doing, and when your working tree is ready (i.e. you
+have finished your work-in-progress), attempt the merge again.
+
+
+See Also
+--------
+gitlink:git-write-tree[1]; gitlink:git-ls-files[1];
+gitlink:gitignore[5]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
new file mode 100644 (file)
index 0000000..e4326d3
--- /dev/null
@@ -0,0 +1,404 @@
+git-rebase(1)
+=============
+
+NAME
+----
+git-rebase - Forward-port local commits to the updated upstream head
+
+SYNOPSIS
+--------
+[verse]
+'git-rebase' [-i | --interactive] [-v | --verbose] [-m | --merge]
+       [-C<n>] [ --whitespace=<option>] [-p | --preserve-merges]
+       [--onto <newbase>] <upstream> [<branch>]
+'git-rebase' --continue | --skip | --abort
+
+DESCRIPTION
+-----------
+If <branch> is specified, git-rebase will perform an automatic
+`git checkout <branch>` before doing anything else.  Otherwise
+it remains on the current branch.
+
+All changes made by commits in the current branch but that are not
+in <upstream> are saved to a temporary area.  This is the same set
+of commits that would be shown by `git log <upstream>..HEAD`.
+
+The current branch is reset to <upstream>, or <newbase> if the
+--onto option was supplied.  This has the exact same effect as
+`git reset --hard <upstream>` (or <newbase>).
+
+The commits that were previously saved into the temporary area are
+then reapplied to the current branch, one by one, in order. Note that
+any commits in HEAD which introduce the same textual changes as a commit
+in HEAD..<upstream> are omitted (i.e., a patch already accepted upstream
+with a different commit message or timestamp will be skipped).
+
+It is possible that a merge failure will prevent this process from being
+completely automatic.  You will have to resolve any such merge failure
+and run `git rebase --continue`.  Another option is to bypass the commit
+that caused the merge failure with `git rebase --skip`.  To restore the
+original <branch> and remove the .dotest working files, use the command
+`git rebase --abort` instead.
+
+Assume the following history exists and the current branch is "topic":
+
+------------
+          A---B---C topic
+         /
+    D---E---F---G master
+------------
+
+From this point, the result of either of the following commands:
+
+
+    git-rebase master
+    git-rebase master topic
+
+would be:
+
+------------
+                  A'--B'--C' topic
+                 /
+    D---E---F---G master
+------------
+
+The latter form is just a short-hand of `git checkout topic`
+followed by `git rebase master`.
+
+If the upstream branch already contains a change you have made (e.g.,
+because you mailed a patch which was applied upstream), then that commit
+will be skipped. For example, running `git-rebase master` on the
+following history (in which A' and A introduce the same set of changes,
+but have different committer information):
+
+------------
+          A---B---C topic
+         /
+    D---E---A'---F master
+------------
+
+will result in:
+
+------------
+                   B'---C' topic
+                  /
+    D---E---A'---F master
+------------
+
+Here is how you would transplant a topic branch based on one
+branch to another, to pretend that you forked the topic branch
+from the latter branch, using `rebase --onto`.
+
+First let's assume your 'topic' is based on branch 'next'.
+For example feature developed in 'topic' depends on some
+functionality which is found in 'next'.
+
+------------
+    o---o---o---o---o  master
+         \
+          o---o---o---o---o  next
+                           \
+                            o---o---o  topic
+------------
+
+We would want to make 'topic' forked from branch 'master',
+for example because the functionality 'topic' branch depend on
+got merged into more stable 'master' branch, like this:
+
+------------
+    o---o---o---o---o  master
+        |            \
+        |             o'--o'--o'  topic
+         \
+          o---o---o---o---o  next
+------------
+
+We can get this using the following command:
+
+    git-rebase --onto master next topic
+
+
+Another example of --onto option is to rebase part of a
+branch.  If we have the following situation:
+
+------------
+                            H---I---J topicB
+                           /
+                  E---F---G  topicA
+                 /
+    A---B---C---D  master
+------------
+
+then the command
+
+    git-rebase --onto master topicA topicB
+
+would result in:
+
+------------
+                 H'--I'--J'  topicB
+                /
+                | E---F---G  topicA
+                |/
+    A---B---C---D  master
+------------
+
+This is useful when topicB does not depend on topicA.
+
+A range of commits could also be removed with rebase.  If we have
+the following situation:
+
+------------
+    E---F---G---H---I---J  topicA
+------------
+
+then the command
+
+    git-rebase --onto topicA~5 topicA~3 topicA
+
+would result in the removal of commits F and G:
+
+------------
+    E---H'---I'---J'  topicA
+------------
+
+This is useful if F and G were flawed in some way, or should not be
+part of topicA.  Note that the argument to --onto and the <upstream>
+parameter can be any valid commit-ish.
+
+In case of conflict, git-rebase will stop at the first problematic commit
+and leave conflict markers in the tree.  You can use git diff to locate
+the markers (<<<<<<) and make edits to resolve the conflict.  For each
+file you edit, you need to tell git that the conflict has been resolved,
+typically this would be done with
+
+
+    git add <filename>
+
+
+After resolving the conflict manually and updating the index with the
+desired resolution, you can continue the rebasing process with
+
+
+    git rebase --continue
+
+
+Alternatively, you can undo the git-rebase with
+
+
+    git rebase --abort
+
+OPTIONS
+-------
+<newbase>::
+       Starting point at which to create the new commits. If the
+       --onto option is not specified, the starting point is
+       <upstream>.  May be any valid commit, and not just an
+       existing branch name.
+
+<upstream>::
+       Upstream branch to compare against.  May be any valid commit,
+       not just an existing branch name.
+
+<branch>::
+       Working branch; defaults to HEAD.
+
+--continue::
+       Restart the rebasing process after having resolved a merge conflict.
+
+--abort::
+       Restore the original branch and abort the rebase operation.
+
+--skip::
+       Restart the rebasing process by skipping the current patch.
+
+-m, \--merge::
+       Use merging strategies to rebase.  When the recursive (default) merge
+       strategy is used, this allows rebase to be aware of renames on the
+       upstream side.
+
+-s <strategy>, \--strategy=<strategy>::
+       Use the given merge strategy; can be supplied more than
+       once to specify them in the order they should be tried.
+       If there is no `-s` option, a built-in list of strategies
+       is used instead (`git-merge-recursive` when merging a single
+       head, `git-merge-octopus` otherwise).  This implies --merge.
+
+-v, \--verbose::
+       Display a diffstat of what changed upstream since the last rebase.
+
+-C<n>::
+       Ensure at least <n> lines of surrounding context match before
+       and after each change.  When fewer lines of surrounding
+       context exist they all must match.  By default no context is
+       ever ignored.
+
+--whitespace=<nowarn|warn|error|error-all|strip>::
+       This flag is passed to the `git-apply` program
+       (see gitlink:git-apply[1]) that applies the patch.
+
+-i, \--interactive::
+       Make a list of the commits which are about to be rebased.  Let the
+       user edit that list before rebasing.  This mode can also be used to
+       split commits (see SPLITTING COMMITS below).
+
+-p, \--preserve-merges::
+       Instead of ignoring merges, try to recreate them.  This option
+       only works in interactive mode.
+
+include::merge-strategies.txt[]
+
+NOTES
+-----
+When you rebase a branch, you are changing its history in a way that
+will cause problems for anyone who already has a copy of the branch
+in their repository and tries to pull updates from you.  You should
+understand the implications of using 'git rebase' on a repository that
+you share.
+
+When the git rebase command is run, it will first execute a "pre-rebase"
+hook if one exists.  You can use this hook to do sanity checks and
+reject the rebase if it isn't appropriate.  Please see the template
+pre-rebase hook script for an example.
+
+You must be in the top directory of your project to start (or continue)
+a rebase.  Upon completion, <branch> will be the current branch.
+
+INTERACTIVE MODE
+----------------
+
+Rebasing interactively means that you have a chance to edit the commits
+which are rebased.  You can reorder the commits, and you can
+remove them (weeding out bad or otherwise unwanted patches).
+
+The interactive mode is meant for this type of workflow:
+
+1. have a wonderful idea
+2. hack on the code
+3. prepare a series for submission
+4. submit
+
+where point 2. consists of several instances of
+
+a. regular use
+ 1. finish something worthy of a commit
+ 2. commit
+b. independent fixup
+ 1. realize that something does not work
+ 2. fix that
+ 3. commit it
+
+Sometimes the thing fixed in b.2. cannot be amended to the not-quite
+perfect commit it fixes, because that commit is buried deeply in a
+patch series.  That is exactly what interactive rebase is for: use it
+after plenty of "a"s and "b"s, by rearranging and editing
+commits, and squashing multiple commits into one.
+
+Start it with the last commit you want to retain as-is:
+
+       git rebase -i <after-this-commit>
+
+An editor will be fired up with all the commits in your current branch
+(ignoring merge commits), which come after the given commit.  You can
+reorder the commits in this list to your heart's content, and you can
+remove them.  The list looks more or less like this:
+
+-------------------------------------------
+pick deadbee The oneline of this commit
+pick fa1afe1 The oneline of the next commit
+...
+-------------------------------------------
+
+The oneline descriptions are purely for your pleasure; `git-rebase` will
+not look at them but at the commit names ("deadbee" and "fa1afe1" in this
+example), so do not delete or edit the names.
+
+By replacing the command "pick" with the command "edit", you can tell
+`git-rebase` to stop after applying that commit, so that you can edit
+the files and/or the commit message, amend the commit, and continue
+rebasing.
+
+If you want to fold two or more commits into one, replace the command
+"pick" with "squash" for the second and subsequent commit.  If the
+commits had different authors, it will attribute the squashed commit to
+the author of the first commit.
+
+In both cases, or when a "pick" does not succeed (because of merge
+errors), the loop will stop to let you fix things, and you can continue
+the loop with `git rebase --continue`.
+
+For example, if you want to reorder the last 5 commits, such that what
+was HEAD~4 becomes the new HEAD. To achieve that, you would call
+`git-rebase` like this:
+
+----------------------
+$ git rebase -i HEAD~5
+----------------------
+
+And move the first patch to the end of the list.
+
+You might want to preserve merges, if you have a history like this:
+
+------------------
+           X
+            \
+         A---M---B
+        /
+---o---O---P---Q
+------------------
+
+Suppose you want to rebase the side branch starting at "A" to "Q". Make
+sure that the current HEAD is "B", and call
+
+-----------------------------
+$ git rebase -i -p --onto Q O
+-----------------------------
+
+
+SPLITTING COMMITS
+-----------------
+
+In interactive mode, you can mark commits with the action "edit".  However,
+this does not necessarily mean that 'git rebase' expects the result of this
+edit to be exactly one commit.  Indeed, you can undo the commit, or you can
+add other commits.  This can be used to split a commit into two:
+
+- Start an interactive rebase with 'git rebase -i <commit>^', where
+  <commit> is the commit you want to split.  In fact, any commit range
+  will do, as long as it contains that commit.
+
+- Mark the commit you want to split with the action "edit".
+
+- When it comes to editing that commit, execute 'git reset HEAD^'.  The
+  effect is that the HEAD is rewound by one, and the index follows suit.
+  However, the working tree stays the same.
+
+- Now add the changes to the index that you want to have in the first
+  commit.  You can use gitlink:git-add[1] (possibly interactively) and/or
+  gitlink:git-gui[1] to do that.
+
+- Commit the now-current index with whatever commit message is appropriate
+  now.
+
+- Repeat the last two steps until your working tree is clean.
+
+- Continue the rebase with 'git rebase --continue'.
+
+If you are not absolutely sure that the intermediate revisions are
+consistent (they compile, pass the testsuite, etc.) you should use
+gitlink:git-stash[1] to stash away the not-yet-committed changes
+after each commit, test, and amend the commit if fixes are necessary.
+
+
+Authors
+------
+Written by Junio C Hamano <junkio@cox.net> and
+Johannes E. Schindelin <johannes.schindelin@gmx.de>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt
new file mode 100644 (file)
index 0000000..2633d94
--- /dev/null
@@ -0,0 +1,165 @@
+git-receive-pack(1)
+===================
+
+NAME
+----
+git-receive-pack - Receive what is pushed into the repository
+
+
+SYNOPSIS
+--------
+'git-receive-pack' <directory>
+
+DESCRIPTION
+-----------
+Invoked by 'git-send-pack' and updates the repository with the
+information fed from the remote end.
+
+This command is usually not invoked directly by the end user.
+The UI for the protocol is on the 'git-send-pack' side, and the
+program pair is meant to be used to push updates to remote
+repository.  For pull operations, see 'git-fetch-pack'.
+
+The command allows for creation and fast forwarding of sha1 refs
+(heads/tags) on the remote end (strictly speaking, it is the
+local end receive-pack runs, but to the user who is sitting at
+the send-pack end, it is updating the remote.  Confused?)
+
+There are other real-world examples of using update and
+post-update hooks found in the Documentation/howto directory.
+
+git-receive-pack honours the receive.denyNonFastForwards config
+option, which tells it if updates to a ref should be denied if they
+are not fast-forwards.
+
+OPTIONS
+-------
+<directory>::
+       The repository to sync into.
+
+pre-receive Hook
+----------------
+Before any ref is updated, if $GIT_DIR/hooks/pre-receive file exists
+and is executable, it will be invoked once with no parameters.  The
+standard input of the hook will be one line per ref to be updated:
+
+       sha1-old SP sha1-new SP refname LF
+
+The refname value is relative to $GIT_DIR; e.g. for the master
+head this is "refs/heads/master".  The two sha1 values before
+each refname are the object names for the refname before and after
+the update.  Refs to be created will have sha1-old equal to 0\{40},
+while refs to be deleted will have sha1-new equal to 0\{40}, otherwise
+sha1-old and sha1-new should be valid objects in the repository.
+
+This hook is called before any refname is updated and before any
+fast-forward checks are performed.
+
+If the pre-receive hook exits with a non-zero exit status no updates
+will be performed, and the update, post-receive and post-update
+hooks will not be invoked either.  This can be useful to quickly
+bail out if the update is not to be supported.
+
+update Hook
+-----------
+Before each ref is updated, if $GIT_DIR/hooks/update file exists
+and is executable, it is invoked once per ref, with three parameters:
+
+       $GIT_DIR/hooks/update refname sha1-old sha1-new
+
+The refname parameter is relative to $GIT_DIR; e.g. for the master
+head this is "refs/heads/master".  The two sha1 arguments are
+the object names for the refname before and after the update.
+Note that the hook is called before the refname is updated,
+so either sha1-old is 0\{40} (meaning there is no such ref yet),
+or it should match what is recorded in refname.
+
+The hook should exit with non-zero status if it wants to disallow
+updating the named ref.  Otherwise it should exit with zero.
+
+Successful execution (a zero exit status) of this hook does not
+ensure the ref will actually be updated, it is only a prerequisite.
+As such it is not a good idea to send notices (e.g. email) from
+this hook.  Consider using the post-receive hook instead.
+
+post-receive Hook
+-----------------
+After all refs were updated (or attempted to be updated), if any
+ref update was successful, and if $GIT_DIR/hooks/post-receive
+file exists and is executable, it will be invoke once with no
+parameters.  The standard input of the hook will be one line
+for each successfully updated ref:
+
+       sha1-old SP sha1-new SP refname LF
+
+The refname value is relative to $GIT_DIR; e.g. for the master
+head this is "refs/heads/master".  The two sha1 values before
+each refname are the object names for the refname before and after
+the update.  Refs that were created will have sha1-old equal to
+0\{40}, while refs that were deleted will have sha1-new equal to
+0\{40}, otherwise sha1-old and sha1-new should be valid objects in
+the repository.
+
+Using this hook, it is easy to generate mails describing the updates
+to the repository.  This example script sends one mail message per
+ref listing the commits pushed to the repository:
+
+       #!/bin/sh
+       # mail out commit update information.
+       while read oval nval ref
+       do
+               if expr "$oval" : '0*$' >/dev/null
+               then
+                       echo "Created a new ref, with the following commits:"
+                       git-rev-list --pretty "$nval"
+               else
+                       echo "New commits:"
+                       git-rev-list --pretty "$nval" "^$oval"
+               fi |
+               mail -s "Changes to ref $ref" commit-list@mydomain
+       done
+       exit 0
+
+The exit code from this hook invocation is ignored, however a
+non-zero exit code will generate an error message.
+
+Note that it is possible for refname to not have sha1-new when this
+hook runs.  This can easily occur if another user modifies the ref
+after it was updated by receive-pack, but before the hook was able
+to evaluate it.  It is recommended that hooks rely on sha1-new
+rather than the current value of refname.
+
+post-update Hook
+----------------
+After all other processing, if at least one ref was updated, and
+if $GIT_DIR/hooks/post-update file exists and is executable, then
+post-update will called with the list of refs that have been updated.
+This can be used to implement any repository wide cleanup tasks.
+
+The exit code from this hook invocation is ignored; the only thing
+left for git-receive-pack to do at that point is to exit itself
+anyway.
+
+This hook can be used, for example, to run "git-update-server-info"
+if the repository is packed and is served via a dumb transport.
+
+       #!/bin/sh
+       exec git-update-server-info
+
+
+SEE ALSO
+--------
+gitlink:git-send-pack[1]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-reflog.txt b/Documentation/git-reflog.txt
new file mode 100644 (file)
index 0000000..25003c3
--- /dev/null
@@ -0,0 +1,91 @@
+git-reflog(1)
+=============
+
+NAME
+----
+git-reflog - Manage reflog information
+
+
+SYNOPSIS
+--------
+'git reflog' <subcommand> <options>
+
+DESCRIPTION
+-----------
+The command takes various subcommands, and different options
+depending on the subcommand:
+
+[verse]
+git reflog expire [--dry-run] [--stale-fix] [--verbose]
+       [--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>...
+
+git reflog [show] [log-options] [<ref>]
+
+Reflog is a mechanism to record when the tip of branches are
+updated.  This command is to manage the information recorded in it.
+
+The subcommand "expire" is used to prune older reflog entries.
+Entries older than `expire` time, or entries older than
+`expire-unreachable` time and are not reachable from the current
+tip, are removed from the reflog.  This is typically not used
+directly by the end users -- instead, see gitlink:git-gc[1].
+
+The subcommand "show" (which is also the default, in the absence of any
+subcommands) will take all the normal log options, and show the log of
+the reference provided in the command-line (or `HEAD`, by default).
+The reflog will cover all recent actions (HEAD reflog records branch switching
+as well).  It is an alias for 'git log -g --abbrev-commit --pretty=oneline';
+see gitlink:git-log[1].
+
+The reflog is useful in various git commands, to specify the old value
+of a reference. For example, `HEAD@\{2\}` means "where HEAD used to be
+two moves ago", `master@\{one.week.ago\}` means "where master used to
+point to one week ago", and so on. See gitlink:git-rev-parse[1] for
+more details.
+
+
+OPTIONS
+-------
+
+--stale-fix::
+       This revamps the logic -- the definition of "broken commit"
+       becomes: a commit that is not reachable from any of the refs and
+       there is a missing object among the commit, tree, or blob
+       objects reachable from it that is not reachable from any of the
+       refs.
++
+This computation involves traversing all the reachable objects, i.e. it
+has the same cost as 'git prune'.  Fortunately, once this is run, we
+should not have to ever worry about missing objects, because the current
+prune and pack-objects know about reflogs and protect objects referred by
+them.
+
+--expire=<time>::
+       Entries older than this time are pruned.  Without the
+       option it is taken from configuration `gc.reflogExpire`,
+       which in turn defaults to 90 days.
+
+--expire-unreachable=<time>::
+       Entries older than this time and are not reachable from
+       the current tip of the branch are pruned.  Without the
+       option it is taken from configuration
+       `gc.reflogExpireUnreachable`, which in turn defaults to
+       30 days.
+
+--all::
+       Instead of listing <refs> explicitly, prune all refs.
+
+--verbose::
+       Print extra information on screen.
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-relink.txt b/Documentation/git-relink.txt
new file mode 100644 (file)
index 0000000..fe631bb
--- /dev/null
@@ -0,0 +1,36 @@
+git-relink(1)
+=============
+
+NAME
+----
+git-relink - Hardlink common objects in local repositories
+
+SYNOPSIS
+--------
+'git-relink' [--safe] <dir> <dir> [<dir>]\*
+
+DESCRIPTION
+-----------
+This will scan 2 or more object repositories and look for common objects, check
+if they are hardlinked, and replace one with a hardlink to the other if not.
+
+OPTIONS
+-------
+--safe::
+       Stops if two objects with the same hash exist but have different sizes.
+       Default is to warn and continue.
+
+<dir>::
+       Directories containing a .git/objects/ subdirectory.
+
+Author
+------
+Written by Ryan Anderson <ryan@michonline.com>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-remote.txt b/Documentation/git-remote.txt
new file mode 100644 (file)
index 0000000..4b263c2
--- /dev/null
@@ -0,0 +1,147 @@
+git-remote(1)
+============
+
+NAME
+----
+git-remote - manage set of tracked repositories
+
+
+SYNOPSIS
+--------
+[verse]
+'git-remote'
+'git-remote' add [-t <branch>] [-m <branch>] [-f] [--mirror] <name> <url>
+'git-remote' rm <name>
+'git-remote' show <name>
+'git-remote' prune <name>
+'git-remote' update [group]
+
+DESCRIPTION
+-----------
+
+Manage the set of repositories ("remotes") whose branches you track.
+
+
+COMMANDS
+--------
+
+With no arguments, shows a list of existing remotes.  Several
+subcommands are available to perform operations on the remotes.
+
+'add'::
+
+Adds a remote named <name> for the repository at
+<url>.  The command `git fetch <name>` can then be used to create and
+update remote-tracking branches <name>/<branch>.
++
+With `-f` option, `git fetch <name>` is run immediately after
+the remote information is set up.
++
+With `-t <branch>` option, instead of the default glob
+refspec for the remote to track all branches under
+`$GIT_DIR/remotes/<name>/`, a refspec to track only `<branch>`
+is created.  You can give more than one `-t <branch>` to track
+multiple branches without grabbing all branches.
++
+With `-m <master>` option, `$GIT_DIR/remotes/<name>/HEAD` is set
+up to point at remote's `<master>` branch instead of whatever
+branch the `HEAD` at the remote repository actually points at.
++
+In mirror mode, enabled with `--mirror`, the refs will not be stored
+in the 'refs/remotes/' namespace, but in 'refs/heads/'.  This option
+only makes sense in bare repositories.
+
+'rm'::
+
+Remove the remote named <name>. All remote tracking branches and
+configuration settings for the remote are removed.
+
+'show'::
+
+Gives some information about the remote <name>.
++
+With `-n` option, the remote heads are not queried first with
+`git ls-remote <name>`; cached information is used instead.
+
+'prune'::
+
+Deletes all stale tracking branches under <name>.
+These stale branches have already been removed from the remote repository
+referenced by <name>, but are still locally available in
+"remotes/<name>".
++
+With `-n` option, the remote heads are not confirmed first with `git
+ls-remote <name>`; cached information is used instead.  Use with
+caution.
+
+'update'::
+
+Fetch updates for a named set of remotes in the repository as defined by
+remotes.<group>.  If a named group is not specified on the command line,
+the configuration parameter remotes.default will get used; if
+remotes.default is not defined, all remotes which do not have the
+configuration parameter remote.<name>.skipDefaultUpdate set to true will
+be updated.  (See gitlink:git-config[1]).
+
+
+DISCUSSION
+----------
+
+The remote configuration is achieved using the `remote.origin.url` and
+`remote.origin.fetch` configuration variables.  (See
+gitlink:git-config[1]).
+
+Examples
+--------
+
+* Add a new remote, fetch, and check out a branch from it
++
+------------
+$ git remote
+origin
+$ git branch -r
+origin/master
+$ git remote add linux-nfs git://linux-nfs.org/pub/linux/nfs-2.6.git
+$ git remote
+linux-nfs
+origin
+$ git fetch
+* refs/remotes/linux-nfs/master: storing branch 'master' ...
+  commit: bf81b46
+$ git branch -r
+origin/master
+linux-nfs/master
+$ git checkout -b nfs linux-nfs/master
+...
+------------
+
+* Imitate 'git clone' but track only selected branches
++
+------------
+$ mkdir project.git
+$ cd project.git
+$ git init
+$ git remote add -f -t master -m master origin git://example.com/git.git/
+$ git merge origin
+------------
+
+
+See Also
+--------
+gitlink:git-fetch[1]
+gitlink:git-branch[1]
+gitlink:git-config[1]
+
+Author
+------
+Written by Junio Hamano
+
+
+Documentation
+--------------
+Documentation by J. Bruce Fields and the git-list <git@vger.kernel.org>.
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-repack.txt b/Documentation/git-repack.txt
new file mode 100644 (file)
index 0000000..12e2079
--- /dev/null
@@ -0,0 +1,117 @@
+git-repack(1)
+=============
+
+NAME
+----
+git-repack - Pack unpacked objects in a repository
+
+
+SYNOPSIS
+--------
+'git-repack' [-a] [-d] [-f] [-l] [-n] [-q] [--window=N] [--depth=N]
+
+DESCRIPTION
+-----------
+
+This script is used to combine all objects that do not currently
+reside in a "pack", into a pack.  It can also be used to re-organize
+existing packs into a single, more efficient pack.
+
+A pack is a collection of objects, individually compressed, with
+delta compression applied, stored in a single file, with an
+associated index file.
+
+Packs are used to reduce the load on mirror systems, backup
+engines, disk storage, etc.
+
+OPTIONS
+-------
+
+-a::
+       Instead of incrementally packing the unpacked objects,
+       pack everything referenced into a single pack.
+       Especially useful when packing a repository that is used
+       for private development and there is no need to worry
+       about people fetching via dumb protocols from it.  Use
+       with '-d'.  This will clean up the objects that `git prune`
+       leaves behind, but `git fsck --full` shows as
+       dangling.
+
+-d::
+       After packing, if the newly created packs make some
+       existing packs redundant, remove the redundant packs.
+       Also runs gitlink:git-prune-packed[1].
+
+-l::
+        Pass the `--local` option to `git pack-objects`, see
+        gitlink:git-pack-objects[1].
+
+-f::
+        Pass the `--no-reuse-delta` option to `git pack-objects`, see
+        gitlink:git-pack-objects[1].
+
+-q::
+        Pass the `-q` option to `git pack-objects`, see
+        gitlink:git-pack-objects[1].
+
+-n::
+        Do not update the server information with
+        `git update-server-info`.
+
+--window=[N], --depth=[N]::
+       These two options affect how the objects contained in the pack are
+       stored using delta compression. The objects are first internally
+       sorted by type, size and optionally names and compared against the
+       other objects within `--window` to see if using delta compression saves
+       space. `--depth` limits the maximum delta depth; making it too deep
+       affects the performance on the unpacker side, because delta data needs
+       to be applied that many times to get to the necessary object.
+       The default value for --window is 10 and --depth is 50.
+
+--window-memory=[N]::
+       This option provides an additional limit on top of `--window`;
+       the window size will dynamically scale down so as to not take
+       up more than N bytes in memory.  This is useful in
+       repositories with a mix of large and small objects to not run
+       out of memory with a large window, but still be able to take
+       advantage of the large window for the smaller objects.  The
+       size can be suffixed with "k", "m", or "g".
+       `--window-memory=0` makes memory usage unlimited, which is the
+       default.
+
+--max-pack-size=<n>::
+       Maximum size of each output packfile, expressed in MiB.
+       If specified,  multiple packfiles may be created.
+       The default is unlimited.
+
+
+Configuration
+-------------
+
+When configuration variable `repack.UseDeltaBaseOffset` is set
+for the repository, the command passes `--delta-base-offset`
+option to `git-pack-objects`; this typically results in slightly
+smaller packs, but the generated packs are incompatible with
+versions of git older than (and including) v1.4.3; do not set
+the variable in a repository that older version of git needs to
+be able to read (this includes repositories from which packs can
+be copied out over http or rsync, and people who obtained packs
+that way can try to use older git with it).
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Ryan Anderson <ryan@michonline.com>
+
+See Also
+--------
+gitlink:git-pack-objects[1]
+gitlink:git-prune-packed[1]
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-repo-config.txt b/Documentation/git-repo-config.txt
new file mode 100644 (file)
index 0000000..2deba31
--- /dev/null
@@ -0,0 +1,18 @@
+git-repo-config(1)
+==================
+
+NAME
+----
+git-repo-config - Get and set repository or global options
+
+
+SYNOPSIS
+--------
+'git-repo-config' ...
+
+
+DESCRIPTION
+-----------
+
+This is a synonym for gitlink:git-config[1].  Please refer to the
+documentation of that command.
diff --git a/Documentation/git-request-pull.txt b/Documentation/git-request-pull.txt
new file mode 100644 (file)
index 0000000..087eeb7
--- /dev/null
@@ -0,0 +1,39 @@
+git-request-pull(1)
+===================
+
+NAME
+----
+git-request-pull - Generates a summary of pending changes
+
+SYNOPSIS
+--------
+'git-request-pull' <start> <url> [<end>]
+
+DESCRIPTION
+-----------
+
+Summarizes the changes between two commits to the standard output, and includes
+the given URL in the generated summary.
+
+OPTIONS
+-------
+<start>::
+       Commit to start at.
+
+<url>::
+       URL to include in the summary.
+
+<end>::
+       Commit to send at; defaults to HEAD.
+
+Author
+------
+Written by Ryan Anderson <ryan@michonline.com> and Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-rerere.txt b/Documentation/git-rerere.txt
new file mode 100644 (file)
index 0000000..8ce492c
--- /dev/null
@@ -0,0 +1,207 @@
+git-rerere(1)
+=============
+
+NAME
+----
+git-rerere - Reuse recorded resolution of conflicted merges
+
+SYNOPSIS
+--------
+'git-rerere' [clear|diff|status|gc]
+
+DESCRIPTION
+-----------
+
+In a workflow that employs relatively long lived topic branches,
+the developer sometimes needs to resolve the same conflict over
+and over again until the topic branches are done (either merged
+to the "release" branch, or sent out and accepted upstream).
+
+This command helps this process by recording conflicted
+automerge results and corresponding hand-resolve results on the
+initial manual merge, and later by noticing the same automerge
+results and applying the previously recorded hand resolution.
+
+
+COMMANDS
+--------
+
+Normally, git-rerere is run without arguments or user-intervention.
+However, it has several commands that allow it to interact with
+its working state.
+
+'clear'::
+
+This resets the metadata used by rerere if a merge resolution is to be
+is aborted.  Calling gitlink:git-am[1] --skip or gitlink:git-rebase[1]
+[--skip|--abort] will automatically invoke this command.
+
+'diff'::
+
+This displays diffs for the current state of the resolution.  It is
+useful for tracking what has changed while the user is resolving
+conflicts.  Additional arguments are passed directly to the system
+diff(1) command installed in PATH.
+
+'status'::
+
+Like diff, but this only prints the filenames that will be tracked
+for resolutions.
+
+'gc'::
+
+This command is used to prune records of conflicted merge that
+occurred long time ago.  By default, conflicts older than 15
+days that you have not recorded their resolution, and conflicts
+older than 60 days, are pruned.  These are controlled with
+`gc.rerereunresolved` and `gc.rerereresolved` configuration
+variables.
+
+
+DISCUSSION
+----------
+
+When your topic branch modifies overlapping area that your
+master branch (or upstream) touched since your topic branch
+forked from it, you may want to test it with the latest master,
+even before your topic branch is ready to be pushed upstream:
+
+------------
+              o---*---o topic
+             /
+    o---o---o---*---o---o master
+------------
+
+For such a test, you need to merge master and topic somehow.
+One way to do it is to pull master into the topic branch:
+
+------------
+       $ git checkout topic
+       $ git merge master
+
+              o---*---o---+ topic
+             /           /
+    o---o---o---*---o---o master
+------------
+
+The commits marked with `*` touch the same area in the same
+file; you need to resolve the conflicts when creating the commit
+marked with `+`.  Then you can test the result to make sure your
+work-in-progress still works with what is in the latest master.
+
+After this test merge, there are two ways to continue your work
+on the topic.  The easiest is to build on top of the test merge
+commit `+`, and when your work in the topic branch is finally
+ready, pull the topic branch into master, and/or ask the
+upstream to pull from you.  By that time, however, the master or
+the upstream might have been advanced since the test merge `+`,
+in which case the final commit graph would look like this:
+
+------------
+       $ git checkout topic
+       $ git merge master
+       $ ... work on both topic and master branches
+       $ git checkout master
+       $ git merge topic
+
+              o---*---o---+---o---o topic
+             /           /         \
+    o---o---o---*---o---o---o---o---+ master
+------------
+
+When your topic branch is long-lived, however, your topic branch
+would end up having many such "Merge from master" commits on it,
+which would unnecessarily clutter the development history.
+Readers of the Linux kernel mailing list may remember that Linus
+complained about such too frequent test merges when a subsystem
+maintainer asked to pull from a branch full of "useless merges".
+
+As an alternative, to keep the topic branch clean of test
+merges, you could blow away the test merge, and keep building on
+top of the tip before the test merge:
+
+------------
+       $ git checkout topic
+       $ git merge master
+       $ git reset --hard HEAD^ ;# rewind the test merge
+       $ ... work on both topic and master branches
+       $ git checkout master
+       $ git merge topic
+
+              o---*---o-------o---o topic
+             /                     \
+    o---o---o---*---o---o---o---o---+ master
+------------
+
+This would leave only one merge commit when your topic branch is
+finally ready and merged into the master branch.  This merge
+would require you to resolve the conflict, introduced by the
+commits marked with `*`.  However, often this conflict is the
+same conflict you resolved when you created the test merge you
+blew away.  `git-rerere` command helps you to resolve this final
+conflicted merge using the information from your earlier hand
+resolve.
+
+Running `git-rerere` command immediately after a conflicted
+automerge records the conflicted working tree files, with the
+usual conflict markers `<<<<<<<`, `=======`, and `>>>>>>>` in
+them.  Later, after you are done resolving the conflicts,
+running `git-rerere` again records the resolved state of these
+files.  Suppose you did this when you created the test merge of
+master into the topic branch.
+
+Next time, running `git-rerere` after seeing a conflicted
+automerge, if the conflict is the same as the earlier one
+recorded, it is noticed and a three-way merge between the
+earlier conflicted automerge, the earlier manual resolution, and
+the current conflicted automerge is performed by the command.
+If this three-way merge resolves cleanly, the result is written
+out to your working tree file, so you would not have to manually
+resolve it.  Note that `git-rerere` leaves the index file alone,
+so you still need to do the final sanity checks with `git diff`
+(or `git diff -c`) and `git add` when you are satisfied.
+
+As a convenience measure, `git-merge` automatically invokes
+`git-rerere` when it exits with a failed automerge, which
+records it if it is a new conflict, or reuses the earlier hand
+resolve when it is not.  `git-commit` also invokes `git-rerere`
+when recording a merge result.  What this means is that you do
+not have to do anything special yourself (Note: you still have
+to set the config variable rerere.enabled to enable this command).
+
+In our example, when you did the test merge, the manual
+resolution is recorded, and it will be reused when you do the
+actual merge later with updated master and topic branch, as long
+as the earlier resolution is still applicable.
+
+The information `git-rerere` records is also used when running
+`git-rebase`.  After blowing away the test merge and continuing
+development on the topic branch:
+
+------------
+              o---*---o-------o---o topic
+             /
+    o---o---o---*---o---o---o---o   master
+
+       $ git rebase master topic
+
+                                 o---*---o-------o---o topic
+                                /
+    o---o---o---*---o---o---o---o   master
+------------
+
+you could run `git rebase master topic`, to keep yourself
+up-to-date even before your topic is ready to be sent upstream.
+This would result in falling back to three-way merge, and it
+would conflict the same way the test merge you resolved earlier.
+`git-rerere` is run by `git rebase` to help you resolve this
+conflict.
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-reset.txt b/Documentation/git-reset.txt
new file mode 100644 (file)
index 0000000..050e4ea
--- /dev/null
@@ -0,0 +1,189 @@
+git-reset(1)
+============
+
+NAME
+----
+git-reset - Reset current HEAD to the specified state
+
+SYNOPSIS
+--------
+[verse]
+'git-reset' [--mixed | --soft | --hard] [-q] [<commit>]
+'git-reset' [--mixed] [-q] <commit> [--] <paths>...
+
+DESCRIPTION
+-----------
+Sets the current head to the specified commit and optionally resets the
+index and working tree to match.
+
+This command is useful if you notice some small error in a recent
+commit (or set of commits) and want to redo that part without showing
+the undo in the history.
+
+If you want to undo a commit other than the latest on a branch,
+gitlink:git-revert[1] is your friend.
+
+The second form with 'paths' is used to revert selected paths in
+the index from a given commit, without moving HEAD.
+
+
+OPTIONS
+-------
+--mixed::
+       Resets the index but not the working tree (i.e., the changed files
+       are preserved but not marked for commit) and reports what has not
+       been updated. This is the default action.
+
+--soft::
+       Does not touch the index file nor the working tree at all, but
+       requires them to be in a good order. This leaves all your changed
+       files "Added but not yet committed", as gitlink:git-status[1] would
+       put it.
+
+--hard::
+       Matches the working tree and index to that of the tree being
+       switched to. Any changes to tracked files in the working tree
+       since <commit> are lost.
+
+-q::
+       Be quiet, only report errors.
+
+<commit>::
+       Commit to make the current HEAD.
+
+Examples
+--------
+
+Undo a commit and redo::
++
+------------
+$ git commit ...
+$ git reset --soft HEAD^      <1>
+$ edit                        <2>
+$ git commit -a -c ORIG_HEAD  <3>
+------------
++
+<1> This is most often done when you remembered what you
+just committed is incomplete, or you misspelled your commit
+message, or both.  Leaves working tree as it was before "reset".
+<2> Make corrections to working tree files.
+<3> "reset" copies the old head to .git/ORIG_HEAD; redo the
+commit by starting with its log message.  If you do not need to
+edit the message further, you can give -C option instead.
++
+See also the --amend option to gitlink:git-commit[1].
+
+Undo commits permanently::
++
+------------
+$ git commit ...
+$ git reset --hard HEAD~3   <1>
+------------
++
+<1> The last three commits (HEAD, HEAD^, and HEAD~2) were bad
+and you do not want to ever see them again.  Do *not* do this if
+you have already given these commits to somebody else.
+
+Undo a commit, making it a topic branch::
++
+------------
+$ git branch topic/wip     <1>
+$ git reset --hard HEAD~3  <2>
+$ git checkout topic/wip   <3>
+------------
++
+<1> You have made some commits, but realize they were premature
+to be in the "master" branch.  You want to continue polishing
+them in a topic branch, so create "topic/wip" branch off of the
+current HEAD.
+<2> Rewind the master branch to get rid of those three commits.
+<3> Switch to "topic/wip" branch and keep working.
+
+Undo add::
++
+------------
+$ edit                                     <1>
+$ git add frotz.c filfre.c
+$ mailx                                    <2>
+$ git reset                                <3>
+$ git pull git://info.example.com/ nitfol  <4>
+------------
++
+<1> You are happily working on something, and find the changes
+in these files are in good order.  You do not want to see them
+when you run "git diff", because you plan to work on other files
+and changes with these files are distracting.
+<2> Somebody asks you to pull, and the changes sounds worthy of merging.
+<3> However, you already dirtied the index (i.e. your index does
+not match the HEAD commit).  But you know the pull you are going
+to make does not affect frotz.c nor filfre.c, so you revert the
+index changes for these two files.  Your changes in working tree
+remain there.
+<4> Then you can pull and merge, leaving frotz.c and filfre.c
+changes still in the working tree.
+
+Undo a merge or pull::
++
+------------
+$ git pull                         <1>
+Auto-merging nitfol
+CONFLICT (content): Merge conflict in nitfol
+Automatic merge failed/prevented; fix up by hand
+$ git reset --hard                 <2>
+$ git pull . topic/branch          <3>
+Updating from 41223... to 13134...
+Fast forward
+$ git reset --hard ORIG_HEAD       <4>
+------------
++
+<1> Try to update from the upstream resulted in a lot of
+conflicts; you were not ready to spend a lot of time merging
+right now, so you decide to do that later.
+<2> "pull" has not made merge commit, so "git reset --hard"
+which is a synonym for "git reset --hard HEAD" clears the mess
+from the index file and the working tree.
+<3> Merge a topic branch into the current branch, which resulted
+in a fast forward.
+<4> But you decided that the topic branch is not ready for public
+consumption yet.  "pull" or "merge" always leaves the original
+tip of the current branch in ORIG_HEAD, so resetting hard to it
+brings your index file and the working tree back to that state,
+and resets the tip of the branch to that commit.
+
+Interrupted workflow::
++
+Suppose you are interrupted by an urgent fix request while you
+are in the middle of a large change.  The files in your
+working tree are not in any shape to be committed yet, but you
+need to get to the other branch for a quick bugfix.
++
+------------
+$ git checkout feature ;# you were working in "feature" branch and
+$ work work work       ;# got interrupted
+$ git commit -a -m "snapshot WIP"                 <1>
+$ git checkout master
+$ fix fix fix
+$ git commit ;# commit with real log
+$ git checkout feature
+$ git reset --soft HEAD^ ;# go back to WIP state  <2>
+$ git reset                                       <3>
+------------
++
+<1> This commit will get blown away so a throw-away log message is OK.
+<2> This removes the 'WIP' commit from the commit history, and sets
+    your working tree to the state just before you made that snapshot.
+<3> At this point the index file still has all the WIP changes you
+    committed as 'snapshot WIP'.  This updates the index to show your
+    WIP files as uncommitted.
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net> and Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-rev-list.txt b/Documentation/git-rev-list.txt
new file mode 100644 (file)
index 0000000..989fbf3
--- /dev/null
@@ -0,0 +1,453 @@
+git-rev-list(1)
+===============
+
+NAME
+----
+git-rev-list - Lists commit objects in reverse chronological order
+
+
+SYNOPSIS
+--------
+[verse]
+'git-rev-list' [ \--max-count=number ]
+            [ \--skip=number ]
+            [ \--max-age=timestamp ]
+            [ \--min-age=timestamp ]
+            [ \--sparse ]
+            [ \--no-merges ]
+            [ \--remove-empty ]
+            [ \--full-history ]
+            [ \--not ]
+            [ \--all ]
+            [ \--stdin ]
+            [ \--quiet ]
+            [ \--topo-order ]
+            [ \--parents ]
+            [ \--timestamp ]
+            [ \--left-right ]
+            [ \--cherry-pick ]
+            [ \--encoding[=<encoding>] ]
+            [ \--(author|committer|grep)=<pattern> ]
+            [ \--regexp-ignore-case | \-i ]
+            [ \--extended-regexp | \-E ]
+            [ \--date={local|relative|default|iso|rfc|short} ]
+            [ [\--objects | \--objects-edge] [ \--unpacked ] ]
+            [ \--pretty | \--header ]
+            [ \--bisect ]
+            [ \--bisect-vars ]
+            [ \--bisect-all ]
+            [ \--merge ]
+            [ \--reverse ]
+            [ \--walk-reflogs ]
+            [ \--no-walk ] [ \--do-walk ]
+            <commit>... [ \-- <paths>... ]
+
+DESCRIPTION
+-----------
+
+Lists commit objects in reverse chronological order starting at the
+given commit(s), taking ancestry relationship into account.  This is
+useful to produce human-readable log output.
+
+Commits which are stated with a preceding '{caret}' cause listing to
+stop at that point. Their parents are implied. Thus the following
+command:
+
+-----------------------------------------------------------------------
+       $ git-rev-list foo bar ^baz
+-----------------------------------------------------------------------
+
+means "list all the commits which are included in 'foo' and 'bar', but
+not in 'baz'".
+
+A special notation "'<commit1>'..'<commit2>'" can be used as a
+short-hand for "{caret}'<commit1>' '<commit2>'". For example, either of
+the following may be used interchangeably:
+
+-----------------------------------------------------------------------
+       $ git-rev-list origin..HEAD
+       $ git-rev-list HEAD ^origin
+-----------------------------------------------------------------------
+
+Another special notation is "'<commit1>'...'<commit2>'" which is useful
+for merges.  The resulting set of commits is the symmetric difference
+between the two operands.  The following two commands are equivalent:
+
+-----------------------------------------------------------------------
+       $ git-rev-list A B --not $(git-merge-base --all A B)
+       $ git-rev-list A...B
+-----------------------------------------------------------------------
+
+gitlink:git-rev-list[1] is a very essential git program, since it
+provides the ability to build and traverse commit ancestry graphs. For
+this reason, it has a lot of different options that enables it to be
+used by commands as different as gitlink:git-bisect[1] and
+gitlink:git-repack[1].
+
+OPTIONS
+-------
+
+Commit Formatting
+~~~~~~~~~~~~~~~~~
+
+Using these options, gitlink:git-rev-list[1] will act similar to the
+more specialized family of commit log tools: gitlink:git-log[1],
+gitlink:git-show[1], and gitlink:git-whatchanged[1]
+
+include::pretty-options.txt[]
+
+--relative-date::
+
+       Synonym for `--date=relative`.
+
+--date={relative,local,default,iso,rfc}::
+
+       Only takes effect for dates shown in human-readable format, such
+       as when using "--pretty".
++
+`--date=relative` shows dates relative to the current time,
+e.g. "2 hours ago".
++
+`--date=local` shows timestamps in user's local timezone.
++
+`--date=iso` (or `--date=iso8601`) shows timestamps in ISO 8601 format.
++
+`--date=rfc` (or `--date=rfc2822`) shows timestamps in RFC 2822
+format, often found in E-mail messages.
++
+`--date=short` shows only date but not time, in `YYYY-MM-DD` format.
++
+`--date=default` shows timestamps in the original timezone
+(either committer's or author's).
+
+--header::
+
+       Print the contents of the commit in raw-format; each record is
+       separated with a NUL character.
+
+--parents::
+
+       Print the parents of the commit.
+
+--timestamp::
+       Print the raw commit timestamp.
+
+--left-right::
+
+       Mark which side of a symmetric diff a commit is reachable from.
+       Commits from the left side are prefixed with `<` and those from
+       the right with `>`.  If combined with `--boundary`, those
+       commits are prefixed with `-`.
++
+For example, if you have this topology:
++
+-----------------------------------------------------------------------
+             y---b---b  branch B
+            / \ /
+           /   .
+          /   / \
+         o---x---a---a  branch A
+-----------------------------------------------------------------------
++
+you would get an output line this:
++
+-----------------------------------------------------------------------
+       $ git rev-list --left-right --boundary --pretty=oneline A...B
+
+       >bbbbbbb... 3rd on b
+       >bbbbbbb... 2nd on b
+       <aaaaaaa... 3rd on a
+       <aaaaaaa... 2nd on a
+       -yyyyyyy... 1st on b
+       -xxxxxxx... 1st on a
+-----------------------------------------------------------------------
+
+Diff Formatting
+~~~~~~~~~~~~~~~
+
+Below are listed options that control the formatting of diff output.
+Some of them are specific to gitlink:git-rev-list[1], however other diff
+options may be given. See gitlink:git-diff-files[1] for more options.
+
+-c::
+
+       This flag changes the way a merge commit is displayed.  It shows
+       the differences from each of the parents to the merge result
+       simultaneously instead of showing pairwise diff between a parent
+       and the result one at a time. Furthermore, it lists only files
+       which were modified from all parents.
+
+--cc::
+
+       This flag implies the '-c' options and further compresses the
+       patch output by omitting hunks that show differences from only
+       one parent, or show the same change from all but one parent for
+       an Octopus merge.
+
+-r::
+
+       Show recursive diffs.
+
+-t::
+
+       Show the tree objects in the diff output. This implies '-r'.
+
+Commit Limiting
+~~~~~~~~~~~~~~~
+
+Besides specifying a range of commits that should be listed using the
+special notations explained in the description, additional commit
+limiting may be applied.
+
+--
+
+-n 'number', --max-count='number'::
+
+       Limit the number of commits output.
+
+--skip='number'::
+
+       Skip 'number' commits before starting to show the commit output.
+
+--since='date', --after='date'::
+
+       Show commits more recent than a specific date.
+
+--until='date', --before='date'::
+
+       Show commits older than a specific date.
+
+--max-age='timestamp', --min-age='timestamp'::
+
+       Limit the commits output to specified time range.
+
+--author='pattern', --committer='pattern'::
+
+       Limit the commits output to ones with author/committer
+       header lines that match the specified pattern (regular expression).
+
+--grep='pattern'::
+
+       Limit the commits output to ones with log message that
+       matches the specified pattern (regular expression).
+
+-i, --regexp-ignore-case::
+
+       Match the regexp limiting patterns without regard to letters case.
+
+-E, --extended-regexp::
+
+       Consider the limiting patterns to be extended regular expressions
+       instead of the default basic regular expressions.
+
+--remove-empty::
+
+       Stop when a given path disappears from the tree.
+
+--full-history::
+
+       Show also parts of history irrelevant to current state of a given
+       path. This turns off history simplification, which removed merges
+       which didn't change anything at all at some child. It will still actually
+       simplify away merges that didn't change anything at all into either
+       child.
+
+--no-merges::
+
+       Do not print commits with more than one parent.
+
+--not::
+
+       Reverses the meaning of the '{caret}' prefix (or lack thereof)
+       for all following revision specifiers, up to the next '--not'.
+
+--all::
+
+       Pretend as if all the refs in `$GIT_DIR/refs/` are listed on the
+       command line as '<commit>'.
+
+--stdin::
+
+       In addition to the '<commit>' listed on the command
+       line, read them from the standard input.
+
+--quiet::
+
+       Don't print anything to standard output.  This form of
+       git-rev-list is primarly meant to allow the caller to
+       test the exit status to see if a range of objects is fully
+       connected (or not).  It is faster than redirecting stdout
+       to /dev/null as the output does not have to be formatted.
+
+--cherry-pick::
+
+       Omit any commit that introduces the same change as
+       another commit on the "other side" when the set of
+       commits are limited with symmetric difference.
++
+For example, if you have two branches, `A` and `B`, a usual way
+to list all commits on only one side of them is with
+`--left-right`, like the example above in the description of
+that option.  It however shows the commits that were cherry-picked
+from the other branch (for example, "3rd on b" may be cherry-picked
+from branch A).  With this option, such pairs of commits are
+excluded from the output.
+
+-g, --walk-reflogs::
+
+       Instead of walking the commit ancestry chain, walk
+       reflog entries from the most recent one to older ones.
+       When this option is used you cannot specify commits to
+       exclude (that is, '{caret}commit', 'commit1..commit2',
+       nor 'commit1...commit2' notations cannot be used).
++
+With '\--pretty' format other than oneline (for obvious reasons),
+this causes the output to have two extra lines of information
+taken from the reflog.  By default, 'commit@\{Nth}' notation is
+used in the output.  When the starting commit is specified as
+'commit@{now}', output also uses 'commit@\{timestamp}' notation
+instead.  Under '\--pretty=oneline', the commit message is
+prefixed with this information on the same line.
+
+Cannot be combined with '\--reverse'.
+
+--merge::
+
+       After a failed merge, show refs that touch files having a
+       conflict and don't exist on all heads to merge.
+
+--boundary::
+
+       Output uninteresting commits at the boundary, which are usually
+       not shown.
+
+--dense, --sparse::
+
+When optional paths are given, the default behaviour ('--dense') is to
+only output commits that changes at least one of them, and also ignore
+merges that do not touch the given paths.
+
+Use the '--sparse' flag to makes the command output all eligible commits
+(still subject to count and age limitation), but apply merge
+simplification nevertheless.
+
+--bisect::
+
+Limit output to the one commit object which is roughly halfway between
+the included and excluded commits. Thus, if
+
+-----------------------------------------------------------------------
+       $ git-rev-list --bisect foo ^bar ^baz
+-----------------------------------------------------------------------
+
+outputs 'midpoint', the output of the two commands
+
+-----------------------------------------------------------------------
+       $ git-rev-list foo ^midpoint
+       $ git-rev-list midpoint ^bar ^baz
+-----------------------------------------------------------------------
+
+would be of roughly the same length.  Finding the change which
+introduces a regression is thus reduced to a binary search: repeatedly
+generate and test new 'midpoint's until the commit chain is of length
+one.
+
+--bisect-vars::
+
+This calculates the same as `--bisect`, but outputs text ready
+to be eval'ed by the shell. These lines will assign the name of
+the midpoint revision to the variable `bisect_rev`, and the
+expected number of commits to be tested after `bisect_rev` is
+tested to `bisect_nr`, the expected number of commits to be
+tested if `bisect_rev` turns out to be good to `bisect_good`,
+the expected number of commits to be tested if `bisect_rev`
+turns out to be bad to `bisect_bad`, and the number of commits
+we are bisecting right now to `bisect_all`.
+
+--bisect-all::
+
+This outputs all the commit objects between the included and excluded
+commits, ordered by their distance to the included and excluded
+commits. The farthest from them is displayed first. (This is the only
+one displayed by `--bisect`.)
+
+This is useful because it makes it easy to choose a good commit to
+test when you want to avoid to test some of them for some reason (they
+may not compile for example).
+
+This option can be used along with `--bisect-vars`, in this case,
+after all the sorted commit objects, there will be the same text as if
+`--bisect-vars` had been used alone.
+
+--
+
+Commit Ordering
+~~~~~~~~~~~~~~~
+
+By default, the commits are shown in reverse chronological order.
+
+--topo-order::
+
+       This option makes them appear in topological order (i.e.
+       descendant commits are shown before their parents).
+
+--date-order::
+
+       This option is similar to '--topo-order' in the sense that no
+       parent comes before all of its children, but otherwise things
+       are still ordered in the commit timestamp order.
+
+--reverse::
+
+       Output the commits in reverse order.
+       Cannot be combined with '\--walk-reflogs'.
+
+Object Traversal
+~~~~~~~~~~~~~~~~
+
+These options are mostly targeted for packing of git repositories.
+
+--objects::
+
+       Print the object IDs of any object referenced by the listed
+       commits.  'git-rev-list --objects foo ^bar' thus means "send me
+       all object IDs which I need to download if I have the commit
+       object 'bar', but not 'foo'".
+
+--objects-edge::
+
+       Similar to '--objects', but also print the IDs of excluded
+       commits prefixed with a "-" character.  This is used by
+       gitlink:git-pack-objects[1] to build "thin" pack, which records
+       objects in deltified form based on objects contained in these
+       excluded commits to reduce network traffic.
+
+--unpacked::
+
+       Only useful with '--objects'; print the object IDs that are not
+       in packs.
+
+--no-walk::
+
+       Only show the given revs, but do not traverse their ancestors.
+
+--do-walk::
+
+       Overrides a previous --no-walk.
+
+
+include::pretty-formats.txt[]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano, Jonas Fonseca
+and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
new file mode 100644 (file)
index 0000000..329fce0
--- /dev/null
@@ -0,0 +1,374 @@
+git-rev-parse(1)
+================
+
+NAME
+----
+git-rev-parse - Pick out and massage parameters
+
+
+SYNOPSIS
+--------
+'git-rev-parse' [ --option ] <args>...
+
+DESCRIPTION
+-----------
+
+Many git porcelainish commands take mixture of flags
+(i.e. parameters that begin with a dash '-') and parameters
+meant for underlying `git-rev-list` command they use internally
+and flags and parameters for other commands they use as the
+downstream of `git-rev-list`.  This command is used to
+distinguish between them.
+
+
+OPTIONS
+-------
+--parseopt::
+       Use `git-rev-parse` in option parsing mode (see PARSEOPT section below).
+
+--keep-dash-dash::
+       Only meaningful in `--parseopt` mode. Tells the option parser to echo
+       out the first `--` met instead of skipping it.
+
+--revs-only::
+       Do not output flags and parameters not meant for
+       `git-rev-list` command.
+
+--no-revs::
+       Do not output flags and parameters meant for
+       `git-rev-list` command.
+
+--flags::
+       Do not output non-flag parameters.
+
+--no-flags::
+       Do not output flag parameters.
+
+--default <arg>::
+       If there is no parameter given by the user, use `<arg>`
+       instead.
+
+--verify::
+       The parameter given must be usable as a single, valid
+       object name.  Otherwise barf and abort.
+
+--sq::
+       Usually the output is made one line per flag and
+       parameter.  This option makes output a single line,
+       properly quoted for consumption by shell.  Useful when
+       you expect your parameter to contain whitespaces and
+       newlines (e.g. when using pickaxe `-S` with
+       `git-diff-\*`).
+
+--not::
+       When showing object names, prefix them with '{caret}' and
+       strip '{caret}' prefix from the object names that already have
+       one.
+
+--symbolic::
+       Usually the object names are output in SHA1 form (with
+       possible '{caret}' prefix); this option makes them output in a
+       form as close to the original input as possible.
+
+
+--all::
+       Show all refs found in `$GIT_DIR/refs`.
+
+--branches::
+       Show branch refs found in `$GIT_DIR/refs/heads`.
+
+--tags::
+       Show tag refs found in `$GIT_DIR/refs/tags`.
+
+--remotes::
+       Show tag refs found in `$GIT_DIR/refs/remotes`.
+
+--show-prefix::
+       When the command is invoked from a subdirectory, show the
+       path of the current directory relative to the top-level
+       directory.
+
+--show-cdup::
+       When the command is invoked from a subdirectory, show the
+       path of the top-level directory relative to the current
+       directory (typically a sequence of "../", or an empty string).
+
+--git-dir::
+       Show `$GIT_DIR` if defined else show the path to the .git directory.
+
+--is-inside-git-dir::
+       When the current working directory is below the repository
+       directory print "true", otherwise "false".
+
+--is-inside-work-tree::
+       When the current working directory is inside the work tree of the
+       repository print "true", otherwise "false".
+
+--is-bare-repository::
+       When the repository is bare print "true", otherwise "false".
+
+--short, --short=number::
+       Instead of outputting the full SHA1 values of object names try to
+       abbreviate them to a shorter unique name. When no length is specified
+       7 is used. The minimum length is 4.
+
+--since=datestring, --after=datestring::
+       Parses the date string, and outputs corresponding
+       --max-age= parameter for git-rev-list command.
+
+--until=datestring, --before=datestring::
+       Parses the date string, and outputs corresponding
+       --min-age= parameter for git-rev-list command.
+
+<args>...::
+       Flags and parameters to be parsed.
+
+
+SPECIFYING REVISIONS
+--------------------
+
+A revision parameter typically, but not necessarily, names a
+commit object.  They use what is called an 'extended SHA1'
+syntax.  Here are various ways to spell object names.  The
+ones listed near the end of this list are to name trees and
+blobs contained in a commit.
+
+* The full SHA1 object name (40-byte hexadecimal string), or
+  a substring of such that is unique within the repository.
+  E.g. dae86e1950b1277e545cee180551750029cfe735 and dae86e both
+  name the same commit object if there are no other object in
+  your repository whose object name starts with dae86e.
+
+* An output from `git-describe`; i.e. a closest tag, followed by a
+  dash, a `g`, and an abbreviated object name.
+
+* A symbolic ref name.  E.g. 'master' typically means the commit
+  object referenced by $GIT_DIR/refs/heads/master.  If you
+  happen to have both heads/master and tags/master, you can
+  explicitly say 'heads/master' to tell git which one you mean.
+  When ambiguous, a `<name>` is disambiguated by taking the
+  first match in the following rules:
+
+  . if `$GIT_DIR/<name>` exists, that is what you mean (this is usually
+    useful only for `HEAD`, `FETCH_HEAD` and `MERGE_HEAD`);
+
+  . otherwise, `$GIT_DIR/refs/<name>` if exists;
+
+  . otherwise, `$GIT_DIR/refs/tags/<name>` if exists;
+
+  . otherwise, `$GIT_DIR/refs/heads/<name>` if exists;
+
+  . otherwise, `$GIT_DIR/refs/remotes/<name>` if exists;
+
+  . otherwise, `$GIT_DIR/refs/remotes/<name>/HEAD` if exists.
+
+* A ref followed by the suffix '@' with a date specification
+  enclosed in a brace
+  pair (e.g. '\{yesterday\}', '\{1 month 2 weeks 3 days 1 hour 1
+  second ago\}' or '\{1979-02-26 18:30:00\}') to specify the value
+  of the ref at a prior point in time.  This suffix may only be
+  used immediately following a ref name and the ref must have an
+  existing log ($GIT_DIR/logs/<ref>).
+
+* A ref followed by the suffix '@' with an ordinal specification
+  enclosed in a brace pair (e.g. '\{1\}', '\{15\}') to specify
+  the n-th prior value of that ref.  For example 'master@\{1\}'
+  is the immediate prior value of 'master' while 'master@\{5\}'
+  is the 5th prior value of 'master'. This suffix may only be used
+  immediately following a ref name and the ref must have an existing
+  log ($GIT_DIR/logs/<ref>).
+
+* You can use the '@' construct with an empty ref part to get at a
+  reflog of the current branch. For example, if you are on the
+  branch 'blabla', then '@\{1\}' means the same as 'blabla@\{1\}'.
+
+* A suffix '{caret}' to a revision parameter means the first parent of
+  that commit object.  '{caret}<n>' means the <n>th parent (i.e.
+  'rev{caret}'
+  is equivalent to 'rev{caret}1').  As a special rule,
+  'rev{caret}0' means the commit itself and is used when 'rev' is the
+  object name of a tag object that refers to a commit object.
+
+* A suffix '{tilde}<n>' to a revision parameter means the commit
+  object that is the <n>th generation grand-parent of the named
+  commit object, following only the first parent.  I.e. rev~3 is
+  equivalent to rev{caret}{caret}{caret} which is equivalent to
+  rev{caret}1{caret}1{caret}1.  See below for a illustration of
+  the usage of this form.
+
+* A suffix '{caret}' followed by an object type name enclosed in
+  brace pair (e.g. `v0.99.8{caret}\{commit\}`) means the object
+  could be a tag, and dereference the tag recursively until an
+  object of that type is found or the object cannot be
+  dereferenced anymore (in which case, barf).  `rev{caret}0`
+  introduced earlier is a short-hand for `rev{caret}\{commit\}`.
+
+* A suffix '{caret}' followed by an empty brace pair
+  (e.g. `v0.99.8{caret}\{\}`) means the object could be a tag,
+  and dereference the tag recursively until a non-tag object is
+  found.
+
+* A colon, followed by a slash, followed by a text: this names
+  a commit whose commit message starts with the specified text.
+  This name returns the youngest matching commit which is
+  reachable from any ref.  If the commit message starts with a
+  '!', you have to repeat that;  the special sequence ':/!',
+  followed by something else than '!' is reserved for now.
+
+* A suffix ':' followed by a path; this names the blob or tree
+  at the given path in the tree-ish object named by the part
+  before the colon.
+
+* A colon, optionally followed by a stage number (0 to 3) and a
+  colon, followed by a path; this names a blob object in the
+  index at the given path.  Missing stage number (and the colon
+  that follows it) names an stage 0 entry. During a merge, stage
+  1 is the common ancestor, stage 2 is the target branch's version
+  (typically the current branch), and stage 3 is the version from
+  the branch being merged.
+
+Here is an illustration, by Jon Loeliger.  Both node B and C are
+a commit parents of commit node A.  Parent commits are ordered
+left-to-right.
+
+    G   H   I   J
+     \ /     \ /
+      D   E   F
+       \  |  / \ 
+        \ | /   |
+         \|/    |
+          B     C
+           \   /
+            \ /
+             A
+
+    A =      = A^0
+    B = A^   = A^1     = A~1
+    C = A^2  = A^2
+    D = A^^  = A^1^1   = A~2
+    E = B^2  = A^^2
+    F = B^3  = A^^3
+    G = A^^^ = A^1^1^1 = A~3
+    H = D^2  = B^^2    = A^^^2  = A~2^2
+    I = F^   = B^3^    = A^^3^
+    J = F^2  = B^3^2   = A^^3^2
+
+
+SPECIFYING RANGES
+-----------------
+
+History traversing commands such as `git-log` operate on a set
+of commits, not just a single commit.  To these commands,
+specifying a single revision with the notation described in the
+previous section means the set of commits reachable from that
+commit, following the commit ancestry chain.
+
+To exclude commits reachable from a commit, a prefix `{caret}`
+notation is used.  E.g. "`{caret}r1 r2`" means commits reachable
+from `r2` but exclude the ones reachable from `r1`.
+
+This set operation appears so often that there is a shorthand
+for it.  "`r1..r2`" is equivalent to "`{caret}r1 r2`".  It is
+the difference of two sets (subtract the set of commits
+reachable from `r1` from the set of commits reachable from
+`r2`).
+
+A similar notation "`r1\...r2`" is called symmetric difference
+of `r1` and `r2` and is defined as
+"`r1 r2 --not $(git-merge-base --all r1 r2)`".
+It is the set of commits that are reachable from either one of
+`r1` or `r2` but not from both.
+
+Two other shorthands for naming a set that is formed by a commit
+and its parent commits exists.  `r1{caret}@` notation means all
+parents of `r1`.  `r1{caret}!` includes commit `r1` but excludes
+its all parents.
+
+Here are a handful examples:
+
+   D                G H D
+   D F              G H I J D F
+   ^G D             H D
+   ^D B             E I J F B
+   B...C            G H D E B C
+   ^D B C           E I J F B C
+   C^@              I J F
+   F^! D            G H D F
+
+PARSEOPT
+--------
+
+In `--parseopt` mode, `git-rev-parse` helps massaging options to bring to shell
+scripts the same facilities C builtins have. It works as an option normalizer
+(e.g. splits single switches aggregate values), a bit like `getopt(1)` does.
+
+It takes on the standard input the specification of the options to parse and
+understand, and echoes on the standard output a line suitable for `sh(1)` `eval`
+to replace the arguments with normalized ones.  In case of error, it outputs
+usage on the standard error stream, and exits with code 129.
+
+Input Format
+~~~~~~~~~~~~
+
+`git-rev-parse --parseopt` input format is fully text based. It has two parts,
+separated by a line that contains only `--`. The lines before the separator
+(should be more than one) are used for the usage.
+The lines after the separator describe the options.
+
+Each line of options has this format:
+
+------------
+<opt_spec><arg_spec>? SP+ help LF
+------------
+
+`<opt_spec>`::
+       its format is the short option character, then the long option name
+       separated by a comma. Both parts are not required, though at least one
+       is necessary. `h,help`, `dry-run` and `f` are all three correct
+       `<opt_spec>`.
+
+`<arg_spec>`::
+       an `<arg_spec>` tells the option parser if the option has an argument
+       (`=`), an optional one (`?` though its use is discouraged) or none
+       (no `<arg_spec>` in that case).
+
+The remainder of the line, after stripping the spaces, is used
+as the help associated to the option.
+
+Blank lines are ignored, and lines that don't match this specification are used
+as option group headers (start the line with a space to create such
+lines on purpose).
+
+Example
+~~~~~~~
+
+------------
+OPTS_SPEC="\
+some-command [options] <args>...
+
+some-command does foo and bar!
+--
+h,help    show the help
+
+foo       some nifty option --foo
+bar=      some cool option --bar with an argument
+
+  An option group Header
+C?        option C with an optional argument"
+
+eval `echo "$OPTS_SPEC" | git-rev-parse --parseopt -- "$@" || echo exit $?`
+------------
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org> .
+Junio C Hamano <junkio@cox.net> and Pierre Habouzit <madcoder@debian.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-revert.txt b/Documentation/git-revert.txt
new file mode 100644 (file)
index 0000000..3457c40
--- /dev/null
@@ -0,0 +1,65 @@
+git-revert(1)
+=============
+
+NAME
+----
+git-revert - Revert an existing commit
+
+SYNOPSIS
+--------
+'git-revert' [--edit | --no-edit] [-n] [-m parent-number] <commit>
+
+DESCRIPTION
+-----------
+Given one existing commit, revert the change the patch introduces, and record a
+new commit that records it.  This requires your working tree to be clean (no
+modifications from the HEAD commit).
+
+OPTIONS
+-------
+<commit>::
+       Commit to revert.
+       For a more complete list of ways to spell commit names, see
+       "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1].
+
+-e|--edit::
+       With this option, `git-revert` will let you edit the commit
+       message prior committing the revert. This is the default if
+       you run the command from a terminal.
+
+-m parent-number|--mainline parent-number::
+       Usually you cannot revert a merge because you do not know which
+       side of the merge should be considered the mainline.  This
+       option specifies the parent number (starting from 1) of
+       the mainline and allows revert to reverse the change
+       relative to the specified parent.
+
+--no-edit::
+       With this option, `git-revert` will not start the commit
+       message editor.
+
+-n|--no-commit::
+       Usually the command automatically creates a commit with
+       a commit log message stating which commit was reverted.
+       This flag applies the change necessary to revert the
+       named commit to your working tree, but does not make the
+       commit.  In addition, when this option is used, your
+       working tree does not have to match the HEAD commit.
+       The revert is done against the beginning state of your
+       working tree.
++
+This is useful when reverting more than one commits'
+effect to your working tree in a row.
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt
new file mode 100644 (file)
index 0000000..48c1d97
--- /dev/null
@@ -0,0 +1,98 @@
+git-rm(1)
+=========
+
+NAME
+----
+git-rm - Remove files from the working tree and from the index
+
+SYNOPSIS
+--------
+'git-rm' [-f] [-n] [-r] [--cached] [--ignore-unmatch] [--quiet] [--] <file>...
+
+DESCRIPTION
+-----------
+Remove files from the working tree and from the index.  The
+files have to be identical to the tip of the branch, and no
+updates to its contents must have been placed in the staging
+area (aka index).  When --cached is given, the staged content has to
+match either the tip of the branch *or* the file on disk.
+
+
+OPTIONS
+-------
+<file>...::
+       Files to remove.  Fileglobs (e.g. `*.c`) can be given to
+       remove all matching files.  Also a leading directory name
+       (e.g. `dir` to add `dir/file1` and `dir/file2`) can be
+       given to remove all files in the directory, recursively,
+       but this requires `-r` option to be given for safety.
+
+-f::
+       Override the up-to-date check.
+
+-n, \--dry-run::
+        Don't actually remove the file(s), just show if they exist in
+        the index.
+
+-r::
+        Allow recursive removal when a leading directory name is
+        given.
+
+\--::
+       This option can be used to separate command-line options from
+       the list of files, (useful when filenames might be mistaken
+       for command-line options).
+
+\--cached::
+       This option can be used to tell the command to remove
+       the paths only from the index, leaving working tree
+       files.
+
+\--ignore-unmatch::
+       Exit with a zero status even if no files matched.
+
+-q, \--quiet::
+       git-rm normally outputs one line (in the form of an "rm" command)
+       for each file removed. This option suppresses that output.
+
+
+DISCUSSION
+----------
+
+The list of <file> given to the command can be exact pathnames,
+file glob patterns, or leading directory name.  The command
+removes only the paths that is known to git.  Giving the name of
+a file that you have not told git about does not remove that file.
+
+
+EXAMPLES
+--------
+git-rm Documentation/\\*.txt::
+       Removes all `\*.txt` files from the index that are under the
+       `Documentation` directory and any of its subdirectories.
++
+Note that the asterisk `\*` is quoted from the shell in this
+example; this lets the command include the files from
+subdirectories of `Documentation/` directory.
+
+git-rm -f git-*.sh::
+       Remove all git-*.sh scripts that are in the index.
+       Because this example lets the shell expand the asterisk
+       (i.e. you are listing the files explicitly), it
+       does not remove `subdir/git-foo.sh`.
+
+See Also
+--------
+gitlink:git-add[1]
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-runstatus.txt b/Documentation/git-runstatus.txt
new file mode 100644 (file)
index 0000000..dee5d0d
--- /dev/null
@@ -0,0 +1,68 @@
+git-runstatus(1)
+================
+
+NAME
+----
+git-runstatus - A helper for git-status and git-commit
+
+
+SYNOPSIS
+--------
+'git-runstatus' [--color|--nocolor] [--amend] [--verbose] [--untracked]
+
+
+DESCRIPTION
+-----------
+Examines paths in the working tree that has changes unrecorded
+to the index file, and changes between the index file and the
+current HEAD commit.  The former paths are what you _could_
+commit by running 'git add' (or 'git rm' if you are deleting) before running 'git
+commit', and the latter paths are what you _would_ commit by
+running 'git commit'.
+
+If there is no path that is different between the index file and
+the current HEAD commit, the command exits with non-zero status.
+
+Note that this is _not_ the user level command you would want to
+run from the command line.  Use 'git-status' instead.
+
+
+OPTIONS
+-------
+--color::
+       Show colored status, highlighting modified file names.
+
+--nocolor::
+       Turn off coloring.
+
+--amend::
+       Show status based on HEAD^1, not HEAD, i.e. show what
+       'git-commit --amend' would do.
+
+--verbose::
+       Show unified diff of all file changes.
+
+--untracked::
+       Show files in untracked directories, too.  Without this
+       option only its name and a trailing slash are displayed
+       for each untracked directory.
+
+
+OUTPUT
+------
+The output from this command is designed to be used as a commit
+template comments, and all the output lines are prefixed with '#'.
+
+
+Author
+------
+Originally written by Linus Torvalds <torvalds@osdl.org> as part
+of git-commit, and later rewritten in C by Jeff King.
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt
new file mode 100644 (file)
index 0000000..659215a
--- /dev/null
@@ -0,0 +1,199 @@
+git-send-email(1)
+=================
+
+NAME
+----
+git-send-email - Send a collection of patches as emails
+
+
+SYNOPSIS
+--------
+'git-send-email' [options] <file|directory> [... file|directory]
+
+
+
+DESCRIPTION
+-----------
+Takes the patches given on the command line and emails them out.
+
+The header of the email is configurable by command line options.  If not
+specified on the command line, the user will be prompted with a ReadLine
+enabled interface to provide the necessary information.
+
+OPTIONS
+-------
+The options available are:
+
+--bcc::
+       Specify a "Bcc:" value for each email.
++
+The --bcc option must be repeated for each user you want on the bcc list.
+
+--cc::
+       Specify a starting "Cc:" value for each email.
++
+The --cc option must be repeated for each user you want on the cc list.
+
+--cc-cmd::
+       Specify a command to execute once per patch file which
+       should generate patch file specific "Cc:" entries.
+       Output of this command must be single email address per line.
+       Default is the value of 'sendemail.cccmd' configuration value.
+
+--chain-reply-to, --no-chain-reply-to::
+       If this is set, each email will be sent as a reply to the previous
+       email sent.  If disabled with "--no-chain-reply-to", all emails after
+       the first will be sent as replies to the first email sent.  When using
+       this, it is recommended that the first file given be an overview of the
+       entire patch series.
+       Default is the value of the 'sendemail.chainreplyto' configuration
+       value; if that is unspecified, default to --chain-reply-to.
+
+--compose::
+       Use $GIT_EDITOR, core.editor, $VISUAL, or $EDITOR to edit an
+       introductory message for the patch series.
+
+--from::
+       Specify the sender of the emails.  This will default to
+       the value GIT_COMMITTER_IDENT, as returned by "git-var -l".
+       The user will still be prompted to confirm this entry.
+
+--in-reply-to::
+       Specify the contents of the first In-Reply-To header.
+       Subsequent emails will refer to the previous email
+       instead of this if --chain-reply-to is set (the default)
+       Only necessary if --compose is also set.  If --compose
+       is not set, this will be prompted for.
+
+--signed-off-by-cc, --no-signed-off-by-cc::
+        If this is set, add emails found in Signed-off-by: or Cc: lines to the
+        cc list.
+        Default is the value of 'sendemail.signedoffcc' configuration value;
+        if that is unspecified, default to --signed-off-by-cc.
+
+--quiet::
+       Make git-send-email less verbose.  One line per email should be
+       all that is output.
+
+--identity::
+       A configuration identity. When given, causes values in the
+       'sendemail.<identity>' subsection to take precedence over
+       values in the 'sendemail' section. The default identity is
+       the value of 'sendemail.identity'.
+
+--smtp-server::
+       If set, specifies the outgoing SMTP server to use (e.g.
+       `smtp.example.com` or a raw IP address).  Alternatively it can
+       specify a full pathname of a sendmail-like program instead;
+       the program must support the `-i` option.  Default value can
+       be specified by the 'sendemail.smtpserver' configuration
+       option; the built-in default is `/usr/sbin/sendmail` or
+       `/usr/lib/sendmail` if such program is available, or
+       `localhost` otherwise.
+
+--smtp-server-port::
+       Specifies a port different from the default port (SMTP
+       servers typically listen to smtp port 25 and ssmtp port
+       465).
+
+--smtp-user, --smtp-pass::
+       Username and password for SMTP-AUTH. Defaults are the values of
+       the configuration values 'sendemail.smtpuser' and
+       'sendemail.smtppass', but see also 'sendemail.identity'.
+       If not set, authentication is not attempted.
+
+--smtp-ssl::
+       If set, connects to the SMTP server using SSL.
+       Default is the value of the 'sendemail.smtpssl' configuration value;
+       if that is unspecified, does not use SSL.
+
+--subject::
+       Specify the initial subject of the email thread.
+       Only necessary if --compose is also set.  If --compose
+       is not set, this will be prompted for.
+
+--suppress-from, --no-suppress-from::
+        If this is set, do not add the From: address to the cc: list.
+        Default is the value of 'sendemail.suppressfrom' configuration value;
+        if that is unspecified, default to --no-suppress-from.
+
+--thread, --no-thread::
+       If this is set, the In-Reply-To header will be set on each email sent.
+       If disabled with "--no-thread", no emails will have the In-Reply-To
+       header set.
+       Default is the value of the 'sendemail.thread' configuration value;
+       if that is unspecified, default to --thread.
+
+--dry-run::
+       Do everything except actually send the emails.
+
+--envelope-sender::
+       Specify the envelope sender used to send the emails.
+       This is useful if your default address is not the address that is
+       subscribed to a list. If you use the sendmail binary, you must have
+       suitable privileges for the -f parameter.
+
+--to::
+       Specify the primary recipient of the emails generated.
+       Generally, this will be the upstream maintainer of the
+       project involved.
++
+The --to option must be repeated for each user you want on the to list.
+
+
+CONFIGURATION
+-------------
+sendemail.identity::
+       The default configuration identity. When specified,
+       'sendemail.<identity>.<item>' will have higher precedence than
+       'sendemail.<item>'. This is useful to declare multiple SMTP
+       identities and to hoist sensitive authentication information
+       out of the repository and into the global configuation file.
+
+sendemail.aliasesfile::
+       To avoid typing long email addresses, point this to one or more
+       email aliases files.  You must also supply 'sendemail.aliasfiletype'.
+
+sendemail.aliasfiletype::
+       Format of the file(s) specified in sendemail.aliasesfile. Must be
+       one of 'mutt', 'mailrc', 'pine', or 'gnus'.
+
+sendemail.to::
+       Email address (or alias) to always send to.
+
+sendemail.cccmd::
+       Command to execute to generate per patch file specific "Cc:"s.
+
+sendemail.bcc::
+       Email address (or alias) to always bcc.
+
+sendemail.chainreplyto::
+       Boolean value specifying the default to the '--chain_reply_to'
+       parameter.
+
+sendemail.smtpserver::
+       Default SMTP server to use.
+
+sendemail.smtpuser::
+       Default SMTP-AUTH username.
+
+sendemail.smtppass::
+       Default SMTP-AUTH password.
+
+sendemail.smtpssl::
+       Boolean value specifying the default to the '--smtp-ssl' parameter.
+
+Author
+------
+Written by Ryan Anderson <ryan@michonline.com>
+
+git-send-email is originally based upon
+send_lots_of_email.pl by Greg Kroah-Hartman.
+
+Documentation
+--------------
+Documentation by Ryan Anderson
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-send-pack.txt b/Documentation/git-send-pack.txt
new file mode 100644 (file)
index 0000000..a2d9cb6
--- /dev/null
@@ -0,0 +1,128 @@
+git-send-pack(1)
+================
+
+NAME
+----
+git-send-pack - Push objects over git protocol to another repository
+
+
+SYNOPSIS
+--------
+'git-send-pack' [--all] [--dry-run] [--force] [--receive-pack=<git-receive-pack>] [--verbose] [--thin] [<host>:]<directory> [<ref>...]
+
+DESCRIPTION
+-----------
+Usually you would want to use gitlink:git-push[1] which is a
+higher level wrapper of this command instead.
+
+Invokes 'git-receive-pack' on a possibly remote repository, and
+updates it from the current repository, sending named refs.
+
+
+OPTIONS
+-------
+\--receive-pack=<git-receive-pack>::
+       Path to the 'git-receive-pack' program on the remote
+       end.  Sometimes useful when pushing to a remote
+       repository over ssh, and you do not have the program in
+       a directory on the default $PATH.
+
+\--exec=<git-receive-pack>::
+       Same as \--receive-pack=<git-receive-pack>.
+
+\--all::
+       Instead of explicitly specifying which refs to update,
+       update all heads that locally exist.
+
+\--dry-run::
+       Do everything except actually send the updates.
+
+\--force::
+       Usually, the command refuses to update a remote ref that
+       is not an ancestor of the local ref used to overwrite it.
+       This flag disables the check.  What this means is that
+       the remote repository can lose commits; use it with
+       care.
+
+\--verbose::
+       Run verbosely.
+
+\--thin::
+       Spend extra cycles to minimize the number of objects to be sent.
+       Use it on slower connection.
+
+<host>::
+       A remote host to house the repository.  When this
+       part is specified, 'git-receive-pack' is invoked via
+       ssh.
+
+<directory>::
+       The repository to update.
+
+<ref>...::
+       The remote refs to update.
+
+
+Specifying the Refs
+-------------------
+
+There are three ways to specify which refs to update on the
+remote end.
+
+With '--all' flag, all refs that exist locally are transferred to
+the remote side.  You cannot specify any '<ref>' if you use
+this flag.
+
+Without '--all' and without any '<ref>', the heads that exist
+both on the local side and on the remote side are updated.
+
+When one or more '<ref>' are specified explicitly, it can be either a
+single pattern, or a pair of such pattern separated by a colon
+":" (this means that a ref name cannot have a colon in it).  A
+single pattern '<name>' is just a shorthand for '<name>:<name>'.
+
+Each pattern pair consists of the source side (before the colon)
+and the destination side (after the colon).  The ref to be
+pushed is determined by finding a match that matches the source
+side, and where it is pushed is determined by using the
+destination side. The rules used to match a ref are the same
+rules used by gitlink:git-rev-parse[1] to resolve a symbolic ref
+name.
+
+ - It is an error if <src> does not match exactly one of the
+   local refs.
+
+ - It is an error if <dst> matches more than one remote refs.
+
+ - If <dst> does not match any remote ref, either
+
+   * it has to start with "refs/"; <dst> is used as the
+     destination literally in this case.
+
+   * <src> == <dst> and the ref that matched the <src> must not
+     exist in the set of remote refs; the ref matched <src>
+     locally is used as the name of the destination.
+
+Without '--force', the <src> ref is stored at the remote only if
+<dst> does not exist, or <dst> is a proper subset (i.e. an
+ancestor) of <src>.  This check, known as "fast forward check",
+is performed in order to avoid accidentally overwriting the
+remote ref and lose other peoples' commits from there.
+
+With '--force', the fast forward check is disabled for all refs.
+
+Optionally, a <ref> parameter can be prefixed with a plus '+' sign
+to disable the fast-forward check only on that ref.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-sh-setup.txt b/Documentation/git-sh-setup.txt
new file mode 100644 (file)
index 0000000..1ea1faa
--- /dev/null
@@ -0,0 +1,71 @@
+git-sh-setup(1)
+===============
+
+NAME
+----
+git-sh-setup - Common git shell script setup code
+
+SYNOPSIS
+--------
+'git-sh-setup'
+
+DESCRIPTION
+-----------
+
+This is not a command the end user would want to run.  Ever.
+This documentation is meant for people who are studying the
+Porcelain-ish scripts and/or are writing new ones.
+
+The `git-sh-setup` scriptlet is designed to be sourced (using
+`.`) by other shell scripts to set up some variables pointing at
+the normal git directories and a few helper shell functions.
+
+Before sourcing it, your script should set up a few variables;
+`USAGE` (and `LONG_USAGE`, if any) is used to define message
+given by `usage()` shell function.  `SUBDIRECTORY_OK` can be set
+if the script can run from a subdirectory of the working tree
+(some commands do not).
+
+The scriptlet sets `GIT_DIR` and `GIT_OBJECT_DIRECTORY` shell
+variables, but does *not* export them to the environment.
+
+FUNCTIONS
+---------
+
+die::
+       exit after emitting the supplied error message to the
+       standard error stream.
+
+usage::
+       die with the usage message.
+
+set_reflog_action::
+       set the message that will be recorded to describe the
+       end-user action in the reflog, when the script updates a
+       ref.
+
+is_bare_repository::
+       outputs `true` or `false` to the standard output stream
+       to indicate if the repository is a bare repository
+       (i.e. without an associated working tree).
+
+cd_to_toplevel::
+       runs chdir to the toplevel of the working tree.
+
+require_work_tree::
+       checks if the repository is a bare repository, and dies
+       if so.  Used by scripts that require working tree
+       (e.g. `checkout`).
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-shell.txt b/Documentation/git-shell.txt
new file mode 100644 (file)
index 0000000..48f2d57
--- /dev/null
@@ -0,0 +1,34 @@
+git-shell(1)
+============
+
+NAME
+----
+git-shell - Restricted login shell for GIT-only SSH access
+
+
+SYNOPSIS
+--------
+'git-shell' -c <command> <argument>
+
+DESCRIPTION
+-----------
+This is meant to be used as a login shell for SSH accounts you want
+to restrict to GIT pull/push access only. It permits execution only
+of server-side GIT commands implementing the pull/push functionality.
+The commands can be executed only by the '-c' option; the shell is not
+interactive.
+
+Currently, only the `git-receive-pack` and `git-upload-pack` commands
+are permitted to be called, with a single required argument.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Petr Baudis and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-shortlog.txt b/Documentation/git-shortlog.txt
new file mode 100644 (file)
index 0000000..2220ef6
--- /dev/null
@@ -0,0 +1,58 @@
+git-shortlog(1)
+===============
+
+NAME
+----
+git-shortlog - Summarize 'git log' output
+
+SYNOPSIS
+--------
+[verse]
+git-log --pretty=short | 'git-shortlog' [-h] [-n] [-s]
+git-shortlog [-n|--numbered] [-s|--summary] [<committish>...]
+
+DESCRIPTION
+-----------
+Summarizes 'git log' output in a format suitable for inclusion
+in release announcements. Each commit will be grouped by author and
+the first line of the commit message will be shown.
+
+Additionally, "[PATCH]" will be stripped from the commit description.
+
+OPTIONS
+-------
+
+-h, \--help::
+       Print a short usage message and exit.
+
+-n, \--numbered::
+       Sort output according to the number of commits per author instead
+       of author alphabetic order.
+
+-s, \--summary::
+       Suppress commit description and provide a commit count summary only.
+
+FILES
+-----
+
+.mailmap::
+       If this file exists, it will be used for mapping author email
+       addresses to a real author name. One mapping per line, first
+       the author name followed by the email address enclosed by
+       '<' and '>'. Use hash '#' for comments. Example:
+
+               # Keep alphabetized
+               Adam Morrow <adam@localhost.localdomain>
+               Eve Jones <eve@laptop.(none)>
+
+Author
+------
+Written by Jeff Garzik <jgarzik@pobox.com>
+
+Documentation
+--------------
+Documentation by Junio C Hamano.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-show-branch.txt b/Documentation/git-show-branch.txt
new file mode 100644 (file)
index 0000000..ba5313d
--- /dev/null
@@ -0,0 +1,193 @@
+git-show-branch(1)
+==================
+
+NAME
+----
+git-show-branch - Show branches and their commits
+
+SYNOPSIS
+--------
+[verse]
+'git-show-branch' [--all] [--remotes] [--topo-order] [--current]
+               [--more=<n> | --list | --independent | --merge-base]
+               [--no-name | --sha1-name] [--topics] [<rev> | <glob>]...
+'git-show-branch' (-g|--reflog)[=<n>[,<base>]] [--list] [<ref>]
+
+DESCRIPTION
+-----------
+
+Shows the commit ancestry graph starting from the commits named
+with <rev>s or <globs>s (or all refs under $GIT_DIR/refs/heads
+and/or $GIT_DIR/refs/tags) semi-visually.
+
+It cannot show more than 29 branches and commits at a time.
+
+It uses `showbranch.default` multi-valued configuration items if
+no <rev> nor <glob> is given on the command line.
+
+
+OPTIONS
+-------
+<rev>::
+       Arbitrary extended SHA1 expression (see `git-rev-parse`)
+       that typically names a branch HEAD or a tag.
+
+<glob>::
+       A glob pattern that matches branch or tag names under
+       $GIT_DIR/refs.  For example, if you have many topic
+       branches under $GIT_DIR/refs/heads/topic, giving
+       `topic/*` would show all of them.
+
+-r|--remotes::
+       Show the remote-tracking branches.
+
+-a|--all::
+       Show both remote-tracking branches and local branches.
+
+--current::
+       With this option, the command includes the current
+       branch to the list of revs to be shown when it is not
+       given on the command line.
+
+--topo-order::
+        By default, the branches and their commits are shown in
+        reverse chronological order.  This option makes them
+        appear in topological order (i.e., descendant commits
+        are shown before their parents).
+
+--sparse::
+       By default, the output omits merges that are reachable
+       from only one tip being shown.  This option makes them
+       visible.
+
+--more=<n>::
+       Usually the command stops output upon showing the commit
+       that is the common ancestor of all the branches.  This
+       flag tells the command to go <n> more common commits
+       beyond that.  When <n> is negative, display only the
+       <reference>s given, without showing the commit ancestry
+       tree.
+
+--list::
+       Synonym to `--more=-1`
+
+--merge-base::
+       Instead of showing the commit list, just act like the
+       'git-merge-base -a' command, except that it can accept
+       more than two heads.
+
+--independent::
+       Among the <reference>s given, display only the ones that
+       cannot be reached from any other <reference>.
+
+--no-name::
+       Do not show naming strings for each commit.
+
+--sha1-name::
+       Instead of naming the commits using the path to reach
+       them from heads (e.g. "master~2" to mean the grandparent
+       of "master"), name them with the unique prefix of their
+       object names.
+
+--topics::
+       Shows only commits that are NOT on the first branch given.
+       This helps track topic branches by hiding any commit that
+       is already in the main line of development.  When given
+       "git show-branch --topics master topic1 topic2", this
+       will show the revisions given by "git rev-list {caret}master
+       topic1 topic2"
+
+--reflog[=<n>[,<base>]] [<ref>]::
+       Shows <n> most recent ref-log entries for the given
+       ref.  If <base> is given, <n> entries going back from
+       that entry.  <base> can be specified as count or date.
+       `-g` can be used as a short-hand for this option.  When
+       no explicit <ref> parameter is given, it defaults to the
+       current branch (or `HEAD` if it is detached).
+
+Note that --more, --list, --independent and --merge-base options
+are mutually exclusive.
+
+
+OUTPUT
+------
+Given N <references>, the first N lines are the one-line
+description from their commit message.  The branch head that is
+pointed at by $GIT_DIR/HEAD is prefixed with an asterisk `*`
+character while other heads are prefixed with a `!` character.
+
+Following these N lines, one-line log for each commit is
+displayed, indented N places.  If a commit is on the I-th
+branch, the I-th indentation character shows a `+` sign;
+otherwise it shows a space.  Merge commits are denoted by
+a `-` sign.  Each commit shows a short name that
+can be used as an extended SHA1 to name that commit.
+
+The following example shows three branches, "master", "fixes"
+and "mhf":
+
+------------------------------------------------
+$ git show-branch master fixes mhf
+* [master] Add 'git show-branch'.
+ ! [fixes] Introduce "reset type" flag to "git reset"
+  ! [mhf] Allow "+remote:local" refspec to cause --force when fetching.
+---
+  + [mhf] Allow "+remote:local" refspec to cause --force when fetching.
+  + [mhf~1] Use git-octopus when pulling more than one heads.
+ +  [fixes] Introduce "reset type" flag to "git reset"
+  + [mhf~2] "git fetch --force".
+  + [mhf~3] Use .git/remote/origin, not .git/branches/origin.
+  + [mhf~4] Make "git pull" and "git fetch" default to origin
+  + [mhf~5] Infamous 'octopus merge'
+  + [mhf~6] Retire git-parse-remote.
+  + [mhf~7] Multi-head fetch.
+  + [mhf~8] Start adding the $GIT_DIR/remotes/ support.
+*++ [master] Add 'git show-branch'.
+------------------------------------------------
+
+These three branches all forked from a common commit, [master],
+whose commit message is "Add 'git show-branch'.  "fixes" branch
+adds one commit 'Introduce "reset type"'.  "mhf" branch has many
+other commits.  The current branch is "master".
+
+
+EXAMPLE
+-------
+
+If you keep your primary branches immediately under
+`$GIT_DIR/refs/heads`, and topic branches in subdirectories of
+it, having the following in the configuration file may help:
+
+------------
+[showbranch]
+       default = --topo-order
+       default = heads/*
+
+------------
+
+With this, `git show-branch` without extra parameters would show
+only the primary branches.  In addition, if you happen to be on
+your topic branch, it is shown as well.
+
+------------
+$ git show-branch --reflog='10,1 hour ago' --list master
+------------
+
+shows 10 reflog entries going back from the tip as of 1 hour ago.
+Without `--list`, the output also shows how these tips are
+topologically related with each other.
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+
+Documentation
+--------------
+Documentation by Junio C Hamano.
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-show-index.txt b/Documentation/git-show-index.txt
new file mode 100644 (file)
index 0000000..764d993
--- /dev/null
@@ -0,0 +1,34 @@
+git-show-index(1)
+=================
+
+NAME
+----
+git-show-index - Show packed archive index
+
+
+SYNOPSIS
+--------
+'git-show-index' < idx-file
+
+
+DESCRIPTION
+-----------
+Reads given idx file for packed git archive created with
+git-pack-objects command, and dumps its contents.
+
+The information it outputs is subset of what you can get from
+'git-verify-pack -v'; this command only shows the packfile
+offset and SHA1 of each object.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-show-ref.txt b/Documentation/git-show-ref.txt
new file mode 100644 (file)
index 0000000..2355aa5
--- /dev/null
@@ -0,0 +1,172 @@
+git-show-ref(1)
+===============
+
+NAME
+----
+git-show-ref - List references in a local repository
+
+SYNOPSIS
+--------
+[verse]
+'git-show-ref' [-q|--quiet] [--verify] [-h|--head] [-d|--dereference]
+            [-s|--hash] [--abbrev] [--tags] [--heads] [--] <pattern>...
+'git-show-ref' --exclude-existing[=pattern]
+
+DESCRIPTION
+-----------
+
+Displays references available in a local repository along with the associated
+commit IDs. Results can be filtered using a pattern and tags can be
+dereferenced into object IDs. Additionally, it can be used to test whether a
+particular ref exists.
+
+The --exclude-existing form is a filter that does the inverse, it shows the
+refs from stdin that don't exist in the local repository.
+
+Use of this utility is encouraged in favor of directly accessing files under
+in the `.git` directory.
+
+OPTIONS
+-------
+
+-h, --head::
+
+       Show the HEAD reference.
+
+--tags, --heads::
+
+       Limit to only "refs/heads" and "refs/tags", respectively.  These
+       options are not mutually exclusive; when given both, references stored
+       in "refs/heads" and "refs/tags" are displayed.
+
+-d, --dereference::
+
+       Dereference tags into object IDs as well. They will be shown with "^{}"
+       appended.
+
+-s, --hash::
+
+       Only show the SHA1 hash, not the reference name. When also using
+       --dereference the dereferenced tag will still be shown after the SHA1.
+
+--verify::
+
+       Enable stricter reference checking by requiring an exact ref path.
+       Aside from returning an error code of 1, it will also print an error
+       message if '--quiet' was not specified.
+
+--abbrev, --abbrev=len::
+
+       Abbreviate the object name.  When using `--hash`, you do
+       not have to say `--hash --abbrev`; `--hash=len` would do.
+
+-q, --quiet::
+
+       Do not print any results to stdout. When combined with '--verify' this
+       can be used to silently check if a reference exists.
+
+--exclude-existing, --exclude-existing=pattern::
+
+       Make git-show-ref act as a filter that reads refs from stdin of the
+       form "^(?:<anything>\s)?<refname>(?:\^\{\})?$" and performs the
+       following actions on each:
+       (1) strip "^{}" at the end of line if any;
+       (2) ignore if pattern is provided and does not head-match refname;
+       (3) warn if refname is not a well-formed refname and skip;
+       (4) ignore if refname is a ref that exists in the local repository;
+       (5) otherwise output the line.
+
+
+<pattern>::
+
+       Show references matching one or more patterns.
+
+OUTPUT
+------
+
+The output is in the format: '<SHA-1 ID>' '<space>' '<reference name>'.
+
+-----------------------------------------------------------------------------
+$ git show-ref --head --dereference
+832e76a9899f560a90ffd62ae2ce83bbeff58f54 HEAD
+832e76a9899f560a90ffd62ae2ce83bbeff58f54 refs/heads/master
+832e76a9899f560a90ffd62ae2ce83bbeff58f54 refs/heads/origin
+3521017556c5de4159da4615a39fa4d5d2c279b5 refs/tags/v0.99.9c
+6ddc0964034342519a87fe013781abf31c6db6ad refs/tags/v0.99.9c^{}
+055e4ae3ae6eb344cbabf2a5256a49ea66040131 refs/tags/v1.0rc4
+423325a2d24638ddcc82ce47be5e40be550f4507 refs/tags/v1.0rc4^{}
+...
+-----------------------------------------------------------------------------
+
+When using --hash (and not --dereference) the output format is: '<SHA-1 ID>'
+
+-----------------------------------------------------------------------------
+$ git show-ref --heads --hash
+2e3ba0114a1f52b47df29743d6915d056be13278
+185008ae97960c8d551adcd9e23565194651b5d1
+03adf42c988195b50e1a1935ba5fcbc39b2b029b
+...
+-----------------------------------------------------------------------------
+
+EXAMPLE
+-------
+
+To show all references called "master", whether tags or heads or anything
+else, and regardless of how deep in the reference naming hierarchy they are,
+use:
+
+-----------------------------------------------------------------------------
+       git show-ref master
+-----------------------------------------------------------------------------
+
+This will show "refs/heads/master" but also "refs/remote/other-repo/master",
+if such references exists.
+
+When using the '--verify' flag, the command requires an exact path:
+
+-----------------------------------------------------------------------------
+       git show-ref --verify refs/heads/master
+-----------------------------------------------------------------------------
+
+will only match the exact branch called "master".
+
+If nothing matches, gitlink:git-show-ref[1] will return an error code of 1,
+and in the case of verification, it will show an error message.
+
+For scripting, you can ask it to be quiet with the "--quiet" flag, which
+allows you to do things like
+
+-----------------------------------------------------------------------------
+       git-show-ref --quiet --verify -- "refs/heads/$headname" ||
+               echo "$headname is not a valid branch"
+-----------------------------------------------------------------------------
+
+to check whether a particular branch exists or not (notice how we don't
+actually want to show any results, and we want to use the full refname for it
+in order to not trigger the problem with ambiguous partial matches).
+
+To show only tags, or only proper branch heads, use "--tags" and/or "--heads"
+respectively (using both means that it shows tags and heads, but not other
+random references under the refs/ subdirectory).
+
+To do automatic tag object dereferencing, use the "-d" or "--dereference"
+flag, so you can do
+
+-----------------------------------------------------------------------------
+       git show-ref --tags --dereference
+-----------------------------------------------------------------------------
+
+to get a listing of all tags together with what they dereference.
+
+SEE ALSO
+--------
+gitlink:git-ls-remote[1], gitlink:git-peek-remote[1]
+
+AUTHORS
+-------
+Written by Linus Torvalds <torvalds@osdl.org>.
+Man page by Jonas Fonseca <fonseca@diku.dk>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-show.txt b/Documentation/git-show.txt
new file mode 100644 (file)
index 0000000..a42e121
--- /dev/null
@@ -0,0 +1,86 @@
+git-show(1)
+===========
+
+NAME
+----
+git-show - Show various types of objects
+
+
+SYNOPSIS
+--------
+'git-show' [options] <object>...
+
+DESCRIPTION
+-----------
+Shows one or more objects (blobs, trees, tags and commits).
+
+For commits it shows the log message and textual diff. It also
+presents the merge commit in a special format as produced by
+'git-diff-tree --cc'.
+
+For tags, it shows the tag message and the referenced objects.
+
+For trees, it shows the names (equivalent to gitlink:git-ls-tree[1]
+with \--name-only).
+
+For plain blobs, it shows the plain contents.
+
+The command takes options applicable to the gitlink:git-diff-tree[1] command to
+control how the changes the commit introduces are shown.
+
+This manual page describes only the most frequently used options.
+
+
+OPTIONS
+-------
+<object>::
+       The name of the object to show.
+       For a more complete list of ways to spell object names, see
+       "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1].
+
+include::pretty-options.txt[]
+
+
+include::pretty-formats.txt[]
+
+
+EXAMPLES
+--------
+
+git show v1.0.0::
+       Shows the tag `v1.0.0`, along with the object the tags
+       points at.
+
+git show v1.0.0^\{tree\}::
+       Shows the tree pointed to by the tag `v1.0.0`.
+
+git show next~10:Documentation/README::
+       Shows the contents of the file `Documentation/README` as
+       they were current in the 10th last commit of the branch
+       `next`.
+
+git show master:Makefile master:t/Makefile::
+       Concatenates the contents of said Makefiles in the head
+       of the branch `master`.
+
+Discussion
+----------
+
+include::i18n.txt[]
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org> and
+Junio C Hamano <junkio@cox.net>.  Significantly enhanced by
+Johannes Schindelin <Johannes.Schindelin@gmx.de>.
+
+
+Documentation
+-------------
+Documentation by David Greaves, Petr Baudis and the git-list <git@vger.kernel.org>.
+
+This manual page is a stub. You can help the git documentation by expanding it.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-stash.txt b/Documentation/git-stash.txt
new file mode 100644 (file)
index 0000000..c0147b9
--- /dev/null
@@ -0,0 +1,170 @@
+git-stash(1)
+============
+
+NAME
+----
+git-stash - Stash the changes in a dirty working directory away
+
+SYNOPSIS
+--------
+[verse]
+'git-stash' (list | show [<stash>] | apply [<stash>] | clear)
+'git-stash' [save] [message...]
+
+DESCRIPTION
+-----------
+
+Use 'git-stash' when you want to record the current state of the
+working directory and the index, but want to go back to a clean
+working directory.  The command saves your local modifications away
+and reverts the working directory to match the `HEAD` commit.
+
+The modifications stashed away by this command can be listed with
+`git-stash list`, inspected with `git-stash show`, and restored
+(potentially on top of a different commit) with `git-stash apply`.
+Calling git-stash without any arguments is equivalent to `git-stash
+save`.  A stash is by default listed as "WIP on 'branchname' ...", but
+you can give a more descriptive message on the command line when
+you create one.
+
+The latest stash you created is stored in `$GIT_DIR/refs/stash`; older
+stashes are found in the reflog of this reference and can be named using
+the usual reflog syntax (e.g. `stash@\{0}` is the most recently
+created stash, `stash@\{1}` is the one before it, `stash@\{2.hours.ago}`
+is also possible).
+
+OPTIONS
+-------
+
+save::
+
+       Save your local modifications to a new 'stash', and run `git-reset
+       --hard` to revert them.  This is the default action when no
+       subcommand is given.
+
+list::
+
+       List the stashes that you currently have.  Each 'stash' is listed
+       with its name (e.g. `stash@\{0}` is the latest stash, `stash@\{1}` is
+       the one before, etc.), the name of the branch that was current when the
+       stash was made, and a short description of the commit the stash was
+       based on.
++
+----------------------------------------------------------------
+stash@{0}: WIP on submit: 6ebd0e2... Update git-stash documentation
+stash@{1}: On master: 9cc0589... Add git-stash
+----------------------------------------------------------------
+
+show [<stash>]::
+
+       Show the changes recorded in the stash as a diff between the
+       stashed state and its original parent. When no `<stash>` is given,
+       shows the latest one. By default, the command shows the diffstat, but
+       it will accept any format known to `git-diff` (e.g., `git-stash show
+       -p stash@\{1}` to view the second most recent stash in patch form).
+
+apply [--index] [<stash>]::
+
+       Restore the changes recorded in the stash on top of the current
+       working tree state.  When no `<stash>` is given, applies the latest
+       one.  The working directory must match the index.
++
+This operation can fail with conflicts; you need to resolve them
+by hand in the working tree.
++
+If the `--index` option is used, then tries to reinstate not only the working
+tree's changes, but also the index's ones. However, this can fail, when you
+have conflicts (which are stored in the index, where you therefore can no
+longer apply the changes as they were originally).
+
+clear::
+       Remove all the stashed states. Note that those states will then
+       be subject to pruning, and may be difficult or impossible to recover.
+
+
+DISCUSSION
+----------
+
+A stash is represented as a commit whose tree records the state of the
+working directory, and its first parent is the commit at `HEAD` when
+the stash was created.  The tree of the second parent records the
+state of the index when the stash is made, and it is made a child of
+the `HEAD` commit.  The ancestry graph looks like this:
+
+            .----W
+           /    /
+     -----H----I
+
+where `H` is the `HEAD` commit, `I` is a commit that records the state
+of the index, and `W` is a commit that records the state of the working
+tree.
+
+
+EXAMPLES
+--------
+
+Pulling into a dirty tree::
+
+When you are in the middle of something, you learn that there are
+upstream changes that are possibly relevant to what you are
+doing.  When your local changes do not conflict with the changes in
+the upstream, a simple `git pull` will let you move forward.
++
+However, there are cases in which your local changes do conflict with
+the upstream changes, and `git pull` refuses to overwrite your
+changes.  In such a case, you can stash your changes away,
+perform a pull, and then unstash, like this:
++
+----------------------------------------------------------------
+$ git pull
+...
+file foobar not up to date, cannot merge.
+$ git stash
+$ git pull
+$ git stash apply
+----------------------------------------------------------------
+
+Interrupted workflow::
+
+When you are in the middle of something, your boss comes in and
+demands that you fix something immediately.  Traditionally, you would
+make a commit to a temporary branch to store your changes away, and
+return to your original branch to make the emergency fix, like this:
++
+----------------------------------------------------------------
+... hack hack hack ...
+$ git checkout -b my_wip
+$ git commit -a -m "WIP"
+$ git checkout master
+$ edit emergency fix
+$ git commit -a -m "Fix in a hurry"
+$ git checkout my_wip
+$ git reset --soft HEAD^
+... continue hacking ...
+----------------------------------------------------------------
++
+You can use `git-stash` to simplify the above, like this:
++
+----------------------------------------------------------------
+... hack hack hack ...
+$ git stash
+$ edit emergency fix
+$ git commit -a -m "Fix in a hurry"
+$ git stash apply
+... continue hacking ...
+----------------------------------------------------------------
+
+SEE ALSO
+--------
+gitlink:git-checkout[1],
+gitlink:git-commit[1],
+gitlink:git-reflog[1],
+gitlink:git-reset[1]
+
+AUTHOR
+------
+Written by Nanako Shiraishi <nanako3@bluebottle.com>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-status.txt b/Documentation/git-status.txt
new file mode 100644 (file)
index 0000000..b0cb6bc
--- /dev/null
@@ -0,0 +1,71 @@
+git-status(1)
+=============
+
+NAME
+----
+git-status - Show the working tree status
+
+
+SYNOPSIS
+--------
+'git-status' <options>...
+
+DESCRIPTION
+-----------
+Examines paths in the working tree that has changes unrecorded
+to the index file, and changes between the index file and the
+current HEAD commit.  The former paths are what you _could_
+commit by running 'git add' before running 'git
+commit', and the latter paths are what you _would_ commit by
+running 'git commit'.
+
+If there is no path that is different between the index file and
+the current HEAD commit, the command exits with non-zero
+status.
+
+The command takes the same set of options as `git-commit`; it
+shows what would be committed if the same options are given to
+`git-commit`.
+
+If any paths have been touched in the working tree (that is,
+their modification times have changed) but their contents and
+permissions are identical to those in the index file, the command
+updates the index file. Running `git-status` can thus speed up
+subsequent operations such as `git-diff` if the working tree
+contains many paths that have been touched but not modified.
+
+
+OUTPUT
+------
+The output from this command is designed to be used as a commit
+template comments, and all the output lines are prefixed with '#'.
+
+The paths mentioned in the output, unlike many other git commands, are
+made relative to the current directory, if you are working in a
+subdirectory (this is on purpose, to help cutting and pasting).
+
+
+CONFIGURATION
+-------------
+
+The command honors `color.status` (or `status.color` -- they
+mean the same thing and the latter is kept for backward
+compatibility) and `color.status.<slot>` configuration variables
+to colorize its output.
+
+See Also
+--------
+gitlink:gitignore[5]
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org> and
+Junio C Hamano <junkio@cox.net>.
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-stripspace.txt b/Documentation/git-stripspace.txt
new file mode 100644 (file)
index 0000000..f80526b
--- /dev/null
@@ -0,0 +1,35 @@
+git-stripspace(1)
+=================
+
+NAME
+----
+git-stripspace - Filter out empty lines
+
+
+SYNOPSIS
+--------
+'git-stripspace' [-s | --strip-comments] < <stream>
+
+DESCRIPTION
+-----------
+Remove multiple empty lines, and empty lines at beginning and end.
+
+OPTIONS
+-------
+-s|--strip-comments::
+       In addition to empty lines, also strip lines starting with '#'.
+
+<stream>::
+       Byte stream to act on.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-submodule.txt b/Documentation/git-submodule.txt
new file mode 100644 (file)
index 0000000..335e973
--- /dev/null
@@ -0,0 +1,80 @@
+git-submodule(1)
+================
+
+NAME
+----
+git-submodule - Initialize, update or inspect submodules
+
+
+SYNOPSIS
+--------
+[verse]
+'git-submodule' [--quiet] [-b branch] add <repository> [<path>]
+'git-submodule' [--quiet] [--cached] [status|init|update] [--] [<path>...]
+
+
+COMMANDS
+--------
+add::
+       Add the given repository as a submodule at the given path
+       to the changeset to be committed next.  In particular, the
+       repository is cloned at the specified path, added to the
+       changeset and registered in .gitmodules.   If no path is
+       specified, the path is deduced from the repository specification.
+       If the repository url begins with ./ or ../, it is stored as
+       given but resolved as a relative path from the main project's
+       url when cloning.
+
+status::
+       Show the status of the submodules. This will print the SHA-1 of the
+       currently checked out commit for each submodule, along with the
+       submodule path and the output of gitlink:git-describe[1] for the
+       SHA-1. Each SHA-1 will be prefixed with `-` if the submodule is not
+       initialized and `+` if the currently checked out submodule commit
+       does not match the SHA-1 found in the index of the containing
+       repository. This command is the default command for git-submodule.
+
+init::
+       Initialize the submodules, i.e. register in .git/config each submodule
+       name and url found in .gitmodules. The key used in .git/config is
+       `submodule.$name.url`. This command does not alter existing information
+       in .git/config.
+
+update::
+       Update the registered submodules, i.e. clone missing submodules and
+       checkout the commit specified in the index of the containing repository.
+       This will make the submodules HEAD be detached.
+
+
+OPTIONS
+-------
+-q, --quiet::
+       Only print error messages.
+
+-b, --branch::
+       Branch of repository to add as submodule.
+
+--cached::
+       Display the SHA-1 stored in the index, not the SHA-1 of the currently
+       checked out submodule commit. This option is only valid for the
+       status command.
+
+<path>::
+       Path to submodule(s). When specified this will restrict the command
+       to only operate on the submodules found at the specified paths.
+
+FILES
+-----
+When initializing submodules, a .gitmodules file in the top-level directory
+of the containing repository is used to find the url of each submodule.
+This file should be formatted in the same way as $GIR_DIR/config. The key
+to each submodule url is "submodule.$name.url".
+
+
+AUTHOR
+------
+Written by Lars Hjemli <hjemli@gmail.com>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt
new file mode 100644 (file)
index 0000000..918a992
--- /dev/null
@@ -0,0 +1,576 @@
+git-svn(1)
+==========
+
+NAME
+----
+git-svn - Bidirectional operation between a single Subversion branch and git
+
+SYNOPSIS
+--------
+'git-svn' <command> [options] [arguments]
+
+DESCRIPTION
+-----------
+git-svn is a simple conduit for changesets between Subversion and git.
+It is not to be confused with gitlink:git-svnimport[1], which is
+read-only.
+
+git-svn was originally designed for an individual developer who wants a
+bidirectional flow of changesets between a single branch in Subversion
+and an arbitrary number of branches in git.  Since its inception,
+git-svn has gained the ability to track multiple branches in a manner
+similar to git-svnimport.
+
+git-svn is especially useful when it comes to tracking repositories
+not organized in the way Subversion developers recommend (trunk,
+branches, tags directories).
+
+COMMANDS
+--------
+--
+
+'init'::
+       Initializes an empty git repository with additional
+       metadata directories for git-svn.  The Subversion URL
+       may be specified as a command-line argument, or as full
+       URL arguments to -T/-t/-b.  Optionally, the target
+       directory to operate on can be specified as a second
+       argument.  Normally this command initializes the current
+       directory.
+
+-T<trunk_subdir>;;
+--trunk=<trunk_subdir>;;
+-t<tags_subdir>;;
+--tags=<tags_subdir>;;
+-b<branches_subdir>;;
+--branches=<branches_subdir>;;
+-s;;
+--stdlayout;;
+       These are optional command-line options for init.  Each of
+       these flags can point to a relative repository path
+       (--tags=project/tags') or a full url
+       (--tags=https://foo.org/project/tags). The option --stdlayout is
+       a shorthand way of setting trunk,tags,branches as the relative paths,
+       which is the Subversion default. If any of the other options are given
+       as well, they take precedence.
+--no-metadata;;
+       Set the 'noMetadata' option in the [svn-remote] config.
+--use-svm-props;;
+       Set the 'useSvmProps' option in the [svn-remote] config.
+--use-svnsync-props;;
+       Set the 'useSvnsyncProps' option in the [svn-remote] config.
+--rewrite-root=<URL>;;
+       Set the 'rewriteRoot' option in the [svn-remote] config.
+--username=<USER>;;
+       For transports that SVN handles authentication for (http,
+       https, and plain svn), specify the username.  For other
+       transports (eg svn+ssh://), you must include the username in
+       the URL, eg svn+ssh://foo@svn.bar.com/project
+--prefix=<prefix>;;
+       This allows one to specify a prefix which is prepended
+       to the names of remotes if trunk/branches/tags are
+       specified.  The prefix does not automatically include a
+       trailing slash, so be sure you include one in the
+       argument if that is what you want.  If --branches/-b is
+       specified, the prefix must include a trailing slash.
+       Setting a prefix is useful if you wish to track multiple
+       projects that share a common repository.
+
+'fetch'::
+       Fetch unfetched revisions from the Subversion remote we are
+       tracking.  The name of the [svn-remote "..."] section in the
+       .git/config file may be specified as an optional command-line
+       argument.
+
+'clone'::
+       Runs 'init' and 'fetch'.  It will automatically create a
+       directory based on the basename of the URL passed to it;
+       or if a second argument is passed; it will create a directory
+       and work within that.  It accepts all arguments that the
+       'init' and 'fetch' commands accept; with the exception of
+       '--fetch-all'.   After a repository is cloned, the 'fetch'
+       command will be able to update revisions without affecting
+       the working tree; and the 'rebase' command will be able
+       to update the working tree with the latest changes.
+
+'rebase'::
+       This fetches revisions from the SVN parent of the current HEAD
+       and rebases the current (uncommitted to SVN) work against it.
+
+This works similarly to 'svn update' or 'git-pull' except that
+it preserves linear history with 'git-rebase' instead of
+'git-merge' for ease of dcommiting with git-svn.
+
+This accepts all options that 'git-svn fetch' and 'git-rebase'
+accepts.  However '--fetch-all' only fetches from the current
+[svn-remote], and not all [svn-remote] definitions.
+
+Like 'git-rebase'; this requires that the working tree be clean
+and have no uncommitted changes.
+
+-l;;
+--local;;
+       Do not fetch remotely; only run 'git-rebase' against the
+       last fetched commit from the upstream SVN.
+
+'dcommit'::
+       Commit each diff from a specified head directly to the SVN
+       repository, and then rebase or reset (depending on whether or
+       not there is a diff between SVN and head).  This will create
+       a revision in SVN for each commit in git.
+       It is recommended that you run git-svn fetch and rebase (not
+       pull or merge) your commits against the latest changes in the
+       SVN repository.
+       An optional command-line argument may be specified as an
+       alternative to HEAD.
+       This is advantageous over 'set-tree' (below) because it produces
+       cleaner, more linear history.
++
+--no-rebase;;
+       After committing, do not rebase or reset.
+--
+
+'log'::
+       This should make it easy to look up svn log messages when svn
+       users refer to -r/--revision numbers.
++
+The following features from `svn log' are supported:
++
+--
+--revision=<n>[:<n>];;
+       is supported, non-numeric args are not:
+       HEAD, NEXT, BASE, PREV, etc ...
+-v/--verbose;;
+       it's not completely compatible with the --verbose
+       output in svn log, but reasonably close.
+--limit=<n>;;
+       is NOT the same as --max-count, doesn't count
+       merged/excluded commits
+--incremental;;
+       supported
+--
++
+New features:
++
+--
+--show-commit;;
+       shows the git commit sha1, as well
+--oneline;;
+       our version of --pretty=oneline
+--
++
+Any other arguments are passed directly to `git log'
+
+--
+'find-rev'::
+       When given an SVN revision number of the form 'rN', returns the
+       corresponding git commit hash (this can optionally be followed by a
+       tree-ish to specify which branch should be searched).  When given a
+       tree-ish, returns the corresponding SVN revision number.
+
+'set-tree'::
+       You should consider using 'dcommit' instead of this command.
+       Commit specified commit or tree objects to SVN.  This relies on
+       your imported fetch data being up-to-date.  This makes
+       absolutely no attempts to do patching when committing to SVN, it
+       simply overwrites files with those specified in the tree or
+       commit.  All merging is assumed to have taken place
+       independently of git-svn functions.
+
+'show-ignore'::
+       Recursively finds and lists the svn:ignore property on
+       directories.  The output is suitable for appending to
+       the $GIT_DIR/info/exclude file.
+
+'commit-diff'::
+       Commits the diff of two tree-ish arguments from the
+       command-line.  This command is intended for interoperability with
+       git-svnimport and does not rely on being inside an git-svn
+       init-ed repository.  This command takes three arguments, (a) the
+       original tree to diff against, (b) the new tree result, (c) the
+       URL of the target Subversion repository.  The final argument
+       (URL) may be omitted if you are working from a git-svn-aware
+       repository (that has been init-ed with git-svn).
+       The -r<revision> option is required for this.
+
+'info'::
+       Shows information about a file or directory similar to what
+       `svn info' provides.  Does not currently support a -r/--revision
+       argument.  Use the --url option to output only the value of the
+       'URL:' field.
+
+--
+
+OPTIONS
+-------
+--
+
+--shared[={false|true|umask|group|all|world|everybody}]::
+--template=<template_directory>::
+       Only used with the 'init' command.
+       These are passed directly to gitlink:git-init[1].
+
+-r <ARG>::
+--revision <ARG>::
+
+Used with the 'fetch' command.
+
+This allows revision ranges for partial/cauterized history
+to be supported.  $NUMBER, $NUMBER1:$NUMBER2 (numeric ranges),
+$NUMBER:HEAD, and BASE:$NUMBER are all supported.
+
+This can allow you to make partial mirrors when running fetch;
+but is generally not recommended because history will be skipped
+and lost.
+
+-::
+--stdin::
+
+Only used with the 'set-tree' command.
+
+Read a list of commits from stdin and commit them in reverse
+order.  Only the leading sha1 is read from each line, so
+git-rev-list --pretty=oneline output can be used.
+
+--rmdir::
+
+Only used with the 'dcommit', 'set-tree' and 'commit-diff' commands.
+
+Remove directories from the SVN tree if there are no files left
+behind.  SVN can version empty directories, and they are not
+removed by default if there are no files left in them.  git
+cannot version empty directories.  Enabling this flag will make
+the commit to SVN act like git.
+
+config key: svn.rmdir
+
+-e::
+--edit::
+
+Only used with the 'dcommit', 'set-tree' and 'commit-diff' commands.
+
+Edit the commit message before committing to SVN.  This is off by
+default for objects that are commits, and forced on when committing
+tree objects.
+
+config key: svn.edit
+
+-l<num>::
+--find-copies-harder::
+
+Only used with the 'dcommit', 'set-tree' and 'commit-diff' commands.
+
+They are both passed directly to git-diff-tree see
+gitlink:git-diff-tree[1] for more information.
+
+[verse]
+config key: svn.l
+config key: svn.findcopiesharder
+
+-A<filename>::
+--authors-file=<filename>::
+
+Syntax is compatible with the files used by git-svnimport and
+git-cvsimport:
+
+------------------------------------------------------------------------
+       loginname = Joe User <user@example.com>
+------------------------------------------------------------------------
+
+If this option is specified and git-svn encounters an SVN
+committer name that does not exist in the authors-file, git-svn
+will abort operation. The user will then have to add the
+appropriate entry.  Re-running the previous git-svn command
+after the authors-file is modified should continue operation.
+
+config key: svn.authorsfile
+
+-q::
+--quiet::
+       Make git-svn less verbose.
+
+--repack[=<n>]::
+--repack-flags=<flags>::
+
+These should help keep disk usage sane for large fetches
+with many revisions.
+
+--repack takes an optional argument for the number of revisions
+to fetch before repacking.  This defaults to repacking every
+1000 commits fetched if no argument is specified.
+
+--repack-flags are passed directly to gitlink:git-repack[1].
+
+[verse]
+config key: svn.repack
+config key: svn.repackflags
+
+-m::
+--merge::
+-s<strategy>::
+--strategy=<strategy>::
+
+These are only used with the 'dcommit' and 'rebase' commands.
+
+Passed directly to git-rebase when using 'dcommit' if a
+'git-reset' cannot be used (see dcommit).
+
+-n::
+--dry-run::
+
+This is only used with the 'dcommit' command.
+
+Print out the series of git arguments that would show
+which diffs would be committed to SVN.
+
+--
+
+ADVANCED OPTIONS
+----------------
+--
+
+-i<GIT_SVN_ID>::
+--id <GIT_SVN_ID>::
+
+This sets GIT_SVN_ID (instead of using the environment).  This
+allows the user to override the default refname to fetch from
+when tracking a single URL.  The 'log' and 'dcommit' commands
+no longer require this switch as an argument.
+
+-R<remote name>::
+--svn-remote <remote name>::
+       Specify the [svn-remote "<remote name>"] section to use,
+       this allows SVN multiple repositories to be tracked.
+       Default: "svn"
+
+--follow-parent::
+       This is especially helpful when we're tracking a directory
+       that has been moved around within the repository, or if we
+       started tracking a branch and never tracked the trunk it was
+       descended from. This feature is enabled by default, use
+       --no-follow-parent to disable it.
+
+config key: svn.followparent
+
+--
+CONFIG FILE-ONLY OPTIONS
+------------------------
+--
+
+svn.noMetadata::
+svn-remote.<name>.noMetadata::
+
+This gets rid of the git-svn-id: lines at the end of every commit.
+
+If you lose your .git/svn/git-svn/.rev_db file, git-svn will not
+be able to rebuild it and you won't be able to fetch again,
+either.  This is fine for one-shot imports.
+
+The 'git-svn log' command will not work on repositories using
+this, either.  Using this conflicts with the 'useSvmProps'
+option for (hopefully) obvious reasons.
+
+svn.useSvmProps::
+svn-remote.<name>.useSvmProps::
+
+This allows git-svn to re-map repository URLs and UUIDs from
+mirrors created using SVN::Mirror (or svk) for metadata.
+
+If an SVN revision has a property, "svm:headrev", it is likely
+that the revision was created by SVN::Mirror (also used by SVK).
+The property contains a repository UUID and a revision.  We want
+to make it look like we are mirroring the original URL, so
+introduce a helper function that returns the original identity
+URL and UUID, and use it when generating metadata in commit
+messages.
+
+svn.useSvnsyncProps::
+svn-remote.<name>.useSvnsyncprops::
+       Similar to the useSvmProps option; this is for users
+       of the svnsync(1) command distributed with SVN 1.4.x and
+       later.
+
+svn-remote.<name>.rewriteRoot::
+       This allows users to create repositories from alternate
+       URLs.  For example, an administrator could run git-svn on the
+       server locally (accessing via file://) but wish to distribute
+       the repository with a public http:// or svn:// URL in the
+       metadata so users of it will see the public URL.
+
+Since the noMetadata, rewriteRoot, useSvnsyncProps and useSvmProps
+options all affect the metadata generated and used by git-svn; they
+*must* be set in the configuration file before any history is imported
+and these settings should never be changed once they are set.
+
+Additionally, only one of these four options can be used per-svn-remote
+section because they affect the 'git-svn-id:' metadata line.
+
+--
+
+BASIC EXAMPLES
+--------------
+
+Tracking and contributing to the trunk of a Subversion-managed project:
+
+------------------------------------------------------------------------
+# Clone a repo (like git clone):
+       git-svn clone http://svn.foo.org/project/trunk
+# Enter the newly cloned directory:
+       cd trunk
+# You should be on master branch, double-check with git-branch
+       git branch
+# Do some work and commit locally to git:
+       git commit ...
+# Something is committed to SVN, rebase your local changes against the
+# latest changes in SVN:
+       git-svn rebase
+# Now commit your changes (that were committed previously using git) to SVN,
+# as well as automatically updating your working HEAD:
+       git-svn dcommit
+# Append svn:ignore settings to the default git exclude file:
+       git-svn show-ignore >> .git/info/exclude
+------------------------------------------------------------------------
+
+Tracking and contributing to an entire Subversion-managed project
+(complete with a trunk, tags and branches):
+
+------------------------------------------------------------------------
+# Clone a repo (like git clone):
+       git-svn clone http://svn.foo.org/project -T trunk -b branches -t tags
+# View all branches and tags you have cloned:
+       git branch -r
+# Reset your master to trunk (or any other branch, replacing 'trunk'
+# with the appropriate name):
+       git reset --hard remotes/trunk
+# You may only dcommit to one branch/tag/trunk at a time.  The usage
+# of dcommit/rebase/show-ignore should be the same as above.
+------------------------------------------------------------------------
+
+The initial 'git-svn clone' can be quite time-consuming
+(especially for large Subversion repositories). If multiple
+people (or one person with multiple machines) want to use
+git-svn to interact with the same Subversion repository, you can
+do the initial 'git-svn clone' to a repository on a server and
+have each person clone that repository with 'git clone':
+
+------------------------------------------------------------------------
+# Do the initial import on a server
+       ssh server "cd /pub && git-svn clone http://svn.foo.org/project
+# Clone locally
+       git clone server:/pub/project
+# Tell git-svn which branch contains the Subversion commits
+       git update-ref refs/remotes/git-svn origin/master
+# Initialize git-svn locally (be sure to use the same URL and -T/-b/-t options as were used on server)
+       git-svn init http://svn.foo.org/project
+# Pull the latest changes from Subversion
+       git-svn rebase
+------------------------------------------------------------------------
+
+REBASE VS. PULL/MERGE
+---------------------
+
+Originally, git-svn recommended that the remotes/git-svn branch be
+pulled or merged from.  This is because the author favored
+'git-svn set-tree B' to commit a single head rather than the
+'git-svn set-tree A..B' notation to commit multiple commits.
+
+If you use 'git-svn set-tree A..B' to commit several diffs and you do
+not have the latest remotes/git-svn merged into my-branch, you should
+use 'git-svn rebase' to update your work branch instead of 'git pull' or
+'git merge'.  'pull/merge' can cause non-linear history to be flattened
+when committing into SVN, which can lead to merge commits reversing
+previous commits in SVN.
+
+DESIGN PHILOSOPHY
+-----------------
+Merge tracking in Subversion is lacking and doing branched development
+with Subversion can be cumbersome as a result.  While git-svn can track
+copy history (including branches and tags) for repositories adopting a
+standard layout, it cannot yet represent merge history that happened
+inside git back upstream to SVN users.  Therefore it is advised that
+users keep history as linear as possible inside git to ease
+compatibility with SVN (see the CAVEATS section below).
+
+CAVEATS
+-------
+
+For the sake of simplicity and interoperating with a less-capable system
+(SVN), it is recommended that all git-svn users clone, fetch and dcommit
+directly from the SVN server, and avoid all git-clone/pull/merge/push
+operations between git repositories and branches.  The recommended
+method of exchanging code between git branches and users is
+git-format-patch and git-am, or just dcommiting to the SVN repository.
+
+Running 'git-merge' or 'git-pull' is NOT recommended on a branch you
+plan to dcommit from.  Subversion does not represent merges in any
+reasonable or useful fashion; so users using Subversion cannot see any
+merges you've made.  Furthermore, if you merge or pull from a git branch
+that is a mirror of an SVN branch, dcommit may commit to the wrong
+branch.
+
+'git-clone' does not clone branches under the refs/remotes/ hierarchy or
+any git-svn metadata, or config.  So repositories created and managed with
+using git-svn should use rsync(1) for cloning, if cloning is to be done
+at all.
+
+Since 'dcommit' uses rebase internally, any git branches you git-push to
+before dcommit on will require forcing an overwrite of the existing ref
+on the remote repository.  This is generally considered bad practice,
+see the git-push(1) documentation for details.
+
+Do not use the --amend option of git-commit(1) on a change you've
+already dcommitted.  It is considered bad practice to --amend commits
+you've already pushed to a remote repository for other users, and
+dcommit with SVN is analogous to that.
+
+BUGS
+----
+
+We ignore all SVN properties except svn:executable.  Any unhandled
+properties are logged to $GIT_DIR/svn/<refname>/unhandled.log
+
+Renamed and copied directories are not detected by git and hence not
+tracked when committing to SVN.  I do not plan on adding support for
+this as it's quite difficult and time-consuming to get working for all
+the possible corner cases (git doesn't do it, either).  Committing
+renamed and copied files are fully supported if they're similar enough
+for git to detect them.
+
+CONFIGURATION
+-------------
+
+git-svn stores [svn-remote] configuration information in the
+repository .git/config file.  It is similar the core git
+[remote] sections except 'fetch' keys do not accept glob
+arguments; but they are instead handled by the 'branches'
+and 'tags' keys.  Since some SVN repositories are oddly
+configured with multiple projects glob expansions such those
+listed below are allowed:
+
+------------------------------------------------------------------------
+[svn-remote "project-a"]
+       url = http://server.org/svn
+       branches = branches/*/project-a:refs/remotes/project-a/branches/*
+       tags = tags/*/project-a:refs/remotes/project-a/tags/*
+       trunk = trunk/project-a:refs/remotes/project-a/trunk
+------------------------------------------------------------------------
+
+Keep in mind that the '*' (asterisk) wildcard of the local ref
+(right of the ':') *must* be the farthest right path component;
+however the remote wildcard may be anywhere as long as it's own
+independent path component (surrounded by '/' or EOL).   This
+type of configuration is not automatically created by 'init' and
+should be manually entered with a text-editor or using
+gitlink:git-config[1]
+
+SEE ALSO
+--------
+gitlink:git-rebase[1]
+
+Author
+------
+Written by Eric Wong <normalperson@yhbt.net>.
+
+Documentation
+-------------
+Written by Eric Wong <normalperson@yhbt.net>.
diff --git a/Documentation/git-symbolic-ref.txt b/Documentation/git-symbolic-ref.txt
new file mode 100644 (file)
index 0000000..694caba
--- /dev/null
@@ -0,0 +1,61 @@
+git-symbolic-ref(1)
+===================
+
+NAME
+----
+git-symbolic-ref - Read and modify symbolic refs
+
+SYNOPSIS
+--------
+'git-symbolic-ref' [-q] [-m <reason>] <name> [<ref>]
+
+DESCRIPTION
+-----------
+Given one argument, reads which branch head the given symbolic
+ref refers to and outputs its path, relative to the `.git/`
+directory.  Typically you would give `HEAD` as the <name>
+argument to see on which branch your working tree is on.
+
+Give two arguments, create or update a symbolic ref <name> to
+point at the given branch <ref>.
+
+A symbolic ref is a regular file that stores a string that
+begins with `ref: refs/`.  For example, your `.git/HEAD` is
+a regular file whose contents is `ref: refs/heads/master`.
+
+OPTIONS
+-------
+
+-q, --quiet::
+       Do not issue an error message if the <name> is not a
+       symbolic ref but a detached HEAD; instead exit with
+       non-zero status silently.
+
+-m::
+       Update the reflog for <name> with <reason>.  This is valid only
+       when creating or updating a symbolic ref.
+
+NOTES
+-----
+In the past, `.git/HEAD` was a symbolic link pointing at
+`refs/heads/master`.  When we wanted to switch to another branch,
+we did `ln -sf refs/heads/newbranch .git/HEAD`, and when we wanted
+to find out which branch we are on, we did `readlink .git/HEAD`.
+This was fine, and internally that is what still happens by
+default, but on platforms that do not have working symlinks,
+or that do not have the `readlink(1)` command, this was a bit
+cumbersome.  On some platforms, `ln -sf` does not even work as
+advertised (horrors).  Therefore symbolic links are now deprecated
+and symbolic refs are used by default.
+
+git-symbolic-ref will exit with status 0 if the contents of the
+symbolic ref were printed correctly, with status 1 if the requested
+name is not a symbolic ref, or 128 if another error occurs.
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-tag.txt b/Documentation/git-tag.txt
new file mode 100644 (file)
index 0000000..784ec6d
--- /dev/null
@@ -0,0 +1,251 @@
+git-tag(1)
+==========
+
+NAME
+----
+git-tag - Create, list, delete or verify a tag object signed with GPG
+
+
+SYNOPSIS
+--------
+[verse]
+'git-tag' [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>]  <name> [<head>]
+'git-tag' -d <name>...
+'git-tag' [-n [<num>]] -l [<pattern>]
+'git-tag' -v <name>...
+
+DESCRIPTION
+-----------
+Adds a 'tag' reference in `.git/refs/tags/`
+
+Unless `-f` is given, the tag must not yet exist in
+`.git/refs/tags/` directory.
+
+If one of `-a`, `-s`, or `-u <key-id>` is passed, the command
+creates a 'tag' object, and requires the tag message.  Unless
+`-m <msg>` or `-F <file>` is given, an editor is started for the user to type
+in the tag message.
+
+Otherwise just the SHA1 object name of the commit object is
+written (i.e. a lightweight tag).
+
+A GnuPG signed tag object will be created when `-s` or `-u
+<key-id>` is used.  When `-u <key-id>` is not used, the
+committer identity for the current user is used to find the
+GnuPG key for signing.
+
+OPTIONS
+-------
+-a::
+       Make an unsigned, annotated tag object
+
+-s::
+       Make a GPG-signed tag, using the default e-mail address's key
+
+-u <key-id>::
+       Make a GPG-signed tag, using the given key
+
+-f::
+       Replace an existing tag with the given name (instead of failing)
+
+-d::
+       Delete existing tags with the given names.
+
+-v::
+       Verify the gpg signature of the given tag names.
+
+-n <num>::
+       <num> specifies how many lines from the annotation, if any,
+       are printed when using -l.
+       The default is not to print any annotation lines.
+       If no number is given to `-n`, only the first line is printed.
+
+-l <pattern>::
+       List tags with names that match the given pattern (or all if no pattern is given).
+       Typing "git tag" without arguments, also lists all tags.
+
+-m <msg>::
+       Use the given tag message (instead of prompting).
+       If multiple `-m` options are given, there values are
+       concatenated as separate paragraphs.
+
+-F <file>::
+       Take the tag message from the given file.  Use '-' to
+       read the message from the standard input.
+
+CONFIGURATION
+-------------
+By default, git-tag in sign-with-default mode (-s) will use your
+committer identity (of the form "Your Name <your@email.address>") to
+find a key.  If you want to use a different default key, you can specify
+it in the repository configuration as follows:
+
+-------------------------------------
+[user]
+    signingkey = <gpg-key-id>
+-------------------------------------
+
+
+DISCUSSION
+----------
+
+On Re-tagging
+~~~~~~~~~~~~~
+
+What should you do when you tag a wrong commit and you would
+want to re-tag?
+
+If you never pushed anything out, just re-tag it. Use "-f" to
+replace the old one. And you're done.
+
+But if you have pushed things out (or others could just read
+your repository directly), then others will have already seen
+the old tag. In that case you can do one of two things:
+
+. The sane thing.
+Just admit you screwed up, and use a different name. Others have
+already seen one tag-name, and if you keep the same name, you
+may be in the situation that two people both have "version X",
+but they actually have 'different' "X"'s.  So just call it "X.1"
+and be done with it.
+
+. The insane thing.
+You really want to call the new version "X" too, 'even though'
+others have already seen the old one. So just use "git tag -f"
+again, as if you hadn't already published the old one.
+
+However, Git does *not* (and it should not) change tags behind
+users back. So if somebody already got the old tag, doing a "git
+pull" on your tree shouldn't just make them overwrite the old
+one.
+
+If somebody got a release tag from you, you cannot just change
+the tag for them by updating your own one. This is a big
+security issue, in that people MUST be able to trust their
+tag-names.  If you really want to do the insane thing, you need
+to just fess up to it, and tell people that you messed up. You
+can do that by making a very public announcement saying:
+
+------------
+Ok, I messed up, and I pushed out an earlier version tagged as X. I
+then fixed something, and retagged the *fixed* tree as X again.
+
+If you got the wrong tag, and want the new one, please delete
+the old one and fetch the new one by doing:
+
+       git tag -d X
+       git fetch origin tag X
+
+to get my updated tag.
+
+You can test which tag you have by doing
+
+       git rev-parse X
+
+which should return 0123456789abcdef.. if you have the new version.
+
+Sorry for inconvenience.
+------------
+
+Does this seem a bit complicated?  It *should* be. There is no
+way that it would be correct to just "fix" it behind peoples
+backs. People need to know that their tags might have been
+changed.
+
+
+On Automatic following
+~~~~~~~~~~~~~~~~~~~~~~
+
+If you are following somebody else's tree, you are most likely
+using tracking branches (`refs/heads/origin` in traditional
+layout, or `refs/remotes/origin/master` in the separate-remote
+layout).  You usually want the tags from the other end.
+
+On the other hand, if you are fetching because you would want a
+one-shot merge from somebody else, you typically do not want to
+get tags from there.  This happens more often for people near
+the toplevel but not limited to them.  Mere mortals when pulling
+from each other do not necessarily want to automatically get
+private anchor point tags from the other person.
+
+You would notice "please pull" messages on the mailing list says
+repo URL and branch name alone.  This is designed to be easily
+cut&pasted to "git fetch" command line:
+
+------------
+Linus, please pull from
+
+       git://git..../proj.git master
+
+to get the following updates...
+------------
+
+becomes:
+
+------------
+$ git pull git://git..../proj.git master
+------------
+
+In such a case, you do not want to automatically follow other's
+tags.
+
+One important aspect of git is it is distributed, and being
+distributed largely means there is no inherent "upstream" or
+"downstream" in the system.  On the face of it, the above
+example might seem to indicate that the tag namespace is owned
+by upper echelon of people and tags only flow downwards, but
+that is not the case.  It only shows that the usage pattern
+determines who are interested in whose tags.
+
+A one-shot pull is a sign that a commit history is now crossing
+the boundary between one circle of people (e.g. "people who are
+primarily interested in networking part of the kernel") who may
+have their own set of tags (e.g. "this is the third release
+candidate from the networking group to be proposed for general
+consumption with 2.6.21 release") to another circle of people
+(e.g. "people who integrate various subsystem improvements").
+The latter are usually not interested in the detailed tags used
+internally in the former group (that is what "internal" means).
+That is why it is desirable not to follow tags automatically in
+this case.
+
+It may well be that among networking people, they may want to
+exchange the tags internal to their group, but in that workflow
+they are most likely tracking with each other's progress by
+having tracking branches.  Again, the heuristic to automatically
+follow such tags is a good thing.
+
+
+On Backdating Tags
+~~~~~~~~~~~~~~~~~~
+
+If you have imported some changes from another VCS and would like
+to add tags for major releases of your work, it is useful to be able
+to specify the date to embed inside of the tag object.  The data in
+the tag object affects, for example, the ordering of tags in the
+gitweb interface.
+
+To set the date used in future tag objects, set the environment
+variable GIT_AUTHOR_DATE to one or more of the date and time.  The
+date and time can be specified in a number of ways; the most common
+is "YYYY-MM-DD HH:MM".
+
+An example follows.
+
+------------
+$ GIT_AUTHOR_DATE="2006-10-02 10:31" git tag -s v1.0.1
+------------
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>,
+Junio C Hamano <junkio@cox.net> and Chris Wright <chrisw@osdl.org>.
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-tar-tree.txt b/Documentation/git-tar-tree.txt
new file mode 100644 (file)
index 0000000..434607b
--- /dev/null
@@ -0,0 +1,89 @@
+git-tar-tree(1)
+===============
+
+NAME
+----
+git-tar-tree - Create a tar archive of the files in the named tree object
+
+
+SYNOPSIS
+--------
+'git-tar-tree' [--remote=<repo>] <tree-ish> [ <base> ]
+
+DESCRIPTION
+-----------
+THIS COMMAND IS DEPRECATED.  Use `git-archive` with `--format=tar`
+option instead (and move the <base> argument to `--prefix=base/`).
+
+Creates a tar archive containing the tree structure for the named tree.
+When <base> is specified it is added as a leading path to the files in the
+generated tar archive.
+
+git-tar-tree behaves differently when given a tree ID versus when given
+a commit ID or tag ID.  In the first case the current time is used as
+modification time of each file in the archive.  In the latter case the
+commit time as recorded in the referenced commit object is used instead.
+Additionally the commit ID is stored in a global extended pax header.
+It can be extracted using git-get-tar-commit-id.
+
+OPTIONS
+-------
+
+<tree-ish>::
+       The tree or commit to produce tar archive for.  If it is
+       the object name of a commit object.
+
+<base>::
+       Leading path to the files in the resulting tar archive.
+
+--remote=<repo>::
+       Instead of making a tar archive from local repository,
+       retrieve a tar archive from a remote repository.
+
+CONFIGURATION
+-------------
+
+tar.umask::
+       This variable can be used to restrict the permission bits of
+       tar archive entries.  The default is 0002, which turns off the
+       world write bit.  The special value "user" indicates that the
+       archiving user's umask will be used instead.  See umask(2) for
+       details.
+
+EXAMPLES
+--------
+git tar-tree HEAD junk | (cd /var/tmp/ && tar xf -)::
+
+       Create a tar archive that contains the contents of the
+       latest commit on the current branch, and extracts it in
+       `/var/tmp/junk` directory.
+
+git tar-tree v1.4.0 git-1.4.0 | gzip >git-1.4.0.tar.gz::
+
+       Create a tarball for v1.4.0 release.
+
+git tar-tree v1.4.0{caret}\{tree\} git-1.4.0 | gzip >git-1.4.0.tar.gz::
+
+       Create a tarball for v1.4.0 release, but without a
+       global extended pax header.
+
+git tar-tree --remote=example.com:git.git v1.4.0 >git-1.4.0.tar::
+
+       Get a tarball v1.4.0 from example.com.
+
+git tar-tree HEAD:Documentation/ git-docs > git-1.4.0-docs.tar::
+
+       Put everything in the current head's Documentation/ directory
+       into 'git-1.4.0-docs.tar', with the prefix 'git-docs/'.
+
+Author
+------
+Written by Rene Scharfe.
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-tools.txt b/Documentation/git-tools.txt
new file mode 100644 (file)
index 0000000..a96403c
--- /dev/null
@@ -0,0 +1,118 @@
+A short git tools survey
+========================
+
+
+Introduction
+------------
+
+Apart from git contrib/ area there are some others third-party tools
+you may want to look.
+
+This document presents a brief summary of each tool and the corresponding
+link.
+
+
+Alternative/Augmentative Porcelains
+-----------------------------------
+
+   - *Cogito* (http://www.kernel.org/pub/software/scm/cogito/)
+
+   Cogito is a version control system layered on top of the git tree history
+   storage system. It aims at seamless user interface and ease of use,
+   providing generally smoother user experience than the "raw" Core GIT
+   itself and indeed many other version control systems.
+
+   Cogito is no longer maintained as most of its functionality
+   is now in core GIT.
+
+
+   - *pg* (http://www.spearce.org/category/projects/scm/pg/)
+
+   pg is a shell script wrapper around GIT to help the user manage a set of
+   patches to files. pg is somewhat like quilt or StGIT, but it does have a
+   slightly different feature set.
+
+
+   - *StGit* (http://www.procode.org/stgit/)
+
+   Stacked GIT provides a quilt-like patch management functionality in the
+   GIT environment. You can easily manage your patches in the scope of GIT
+   until they get merged upstream.
+
+
+History Viewers
+---------------
+
+   - *gitk* (shipped with git-core)
+
+   gitk is a simple Tk GUI for browsing history of GIT repositories easily.
+
+
+   - *gitview*  (contrib/)
+
+   gitview is a GTK based repository browser for git
+
+
+   - *gitweb* (shipped with git-core)
+
+   GITweb provides full-fledged web interface for GIT repositories.
+
+
+   - *qgit* (http://digilander.libero.it/mcostalba/)
+
+   QGit is a git/StGIT GUI viewer built on Qt/C++. QGit could be used
+   to browse history and directory tree, view annotated files, commit
+   changes cherry picking single files or applying patches.
+   Currently it is the fastest and most feature rich among the git
+   viewers and commit tools.
+
+   - *tig* (http://jonas.nitro.dk/tig/)
+
+   tig by Jonas Fonseca is a simple git repository browser
+   written using ncurses. Basically, it just acts as a front-end
+   for git-log and git-show/git-diff. Additionally, you can also
+   use it as a pager for git commands.
+
+
+Foreign SCM interface
+---------------------
+
+   - *git-svn* (shipped with git-core)
+
+   git-svn is a simple conduit for changesets between a single Subversion
+   branch and git.
+
+
+   - *quilt2git / git2quilt* (http://home-tj.org/wiki/index.php/Misc)
+
+   These utilities convert patch series in a quilt repository and commit
+   series in git back and forth.
+
+
+   - *hg-to-git* (contrib/)
+
+   hg-to-git converts a Mercurial repository into a git one, and
+   preserves the full branch history in the process. hg-to-git can
+   also be used in an incremental way to keep the git repository
+   in sync with the master Mercurial repository.
+
+
+Others
+------
+
+   - *(h)gct* (http://www.cyd.liu.se/users/~freku045/gct/)
+
+   Commit Tool or (h)gct is a GUI enabled commit tool for git and
+   Mercurial (hg). It allows the user to view diffs, select which files
+   to committed (or ignored / reverted) write commit messages and
+   perform the commit itself.
+
+   - *git.el* (contrib/)
+
+   This is an Emacs interface for git. The user interface is modeled on
+   pcl-cvs. It has been developed on Emacs 21 and will probably need some
+   tweaking to work on XEmacs.
+
+
+http://git.or.cz/gitwiki/InterfacesFrontendsAndTools has more
+comprehensive list.
diff --git a/Documentation/git-unpack-file.txt b/Documentation/git-unpack-file.txt
new file mode 100644 (file)
index 0000000..20bb6a7
--- /dev/null
@@ -0,0 +1,35 @@
+git-unpack-file(1)
+==================
+
+NAME
+----
+git-unpack-file - Creates a temporary file with a blob's contents
+
+
+
+SYNOPSIS
+--------
+'git-unpack-file' <blob>
+
+DESCRIPTION
+-----------
+Creates a file holding the contents of the blob specified by sha1. It
+returns the name of the temporary file in the following format:
+       .merge_file_XXXXX
+
+OPTIONS
+-------
+<blob>::
+       Must be a blob id
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-unpack-objects.txt b/Documentation/git-unpack-objects.txt
new file mode 100644 (file)
index 0000000..d529a43
--- /dev/null
@@ -0,0 +1,54 @@
+git-unpack-objects(1)
+=====================
+
+NAME
+----
+git-unpack-objects - Unpack objects from a packed archive
+
+
+SYNOPSIS
+--------
+'git-unpack-objects' [-n] [-q] [-r] <pack-file
+
+
+DESCRIPTION
+-----------
+Read a packed archive (.pack) from the standard input, expanding
+the objects contained within and writing them into the repository in
+"loose" (one object per file) format.
+
+Objects that already exist in the repository will *not* be unpacked
+from the pack-file.  Therefore, nothing will be unpacked if you use
+this command on a pack-file that exists within the target repository.
+
+Please see the `git-repack` documentation for options to generate
+new packs and replace existing ones.
+
+OPTIONS
+-------
+-n::
+        Dry run.  Check the pack file without actually unpacking
+       the objects.
+
+-q::
+       The command usually shows percentage progress.  This
+       flag suppresses it.
+
+-r::
+       When unpacking a corrupt packfile, the command dies at
+       the first corruption.  This flag tells it to keep going
+       and make the best effort to recover as many objects as
+       possible.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+-------------
+Documentation by Junio C Hamano
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-update-index.txt b/Documentation/git-update-index.txt
new file mode 100644 (file)
index 0000000..0a19538
--- /dev/null
@@ -0,0 +1,326 @@
+git-update-index(1)
+===================
+
+NAME
+----
+git-update-index - Register file contents in the working tree to the index
+
+
+SYNOPSIS
+--------
+[verse]
+'git-update-index'
+            [--add] [--remove | --force-remove] [--replace]
+            [--refresh] [-q] [--unmerged] [--ignore-missing]
+            [--cacheinfo <mode> <object> <file>]\*
+            [--chmod=(+|-)x]
+            [--assume-unchanged | --no-assume-unchanged]
+            [--really-refresh] [--unresolve] [--again | -g]
+            [--info-only] [--index-info]
+            [-z] [--stdin]
+            [--verbose]
+            [--] [<file>]\*
+
+DESCRIPTION
+-----------
+Modifies the index or directory cache. Each file mentioned is updated
+into the index and any 'unmerged' or 'needs updating' state is
+cleared.
+
+See also gitlink:git-add[1] for a more user-friendly way to do some of
+the most common operations on the index.
+
+The way "git-update-index" handles files it is told about can be modified
+using the various options:
+
+OPTIONS
+-------
+--add::
+       If a specified file isn't in the index already then it's
+       added.
+       Default behaviour is to ignore new files.
+
+--remove::
+       If a specified file is in the index but is missing then it's
+       removed.
+       Default behavior is to ignore removed file.
+
+--refresh::
+       Looks at the current index and checks to see if merges or
+       updates are needed by checking stat() information.
+
+-q::
+        Quiet.  If --refresh finds that the index needs an update, the
+        default behavior is to error out.  This option makes
+        git-update-index continue anyway.
+
+--unmerged::
+        If --refresh finds unmerged changes in the index, the default
+        behavior is to error out.  This option makes git-update-index
+        continue anyway.
+
+--ignore-missing::
+       Ignores missing files during a --refresh
+
+--cacheinfo <mode> <object> <path>::
+       Directly insert the specified info into the index.
+
+--index-info::
+        Read index information from stdin.
+
+--chmod=(+|-)x::
+        Set the execute permissions on the updated files.
+
+--assume-unchanged, --no-assume-unchanged::
+       When these flags are specified, the object name recorded
+       for the paths are not updated.  Instead, these options
+       sets and unsets the "assume unchanged" bit for the
+       paths.  When the "assume unchanged" bit is on, git stops
+       checking the working tree files for possible
+       modifications, so you need to manually unset the bit to
+       tell git when you change the working tree file. This is
+       sometimes helpful when working with a big project on a
+       filesystem that has very slow lstat(2) system call
+       (e.g. cifs).
+
+--again, -g::
+       Runs `git-update-index` itself on the paths whose index
+       entries are different from those from the `HEAD` commit.
+
+--unresolve::
+       Restores the 'unmerged' or 'needs updating' state of a
+       file during a merge if it was cleared by accident.
+
+--info-only::
+       Do not create objects in the object database for all
+       <file> arguments that follow this flag; just insert
+       their object IDs into the index.
+
+--force-remove::
+       Remove the file from the index even when the working directory
+       still has such a file. (Implies --remove.)
+
+--replace::
+       By default, when a file `path` exists in the index,
+       git-update-index refuses an attempt to add `path/file`.
+       Similarly if a file `path/file` exists, a file `path`
+       cannot be added.  With --replace flag, existing entries
+       that conflicts with the entry being added are
+       automatically removed with warning messages.
+
+--stdin::
+       Instead of taking list of paths from the command line,
+       read list of paths from the standard input.  Paths are
+       separated by LF (i.e. one path per line) by default.
+
+--verbose::
+        Report what is being added and removed from index.
+
+-z::
+       Only meaningful with `--stdin`; paths are separated with
+       NUL character instead of LF.
+
+\--::
+       Do not interpret any more arguments as options.
+
+<file>::
+       Files to act on.
+       Note that files beginning with '.' are discarded. This includes
+       `./file` and `dir/./file`. If you don't want this, then use
+       cleaner names.
+       The same applies to directories ending '/' and paths with '//'
+
+Using --refresh
+---------------
+'--refresh' does not calculate a new sha1 file or bring the index
+up-to-date for mode/content changes. But what it *does* do is to
+"re-match" the stat information of a file with the index, so that you
+can refresh the index for a file that hasn't been changed but where
+the stat entry is out of date.
+
+For example, you'd want to do this after doing a "git-read-tree", to link
+up the stat index details with the proper files.
+
+Using --cacheinfo or --info-only
+--------------------------------
+'--cacheinfo' is used to register a file that is not in the
+current working directory.  This is useful for minimum-checkout
+merging.
+
+To pretend you have a file with mode and sha1 at path, say:
+
+----------------
+$ git-update-index --cacheinfo mode sha1 path
+----------------
+
+'--info-only' is used to register files without placing them in the object
+database.  This is useful for status-only repositories.
+
+Both '--cacheinfo' and '--info-only' behave similarly: the index is updated
+but the object database isn't.  '--cacheinfo' is useful when the object is
+in the database but the file isn't available locally.  '--info-only' is
+useful when the file is available, but you do not wish to update the
+object database.
+
+
+Using --index-info
+------------------
+
+`--index-info` is a more powerful mechanism that lets you feed
+multiple entry definitions from the standard input, and designed
+specifically for scripts.  It can take inputs of three formats:
+
+    . mode         SP sha1          TAB path
++
+The first format is what "git-apply --index-info"
+reports, and used to reconstruct a partial tree
+that is used for phony merge base tree when falling
+back on 3-way merge.
+
+    . mode SP type SP sha1          TAB path
++
+The second format is to stuff git-ls-tree output
+into the index file.
+
+    . mode         SP sha1 SP stage TAB path
++
+This format is to put higher order stages into the
+index file and matches git-ls-files --stage output.
+
+To place a higher stage entry to the index, the path should
+first be removed by feeding a mode=0 entry for the path, and
+then feeding necessary input lines in the third format.
+
+For example, starting with this index:
+
+------------
+$ git ls-files -s
+100644 8a1218a1024a212bb3db30becd860315f9f3ac52 0       frotz
+------------
+
+you can feed the following input to `--index-info`:
+
+------------
+$ git update-index --index-info
+0 0000000000000000000000000000000000000000     frotz
+100644 8a1218a1024a212bb3db30becd860315f9f3ac52 1      frotz
+100755 8a1218a1024a212bb3db30becd860315f9f3ac52 2      frotz
+------------
+
+The first line of the input feeds 0 as the mode to remove the
+path; the SHA1 does not matter as long as it is well formatted.
+Then the second and third line feeds stage 1 and stage 2 entries
+for that path.  After the above, we would end up with this:
+
+------------
+$ git ls-files -s
+100644 8a1218a1024a212bb3db30becd860315f9f3ac52 1      frotz
+100755 8a1218a1024a212bb3db30becd860315f9f3ac52 2      frotz
+------------
+
+
+Using ``assume unchanged'' bit
+------------------------------
+
+Many operations in git depend on your filesystem to have an
+efficient `lstat(2)` implementation, so that `st_mtime`
+information for working tree files can be cheaply checked to see
+if the file contents have changed from the version recorded in
+the index file.  Unfortunately, some filesystems have
+inefficient `lstat(2)`.  If your filesystem is one of them, you
+can set "assume unchanged" bit to paths you have not changed to
+cause git not to do this check.  Note that setting this bit on a
+path does not mean git will check the contents of the file to
+see if it has changed -- it makes git to omit any checking and
+assume it has *not* changed.  When you make changes to working
+tree files, you have to explicitly tell git about it by dropping
+"assume unchanged" bit, either before or after you modify them.
+
+In order to set "assume unchanged" bit, use `--assume-unchanged`
+option.  To unset, use `--no-assume-unchanged`.
+
+The command looks at `core.ignorestat` configuration variable.  When
+this is true, paths updated with `git-update-index paths...` and
+paths updated with other git commands that update both index and
+working tree (e.g. `git-apply --index`, `git-checkout-index -u`,
+and `git-read-tree -u`) are automatically marked as "assume
+unchanged".  Note that "assume unchanged" bit is *not* set if
+`git-update-index --refresh` finds the working tree file matches
+the index (use `git-update-index --really-refresh` if you want
+to mark them as "assume unchanged").
+
+
+Examples
+--------
+To update and refresh only the files already checked out:
+
+----------------
+$ git-checkout-index -n -f -a && git-update-index --ignore-missing --refresh
+----------------
+
+On an inefficient filesystem with `core.ignorestat` set::
++
+------------
+$ git update-index --really-refresh              <1>
+$ git update-index --no-assume-unchanged foo.c   <2>
+$ git diff --name-only                           <3>
+$ edit foo.c
+$ git diff --name-only                           <4>
+M foo.c
+$ git update-index foo.c                         <5>
+$ git diff --name-only                           <6>
+$ edit foo.c
+$ git diff --name-only                           <7>
+$ git update-index --no-assume-unchanged foo.c   <8>
+$ git diff --name-only                           <9>
+M foo.c
+------------
++
+<1> forces lstat(2) to set "assume unchanged" bits for paths that match index.
+<2> mark the path to be edited.
+<3> this does lstat(2) and finds index matches the path.
+<4> this does lstat(2) and finds index does *not* match the path.
+<5> registering the new version to index sets "assume unchanged" bit.
+<6> and it is assumed unchanged.
+<7> even after you edit it.
+<8> you can tell about the change after the fact.
+<9> now it checks with lstat(2) and finds it has been changed.
+
+
+Configuration
+-------------
+
+The command honors `core.filemode` configuration variable.  If
+your repository is on an filesystem whose executable bits are
+unreliable, this should be set to 'false' (see gitlink:git-config[1]).
+This causes the command to ignore differences in file modes recorded
+in the index and the file mode on the filesystem if they differ only on
+executable bit.   On such an unfortunate filesystem, you may
+need to use `git-update-index --chmod=`.
+
+Quite similarly, if `core.symlinks` configuration variable is set
+to 'false' (see gitlink:git-config[1]), symbolic links are checked out
+as plain files, and this command does not modify a recorded file mode
+from symbolic link to regular file.
+
+The command looks at `core.ignorestat` configuration variable.  See
+'Using "assume unchanged" bit' section above.
+
+
+See Also
+--------
+gitlink:git-config[1],
+gitlink:git-add[1]
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt
new file mode 100644 (file)
index 0000000..f222616
--- /dev/null
@@ -0,0 +1,93 @@
+git-update-ref(1)
+=================
+
+NAME
+----
+git-update-ref - Update the object name stored in a ref safely
+
+SYNOPSIS
+--------
+'git-update-ref' [-m <reason>] (-d <ref> <oldvalue> | [--no-deref] <ref> <newvalue> [<oldvalue>])
+
+DESCRIPTION
+-----------
+Given two arguments, stores the <newvalue> in the <ref>, possibly
+dereferencing the symbolic refs.  E.g. `git-update-ref HEAD
+<newvalue>` updates the current branch head to the new object.
+
+Given three arguments, stores the <newvalue> in the <ref>,
+possibly dereferencing the symbolic refs, after verifying that
+the current value of the <ref> matches <oldvalue>.
+E.g. `git-update-ref refs/heads/master <newvalue> <oldvalue>`
+updates the master branch head to <newvalue> only if its current
+value is <oldvalue>.  You can specify 40 "0" or an empty string
+as <oldvalue> to make sure that the ref you are creating does
+not exist.
+
+It also allows a "ref" file to be a symbolic pointer to another
+ref file by starting with the four-byte header sequence of
+"ref:".
+
+More importantly, it allows the update of a ref file to follow
+these symbolic pointers, whether they are symlinks or these
+"regular file symbolic refs".  It follows *real* symlinks only
+if they start with "refs/": otherwise it will just try to read
+them and update them as a regular file (i.e. it will allow the
+filesystem to follow them, but will overwrite such a symlink to
+somewhere else with a regular filename).
+
+If --no-deref is given, <ref> itself is overwritten, rather than
+the result of following the symbolic pointers.
+
+In general, using
+
+       git-update-ref HEAD "$head"
+
+should be a _lot_ safer than doing
+
+       echo "$head" > "$GIT_DIR/HEAD"
+
+both from a symlink following standpoint *and* an error checking
+standpoint.  The "refs/" rule for symlinks means that symlinks
+that point to "outside" the tree are safe: they'll be followed
+for reading but not for writing (so we'll never write through a
+ref symlink to some other tree, if you have copied a whole
+archive by creating a symlink tree).
+
+With `-d` flag, it deletes the named <ref> after verifying it
+still contains <oldvalue>.
+
+
+Logging Updates
+---------------
+If config parameter "core.logAllRefUpdates" is true or the file
+"$GIT_DIR/logs/<ref>" exists then `git-update-ref` will append
+a line to the log file "$GIT_DIR/logs/<ref>" (dereferencing all
+symbolic refs before creating the log name) describing the change
+in ref value.  Log lines are formatted as:
+
+    . oldsha1 SP newsha1 SP committer LF
++
+Where "oldsha1" is the 40 character hexadecimal value previously
+stored in <ref>, "newsha1" is the 40 character hexadecimal value of
+<newvalue> and "committer" is the committer's name, email address
+and date in the standard GIT committer ident format.
+
+Optionally with -m:
+
+    . oldsha1 SP newsha1 SP committer TAB message LF
++
+Where all fields are as described above and "message" is the
+value supplied to the -m option.
+
+An update will fail (without changing <ref>) if the current user is
+unable to create a new log file, append to the existing log file
+or does not have committer information available.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-update-server-info.txt b/Documentation/git-update-server-info.txt
new file mode 100644 (file)
index 0000000..e7e82a3
--- /dev/null
@@ -0,0 +1,57 @@
+git-update-server-info(1)
+=========================
+
+NAME
+----
+git-update-server-info - Update auxiliary info file to help dumb servers
+
+
+SYNOPSIS
+--------
+'git-update-server-info' [--force]
+
+DESCRIPTION
+-----------
+A dumb server that does not do on-the-fly pack generations must
+have some auxiliary information files in $GIT_DIR/info and
+$GIT_OBJECT_DIRECTORY/info directories to help clients discover
+what references and packs the server has.  This command
+generates such auxiliary files.
+
+
+OPTIONS
+-------
+
+-f|--force::
+       Update the info files from scratch.
+
+
+OUTPUT
+------
+
+Currently the command updates the following files.  Please see
+link:repository-layout.html[repository-layout] for description
+of what they are for:
+
+* objects/info/packs
+
+* info/refs
+
+
+BUGS
+----
+When you remove an existing ref, the command fails to update
+info/refs file unless `--force` flag is given.
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-upload-archive.txt b/Documentation/git-upload-archive.txt
new file mode 100644 (file)
index 0000000..403871d
--- /dev/null
@@ -0,0 +1,37 @@
+git-upload-archive(1)
+====================
+
+NAME
+----
+git-upload-archive - Send archive back to git-archive
+
+
+SYNOPSIS
+--------
+'git-upload-archive' <directory>
+
+DESCRIPTION
+-----------
+Invoked by 'git-archive --remote' and sends a generated archive to the
+other end over the git protocol.
+
+This command is usually not invoked directly by the end user.  The UI
+for the protocol is on the 'git-archive' side, and the program pair
+is meant to be used to get an archive from a remote repository.
+
+OPTIONS
+-------
+<directory>::
+       The repository to get a tar archive from.
+
+Author
+------
+Written by Franck Bui-Huu.
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-upload-pack.txt b/Documentation/git-upload-pack.txt
new file mode 100644 (file)
index 0000000..fd65192
--- /dev/null
@@ -0,0 +1,46 @@
+git-upload-pack(1)
+==================
+
+NAME
+----
+git-upload-pack - Send objects packed back to git-fetch-pack
+
+
+SYNOPSIS
+--------
+'git-upload-pack' [--strict] [--timeout=<n>] <directory>
+
+DESCRIPTION
+-----------
+Invoked by 'git-fetch-pack', learns what
+objects the other side is missing, and sends them after packing.
+
+This command is usually not invoked directly by the end user.
+The UI for the protocol is on the 'git-fetch-pack' side, and the
+program pair is meant to be used to pull updates from a remote
+repository.  For push operations, see 'git-send-pack'.
+
+
+OPTIONS
+-------
+
+\--strict::
+       Do not try <directory>/.git/ if <directory> is no git directory.
+
+\--timeout=<n>::
+       Interrupt transfer after <n> seconds of inactivity.
+
+<directory>::
+       The repository to sync from.
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-var.txt b/Documentation/git-var.txt
new file mode 100644 (file)
index 0000000..8139423
--- /dev/null
@@ -0,0 +1,64 @@
+git-var(1)
+==========
+
+NAME
+----
+git-var - Show a git logical variable
+
+
+SYNOPSIS
+--------
+'git-var' [ -l | <variable> ]
+
+DESCRIPTION
+-----------
+Prints a git logical variable.
+
+OPTIONS
+-------
+-l::
+       Cause the logical variables to be listed. In addition, all the
+       variables of the git configuration file .git/config are listed
+       as well. (However, the configuration variables listing functionality
+       is deprecated in favor of `git-config -l`.)
+
+EXAMPLE
+--------
+       $ git-var GIT_AUTHOR_IDENT
+       Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600
+
+
+VARIABLES
+----------
+GIT_AUTHOR_IDENT::
+    The author of a piece of code.
+
+GIT_COMMITTER_IDENT::
+    The person who put a piece of code into git.
+
+Diagnostics
+-----------
+You don't exist. Go away!::
+    The passwd(5) gecos field couldn't be read
+Your parents must have hated you!::
+    The password(5) gecos field is longer than a giant static buffer.
+Your sysadmin must hate you!::
+    The password(5) name field is longer than a giant static buffer.
+
+See Also
+--------
+gitlink:git-commit-tree[1]
+gitlink:git-tag[1]
+gitlink:git-config[1]
+
+Author
+------
+Written by Eric Biederman <ebiederm@xmission.com>
+
+Documentation
+--------------
+Documentation by Eric Biederman and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-verify-pack.txt b/Documentation/git-verify-pack.txt
new file mode 100644 (file)
index 0000000..f4c540f
--- /dev/null
@@ -0,0 +1,53 @@
+git-verify-pack(1)
+==================
+
+NAME
+----
+git-verify-pack - Validate packed git archive files
+
+
+SYNOPSIS
+--------
+'git-verify-pack' [-v] [--] <pack>.idx ...
+
+
+DESCRIPTION
+-----------
+Reads given idx file for packed git archive created with
+git-pack-objects command and verifies idx file and the
+corresponding pack file.
+
+OPTIONS
+-------
+<pack>.idx ...::
+       The idx files to verify.
+
+-v::
+       After verifying the pack, show list of objects contained
+       in the pack.
+\--::
+       Do not interpret any more arguments as options.
+
+OUTPUT FORMAT
+-------------
+When specifying the -v option the format used is:
+
+       SHA1 type size offset-in-packfile
+
+for objects that are not deltified in the pack, and
+
+       SHA1 type size offset-in-packfile depth base-SHA1
+
+for objects that are deltified.
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by Junio C Hamano
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-verify-tag.txt b/Documentation/git-verify-tag.txt
new file mode 100644 (file)
index 0000000..ac7fb19
--- /dev/null
@@ -0,0 +1,31 @@
+git-verify-tag(1)
+=================
+
+NAME
+----
+git-verify-tag - Check the GPG signature of tags
+
+SYNOPSIS
+--------
+'git-verify-tag' <tag>...
+
+DESCRIPTION
+-----------
+Validates the gpg signature created by git-tag.
+
+OPTIONS
+-------
+<tag>::
+       SHA1 identifier of a git tag object.
+
+Author
+------
+Written by Jan Harkes <jaharkes@cs.cmu.edu> and Eric W. Biederman <ebiederm@xmission.com>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-whatchanged.txt b/Documentation/git-whatchanged.txt
new file mode 100644 (file)
index 0000000..607df48
--- /dev/null
@@ -0,0 +1,80 @@
+git-whatchanged(1)
+==================
+
+NAME
+----
+git-whatchanged - Show logs with difference each commit introduces
+
+
+SYNOPSIS
+--------
+'git-whatchanged' <option>...
+
+DESCRIPTION
+-----------
+Shows commit logs and diff output each commit introduces.  The
+command internally invokes 'git-rev-list' piped to
+'git-diff-tree', and takes command line options for both of
+these commands.
+
+This manual page describes only the most frequently used options.
+
+
+OPTIONS
+-------
+-p::
+       Show textual diffs, instead of the git internal diff
+       output format that is useful only to tell the changed
+       paths and their nature of changes.
+
+-<n>::
+       Limit output to <n> commits.
+
+<since>..<until>::
+       Limit output to between the two named commits (bottom
+       exclusive, top inclusive).
+
+-r::
+       Show git internal diff output, but for the whole tree,
+       not just the top level.
+
+--pretty=<format>::
+       Controls the output format for the commit logs.
+       <format> can be one of 'raw', 'medium', 'short', 'full',
+       and 'oneline'.
+
+-m::
+       By default, differences for merge commits are not shown.
+       With this flag, show differences to that commit from all
+       of its parents.
++
+However, it is not very useful in general, although it
+*is* useful on a file-by-file basis.
+
+Examples
+--------
+git-whatchanged -p v2.6.12.. include/scsi drivers/scsi::
+
+       Show as patches the commits since version 'v2.6.12' that changed
+       any file in the include/scsi or drivers/scsi subdirectories
+
+git-whatchanged --since="2 weeks ago" \-- gitk::
+
+       Show the changes during the last two weeks to the file 'gitk'.
+       The "--" is necessary to avoid confusion with the *branch* named
+       'gitk'
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org> and
+Junio C Hamano <junkio@cox.net>
+
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-write-tree.txt b/Documentation/git-write-tree.txt
new file mode 100644 (file)
index 0000000..cb8d6aa
--- /dev/null
@@ -0,0 +1,49 @@
+git-write-tree(1)
+=================
+
+NAME
+----
+git-write-tree - Create a tree object from the current index
+
+
+SYNOPSIS
+--------
+'git-write-tree' [--missing-ok] [--prefix=<prefix>/]
+
+DESCRIPTION
+-----------
+Creates a tree object using the current index.
+
+The index must be in a fully merged state.
+
+Conceptually, `git-write-tree` sync()s the current index contents
+into a set of tree files.
+In order to have that match what is actually in your directory right
+now, you need to have done a `git-update-index` phase before you did the
+`git-write-tree`.
+
+
+OPTIONS
+-------
+--missing-ok::
+       Normally `git-write-tree` ensures that the objects referenced by the
+       directory exist in the object database.  This option disables this
+       check.
+
+--prefix=<prefix>/::
+       Writes a tree object that represents a subdirectory
+       `<prefix>`.  This can be used to write the tree object
+       for a subproject that is in the named subdirectory.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git.txt b/Documentation/git.txt
new file mode 100644 (file)
index 0000000..1574ecd
--- /dev/null
@@ -0,0 +1,550 @@
+git(7)
+======
+
+NAME
+----
+git - the stupid content tracker
+
+
+SYNOPSIS
+--------
+[verse]
+'git' [--version] [--exec-path[=GIT_EXEC_PATH]]
+    [-p|--paginate|--no-pager]
+    [--bare] [--git-dir=GIT_DIR] [--work-tree=GIT_WORK_TREE]
+    [--help] COMMAND [ARGS]
+
+DESCRIPTION
+-----------
+Git is a fast, scalable, distributed revision control system with an
+unusually rich command set that provides both high-level operations
+and full access to internals.
+
+See this link:tutorial.html[tutorial] to get started, then see
+link:everyday.html[Everyday Git] for a useful minimum set of commands, and
+"man git-commandname" for documentation of each command.  CVS users may
+also want to read link:cvs-migration.html[CVS migration].  See
+link:user-manual.html[Git User's Manual] for a more in-depth
+introduction.
+
+The COMMAND is either a name of a Git command (see below) or an alias
+as defined in the configuration file (see gitlink:git-config[1]).
+
+Formatted and hyperlinked version of the latest git
+documentation can be viewed at
+`http://www.kernel.org/pub/software/scm/git/docs/`.
+
+ifdef::stalenotes[]
+[NOTE]
+============
+
+You are reading the documentation for the latest (possibly
+unreleased) version of git, that is available from 'master'
+branch of the `git.git` repository.
+Documentation for older releases are available here:
+
+* link:v1.5.3/git.html[documentation for release 1.5.3]
+
+* release notes for
+  link:RelNotes-1.5.3.7.txt[1.5.3.7],
+  link:RelNotes-1.5.3.6.txt[1.5.3.6],
+  link:RelNotes-1.5.3.5.txt[1.5.3.5],
+  link:RelNotes-1.5.3.4.txt[1.5.3.4],
+  link:RelNotes-1.5.3.3.txt[1.5.3.3],
+  link:RelNotes-1.5.3.2.txt[1.5.3.2],
+  link:RelNotes-1.5.3.1.txt[1.5.3.1],
+  link:RelNotes-1.5.3.txt[1.5.3].
+
+* release notes for
+  link:RelNotes-1.5.2.5.txt[1.5.2.5],
+  link:RelNotes-1.5.2.4.txt[1.5.2.4],
+  link:RelNotes-1.5.2.3.txt[1.5.2.3],
+  link:RelNotes-1.5.2.2.txt[1.5.2.2],
+  link:RelNotes-1.5.2.1.txt[1.5.2.1],
+  link:RelNotes-1.5.2.txt[1.5.2].
+
+* link:v1.5.1.6/git.html[documentation for release 1.5.1.6]
+
+* release notes for
+  link:RelNotes-1.5.1.6.txt[1.5.1.6],
+  link:RelNotes-1.5.1.5.txt[1.5.1.5],
+  link:RelNotes-1.5.1.4.txt[1.5.1.4],
+  link:RelNotes-1.5.1.3.txt[1.5.1.3],
+  link:RelNotes-1.5.1.2.txt[1.5.1.2],
+  link:RelNotes-1.5.1.1.txt[1.5.1.1],
+  link:RelNotes-1.5.1.txt[1.5.1].
+
+* link:v1.5.0.7/git.html[documentation for release 1.5.0.7]
+
+* release notes for
+  link:RelNotes-1.5.0.7.txt[1.5.0.7],
+  link:RelNotes-1.5.0.6.txt[1.5.0.6],
+  link:RelNotes-1.5.0.5.txt[1.5.0.5],
+  link:RelNotes-1.5.0.3.txt[1.5.0.3],
+  link:RelNotes-1.5.0.2.txt[1.5.0.2],
+  link:RelNotes-1.5.0.1.txt[1.5.0.1],
+  link:RelNotes-1.5.0.txt[1.5.0].
+
+* documentation for release link:v1.4.4.4/git.html[1.4.4.4],
+  link:v1.3.3/git.html[1.3.3],
+  link:v1.2.6/git.html[1.2.6],
+  link:v1.0.13/git.html[1.0.13].
+
+============
+
+endif::stalenotes[]
+
+OPTIONS
+-------
+--version::
+       Prints the git suite version that the 'git' program came from.
+
+--help::
+       Prints the synopsis and a list of the most commonly used
+       commands. If the option '--all' or '-a' is given then all
+       available commands are printed. If a git command is named this
+       option will bring up the manual page for that command.
+
+--exec-path::
+       Path to wherever your core git programs are installed.
+       This can also be controlled by setting the GIT_EXEC_PATH
+       environment variable. If no path is given 'git' will print
+       the current setting and then exit.
+
+-p|--paginate::
+       Pipe all output into 'less' (or if set, $PAGER).
+
+--no-pager::
+       Do not pipe git output into a pager.
+
+--git-dir=<path>::
+       Set the path to the repository. This can also be controlled by
+       setting the GIT_DIR environment variable.
+
+--work-tree=<path>::
+       Set the path to the working tree.  The value will not be
+       used in combination with repositories found automatically in
+       a .git directory (i.e. $GIT_DIR is not set).
+       This can also be controlled by setting the GIT_WORK_TREE
+       environment variable and the core.worktree configuration
+       variable.
+
+--bare::
+       Treat the repository as a bare repository.  If GIT_DIR
+       environment is not set, it is set to the current working
+       directory.
+
+
+FURTHER DOCUMENTATION
+---------------------
+
+See the references above to get started using git.  The following is
+probably more detail than necessary for a first-time user.
+
+The link:user-manual.html#git-concepts[git concepts chapter of the
+user-manual] and the link:core-tutorial.html[Core tutorial] both provide
+introductions to the underlying git architecture.
+
+See also the link:howto-index.html[howto] documents for some useful
+examples.
+
+GIT COMMANDS
+------------
+
+We divide git into high level ("porcelain") commands and low level
+("plumbing") commands.
+
+High-level commands (porcelain)
+-------------------------------
+
+We separate the porcelain commands into the main commands and some
+ancillary user utilities.
+
+Main porcelain commands
+~~~~~~~~~~~~~~~~~~~~~~~
+
+include::cmds-mainporcelain.txt[]
+
+Ancillary Commands
+~~~~~~~~~~~~~~~~~~
+Manipulators:
+
+include::cmds-ancillarymanipulators.txt[]
+
+Interrogators:
+
+include::cmds-ancillaryinterrogators.txt[]
+
+
+Interacting with Others
+~~~~~~~~~~~~~~~~~~~~~~~
+
+These commands are to interact with foreign SCM and with other
+people via patch over e-mail.
+
+include::cmds-foreignscminterface.txt[]
+
+
+Low-level commands (plumbing)
+-----------------------------
+
+Although git includes its
+own porcelain layer, its low-level commands are sufficient to support
+development of alternative porcelains.  Developers of such porcelains
+might start by reading about gitlink:git-update-index[1] and
+gitlink:git-read-tree[1].
+
+The interface (input, output, set of options and the semantics)
+to these low-level commands are meant to be a lot more stable
+than Porcelain level commands, because these commands are
+primarily for scripted use.  The interface to Porcelain commands
+on the other hand are subject to change in order to improve the
+end user experience.
+
+The following description divides
+the low-level commands into commands that manipulate objects (in
+the repository, index, and working tree), commands that interrogate and
+compare objects, and commands that move objects and references between
+repositories.
+
+
+Manipulation commands
+~~~~~~~~~~~~~~~~~~~~~
+
+include::cmds-plumbingmanipulators.txt[]
+
+
+Interrogation commands
+~~~~~~~~~~~~~~~~~~~~~~
+
+include::cmds-plumbinginterrogators.txt[]
+
+In general, the interrogate commands do not touch the files in
+the working tree.
+
+
+Synching repositories
+~~~~~~~~~~~~~~~~~~~~~
+
+include::cmds-synchingrepositories.txt[]
+
+The following are helper programs used by the above; end users
+typically do not use them directly.
+
+include::cmds-synchelpers.txt[]
+
+
+Internal helper commands
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+These are internal helper commands used by other commands; end
+users typically do not use them directly.
+
+include::cmds-purehelpers.txt[]
+
+
+Configuration Mechanism
+-----------------------
+
+Starting from 0.99.9 (actually mid 0.99.8.GIT), `.git/config` file
+is used to hold per-repository configuration options.  It is a
+simple text file modeled after `.ini` format familiar to some
+people.  Here is an example:
+
+------------
+#
+# A '#' or ';' character indicates a comment.
+#
+
+; core variables
+[core]
+       ; Don't trust file modes
+       filemode = false
+
+; user identity
+[user]
+       name = "Junio C Hamano"
+       email = "junkio@twinsun.com"
+
+------------
+
+Various commands read from the configuration file and adjust
+their operation accordingly.
+
+
+Identifier Terminology
+----------------------
+<object>::
+       Indicates the object name for any type of object.
+
+<blob>::
+       Indicates a blob object name.
+
+<tree>::
+       Indicates a tree object name.
+
+<commit>::
+       Indicates a commit object name.
+
+<tree-ish>::
+       Indicates a tree, commit or tag object name.  A
+       command that takes a <tree-ish> argument ultimately wants to
+       operate on a <tree> object but automatically dereferences
+       <commit> and <tag> objects that point at a <tree>.
+
+<commit-ish>::
+       Indicates a commit or tag object name.  A
+       command that takes a <commit-ish> argument ultimately wants to
+       operate on a <commit> object but automatically dereferences
+       <tag> objects that point at a <commit>.
+
+<type>::
+       Indicates that an object type is required.
+       Currently one of: `blob`, `tree`, `commit`, or `tag`.
+
+<file>::
+       Indicates a filename - almost always relative to the
+       root of the tree structure `GIT_INDEX_FILE` describes.
+
+Symbolic Identifiers
+--------------------
+Any git command accepting any <object> can also use the following
+symbolic notation:
+
+HEAD::
+       indicates the head of the current branch (i.e. the
+       contents of `$GIT_DIR/HEAD`).
+
+<tag>::
+       a valid tag 'name'
+       (i.e. the contents of `$GIT_DIR/refs/tags/<tag>`).
+
+<head>::
+       a valid head 'name'
+       (i.e. the contents of `$GIT_DIR/refs/heads/<head>`).
+
+For a more complete list of ways to spell object names, see
+"SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1].
+
+
+File/Directory Structure
+------------------------
+
+Please see the link:repository-layout.html[repository layout] document.
+
+Read link:hooks.html[hooks] for more details about each hook.
+
+Higher level SCMs may provide and manage additional information in the
+`$GIT_DIR`.
+
+
+Terminology
+-----------
+Please see the link:glossary.html[glossary] document.
+
+
+Environment Variables
+---------------------
+Various git commands use the following environment variables:
+
+The git Repository
+~~~~~~~~~~~~~~~~~~
+These environment variables apply to 'all' core git commands. Nb: it
+is worth noting that they may be used/overridden by SCMS sitting above
+git so take care if using Cogito etc.
+
+'GIT_INDEX_FILE'::
+       This environment allows the specification of an alternate
+       index file. If not specified, the default of `$GIT_DIR/index`
+       is used.
+
+'GIT_OBJECT_DIRECTORY'::
+       If the object storage directory is specified via this
+       environment variable then the sha1 directories are created
+       underneath - otherwise the default `$GIT_DIR/objects`
+       directory is used.
+
+'GIT_ALTERNATE_OBJECT_DIRECTORIES'::
+       Due to the immutable nature of git objects, old objects can be
+       archived into shared, read-only directories. This variable
+       specifies a ":" separated list of git object directories which
+       can be used to search for git objects. New objects will not be
+       written to these directories.
+
+'GIT_DIR'::
+       If the 'GIT_DIR' environment variable is set then it
+       specifies a path to use instead of the default `.git`
+       for the base of the repository.
+
+'GIT_WORK_TREE'::
+       Set the path to the working tree.  The value will not be
+       used in combination with repositories found automatically in
+       a .git directory (i.e. $GIT_DIR is not set).
+       This can also be controlled by the '--work-tree' command line
+       option and the core.worktree configuration variable.
+
+git Commits
+~~~~~~~~~~~
+'GIT_AUTHOR_NAME'::
+'GIT_AUTHOR_EMAIL'::
+'GIT_AUTHOR_DATE'::
+'GIT_COMMITTER_NAME'::
+'GIT_COMMITTER_EMAIL'::
+'GIT_COMMITTER_DATE'::
+'EMAIL'::
+       see gitlink:git-commit-tree[1]
+
+git Diffs
+~~~~~~~~~
+'GIT_DIFF_OPTS'::
+       Only valid setting is "--unified=??" or "-u??" to set the
+       number of context lines shown when a unified diff is created.
+       This takes precedence over any "-U" or "--unified" option
+       value passed on the git diff command line.
+
+'GIT_EXTERNAL_DIFF'::
+       When the environment variable 'GIT_EXTERNAL_DIFF' is set, the
+       program named by it is called, instead of the diff invocation
+       described above.  For a path that is added, removed, or modified,
+        'GIT_EXTERNAL_DIFF' is called with 7 parameters:
+
+       path old-file old-hex old-mode new-file new-hex new-mode
++
+where:
+
+       <old|new>-file:: are files GIT_EXTERNAL_DIFF can use to read the
+                         contents of <old|new>,
+       <old|new>-hex:: are the 40-hexdigit SHA1 hashes,
+       <old|new>-mode:: are the octal representation of the file modes.
+
++
+The file parameters can point at the user's working file
+(e.g. `new-file` in "git-diff-files"), `/dev/null` (e.g. `old-file`
+when a new file is added), or a temporary file (e.g. `old-file` in the
+index).  'GIT_EXTERNAL_DIFF' should not worry about unlinking the
+temporary file --- it is removed when 'GIT_EXTERNAL_DIFF' exits.
++
+For a path that is unmerged, 'GIT_EXTERNAL_DIFF' is called with 1
+parameter, <path>.
+
+other
+~~~~~
+'GIT_MERGE_VERBOSITY'::
+       A number controlling the amount of output shown by
+       the recursive merge strategy.  Overrides merge.verbosity.
+       See gitlink:git-merge[1]
+
+'GIT_PAGER'::
+       This environment variable overrides `$PAGER`. If it is set
+       to an empty string or to the value "cat", git will not launch
+       a pager.
+
+'GIT_SSH'::
+       If this environment variable is set then gitlink:git-fetch[1]
+       and gitlink:git-push[1] will use this command instead
+       of `ssh` when they need to connect to a remote system.
+       The 'GIT_SSH' command will be given exactly two arguments:
+       the 'username@host' (or just 'host') from the URL and the
+       shell command to execute on that remote system.
++
+To pass options to the program that you want to list in GIT_SSH
+you will need to wrap the program and options into a shell script,
+then set GIT_SSH to refer to the shell script.
++
+Usually it is easier to configure any desired options through your
+personal `.ssh/config` file.  Please consult your ssh documentation
+for further details.
+
+'GIT_FLUSH'::
+       If this environment variable is set to "1", then commands such
+       as git-blame (in incremental mode), git-rev-list, git-log,
+       git-whatchanged, etc., will force a flush of the output stream
+       after each commit-oriented record have been flushed.   If this
+       variable is set to "0", the output of these commands will be done
+       using completely buffered I/O.   If this environment variable is
+       not set, git will choose buffered or record-oriented flushing
+       based on whether stdout appears to be redirected to a file or not.
+
+'GIT_TRACE'::
+       If this variable is set to "1", "2" or "true" (comparison
+       is case insensitive), git will print `trace:` messages on
+       stderr telling about alias expansion, built-in command
+       execution and external command execution.
+       If this variable is set to an integer value greater than 1
+       and lower than 10 (strictly) then git will interpret this
+       value as an open file descriptor and will try to write the
+       trace messages into this file descriptor.
+       Alternatively, if this variable is set to an absolute path
+       (starting with a '/' character), git will interpret this
+       as a file path and will try to write the trace messages
+       into it.
+
+Discussion[[Discussion]]
+------------------------
+
+More detail on the following is available from the
+link:user-manual.html#git-concepts[git concepts chapter of the
+user-manual] and the link:core-tutorial.html[Core tutorial].
+
+A git project normally consists of a working directory with a ".git"
+subdirectory at the top level.  The .git directory contains, among other
+things, a compressed object database representing the complete history
+of the project, an "index" file which links that history to the current
+contents of the working tree, and named pointers into that history such
+as tags and branch heads.
+
+The object database contains objects of three main types: blobs, which
+hold file data; trees, which point to blobs and other trees to build up
+directory heirarchies; and commits, which each reference a single tree
+and some number of parent commits.
+
+The commit, equivalent to what other systems call a "changeset" or
+"version", represents a step in the project's history, and each parent
+represents an immediately preceding step.  Commits with more than one
+parent represent merges of independent lines of development.
+
+All objects are named by the SHA1 hash of their contents, normally
+written as a string of 40 hex digits.  Such names are globally unique.
+The entire history leading up to a commit can be vouched for by signing
+just that commit.  A fourth object type, the tag, is provided for this
+purpose.
+
+When first created, objects are stored in individual files, but for
+efficiency may later be compressed together into "pack files".
+
+Named pointers called refs mark interesting points in history.  A ref
+may contain the SHA1 name of an object or the name of another ref.  Refs
+with names beginning `ref/head/` contain the SHA1 name of the most
+recent commit (or "head") of a branch under developement.  SHA1 names of
+tags of interest are stored under `ref/tags/`.  A special ref named
+`HEAD` contains the name of the currently checked-out branch.
+
+The index file is initialized with a list of all paths and, for each
+path, a blob object and a set of attributes.  The blob object represents
+the contents of the file as of the head of the current branch.  The
+attributes (last modified time, size, etc.) are taken from the
+corresponding file in the working tree.  Subsequent changes to the
+working tree can be found by comparing these attributes.  The index may
+be updated with new content, and new commits may be created from the
+content stored in the index.
+
+The index is also capable of storing multiple entries (called "stages")
+for a given pathname.  These stages are used to hold the various
+unmerged version of a file when a merge is in progress.
+
+Authors
+-------
+* git's founding father is Linus Torvalds <torvalds@osdl.org>.
+* The current git nurse is Junio C Hamano <gitster@pobox.com>.
+* The git potty was written by Andreas Ericsson <ae@op5.se>.
+* General upbringing is handled by the git-list <git@vger.kernel.org>.
+
+Documentation
+--------------
+The documentation for git suite was started by David Greaves
+<david@dgreaves.com>, and later enhanced greatly by the
+contributors on the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt
new file mode 100644 (file)
index 0000000..19bd25f
--- /dev/null
@@ -0,0 +1,432 @@
+gitattributes(5)
+================
+
+NAME
+----
+gitattributes - defining attributes per path
+
+SYNOPSIS
+--------
+$GIT_DIR/info/attributes, gitattributes
+
+
+DESCRIPTION
+-----------
+
+A `gitattributes` file is a simple text file that gives
+`attributes` to pathnames.
+
+Each line in `gitattributes` file is of form:
+
+       glob    attr1 attr2 ...
+
+That is, a glob pattern followed by an attributes list,
+separated by whitespaces.  When the glob pattern matches the
+path in question, the attributes listed on the line are given to
+the path.
+
+Each attribute can be in one of these states for a given path:
+
+Set::
+
+       The path has the attribute with special value "true";
+       this is specified by listing only the name of the
+       attribute in the attribute list.
+
+Unset::
+
+       The path has the attribute with special value "false";
+       this is specified by listing the name of the attribute
+       prefixed with a dash `-` in the attribute list.
+
+Set to a value::
+
+       The path has the attribute with specified string value;
+       this is specified by listing the name of the attribute
+       followed by an equal sign `=` and its value in the
+       attribute list.
+
+Unspecified::
+
+       No glob pattern matches the path, and nothing says if
+       the path has or does not have the attribute, the
+       attribute for the path is said to be Unspecified.
+
+When more than one glob pattern matches the path, a later line
+overrides an earlier line.  This overriding is done per
+attribute.
+
+When deciding what attributes are assigned to a path, git
+consults `$GIT_DIR/info/attributes` file (which has the highest
+precedence), `.gitattributes` file in the same directory as the
+path in question, and its parent directories (the further the
+directory that contains `.gitattributes` is from the path in
+question, the lower its precedence).
+
+Sometimes you would need to override an setting of an attribute
+for a path to `unspecified` state.  This can be done by listing
+the name of the attribute prefixed with an exclamation point `!`.
+
+
+EFFECTS
+-------
+
+Certain operations by git can be influenced by assigning
+particular attributes to a path.  Currently, the following
+operations are attributes-aware.
+
+Checking-out and checking-in
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+These attributes affect how the contents stored in the
+repository are copied to the working tree files when commands
+such as `git checkout` and `git merge` run.  They also affect how
+git stores the contents you prepare in the working tree in the
+repository upon `git add` and `git commit`.
+
+`crlf`
+^^^^^^
+
+This attribute controls the line-ending convention.
+
+Set::
+
+       Setting the `crlf` attribute on a path is meant to mark
+       the path as a "text" file.  'core.autocrlf' conversion
+       takes place without guessing the content type by
+       inspection.
+
+Unset::
+
+       Unsetting the `crlf` attribute on a path is meant to
+       mark the path as a "binary" file.  The path never goes
+       through line endings conversion upon checkin/checkout.
+
+Unspecified::
+
+       Unspecified `crlf` attribute tells git to apply the
+       `core.autocrlf` conversion when the file content looks
+       like text.
+
+Set to string value "input"::
+
+       This is similar to setting the attribute to `true`, but
+       also forces git to act as if `core.autocrlf` is set to
+       `input` for the path.
+
+Any other value set to `crlf` attribute is ignored and git acts
+as if the attribute is left unspecified.
+
+
+The `core.autocrlf` conversion
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If the configuration variable `core.autocrlf` is false, no
+conversion is done.
+
+When `core.autocrlf` is true, it means that the platform wants
+CRLF line endings for files in the working tree, and you want to
+convert them back to the normal LF line endings when checking
+in to the repository.
+
+When `core.autocrlf` is set to "input", line endings are
+converted to LF upon checkin, but there is no conversion done
+upon checkout.
+
+
+`ident`
+^^^^^^^
+
+When the attribute `ident` is set to a path, git replaces
+`$Id$` in the blob object with `$Id:`, followed by
+40-character hexadecimal blob object name, followed by a dollar
+sign `$` upon checkout.  Any byte sequence that begins with
+`$Id:` and ends with `$` in the worktree file is replaced
+with `$Id$` upon check-in.
+
+
+`filter`
+^^^^^^^^
+
+A `filter` attribute can be set to a string value that names a
+filter driver specified in the configuration.
+
+A filter driver consists of a `clean` command and a `smudge`
+command, either of which can be left unspecified.  Upon
+checkout, when the `smudge` command is specified, the command is
+fed the blob object from its standard input, and its standard
+output is used to update the worktree file.  Similarly, the
+`clean` command is used to convert the contents of worktree file
+upon checkin.
+
+A missing filter driver definition in the config is not an error
+but makes the filter a no-op passthru.
+
+The content filtering is done to massage the content into a
+shape that is more convenient for the platform, filesystem, and
+the user to use.  The key phrase here is "more convenient" and not
+"turning something unusable into usable".  In other words, the
+intent is that if someone unsets the filter driver definition,
+or does not have the appropriate filter program, the project
+should still be usable.
+
+
+Interaction between checkin/checkout attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In the check-in codepath, the worktree file is first converted
+with `filter` driver (if specified and corresponding driver
+defined), then the result is processed with `ident` (if
+specified), and then finally with `crlf` (again, if specified
+and applicable).
+
+In the check-out codepath, the blob content is first converted
+with `crlf`, and then `ident` and fed to `filter`.
+
+
+Generating diff text
+~~~~~~~~~~~~~~~~~~~~
+
+The attribute `diff` affects if `git diff` generates textual
+patch for the path or just says `Binary files differ`.  It also
+can affect what line is shown on the hunk header `@@ -k,l +n,m @@`
+line.
+
+Set::
+
+       A path to which the `diff` attribute is set is treated
+       as text, even when they contain byte values that
+       normally never appear in text files, such as NUL.
+
+Unset::
+
+       A path to which the `diff` attribute is unset will
+       generate `Binary files differ`.
+
+Unspecified::
+
+       A path to which the `diff` attribute is unspecified
+       first gets its contents inspected, and if it looks like
+       text, it is treated as text.  Otherwise it would
+       generate `Binary files differ`.
+
+String::
+
+       Diff is shown using the specified custom diff driver.
+       The driver program is given its input using the same
+       calling convention as used for GIT_EXTERNAL_DIFF
+       program.  This name is also used for custom hunk header
+       selection.
+
+
+Defining a custom diff driver
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The definition of a diff driver is done in `gitconfig`, not
+`gitattributes` file, so strictly speaking this manual page is a
+wrong place to talk about it.  However...
+
+To define a custom diff driver `jcdiff`, add a section to your
+`$GIT_DIR/config` file (or `$HOME/.gitconfig` file) like this:
+
+----------------------------------------------------------------
+[diff "jcdiff"]
+       command = j-c-diff
+----------------------------------------------------------------
+
+When git needs to show you a diff for the path with `diff`
+attribute set to `jcdiff`, it calls the command you specified
+with the above configuration, i.e. `j-c-diff`, with 7
+parameters, just like `GIT_EXTERNAL_DIFF` program is called.
+See gitlink:git[7] for details.
+
+
+Defining a custom hunk-header
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Each group of changes (called "hunk") in the textual diff output
+is prefixed with a line of the form:
+
+       @@ -k,l +n,m @@ TEXT
+
+The text is called 'hunk header', and by default a line that
+begins with an alphabet, an underscore or a dollar sign is used,
+which matches what GNU `diff -p` output uses.  This default
+selection however is not suited for some contents, and you can
+use customized pattern to make a selection.
+
+First in .gitattributes, you would assign the `diff` attribute
+for paths.
+
+------------------------
+*.tex  diff=tex
+------------------------
+
+Then, you would define "diff.tex.funcname" configuration to
+specify a regular expression that matches a line that you would
+want to appear as the hunk header, like this:
+
+------------------------
+[diff "tex"]
+       funcname = "^\\(\\\\\\(sub\\)*section{.*\\)$"
+------------------------
+
+Note.  A single level of backslashes are eaten by the
+configuration file parser, so you would need to double the
+backslashes; the pattern above picks a line that begins with a
+backslash, and zero or more occurrences of `sub` followed by
+`section` followed by open brace, to the end of line.
+
+There are a few built-in patterns to make this easier, and `tex`
+is one of them, so you do not have to write the above in your
+configuration file (you still need to enable this with the
+attribute mechanism, via `.gitattributes`).  Another built-in
+pattern is defined for `java` that defines a pattern suitable
+for program text in Java language.
+
+
+Performing a three-way merge
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The attribute `merge` affects how three versions of a file is
+merged when a file-level merge is necessary during `git merge`,
+and other programs such as `git revert` and `git cherry-pick`.
+
+Set::
+
+       Built-in 3-way merge driver is used to merge the
+       contents in a way similar to `merge` command of `RCS`
+       suite.  This is suitable for ordinary text files.
+
+Unset::
+
+       Take the version from the current branch as the
+       tentative merge result, and declare that the merge has
+       conflicts.  This is suitable for binary files that does
+       not have a well-defined merge semantics.
+
+Unspecified::
+
+       By default, this uses the same built-in 3-way merge
+       driver as is the case the `merge` attribute is set.
+       However, `merge.default` configuration variable can name
+       different merge driver to be used for paths to which the
+       `merge` attribute is unspecified.
+
+String::
+
+       3-way merge is performed using the specified custom
+       merge driver.  The built-in 3-way merge driver can be
+       explicitly specified by asking for "text" driver; the
+       built-in "take the current branch" driver can be
+       requested with "binary".
+
+
+Defining a custom merge driver
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The definition of a merge driver is done in `gitconfig` not
+`gitattributes` file, so strictly speaking this manual page is a
+wrong place to talk about it.  However...
+
+To define a custom merge driver `filfre`, add a section to your
+`$GIT_DIR/config` file (or `$HOME/.gitconfig` file) like this:
+
+----------------------------------------------------------------
+[merge "filfre"]
+       name = feel-free merge driver
+       driver = filfre %O %A %B
+       recursive = binary
+----------------------------------------------------------------
+
+The `merge.*.name` variable gives the driver a human-readable
+name.
+
+The `merge.*.driver` variable's value is used to construct a
+command to run to merge ancestor's version (`%O`), current
+version (`%A`) and the other branches' version (`%B`).  These
+three tokens are replaced with the names of temporary files that
+hold the contents of these versions when the command line is
+built.
+
+The merge driver is expected to leave the result of the merge in
+the file named with `%A` by overwriting it, and exit with zero
+status if it managed to merge them cleanly, or non-zero if there
+were conflicts.
+
+The `merge.*.recursive` variable specifies what other merge
+driver to use when the merge driver is called for an internal
+merge between common ancestors, when there are more than one.
+When left unspecified, the driver itself is used for both
+internal merge and the final merge.
+
+
+EXAMPLE
+-------
+
+If you have these three `gitattributes` file:
+
+----------------------------------------------------------------
+(in $GIT_DIR/info/attributes)
+
+a*     foo !bar -baz
+
+(in .gitattributes)
+abc    foo bar baz
+
+(in t/.gitattributes)
+ab*    merge=filfre
+abc    -foo -bar
+*.c    frotz
+----------------------------------------------------------------
+
+the attributes given to path `t/abc` are computed as follows:
+
+1. By examining `t/.gitattributes` (which is in the same
+   directory as the path in question), git finds that the first
+   line matches.  `merge` attribute is set.  It also finds that
+   the second line matches, and attributes `foo` and `bar`
+   are unset.
+
+2. Then it examines `.gitattributes` (which is in the parent
+   directory), and finds that the first line matches, but
+   `t/.gitattributes` file already decided how `merge`, `foo`
+   and `bar` attributes should be given to this path, so it
+   leaves `foo` and `bar` unset.  Attribute `baz` is set.
+
+3. Finally it examines `$GIT_DIR/info/attributes`.  This file
+   is used to override the in-tree settings.  The first line is
+   a match, and `foo` is set, `bar` is reverted to unspecified
+   state, and `baz` is unset.
+
+As the result, the attributes assignment to `t/abc` becomes:
+
+----------------------------------------------------------------
+foo    set to true
+bar    unspecified
+baz    set to false
+merge  set to string value "filfre"
+frotz  unspecified
+----------------------------------------------------------------
+
+
+Creating an archive
+~~~~~~~~~~~~~~~~~~~
+
+`export-subst`
+^^^^^^^^^^^^^^
+
+If the attribute `export-subst` is set for a file then git will expand
+several placeholders when adding this file to an archive.  The
+expansion depends on the availability of a commit ID, i.e. if
+gitlink:git-archive[1] has been given a tree instead of a commit or a
+tag then no replacement will be done.  The placeholders are the same
+as those for the option `--pretty=format:` of gitlink:git-log[1],
+except that they need to be wrapped like this: `$Format:PLACEHOLDERS$`
+in the file.  E.g. the string `$Format:%H$` will be replaced by the
+commit hash.
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
new file mode 100644 (file)
index 0000000..e8b8581
--- /dev/null
@@ -0,0 +1,122 @@
+gitignore(5)
+============
+
+NAME
+----
+gitignore - Specifies intentionally untracked files to ignore
+
+SYNOPSIS
+--------
+$GIT_DIR/info/exclude, .gitignore
+
+DESCRIPTION
+-----------
+
+A `gitignore` file specifies intentionally untracked files that
+git should ignore.  Each line in a `gitignore` file specifies a
+pattern.
+
+When deciding whether to ignore a path, git normally checks
+`gitignore` patterns from multiple sources, with the following
+order of precedence, from highest to lowest (within one level of
+precedence, the last matching pattern decides the outcome):
+
+ * Patterns read from the command line for those commands that support
+   them.
+
+ * Patterns read from a `.gitignore` file in the same directory
+   as the path, or in any parent directory, with patterns in the
+   higher level files (up to the root) being overridden by those in
+   lower level files down to the directory containing the file.
+   These patterns match relative to the location of the
+   `.gitignore` file.  A project normally includes such
+   `.gitignore` files in its repository, containing patterns for
+   files generated as part of the project build.
+
+ * Patterns read from `$GIT_DIR/info/exclude`.
+
+ * Patterns read from the file specified by the configuration
+   variable 'core.excludesfile'.
+
+The underlying git plumbing tools, such as
+gitlink:git-ls-files[1] and gitlink:git-read-tree[1], read
+`gitignore` patterns specified by command-line options, or from
+files specified by command-line options.  Higher-level git
+tools, such as gitlink:git-status[1] and gitlink:git-add[1],
+use patterns from the sources specified above.
+
+Patterns have the following format:
+
+ - A blank line matches no files, so it can serve as a separator
+   for readability.
+
+ - A line starting with # serves as a comment.
+
+ - An optional prefix '!' which negates the pattern; any
+   matching file excluded by a previous pattern will become
+   included again.  If a negated pattern matches, this will
+   override lower precedence patterns sources.
+
+ - If the pattern does not contain a slash '/', git treats it as
+   a shell glob pattern and checks for a match against the
+   pathname without leading directories.
+
+ - Otherwise, git treats the pattern as a shell glob suitable
+   for consumption by fnmatch(3) with the FNM_PATHNAME flag:
+   wildcards in the pattern will not match a / in the pathname.
+   For example, "Documentation/\*.html" matches
+   "Documentation/git.html" but not
+   "Documentation/ppc/ppc.html".  A leading slash matches the
+   beginning of the pathname; for example, "/*.c" matches
+   "cat-file.c" but not "mozilla-sha1/sha1.c".
+
+An example:
+
+--------------------------------------------------------------
+    $ git-status
+    [...]
+    # Untracked files:
+    [...]
+    #       Documentation/foo.html
+    #       Documentation/gitignore.html
+    #       file.o
+    #       lib.a
+    #       src/internal.o
+    [...]
+    $ cat .git/info/exclude
+    # ignore objects and archives, anywhere in the tree.
+    *.[oa]
+    $ cat Documentation/.gitignore
+    # ignore generated html files,
+    *.html
+    # except foo.html which is maintained by hand
+    !foo.html
+    $ git-status
+    [...]
+    # Untracked files:
+    [...]
+    #       Documentation/foo.html
+    [...]
+--------------------------------------------------------------
+
+Another example:
+
+--------------------------------------------------------------
+    $ cat .gitignore
+    vmlinux*
+    $ ls arch/foo/kernel/vm*
+    arch/foo/kernel/vmlinux.lds.S
+    $ echo '!/vmlinux*' >arch/foo/kernel/.gitignore
+--------------------------------------------------------------
+
+The second .gitignore prevents git from ignoring
+`arch/foo/kernel/vmlinux.lds.S`.
+
+Documentation
+-------------
+Documentation by David Greaves, Junio C Hamano, Josh Triplett,
+Frank Lichtenheld, and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/gitk.txt b/Documentation/gitk.txt
new file mode 100644 (file)
index 0000000..8dbfb0d
--- /dev/null
@@ -0,0 +1,101 @@
+gitk(1)
+=======
+
+NAME
+----
+gitk - The git repository browser
+
+SYNOPSIS
+--------
+'gitk' [<option>...] [<revs>] [--] [<path>...]
+
+DESCRIPTION
+-----------
+Displays changes in a repository or a selected set of commits. This includes
+visualizing the commit graph, showing information related to each commit, and
+the files in the trees of each revision.
+
+Historically, gitk was the first repository browser. It's written in tcl/tk
+and started off in a separate repository but was later merged into the main
+git repository.
+
+OPTIONS
+-------
+To control which revisions to shown, the command takes options applicable to
+the gitlink:git-rev-list[1] command. This manual page describes only the most
+frequently used options.
+
+-n <number>, --max-count=<number>::
+
+       Limits the number of commits to show.
+
+--since=<date>::
+
+       Show commits more recent than a specific date.
+
+--until=<date>::
+
+       Show commits older than a specific date.
+
+--all::
+
+       Show all branches.
+
+<revs>::
+
+       Limit the revisions to show. This can be either a single revision
+       meaning show from the given revision and back, or it can be a range in
+       the form "'<from>'..'<to>'" to show all revisions between '<from>' and
+       back to '<to>'. Note, more advanced revision selection can be applied.
+       For a more complete list of ways to spell object names, see
+       "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1].
+
+<path>::
+
+       Limit commits to the ones touching files in the given paths. Note, to
+       avoid ambiguity wrt. revision names use "--" to separate the paths
+       from any preceding options.
+
+Examples
+--------
+gitk 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
+
+gitk --since="2 weeks ago" \-- gitk::
+
+       Show the changes during the last two weeks to the file 'gitk'.
+       The "--" is necessary to avoid confusion with the *branch* named
+       'gitk'
+
+gitk --max-count=100 --all \-- Makefile::
+
+       Show at most 100 changes made to the file 'Makefile'. Instead of only
+       looking for changes in the current branch look in all branches.
+
+See Also
+--------
+'qgit(1)'::
+       A repository browser written in C++ using Qt.
+
+'gitview(1)'::
+       A repository browser written in Python using Gtk. It's based on
+       'bzrk(1)' and distributed in the contrib area of the git repository.
+
+'tig(1)'::
+       A minimal repository browser and git tool output highlighter written
+       in C using Ncurses.
+
+Author
+------
+Written by Paul Mackerras <paulus@samba.org>.
+
+Documentation
+--------------
+Documentation by Junio C Hamano, Jonas Fonseca, and the git-list
+<git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/gitmodules.txt b/Documentation/gitmodules.txt
new file mode 100644 (file)
index 0000000..035294e
--- /dev/null
@@ -0,0 +1,62 @@
+gitmodules(5)
+=============
+
+NAME
+----
+gitmodules - defining submodule properties
+
+SYNOPSIS
+--------
+gitmodules
+
+
+DESCRIPTION
+-----------
+
+The `.gitmodules` file, located in the top-level directory of a git
+working tree, is a text file with a syntax matching the requirements
+of gitlink:git-config[1].
+
+The file contains one subsection per submodule, and the subsection value
+is the name of the submodule. Each submodule section also contains the
+following required keys:
+
+submodule.<name>.path::
+       Defines the path, relative to the top-level directory of the git
+       working tree, where the submodule is expected to be checked out.
+       The path name must not end with a `/`. All submodule paths must
+       be unique within the .gitmodules file.
+
+submodule.<name>.url::
+       Defines an url from where the submodule repository can be cloned.
+
+
+EXAMPLES
+--------
+
+Consider the following .gitmodules file:
+
+       [submodule "libfoo"]
+               path = include/foo
+               url = git://foo.com/git/lib.git
+
+       [submodule "libbar"]
+               path = include/bar
+               url = git://bar.com/git/lib.git
+
+
+This defines two submodules, `libfoo` and `libbar`. These are expected to
+be checked out in the paths 'include/foo' and 'include/bar', and for both
+submodules an url is specified which can be used for cloning the submodules.
+
+SEE ALSO
+--------
+gitlink:git-submodule[1] gitlink:git-config[1]
+
+DOCUMENTATION
+-------------
+Documentation by Lars Hjemli <hjemli@gmail.com>
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/glossary.txt b/Documentation/glossary.txt
new file mode 100644 (file)
index 0000000..fc18744
--- /dev/null
@@ -0,0 +1,455 @@
+GIT Glossary
+============
+
+[[def_alternate_object_database]]alternate object database::
+       Via the alternates mechanism, a <<def_repository,repository>>
+       can inherit part of its <<def_object_database,object database>>
+       from another object database, which is called "alternate".
+
+[[def_bare_repository]]bare repository::
+       A bare repository is normally an appropriately
+       named <<def_directory,directory>> with a `.git` suffix that does not
+       have a locally checked-out copy of any of the files under
+       revision control. That is, all of the `git`
+       administrative and control files that would normally be present in the
+       hidden `.git` sub-directory are directly present in the
+       `repository.git` directory instead,
+       and no other files are present and checked out. Usually publishers of
+       public repositories make bare repositories available.
+
+[[def_blob_object]]blob object::
+       Untyped <<def_object,object>>, e.g. the contents of a file.
+
+[[def_branch]]branch::
+       A "branch" is an active line of development.  The most recent
+       <<def_commit,commit>> on a branch is referred to as the tip of
+       that branch.  The tip of the branch is referenced by a branch
+       <<def_head,head>>, which moves forward as additional development
+       is done on the branch.  A single git
+       <<def_repository,repository>> can track an arbitrary number of
+       branches, but your <<def_working_tree,working tree>> is
+       associated with just one of them (the "current" or "checked out"
+       branch), and <<def_HEAD,HEAD>> points to that branch.
+
+[[def_cache]]cache::
+       Obsolete for: <<def_index,index>>.
+
+[[def_chain]]chain::
+       A list of objects, where each <<def_object,object>> in the list contains
+       a reference to its successor (for example, the successor of a
+       <<def_commit,commit>> could be one of its <<def_parent,parents>>).
+
+[[def_changeset]]changeset::
+       BitKeeper/cvsps speak for "<<def_commit,commit>>". Since git does not
+       store changes, but states, it really does not make sense to use the term
+       "changesets" with git.
+
+[[def_checkout]]checkout::
+       The action of updating the <<def_working_tree,working tree>> to a
+       <<def_revision,revision>> which was stored in the
+       <<def_object_database,object database>>.
+
+[[def_cherry-picking]]cherry-picking::
+       In <<def_SCM,SCM>> jargon, "cherry pick" means to choose a subset of
+       changes out of a series of changes (typically commits) and record them
+       as a new series of changes on top of a different codebase. In GIT, this is
+       performed by the "git cherry-pick" command to extract the change introduced
+       by an existing <<def_commit,commit>> and to record it based on the tip
+       of the current <<def_branch,branch>> as a new commit.
+
+[[def_clean]]clean::
+       A <<def_working_tree,working tree>> is clean, if it
+       corresponds to the <<def_revision,revision>> referenced by the current
+       <<def_head,head>>. Also see "<<def_dirty,dirty>>".
+
+[[def_commit]]commit::
+       As a noun: A single point in the
+       git history; the entire history of a project is represented as a
+       set of interrelated commits.  The word "commit" is often
+       used by git in the same places other revision control systems
+       use the words "revision" or "version".  Also used as a short
+       hand for <<def_commit_object,commit object>>.
++
+As a verb: The action of storing a new snapshot of the project's
+state in the git history, by creating a new commit representing the current
+state of the <<def_index,index>> and advancing <<def_HEAD,HEAD>>
+to point at the new commit.
+
+[[def_commit_object]]commit object::
+       An <<def_object,object>> which contains the information about a
+       particular <<def_revision,revision>>, such as <<def_parent,parents>>, committer,
+       author, date and the <<def_tree_object,tree object>> which corresponds
+       to the top <<def_directory,directory>> of the stored
+       revision.
+
+[[def_core_git]]core git::
+       Fundamental data structures and utilities of git. Exposes only limited
+       source code management tools.
+
+[[def_DAG]]DAG::
+       Directed acyclic graph. The <<def_commit,commit>> objects form a
+       directed acyclic graph, because they have parents (directed), and the
+       graph of commit objects is acyclic (there is no
+       <<def_chain,chain>> which begins and ends with the same
+       <<def_object,object>>).
+
+[[def_dangling_object]]dangling object::
+       An <<def_unreachable_object,unreachable object>> which is not
+       <<def_reachable,reachable>> even from other unreachable objects; a
+       dangling object has no references to it from any
+       reference or <<def_object,object>> in the <<def_repository,repository>>.
+
+[[def_detached_HEAD]]detached HEAD::
+       Normally the <<def_HEAD,HEAD>> stores the name of a
+       <<def_branch,branch>>.  However, git also allows you to <<def_checkout,check out>>
+       an arbitrary <<def_commit,commit>> that isn't necessarily the tip of any
+       particular branch.  In this case HEAD is said to be "detached".
+
+[[def_dircache]]dircache::
+       You are *waaaaay* behind. See <<def_index,index>>.
+
+[[def_directory]]directory::
+       The list you get with "ls" :-)
+
+[[def_dirty]]dirty::
+       A <<def_working_tree,working tree>> is said to be "dirty" if
+       it contains modifications which have not been <<def_commit,committed>> to the current
+       <<def_branch,branch>>.
+
+[[def_ent]]ent::
+       Favorite synonym to "<<def_tree-ish,tree-ish>>" by some total geeks. See
+       `http://en.wikipedia.org/wiki/Ent_(Middle-earth)` for an in-depth
+       explanation. Avoid this term, not to confuse people.
+
+[[def_evil_merge]]evil merge::
+       An evil merge is a <<def_merge,merge>> that introduces changes that
+       do not appear in any <<def_parent,parent>>.
+
+[[def_fast_forward]]fast forward::
+       A fast-forward is a special type of <<def_merge,merge>> where you have a
+       <<def_revision,revision>> and you are "merging" another
+       <<def_branch,branch>>'s changes that happen to be a descendant of what
+       you have. In such these cases, you do not make a new <<def_merge,merge>>
+       <<def_commit,commit>> but instead just update to his
+       revision. This will happen frequently on a
+       <<def_tracking_branch,tracking branch>> of a remote
+       <<def_repository,repository>>.
+
+[[def_fetch]]fetch::
+       Fetching a <<def_branch,branch>> means to get the
+       branch's <<def_head_ref,head ref>> from a remote
+       <<def_repository,repository>>, to find out which objects are
+       missing from the local <<def_object_database,object database>>,
+       and to get them, too.  See also gitlink:git-fetch[1].
+
+[[def_file_system]]file system::
+       Linus Torvalds originally designed git to be a user space file system,
+       i.e. the infrastructure to hold files and directories. That ensured the
+       efficiency and speed of git.
+
+[[def_git_archive]]git archive::
+       Synonym for <<def_repository,repository>> (for arch people).
+
+[[def_grafts]]grafts::
+       Grafts enables two otherwise different lines of development to be joined
+       together by recording fake ancestry information for commits. This way
+       you can make git pretend the set of <<def_parent,parents>> a <<def_commit,commit>> has
+       is different from what was recorded when the commit was
+       created. Configured via the `.git/info/grafts` file.
+
+[[def_hash]]hash::
+       In git's context, synonym to <<def_object_name,object name>>.
+
+[[def_head]]head::
+       A <<def_ref,named reference>> to the <<def_commit,commit>> at the tip of a
+       <<def_branch,branch>>.  Heads are stored in
+       `$GIT_DIR/refs/heads/`, except when using packed refs. (See
+       gitlink:git-pack-refs[1].)
+
+[[def_HEAD]]HEAD::
+       The current <<def_branch,branch>>.  In more detail: Your <<def_working_tree,
+       working tree>> is normally derived from the state of the tree
+       referred to by HEAD.  HEAD is a reference to one of the
+       <<def_head,heads>> in your repository, except when using a
+       <<def_detached_HEAD,detached HEAD>>, in which case it may
+       reference an arbitrary commit.
+
+[[def_head_ref]]head ref::
+       A synonym for <<def_head,head>>.
+
+[[def_hook]]hook::
+       During the normal execution of several git commands, call-outs are made
+       to optional scripts that allow a developer to add functionality or
+       checking. Typically, the hooks allow for a command to be pre-verified
+       and potentially aborted, and allow for a post-notification after the
+       operation is done. The hook scripts are found in the
+       `$GIT_DIR/hooks/` directory, and are enabled by simply
+       making them executable.
+
+[[def_index]]index::
+       A collection of files with stat information, whose contents are stored
+       as objects. The index is a stored version of your
+       <<def_working_tree,working tree>>. Truth be told, it can also contain a second, and even
+       a third version of a working tree, which are used
+       when <<def_merge,merging>>.
+
+[[def_index_entry]]index entry::
+       The information regarding a particular file, stored in the
+       <<def_index,index>>. An index entry can be unmerged, if a
+       <<def_merge,merge>> was started, but not yet finished (i.e. if
+       the index contains multiple versions of that file).
+
+[[def_master]]master::
+       The default development <<def_branch,branch>>. Whenever you
+       create a git <<def_repository,repository>>, a branch named
+       "master" is created, and becomes the active branch. In most
+       cases, this contains the local development, though that is
+       purely by convention and is not required.
+
+[[def_merge]]merge::
+       As a verb: To bring the contents of another
+       <<def_branch,branch>> (possibly from an external
+       <<def_repository,repository>>) into the current branch.  In the
+       case where the merged-in branch is from a different repository,
+       this is done by first <<def_fetch,fetching>> the remote branch
+       and then merging the result into the current branch.  This
+       combination of fetch and merge operations is called a
+       <<def_pull,pull>>.  Merging is performed by an automatic process
+       that identifies changes made since the branches diverged, and
+       then applies all those changes together.  In cases where changes
+       conflict, manual intervention may be required to complete the
+       merge.
++
+As a noun: unless it is a <<def_fast_forward,fast forward>>, a
+successful merge results in the creation of a new <<def_commit,commit>>
+representing the result of the merge, and having as
+<<def_parent,parents>> the tips of the merged <<def_branch,branches>>.
+This commit is referred to as a "merge commit", or sometimes just a
+"merge".
+
+[[def_object]]object::
+       The unit of storage in git. It is uniquely identified by the
+       <<def_SHA1,SHA1>> of its contents. Consequently, an
+       object can not be changed.
+
+[[def_object_database]]object database::
+       Stores a set of "objects", and an individual <<def_object,object>> is
+       identified by its <<def_object_name,object name>>. The objects usually
+       live in `$GIT_DIR/objects/`.
+
+[[def_object_identifier]]object identifier::
+       Synonym for <<def_object_name,object name>>.
+
+[[def_object_name]]object name::
+       The unique identifier of an <<def_object,object>>. The <<def_hash,hash>>
+       of the object's contents using the Secure Hash Algorithm
+       1 and usually represented by the 40 character hexadecimal encoding of
+       the <<def_hash,hash>> of the object (possibly followed by
+       a white space).
+
+[[def_object_type]]object type::
+       One of the identifiers
+       "<<def_commit,commit>>","<<def_tree,tree>>","<<def_tag,tag>>" or "<<def_blob_object,blob>>"
+       describing the type of an <<def_object,object>>.
+
+[[def_octopus]]octopus::
+       To <<def_merge,merge>> more than two <<def_branch,branches>>. Also denotes an
+       intelligent predator.
+
+[[def_origin]]origin::
+       The default upstream <<def_repository,repository>>. Most projects have
+       at least one upstream project which they track. By default
+       'origin' is used for that purpose. New upstream updates
+       will be fetched into remote <<def_tracking_branch,tracking branches>> named
+       origin/name-of-upstream-branch, which you can see using
+       "`git branch -r`".
+
+[[def_pack]]pack::
+       A set of objects which have been compressed into one file (to save space
+       or to transmit them efficiently).
+
+[[def_pack_index]]pack index::
+       The list of identifiers, and other information, of the objects in a
+       <<def_pack,pack>>, to assist in efficiently accessing the contents of a
+       pack.
+
+[[def_parent]]parent::
+       A <<def_commit_object,commit object>> contains a (possibly empty) list
+       of the logical predecessor(s) in the line of development, i.e. its
+       parents.
+
+[[def_pickaxe]]pickaxe::
+       The term <<def_pickaxe,pickaxe>> refers to an option to the diffcore
+       routines that help select changes that add or delete a given text
+       string. With the `--pickaxe-all` option, it can be used to view the full
+       <<def_changeset,changeset>> that introduced or removed, say, a
+       particular line of text. See gitlink:git-diff[1].
+
+[[def_plumbing]]plumbing::
+       Cute name for <<def_core_git,core git>>.
+
+[[def_porcelain]]porcelain::
+       Cute name for programs and program suites depending on
+       <<def_core_git,core git>>, presenting a high level access to
+       core git. Porcelains expose more of a <<def_SCM,SCM>>
+       interface than the <<def_plumbing,plumbing>>.
+
+[[def_pull]]pull::
+       Pulling a <<def_branch,branch>> means to <<def_fetch,fetch>> it and
+       <<def_merge,merge>> it.  See also gitlink:git-pull[1].
+
+[[def_push]]push::
+       Pushing a <<def_branch,branch>> means to get the branch's
+       <<def_head_ref,head ref>> from a remote <<def_repository,repository>>,
+       find out if it is a direct ancestor to the branch's local
+       head ref, and in that case, putting all
+       objects, which are <<def_reachable,reachable>> from the local
+       head ref, and which are missing from the remote
+       repository, into the remote
+       <<def_object_database,object database>>, and updating the remote
+       head ref. If the remote <<def_head,head>> is not an
+       ancestor to the local head, the push fails.
+
+[[def_reachable]]reachable::
+       All of the ancestors of a given <<def_commit,commit>> are said to be
+       "reachable" from that commit. More
+       generally, one <<def_object,object>> is reachable from
+       another if we can reach the one from the other by a <<def_chain,chain>>
+       that follows <<def_tag,tags>> to whatever they tag,
+       <<def_commit_object,commits>> to their parents or trees, and
+       <<def_tree_object,trees>> to the trees or <<def_blob_object,blobs>>
+       that they contain.
+
+[[def_rebase]]rebase::
+       To reapply a series of changes from a <<def_branch,branch>> to a
+       different base, and reset the <<def_head,head>> of that branch
+       to the result.
+
+[[def_ref]]ref::
+       A 40-byte hex representation of a <<def_SHA1,SHA1>> or a name that
+       denotes a particular <<def_object,object>>. These may be stored in
+       `$GIT_DIR/refs/`.
+
+[[def_reflog]]reflog::
+       A reflog shows the local "history" of a ref.  In other words,
+       it can tell you what the 3rd last revision in _this_ repository
+       was, and what was the current state in _this_ repository,
+       yesterday 9:14pm.  See gitlink:git-reflog[1] for details.
+
+[[def_refspec]]refspec::
+       A "refspec" is used by <<def_fetch,fetch>> and
+       <<def_push,push>> to describe the mapping between remote
+       <<def_ref,ref>> and local ref. They are combined with a colon in
+       the format <src>:<dst>, preceded by an optional plus sign, +.
+       For example: `git fetch $URL
+       refs/heads/master:refs/heads/origin` means "grab the master
+       <<def_branch,branch>> <<def_head,head>> from the $URL and store
+       it as my origin branch head". And `git push
+       $URL refs/heads/master:refs/heads/to-upstream` means "publish my
+       master branch head as to-upstream branch at $URL". See also
+       gitlink:git-push[1].
+
+[[def_repository]]repository::
+       A collection of <<def_ref,refs>> together with an
+       <<def_object_database,object database>> containing all objects
+       which are <<def_reachable,reachable>> from the refs, possibly
+       accompanied by meta data from one or more <<def_porcelain,porcelains>>. A
+       repository can share an object database with other repositories
+       via <<def_alternate_object_database,alternates mechanism>>.
+
+[[def_resolve]]resolve::
+       The action of fixing up manually what a failed automatic
+       <<def_merge,merge>> left behind.
+
+[[def_revision]]revision::
+       A particular state of files and directories which was stored in the
+       <<def_object_database,object database>>. It is referenced by a
+       <<def_commit_object,commit object>>.
+
+[[def_rewind]]rewind::
+       To throw away part of the development, i.e. to assign the
+       <<def_head,head>> to an earlier <<def_revision,revision>>.
+
+[[def_SCM]]SCM::
+       Source code management (tool).
+
+[[def_SHA1]]SHA1::
+       Synonym for <<def_object_name,object name>>.
+
+[[def_shallow_repository]]shallow repository::
+       A shallow <<def_repository,repository>> has an incomplete
+       history some of whose <<def_commit,commits>> have <<def_parent,parents>> cauterized away (in other
+       words, git is told to pretend that these commits do not have the
+       parents, even though they are recorded in the <<def_commit_object,commit
+       object>>). This is sometimes useful when you are interested only in the
+       recent history of a project even though the real history recorded in the
+       upstream is much larger. A shallow repository
+       is created by giving the `--depth` option to gitlink:git-clone[1], and
+       its history can be later deepened with gitlink:git-fetch[1].
+
+[[def_symref]]symref::
+       Symbolic reference: instead of containing the <<def_SHA1,SHA1>>
+       id itself, it is of the format 'ref: refs/some/thing' and when
+       referenced, it recursively dereferences to this reference.
+       '<<def_HEAD,HEAD>>' is a prime example of a symref. Symbolic
+       references are manipulated with the gitlink:git-symbolic-ref[1]
+       command.
+
+[[def_tag]]tag::
+       A <<def_ref,ref>> pointing to a <<def_tag_object,tag>> or
+       <<def_commit_object,commit object>>. In contrast to a <<def_head,head>>,
+       a tag is not changed by a <<def_commit,commit>>. Tags (not
+       <<def_tag_object,tag objects>>) are stored in `$GIT_DIR/refs/tags/`. A
+       git tag has nothing to do with a Lisp tag (which would be
+       called an <<def_object_type,object type>> in git's context). A
+       tag is most typically used to mark a particular point in the
+       commit ancestry <<def_chain,chain>>.
+
+[[def_tag_object]]tag object::
+       An <<def_object,object>> containing a <<def_ref,ref>> pointing to
+       another object, which can contain a message just like a
+       <<def_commit_object,commit object>>. It can also contain a (PGP)
+       signature, in which case it is called a "signed tag object".
+
+[[def_topic_branch]]topic branch::
+       A regular git <<def_branch,branch>> that is used by a developer to
+       identify a conceptual line of development. Since branches are very easy
+       and inexpensive, it is often desirable to have several small branches
+       that each contain very well defined concepts or small incremental yet
+       related changes.
+
+[[def_tracking_branch]]tracking branch::
+       A regular git <<def_branch,branch>> that is used to follow changes from
+       another <<def_repository,repository>>. A tracking
+       branch should not contain direct modifications or have local commits
+       made to it. A tracking branch can usually be
+       identified as the right-hand-side <<def_ref,ref>> in a Pull:
+       <<def_refspec,refspec>>.
+
+[[def_tree]]tree::
+       Either a <<def_working_tree,working tree>>, or a <<def_tree_object,tree
+       object>> together with the dependent <<def_blob_object,blob>> and tree objects
+       (i.e. a stored representation of a working tree).
+
+[[def_tree_object]]tree object::
+       An <<def_object,object>> containing a list of file names and modes along
+       with refs to the associated blob and/or tree objects. A
+       <<def_tree,tree>> is equivalent to a <<def_directory,directory>>.
+
+[[def_tree-ish]]tree-ish::
+       A <<def_ref,ref>> pointing to either a <<def_commit_object,commit
+       object>>, a <<def_tree_object,tree object>>, or a <<def_tag_object,tag
+       object>> pointing to a tag or commit or tree object.
+
+[[def_unmerged_index]]unmerged index::
+       An <<def_index,index>> which contains unmerged
+       <<def_index_entry,index entries>>.
+
+[[def_unreachable_object]]unreachable object::
+       An <<def_object,object>> which is not <<def_reachable,reachable>> from a
+       <<def_branch,branch>>, <<def_tag,tag>>, or any other reference.
+
+[[def_working_tree]]working tree::
+       The tree of actual checked out files.  The working tree is
+       normally equal to the <<def_HEAD,HEAD>> plus any local changes
+       that you have made but not yet committed.
diff --git a/Documentation/hooks.txt b/Documentation/hooks.txt
new file mode 100644 (file)
index 0000000..f110162
--- /dev/null
@@ -0,0 +1,249 @@
+Hooks used by git
+=================
+
+Hooks are little scripts you can place in `$GIT_DIR/hooks`
+directory to trigger action at certain points.  When
+`git-init` is run, a handful example hooks are copied in the
+`hooks` directory of the new repository, but by default they are
+all disabled.  To enable a hook, make it executable with `chmod +x`.
+
+This document describes the currently defined hooks.
+
+applypatch-msg
+--------------
+
+This hook is invoked by `git-am` script.  It takes a single
+parameter, the name of the file that holds the proposed commit
+log message.  Exiting with non-zero status causes
+`git-am` to abort before applying the patch.
+
+The hook is allowed to edit the message file in place, and can
+be used to normalize the message into some project standard
+format (if the project has one). It can also be used to refuse
+the commit after inspecting the message file.
+
+The default 'applypatch-msg' hook, when enabled, runs the
+'commit-msg' hook, if the latter is enabled.
+
+pre-applypatch
+--------------
+
+This hook is invoked by `git-am`.  It takes no parameter,
+and is invoked after the patch is applied, but before a commit
+is made.  Exiting with non-zero status causes the working tree
+after application of the patch not committed.
+
+It can be used to inspect the current working tree and refuse to
+make a commit if it does not pass certain test.
+
+The default 'pre-applypatch' hook, when enabled, runs the
+'pre-commit' hook, if the latter is enabled.
+
+post-applypatch
+---------------
+
+This hook is invoked by `git-am`.  It takes no parameter,
+and is invoked after the patch is applied and a commit is made.
+
+This hook is meant primarily for notification, and cannot affect
+the outcome of `git-am`.
+
+pre-commit
+----------
+
+This hook is invoked by `git-commit`, and can be bypassed
+with `\--no-verify` option.  It takes no parameter, and is
+invoked before obtaining the proposed commit log message and
+making a commit.  Exiting with non-zero status from this script
+causes the `git-commit` to abort.
+
+The default 'pre-commit' hook, when enabled, catches introduction
+of lines with trailing whitespaces and aborts the commit when
+such a line is found.
+
+commit-msg
+----------
+
+This hook is invoked by `git-commit`, and can be bypassed
+with `\--no-verify` option.  It takes a single parameter, the
+name of the file that holds the proposed commit log message.
+Exiting with non-zero status causes the `git-commit` to
+abort.
+
+The hook is allowed to edit the message file in place, and can
+be used to normalize the message into some project standard
+format (if the project has one). It can also be used to refuse
+the commit after inspecting the message file.
+
+The default 'commit-msg' hook, when enabled, detects duplicate
+"Signed-off-by" lines, and aborts the commit if one is found.
+
+post-commit
+-----------
+
+This hook is invoked by `git-commit`.  It takes no
+parameter, and is invoked after a commit is made.
+
+This hook is meant primarily for notification, and cannot affect
+the outcome of `git-commit`.
+
+post-checkout
+-----------
+
+This hook is invoked when a `git-checkout` is run after having updated the
+worktree.  The hook is given three parameters: the ref of the previous HEAD,
+the ref of the new HEAD (which may or may not have changed), and a flag
+indicating whether the checkout was a branch checkout (changing branches,
+flag=1) or a file checkout (retrieving a file from the index, flag=0).
+This hook cannot affect the outcome of `git-checkout`.
+
+This hook can be used to perform repository validity checks, auto-display
+differences from the previous HEAD if different, or set working dir metadata
+properties.
+
+post-merge
+-----------
+
+This hook is invoked by `git-merge`, which happens when a `git pull`
+is done on a local repository.  The hook takes a single parameter, a status
+flag specifying whether or not the merge being done was a squash merge.
+This hook cannot affect the outcome of `git-merge`.
+
+This hook can be used in conjunction with a corresponding pre-commit hook to
+save and restore any form of metadata associated with the working tree
+(eg: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl
+for an example of how to do this.
+
+[[pre-receive]]
+pre-receive
+-----------
+
+This hook is invoked by `git-receive-pack` on the remote repository,
+which happens when a `git push` is done on a local repository.
+Just before starting to update refs on the remote repository, the
+pre-receive hook is invoked.  Its exit status determines the success
+or failure of the update.
+
+This hook executes once for the receive operation. It takes no
+arguments, but for each ref to be updated it receives on standard
+input a line of the format:
+
+  <old-value> SP <new-value> SP <ref-name> LF
+
+where `<old-value>` is the old object name stored in the ref,
+`<new-value>` is the new object name to be stored in the ref and
+`<ref-name>` is the full name of the ref.
+When creating a new ref, `<old-value>` is 40 `0`.
+
+If the hook exits with non-zero status, none of the refs will be
+updated. If the hook exits with zero, updating of individual refs can
+still be prevented by the <<update,'update'>> hook.
+
+Both standard output and standard error output are forwarded to
+`git-send-pack` on the other end, so you can simply `echo` messages
+for the user.
+
+[[update]]
+update
+------
+
+This hook is invoked by `git-receive-pack` on the remote repository,
+which happens when a `git push` is done on a local repository.
+Just before updating the ref on the remote repository, the update hook
+is invoked.  Its exit status determines the success or failure of
+the ref update.
+
+The hook executes once for each ref to be updated, and takes
+three parameters:
+
+ - the name of the ref being updated,
+ - the old object name stored in the ref,
+ - and the new objectname to be stored in the ref.
+
+A zero exit from the update hook allows the ref to be updated.
+Exiting with a non-zero status prevents `git-receive-pack`
+from updating that ref.
+
+This hook can be used to prevent 'forced' update on certain refs by
+making sure that the object name is a commit object that is a
+descendant of the commit object named by the old object name.
+That is, to enforce a "fast forward only" policy.
+
+It could also be used to log the old..new status.  However, it
+does not know the entire set of branches, so it would end up
+firing one e-mail per ref when used naively, though.  The
+<<post-receive,'post-receive'>> hook is more suited to that.
+
+Another use suggested on the mailing list is to use this hook to
+implement access control which is finer grained than the one
+based on filesystem group.
+
+Both standard output and standard error output are forwarded to
+`git-send-pack` on the other end, so you can simply `echo` messages
+for the user.
+
+The default 'update' hook, when enabled--and with
+`hooks.allowunannotated` config option turned on--prevents
+unannotated tags to be pushed.
+
+[[post-receive]]
+post-receive
+------------
+
+This hook is invoked by `git-receive-pack` on the remote repository,
+which happens when a `git push` is done on a local repository.
+It executes on the remote repository once after all the refs have
+been updated.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but gets the same information as the
+<<pre-receive,'pre-receive'>>
+hook does on its standard input.
+
+This hook does not affect the outcome of `git-receive-pack`, as it
+is called after the real work is done.
+
+This supersedes the <<post-update,'post-update'>> hook in that it gets
+both old and new values of all the refs in addition to their
+names.
+
+Both standard output and standard error output are forwarded to
+`git-send-pack` on the other end, so you can simply `echo` messages
+for the user.
+
+The default 'post-receive' hook is empty, but there is
+a sample script `post-receive-email` provided in the `contrib/hooks`
+directory in git distribution, which implements sending commit
+emails.
+
+[[post-update]]
+post-update
+-----------
+
+This hook is invoked by `git-receive-pack` on the remote repository,
+which happens when a `git push` is done on a local repository.
+It executes on the remote repository once after all the refs have
+been updated.
+
+It takes a variable number of parameters, each of which is the
+name of ref that was actually updated.
+
+This hook is meant primarily for notification, and cannot affect
+the outcome of `git-receive-pack`.
+
+The 'post-update' hook can tell what are the heads that were pushed,
+but it does not know what their original and updated values are,
+so it is a poor place to do log old..new. The
+<<post-receive,'post-receive'>> hook does get both original and
+updated values of the refs. You might consider it instead if you need
+them.
+
+When enabled, the default 'post-update' hook runs
+`git-update-server-info` to keep the information used by dumb
+transports (e.g., HTTP) up-to-date.  If you are publishing
+a git repository that is accessible via HTTP, you should
+probably enable this hook.
+
+Both standard output and standard error output are forwarded to
+`git-send-pack` on the other end, so you can simply `echo` messages
+for the user.
diff --git a/Documentation/howto-index.sh b/Documentation/howto-index.sh
new file mode 100755 (executable)
index 0000000..34aa30c
--- /dev/null
@@ -0,0 +1,56 @@
+#!/bin/sh
+
+cat <<\EOF
+GIT Howto Index
+===============
+
+Here is a collection of mailing list postings made by various
+people describing how they use git in their workflow.
+
+EOF
+
+for txt
+do
+       title=`expr "$txt" : '.*/\(.*\)\.txt$'`
+       from=`sed -ne '
+       /^$/q
+       /^From:[        ]/{
+               s///
+               s/^[    ]*//
+               s/[     ]*$//
+               s/^/by /
+               p
+       }
+       ' "$txt"`
+
+       abstract=`sed -ne '
+       /^Abstract:[    ]/{
+               s/^[^   ]*//
+               x
+               s/.*//
+               x
+               : again
+               /^[     ]/{
+                       s/^[    ]*//
+                       H
+                       n
+                       b again
+               }
+               x
+               p
+               q
+       }' "$txt"`
+
+       if grep 'Content-type: text/asciidoc' >/dev/null $txt
+       then
+               file=`expr "$txt" : '\(.*\)\.txt$'`.html
+       else
+               file="$txt"
+       fi
+
+       echo "* link:$file[$title] $from
+$abstract
+
+"
+
+done
diff --git a/Documentation/howto/maintain-git.txt b/Documentation/howto/maintain-git.txt
new file mode 100644 (file)
index 0000000..4357e26
--- /dev/null
@@ -0,0 +1,277 @@
+From: Junio C Hamano <gitster@pobox.com>
+Date: Wed, 21 Nov 2007 16:32:55 -0800
+Subject: Addendum to "MaintNotes"
+Abstract: Imagine that git development is racing along as usual, when our friendly
+ neighborhood maintainer is struck down by a wayward bus. Out of the
+ hordes of suckers (loyal developers), you have been tricked (chosen) to
+ step up as the new maintainer. This howto will show you "how to" do it.
+
+The maintainer's git time is spent on three activities.
+
+ - Communication (60%)
+
+   Mailing list discussions on general design, fielding user
+   questions, diagnosing bug reports; reviewing, commenting on,
+   suggesting alternatives to, and rejecting patches.
+
+ - Integration (30%)
+
+   Applying new patches from the contributors while spotting and
+   correcting minor mistakes, shuffling the integration and
+   testing branches, pushing the results out, cutting the
+   releases, and making announcements.
+
+ - Own development (10%)
+
+   Scratching my own itch and sending proposed patch series out.
+
+The policy on Integration is informally mentioned in "A Note
+from the maintainer" message, which is periodically posted to
+this mailing list after each feature release is made.
+
+The policy.
+
+ - Feature releases are numbered as vX.Y.Z and are meant to
+   contain bugfixes and enhancements in any area, including
+   functionality, performance and usability, without regression.
+
+ - Maintenance releases are numbered as vX.Y.Z.W and are meant
+   to contain only bugfixes for the corresponding vX.Y.Z feature
+   release and earlier maintenance releases vX.Y.Z.V (V < W).
+
+ - 'master' branch is used to prepare for the next feature
+   release. In other words, at some point, the tip of 'master'
+   branch is tagged with vX.Y.Z.
+
+ - 'maint' branch is used to prepare for the next maintenance
+   release.  After the feature release vX.Y.Z is made, the tip
+   of 'maint' branch is set to that release, and bugfixes will
+   accumulate on the branch, and at some point, the tip of the
+   branch is tagged with vX.Y.Z.1, vX.Y.Z.2, and so on.
+
+ - 'next' branch is used to publish changes (both enhancements
+   and fixes) that (1) have worthwhile goal, (2) are in a fairly
+   good shape suitable for everyday use, (3) but have not yet
+   demonstrated to be regression free.  New changes are tested
+   in 'next' before merged to 'master'.
+
+ - 'pu' branch is used to publish other proposed changes that do
+   not yet pass the criteria set for 'next'.
+
+ - The tips of 'master', 'maint' and 'next' branches will always
+   fast forward, to allow people to build their own
+   customization on top of them.
+
+ - Usually 'master' contains all of 'maint', 'next' contains all
+   of 'master' and 'pu' contains all of 'next'.
+
+ - The tip of 'master' is meant to be more stable than any
+   tagged releases, and the users are encouraged to follow it.
+
+ - The 'next' branch is where new action takes place, and the
+   users are encouraged to test it so that regressions and bugs
+   are found before new topics are merged to 'master'.
+
+
+A typical git day for the maintainer implements the above policy
+by doing the following:
+
+ - Scan mailing list and #git channel log.  Respond with review
+   comments, suggestions etc.  Kibitz.  Collect potentially
+   usable patches from the mailing list.  Patches about a single
+   topic go to one mailbox (I read my mail in Gnus, and type
+   \C-o to save/append messages in files in mbox format).
+
+ - Review the patches in the saved mailboxes.  Edit proposed log
+   message for typofixes and clarifications, and add Acks
+   collected from the list.  Edit patch to incorporate "Oops,
+   that should have been like this" fixes from the discussion.
+
+ - Classify the collected patches and handle 'master' and
+   'maint' updates:
+
+   - Obviously correct fixes that pertain to the tip of 'maint'
+     are directly applied to 'maint'.
+
+   - Obviously correct fixes that pertain to the tip of 'master'
+     are directly applied to 'master'.
+
+   This step is done with "git am".
+
+     $ git checkout master    ;# or "git checkout maint"
+     $ git am -3 -s mailbox
+     $ make test
+
+ - Merge downwards (maint->master):
+
+     $ git checkout master
+     $ git merge maint
+     $ make test
+
+ - Review the last issue of "What's cooking" message, review the
+   topics scheduled for merging upwards (topic->master and
+   topic->maint), and merge.
+
+     $ git checkout master    ;# or "git checkout maint"
+     $ git merge ai/topic     ;# or "git merge ai/maint-topic"
+     $ git log -p ORIG_HEAD.. ;# final review
+     $ git diff ORIG_HEAD..   ;# final review
+     $ make test              ;# final review
+     $ git branch -d ai/topic ;# or "git branch -d ai/maint-topic"
+
+ - Merge downwards (maint->master) if needed:
+
+     $ git checkout master
+     $ git merge maint
+     $ make test
+
+ - Merge downwards (master->next) if needed:
+
+     $ git checkout next
+     $ git merge master
+     $ make test
+
+ - Handle the remaining patches:
+
+   - Anything unobvious that is applicable to 'master' (in other
+     words, does not depend on anything that is still in 'next'
+     and not in 'master') is applied to a new topic branch that
+     is forked from the tip of 'master'.  This includes both
+     enhancements and unobvious fixes to 'master'.  A topic
+     branch is named as ai/topic where "ai" is typically
+     author's initial and "topic" is a descriptive name of the
+     topic (in other words, "what's the series is about").
+
+   - An unobvious fix meant for 'maint' is applied to a new
+     topic branch that is forked from the tip of 'maint'.  The
+     topic is named as ai/maint-topic.
+
+   - Changes that pertain to an existing topic are applied to
+     the branch, but:
+
+     - obviously correct ones are applied first;
+
+     - questionable ones are discarded or applied to near the tip;
+
+   - Replacement patches to an existing topic are accepted only
+     for commits not in 'next'.
+
+   The above except the "replacement" are all done with:
+
+     $ git am -3 -s mailbox
+
+   while patch replacement is often done by:
+
+     $ git format-patch ai/topic~$n..ai/topic ;# export existing
+
+   then replace some parts with the new patch, and reapplying:
+
+     $ git reset --hard ai/topic~$n
+     $ git am -3 -s 000*.txt
+
+   The full test suite is always run for 'maint' and 'master'
+   after patch application; for topic branches the tests are run
+   as time permits.
+
+ - Update "What's cooking" message to review the updates to
+   existing topics, newly added topics and graduated topics.
+
+   This step is helped with Meta/UWC script (where Meta/ contains
+   a checkout of the 'todo' branch).
+
+ - Merge topics to 'next'.  For each branch whose tip is not
+   merged to 'next', one of three things can happen:
+
+   - The commits are all next-worthy; merge the topic to next:
+
+     $ git checkout next
+     $ git merge ai/topic     ;# or "git merge ai/maint-topic"
+     $ make test
+
+   - The new parts are of mixed quality, but earlier ones are
+     next-worthy; merge the early parts to next:
+
+     $ git checkout next
+     $ git merge ai/topic~2   ;# the tip two are dubious
+     $ make test
+
+   - Nothing is next-worthy; do not do anything.
+
+ - Rebase topics that do not have any commit in next yet.  This
+   step is optional but sometimes is worth doing when an old
+   series that is not in next can take advantage of low-level
+   framework change that is merged to 'master' already.
+
+     $ git rebase master ai/topic
+
+   This step is helped with Meta/git-topic.perl script to
+   identify which topic is rebaseable.  There also is a
+   pre-rebase hook to make sure that topics that are already in
+   'next' are not rebased beyond the merged commit.
+
+ - Rebuild "pu" to merge the tips of topics not in 'next'.
+
+     $ git checkout pu
+     $ git reset --hard next
+     $ git merge ai/topic     ;# repeat for all remaining topics
+     $ make test
+
+   This step is helped with Meta/PU script
+
+ - Push four integration branches to a private repository at
+   k.org and run "make test" on all of them.
+
+ - Push four integration branches to /pub/scm/git/git.git at
+   k.org.  This triggers its post-update hook which:
+
+    (1) runs "git pull" in $HOME/git-doc/ repository to pull
+        'master' just pushed out;
+
+    (2) runs "make doc" in $HOME/git-doc/, install the generated
+        documentation in staging areas, which are separate
+        repositories that have html and man branches checked
+        out.
+
+    (3) runs "git commit" in the staging areas, and run "git
+        push" back to /pub/scm/git/git.git/ to update the html
+        and man branches.
+
+    (4) installs generated documentation to /pub/software/scm/git/docs/
+        to be viewed from http://www.kernel.org/
+
+ - Fetch html and man branches back from k.org, and push four
+   integration branches and the two documentation branches to
+   repo.or.cz
+
+
+Some observations to be made.
+
+ * Each topic is tested individually, and also together with
+   other topics cooking in 'next'.  Until it matures, none part
+   of it is merged to 'master'.
+
+ * A topic already in 'next' can get fixes while still in
+   'next'.  Such a topic will have many merges to 'next' (in
+   other words, "git log --first-parent next" will show many
+   "Merge ai/topic to next" for the same topic.
+
+ * An unobvious fix for 'maint' is cooked in 'next' and then
+   merged to 'master' to make extra sure it is Ok and then
+   merged to 'maint'.
+
+ * Even when 'next' becomes empty (in other words, all topics
+   prove stable and are merged to 'master' and "git diff master
+   next" shows empty), it has tons of merge commits that will
+   never be in 'master'.
+
+ * In principle, "git log --first-parent master..next" should
+   show nothing but merges (in practice, there are fixup commits
+   and reverts that are not merges).
+
+ * Commits near the tip of a topic branch that are not in 'next'
+   are fair game to be discarded, replaced or rewritten.
+   Commits already merged to 'next' will not be.
+
+ * Being in the 'next' branch is not a guarantee for a topic to
+   be included in the next feature release.  Being in the
+   'master' branch typically is.
diff --git a/Documentation/howto/rebase-and-edit.txt b/Documentation/howto/rebase-and-edit.txt
new file mode 100644 (file)
index 0000000..554909f
--- /dev/null
@@ -0,0 +1,79 @@
+Date:  Sat, 13 Aug 2005 22:16:02 -0700 (PDT)
+From:  Linus Torvalds <torvalds@osdl.org>
+To:    Steve French <smfrench@austin.rr.com>
+cc:    git@vger.kernel.org
+Subject: Re: sending changesets from the middle of a git tree
+Abstract: In this article, Linus demonstrates how a broken commit
+ in a sequence of commits can be removed by rewinding the head and
+ reapplying selected changes.
+
+On Sat, 13 Aug 2005, Linus Torvalds wrote:
+
+> That's correct. Same things apply: you can move a patch over, and create a
+> new one with a modified comment, but basically the _old_ commit will be
+> immutable.
+
+Let me clarify.
+
+You can entirely _drop_ old branches, so commits may be immutable, but
+nothing forces you to keep them. Of course, when you drop a commit, you'll
+always end up dropping all the commits that depended on it, and if you
+actually got somebody else to pull that commit you can't drop it from
+_their_ repository, but undoing things is not impossible.
+
+For example, let's say that you've made a mess of things: you've committed
+three commits "old->a->b->c", and you notice that "a" was broken, but you
+want to save "b" and "c". What you can do is
+
+       # Create a branch "broken" that is the current code
+       # for reference
+       git branch broken
+
+       # Reset the main branch to three parents back: this
+       # effectively undoes the three top commits
+       git reset HEAD^^^
+       git checkout -f
+
+       # Check the result visually to make sure you know what's
+       # going on
+       gitk --all
+
+       # Re-apply the two top ones from "broken"
+       #
+       # First "parent of broken" (aka b):
+       git-diff-tree -p broken^ | git-apply --index
+       git commit --reedit=broken^
+
+       # Then "top of broken" (aka c):
+       git-diff-tree -p broken | git-apply --index
+       git commit --reedit=broken
+
+and you've now re-applied (and possibly edited the comments) the two
+commits b/c, and commit "a" is basically gone (it still exists in the
+"broken" branch, of course).
+
+Finally, check out the end result again:
+
+       # Look at the new commit history
+       gitk --all
+
+to see that everything looks sensible.
+
+And then, you can just remove the broken branch if you decide you really
+don't want it:
+
+       # remove 'broken' branch
+       git branch -d broken
+
+       # Prune old objects if you're really really sure
+       git prune
+
+And yeah, I'm sure there are other ways of doing this. And as usual, the
+above is totally untested, and I just wrote it down in this email, so if
+I've done something wrong, you'll have to figure it out on your own ;)
+
+                       Linus
+-
+To unsubscribe from this list: send the line "unsubscribe git" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
diff --git a/Documentation/howto/rebase-from-internal-branch.txt b/Documentation/howto/rebase-from-internal-branch.txt
new file mode 100644 (file)
index 0000000..7a76045
--- /dev/null
@@ -0,0 +1,163 @@
+From:  Junio C Hamano <junkio@cox.net>
+To:    git@vger.kernel.org
+Cc:    Petr Baudis <pasky@suse.cz>, Linus Torvalds <torvalds@osdl.org>
+Subject: Re: sending changesets from the middle of a git tree
+Date:  Sun, 14 Aug 2005 18:37:39 -0700
+Abstract: In this article, JC talks about how he rebases the
+ public "pu" branch using the core GIT tools when he updates
+ the "master" branch, and how "rebase" works.  Also discussed
+ is how this applies to individual developers who sends patches
+ upstream.
+
+Petr Baudis <pasky@suse.cz> writes:
+
+> Dear diary, on Sun, Aug 14, 2005 at 09:57:13AM CEST, I got a letter
+> where Junio C Hamano <junkio@cox.net> told me that...
+>> Linus Torvalds <torvalds@osdl.org> writes:
+>>
+>> > Junio, maybe you want to talk about how you move patches from your "pu"
+>> > branch to the real branches.
+>>
+> Actually, wouldn't this be also precisely for what StGIT is intended to?
+
+Exactly my feeling.  I was sort of waiting for Catalin to speak
+up.  With its basing philosophical ancestry on quilt, this is
+the kind of task StGIT is designed to do.
+
+I just have done a simpler one, this time using only the core
+GIT tools.
+
+I had a handful commits that were ahead of master in pu, and I
+wanted to add some documentation bypassing my usual habit of
+placing new things in pu first.  At the beginning, the commit
+ancestry graph looked like this:
+
+                             *"pu" head
+    master --> #1 --> #2 --> #3
+
+So I started from master, made a bunch of edits, and committed:
+
+    $ git checkout master
+    $ cd Documentation; ed git.txt ...
+    $ cd ..; git add Documentation/*.txt
+    $ git commit -s
+
+After the commit, the ancestry graph would look like this:
+
+                              *"pu" head
+    master^ --> #1 --> #2 --> #3
+          \
+            \---> master
+
+The old master is now master^ (the first parent of the master).
+The new master commit holds my documentation updates.
+
+Now I have to deal with "pu" branch.
+
+This is the kind of situation I used to have all the time when
+Linus was the maintainer and I was a contributor, when you look
+at "master" branch being the "maintainer" branch, and "pu"
+branch being the "contributor" branch.  Your work started at the
+tip of the "maintainer" branch some time ago, you made a lot of
+progress in the meantime, and now the maintainer branch has some
+other commits you do not have yet.  And "git rebase" was written
+with the explicit purpose of helping to maintain branches like
+"pu".  You _could_ merge master to pu and keep going, but if you
+eventually want to cherrypick and merge some but not necessarily
+all changes back to the master branch, it often makes later
+operations for _you_ easier if you rebase (i.e. carry forward
+your changes) "pu" rather than merge.  So I ran "git rebase":
+
+    $ git checkout pu
+    $ git rebase master pu
+
+What this does is to pick all the commits since the current
+branch (note that I now am on "pu" branch) forked from the
+master branch, and forward port these changes.
+
+    master^ --> #1 --> #2 --> #3
+          \                                  *"pu" head
+            \---> master --> #1' --> #2' --> #3'
+
+The diff between master^ and #1 is applied to master and
+committed to create #1' commit with the commit information (log,
+author and date) taken from commit #1.  On top of that #2' and #3'
+commits are made similarly out of #2 and #3 commits.
+
+Old #3 is not recorded in any of the .git/refs/heads/ file
+anymore, so after doing this you will have dangling commit if
+you ran fsck-cache, which is normal.  After testing "pu", you
+can run "git prune" to get rid of those original three commits.
+
+While I am talking about "git rebase", I should talk about how
+to do cherrypicking using only the core GIT tools.
+
+Let's go back to the earlier picture, with different labels.
+
+You, as an individual developer, cloned upstream repository and
+made a couple of commits on top of it.
+
+                              *your "master" head
+   upstream --> #1 --> #2 --> #3
+
+You would want changes #2 and #3 incorporated in the upstream,
+while you feel that #1 may need further improvements.  So you
+prepare #2 and #3 for e-mail submission.
+
+    $ git format-patch master^^ master
+
+This creates two files, 0001-XXXX.patch and 0002-XXXX.patch.  Send
+them out "To: " your project maintainer and "Cc: " your mailing
+list.  You could use contributed script git-send-email if
+your host has necessary perl modules for this, but your usual
+MUA would do as long as it does not corrupt whitespaces in the
+patch.
+
+Then you would wait, and you find out that the upstream picked
+up your changes, along with other changes.
+
+   where                      *your "master" head
+  upstream --> #1 --> #2 --> #3
+    used   \
+   to be     \--> #A --> #2' --> #3' --> #B --> #C
+                                                *upstream head
+
+The two commits #2' and #3' in the above picture record the same
+changes your e-mail submission for #2 and #3 contained, but
+probably with the new sign-off line added by the upstream
+maintainer and definitely with different committer and ancestry
+information, they are different objects from #2 and #3 commits.
+
+You fetch from upstream, but not merge.
+
+    $ git fetch upstream
+
+This leaves the updated upstream head in .git/FETCH_HEAD but
+does not touch your .git/HEAD nor .git/refs/heads/master.
+You run "git rebase" now.
+
+    $ git rebase FETCH_HEAD master
+
+Earlier, I said that rebase applies all the commits from your
+branch on top of the upstream head.  Well, I lied.  "git rebase"
+is a bit smarter than that and notices that #2 and #3 need not
+be applied, so it only applies #1.  The commit ancestry graph
+becomes something like this:
+
+   where                     *your old "master" head
+  upstream --> #1 --> #2 --> #3
+    used   \                      your new "master" head*
+   to be     \--> #A --> #2' --> #3' --> #B --> #C --> #1'
+                                                *upstream
+                                                head
+
+Again, "git prune" would discard the disused commits #1-#3 and
+you continue on starting from the new "master" head, which is
+the #1' commit.
+
+-jc
+
+-
+To unsubscribe from this list: send the line "unsubscribe git" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
diff --git a/Documentation/howto/rebuild-from-update-hook.txt b/Documentation/howto/rebuild-from-update-hook.txt
new file mode 100644 (file)
index 0000000..8d55dfb
--- /dev/null
@@ -0,0 +1,86 @@
+Subject: [HOWTO] Using post-update hook
+Message-ID: <7vy86o6usx.fsf@assigned-by-dhcp.cox.net>
+From: Junio C Hamano <junkio@cox.net>
+Date: Fri, 26 Aug 2005 18:19:10 -0700
+Abstract: In this how-to article, JC talks about how he
+ uses the post-update hook to automate git documentation page
+ shown at http://www.kernel.org/pub/software/scm/git/docs/.
+
+The pages under http://www.kernel.org/pub/software/scm/git/docs/
+are built from Documentation/ directory of the git.git project
+and needed to be kept up-to-date.  The www.kernel.org/ servers
+are mirrored and I was told that the origin of the mirror is on
+the machine $some.kernel.org, on which I was given an account
+when I took over git maintainership from Linus.
+
+The directories relevant to this how-to are these two:
+
+    /pub/scm/git/git.git/      The public git repository.
+    /pub/software/scm/git/docs/        The HTML documentation page.
+
+So I made a repository to generate the documentation under my
+home directory over there.
+
+    $ cd
+    $ mkdir doc-git && cd doc-git
+    $ git clone /pub/scm/git/git.git/ docgen
+
+What needs to happen is to update the $HOME/doc-git/docgen/
+working tree, build HTML docs there and install the result in
+/pub/software/scm/git/docs/ directory.  So I wrote a little
+script:
+
+    $ cat >dododoc.sh <<\EOF
+    #!/bin/sh
+    cd $HOME/doc-git/docgen || exit
+
+    unset GIT_DIR
+
+    git pull /pub/scm/git/git.git/ master &&
+    cd Documentation &&
+    make install-webdoc
+    EOF
+
+Initially I used to run this by hand whenever I push into the
+public git repository.  Then I did a cron job that ran twice a
+day.  The current round uses the post-update hook mechanism,
+like this:
+
+    $ cat >/pub/scm/git/git.git/hooks/post-update <<\EOF
+    #!/bin/sh
+    #
+    # An example hook script to prepare a packed repository for use over
+    # dumb transports.
+    #
+    # To enable this hook, make this file executable by "chmod +x post-update".
+
+    case " $* " in
+    *' refs/heads/master '*)
+            echo $HOME/doc-git/dododoc.sh | at now
+            ;;
+    esac
+    exec git-update-server-info
+    EOF
+    $ chmod +x /pub/scm/git/git.git/hooks/post-update
+
+There are four things worth mentioning:
+
+ - The update-hook is run after the repository accepts a "git
+   push", under my user privilege.  It is given the full names
+   of refs that have been updated as arguments.  My post-update
+   runs the dododoc.sh script only when the master head is
+   updated.
+
+ - When update-hook is run, GIT_DIR is set to '.' by the calling
+   receive-pack.  This is inherited by the dododoc.sh run via
+   the "at" command, and needs to be unset; otherwise, "git
+   pull" it does into $HOME/doc-git/docgen/ repository would not
+   work correctly.
+
+ - The stdout of update hook script is not connected to git
+   push; I run the heavy part of the command inside "at", to
+   receive the execution report via e-mail.
+
+ - This is still crude and does not protect against simultaneous
+   make invocations stomping on each other.  I would need to add
+   some locking mechanism for this.
diff --git a/Documentation/howto/recover-corrupted-blob-object.txt b/Documentation/howto/recover-corrupted-blob-object.txt
new file mode 100644 (file)
index 0000000..323b513
--- /dev/null
@@ -0,0 +1,134 @@
+Date: Fri, 9 Nov 2007 08:28:38 -0800 (PST)
+From: Linus Torvalds <torvalds@linux-foundation.org>
+Subject: corrupt object on git-gc
+Abstract: Some tricks to reconstruct blob objects in order to fix
+ a corrupted repository.
+
+On Fri, 9 Nov 2007, Yossi Leybovich wrote:
+>
+> Did not help still the repository look for this object?
+> Any one know how can I track this object and understand which file is it
+
+So exactly *because* the SHA1 hash is cryptographically secure, the hash
+itself doesn't actually tell you anything, in order to fix a corrupt
+object you basically have to find the "original source" for it.
+
+The easiest way to do that is almost always to have backups, and find the
+same object somewhere else. Backups really are a good idea, and git makes
+it pretty easy (if nothing else, just clone the repository somewhere else,
+and make sure that you do *not* use a hard-linked clone, and preferably
+not the same disk/machine).
+
+But since you don't seem to have backups right now, the good news is that
+especially with a single blob being corrupt, these things *are* somewhat
+debuggable.
+
+First off, move the corrupt object away, and *save* it. The most common
+cause of corruption so far has been memory corruption, but even so, there
+are people who would be interested in seeing the corruption - but it's
+basically impossible to judge the corruption until we can also see the
+original object, so right now the corrupt object is useless, but it's very
+interesting for the future, in the hope that you can re-create a
+non-corrupt version.
+
+So:
+
+> ib]$ mv .git/objects/4b/9458b3786228369c63936db65827de3cc06200 ../
+
+This is the right thing to do, although it's usually best to save it under
+it's full SHA1 name (you just dropped the "4b" from the result ;).
+
+Let's see what that tells us:
+
+> ib]$ git-fsck --full
+> broken link from    tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
+>              to    blob 4b9458b3786228369c63936db65827de3cc06200
+> missing blob 4b9458b3786228369c63936db65827de3cc06200
+
+Ok, I removed the "dangling commit" messages, because they are just
+messages about the fact that you probably have rebased etc, so they're not
+at all interesting. But what remains is still very useful. In particular,
+we now know which tree points to it!
+
+Now you can do
+
+       git ls-tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
+
+which will show something like
+
+       100644 blob 8d14531846b95bfa3564b58ccfb7913a034323b8    .gitignore
+       100644 blob ebf9bf84da0aab5ed944264a5db2a65fe3a3e883    .mailmap
+       100644 blob ca442d313d86dc67e0a2e5d584b465bd382cbf5c    COPYING
+       100644 blob ee909f2cc49e54f0799a4739d24c4cb9151ae453    CREDITS
+       040000 tree 0f5f709c17ad89e72bdbbef6ea221c69807009f6    Documentation
+       100644 blob 1570d248ad9237e4fa6e4d079336b9da62d9ba32    Kbuild
+       100644 blob 1c7c229a092665b11cd46a25dbd40feeb31661d9    MAINTAINERS
+       ...
+
+and you should now have a line that looks like
+
+       10064 blob 4b9458b3786228369c63936db65827de3cc06200     my-magic-file
+
+in the output. This already tells you a *lot* it tells you what file the
+corrupt blob came from!
+
+Now, it doesn't tell you quite enough, though: it doesn't tell what
+*version* of the file didn't get correctly written! You might be really
+lucky, and it may be the version that you already have checked out in your
+working tree, in which case fixing this problem is really simple, just do
+
+       git hash-object -w my-magic-file
+
+again, and if it outputs the missing SHA1 (4b945..) you're now all done!
+
+But that's the really lucky case, so let's assume that it was some older
+version that was broken. How do you tell which version it was?
+
+The easiest way to do it is to do
+
+       git log --raw --all --full-history -- subdirectory/my-magic-file
+
+and that will show you the whole log for that file (please realize that
+the tree you had may not be the top-level tree, so you need to figure out
+which subdirectory it was in on your own), and because you're asking for
+raw output, you'll now get something like
+
+       commit abc
+       Author:
+       Date:
+         ..
+       :100644 100644 4b9458b... newsha... M  somedirectory/my-magic-file
+
+
+       commit xyz
+       Author:
+       Date:
+
+         ..
+       :100644 100644 oldsha... 4b9458b... M   somedirectory/my-magic-file
+
+and this actually tells you what the *previous* and *subsequent* versions
+of that file were! So now you can look at those ("oldsha" and "newsha"
+respectively), and hopefully you have done commits often, and can
+re-create the missing my-magic-file version by looking at those older and
+newer versions!
+
+If you can do that, you can now recreate the missing object with
+
+       git hash-object -w <recreated-file>
+
+and your repository is good again!
+
+(Btw, you could have ignored the fsck, and started with doing a
+
+       git log --raw --all
+
+and just looked for the sha of the missing object (4b9458b..) in that
+whole thing. It's up to you - git does *have* a lot of information, it is
+just missing one particular blob version.
+
+Trying to recreate trees and especially commits is *much* harder. So you
+were lucky that it's a blob. It's quite possible that you can recreate the
+thing.
+
+                       Linus
diff --git a/Documentation/howto/revert-branch-rebase.txt b/Documentation/howto/revert-branch-rebase.txt
new file mode 100644 (file)
index 0000000..865a666
--- /dev/null
@@ -0,0 +1,193 @@
+From: Junio C Hamano <junkio@cox.net>
+To: git@vger.kernel.org
+Subject: [HOWTO] Reverting an existing commit
+Abstract: In this article, JC gives a small real-life example of using
+ 'git revert' command, and using a temporary branch and tag for safety
+ and easier sanity checking.
+Date: Mon, 29 Aug 2005 21:39:02 -0700
+Content-type: text/asciidoc
+Message-ID: <7voe7g3uop.fsf@assigned-by-dhcp.cox.net>
+
+Reverting an existing commit
+============================
+
+One of the changes I pulled into the 'master' branch turns out to
+break building GIT with GCC 2.95.  While they were well intentioned
+portability fixes, keeping things working with gcc-2.95 was also
+important.  Here is what I did to revert the change in the 'master'
+branch and to adjust the 'pu' branch, using core GIT tools and
+barebone Porcelain.
+
+First, prepare a throw-away branch in case I screw things up.
+
+------------------------------------------------
+$ git checkout -b revert-c99 master
+------------------------------------------------
+
+Now I am on the 'revert-c99' branch.  Let's figure out which commit to
+revert.  I happen to know that the top of the 'master' branch is a
+merge, and its second parent (i.e. foreign commit I merged from) has
+the change I would want to undo.  Further I happen to know that that
+merge introduced 5 commits or so:
+
+------------------------------------------------
+$ git show-branch --more=4 master master^2 | head
+* [master] Merge refs/heads/portable from http://www.cs.berkeley....
+ ! [master^2] Replace C99 array initializers with code.
+--
+-  [master] Merge refs/heads/portable from http://www.cs.berkeley....
+*+ [master^2] Replace C99 array initializers with code.
+*+ [master^2~1] Replace unsetenv() and setenv() with older putenv().
+*+ [master^2~2] Include sys/time.h in daemon.c.
+*+ [master^2~3] Fix ?: statements.
+*+ [master^2~4] Replace zero-length array decls with [].
+*  [master~1] tutorial note about git branch
+------------------------------------------------
+
+The '--more=4' above means "after we reach the merge base of refs,
+show until we display four more common commits".  That last commit
+would have been where the "portable" branch was forked from the main
+git.git repository, so this would show everything on both branches
+since then.  I just limited the output to the first handful using
+'head'.
+
+Now I know 'master^2~4' (pronounce it as "find the second parent of
+the 'master', and then go four generations back following the first
+parent") is the one I would want to revert.  Since I also want to say
+why I am reverting it, the '-n' flag is given to 'git revert'.  This
+prevents it from actually making a commit, and instead 'git revert'
+leaves the commit log message it wanted to use in '.msg' file:
+
+------------------------------------------------
+$ git revert -n master^2~4
+$ cat .msg
+Revert "Replace zero-length array decls with []."
+
+This reverts 6c5f9baa3bc0d63e141e0afc23110205379905a4 commit.
+$ git diff HEAD ;# to make sure what we are reverting makes sense.
+$ make CC=gcc-2.95 clean test ;# make sure it fixed the breakage.
+$ make clean test ;# make sure it did not cause other breakage.
+------------------------------------------------
+
+The reverted change makes sense (from reading the 'diff' output), does
+fix the problem (from 'make CC=gcc-2.95' test), and does not cause new
+breakage (from the last 'make test').  I'm ready to commit:
+
+------------------------------------------------
+$ git commit -a -s ;# read .msg into the log,
+                    # and explain why I am reverting.
+------------------------------------------------
+
+I could have screwed up in any of the above steps, but in the worst
+case I could just have done 'git checkout master' to start over.
+Fortunately I did not have to; what I have in the current branch
+'revert-c99' is what I want.  So merge that back into 'master':
+
+------------------------------------------------
+$ git checkout master
+$ git merge revert-c99 ;# this should be a fast forward
+Updating from 10d781b9caa4f71495c7b34963bef137216f86a8 to e3a693c...
+ cache.h        |    8 ++++----
+ commit.c       |    2 +-
+ ls-files.c     |    2 +-
+ receive-pack.c |    2 +-
+ server-info.c  |    2 +-
+ 5 files changed, 8 insertions(+), 8 deletions(-)
+------------------------------------------------
+
+There is no need to redo the test at this point.  We fast forwarded
+and we know 'master' matches 'revert-c99' exactly.  In fact:
+
+------------------------------------------------
+$ git diff master..revert-c99
+------------------------------------------------
+
+says nothing.
+
+Then we rebase the 'pu' branch as usual.
+
+------------------------------------------------
+$ git checkout pu
+$ git tag pu-anchor pu
+$ git rebase master
+* Applying: Redo "revert" using three-way merge machinery.
+First trying simple merge strategy to cherry-pick.
+Finished one cherry-pick.
+* Applying: Remove git-apply-patch-script.
+First trying simple merge strategy to cherry-pick.
+Simple cherry-pick fails; trying Automatic cherry-pick.
+Removing Documentation/git-apply-patch-script.txt
+Removing git-apply-patch-script
+Finished one cherry-pick.
+* Applying: Document "git cherry-pick" and "git revert"
+First trying simple merge strategy to cherry-pick.
+Finished one cherry-pick.
+* Applying: mailinfo and applymbox updates
+First trying simple merge strategy to cherry-pick.
+Finished one cherry-pick.
+* Applying: Show commits in topo order and name all commits.
+First trying simple merge strategy to cherry-pick.
+Finished one cherry-pick.
+* Applying: More documentation updates.
+First trying simple merge strategy to cherry-pick.
+Finished one cherry-pick.
+------------------------------------------------
+
+The temporary tag 'pu-anchor' is me just being careful, in case 'git
+rebase' screws up.  After this, I can do these for sanity check:
+
+------------------------------------------------
+$ git diff pu-anchor..pu ;# make sure we got the master fix.
+$ make CC=gcc-2.95 clean test ;# make sure it fixed the breakage.
+$ make clean test ;# make sure it did not cause other breakage.
+------------------------------------------------
+
+Everything is in the good order.  I do not need the temporary branch
+nor tag anymore, so remove them:
+
+------------------------------------------------
+$ rm -f .git/refs/tags/pu-anchor
+$ git branch -d revert-c99
+------------------------------------------------
+
+It was an emergency fix, so we might as well merge it into the
+'release candidate' branch, although I expect the next release would
+be some days off:
+
+------------------------------------------------
+$ git checkout rc
+$ git pull . master
+Packing 0 objects
+Unpacking 0 objects
+
+* committish: e3a693c...       refs/heads/master from .
+Trying to merge e3a693c... into 8c1f5f0... using 10d781b...
+Committed merge 7fb9b7262a1d1e0a47bbfdcbbcf50ce0635d3f8f
+ cache.h        |    8 ++++----
+ commit.c       |    2 +-
+ ls-files.c     |    2 +-
+ receive-pack.c |    2 +-
+ server-info.c  |    2 +-
+ 5 files changed, 8 insertions(+), 8 deletions(-)
+------------------------------------------------
+
+And the final repository status looks like this:
+
+------------------------------------------------
+$ git show-branch --more=1 master pu rc
+! [master] Revert "Replace zero-length array decls with []."
+ ! [pu] git-repack: Add option to repack all objects.
+  * [rc] Merge refs/heads/master from .
+---
+ +  [pu] git-repack: Add option to repack all objects.
+ +  [pu~1] More documentation updates.
+ +  [pu~2] Show commits in topo order and name all commits.
+ +  [pu~3] mailinfo and applymbox updates
+ +  [pu~4] Document "git cherry-pick" and "git revert"
+ +  [pu~5] Remove git-apply-patch-script.
+ +  [pu~6] Redo "revert" using three-way merge machinery.
+  - [rc] Merge refs/heads/master from .
+++* [master] Revert "Replace zero-length array decls with []."
+  - [rc~1] Merge refs/heads/master from .
+... [master~1] Merge refs/heads/portable from http://www.cs.berkeley....
+------------------------------------------------
diff --git a/Documentation/howto/separating-topic-branches.txt b/Documentation/howto/separating-topic-branches.txt
new file mode 100644 (file)
index 0000000..0d73b31
--- /dev/null
@@ -0,0 +1,90 @@
+From: Junio C Hamano <junkio@cox.net>
+Subject: Separating topic branches
+Abstract: In this article, JC describes how to separate topic branches.
+
+This text was originally a footnote to a discussion about the
+behaviour of the git diff commands.
+
+Often I find myself doing that [running diff against something other
+than HEAD] while rewriting messy development history.  For example, I
+start doing some work without knowing exactly where it leads, and end
+up with a history like this:
+
+            "master"
+        o---o
+             \                    "topic"
+              o---o---o---o---o---o
+
+At this point, "topic" contains something I know I want, but it
+contains two concepts that turned out to be completely independent.
+And often, one topic component is larger than the other.  It may
+contain more than two topics.
+
+In order to rewrite this mess to be more manageable, I would first do
+"diff master..topic", to extract the changes into a single patch, start
+picking pieces from it to get logically self-contained units, and
+start building on top of "master":
+
+        $ git diff master..topic >P.diff
+        $ git checkout -b topicA master
+        ... pick and apply pieces from P.diff to build
+        ... commits on topicA branch.
+
+              o---o---o
+             /        "topicA"
+        o---o"master"
+             \                    "topic"
+              o---o---o---o---o---o
+
+Before doing each commit on "topicA" HEAD, I run "diff HEAD"
+before update-index the affected paths, or "diff --cached HEAD"
+after.  Also I would run "diff --cached master" to make sure
+that the changes are only the ones related to "topicA".  Usually
+I do this for smaller topics first.
+
+After that, I'd do the remainder of the original "topic", but
+for that, I do not start from the patchfile I extracted by
+comparing "master" and "topic" I used initially.  Still on
+"topicA", I extract "diff topic", and use it to rebuild the
+other topic:
+
+        $ git diff -R topic >P.diff ;# --cached also would work fine
+        $ git checkout -b topicB master
+        ... pick and apply pieces from P.diff to build
+        ... commits on topicB branch.
+
+                                "topicB"
+               o---o---o---o---o
+              /
+             /o---o---o
+            |/        "topicA"
+        o---o"master"
+             \                    "topic"
+              o---o---o---o---o---o
+
+After I am done, I'd try a pretend-merge between "topicA" and
+"topicB" in order to make sure I have not missed anything:
+
+        $ git pull . topicA ;# merge it into current "topicB"
+        $ git diff topic
+                                "topicB"
+               o---o---o---o---o---* (pretend merge)
+              /                   /
+             /o---o---o----------'
+            |/        "topicA"
+        o---o"master"
+             \                    "topic"
+              o---o---o---o---o---o
+
+The last diff better not to show anything other than cleanups
+for crufts.  Then I can finally clean things up:
+
+        $ git branch -D topic
+        $ git reset --hard HEAD^ ;# nuke pretend merge
+
+                                "topicB"
+               o---o---o---o---o
+              /
+             /o---o---o
+            |/        "topicA"
+        o---o"master"
diff --git a/Documentation/howto/setup-git-server-over-http.txt b/Documentation/howto/setup-git-server-over-http.txt
new file mode 100644 (file)
index 0000000..8eadc20
--- /dev/null
@@ -0,0 +1,256 @@
+From: Rutger Nijlunsing <rutger@nospam.com>
+Subject: Setting up a git repository which can be pushed into and pulled from over HTTP.
+Date: Thu, 10 Aug 2006 22:00:26 +0200
+
+Since Apache is one of those packages people like to compile
+themselves while others prefer the bureaucrat's dream Debian, it is
+impossible to give guidelines which will work for everyone. Just send
+some feedback to the mailing list at git@vger.kernel.org to get this
+document tailored to your favorite distro.
+
+
+What's needed:
+
+- Have an Apache web-server
+
+  On Debian:
+    $ apt-get install apache2
+    To get apache2 by default started,
+    edit /etc/default/apache2 and set NO_START=0
+
+- can edit the configuration of it.
+
+  This could be found under /etc/httpd, or refer to your Apache documentation.
+
+  On Debian: this means being able to edit files under /etc/apache2
+
+- can restart it.
+
+  'apachectl --graceful' might do. If it doesn't, just stop and
+  restart apache. Be warning that active connections to your server
+  might be aborted by this.
+
+  On Debian:
+    $ /etc/init.d/apache2 restart
+  or
+    $ /etc/init.d/apache2 force-reload
+    (which seems to do the same)
+  This adds symlinks from the /etc/apache2/mods-enabled to
+  /etc/apache2/mods-available.
+
+- have permissions to chown a directory
+
+- have git installed at the server _and_ client
+
+In effect, this probably means you're going to be root.
+
+
+Step 1: setup a bare GIT repository
+-----------------------------------
+
+At the time of writing, git-http-push cannot remotely create a GIT
+repository. So we have to do that at the server side with git. Another
+option would be to generate an empty repository at the client and copy
+it to the server with WebDAV. But then you're probably the first to
+try that out :)
+
+Create the directory under the DocumentRoot of the directories served
+by Apache. As an example we take /usr/local/apache2, but try "grep
+DocumentRoot /where/ever/httpd.conf" to find your root:
+
+    $ cd /usr/local/apache/htdocs
+    $ mkdir my-new-repo.git
+
+  On Debian:
+
+    $ cd /var/www
+    $ mkdir my-new-repo.git
+
+
+Initialize a bare repository
+
+    $ cd my-new-repo.git
+    $ git --bare init
+
+
+Change the ownership to your web-server's credentials. Use "grep ^User
+httpd.conf" and "grep ^Group httpd.conf" to find out:
+
+    $ chown -R www.www .
+
+  On Debian:
+
+    $ chown -R www-data.www-data .
+
+
+If you do not know which user Apache runs as, you can alternatively do
+a "chmod -R a+w .", inspect the files which are created later on, and
+set the permissions appropriately.
+
+Restart apache2, and check whether http://server/my-new-repo.git gives
+a directory listing. If not, check whether apache started up
+successfully.
+
+
+Step 2: enable DAV on this repository
+-------------------------------------
+
+First make sure the dav_module is loaded. For this, insert in httpd.conf:
+
+    LoadModule dav_module libexec/httpd/libdav.so
+    AddModule mod_dav.c
+
+Also make sure that this line exists which is the file used for
+locking DAV operations:
+
+  DAVLockDB "/usr/local/apache2/temp/DAV.lock"
+
+  On Debian these steps can be performed with:
+
+    Enable the dav and dav_fs modules of apache:
+    $ a2enmod dav_fs
+    (just to be sure. dav_fs might be unneeded, I don't know)
+    $ a2enmod dav
+    The DAV lock is located in /etc/apache2/mods-available/dav_fs.conf:
+      DAVLockDB /var/lock/apache2/DAVLock
+
+Of course, it can point somewhere else, but the string is actually just a
+prefix in some Apache configurations, and therefore the _directory_ has to
+be writable by the user Apache runs as.
+
+Then, add something like this to your httpd.conf
+
+  <Location /my-new-repo.git>
+     DAV on
+     AuthType Basic
+     AuthName "Git"
+     AuthUserFile /usr/local/apache2/conf/passwd.git
+     Require valid-user
+  </Location>
+
+  On Debian:
+    Create (or add to) /etc/apache2/conf.d/git.conf :
+
+    <Location /my-new-repo.git>
+       DAV on
+       AuthType Basic
+       AuthName "Git"
+       AuthUserFile /etc/apache2/passwd.git
+       Require valid-user
+    </Location>
+
+    Debian automatically reads all files under /etc/apach2/conf.d.
+
+The password file can be somewhere else, but it has to be readable by
+Apache and preferably not readable by the world.
+
+Create this file by
+    $ htpasswd -c /usr/local/apache2/conf/passwd.git <user>
+
+    On Debian:
+      $ htpasswd -c /etc/apache2/passwd.git <user>
+
+You will be asked a password, and the file is created. Subsequent calls
+to htpasswd should omit the '-c' option, since you want to append to the
+existing file.
+
+You need to restart Apache.
+
+Now go to http://<username>@<servername>/my-new-repo.git in your
+browser to check whether it asks for a password and accepts the right
+password.
+
+On Debian:
+
+   To test the WebDAV part, do:
+
+   $ apt-get install litmus
+   $ litmus http://<servername>/my-new-repo.git <username> <password>
+
+   Most tests should pass.
+
+A command line tool to test WebDAV is cadaver.
+
+If you're into Windows, from XP onwards Internet Explorer supports
+WebDAV. For this, do Internet Explorer -> Open Location ->
+http://<servername>/my-new-repo.git [x] Open as webfolder -> login .
+
+
+Step 3: setup the client
+------------------------
+
+Make sure that you have HTTP support, i.e. your git was built with curl.
+The easiest way to check is to look for the executable 'git-http-push'.
+
+Then, add the following to your $HOME/.netrc (you can do without, but will be
+asked to input your password a _lot_ of times):
+
+    machine <servername>
+    login <username>
+    password <password>
+
+...and set permissions:
+     chmod 600 ~/.netrc
+
+If you want to access the web-server by its IP, you have to type that in,
+instead of the server name.
+
+To check whether all is OK, do:
+
+   curl --netrc --location -v http://<username>@<servername>/my-new-repo.git/
+
+...this should give a directory listing in HTML of /var/www/my-new-repo.git .
+
+
+Now, add the remote in your existing repository which contains the project
+you want to export:
+
+   $ git-config remote.upload.url \
+       http://<username>@<servername>/my-new-repo.git/
+
+It is important to put the last '/'; Without it, the server will send
+a redirect which git-http-push does not (yet) understand, and git-http-push
+will repeat the request infinitely.
+
+
+Step 4: make the initial push
+-----------------------------
+
+From your client repository, do
+
+   $ git push upload master
+
+This pushes branch 'master' (which is assumed to be the branch you
+want to export) to repository called 'upload', which we previously
+defined with git-config.
+
+
+Troubleshooting:
+----------------
+
+If git-http-push says
+
+   Error: no DAV locking support on remote repo http://...
+
+then it means the web-server did not accept your authentication. Make sure
+that the user name and password matches in httpd.conf, .netrc and the URL
+you are uploading to.
+
+If git-http-push shows you an error (22/502) when trying to MOVE a blob,
+it means that your web-server somehow does not recognize its name in the
+request; This can happen when you start Apache, but then disable the
+network interface. A simple restart of Apache helps.
+
+Errors like (22/502) are of format (curl error code/http error
+code). So (22/404) means something like 'not found' at the server.
+
+Reading /usr/local/apache2/logs/error_log is often helpful.
+
+  On Debian: Read /var/log/apache2/error.log instead.
+
+
+Debian References: http://www.debian-administration.org/articles/285
+
+Authors
+  Johannes Schindelin <Johannes.Schindelin@gmx.de>
+  Rutger Nijlunsing <git@wingding.demon.nl>
diff --git a/Documentation/howto/update-hook-example.txt b/Documentation/howto/update-hook-example.txt
new file mode 100644 (file)
index 0000000..88765b5
--- /dev/null
@@ -0,0 +1,172 @@
+From: Junio C Hamano <junkio@cox.net> and Carl Baldwin <cnb@fc.hp.com>
+Subject: control access to branches.
+Date: Thu, 17 Nov 2005 23:55:32 -0800
+Message-ID: <7vfypumlu3.fsf@assigned-by-dhcp.cox.net>
+Abstract: An example hooks/update script is presented to
+ implement repository maintenance policies, such as who can push
+ into which branch and who can make a tag.
+
+When your developer runs git-push into the repository,
+git-receive-pack is run (either locally or over ssh) as that
+developer, so is hooks/update script.  Quoting from the relevant
+section of the documentation:
+
+    Before each ref is updated, if $GIT_DIR/hooks/update file exists
+    and executable, it is called with three parameters:
+
+           $GIT_DIR/hooks/update refname sha1-old sha1-new
+
+    The refname parameter is relative to $GIT_DIR; e.g. for the
+    master head this is "refs/heads/master".  Two sha1 are the
+    object names for the refname before and after the update.  Note
+    that the hook is called before the refname is updated, so either
+    sha1-old is 0{40} (meaning there is no such ref yet), or it
+    should match what is recorded in refname.
+
+So if your policy is (1) always require fast-forward push
+(i.e. never allow "git-push repo +branch:branch"), (2) you
+have a list of users allowed to update each branch, and (3) you
+do not let tags to be overwritten, then you can use something
+like this as your hooks/update script.
+
+[jc: editorial note.  This is a much improved version by Carl
+since I posted the original outline]
+
+-- >8 -- beginning of script -- >8 --
+
+#!/bin/bash
+
+umask 002
+
+# If you are having trouble with this access control hook script
+# you can try setting this to true.  It will tell you exactly
+# why a user is being allowed/denied access.
+
+verbose=false
+
+# Default shell globbing messes things up downstream
+GLOBIGNORE=*
+
+function grant {
+  $verbose && echo >&2 "-Grant-                $1"
+  echo grant
+  exit 0
+}
+
+function deny {
+  $verbose && echo >&2 "-Deny-         $1"
+  echo deny
+  exit 1
+}
+
+function info {
+  $verbose && echo >&2 "-Info-         $1"
+}
+
+# Implement generic branch and tag policies.
+# - Tags should not be updated once created.
+# - Branches should only be fast-forwarded.
+case "$1" in
+  refs/tags/*)
+    [ -f "$GIT_DIR/$1" ] &&
+    deny >/dev/null "You can't overwrite an existing tag"
+    ;;
+  refs/heads/*)
+    # No rebasing or rewinding
+    if expr "$2" : '0*$' >/dev/null; then
+      info "The branch '$1' is new..."
+    else
+      # updating -- make sure it is a fast forward
+      mb=$(git-merge-base "$2" "$3")
+      case "$mb,$2" in
+        "$2,$mb") info "Update is fast-forward" ;;
+        *)        deny >/dev/null  "This is not a fast-forward update." ;;
+      esac
+    fi
+    ;;
+  *)
+    deny >/dev/null \
+    "Branch is not under refs/heads or refs/tags.  What are you trying to do?"
+    ;;
+esac
+
+# Implement per-branch controls based on username
+allowed_users_file=$GIT_DIR/info/allowed-users
+username=$(id -u -n)
+info "The user is: '$username'"
+
+if [ -f "$allowed_users_file" ]; then
+  rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' |
+    while read head_pattern user_patterns; do
+      matchlen=$(expr "$1" : "$head_pattern")
+      if [ "$matchlen" == "${#1}" ]; then
+        info "Found matching head pattern: '$head_pattern'"
+        for user_pattern in $user_patterns; do
+          info "Checking user: '$username' against pattern: '$user_pattern'"
+          matchlen=$(expr "$username" : "$user_pattern")
+          if [ "$matchlen" == "${#username}" ]; then
+            grant "Allowing user: '$username' with pattern: '$user_pattern'"
+          fi
+        done
+        deny "The user is not in the access list for this branch"
+      fi
+    done
+  )
+  case "$rc" in
+    grant) grant >/dev/null "Granting access based on $allowed_users_file" ;;
+    deny)  deny  >/dev/null "Denying  access based on $allowed_users_file" ;;
+    *) ;;
+  esac
+fi
+
+allowed_groups_file=$GIT_DIR/info/allowed-groups
+groups=$(id -G -n)
+info "The user belongs to the following groups:"
+info "'$groups'"
+
+if [ -f "$allowed_groups_file" ]; then
+  rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' |
+    while read head_pattern group_patterns; do
+      matchlen=$(expr "$1" : "$head_pattern")
+      if [ "$matchlen" == "${#1}" ]; then
+        info "Found matching head pattern: '$head_pattern'"
+        for group_pattern in $group_patterns; do
+          for groupname in $groups; do
+            info "Checking group: '$groupname' against pattern: '$group_pattern'"
+            matchlen=$(expr "$groupname" : "$group_pattern")
+            if [ "$matchlen" == "${#groupname}" ]; then
+              grant "Allowing group: '$groupname' with pattern: '$group_pattern'"
+            fi
+          done
+        done
+        deny "None of the user's groups are in the access list for this branch"
+      fi
+    done
+  )
+  case "$rc" in
+    grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;;
+    deny)  deny  >/dev/null "Denying  access based on $allowed_groups_file" ;;
+    *) ;;
+  esac
+fi
+
+deny >/dev/null "There are no more rules to check.  Denying access"
+
+-- >8 -- end of script -- >8 --
+
+This uses two files, $GIT_DIR/info/allowed-users and
+allowed-groups, to describe which heads can be pushed into by
+whom.  The format of each file would look like this:
+
+        refs/heads/master      junio
+        refs/heads/cogito$     pasky
+        refs/heads/bw/.*       linus
+        refs/heads/tmp/.*      .*
+        refs/tags/v[0-9].*     junio
+
+With this, Linus can push or create "bw/penguin" or "bw/zebra"
+or "bw/panda" branches, Pasky can do only "cogito", and JC can
+do master branch and make versioned tags.  And anybody can do
+tmp/blah branches.
+
+------------
diff --git a/Documentation/howto/use-git-daemon.txt b/Documentation/howto/use-git-daemon.txt
new file mode 100644 (file)
index 0000000..4e2f75c
--- /dev/null
@@ -0,0 +1,51 @@
+How to use git-daemon
+
+Git can be run in inetd mode and in stand alone mode. But all you want is
+let a coworker pull from you, and therefore need to set up a git server
+real quick, right?
+
+Note that git-daemon is not really chatty at the moment, especially when
+things do not go according to plan (e.g. a socket could not be bound).
+
+Another word of warning: if you run
+
+       $ git ls-remote git://127.0.0.1/rule-the-world.git
+
+and you see a message like
+
+       fatal: The remote end hung up unexpectedly
+
+it only means that _something_ went wrong. To find out _what_ went wrong,
+you have to ask the server. (Git refuses to be more precise for your
+security only. Take off your shoes now. You have any coins in your pockets?
+Sorry, not allowed -- who knows what you planned to do with them?)
+
+With these two caveats, let's see an example:
+
+       $ git daemon --reuseaddr --verbose --base-path=/home/gitte/git \
+         --export-all -- /home/gitte/git/rule-the-world.git
+
+(Of course, unless your user name is `gitte` _and_ your repository is in
+~/rule-the-world.git, you have to adjust the paths. If your repository is
+not bare, be aware that you have to type the path to the .git directory!)
+
+This invocation tries to reuse the address if it is already taken
+(this can save you some debugging, because otherwise killing and restarting
+git-daemon could just silently fail to bind to a socket).
+
+Also, it is (relatively) verbose when somebody actually connects to it.
+It also sets the base path, which means that all the projects which can be
+accessed using this daemon have to reside in or under that path.
+
+The option `--export-all` just means that you _don't_ have to create a
+file named `git-daemon-export-ok` in each exported repository. (Otherwise,
+git-daemon would complain loudly, and refuse to cooperate.)
+
+Last of all, the repository which should be exported is specified. It is
+a good practice to put the paths after a "--" separator.
+
+Now, test your daemon with
+
+       $ git ls-remote git://127.0.0.1/rule-the-world.git
+
+If this does not work, find out why, and submit a patch to this document.
diff --git a/Documentation/i18n.txt b/Documentation/i18n.txt
new file mode 100644 (file)
index 0000000..b95f99b
--- /dev/null
@@ -0,0 +1,57 @@
+At the core level, git is character encoding agnostic.
+
+ - The pathnames recorded in the index and in the tree objects
+   are treated as uninterpreted sequences of non-NUL bytes.
+   What readdir(2) returns are what are recorded and compared
+   with the data git keeps track of, which in turn are expected
+   to be what lstat(2) and creat(2) accepts.  There is no such
+   thing as pathname encoding translation.
+
+ - The contents of the blob objects are uninterpreted sequence
+   of bytes.  There is no encoding translation at the core
+   level.
+
+ - The commit log messages are uninterpreted sequence of non-NUL
+   bytes.
+
+Although we encourage that the commit log messages are encoded
+in UTF-8, both the core and git Porcelain are designed not to
+force UTF-8 on projects.  If all participants of a particular
+project find it more convenient to use legacy encodings, git
+does not forbid it.  However, there are a few things to keep in
+mind.
+
+. `git-commit-tree` (hence, `git-commit` which uses it) issues
+  an warning if the commit log message given to it does not look
+  like a valid UTF-8 string, unless you explicitly say your
+  project uses a legacy encoding.  The way to say this is to
+  have i18n.commitencoding in `.git/config` file, like this:
++
+------------
+[i18n]
+       commitencoding = ISO-8859-1
+------------
++
+Commit objects created with the above setting record the value
+of `i18n.commitencoding` in its `encoding` header.  This is to
+help other people who look at them later.  Lack of this header
+implies that the commit log message is encoded in UTF-8.
+
+. `git-log`, `git-show` and friends looks at the `encoding`
+  header of a commit object, and tries to re-code the log
+  message into UTF-8 unless otherwise specified.  You can
+  specify the desired output encoding with
+  `i18n.logoutputencoding` in `.git/config` file, like this:
++
+------------
+[i18n]
+       logoutputencoding = ISO-8859-1
+------------
++
+If you do not have this configuration variable, the value of
+`i18n.commitencoding` is used instead.
+
+Note that we deliberately chose not to re-code the commit log
+message when a commit is made to force UTF-8 at the commit
+object level, because re-coding to UTF-8 is not necessarily a
+reversible operation.
diff --git a/Documentation/install-doc-quick.sh b/Documentation/install-doc-quick.sh
new file mode 100755 (executable)
index 0000000..5433cf8
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/sh
+# This requires a branch named in $head
+# (usually 'man' or 'html', provided by the git.git repository)
+set -e
+head="$1"
+mandir="$2"
+SUBDIRECTORY_OK=t
+USAGE='<refname> <target directory>'
+. git-sh-setup
+cd_to_toplevel
+
+test -z "$mandir" && usage
+if ! git rev-parse --verify "$head^0" >/dev/null; then
+       echo >&2 "head: $head does not exist in the current repository"
+       usage
+fi
+
+GIT_INDEX_FILE=`pwd`/.quick-doc.index
+export GIT_INDEX_FILE
+rm -f "$GIT_INDEX_FILE"
+trap 'rm -f "$GIT_INDEX_FILE"' 0
+
+git read-tree $head
+git checkout-index -a -f --prefix="$mandir"/
+
+if test -n "$GZ"; then
+       git ls-tree -r --name-only $head |
+       xargs printf "$mandir/%s\n" |
+       xargs gzip -f
+fi
+rm -f "$GIT_INDEX_FILE"
diff --git a/Documentation/install-webdoc.sh b/Documentation/install-webdoc.sh
new file mode 100755 (executable)
index 0000000..cd3a18e
--- /dev/null
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+T="$1"
+
+for h in *.html *.txt howto/*.txt howto/*.html RelNotes-*.txt *.css
+do
+       if test -f "$T/$h" &&
+          diff -u -I'Last updated [0-9][0-9]-[A-Z][a-z][a-z]-' "$T/$h" "$h"
+       then
+               :; # up to date
+       else
+               echo >&2 "# install $h $T/$h"
+               rm -f "$T/$h"
+               mkdir -p `dirname "$T/$h"`
+               cp "$h" "$T/$h"
+       fi
+done
+strip_leading=`echo "$T/" | sed -e 's|.|.|g'`
+for th in "$T"/*.html "$T"/*.txt "$T"/howto/*.txt "$T"/howto/*.html
+do
+       h=`expr "$th" : "$strip_leading"'\(.*\)'`
+       case "$h" in
+       index.html) continue ;;
+       esac
+       test -f "$h" && continue
+       echo >&2 "# rm -f $th"
+       rm -f "$th"
+done
+ln -sf git.html "$T/index.html"
diff --git a/Documentation/merge-options.txt b/Documentation/merge-options.txt
new file mode 100644 (file)
index 0000000..9f1fc82
--- /dev/null
@@ -0,0 +1,44 @@
+--summary::
+       Show a diffstat at the end of the merge. The diffstat is also
+       controlled by the configuration option merge.diffstat.
+
+-n, \--no-summary::
+       Do not show diffstat at the end of the merge.
+
+--no-commit::
+       Perform the merge but pretend the merge failed and do
+       not autocommit, to give the user a chance to inspect and
+       further tweak the merge result before committing.
+
+--commit::
+       Perform the merge and commit the result. This option can
+       be used to override --no-commit.
+
+--squash::
+       Produce the working tree and index state as if a real
+       merge happened, but do not actually make a commit or
+       move the `HEAD`, nor record `$GIT_DIR/MERGE_HEAD` to
+       cause the next `git commit` command to create a merge
+       commit.  This allows you to create a single commit on
+       top of the current branch whose effect is the same as
+       merging another branch (or more in case of an octopus).
+
+--no-squash::
+       Perform the merge and commit the result. This option can
+       be used to override --squash.
+
+--no-ff::
+       Generate a merge commit even if the merge resolved as a
+       fast-forward.
+
+--ff::
+       Do not generate a merge commit if the merge resolved as
+       a fast-forward, only update the branch pointer. This is
+       the default behavior of git-merge.
+
+-s <strategy>, \--strategy=<strategy>::
+       Use the given merge strategy; can be supplied more than
+       once to specify them in the order they should be tried.
+       If there is no `-s` option, a built-in list of strategies
+       is used instead (`git-merge-recursive` when merging a single
+       head, `git-merge-octopus` otherwise).
diff --git a/Documentation/merge-strategies.txt b/Documentation/merge-strategies.txt
new file mode 100644 (file)
index 0000000..7df0266
--- /dev/null
@@ -0,0 +1,35 @@
+MERGE STRATEGIES
+----------------
+
+resolve::
+       This can only resolve two heads (i.e. the current branch
+       and another branch you pulled from) using 3-way merge
+       algorithm.  It tries to carefully detect criss-cross
+       merge ambiguities and is considered generally safe and
+       fast.
+
+recursive::
+       This can only resolve two heads using 3-way merge
+       algorithm.  When there are more than one common
+       ancestors that can be used for 3-way merge, it creates a
+       merged tree of the common ancestors and uses that as
+       the reference tree for the 3-way merge.  This has been
+       reported to result in fewer merge conflicts without
+       causing mis-merges by tests done on actual merge commits
+       taken from Linux 2.6 kernel development history.
+       Additionally this can detect and handle merges involving
+       renames.  This is the default merge strategy when
+       pulling or merging one branch.
+
+octopus::
+       This resolves more than two-head case, but refuses to do
+       complex merge that needs manual resolution.  It is
+       primarily meant to be used for bundling topic branch
+       heads together.  This is the default merge strategy when
+       pulling or merging more than one branches.
+
+ours::
+       This resolves any number of heads, but the result of the
+       merge is always the current branch head.  It is meant to
+       be used to supersede old development history of side
+       branches.
diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt
new file mode 100644 (file)
index 0000000..0193c3c
--- /dev/null
@@ -0,0 +1,125 @@
+PRETTY FORMATS
+--------------
+
+If the commit is a merge, and if the pretty-format
+is not 'oneline', 'email' or 'raw', an additional line is
+inserted before the 'Author:' line.  This line begins with
+"Merge: " and the sha1s of ancestral commits are printed,
+separated by spaces.  Note that the listed commits may not
+necessarily be the list of the *direct* parent commits if you
+have limited your view of history: for example, if you are
+only interested in changes related to a certain directory or
+file.
+
+Here are some additional details for each format:
+
+* 'oneline'
+
+         <sha1> <title line>
++
+This is designed to be as compact as possible.
+
+* 'short'
+
+         commit <sha1>
+         Author: <author>
+
+             <title line>
+
+* 'medium'
+
+         commit <sha1>
+         Author: <author>
+         Date: <date>
+
+             <title line>
+
+             <full commit message>
+
+* 'full'
+
+         commit <sha1>
+         Author: <author>
+         Commit: <committer>
+
+             <title line>
+
+             <full commit message>
+
+* 'fuller'
+
+         commit <sha1>
+         Author: <author>
+         AuthorDate: <date & time>
+         Commit: <committer>
+         CommitDate: <date & time>
+
+              <title line>
+
+              <full commit message>
+
+* 'email'
+
+         From <sha1> <date>
+         From: <author>
+         Date: <date & time>
+         Subject: [PATCH] <title line>
+
+         <full commit message>
+
+* 'raw'
++
+The 'raw' format shows the entire commit exactly as
+stored in the commit object.  Notably, the SHA1s are
+displayed in full, regardless of whether --abbrev or
+--no-abbrev are used, and 'parents' information show the
+true parent commits, without taking grafts nor history
+simplification into account.
+
+* 'format:'
++
+The 'format:' format allows you to specify which information
+you want to show. It works a little bit like printf format,
+with the notable exception that you get a newline with '%n'
+instead of '\n'.
++
+E.g, 'format:"The author of %h was %an, %ar%nThe title was >>%s<<%n"'
+would show something like this:
++
+-------
+The author of fe6e0ee was Junio C Hamano, 23 hours ago
+The title was >>t4119: test autocomputing -p<n> for traditional diff input.<<
+
+--------
++
+The placeholders are:
+
+- '%H': commit hash
+- '%h': abbreviated commit hash
+- '%T': tree hash
+- '%t': abbreviated tree hash
+- '%P': parent hashes
+- '%p': abbreviated parent hashes
+- '%an': author name
+- '%ae': author email
+- '%ad': author date
+- '%aD': author date, RFC2822 style
+- '%ar': author date, relative
+- '%at': author date, UNIX timestamp
+- '%ai': author date, ISO 8601 format
+- '%cn': committer name
+- '%ce': committer email
+- '%cd': committer date
+- '%cD': committer date, RFC2822 style
+- '%cr': committer date, relative
+- '%ct': committer date, UNIX timestamp
+- '%ci': committer date, ISO 8601 format
+- '%e': encoding
+- '%s': subject
+- '%b': body
+- '%Cred': switch color to red
+- '%Cgreen': switch color to green
+- '%Cblue': switch color to blue
+- '%Creset': reset color
+- '%m': left, right or boundary mark
+- '%n': newline
diff --git a/Documentation/pretty-options.txt b/Documentation/pretty-options.txt
new file mode 100644 (file)
index 0000000..973d8dd
--- /dev/null
@@ -0,0 +1,22 @@
+--pretty[='<format>']::
+
+       Pretty-print the contents of the commit logs in a given format,
+       where '<format>' can be one of 'oneline', 'short', 'medium',
+       'full', 'fuller', 'email', 'raw' and 'format:<string>'.
+       When omitted, the format defaults to 'medium'.
+
+--abbrev-commit::
+       Instead of showing the full 40-byte hexadecimal commit object
+       name, show only handful hexdigits prefix.  Non default number of
+       digits can be specified with "--abbrev=<n>" (which also modifies
+       diff output, if it is displayed).
++
+This should make "--pretty=oneline" a whole lot more readable for
+people using 80-column terminals.
+
+--encoding[=<encoding>]::
+       The commit objects record the encoding used for the log message
+       in their encoding header; this option can be used to tell the
+       command to re-code the commit log message in the encoding
+       preferred by the user.  For non plumbing commands this
+       defaults to UTF-8.
diff --git a/Documentation/pull-fetch-param.txt b/Documentation/pull-fetch-param.txt
new file mode 100644 (file)
index 0000000..b6eb7fc
--- /dev/null
@@ -0,0 +1,65 @@
+<repository>::
+       The "remote" repository that is the source of a fetch
+       or pull operation.  See the section <<URLS,GIT URLS>> below.
+
+<refspec>::
+       The canonical format of a <refspec> parameter is
+       `+?<src>:<dst>`; that is, an optional plus `+`, followed
+       by the source ref, followed by a colon `:`, followed by
+       the destination ref.
++
+The remote ref that matches <src>
+is fetched, and if <dst> is not empty string, the local
+ref that matches it is fast forwarded using <src>.
+Again, if the optional plus `+` is used, the local ref
+is updated even if it does not result in a fast forward
+update.
++
+[NOTE]
+If the remote branch from which you want to pull is
+modified in non-linear ways such as being rewound and
+rebased frequently, then a pull will attempt a merge with
+an older version of itself, likely conflict, and fail.
+It is under these conditions that you would want to use
+the `+` sign to indicate non-fast-forward updates will
+be needed.  There is currently no easy way to determine
+or declare that a branch will be made available in a
+repository with this behavior; the pulling user simply
+must know this is the expected usage pattern for a branch.
++
+[NOTE]
+You never do your own development on branches that appear
+on the right hand side of a <refspec> colon on `Pull:` lines;
+they are to be updated by `git-fetch`.  If you intend to do
+development derived from a remote branch `B`, have a `Pull:`
+line to track it (i.e. `Pull: B:remote-B`), and have a separate
+branch `my-B` to do your development on top of it.  The latter
+is created by `git branch my-B remote-B` (or its equivalent `git
+checkout -b my-B remote-B`).  Run `git fetch` to keep track of
+the progress of the remote side, and when you see something new
+on the remote branch, merge it into your development branch with
+`git pull . remote-B`, while you are on `my-B` branch.
++
+[NOTE]
+There is a difference between listing multiple <refspec>
+directly on `git-pull` command line and having multiple
+`Pull:` <refspec> lines for a <repository> and running
+`git-pull` command without any explicit <refspec> parameters.
+<refspec> listed explicitly on the command line are always
+merged into the current branch after fetching.  In other words,
+if you list more than one remote refs, you would be making
+an Octopus.  While `git-pull` run without any explicit <refspec>
+parameter takes default <refspec>s from `Pull:` lines, it
+merges only the first <refspec> found into the current branch,
+after fetching all the remote refs.  This is because making an
+Octopus from remote refs is rarely done, while keeping track
+of multiple remote heads in one-go by fetching more than one
+is often useful.
++
+Some short-cut notations are also supported.
++
+* `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`;
+  it requests fetching everything up to the given tag.
+* A parameter <ref> without a colon is equivalent to
+  <ref>: when pulling/fetching, so it merges <ref> into the current
+  branch without storing the remote branch anywhere locally
diff --git a/Documentation/repository-layout.txt b/Documentation/repository-layout.txt
new file mode 100644 (file)
index 0000000..4c92e37
--- /dev/null
@@ -0,0 +1,179 @@
+git repository layout
+=====================
+
+You may find these things in your git repository (`.git`
+directory for a repository associated with your working tree, or
+`'project'.git` directory for a public 'bare' repository).
+
+objects::
+       Object store associated with this repository.  Usually
+       an object store is self sufficient (i.e. all the objects
+       that are referred to by an object found in it are also
+       found in it), but there are couple of ways to violate
+       it.
++
+. You could populate the repository by running a commit walker
+without `-a` option.  Depending on which options are given, you
+could have only commit objects without associated blobs and
+trees this way, for example.  A repository with this kind of
+incomplete object store is not suitable to be published to the
+outside world but sometimes useful for private repository.
+. You also could have an incomplete but locally usable repository
+by cloning shallowly.  See gitlink:git-clone[1].
+. You can be using `objects/info/alternates` mechanism, or
+`$GIT_ALTERNATE_OBJECT_DIRECTORIES` mechanism to 'borrow'
+objects from other object stores.  A repository with this kind
+of incomplete object store is not suitable to be published for
+use with dumb transports but otherwise is OK as long as
+`objects/info/alternates` points at the right object stores
+it borrows from.
+
+objects/[0-9a-f][0-9a-f]::
+       Traditionally, each object is stored in its own file.
+       They are split into 256 subdirectories using the first
+       two letters from its object name to keep the number of
+       directory entries `objects` directory itself needs to
+       hold.  Objects found here are often called 'unpacked'
+       (or 'loose') objects.
+
+objects/pack::
+       Packs (files that store many object in compressed form,
+       along with index files to allow them to be randomly
+       accessed) are found in this directory.
+
+objects/info::
+       Additional information about the object store is
+       recorded in this directory.
+
+objects/info/packs::
+       This file is to help dumb transports discover what packs
+       are available in this object store.  Whenever a pack is
+       added or removed, `git update-server-info` should be run
+       to keep this file up-to-date if the repository is
+       published for dumb transports.  `git repack` does this
+       by default.
+
+objects/info/alternates::
+       This file records paths to alternate object stores that
+       this object store borrows objects from, one pathname per
+       line. Note that not only native Git tools use it locally,
+       but the HTTP fetcher also tries to use it remotely; this
+       will usually work if you have relative paths (relative
+       to the object database, not to the repository!) in your
+       alternates file, but it will not work if you use absolute
+       paths unless the absolute path in filesystem and web URL
+       is the same. See also 'objects/info/http-alternates'.
+
+objects/info/http-alternates::
+       This file records URLs to alternate object stores that
+       this object store borrows objects from, to be used when
+       the repository is fetched over HTTP.
+
+refs::
+       References are stored in subdirectories of this
+       directory.  The `git prune` command knows to keep
+       objects reachable from refs found in this directory and
+       its subdirectories.
+
+refs/heads/`name`::
+       records tip-of-the-tree commit objects of branch `name`
+
+refs/tags/`name`::
+       records any object name (not necessarily a commit
+       object, or a tag object that points at a commit object).
+
+refs/remotes/`name`::
+       records tip-of-the-tree commit objects of branches copied
+       from a remote repository.
+
+packed-refs::
+       records the same information as refs/heads/, refs/tags/,
+       and friends record in a more efficient way.  See
+       gitlink:git-pack-refs[1].
+
+HEAD::
+       A symref (see glossary) to the `refs/heads/` namespace
+       describing the currently active branch.  It does not mean
+       much if the repository is not associated with any working tree
+       (i.e. a 'bare' repository), but a valid git repository
+       *must* have the HEAD file; some porcelains may use it to
+       guess the designated "default" branch of the repository
+       (usually 'master').  It is legal if the named branch
+       'name' does not (yet) exist.  In some legacy setups, it is
+       a symbolic link instead of a symref that points at the current
+       branch.
++
+HEAD can also record a specific commit directly, instead of
+being a symref to point at the current branch.  Such a state
+is often called 'detached HEAD', and almost all commands work
+identically as normal.  See gitlink:git-checkout[1] for
+details.
+
+branches::
+       A slightly deprecated way to store shorthands to be used
+       to specify URL to `git fetch`, `git pull` and `git push`
+       commands is to store a file in `branches/'name'` and
+       give 'name' to these commands in place of 'repository'
+       argument.
+
+hooks::
+       Hooks are customization scripts used by various git
+       commands.  A handful of sample hooks are installed when
+       `git init` is run, but all of them are disabled by
+       default.  To enable, they need to be made executable.
+       Read link:hooks.html[hooks] for more details about
+       each hook.
+
+index::
+       The current index file for the repository.  It is
+       usually not found in a bare repository.
+
+info::
+       Additional information about the repository is recorded
+       in this directory.
+
+info/refs::
+       This file helps dumb transports discover what refs are
+       available in this repository.  If the repository is
+       published for dumb transports, this file should be
+       regenerated by `git update-server-info` every time a tag
+       or branch is created or modified.  This is normally done
+       from the `hooks/update` hook, which is run by the
+       `git-receive-pack` command when you `git push` into the
+       repository.
+
+info/grafts::
+       This file records fake commit ancestry information, to
+       pretend the set of parents a commit has is different
+       from how the commit was actually created.  One record
+       per line describes a commit and its fake parents by
+       listing their 40-byte hexadecimal object names separated
+       by a space and terminated by a newline.
+
+info/exclude::
+       This file, by convention among Porcelains, stores the
+       exclude pattern list. `.gitignore` is the per-directory
+       ignore file.  `git status`, `git add`, `git rm` and `git
+       clean` look at it but the core git commands do not look
+       at it.  See also: gitlink:gitignore[5].
+
+remotes::
+       Stores shorthands to be used to give URL and default
+       refnames to interact with remote repository to `git
+       fetch`, `git pull` and `git push` commands.
+
+logs::
+       Records of changes made to refs are stored in this
+       directory.  See the documentation on git-update-ref
+       for more information.
+
+logs/refs/heads/`name`::
+       Records all changes made to the branch tip named `name`.
+
+logs/refs/tags/`name`::
+       Records all changes made to the tag named `name`.
+
+shallow::
+       This is similar to `info/grafts` but is internally used
+       and maintained by shallow clone mechanism.  See `--depth`
+       option to gitlink:git-clone[1] and gitlink:git-fetch[1].
diff --git a/Documentation/technical/pack-format.txt b/Documentation/technical/pack-format.txt
new file mode 100644 (file)
index 0000000..e5b31c8
--- /dev/null
@@ -0,0 +1,118 @@
+GIT pack format
+===============
+
+= pack-*.pack file has the following format:
+
+   - The header appears at the beginning and consists of the following:
+
+     4-byte signature:
+         The signature is: {'P', 'A', 'C', 'K'}
+
+     4-byte version number (network byte order):
+         GIT currently accepts version number 2 or 3 but
+         generates version 2 only.
+
+     4-byte number of objects contained in the pack (network byte order)
+
+     Observation: we cannot have more than 4G versions ;-) and
+     more than 4G objects in a pack.
+
+   - The header is followed by number of object entries, each of
+     which looks like this:
+
+     (undeltified representation)
+     n-byte type and length (3-bit type, (n-1)*7+4-bit length)
+     compressed data
+
+     (deltified representation)
+     n-byte type and length (3-bit type, (n-1)*7+4-bit length)
+     20-byte base object name
+     compressed delta data
+
+     Observation: length of each object is encoded in a variable
+     length format and is not constrained to 32-bit or anything.
+
+  - The trailer records 20-byte SHA1 checksum of all of the above.
+
+= pack-*.idx file has the following format:
+
+  - The header consists of 256 4-byte network byte order
+    integers.  N-th entry of this table records the number of
+    objects in the corresponding pack, the first byte of whose
+    object name are smaller than N.  This is called the
+    'first-level fan-out' table.
+
+    Observation: we would need to extend this to an array of
+    8-byte integers to go beyond 4G objects per pack, but it is
+    not strictly necessary.
+
+  - The header is followed by sorted 24-byte entries, one entry
+    per object in the pack.  Each entry is:
+
+    4-byte network byte order integer, recording where the
+    object is stored in the packfile as the offset from the
+    beginning.
+
+    20-byte object name.
+
+    Observation: we would definitely need to extend this to
+    8-byte integer plus 20-byte object name to handle a packfile
+    that is larger than 4GB.
+
+  - The file is concluded with a trailer:
+
+    A copy of the 20-byte SHA1 checksum at the end of
+    corresponding packfile.
+
+    20-byte SHA1-checksum of all of the above.
+
+Pack Idx file:
+
+       idx
+           +--------------------------------+
+           | fanout[0] = 2                  |-.
+           +--------------------------------+ |
+           | fanout[1]                      | |
+           +--------------------------------+ |
+           | fanout[2]                      | |
+           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
+           | fanout[255]                    | |
+           +--------------------------------+ |
+main       | offset                         | |
+index      | object name 00XXXXXXXXXXXXXXXX | |
+table      +--------------------------------+ |
+           | offset                         | |
+           | object name 00XXXXXXXXXXXXXXXX | |
+           +--------------------------------+ |
+         .-| offset                         |<+
+         | | object name 01XXXXXXXXXXXXXXXX |
+         | +--------------------------------+
+         | | offset                         |
+         | | object name 01XXXXXXXXXXXXXXXX |
+         | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+         | | offset                         |
+         | | object name FFXXXXXXXXXXXXXXXX |
+         | +--------------------------------+
+trailer          | | packfile checksum              |
+         | +--------------------------------+
+         | | idxfile checksum               |
+         | +--------------------------------+
+          .-------.
+                  |
+Pack file entry: <+
+
+     packed object header:
+       1-byte size extension bit (MSB)
+              type (next 3 bit)
+              size0 (lower 4-bit)
+        n-byte sizeN (as long as MSB is set, each 7-bit)
+               size0..sizeN form 4+7+7+..+7 bit integer, size0
+               is the least significant part, and sizeN is the
+               most significant part.
+     packed object data:
+        If it is not DELTA, then deflated bytes (the size above
+               is the size before compression).
+       If it is DELTA, then
+         20-byte base object name SHA1 (the size above is the
+               size of the delta data that follows).
+          delta data, deflated.
diff --git a/Documentation/technical/pack-heuristics.txt b/Documentation/technical/pack-heuristics.txt
new file mode 100644 (file)
index 0000000..103eb5d
--- /dev/null
@@ -0,0 +1,466 @@
+        Concerning Git's Packing Heuristics
+        ===================================
+
+        Oh, here's a really stupid question:
+
+                  Where do I go
+               to learn the details
+           of git's packing heuristics?
+
+Be careful what you ask!
+
+Followers of the git, please open the git IRC Log and turn to
+February 10, 2006.
+
+It's a rare occasion, and we are joined by the King Git Himself,
+Linus Torvalds (linus).  Nathaniel Smith, (njs`), has the floor
+and seeks enlightenment.  Others are present, but silent.
+
+Let's listen in!
+
+    <njs`> Oh, here's a really stupid question -- where do I go to
+        learn the details of git's packing heuristics?  google avails
+        me not, reading the source didn't help a lot, and wading
+        through the whole mailing list seems less efficient than any
+        of that.
+
+It is a bold start!  A plea for help combined with a simultaneous
+tri-part attack on some of the tried and true mainstays in the quest
+for enlightenment.  Brash accusations of google being useless. Hubris!
+Maligning the source.  Heresy!  Disdain for the mailing list archives.
+Woe.
+
+    <pasky> yes, the packing-related delta stuff is somewhat
+        mysterious even for me ;)
+
+Ah!  Modesty after all.
+
+    <linus> njs, I don't think the docs exist. That's something where
+        I don't think anybody else than me even really got involved.
+        Most of the rest of git others have been busy with (especially
+        Junio), but packing nobody touched after I did it.
+
+It's cryptic, yet vague.  Linus in style for sure.  Wise men
+interpret this as an apology.  A few argue it is merely a
+statement of fact.
+
+    <njs`> I guess the next step is "read the source again", but I
+        have to build up a certain level of gumption first :-)
+
+Indeed!  On both points.
+
+    <linus> The packing heuristic is actually really really simple.
+
+Bait...
+
+    <linus> But strange.
+
+And switch.  That ought to do it!
+
+    <linus> Remember: git really doesn't follow files. So what it does is
+        - generate a list of all objects
+        - sort the list according to magic heuristics
+        - walk the list, using a sliding window, seeing if an object
+          can be diffed against another object in the window
+        - write out the list in recency order
+
+The traditional understatement:
+
+    <njs`> I suspect that what I'm missing is the precise definition of
+        the word "magic"
+
+The traditional insight:
+
+    <pasky> yes
+
+And Babel-like confusion flowed.
+
+    <njs`> oh, hmm, and I'm not sure what this sliding window means either
+
+    <pasky> iirc, it appeared to me to be just the sha1 of the object
+        when reading the code casually ...
+
+        ... which simply doesn't sound as a very good heuristics, though ;)
+
+    <njs`> .....and recency order.  okay, I think it's clear I didn't
+       even realize how much I wasn't realizing :-)
+
+Ah, grasshopper!  And thus the enlightenment begins anew.
+
+    <linus> The "magic" is actually in theory totally arbitrary.
+        ANY order will give you a working pack, but no, it's not
+        ordered by SHA1.
+
+        Before talking about the ordering for the sliding delta
+        window, let's talk about the recency order. That's more
+        important in one way.
+
+    <njs`> Right, but if all you want is a working way to pack things
+        together, you could just use cat and save yourself some
+        trouble...
+
+Waaait for it....
+
+    <linus> The recency ordering (which is basically: put objects
+        _physically_ into the pack in the order that they are
+        "reachable" from the head) is important.
+
+    <njs`> okay
+
+    <linus> It's important because that's the thing that gives packs
+        good locality. It keeps the objects close to the head (whether
+        they are old or new, but they are _reachable_ from the head)
+        at the head of the pack. So packs actually have absolutely
+        _wonderful_ IO patterns.
+
+Read that again, because it is important.
+
+    <linus> But recency ordering is totally useless for deciding how
+        to actually generate the deltas, so the delta ordering is
+        something else.
+
+        The delta ordering is (wait for it):
+        - first sort by the "basename" of the object, as defined by
+          the name the object was _first_ reached through when
+          generating the object list
+        - within the same basename, sort by size of the object
+        - but always sort different types separately (commits first).
+
+        That's not exactly it, but it's very close.
+
+    <njs`> The "_first_ reached" thing is not too important, just you
+        need some way to break ties since the same objects may be
+        reachable many ways, yes?
+
+And as if to clarify:
+
+    <linus> The point is that it's all really just any random
+        heuristic, and the ordering is totally unimportant for
+        correctness, but it helps a lot if the heuristic gives
+        "clumping" for things that are likely to delta well against
+        each other.
+
+It is an important point, so secretly, I did my own research and have
+included my results below.  To be fair, it has changed some over time.
+And through the magic of Revisionistic History, I draw upon this entry
+from The Git IRC Logs on my father's birthday, March 1:
+
+    <gitster> The quote from the above linus should be rewritten a
+        bit (wait for it):
+        - first sort by type.  Different objects never delta with
+         each other.
+        - then sort by filename/dirname.  hash of the basename
+          occupies the top BITS_PER_INT-DIR_BITS bits, and bottom
+          DIR_BITS are for the hash of leading path elements.
+        - then if we are doing "thin" pack, the objects we are _not_
+          going to pack but we know about are sorted earlier than
+          other objects.
+        - and finally sort by size, larger to smaller.
+
+In one swell-foop, clarification and obscurification!  Nonetheless,
+authoritative.  Cryptic, yet concise.  It even solicits notions of
+quotes from The Source Code.  Clearly, more study is needed.
+
+    <gitster> That's the sort order.  What this means is:
+        - we do not delta different object types.
+       - we prefer to delta the objects with the same full path, but
+          allow files with the same name from different directories.
+       - we always prefer to delta against objects we are not going
+          to send, if there are some.
+       - we prefer to delta against larger objects, so that we have
+          lots of removals.
+
+        The penultimate rule is for "thin" packs.  It is used when
+        the other side is known to have such objects.
+
+There it is again. "Thin" packs.  I'm thinking to myself, "What
+is a 'thin' pack?"  So I ask:
+
+    <jdl> What is a "thin" pack?
+
+    <gitster> Use of --objects-edge to rev-list as the upstream of
+        pack-objects.  The pack transfer protocol negotiates that.
+
+Woo hoo!  Cleared that _right_ up!
+
+    <gitster> There are two directions - push and fetch.
+
+There!  Did you see it?  It is not '"push" and "pull"'!  How often the
+confusion has started here.  So casually mentioned, too!
+
+    <gitster> For push, git-send-pack invokes git-receive-pack on the
+        other end.  The receive-pack says "I have up to these commits".
+        send-pack looks at them, and computes what are missing from
+        the other end.  So "thin" could be the default there.
+
+        In the other direction, fetch, git-fetch-pack and
+        git-clone-pack invokes git-upload-pack on the other end
+       (via ssh or by talking to the daemon).
+
+       There are two cases: fetch-pack with -k and clone-pack is one,
+        fetch-pack without -k is the other.  clone-pack and fetch-pack
+        with -k will keep the downloaded packfile without expanded, so
+        we do not use thin pack transfer.  Otherwise, the generated
+        pack will have delta without base object in the same pack.
+
+        But fetch-pack without -k will explode the received pack into
+        individual objects, so we automatically ask upload-pack to
+        give us a thin pack if upload-pack supports it.
+
+OK then.
+
+Uh.
+
+Let's return to the previous conversation still in progress.
+
+    <njs`> and "basename" means something like "the tail of end of
+        path of file objects and dir objects, as per basename(3), and
+        we just declare all commit and tag objects to have the same
+        basename" or something?
+
+Luckily, that too is a point that gitster clarified for us!
+
+If I might add, the trick is to make files that _might_ be similar be
+located close to each other in the hash buckets based on their file
+names.  It used to be that "foo/Makefile", "bar/baz/quux/Makefile" and
+"Makefile" all landed in the same bucket due to their common basename,
+"Makefile". However, now they land in "close" buckets.
+
+The algorithm allows not just for the _same_ bucket, but for _close_
+buckets to be considered delta candidates.  The rationale is
+essentially that files, like Makefiles, often have very similar
+content no matter what directory they live in.
+
+    <linus> I played around with different delta algorithms, and with
+        making the "delta window" bigger, but having too big of a
+        sliding window makes it very expensive to generate the pack:
+        you need to compare every object with a _ton_ of other objects.
+
+        There are a number of other trivial heuristics too, which
+        basically boil down to "don't bother even trying to delta this
+        pair" if we can tell before-hand that the delta isn't worth it
+        (due to size differences, where we can take a previous delta
+        result into account to decide that "ok, no point in trying
+        that one, it will be worse").
+
+        End result: packing is actually very size efficient. It's
+        somewhat CPU-wasteful, but on the other hand, since you're
+        really only supposed to do it maybe once a month (and you can
+        do it during the night), nobody really seems to care.
+
+Nice Engineering Touch, there.  Find when it doesn't matter, and
+proclaim it a non-issue.  Good style too!
+
+    <njs`> So, just to repeat to see if I'm following, we start by
+        getting a list of the objects we want to pack, we sort it by
+        this heuristic (basically lexicographically on the tuple
+        (type, basename, size)).
+
+        Then we walk through this list, and calculate a delta of
+        each object against the last n (tunable parameter) objects,
+        and pick the smallest of these deltas.
+
+Vastly simplified, but the essence is there!
+
+    <linus> Correct.
+
+    <njs`> And then once we have picked a delta or fulltext to
+        represent each object, we re-sort by recency, and write them
+        out in that order.
+
+    <linus> Yup. Some other small details:
+
+And of course there is the "Other Shoe" Factor too.
+
+    <linus> - We limit the delta depth to another magic value (right
+        now both the window and delta depth magic values are just "10")
+
+    <njs`> Hrm, my intuition is that you'd end up with really _bad_ IO
+        patterns, because the things you want are near by, but to
+        actually reconstruct them you may have to jump all over in
+        random ways.
+
+    <linus> - When we write out a delta, and we haven't yet written
+        out the object it is a delta against, we write out the base
+        object first.  And no, when we reconstruct them, we actually
+        get nice IO patterns, because:
+        - larger objects tend to be "more recent" (Linus' law: files grow)
+        - we actively try to generate deltas from a larger object to a
+          smaller one
+        - this means that the top-of-tree very seldom has deltas
+          (i.e. deltas in _practice_ are "backwards deltas")
+
+Again, we should reread that whole paragraph.  Not just because
+Linus has slipped Linus's Law in there on us, but because it is
+important.  Let's make sure we clarify some of the points here:
+
+    <njs`> So the point is just that in practice, delta order and
+        recency order match each other quite well.
+
+    <linus> Yes. There's another nice side to this (and yes, it was
+       designed that way ;):
+        - the reason we generate deltas against the larger object is
+         actually a big space saver too!
+
+    <njs`> Hmm, but your last comment (if "we haven't yet written out
+        the object it is a delta against, we write out the base object
+        first"), seems like it would make these facts mostly
+        irrelevant because even if in practice you would not have to
+        wander around much, in fact you just brute-force say that in
+        the cases where you might have to wander, don't do that :-)
+
+    <linus> Yes and no. Notice the rule: we only write out the base
+        object first if the delta against it was more recent.  That
+        means that you can actually have deltas that refer to a base
+        object that is _not_ close to the delta object, but that only
+        happens when the delta is needed to generate an _old_ object.
+
+    <linus> See?
+
+Yeah, no.  I missed that on the first two or three readings myself.
+
+    <linus> This keeps the front of the pack dense. The front of the
+        pack never contains data that isn't relevant to a "recent"
+        object.  The size optimization comes from our use of xdelta
+        (but is true for many other delta algorithms): removing data
+        is cheaper (in size) than adding data.
+
+        When you remove data, you only need to say "copy bytes n--m".
+       In contrast, in a delta that _adds_ data, you have to say "add
+        these bytes: 'actual data goes here'"
+
+    *** njs` has quit: Read error: 104 (Connection reset by peer)
+
+    <linus> Uhhuh. I hope I didn't blow njs` mind.
+
+    *** njs` has joined channel #git
+
+    <pasky> :)
+
+The silent observers are amused.  Of course.
+
+And as if njs` was expected to be omniscient:
+
+    <linus> njs - did you miss anything?
+
+OK, I'll spell it out.  That's Geek Humor.  If njs` was not actually
+connected for a little bit there, how would he know if missed anything
+while he was disconnected?  He's a benevolent dictator with a sense of
+humor!  Well noted!
+
+    <njs`> Stupid router.  Or gremlins, or whatever.
+
+It's a cheap shot at Cisco.  Take 'em when you can.
+
+    <njs`> Yes and no. Notice the rule: we only write out the base
+        object first if the delta against it was more recent.
+
+        I'm getting lost in all these orders, let me re-read :-)
+       So the write-out order is from most recent to least recent?
+        (Conceivably it could be the opposite way too, I'm not sure if
+        we've said) though my connection back at home is logging, so I
+        can just read what you said there :-)
+
+And for those of you paying attention, the Omniscient Trick has just
+been detailed!
+
+    <linus> Yes, we always write out most recent first
+
+For the other record:
+
+    <pasky> njs`: http://pastebin.com/547965
+
+The 'net never forgets, so that should be good until the end of time.
+
+    <njs`> And, yeah, I got the part about deeper-in-history stuff
+        having worse IO characteristics, one sort of doesn't care.
+
+    <linus> With the caveat that if the "most recent" needs an older
+        object to delta against (hey, shrinking sometimes does
+        happen), we write out the old object with the delta.
+
+    <njs`> (if only it happened more...)
+
+    <linus> Anyway, the pack-file could easily be denser still, but
+        because it's used both for streaming (the git protocol) and
+        for on-disk, it has a few pessimizations.
+
+Actually, it is a made-up word. But it is a made-up word being
+used as setup for a later optimization, which is a real word:
+
+    <linus> In particular, while the pack-file is then compressed,
+        it's compressed just one object at a time, so the actual
+        compression factor is less than it could be in theory. But it
+        means that it's all nice random-access with a simple index to
+        do "object name->location in packfile" translation.
+
+    <njs`> I'm assuming the real win for delta-ing large->small is
+        more homogeneous statistics for gzip to run over?
+
+        (You have to put the bytes in one place or another, but
+        putting them in a larger blob wins on compression)
+
+        Actually, what is the compression strategy -- each delta
+        individually gzipped, the whole file gzipped, somewhere in
+        between, no compression at all, ....?
+
+        Right.
+
+Reality IRC sets in.  For example:
+
+    <pasky> I'll read the rest in the morning, I really have to go
+        sleep or there's no hope whatsoever for me at the today's
+        exam... g'nite all.
+
+Heh.
+
+    <linus> pasky: g'nite
+
+    <njs`> pasky: 'luck
+
+    <linus> Right: large->small matters exactly because of compression
+        behaviour. If it was non-compressed, it probably wouldn't make
+        any difference.
+
+    <njs`> yeah
+
+    <linus> Anyway: I'm not even trying to claim that the pack-files
+        are perfect, but they do tend to have a nice balance of
+        density vs ease-of use.
+
+Gasp!  OK, saved.  That's a fair Engineering trade off.  Close call!
+In fact, Linus reflects on some Basic Engineering Fundamentals,
+design options, etc.
+
+    <linus> More importantly, they allow git to still _conceptually_
+        never deal with deltas at all, and be a "whole object" store.
+
+        Which has some problems (we discussed bad huge-file
+        behaviour on the git lists the other day), but it does mean
+        that the basic git concepts are really really simple and
+        straightforward.
+
+        It's all been quite stable.
+
+        Which I think is very much a result of having very simple
+        basic ideas, so that there's never any confusion about what's
+        going on.
+
+        Bugs happen, but they are "simple" bugs. And bugs that
+        actually get some object store detail wrong are almost always
+        so obvious that they never go anywhere.
+
+    <njs`> Yeah.
+
+Nuff said.
+
+    <linus> Anyway.  I'm off for bed. It's not 6AM here, but I've got
+        three kids, and have to get up early in the morning to send
+        them off. I need my beauty sleep.
+
+    <njs`> :-)
+
+    <njs`> appreciate the infodump, I really was failing to find the
+        details on git packs :-)
+
+And now you know the rest of the story.
diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt
new file mode 100644 (file)
index 0000000..9cd48b4
--- /dev/null
@@ -0,0 +1,41 @@
+Pack transfer protocols
+=======================
+
+There are two Pack push-pull protocols.
+
+upload-pack (S) | fetch/clone-pack (C) protocol:
+
+       # Tell the puller what commits we have and what their names are
+       S: SHA1 name
+       S: ...
+       S: SHA1 name
+       S: # flush -- it's your turn
+       # Tell the pusher what commits we want, and what we have
+       C: want name
+       C: ..
+       C: want name
+       C: have SHA1
+       C: have SHA1
+       C: ...
+       C: # flush -- occasionally ask "had enough?"
+       S: NAK
+       C: have SHA1
+       C: ...
+       C: have SHA1
+       S: ACK
+       C: done
+       S: XXXXXXX -- packfile contents.
+
+send-pack | receive-pack protocol.
+
+       # Tell the pusher what commits we have and what their names are
+       C: SHA1 name
+       C: ...
+       C: SHA1 name
+       C: # flush -- it's your turn
+       # Tell the puller what the pusher has
+       S: old-SHA1 new-SHA1 name
+       S: old-SHA1 new-SHA1 name
+       S: ...
+       S: # flush -- done with the list
+       S: XXXXXXX --- packfile contents.
diff --git a/Documentation/technical/racy-git.txt b/Documentation/technical/racy-git.txt
new file mode 100644 (file)
index 0000000..5030d9f
--- /dev/null
@@ -0,0 +1,195 @@
+Use of index and Racy git problem
+=================================
+
+Background
+----------
+
+The index is one of the most important data structures in git.
+It represents a virtual working tree state by recording list of
+paths and their object names and serves as a staging area to
+write out the next tree object to be committed.  The state is
+"virtual" in the sense that it does not necessarily have to, and
+often does not, match the files in the working tree.
+
+There are cases git needs to examine the differences between the
+virtual working tree state in the index and the files in the
+working tree.  The most obvious case is when the user asks `git
+diff` (or its low level implementation, `git diff-files`) or
+`git-ls-files --modified`.  In addition, git internally checks
+if the files in the working tree are different from what are
+recorded in the index to avoid stomping on local changes in them
+during patch application, switching branches, and merging.
+
+In order to speed up this comparison between the files in the
+working tree and the index entries, the index entries record the
+information obtained from the filesystem via `lstat(2)` system
+call when they were last updated.  When checking if they differ,
+git first runs `lstat(2)` on the files and compares the result
+with this information (this is what was originally done by the
+`ce_match_stat()` function, but the current code does it in
+`ce_match_stat_basic()` function).  If some of these "cached
+stat information" fields do not match, git can tell that the
+files are modified without even looking at their contents.
+
+Note: not all members in `struct stat` obtained via `lstat(2)`
+are used for this comparison.  For example, `st_atime` obviously
+is not useful.  Currently, git compares the file type (regular
+files vs symbolic links) and executable bits (only for regular
+files) from `st_mode` member, `st_mtime` and `st_ctime`
+timestamps, `st_uid`, `st_gid`, `st_ino`, and `st_size` members.
+With a `USE_STDEV` compile-time option, `st_dev` is also
+compared, but this is not enabled by default because this member
+is not stable on network filesystems.  With `USE_NSEC`
+compile-time option, `st_mtim.tv_nsec` and `st_ctim.tv_nsec`
+members are also compared, but this is not enabled by default
+because the value of this member becomes meaningless once the
+inode is evicted from the inode cache on filesystems that do not
+store it on disk.
+
+
+Racy git
+--------
+
+There is one slight problem with the optimization based on the
+cached stat information.  Consider this sequence:
+
+  : modify 'foo'
+  $ git update-index 'foo'
+  : modify 'foo' again, in-place, without changing its size
+
+The first `update-index` computes the object name of the
+contents of file `foo` and updates the index entry for `foo`
+along with the `struct stat` information.  If the modification
+that follows it happens very fast so that the file's `st_mtime`
+timestamp does not change, after this sequence, the cached stat
+information the index entry records still exactly match what you
+would see in the filesystem, even though the file `foo` is now
+different.
+This way, git can incorrectly think files in the working tree
+are unmodified even though they actually are.  This is called
+the "racy git" problem (discovered by Pasky), and the entries
+that appear clean when they may not be because of this problem
+are called "racily clean".
+
+To avoid this problem, git does two things:
+
+. When the cached stat information says the file has not been
+  modified, and the `st_mtime` is the same as (or newer than)
+  the timestamp of the index file itself (which is the time `git
+  update-index foo` finished running in the above example), it
+  also compares the contents with the object registered in the
+  index entry to make sure they match.
+
+. When the index file is updated that contains racily clean
+  entries, cached `st_size` information is truncated to zero
+  before writing a new version of the index file.
+
+Because the index file itself is written after collecting all
+the stat information from updated paths, `st_mtime` timestamp of
+it is usually the same as or newer than any of the paths the
+index contains.  And no matter how quick the modification that
+follows `git update-index foo` finishes, the resulting
+`st_mtime` timestamp on `foo` cannot get a value earlier
+than the index file.  Therefore, index entries that can be
+racily clean are limited to the ones that have the same
+timestamp as the index file itself.
+
+The callers that want to check if an index entry matches the
+corresponding file in the working tree continue to call
+`ce_match_stat()`, but with this change, `ce_match_stat()` uses
+`ce_modified_check_fs()` to see if racily clean ones are
+actually clean after comparing the cached stat information using
+`ce_match_stat_basic()`.
+
+The problem the latter solves is this sequence:
+
+  $ git update-index 'foo'
+  : modify 'foo' in-place without changing its size
+  : wait for enough time
+  $ git update-index 'bar'
+
+Without the latter, the timestamp of the index file gets a newer
+value, and falsely clean entry `foo` would not be caught by the
+timestamp comparison check done with the former logic anymore.
+The latter makes sure that the cached stat information for `foo`
+would never match with the file in the working tree, so later
+checks by `ce_match_stat_basic()` would report that the index entry
+does not match the file and git does not have to fall back on more
+expensive `ce_modified_check_fs()`.
+
+
+Runtime penalty
+---------------
+
+The runtime penalty of falling back to `ce_modified_check_fs()`
+from `ce_match_stat()` can be very expensive when there are many
+racily clean entries.  An obvious way to artificially create
+this situation is to give the same timestamp to all the files in
+the working tree in a large project, run `git update-index` on
+them, and give the same timestamp to the index file:
+
+  $ date >.datestamp
+  $ git ls-files | xargs touch -r .datestamp
+  $ git ls-files | git update-index --stdin
+  $ touch -r .datestamp .git/index
+
+This will make all index entries racily clean.  The linux-2.6
+project, for example, there are over 20,000 files in the working
+tree.  On my Athron 64X2 3800+, after the above:
+
+  $ /usr/bin/time git diff-files
+  1.68user 0.54system 0:02.22elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k
+  0inputs+0outputs (0major+67111minor)pagefaults 0swaps
+  $ git update-index MAINTAINERS
+  $ /usr/bin/time git diff-files
+  0.02user 0.12system 0:00.14elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k
+  0inputs+0outputs (0major+935minor)pagefaults 0swaps
+
+Running `git update-index` in the middle checked the racily
+clean entries, and left the cached `st_mtime` for all the paths
+intact because they were actually clean (so this step took about
+the same amount of time as the first `git diff-files`).  After
+that, they are not racily clean anymore but are truly clean, so
+the second invocation of `git diff-files` fully took advantage
+of the cached stat information.
+
+
+Avoiding runtime penalty
+------------------------
+
+In order to avoid the above runtime penalty, post 1.4.2 git used
+to have a code that made sure the index file
+got timestamp newer than the youngest files in the index when
+there are many young files with the same timestamp as the
+resulting index file would otherwise would have by waiting
+before finishing writing the index file out.
+
+I suspected that in practice the situation where many paths in the
+index are all racily clean was quite rare.  The only code paths
+that can record recent timestamp for large number of paths are:
+
+. Initial `git add .` of a large project.
+
+. `git checkout` of a large project from an empty index into an
+  unpopulated working tree.
+
+Note: switching branches with `git checkout` keeps the cached
+stat information of existing working tree files that are the
+same between the current branch and the new branch, which are
+all older than the resulting index file, and they will not
+become racily clean.  Only the files that are actually checked
+out can become racily clean.
+
+In a large project where raciness avoidance cost really matters,
+however, the initial computation of all object names in the
+index takes more than one second, and the index file is written
+out after all that happens.  Therefore the timestamp of the
+index file will be more than one seconds later than the the
+youngest file in the working tree.  This means that in these
+cases there actually will not be any racily clean entry in
+the resulting index.
+
+Based on this discussion, the current code does not use the
+"workaround" to avoid the runtime penalty that does not exist in
+practice anymore.  This was done with commit 0fc82cff on Aug 15,
+2006.
diff --git a/Documentation/technical/send-pack-pipeline.txt b/Documentation/technical/send-pack-pipeline.txt
new file mode 100644 (file)
index 0000000..681efe4
--- /dev/null
@@ -0,0 +1,63 @@
+git-send-pack
+=============
+
+Overall operation
+-----------------
+
+. Connects to the remote side and invokes git-receive-pack.
+
+. Learns what refs the remote has and what commit they point at.
+  Matches them to the refspecs we are pushing.
+
+. Checks if there are non-fast-forwards.  Unlike fetch-pack,
+  the repository send-pack runs in is supposed to be a superset
+  of the recipient in fast-forward cases, so there is no need
+  for want/have exchanges, and fast-forward check can be done
+  locally.  Tell the result to the other end.
+
+. Calls pack_objects() which generates a packfile and sends it
+  over to the other end.
+
+. If the remote side is new enough (v1.1.0 or later), wait for
+  the unpack and hook status from the other end.
+
+. Exit with appropriate error codes.
+
+
+Pack_objects pipeline
+---------------------
+
+This function gets one file descriptor (`fd`) which is either a
+socket (over the network) or a pipe (local).  What's written to
+this fd goes to git-receive-pack to be unpacked.
+
+    send-pack ---> fd ---> receive-pack
+
+The function pack_objects creates a pipe and then forks.  The
+forked child execs pack-objects with --revs to receive revision
+parameters from its standard input. This process will write the
+packfile to the other end.
+
+    send-pack
+       |
+       pack_objects() ---> fd ---> receive-pack
+          | ^ (pipe)
+         v |
+         (child)
+
+The child dup2's to arrange its standard output to go back to
+the other end, and read its standard input to come from the
+pipe.  After that it exec's pack-objects.  On the other hand,
+the parent process, before starting to feed the child pipeline,
+closes the reading side of the pipe and fd to receive-pack.
+
+    send-pack
+       |
+       pack_objects(parent)
+          |
+         v [0]
+         pack-objects [0] ---> receive-pack
+
+
+[jc: the pipeline was much more complex and needed documentation before
+ I understood an earlier bug, but now it is trivial and straightforward.]
diff --git a/Documentation/technical/shallow.txt b/Documentation/technical/shallow.txt
new file mode 100644 (file)
index 0000000..559263a
--- /dev/null
@@ -0,0 +1,49 @@
+Def.: Shallow commits do have parents, but not in the shallow
+repo, and therefore grafts are introduced pretending that
+these commits have no parents.
+
+The basic idea is to write the SHA1s of shallow commits into
+$GIT_DIR/shallow, and handle its contents like the contents
+of $GIT_DIR/info/grafts (with the difference that shallow
+cannot contain parent information).
+
+This information is stored in a new file instead of grafts, or
+even the config, since the user should not touch that file
+at all (even throughout development of the shallow clone, it
+was never manually edited!).
+
+Each line contains exactly one SHA1. When read, a commit_graft
+will be constructed, which has nr_parent < 0 to make it easier
+to discern from user provided grafts.
+
+Since fsck-objects relies on the library to read the objects,
+it honours shallow commits automatically.
+
+There are some unfinished ends of the whole shallow business:
+
+- maybe we have to force non-thin packs when fetching into a
+  shallow repo (ATM they are forced non-thin).
+
+- A special handling of a shallow upstream is needed. At some
+  stage, upload-pack has to check if it sends a shallow commit,
+  and it should send that information early (or fail, if the
+  client does not support shallow repositories). There is no
+  support at all for this in this patch series.
+
+- Instead of locking $GIT_DIR/shallow at the start, just
+  the timestamp of it is noted, and when it comes to writing it,
+  a check is performed if the mtime is still the same, dying if
+  it is not.
+
+- It is unclear how "push into/from a shallow repo" should behave.
+
+- If you deepen a history, you'd want to get the tags of the
+  newly stored (but older!) commits. This does not work right now.
+
+To make a shallow clone, you can call "git-clone --depth 20 repo".
+The result contains only commit chains with a length of at most 20.
+It also writes an appropriate $GIT_DIR/shallow.
+
+You can deepen a shallow repository with "git-fetch --depth 20
+repo branch", which will fetch branch from repo, but stop at depth
+20, updating $GIT_DIR/shallow.
diff --git a/Documentation/technical/trivial-merge.txt b/Documentation/technical/trivial-merge.txt
new file mode 100644 (file)
index 0000000..24c8410
--- /dev/null
@@ -0,0 +1,121 @@
+Trivial merge rules
+===================
+
+This document describes the outcomes of the trivial merge logic in read-tree.
+
+One-way merge
+-------------
+
+This replaces the index with a different tree, keeping the stat info
+for entries that don't change, and allowing -u to make the minimum
+required changes to the working tree to have it match.
+
+Entries marked '+' have stat information. Spaces marked '*' don't
+affect the result.
+
+   index   tree    result
+   -----------------------
+   *       (empty) (empty)
+   (empty) tree    tree
+   index+  tree    tree
+   index+  index   index+
+
+Two-way merge
+-------------
+
+It is permitted for the index to lack an entry; this does not prevent
+any case from applying.
+
+If the index exists, it is an error for it not to match either the old
+or the result.
+
+If multiple cases apply, the one used is listed first.
+
+A result which changes the index is an error if the index is not empty
+and not up-to-date.
+
+Entries marked '+' have stat information. Spaces marked '*' don't
+affect the result.
+
+ case  index   old     new     result
+ -------------------------------------
+ 0/2   (empty) *       (empty) (empty)
+ 1/3   (empty) *       new     new
+ 4/5   index+  (empty) (empty) index+
+ 6/7   index+  (empty) index   index+
+ 10    index+  index   (empty) (empty)
+ 14/15 index+  old     old     index+
+ 18/19 index+  old     index   index+
+ 20    index+  index   new     new
+
+Three-way merge
+---------------
+
+It is permitted for the index to lack an entry; this does not prevent
+any case from applying.
+
+If the index exists, it is an error for it not to match either the
+head or (if the merge is trivial) the result.
+
+If multiple cases apply, the one used is listed first.
+
+A result of "no merge" means that index is left in stage 0, ancest in
+stage 1, head in stage 2, and remote in stage 3 (if any of these are
+empty, no entry is left for that stage). Otherwise, the given entry is
+left in stage 0, and there are no other entries.
+
+A result of "no merge" is an error if the index is not empty and not
+up-to-date.
+
+*empty* means that the tree must not have a directory-file conflict
+ with the entry.
+
+For multiple ancestors, a '+' means that this case applies even if
+only one ancestor or remote fits; a '^' means all of the ancestors
+must be the same.
+
+case  ancest    head    remote    result
+----------------------------------------
+1     (empty)+  (empty) (empty)   (empty)
+2ALT  (empty)+  *empty* remote    remote
+2     (empty)^  (empty) remote    no merge
+3ALT  (empty)+  head    *empty*   head
+3     (empty)^  head    (empty)   no merge
+4     (empty)^  head    remote    no merge
+5ALT  *         head    head      head
+6     ancest+   (empty) (empty)   no merge
+8     ancest^   (empty) ancest    no merge
+7     ancest+   (empty) remote    no merge
+10    ancest^   ancest  (empty)   no merge
+9     ancest+   head    (empty)   no merge
+16    anc1/anc2 anc1    anc2      no merge
+13    ancest+   head    ancest    head
+14    ancest+   ancest  remote    remote
+11    ancest+   head    remote    no merge
+
+Only #2ALT and #3ALT use *empty*, because these are the only cases
+where there can be conflicts that didn't exist before. Note that we
+allow directory-file conflicts between things in different stages
+after the trivial merge.
+
+A possible alternative for #6 is (empty), which would make it like
+#1. This is not used, due to the likelihood that it arises due to
+moving the file to multiple different locations or moving and deleting
+it in different branches.
+
+Case #1 is included for completeness, and also in case we decide to
+put on '+' markings; any path that is never mentioned at all isn't
+handled.
+
+Note that #16 is when both #13 and #14 apply; in this case, we refuse
+the trivial merge, because we can't tell from this data which is
+right. This is a case of a reverted patch (in some direction, maybe
+multiple times), and the right answer depends on looking at crossings
+of history or common ancestors of the ancestors.
+
+Note that, between #6, #7, #9, and #11, all cases not otherwise
+covered are handled in this table.
+
+For #8 and #10, there is alternative behavior, not currently
+implemented, where the result is (empty). As currently implemented,
+the automatic merge will generally give this effect.
diff --git a/Documentation/tutorial-2.txt b/Documentation/tutorial-2.txt
new file mode 100644 (file)
index 0000000..5c39a16
--- /dev/null
@@ -0,0 +1,406 @@
+A tutorial introduction to git: part two
+========================================
+
+You should work through link:tutorial.html[A tutorial introduction to
+git] before reading this tutorial.
+
+The goal of this tutorial is to introduce two fundamental pieces of
+git's architecture--the object database and the index file--and to
+provide the reader with everything necessary to understand the rest
+of the git documentation.
+
+The git object database
+-----------------------
+
+Let's start a new project and create a small amount of history:
+
+------------------------------------------------
+$ mkdir test-project
+$ cd test-project
+$ git init
+Initialized empty Git repository in .git/
+$ echo 'hello world' > file.txt
+$ git add .
+$ git commit -a -m "initial commit"
+Created initial commit 54196cc2703dc165cbd373a65a4dcf22d50ae7f7
+ create mode 100644 file.txt
+$ echo 'hello world!' >file.txt
+$ git commit -a -m "add emphasis"
+Created commit c4d59f390b9cfd4318117afde11d601c1085f241
+------------------------------------------------
+
+What are the 40 digits of hex that git responded to the commit with?
+
+We saw in part one of the tutorial that commits have names like this.
+It turns out that every object in the git history is stored under
+such a 40-digit hex name.  That name is the SHA1 hash of the object's
+contents; among other things, this ensures that git will never store
+the same data twice (since identical data is given an identical SHA1
+name), and that the contents of a git object will never change (since
+that would change the object's name as well).
+
+It is expected that the content of the commit object you created while
+following the example above generates a different SHA1 hash than
+the one shown above because the commit object records the time when
+it was created and the name of the person performing the commit.
+
+We can ask git about this particular object with the cat-file
+command. Don't copy the 40 hex digits from this example but use those
+from your own version. Note that you can shorten it to only a few
+characters to save yourself typing all 40 hex digits:
+
+------------------------------------------------
+$ git-cat-file -t 54196cc2
+commit
+$ git-cat-file commit 54196cc2
+tree 92b8b694ffb1675e5975148e1121810081dbdffe
+author J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500
+committer J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500
+
+initial commit
+------------------------------------------------
+
+A tree can refer to one or more "blob" objects, each corresponding to
+a file.  In addition, a tree can also refer to other tree objects,
+thus creating a directory hierarchy.  You can examine the contents of
+any tree using ls-tree (remember that a long enough initial portion
+of the SHA1 will also work):
+
+------------------------------------------------
+$ git ls-tree 92b8b694
+100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    file.txt
+------------------------------------------------
+
+Thus we see that this tree has one file in it.  The SHA1 hash is a
+reference to that file's data:
+
+------------------------------------------------
+$ git cat-file -t 3b18e512
+blob
+------------------------------------------------
+
+A "blob" is just file data, which we can also examine with cat-file:
+
+------------------------------------------------
+$ git cat-file blob 3b18e512
+hello world
+------------------------------------------------
+
+Note that this is the old file data; so the object that git named in
+its response to the initial tree was a tree with a snapshot of the
+directory state that was recorded by the first commit.
+
+All of these objects are stored under their SHA1 names inside the git
+directory:
+
+------------------------------------------------
+$ find .git/objects/
+.git/objects/
+.git/objects/pack
+.git/objects/info
+.git/objects/3b
+.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
+.git/objects/92
+.git/objects/92/b8b694ffb1675e5975148e1121810081dbdffe
+.git/objects/54
+.git/objects/54/196cc2703dc165cbd373a65a4dcf22d50ae7f7
+.git/objects/a0
+.git/objects/a0/423896973644771497bdc03eb99d5281615b51
+.git/objects/d0
+.git/objects/d0/492b368b66bdabf2ac1fd8c92b39d3db916e59
+.git/objects/c4
+.git/objects/c4/d59f390b9cfd4318117afde11d601c1085f241
+------------------------------------------------
+
+and the contents of these files is just the compressed data plus a
+header identifying their length and their type.  The type is either a
+blob, a tree, a commit, or a tag.
+
+The simplest commit to find is the HEAD commit, which we can find
+from .git/HEAD:
+
+------------------------------------------------
+$ cat .git/HEAD
+ref: refs/heads/master
+------------------------------------------------
+
+As you can see, this tells us which branch we're currently on, and it
+tells us this by naming a file under the .git directory, which itself
+contains a SHA1 name referring to a commit object, which we can
+examine with cat-file:
+
+------------------------------------------------
+$ cat .git/refs/heads/master
+c4d59f390b9cfd4318117afde11d601c1085f241
+$ git cat-file -t c4d59f39
+commit
+$ git cat-file commit c4d59f39
+tree d0492b368b66bdabf2ac1fd8c92b39d3db916e59
+parent 54196cc2703dc165cbd373a65a4dcf22d50ae7f7
+author J. Bruce Fields <bfields@puzzle.fieldses.org> 1143418702 -0500
+committer J. Bruce Fields <bfields@puzzle.fieldses.org> 1143418702 -0500
+
+add emphasis
+------------------------------------------------
+
+The "tree" object here refers to the new state of the tree:
+
+------------------------------------------------
+$ git ls-tree d0492b36
+100644 blob a0423896973644771497bdc03eb99d5281615b51    file.txt
+$ git cat-file blob a0423896
+hello world!
+------------------------------------------------
+
+and the "parent" object refers to the previous commit:
+
+------------------------------------------------
+$ git-cat-file commit 54196cc2
+tree 92b8b694ffb1675e5975148e1121810081dbdffe
+author J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500
+committer J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500
+
+initial commit
+------------------------------------------------
+
+The tree object is the tree we examined first, and this commit is
+unusual in that it lacks any parent.
+
+Most commits have only one parent, but it is also common for a commit
+to have multiple parents.   In that case the commit represents a
+merge, with the parent references pointing to the heads of the merged
+branches.
+
+Besides blobs, trees, and commits, the only remaining type of object
+is a "tag", which we won't discuss here; refer to gitlink:git-tag[1]
+for details.
+
+So now we know how git uses the object database to represent a
+project's history:
+
+  * "commit" objects refer to "tree" objects representing the
+    snapshot of a directory tree at a particular point in the
+    history, and refer to "parent" commits to show how they're
+    connected into the project history.
+  * "tree" objects represent the state of a single directory,
+    associating directory names to "blob" objects containing file
+    data and "tree" objects containing subdirectory information.
+  * "blob" objects contain file data without any other structure.
+  * References to commit objects at the head of each branch are
+    stored in files under .git/refs/heads/.
+  * The name of the current branch is stored in .git/HEAD.
+
+Note, by the way, that lots of commands take a tree as an argument.
+But as we can see above, a tree can be referred to in many different
+ways--by the SHA1 name for that tree, by the name of a commit that
+refers to the tree, by the name of a branch whose head refers to that
+tree, etc.--and most such commands can accept any of these names.
+
+In command synopses, the word "tree-ish" is sometimes used to
+designate such an argument.
+
+The index file
+--------------
+
+The primary tool we've been using to create commits is "git commit
+-a", which creates a commit including every change you've made to
+your working tree.  But what if you want to commit changes only to
+certain files?  Or only certain changes to certain files?
+
+If we look at the way commits are created under the cover, we'll see
+that there are more flexible ways creating commits.
+
+Continuing with our test-project, let's modify file.txt again:
+
+------------------------------------------------
+$ echo "hello world, again" >>file.txt
+------------------------------------------------
+
+but this time instead of immediately making the commit, let's take an
+intermediate step, and ask for diffs along the way to keep track of
+what's happening:
+
+------------------------------------------------
+$ git diff
+--- a/file.txt
++++ b/file.txt
+@@ -1 +1,2 @@
+ hello world!
++hello world, again
+$ git add file.txt
+$ git diff
+------------------------------------------------
+
+The last diff is empty, but no new commits have been made, and the
+head still doesn't contain the new line:
+
+------------------------------------------------
+$ git-diff HEAD
+diff --git a/file.txt b/file.txt
+index a042389..513feba 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1 +1,2 @@
+ hello world!
++hello world, again
+------------------------------------------------
+
+So "git diff" is comparing against something other than the head.
+The thing that it's comparing against is actually the index file,
+which is stored in .git/index in a binary format, but whose contents
+we can examine with ls-files:
+
+------------------------------------------------
+$ git ls-files --stage
+100644 513feba2e53ebbd2532419ded848ba19de88ba00 0       file.txt
+$ git cat-file -t 513feba2
+blob
+$ git cat-file blob 513feba2
+hello world!
+hello world, again
+------------------------------------------------
+
+So what our "git add" did was store a new blob and then put
+a reference to it in the index file.  If we modify the file again,
+we'll see that the new modifications are reflected in the "git-diff"
+output:
+
+------------------------------------------------
+$ echo 'again?' >>file.txt
+$ git diff
+index 513feba..ba3da7b 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1,2 +1,3 @@
+ hello world!
+ hello world, again
++again?
+------------------------------------------------
+
+With the right arguments, git diff can also show us the difference
+between the working directory and the last commit, or between the
+index and the last commit:
+
+------------------------------------------------
+$ git diff HEAD
+diff --git a/file.txt b/file.txt
+index a042389..ba3da7b 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1 +1,3 @@
+ hello world!
++hello world, again
++again?
+$ git diff --cached
+diff --git a/file.txt b/file.txt
+index a042389..513feba 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1 +1,2 @@
+ hello world!
++hello world, again
+------------------------------------------------
+
+At any time, we can create a new commit using "git commit" (without
+the -a option), and verify that the state committed only includes the
+changes stored in the index file, not the additional change that is
+still only in our working tree:
+
+------------------------------------------------
+$ git commit -m "repeat"
+$ git diff HEAD
+diff --git a/file.txt b/file.txt
+index 513feba..ba3da7b 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1,2 +1,3 @@
+ hello world!
+ hello world, again
++again?
+------------------------------------------------
+
+So by default "git commit" uses the index to create the commit, not
+the working tree; the -a option to commit tells it to first update
+the index with all changes in the working tree.
+
+Finally, it's worth looking at the effect of "git add" on the index
+file:
+
+------------------------------------------------
+$ echo "goodbye, world" >closing.txt
+$ git add closing.txt
+------------------------------------------------
+
+The effect of the "git add" was to add one entry to the index file:
+
+------------------------------------------------
+$ git ls-files --stage
+100644 8b9743b20d4b15be3955fc8d5cd2b09cd2336138 0       closing.txt
+100644 513feba2e53ebbd2532419ded848ba19de88ba00 0       file.txt
+------------------------------------------------
+
+And, as you can see with cat-file, this new entry refers to the
+current contents of the file:
+
+------------------------------------------------
+$ git cat-file blob 8b9743b2
+goodbye, world
+------------------------------------------------
+
+The "status" command is a useful way to get a quick summary of the
+situation:
+
+------------------------------------------------
+$ git status
+# On branch master
+# Changes to be committed:
+#   (use "git reset HEAD <file>..." to unstage)
+#
+#       new file: closing.txt
+#
+# Changed but not updated:
+#   (use "git add <file>..." to update what will be committed)
+#
+#       modified: file.txt
+#
+------------------------------------------------
+
+Since the current state of closing.txt is cached in the index file,
+it is listed as "Changes to be committed".  Since file.txt has
+changes in the working directory that aren't reflected in the index,
+it is marked "changed but not updated".  At this point, running "git
+commit" would create a commit that added closing.txt (with its new
+contents), but that didn't modify file.txt.
+
+Also, note that a bare "git diff" shows the changes to file.txt, but
+not the addition of closing.txt, because the version of closing.txt
+in the index file is identical to the one in the working directory.
+
+In addition to being the staging area for new commits, the index file
+is also populated from the object database when checking out a
+branch, and is used to hold the trees involved in a merge operation.
+See the link:core-tutorial.html[core tutorial] and the relevant man
+pages for details.
+
+What next?
+----------
+
+At this point you should know everything necessary to read the man
+pages for any of the git commands; one good place to start would be
+with the commands mentioned in link:everyday.html[Everyday git].  You
+should be able to find any unknown jargon in the
+link:glossary.html[Glossary].
+
+The link:user-manual.html[Git User's Manual] provides a more
+comprehensive introduction to git.
+
+The link:cvs-migration.html[CVS migration] document explains how to
+import a CVS repository into git, and shows how to use git in a
+CVS-like way.
+
+For some interesting examples of git use, see the
+link:howto-index.html[howtos].
+
+For git developers, the link:core-tutorial.html[Core tutorial] goes
+into detail on the lower-level git mechanisms involved in, for
+example, creating a new commit.
diff --git a/Documentation/tutorial.txt b/Documentation/tutorial.txt
new file mode 100644 (file)
index 0000000..fff1068
--- /dev/null
@@ -0,0 +1,584 @@
+A tutorial introduction to git (for version 1.5.1 or newer)
+===========================================================
+
+This tutorial explains how to import a new project into git, make
+changes to it, and share changes with other developers.
+
+If you are instead primarily interested in using git to fetch a project,
+for example, to test the latest version, you may prefer to start with
+the first two chapters of link:user-manual.html[The Git User's Manual].
+
+First, note that you can get documentation for a command such as "git
+diff" with:
+
+------------------------------------------------
+$ man git-diff
+------------------------------------------------
+
+It is a good idea to introduce yourself to git with your name and
+public email address before doing any operation.  The easiest
+way to do so is:
+
+------------------------------------------------
+$ git config --global user.name "Your Name Comes Here"
+$ git config --global user.email you@yourdomain.example.com
+------------------------------------------------
+
+
+Importing a new project
+-----------------------
+
+Assume you have a tarball project.tar.gz with your initial work.  You
+can place it under git revision control as follows.
+
+------------------------------------------------
+$ tar xzf project.tar.gz
+$ cd project
+$ git init
+------------------------------------------------
+
+Git will reply
+
+------------------------------------------------
+Initialized empty Git repository in .git/
+------------------------------------------------
+
+You've now initialized the working directory--you may notice a new
+directory created, named ".git".
+
+Next, tell git to take a snapshot of the contents of all files under the
+current directory (note the '.'), with gitlink:git-add[1]:
+
+------------------------------------------------
+$ git add .
+------------------------------------------------
+
+This snapshot is now stored in a temporary staging area which git calls
+the "index".  You can permanently store the contents of the index in the
+repository with gitlink:git-commit[1]:
+
+------------------------------------------------
+$ git commit
+------------------------------------------------
+
+This will prompt you for a commit message.  You've now stored the first
+version of your project in git.
+
+Making changes
+--------------
+
+Modify some files, then add their updated contents to the index:
+
+------------------------------------------------
+$ git add file1 file2 file3
+------------------------------------------------
+
+You are now ready to commit.  You can see what is about to be committed
+using gitlink:git-diff[1] with the --cached option:
+
+------------------------------------------------
+$ git diff --cached
+------------------------------------------------
+
+(Without --cached, gitlink:git-diff[1] will show you any changes that
+you've made but not yet added to the index.)  You can also get a brief
+summary of the situation with gitlink:git-status[1]:
+
+------------------------------------------------
+$ git status
+# On branch master
+# Changes to be committed:
+#   (use "git reset HEAD <file>..." to unstage)
+#
+#      modified:   file1
+#      modified:   file2
+#      modified:   file3
+#
+------------------------------------------------
+
+If you need to make any further adjustments, do so now, and then add any
+newly modified content to the index.  Finally, commit your changes with:
+
+------------------------------------------------
+$ git commit
+------------------------------------------------
+
+This will again prompt your for a message describing the change, and then
+record a new version of the project.
+
+Alternatively, instead of running `git add` beforehand, you can use
+
+------------------------------------------------
+$ git commit -a
+------------------------------------------------
+
+which will automatically notice any modified (but not new) files, add
+them to the index, and commit, all in one step.
+
+A note on commit messages: Though not required, it's a good idea to
+begin the commit message with a single short (less than 50 character)
+line summarizing the change, followed by a blank line and then a more
+thorough description.  Tools that turn commits into email, for
+example, use the first line on the Subject: line and the rest of the
+commit in the body.
+
+Git tracks content not files
+----------------------------
+
+Many revision control systems provide an "add" command that tells the
+system to start tracking changes to a new file.  Git's "add" command
+does something simpler and more powerful: `git add` is used both for new
+and newly modified files, and in both cases it takes a snapshot of the
+given files and stages that content in the index, ready for inclusion in
+the next commit.
+
+Viewing project history
+-----------------------
+
+At any point you can view the history of your changes using
+
+------------------------------------------------
+$ git log
+------------------------------------------------
+
+If you also want to see complete diffs at each step, use
+
+------------------------------------------------
+$ git log -p
+------------------------------------------------
+
+Often the overview of the change is useful to get a feel of
+each step
+
+------------------------------------------------
+$ git log --stat --summary
+------------------------------------------------
+
+Managing branches
+-----------------
+
+A single git repository can maintain multiple branches of
+development.  To create a new branch named "experimental", use
+
+------------------------------------------------
+$ git branch experimental
+------------------------------------------------
+
+If you now run
+
+------------------------------------------------
+$ git branch
+------------------------------------------------
+
+you'll get a list of all existing branches:
+
+------------------------------------------------
+  experimental
+* master
+------------------------------------------------
+
+The "experimental" branch is the one you just created, and the
+"master" branch is a default branch that was created for you
+automatically.  The asterisk marks the branch you are currently on;
+type
+
+------------------------------------------------
+$ git checkout experimental
+------------------------------------------------
+
+to switch to the experimental branch.  Now edit a file, commit the
+change, and switch back to the master branch:
+
+------------------------------------------------
+(edit file)
+$ git commit -a
+$ git checkout master
+------------------------------------------------
+
+Check that the change you made is no longer visible, since it was
+made on the experimental branch and you're back on the master branch.
+
+You can make a different change on the master branch:
+
+------------------------------------------------
+(edit file)
+$ git commit -a
+------------------------------------------------
+
+at this point the two branches have diverged, with different changes
+made in each.  To merge the changes made in experimental into master, run
+
+------------------------------------------------
+$ git merge experimental
+------------------------------------------------
+
+If the changes don't conflict, you're done.  If there are conflicts,
+markers will be left in the problematic files showing the conflict;
+
+------------------------------------------------
+$ git diff
+------------------------------------------------
+
+will show this.  Once you've edited the files to resolve the
+conflicts,
+
+------------------------------------------------
+$ git commit -a
+------------------------------------------------
+
+will commit the result of the merge. Finally,
+
+------------------------------------------------
+$ gitk
+------------------------------------------------
+
+will show a nice graphical representation of the resulting history.
+
+At this point you could delete the experimental branch with
+
+------------------------------------------------
+$ git branch -d experimental
+------------------------------------------------
+
+This command ensures that the changes in the experimental branch are
+already in the current branch.
+
+If you develop on a branch crazy-idea, then regret it, you can always
+delete the branch with
+
+-------------------------------------
+$ git branch -D crazy-idea
+-------------------------------------
+
+Branches are cheap and easy, so this is a good way to try something
+out.
+
+Using git for collaboration
+---------------------------
+
+Suppose that Alice has started a new project with a git repository in
+/home/alice/project, and that Bob, who has a home directory on the
+same machine, wants to contribute.
+
+Bob begins with:
+
+------------------------------------------------
+$ git clone /home/alice/project myrepo
+------------------------------------------------
+
+This creates a new directory "myrepo" containing a clone of Alice's
+repository.  The clone is on an equal footing with the original
+project, possessing its own copy of the original project's history.
+
+Bob then makes some changes and commits them:
+
+------------------------------------------------
+(edit files)
+$ git commit -a
+(repeat as necessary)
+------------------------------------------------
+
+When he's ready, he tells Alice to pull changes from the repository
+at /home/bob/myrepo.  She does this with:
+
+------------------------------------------------
+$ cd /home/alice/project
+$ git pull /home/bob/myrepo master
+------------------------------------------------
+
+This merges the changes from Bob's "master" branch into Alice's
+current branch.  If Alice has made her own changes in the meantime,
+then she may need to manually fix any conflicts.  (Note that the
+"master" argument in the above command is actually unnecessary, as it
+is the default.)
+
+The "pull" command thus performs two operations: it fetches changes
+from a remote branch, then merges them into the current branch.
+
+When you are working in a small closely knit group, it is not
+unusual to interact with the same repository over and over
+again.  By defining 'remote' repository shorthand, you can make
+it easier:
+
+------------------------------------------------
+$ git remote add bob /home/bob/myrepo
+------------------------------------------------
+
+With this, Alice can perform the first operation alone using the
+"git fetch" command without merging them with her own branch,
+using:
+
+-------------------------------------
+$ git fetch bob
+-------------------------------------
+
+Unlike the longhand form, when Alice fetches from Bob using a
+remote repository shorthand set up with `git remote`, what was
+fetched is stored in a remote tracking branch, in this case
+`bob/master`.  So after this:
+
+-------------------------------------
+$ git log -p master..bob/master
+-------------------------------------
+
+shows a list of all the changes that Bob made since he branched from
+Alice's master branch.
+
+After examining those changes, Alice
+could merge the changes into her master branch:
+
+-------------------------------------
+$ git merge bob/master
+-------------------------------------
+
+This `merge` can also be done by 'pulling from her own remote
+tracking branch', like this:
+
+-------------------------------------
+$ git pull . remotes/bob/master
+-------------------------------------
+
+Note that git pull always merges into the current branch,
+regardless of what else is given on the command line.
+
+Later, Bob can update his repo with Alice's latest changes using
+
+-------------------------------------
+$ git pull
+-------------------------------------
+
+Note that he doesn't need to give the path to Alice's repository;
+when Bob cloned Alice's repository, git stored the location of her
+repository in the repository configuration, and that location is
+used for pulls:
+
+-------------------------------------
+$ git config --get remote.origin.url
+/home/alice/project
+-------------------------------------
+
+(The complete configuration created by git-clone is visible using
+"git config -l", and the gitlink:git-config[1] man page
+explains the meaning of each option.)
+
+Git also keeps a pristine copy of Alice's master branch under the
+name "origin/master":
+
+-------------------------------------
+$ git branch -r
+  origin/master
+-------------------------------------
+
+If Bob later decides to work from a different host, he can still
+perform clones and pulls using the ssh protocol:
+
+-------------------------------------
+$ git clone alice.org:/home/alice/project myrepo
+-------------------------------------
+
+Alternatively, git has a native protocol, or can use rsync or http;
+see gitlink:git-pull[1] for details.
+
+Git can also be used in a CVS-like mode, with a central repository
+that various users push changes to; see gitlink:git-push[1] and
+link:cvs-migration.html[git for CVS users].
+
+Exploring history
+-----------------
+
+Git history is represented as a series of interrelated commits.  We
+have already seen that the git log command can list those commits.
+Note that first line of each git log entry also gives a name for the
+commit:
+
+-------------------------------------
+$ git log
+commit c82a22c39cbc32576f64f5c6b3f24b99ea8149c7
+Author: Junio C Hamano <junkio@cox.net>
+Date:   Tue May 16 17:18:22 2006 -0700
+
+    merge-base: Clarify the comments on post processing.
+-------------------------------------
+
+We can give this name to git show to see the details about this
+commit.
+
+-------------------------------------
+$ git show c82a22c39cbc32576f64f5c6b3f24b99ea8149c7
+-------------------------------------
+
+But there are other ways to refer to commits.  You can use any initial
+part of the name that is long enough to uniquely identify the commit:
+
+-------------------------------------
+$ git show c82a22c39c  # the first few characters of the name are
+                       # usually enough
+$ git show HEAD                # the tip of the current branch
+$ git show experimental        # the tip of the "experimental" branch
+-------------------------------------
+
+Every commit usually has one "parent" commit
+which points to the previous state of the project:
+
+-------------------------------------
+$ git show HEAD^  # to see the parent of HEAD
+$ git show HEAD^^ # to see the grandparent of HEAD
+$ git show HEAD~4 # to see the great-great grandparent of HEAD
+-------------------------------------
+
+Note that merge commits may have more than one parent:
+
+-------------------------------------
+$ git show HEAD^1 # show the first parent of HEAD (same as HEAD^)
+$ git show HEAD^2 # show the second parent of HEAD
+-------------------------------------
+
+You can also give commits names of your own; after running
+
+-------------------------------------
+$ git-tag v2.5 1b2e1d63ff
+-------------------------------------
+
+you can refer to 1b2e1d63ff by the name "v2.5".  If you intend to
+share this name with other people (for example, to identify a release
+version), you should create a "tag" object, and perhaps sign it; see
+gitlink:git-tag[1] for details.
+
+Any git command that needs to know a commit can take any of these
+names.  For example:
+
+-------------------------------------
+$ git diff v2.5 HEAD    # compare the current HEAD to v2.5
+$ git branch stable v2.5 # start a new branch named "stable" based
+                        # at v2.5
+$ git reset --hard HEAD^ # reset your current branch and working
+                        # directory to its state at HEAD^
+-------------------------------------
+
+Be careful with that last command: in addition to losing any changes
+in the working directory, it will also remove all later commits from
+this branch.  If this branch is the only branch containing those
+commits, they will be lost.  Also, don't use "git reset" on a
+publicly-visible branch that other developers pull from, as it will
+force needless merges on other developers to clean up the history.
+If you need to undo changes that you have pushed, use gitlink:git-revert[1]
+instead.
+
+The git grep command can search for strings in any version of your
+project, so
+
+-------------------------------------
+$ git grep "hello" v2.5
+-------------------------------------
+
+searches for all occurrences of "hello" in v2.5.
+
+If you leave out the commit name, git grep will search any of the
+files it manages in your current directory.  So
+
+-------------------------------------
+$ git grep "hello"
+-------------------------------------
+
+is a quick way to search just the files that are tracked by git.
+
+Many git commands also take sets of commits, which can be specified
+in a number of ways.  Here are some examples with git log:
+
+-------------------------------------
+$ git log v2.5..v2.6            # commits between v2.5 and v2.6
+$ git log v2.5..                # commits since v2.5
+$ git log --since="2 weeks ago" # commits from the last 2 weeks
+$ git log v2.5.. Makefile       # commits since v2.5 which modify
+                               # Makefile
+-------------------------------------
+
+You can also give git log a "range" of commits where the first is not
+necessarily an ancestor of the second; for example, if the tips of
+the branches "stable-release" and "master" diverged from a common
+commit some time ago, then
+
+-------------------------------------
+$ git log stable..experimental
+-------------------------------------
+
+will list commits made in the experimental branch but not in the
+stable branch, while
+
+-------------------------------------
+$ git log experimental..stable
+-------------------------------------
+
+will show the list of commits made on the stable branch but not
+the experimental branch.
+
+The "git log" command has a weakness: it must present commits in a
+list.  When the history has lines of development that diverged and
+then merged back together, the order in which "git log" presents
+those commits is meaningless.
+
+Most projects with multiple contributors (such as the linux kernel,
+or git itself) have frequent merges, and gitk does a better job of
+visualizing their history.  For example,
+
+-------------------------------------
+$ gitk --since="2 weeks ago" drivers/
+-------------------------------------
+
+allows you to browse any commits from the last 2 weeks of commits
+that modified files under the "drivers" directory.  (Note: you can
+adjust gitk's fonts by holding down the control key while pressing
+"-" or "+".)
+
+Finally, most commands that take filenames will optionally allow you
+to precede any filename by a commit, to specify a particular version
+of the file:
+
+-------------------------------------
+$ git diff v2.5:Makefile HEAD:Makefile.in
+-------------------------------------
+
+You can also use "git show" to see any such file:
+
+-------------------------------------
+$ git show v2.5:Makefile
+-------------------------------------
+
+Next Steps
+----------
+
+This tutorial should be enough to perform basic distributed revision
+control for your projects.  However, to fully understand the depth
+and power of git you need to understand two simple ideas on which it
+is based:
+
+  * The object database is the rather elegant system used to
+    store the history of your project--files, directories, and
+    commits.
+
+  * The index file is a cache of the state of a directory tree,
+    used to create commits, check out working directories, and
+    hold the various trees involved in a merge.
+
+link:tutorial-2.html[Part two of this tutorial] explains the object
+database, the index file, and a few other odds and ends that you'll
+need to make the most of git.
+
+If you don't want to continue with that right away, a few other
+digressions that may be interesting at this point are:
+
+  * gitlink:git-format-patch[1], gitlink:git-am[1]: These convert
+    series of git commits into emailed patches, and vice versa,
+    useful for projects such as the linux kernel which rely heavily
+    on emailed patches.
+
+  * gitlink:git-bisect[1]: When there is a regression in your
+    project, one way to track down the bug is by searching through
+    the history to find the exact commit that's to blame.  Git bisect
+    can help you perform a binary search for that commit.  It is
+    smart enough to perform a close-to-optimal search even in the
+    case of complex non-linear history with lots of merged branches.
+
+  * link:everyday.html[Everyday GIT with 20 Commands Or So]
+
+  * link:cvs-migration.html[git for CVS users].
diff --git a/Documentation/urls-remotes.txt b/Documentation/urls-remotes.txt
new file mode 100644 (file)
index 0000000..5dd1f83
--- /dev/null
@@ -0,0 +1,55 @@
+include::urls.txt[]
+
+REMOTES
+-------
+
+In addition to the above, as a short-hand, the name of a
+file in `$GIT_DIR/remotes` directory can be given; the
+named file should be in the following format:
+
+------------
+       URL: one of the above URL format
+       Push: <refspec>
+       Pull: <refspec>
+
+------------
+
+Then such a short-hand is specified in place of
+<repository> without <refspec> parameters on the command
+line, <refspec> specified on `Push:` lines or `Pull:`
+lines are used for `git-push` and `git-fetch`/`git-pull`,
+respectively.  Multiple `Push:` and `Pull:` lines may
+be specified for additional branch mappings.
+
+Or, equivalently, in the `$GIT_DIR/config` (note the use
+of `fetch` instead of `Pull:`):
+
+------------
+       [remote "<remote>"]
+               url = <url>
+               push = <refspec>
+               fetch = <refspec>
+
+------------
+
+The name of a file in `$GIT_DIR/branches` directory can be
+specified as an older notation short-hand; the named
+file should contain a single line, a URL in one of the
+above formats, optionally followed by a hash `#` and the
+name of remote head (URL fragment notation).
+`$GIT_DIR/branches/<remote>` file that stores a <url>
+without the fragment is equivalent to have this in the
+corresponding file in the `$GIT_DIR/remotes/` directory.
+
+------------
+       URL: <url>
+       Pull: refs/heads/master:<remote>
+
+------------
+
+while having `<url>#<head>` is equivalent to
+
+------------
+       URL: <url>
+       Pull: refs/heads/<head>:<remote>
+------------
diff --git a/Documentation/urls.txt b/Documentation/urls.txt
new file mode 100644 (file)
index 0000000..4f66738
--- /dev/null
@@ -0,0 +1,46 @@
+GIT URLS[[URLS]]
+----------------
+
+One of the following notations can be used
+to name the remote repository:
+
+===============================================================
+- rsync://host.xz/path/to/repo.git/
+- http://host.xz/path/to/repo.git/
+- https://host.xz/path/to/repo.git/
+- git://host.xz/path/to/repo.git/
+- git://host.xz/~user/path/to/repo.git/
+- ssh://{startsb}user@{endsb}host.xz{startsb}:port{endsb}/path/to/repo.git/
+- ssh://{startsb}user@{endsb}host.xz/path/to/repo.git/
+- ssh://{startsb}user@{endsb}host.xz/~user/path/to/repo.git/
+- ssh://{startsb}user@{endsb}host.xz/~/path/to/repo.git
+===============================================================
+
+SSH is the default transport protocol over the network.  You can
+optionally specify which user to log-in as, and an alternate,
+scp-like syntax is also supported.  Both syntaxes support
+username expansion, as does the native git protocol, but
+only the former supports port specification. The following
+three are identical to the last three above, respectively:
+
+===============================================================
+- {startsb}user@{endsb}host.xz:/path/to/repo.git/
+- {startsb}user@{endsb}host.xz:~user/path/to/repo.git/
+- {startsb}user@{endsb}host.xz:path/to/repo.git
+===============================================================
+
+To sync with a local directory, you can use:
+
+===============================================================
+- /path/to/repo.git/
+- file:///path/to/repo.git/
+===============================================================
+
+ifndef::git-clone[]
+They are mostly equivalent, except when cloning.  See
+gitlink:git-clone[1] for details.
+endif::git-clone[]
+
+ifdef::git-clone[]
+They are equivalent, except the former implies --local option.
+endif::git-clone[]
diff --git a/Documentation/user-manual.conf b/Documentation/user-manual.conf
new file mode 100644 (file)
index 0000000..92b01ec
--- /dev/null
@@ -0,0 +1,21 @@
+[titles]
+       underlines="__","==","--","~~","^^"
+
+[attributes]
+caret=^
+startsb=&#91;
+endsb=&#93;
+tilde=&#126;
+
+[gitlink-inlinemacro]
+<ulink url="{target}.html">{target}{0?({0})}</ulink>
+
+ifdef::backend-docbook[]
+# "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this.
+[listingblock]
+<example><title>{title}</title>
+<literallayout>
+|
+</literallayout>
+{title#}</example>
+endif::backend-docbook[]
diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt
new file mode 100644 (file)
index 0000000..93a47b4
--- /dev/null
@@ -0,0 +1,4562 @@
+Git User's Manual (for version 1.5.3 or newer)
+______________________________________________
+
+
+Git is a fast distributed revision control system.
+
+This manual is designed to be readable by someone with basic UNIX
+command-line skills, but no previous knowledge of git.
+
+<<repositories-and-branches>> and <<exploring-git-history>> explain how
+to fetch and study a project using git--read these chapters to learn how
+to build and test a particular version of a software project, search for
+regressions, and so on.
+
+People needing to do actual development will also want to read
+<<Developing-with-git>> and <<sharing-development>>.
+
+Further chapters cover more specialized topics.
+
+Comprehensive reference documentation is available through the man
+pages.  For a command such as "git clone", just use
+
+------------------------------------------------
+$ man git-clone
+------------------------------------------------
+
+See also <<git-quick-start>> for a brief overview of git commands,
+without any explanation.
+
+Finally, see <<todo>> for ways that you can help make this manual more
+complete.
+
+
+[[repositories-and-branches]]
+Repositories and Branches
+=========================
+
+[[how-to-get-a-git-repository]]
+How to get a git repository
+---------------------------
+
+It will be useful to have a git repository to experiment with as you
+read this manual.
+
+The best way to get one is by using the gitlink:git-clone[1] command to
+download a copy of an existing repository.  If you don't already have a
+project in mind, here are some interesting examples:
+
+------------------------------------------------
+       # git itself (approx. 10MB download):
+$ git clone git://git.kernel.org/pub/scm/git/git.git
+       # the linux kernel (approx. 150MB download):
+$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
+------------------------------------------------
+
+The initial clone may be time-consuming for a large project, but you
+will only need to clone once.
+
+The clone command creates a new directory named after the project ("git"
+or "linux-2.6" in the examples above).  After you cd into this
+directory, you will see that it contains a copy of the project files,
+called the <<def_working_tree,working tree>>, together with a special
+top-level directory named ".git", which contains all the information
+about the history of the project.
+
+[[how-to-check-out]]
+How to check out a different version of a project
+-------------------------------------------------
+
+Git is best thought of as a tool for storing the history of a collection
+of files.  It stores the history as a compressed collection of
+interrelated snapshots of the project's contents.  In git each such
+version is called a <<def_commit,commit>>.
+
+Those snapshots aren't necessarily all arranged in a single line from
+oldest to newest; instead, work may simultaneously proceed along
+parallel lines of development, called <def_branch,branches>>, which may
+merge and diverge.
+
+A single git repository can track development on multiple branches.  It
+does this by keeping a list of <<def_head,heads>> which reference the
+latest commit on each branch; the gitlink:git-branch[1] command shows
+you the list of branch heads:
+
+------------------------------------------------
+$ git branch
+* master
+------------------------------------------------
+
+A freshly cloned repository contains a single branch head, by default
+named "master", with the working directory initialized to the state of
+the project referred to by that branch head.
+
+Most projects also use <<def_tag,tags>>.  Tags, like heads, are
+references into the project's history, and can be listed using the
+gitlink:git-tag[1] command:
+
+------------------------------------------------
+$ git tag -l
+v2.6.11
+v2.6.11-tree
+v2.6.12
+v2.6.12-rc2
+v2.6.12-rc3
+v2.6.12-rc4
+v2.6.12-rc5
+v2.6.12-rc6
+v2.6.13
+...
+------------------------------------------------
+
+Tags are expected to always point at the same version of a project,
+while heads are expected to advance as development progresses.
+
+Create a new branch head pointing to one of these versions and check it
+out using gitlink:git-checkout[1]:
+
+------------------------------------------------
+$ git checkout -b new v2.6.13
+------------------------------------------------
+
+The working directory then reflects the contents that the project had
+when it was tagged v2.6.13, and gitlink:git-branch[1] shows two
+branches, with an asterisk marking the currently checked-out branch:
+
+------------------------------------------------
+$ git branch
+  master
+* new
+------------------------------------------------
+
+If you decide that you'd rather see version 2.6.17, you can modify
+the current branch to point at v2.6.17 instead, with
+
+------------------------------------------------
+$ git reset --hard v2.6.17
+------------------------------------------------
+
+Note that if the current branch head was your only reference to a
+particular point in history, then resetting that branch may leave you
+with no way to find the history it used to point to; so use this command
+carefully.
+
+[[understanding-commits]]
+Understanding History: Commits
+------------------------------
+
+Every change in the history of a project is represented by a commit.
+The gitlink:git-show[1] command shows the most recent commit on the
+current branch:
+
+------------------------------------------------
+$ git show
+commit 17cf781661e6d38f737f15f53ab552f1e95960d7
+Author: Linus Torvalds <torvalds@ppc970.osdl.org.(none)>
+Date:   Tue Apr 19 14:11:06 2005 -0700
+
+    Remove duplicate getenv(DB_ENVIRONMENT) call
+
+    Noted by Tony Luck.
+
+diff --git a/init-db.c b/init-db.c
+index 65898fa..b002dc6 100644
+--- a/init-db.c
++++ b/init-db.c
+@@ -7,7 +7,7 @@
+ int main(int argc, char **argv)
+ {
+-      char *sha1_dir = getenv(DB_ENVIRONMENT), *path;
++      char *sha1_dir, *path;
+       int len, i;
+       if (mkdir(".git", 0755) < 0) {
+------------------------------------------------
+
+As you can see, a commit shows who made the latest change, what they
+did, and why.
+
+Every commit has a 40-hexdigit id, sometimes called the "object name" or the
+"SHA1 id", shown on the first line of the "git show" output.  You can usually
+refer to a commit by a shorter name, such as a tag or a branch name, but this
+longer name can also be useful.  Most importantly, it is a globally unique
+name for this commit: so if you tell somebody else the object name (for
+example in email), then you are guaranteed that name will refer to the same
+commit in their repository that it does in yours (assuming their repository
+has that commit at all).  Since the object name is computed as a hash over the
+contents of the commit, you are guaranteed that the commit can never change
+without its name also changing.
+
+In fact, in <<git-concepts>> we shall see that everything stored in git
+history, including file data and directory contents, is stored in an object
+with a name that is a hash of its contents.
+
+[[understanding-reachability]]
+Understanding history: commits, parents, and reachability
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Every commit (except the very first commit in a project) also has a
+parent commit which shows what happened before this commit.
+Following the chain of parents will eventually take you back to the
+beginning of the project.
+
+However, the commits do not form a simple list; git allows lines of
+development to diverge and then reconverge, and the point where two
+lines of development reconverge is called a "merge".  The commit
+representing a merge can therefore have more than one parent, with
+each parent representing the most recent commit on one of the lines
+of development leading to that point.
+
+The best way to see how this works is using the gitlink:gitk[1]
+command; running gitk now on a git repository and looking for merge
+commits will help understand how the git organizes history.
+
+In the following, we say that commit X is "reachable" from commit Y
+if commit X is an ancestor of commit Y.  Equivalently, you could say
+that Y is a descendant of X, or that there is a chain of parents
+leading from commit Y to commit X.
+
+[[history-diagrams]]
+Understanding history: History diagrams
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We will sometimes represent git history using diagrams like the one
+below.  Commits are shown as "o", and the links between them with
+lines drawn with - / and \.  Time goes left to right:
+
+
+................................................
+         o--o--o <-- Branch A
+        /
+ o--o--o <-- master
+        \
+         o--o--o <-- Branch B
+................................................
+
+If we need to talk about a particular commit, the character "o" may
+be replaced with another letter or number.
+
+[[what-is-a-branch]]
+Understanding history: What is a branch?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When we need to be precise, we will use the word "branch" to mean a line
+of development, and "branch head" (or just "head") to mean a reference
+to the most recent commit on a branch.  In the example above, the branch
+head named "A" is a pointer to one particular commit, but we refer to
+the line of three commits leading up to that point as all being part of
+"branch A".
+
+However, when no confusion will result, we often just use the term
+"branch" both for branches and for branch heads.
+
+[[manipulating-branches]]
+Manipulating branches
+---------------------
+
+Creating, deleting, and modifying branches is quick and easy; here's
+a summary of the commands:
+
+git branch::
+       list all branches
+git branch <branch>::
+       create a new branch named <branch>, referencing the same
+       point in history as the current branch
+git branch <branch> <start-point>::
+       create a new branch named <branch>, referencing
+       <start-point>, which may be specified any way you like,
+       including using a branch name or a tag name
+git branch -d <branch>::
+       delete the branch <branch>; if the branch you are deleting
+       points to a commit which is not reachable from the current
+       branch, this command will fail with a warning.
+git branch -D <branch>::
+       even if the branch points to a commit not reachable
+       from the current branch, you may know that that commit
+       is still reachable from some other branch or tag.  In that
+       case it is safe to use this command to force git to delete
+       the branch.
+git checkout <branch>::
+       make the current branch <branch>, updating the working
+       directory to reflect the version referenced by <branch>
+git checkout -b <new> <start-point>::
+       create a new branch <new> referencing <start-point>, and
+       check it out.
+
+The special symbol "HEAD" can always be used to refer to the current
+branch.  In fact, git uses a file named "HEAD" in the .git directory to
+remember which branch is current:
+
+------------------------------------------------
+$ cat .git/HEAD
+ref: refs/heads/master
+------------------------------------------------
+
+[[detached-head]]
+Examining an old version without creating a new branch
+------------------------------------------------------
+
+The git-checkout command normally expects a branch head, but will also
+accept an arbitrary commit; for example, you can check out the commit
+referenced by a tag:
+
+------------------------------------------------
+$ git checkout v2.6.17
+Note: moving to "v2.6.17" which isn't a local branch
+If you want to create a new branch from this checkout, you may do so
+(now or later) by using -b with the checkout command again. Example:
+  git checkout -b <new_branch_name>
+HEAD is now at 427abfa... Linux v2.6.17
+------------------------------------------------
+
+The HEAD then refers to the SHA1 of the commit instead of to a branch,
+and git branch shows that you are no longer on a branch:
+
+------------------------------------------------
+$ cat .git/HEAD
+427abfa28afedffadfca9dd8b067eb6d36bac53f
+$ git branch
+* (no branch)
+  master
+------------------------------------------------
+
+In this case we say that the HEAD is "detached".
+
+This is an easy way to check out a particular version without having to
+make up a name for the new branch.   You can still create a new branch
+(or tag) for this version later if you decide to.
+
+[[examining-remote-branches]]
+Examining branches from a remote repository
+-------------------------------------------
+
+The "master" branch that was created at the time you cloned is a copy
+of the HEAD in the repository that you cloned from.  That repository
+may also have had other branches, though, and your local repository
+keeps branches which track each of those remote branches, which you
+can view using the "-r" option to gitlink:git-branch[1]:
+
+------------------------------------------------
+$ git branch -r
+  origin/HEAD
+  origin/html
+  origin/maint
+  origin/man
+  origin/master
+  origin/next
+  origin/pu
+  origin/todo
+------------------------------------------------
+
+You cannot check out these remote-tracking branches, but you can
+examine them on a branch of your own, just as you would a tag:
+
+------------------------------------------------
+$ git checkout -b my-todo-copy origin/todo
+------------------------------------------------
+
+Note that the name "origin" is just the name that git uses by default
+to refer to the repository that you cloned from.
+
+[[how-git-stores-references]]
+Naming branches, tags, and other references
+-------------------------------------------
+
+Branches, remote-tracking branches, and tags are all references to
+commits.  All references are named with a slash-separated path name
+starting with "refs"; the names we've been using so far are actually
+shorthand:
+
+       - The branch "test" is short for "refs/heads/test".
+       - The tag "v2.6.18" is short for "refs/tags/v2.6.18".
+       - "origin/master" is short for "refs/remotes/origin/master".
+
+The full name is occasionally useful if, for example, there ever
+exists a tag and a branch with the same name.
+
+(Newly created refs are actually stored in the .git/refs directory,
+under the path given by their name.  However, for efficiency reasons
+they may also be packed together in a single file; see
+gitlink:git-pack-refs[1]).
+
+As another useful shortcut, the "HEAD" of a repository can be referred
+to just using the name of that repository.  So, for example, "origin"
+is usually a shortcut for the HEAD branch in the repository "origin".
+
+For the complete list of paths which git checks for references, and
+the order it uses to decide which to choose when there are multiple
+references with the same shorthand name, see the "SPECIFYING
+REVISIONS" section of gitlink:git-rev-parse[1].
+
+[[Updating-a-repository-with-git-fetch]]
+Updating a repository with git fetch
+------------------------------------
+
+Eventually the developer cloned from will do additional work in her
+repository, creating new commits and advancing the branches to point
+at the new commits.
+
+The command "git fetch", with no arguments, will update all of the
+remote-tracking branches to the latest version found in her
+repository.  It will not touch any of your own branches--not even the
+"master" branch that was created for you on clone.
+
+[[fetching-branches]]
+Fetching branches from other repositories
+-----------------------------------------
+
+You can also track branches from repositories other than the one you
+cloned from, using gitlink:git-remote[1]:
+
+-------------------------------------------------
+$ git remote add linux-nfs git://linux-nfs.org/pub/nfs-2.6.git
+$ git fetch linux-nfs
+* refs/remotes/linux-nfs/master: storing branch 'master' ...
+  commit: bf81b46
+-------------------------------------------------
+
+New remote-tracking branches will be stored under the shorthand name
+that you gave "git remote add", in this case linux-nfs:
+
+-------------------------------------------------
+$ git branch -r
+linux-nfs/master
+origin/master
+-------------------------------------------------
+
+If you run "git fetch <remote>" later, the tracking branches for the
+named <remote> will be updated.
+
+If you examine the file .git/config, you will see that git has added
+a new stanza:
+
+-------------------------------------------------
+$ cat .git/config
+...
+[remote "linux-nfs"]
+       url = git://linux-nfs.org/pub/nfs-2.6.git
+       fetch = +refs/heads/*:refs/remotes/linux-nfs/*
+...
+-------------------------------------------------
+
+This is what causes git to track the remote's branches; you may modify
+or delete these configuration options by editing .git/config with a
+text editor.  (See the "CONFIGURATION FILE" section of
+gitlink:git-config[1] for details.)
+
+[[exploring-git-history]]
+Exploring git history
+=====================
+
+Git is best thought of as a tool for storing the history of a
+collection of files.  It does this by storing compressed snapshots of
+the contents of a file hierarchy, together with "commits" which show
+the relationships between these snapshots.
+
+Git provides extremely flexible and fast tools for exploring the
+history of a project.
+
+We start with one specialized tool that is useful for finding the
+commit that introduced a bug into a project.
+
+[[using-bisect]]
+How to use bisect to find a regression
+--------------------------------------
+
+Suppose version 2.6.18 of your project worked, but the version at
+"master" crashes.  Sometimes the best way to find the cause of such a
+regression is to perform a brute-force search through the project's
+history to find the particular commit that caused the problem.  The
+gitlink:git-bisect[1] command can help you do this:
+
+-------------------------------------------------
+$ git bisect start
+$ git bisect good v2.6.18
+$ git bisect bad master
+Bisecting: 3537 revisions left to test after this
+[65934a9a028b88e83e2b0f8b36618fe503349f8e] BLOCK: Make USB storage depend on SCSI rather than selecting it [try #6]
+-------------------------------------------------
+
+If you run "git branch" at this point, you'll see that git has
+temporarily moved you to a new branch named "bisect".  This branch
+points to a commit (with commit id 65934...) that is reachable from
+"master" but not from v2.6.18.  Compile and test it, and see whether
+it crashes.  Assume it does crash.  Then:
+
+-------------------------------------------------
+$ git bisect bad
+Bisecting: 1769 revisions left to test after this
+[7eff82c8b1511017ae605f0c99ac275a7e21b867] i2c-core: Drop useless bitmaskings
+-------------------------------------------------
+
+checks out an older version.  Continue like this, telling git at each
+stage whether the version it gives you is good or bad, and notice
+that the number of revisions left to test is cut approximately in
+half each time.
+
+After about 13 tests (in this case), it will output the commit id of
+the guilty commit.  You can then examine the commit with
+gitlink:git-show[1], find out who wrote it, and mail them your bug
+report with the commit id.  Finally, run
+
+-------------------------------------------------
+$ git bisect reset
+-------------------------------------------------
+
+to return you to the branch you were on before and delete the
+temporary "bisect" branch.
+
+Note that the version which git-bisect checks out for you at each
+point is just a suggestion, and you're free to try a different
+version if you think it would be a good idea.  For example,
+occasionally you may land on a commit that broke something unrelated;
+run
+
+-------------------------------------------------
+$ git bisect visualize
+-------------------------------------------------
+
+which will run gitk and label the commit it chose with a marker that
+says "bisect".  Chose a safe-looking commit nearby, note its commit
+id, and check it out with:
+
+-------------------------------------------------
+$ git reset --hard fb47ddb2db...
+-------------------------------------------------
+
+then test, run "bisect good" or "bisect bad" as appropriate, and
+continue.
+
+[[naming-commits]]
+Naming commits
+--------------
+
+We have seen several ways of naming commits already:
+
+       - 40-hexdigit object name
+       - branch name: refers to the commit at the head of the given
+         branch
+       - tag name: refers to the commit pointed to by the given tag
+         (we've seen branches and tags are special cases of
+         <<how-git-stores-references,references>>).
+       - HEAD: refers to the head of the current branch
+
+There are many more; see the "SPECIFYING REVISIONS" section of the
+gitlink:git-rev-parse[1] man page for the complete list of ways to
+name revisions.  Some examples:
+
+-------------------------------------------------
+$ git show fb47ddb2 # the first few characters of the object name
+                   # are usually enough to specify it uniquely
+$ git show HEAD^    # the parent of the HEAD commit
+$ git show HEAD^^   # the grandparent
+$ git show HEAD~4   # the great-great-grandparent
+-------------------------------------------------
+
+Recall that merge commits may have more than one parent; by default,
+^ and ~ follow the first parent listed in the commit, but you can
+also choose:
+
+-------------------------------------------------
+$ git show HEAD^1   # show the first parent of HEAD
+$ git show HEAD^2   # show the second parent of HEAD
+-------------------------------------------------
+
+In addition to HEAD, there are several other special names for
+commits:
+
+Merges (to be discussed later), as well as operations such as
+git-reset, which change the currently checked-out commit, generally
+set ORIG_HEAD to the value HEAD had before the current operation.
+
+The git-fetch operation always stores the head of the last fetched
+branch in FETCH_HEAD.  For example, if you run git fetch without
+specifying a local branch as the target of the operation
+
+-------------------------------------------------
+$ git fetch git://example.com/proj.git theirbranch
+-------------------------------------------------
+
+the fetched commits will still be available from FETCH_HEAD.
+
+When we discuss merges we'll also see the special name MERGE_HEAD,
+which refers to the other branch that we're merging in to the current
+branch.
+
+The gitlink:git-rev-parse[1] command is a low-level command that is
+occasionally useful for translating some name for a commit to the object
+name for that commit:
+
+-------------------------------------------------
+$ git rev-parse origin
+e05db0fd4f31dde7005f075a84f96b360d05984b
+-------------------------------------------------
+
+[[creating-tags]]
+Creating tags
+-------------
+
+We can also create a tag to refer to a particular commit; after
+running
+
+-------------------------------------------------
+$ git tag stable-1 1b2e1d63ff
+-------------------------------------------------
+
+You can use stable-1 to refer to the commit 1b2e1d63ff.
+
+This creates a "lightweight" tag.  If you would also like to include a
+comment with the tag, and possibly sign it cryptographically, then you
+should create a tag object instead; see the gitlink:git-tag[1] man page
+for details.
+
+[[browsing-revisions]]
+Browsing revisions
+------------------
+
+The gitlink:git-log[1] command can show lists of commits.  On its
+own, it shows all commits reachable from the parent commit; but you
+can also make more specific requests:
+
+-------------------------------------------------
+$ git log v2.5..       # commits since (not reachable from) v2.5
+$ git log test..master # commits reachable from master but not test
+$ git log master..test # ...reachable from test but not master
+$ git log master...test        # ...reachable from either test or master,
+                       #    but not both
+$ git log --since="2 weeks ago" # commits from the last 2 weeks
+$ git log Makefile      # commits which modify Makefile
+$ git log fs/          # ... which modify any file under fs/
+$ git log -S'foo()'    # commits which add or remove any file data
+                       # matching the string 'foo()'
+-------------------------------------------------
+
+And of course you can combine all of these; the following finds
+commits since v2.5 which touch the Makefile or any file under fs:
+
+-------------------------------------------------
+$ git log v2.5.. Makefile fs/
+-------------------------------------------------
+
+You can also ask git log to show patches:
+
+-------------------------------------------------
+$ git log -p
+-------------------------------------------------
+
+See the "--pretty" option in the gitlink:git-log[1] man page for more
+display options.
+
+Note that git log starts with the most recent commit and works
+backwards through the parents; however, since git history can contain
+multiple independent lines of development, the particular order that
+commits are listed in may be somewhat arbitrary.
+
+[[generating-diffs]]
+Generating diffs
+----------------
+
+You can generate diffs between any two versions using
+gitlink:git-diff[1]:
+
+-------------------------------------------------
+$ git diff master..test
+-------------------------------------------------
+
+That will produce the diff between the tips of the two branches.  If
+you'd prefer to find the diff from their common ancestor to test, you
+can use three dots instead of two:
+
+-------------------------------------------------
+$ git diff master...test
+-------------------------------------------------
+
+Sometimes what you want instead is a set of patches; for this you can
+use gitlink:git-format-patch[1]:
+
+-------------------------------------------------
+$ git format-patch master..test
+-------------------------------------------------
+
+will generate a file with a patch for each commit reachable from test
+but not from master.
+
+[[viewing-old-file-versions]]
+Viewing old file versions
+-------------------------
+
+You can always view an old version of a file by just checking out the
+correct revision first.  But sometimes it is more convenient to be
+able to view an old version of a single file without checking
+anything out; this command does that:
+
+-------------------------------------------------
+$ git show v2.5:fs/locks.c
+-------------------------------------------------
+
+Before the colon may be anything that names a commit, and after it
+may be any path to a file tracked by git.
+
+[[history-examples]]
+Examples
+--------
+
+[[counting-commits-on-a-branch]]
+Counting the number of commits on a branch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Suppose you want to know how many commits you've made on "mybranch"
+since it diverged from "origin":
+
+-------------------------------------------------
+$ git log --pretty=oneline origin..mybranch | wc -l
+-------------------------------------------------
+
+Alternatively, you may often see this sort of thing done with the
+lower-level command gitlink:git-rev-list[1], which just lists the SHA1's
+of all the given commits:
+
+-------------------------------------------------
+$ git rev-list origin..mybranch | wc -l
+-------------------------------------------------
+
+[[checking-for-equal-branches]]
+Check whether two branches point at the same history
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Suppose you want to check whether two branches point at the same point
+in history.
+
+-------------------------------------------------
+$ git diff origin..master
+-------------------------------------------------
+
+will tell you whether the contents of the project are the same at the
+two branches; in theory, however, it's possible that the same project
+contents could have been arrived at by two different historical
+routes.  You could compare the object names:
+
+-------------------------------------------------
+$ git rev-list origin
+e05db0fd4f31dde7005f075a84f96b360d05984b
+$ git rev-list master
+e05db0fd4f31dde7005f075a84f96b360d05984b
+-------------------------------------------------
+
+Or you could recall that the ... operator selects all commits
+contained reachable from either one reference or the other but not
+both: so
+
+-------------------------------------------------
+$ git log origin...master
+-------------------------------------------------
+
+will return no commits when the two branches are equal.
+
+[[finding-tagged-descendants]]
+Find first tagged version including a given fix
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Suppose you know that the commit e05db0fd fixed a certain problem.
+You'd like to find the earliest tagged release that contains that
+fix.
+
+Of course, there may be more than one answer--if the history branched
+after commit e05db0fd, then there could be multiple "earliest" tagged
+releases.
+
+You could just visually inspect the commits since e05db0fd:
+
+-------------------------------------------------
+$ gitk e05db0fd..
+-------------------------------------------------
+
+Or you can use gitlink:git-name-rev[1], which will give the commit a
+name based on any tag it finds pointing to one of the commit's
+descendants:
+
+-------------------------------------------------
+$ git name-rev --tags e05db0fd
+e05db0fd tags/v1.5.0-rc1^0~23
+-------------------------------------------------
+
+The gitlink:git-describe[1] command does the opposite, naming the
+revision using a tag on which the given commit is based:
+
+-------------------------------------------------
+$ git describe e05db0fd
+v1.5.0-rc0-260-ge05db0f
+-------------------------------------------------
+
+but that may sometimes help you guess which tags might come after the
+given commit.
+
+If you just want to verify whether a given tagged version contains a
+given commit, you could use gitlink:git-merge-base[1]:
+
+-------------------------------------------------
+$ git merge-base e05db0fd v1.5.0-rc1
+e05db0fd4f31dde7005f075a84f96b360d05984b
+-------------------------------------------------
+
+The merge-base command finds a common ancestor of the given commits,
+and always returns one or the other in the case where one is a
+descendant of the other; so the above output shows that e05db0fd
+actually is an ancestor of v1.5.0-rc1.
+
+Alternatively, note that
+
+-------------------------------------------------
+$ git log v1.5.0-rc1..e05db0fd
+-------------------------------------------------
+
+will produce empty output if and only if v1.5.0-rc1 includes e05db0fd,
+because it outputs only commits that are not reachable from v1.5.0-rc1.
+
+As yet another alternative, the gitlink:git-show-branch[1] command lists
+the commits reachable from its arguments with a display on the left-hand
+side that indicates which arguments that commit is reachable from.  So,
+you can run something like
+
+-------------------------------------------------
+$ git show-branch e05db0fd v1.5.0-rc0 v1.5.0-rc1 v1.5.0-rc2
+! [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if
+available
+ ! [v1.5.0-rc0] GIT v1.5.0 preview
+  ! [v1.5.0-rc1] GIT v1.5.0-rc1
+   ! [v1.5.0-rc2] GIT v1.5.0-rc2
+...
+-------------------------------------------------
+
+then search for a line that looks like
+
+-------------------------------------------------
++ ++ [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if
+available
+-------------------------------------------------
+
+Which shows that e05db0fd is reachable from itself, from v1.5.0-rc1, and
+from v1.5.0-rc2, but not from v1.5.0-rc0.
+
+[[showing-commits-unique-to-a-branch]]
+Showing commits unique to a given branch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Suppose you would like to see all the commits reachable from the branch
+head named "master" but not from any other head in your repository.
+
+We can list all the heads in this repository with
+gitlink:git-show-ref[1]:
+
+-------------------------------------------------
+$ git show-ref --heads
+bf62196b5e363d73353a9dcf094c59595f3153b7 refs/heads/core-tutorial
+db768d5504c1bb46f63ee9d6e1772bd047e05bf9 refs/heads/maint
+a07157ac624b2524a059a3414e99f6f44bebc1e7 refs/heads/master
+24dbc180ea14dc1aebe09f14c8ecf32010690627 refs/heads/tutorial-2
+1e87486ae06626c2f31eaa63d26fc0fd646c8af2 refs/heads/tutorial-fixes
+-------------------------------------------------
+
+We can get just the branch-head names, and remove "master", with
+the help of the standard utilities cut and grep:
+
+-------------------------------------------------
+$ git show-ref --heads | cut -d' ' -f2 | grep -v '^refs/heads/master'
+refs/heads/core-tutorial
+refs/heads/maint
+refs/heads/tutorial-2
+refs/heads/tutorial-fixes
+-------------------------------------------------
+
+And then we can ask to see all the commits reachable from master
+but not from these other heads:
+
+-------------------------------------------------
+$ gitk master --not $( git show-ref --heads | cut -d' ' -f2 |
+                               grep -v '^refs/heads/master' )
+-------------------------------------------------
+
+Obviously, endless variations are possible; for example, to see all
+commits reachable from some head but not from any tag in the repository:
+
+-------------------------------------------------
+$ gitk $( git show-ref --heads ) --not  $( git show-ref --tags )
+-------------------------------------------------
+
+(See gitlink:git-rev-parse[1] for explanations of commit-selecting
+syntax such as `--not`.)
+
+[[making-a-release]]
+Creating a changelog and tarball for a software release
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The gitlink:git-archive[1] command can create a tar or zip archive from
+any version of a project; for example:
+
+-------------------------------------------------
+$ git archive --format=tar --prefix=project/ HEAD | gzip >latest.tar.gz
+-------------------------------------------------
+
+will use HEAD to produce a tar archive in which each filename is
+preceded by "project/".
+
+If you're releasing a new version of a software project, you may want
+to simultaneously make a changelog to include in the release
+announcement.
+
+Linus Torvalds, for example, makes new kernel releases by tagging them,
+then running:
+
+-------------------------------------------------
+$ release-script 2.6.12 2.6.13-rc6 2.6.13-rc7
+-------------------------------------------------
+
+where release-script is a shell script that looks like:
+
+-------------------------------------------------
+#!/bin/sh
+stable="$1"
+last="$2"
+new="$3"
+echo "# git tag v$new"
+echo "git archive --prefix=linux-$new/ v$new | gzip -9 > ../linux-$new.tar.gz"
+echo "git diff v$stable v$new | gzip -9 > ../patch-$new.gz"
+echo "git log --no-merges v$new ^v$last > ../ChangeLog-$new"
+echo "git shortlog --no-merges v$new ^v$last > ../ShortLog"
+echo "git diff --stat --summary -M v$last v$new > ../diffstat-$new"
+-------------------------------------------------
+
+and then he just cut-and-pastes the output commands after verifying that
+they look OK.
+
+[[Finding-comments-with-given-content]]
+Finding commits referencing a file with given content
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Somebody hands you a copy of a file, and asks which commits modified a
+file such that it contained the given content either before or after the
+commit.  You can find out with this:
+
+-------------------------------------------------
+$  git log --raw --abbrev=40 --pretty=oneline |
+       grep -B 1 `git hash-object filename`
+-------------------------------------------------
+
+Figuring out why this works is left as an exercise to the (advanced)
+student.  The gitlink:git-log[1], gitlink:git-diff-tree[1], and
+gitlink:git-hash-object[1] man pages may prove helpful.
+
+[[Developing-with-git]]
+Developing with git
+===================
+
+[[telling-git-your-name]]
+Telling git your name
+---------------------
+
+Before creating any commits, you should introduce yourself to git.  The
+easiest way to do so is to make sure the following lines appear in a
+file named .gitconfig in your home directory:
+
+------------------------------------------------
+[user]
+       name = Your Name Comes Here
+       email = you@yourdomain.example.com
+------------------------------------------------
+
+(See the "CONFIGURATION FILE" section of gitlink:git-config[1] for
+details on the configuration file.)
+
+
+[[creating-a-new-repository]]
+Creating a new repository
+-------------------------
+
+Creating a new repository from scratch is very easy:
+
+-------------------------------------------------
+$ mkdir project
+$ cd project
+$ git init
+-------------------------------------------------
+
+If you have some initial content (say, a tarball):
+
+-------------------------------------------------
+$ tar -xzvf project.tar.gz
+$ cd project
+$ git init
+$ git add . # include everything below ./ in the first commit:
+$ git commit
+-------------------------------------------------
+
+[[how-to-make-a-commit]]
+How to make a commit
+--------------------
+
+Creating a new commit takes three steps:
+
+       1. Making some changes to the working directory using your
+          favorite editor.
+       2. Telling git about your changes.
+       3. Creating the commit using the content you told git about
+          in step 2.
+
+In practice, you can interleave and repeat steps 1 and 2 as many
+times as you want: in order to keep track of what you want committed
+at step 3, git maintains a snapshot of the tree's contents in a
+special staging area called "the index."
+
+At the beginning, the content of the index will be identical to
+that of the HEAD.  The command "git diff --cached", which shows
+the difference between the HEAD and the index, should therefore
+produce no output at that point.
+
+Modifying the index is easy:
+
+To update the index with the new contents of a modified file, use
+
+-------------------------------------------------
+$ git add path/to/file
+-------------------------------------------------
+
+To add the contents of a new file to the index, use
+
+-------------------------------------------------
+$ git add path/to/file
+-------------------------------------------------
+
+To remove a file from the index and from the working tree,
+
+-------------------------------------------------
+$ git rm path/to/file
+-------------------------------------------------
+
+After each step you can verify that
+
+-------------------------------------------------
+$ git diff --cached
+-------------------------------------------------
+
+always shows the difference between the HEAD and the index file--this
+is what you'd commit if you created the commit now--and that
+
+-------------------------------------------------
+$ git diff
+-------------------------------------------------
+
+shows the difference between the working tree and the index file.
+
+Note that "git add" always adds just the current contents of a file
+to the index; further changes to the same file will be ignored unless
+you run git-add on the file again.
+
+When you're ready, just run
+
+-------------------------------------------------
+$ git commit
+-------------------------------------------------
+
+and git will prompt you for a commit message and then create the new
+commit.  Check to make sure it looks like what you expected with
+
+-------------------------------------------------
+$ git show
+-------------------------------------------------
+
+As a special shortcut,
+
+-------------------------------------------------
+$ git commit -a
+-------------------------------------------------
+
+will update the index with any files that you've modified or removed
+and create a commit, all in one step.
+
+A number of commands are useful for keeping track of what you're
+about to commit:
+
+-------------------------------------------------
+$ git diff --cached # difference between HEAD and the index; what
+                   # would be committed if you ran "commit" now.
+$ git diff         # difference between the index file and your
+                   # working directory; changes that would not
+                   # be included if you ran "commit" now.
+$ git diff HEAD            # difference between HEAD and working tree; what
+                   # would be committed if you ran "commit -a" now.
+$ git status       # a brief per-file summary of the above.
+-------------------------------------------------
+
+You can also use gitlink:git-gui[1] to create commits, view changes in
+the index and the working tree files, and individually select diff hunks
+for inclusion in the index (by right-clicking on the diff hunk and
+choosing "Stage Hunk For Commit").
+
+[[creating-good-commit-messages]]
+Creating good commit messages
+-----------------------------
+
+Though not required, it's a good idea to begin the commit message
+with a single short (less than 50 character) line summarizing the
+change, followed by a blank line and then a more thorough
+description.  Tools that turn commits into email, for example, use
+the first line on the Subject line and the rest of the commit in the
+body.
+
+[[ignoring-files]]
+Ignoring files
+--------------
+
+A project will often generate files that you do 'not' want to track with git.
+This typically includes files generated by a build process or temporary
+backup files made by your editor. Of course, 'not' tracking files with git
+is just a matter of 'not' calling "`git add`" on them. But it quickly becomes
+annoying to have these untracked files lying around; e.g. they make
+"`git add .`" and "`git commit -a`" practically useless, and they keep
+showing up in the output of "`git status`".
+
+You can tell git to ignore certain files by creating a file called .gitignore
+in the top level of your working directory, with contents such as:
+
+-------------------------------------------------
+# Lines starting with '#' are considered comments.
+# Ignore any file named foo.txt.
+foo.txt
+# Ignore (generated) html files,
+*.html
+# except foo.html which is maintained by hand.
+!foo.html
+# Ignore objects and archives.
+*.[oa]
+-------------------------------------------------
+
+See gitlink:gitignore[5] for a detailed explanation of the syntax.  You can
+also place .gitignore files in other directories in your working tree, and they
+will apply to those directories and their subdirectories.  The `.gitignore`
+files can be added to your repository like any other files (just run `git add
+.gitignore` and `git commit`, as usual), which is convenient when the exclude
+patterns (such as patterns matching build output files) would also make sense
+for other users who clone your repository.
+
+If you wish the exclude patterns to affect only certain repositories
+(instead of every repository for a given project), you may instead put
+them in a file in your repository named .git/info/exclude, or in any file
+specified by the `core.excludesfile` configuration variable.  Some git
+commands can also take exclude patterns directly on the command line.
+See gitlink:gitignore[5] for the details.
+
+[[how-to-merge]]
+How to merge
+------------
+
+You can rejoin two diverging branches of development using
+gitlink:git-merge[1]:
+
+-------------------------------------------------
+$ git merge branchname
+-------------------------------------------------
+
+merges the development in the branch "branchname" into the current
+branch.  If there are conflicts--for example, if the same file is
+modified in two different ways in the remote branch and the local
+branch--then you are warned; the output may look something like this:
+
+-------------------------------------------------
+$ git merge next
+ 100% (4/4) done
+Auto-merged file.txt
+CONFLICT (content): Merge conflict in file.txt
+Automatic merge failed; fix conflicts and then commit the result.
+-------------------------------------------------
+
+Conflict markers are left in the problematic files, and after
+you resolve the conflicts manually, you can update the index
+with the contents and run git commit, as you normally would when
+creating a new file.
+
+If you examine the resulting commit using gitk, you will see that it
+has two parents, one pointing to the top of the current branch, and
+one to the top of the other branch.
+
+[[resolving-a-merge]]
+Resolving a merge
+-----------------
+
+When a merge isn't resolved automatically, git leaves the index and
+the working tree in a special state that gives you all the
+information you need to help resolve the merge.
+
+Files with conflicts are marked specially in the index, so until you
+resolve the problem and update the index, gitlink:git-commit[1] will
+fail:
+
+-------------------------------------------------
+$ git commit
+file.txt: needs merge
+-------------------------------------------------
+
+Also, gitlink:git-status[1] will list those files as "unmerged", and the
+files with conflicts will have conflict markers added, like this:
+
+-------------------------------------------------
+<<<<<<< HEAD:file.txt
+Hello world
+=======
+Goodbye
+>>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt
+-------------------------------------------------
+
+All you need to do is edit the files to resolve the conflicts, and then
+
+-------------------------------------------------
+$ git add file.txt
+$ git commit
+-------------------------------------------------
+
+Note that the commit message will already be filled in for you with
+some information about the merge.  Normally you can just use this
+default message unchanged, but you may add additional commentary of
+your own if desired.
+
+The above is all you need to know to resolve a simple merge.  But git
+also provides more information to help resolve conflicts:
+
+[[conflict-resolution]]
+Getting conflict-resolution help during a merge
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+All of the changes that git was able to merge automatically are
+already added to the index file, so gitlink:git-diff[1] shows only
+the conflicts.  It uses an unusual syntax:
+
+-------------------------------------------------
+$ git diff
+diff --cc file.txt
+index 802992c,2b60207..0000000
+--- a/file.txt
++++ b/file.txt
+@@@ -1,1 -1,1 +1,5 @@@
+++<<<<<<< HEAD:file.txt
+ +Hello world
+++=======
++ Goodbye
+++>>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt
+-------------------------------------------------
+
+Recall that the commit which will be committed after we resolve this
+conflict will have two parents instead of the usual one: one parent
+will be HEAD, the tip of the current branch; the other will be the
+tip of the other branch, which is stored temporarily in MERGE_HEAD.
+
+During the merge, the index holds three versions of each file.  Each of
+these three "file stages" represents a different version of the file:
+
+-------------------------------------------------
+$ git show :1:file.txt # the file in a common ancestor of both branches
+$ git show :2:file.txt # the version from HEAD, but including any
+                       # nonconflicting changes from MERGE_HEAD
+$ git show :3:file.txt # the version from MERGE_HEAD, but including any
+                       # nonconflicting changes from HEAD.
+-------------------------------------------------
+
+Since the stage 2 and stage 3 versions have already been updated with
+nonconflicting changes, the only remaining differences between them are
+the important ones; thus gitlink:git-diff[1] can use the information in
+the index to show only those conflicts.
+
+The diff above shows the differences between the working-tree version of
+file.txt and the stage 2 and stage 3 versions.  So instead of preceding
+each line by a single "+" or "-", it now uses two columns: the first
+column is used for differences between the first parent and the working
+directory copy, and the second for differences between the second parent
+and the working directory copy.  (See the "COMBINED DIFF FORMAT" section
+of gitlink:git-diff-files[1] for a details of the format.)
+
+After resolving the conflict in the obvious way (but before updating the
+index), the diff will look like:
+
+-------------------------------------------------
+$ git diff
+diff --cc file.txt
+index 802992c,2b60207..0000000
+--- a/file.txt
++++ b/file.txt
+@@@ -1,1 -1,1 +1,1 @@@
+- Hello world
+ -Goodbye
+++Goodbye world
+-------------------------------------------------
+
+This shows that our resolved version deleted "Hello world" from the
+first parent, deleted "Goodbye" from the second parent, and added
+"Goodbye world", which was previously absent from both.
+
+Some special diff options allow diffing the working directory against
+any of these stages:
+
+-------------------------------------------------
+$ git diff -1 file.txt         # diff against stage 1
+$ git diff --base file.txt     # same as the above
+$ git diff -2 file.txt         # diff against stage 2
+$ git diff --ours file.txt     # same as the above
+$ git diff -3 file.txt         # diff against stage 3
+$ git diff --theirs file.txt   # same as the above.
+-------------------------------------------------
+
+The gitlink:git-log[1] and gitk[1] commands also provide special help
+for merges:
+
+-------------------------------------------------
+$ git log --merge
+$ gitk --merge
+-------------------------------------------------
+
+These will display all commits which exist only on HEAD or on
+MERGE_HEAD, and which touch an unmerged file.
+
+You may also use gitlink:git-mergetool[1], which lets you merge the
+unmerged files using external tools such as emacs or kdiff3.
+
+Each time you resolve the conflicts in a file and update the index:
+
+-------------------------------------------------
+$ git add file.txt
+-------------------------------------------------
+
+the different stages of that file will be "collapsed", after which
+git-diff will (by default) no longer show diffs for that file.
+
+[[undoing-a-merge]]
+Undoing a merge
+---------------
+
+If you get stuck and decide to just give up and throw the whole mess
+away, you can always return to the pre-merge state with
+
+-------------------------------------------------
+$ git reset --hard HEAD
+-------------------------------------------------
+
+Or, if you've already committed the merge that you want to throw away,
+
+-------------------------------------------------
+$ git reset --hard ORIG_HEAD
+-------------------------------------------------
+
+However, this last command can be dangerous in some cases--never
+throw away a commit you have already committed if that commit may
+itself have been merged into another branch, as doing so may confuse
+further merges.
+
+[[fast-forwards]]
+Fast-forward merges
+-------------------
+
+There is one special case not mentioned above, which is treated
+differently.  Normally, a merge results in a merge commit, with two
+parents, one pointing at each of the two lines of development that
+were merged.
+
+However, if the current branch is a descendant of the other--so every
+commit present in the one is already contained in the other--then git
+just performs a "fast forward"; the head of the current branch is moved
+forward to point at the head of the merged-in branch, without any new
+commits being created.
+
+[[fixing-mistakes]]
+Fixing mistakes
+---------------
+
+If you've messed up the working tree, but haven't yet committed your
+mistake, you can return the entire working tree to the last committed
+state with
+
+-------------------------------------------------
+$ git reset --hard HEAD
+-------------------------------------------------
+
+If you make a commit that you later wish you hadn't, there are two
+fundamentally different ways to fix the problem:
+
+       1. You can create a new commit that undoes whatever was done
+       by the old commit.  This is the correct thing if your
+       mistake has already been made public.
+
+       2. You can go back and modify the old commit.  You should
+       never do this if you have already made the history public;
+       git does not normally expect the "history" of a project to
+       change, and cannot correctly perform repeated merges from
+       a branch that has had its history changed.
+
+[[reverting-a-commit]]
+Fixing a mistake with a new commit
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Creating a new commit that reverts an earlier change is very easy;
+just pass the gitlink:git-revert[1] command a reference to the bad
+commit; for example, to revert the most recent commit:
+
+-------------------------------------------------
+$ git revert HEAD
+-------------------------------------------------
+
+This will create a new commit which undoes the change in HEAD.  You
+will be given a chance to edit the commit message for the new commit.
+
+You can also revert an earlier change, for example, the next-to-last:
+
+-------------------------------------------------
+$ git revert HEAD^
+-------------------------------------------------
+
+In this case git will attempt to undo the old change while leaving
+intact any changes made since then.  If more recent changes overlap
+with the changes to be reverted, then you will be asked to fix
+conflicts manually, just as in the case of <<resolving-a-merge,
+resolving a merge>>.
+
+[[fixing-a-mistake-by-rewriting-history]]
+Fixing a mistake by rewriting history
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If the problematic commit is the most recent commit, and you have not
+yet made that commit public, then you may just
+<<undoing-a-merge,destroy it using git-reset>>.
+
+Alternatively, you
+can edit the working directory and update the index to fix your
+mistake, just as if you were going to <<how-to-make-a-commit,create a
+new commit>>, then run
+
+-------------------------------------------------
+$ git commit --amend
+-------------------------------------------------
+
+which will replace the old commit by a new commit incorporating your
+changes, giving you a chance to edit the old commit message first.
+
+Again, you should never do this to a commit that may already have
+been merged into another branch; use gitlink:git-revert[1] instead in
+that case.
+
+It is also possible to replace commits further back in the history, but
+this is an advanced topic to be left for
+<<cleaning-up-history,another chapter>>.
+
+[[checkout-of-path]]
+Checking out an old version of a file
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the process of undoing a previous bad change, you may find it
+useful to check out an older version of a particular file using
+gitlink:git-checkout[1].  We've used git checkout before to switch
+branches, but it has quite different behavior if it is given a path
+name: the command
+
+-------------------------------------------------
+$ git checkout HEAD^ path/to/file
+-------------------------------------------------
+
+replaces path/to/file by the contents it had in the commit HEAD^, and
+also updates the index to match.  It does not change branches.
+
+If you just want to look at an old version of the file, without
+modifying the working directory, you can do that with
+gitlink:git-show[1]:
+
+-------------------------------------------------
+$ git show HEAD^:path/to/file
+-------------------------------------------------
+
+which will display the given version of the file.
+
+[[interrupted-work]]
+Temporarily setting aside work in progress
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+While you are in the middle of working on something complicated, you
+find an unrelated but obvious and trivial bug.  You would like to fix it
+before continuing.  You can use gitlink:git-stash[1] to save the current
+state of your work, and after fixing the bug (or, optionally after doing
+so on a different branch and then coming back), unstash the
+work-in-progress changes.
+
+------------------------------------------------
+$ git stash "work in progress for foo feature"
+------------------------------------------------
+
+This command will save your changes away to the `stash`, and
+reset your working tree and the index to match the tip of your
+current branch.  Then you can make your fix as usual.
+
+------------------------------------------------
+... edit and test ...
+$ git commit -a -m "blorpl: typofix"
+------------------------------------------------
+
+After that, you can go back to what you were working on with
+`git stash apply`:
+
+------------------------------------------------
+$ git stash apply
+------------------------------------------------
+
+
+[[ensuring-good-performance]]
+Ensuring good performance
+-------------------------
+
+On large repositories, git depends on compression to keep the history
+information from taking up too much space on disk or in memory.
+
+This compression is not performed automatically.  Therefore you
+should occasionally run gitlink:git-gc[1]:
+
+-------------------------------------------------
+$ git gc
+-------------------------------------------------
+
+to recompress the archive.  This can be very time-consuming, so
+you may prefer to run git-gc when you are not doing other work.
+
+
+[[ensuring-reliability]]
+Ensuring reliability
+--------------------
+
+[[checking-for-corruption]]
+Checking the repository for corruption
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The gitlink:git-fsck[1] command runs a number of self-consistency checks
+on the repository, and reports on any problems.  This may take some
+time.  The most common warning by far is about "dangling" objects:
+
+-------------------------------------------------
+$ git fsck
+dangling commit 7281251ddd2a61e38657c827739c57015671a6b3
+dangling commit 2706a059f258c6b245f298dc4ff2ccd30ec21a63
+dangling commit 13472b7c4b80851a1bc551779171dcb03655e9b5
+dangling blob 218761f9d90712d37a9c5e36f406f92202db07eb
+dangling commit bf093535a34a4d35731aa2bd90fe6b176302f14f
+dangling commit 8e4bec7f2ddaa268bef999853c25755452100f8e
+dangling tree d50bb86186bf27b681d25af89d3b5b68382e4085
+dangling tree b24c2473f1fd3d91352a624795be026d64c8841f
+...
+-------------------------------------------------
+
+Dangling objects are not a problem.  At worst they may take up a little
+extra disk space.  They can sometimes provide a last-resort method for
+recovering lost work--see <<dangling-objects>> for details.  However, if
+you wish, you can remove them with gitlink:git-prune[1] or the `--prune`
+option to gitlink:git-gc[1]:
+
+-------------------------------------------------
+$ git gc --prune
+-------------------------------------------------
+
+This may be time-consuming.  Unlike most other git operations (including
+git-gc when run without any options), it is not safe to prune while
+other git operations are in progress in the same repository.
+
+If gitlink:git-fsck[1] complains about sha1 mismatches or missing
+objects, you may have a much more serious problem; your best option is
+probably restoring from backups.  See
+<<recovering-from-repository-corruption>> for a detailed discussion.
+
+[[recovering-lost-changes]]
+Recovering lost changes
+~~~~~~~~~~~~~~~~~~~~~~~
+
+[[reflogs]]
+Reflogs
+^^^^^^^
+
+Say you modify a branch with `gitlink:git-reset[1] --hard`, and then
+realize that the branch was the only reference you had to that point in
+history.
+
+Fortunately, git also keeps a log, called a "reflog", of all the
+previous values of each branch.  So in this case you can still find the
+old history using, for example,
+
+-------------------------------------------------
+$ git log master@{1}
+-------------------------------------------------
+
+This lists the commits reachable from the previous version of the
+"master" branch head.  This syntax can be used with any git command
+that accepts a commit, not just with git log.  Some other examples:
+
+-------------------------------------------------
+$ git show master@{2}          # See where the branch pointed 2,
+$ git show master@{3}          # 3, ... changes ago.
+$ gitk master@{yesterday}      # See where it pointed yesterday,
+$ gitk master@{"1 week ago"}   # ... or last week
+$ git log --walk-reflogs master        # show reflog entries for master
+-------------------------------------------------
+
+A separate reflog is kept for the HEAD, so
+
+-------------------------------------------------
+$ git show HEAD@{"1 week ago"}
+-------------------------------------------------
+
+will show what HEAD pointed to one week ago, not what the current branch
+pointed to one week ago.  This allows you to see the history of what
+you've checked out.
+
+The reflogs are kept by default for 30 days, after which they may be
+pruned.  See gitlink:git-reflog[1] and gitlink:git-gc[1] to learn
+how to control this pruning, and see the "SPECIFYING REVISIONS"
+section of gitlink:git-rev-parse[1] for details.
+
+Note that the reflog history is very different from normal git history.
+While normal history is shared by every repository that works on the
+same project, the reflog history is not shared: it tells you only about
+how the branches in your local repository have changed over time.
+
+[[dangling-object-recovery]]
+Examining dangling objects
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In some situations the reflog may not be able to save you.  For example,
+suppose you delete a branch, then realize you need the history it
+contained.  The reflog is also deleted; however, if you have not yet
+pruned the repository, then you may still be able to find the lost
+commits in the dangling objects that git-fsck reports.  See
+<<dangling-objects>> for the details.
+
+-------------------------------------------------
+$ git fsck
+dangling commit 7281251ddd2a61e38657c827739c57015671a6b3
+dangling commit 2706a059f258c6b245f298dc4ff2ccd30ec21a63
+dangling commit 13472b7c4b80851a1bc551779171dcb03655e9b5
+...
+-------------------------------------------------
+
+You can examine
+one of those dangling commits with, for example,
+
+------------------------------------------------
+$ gitk 7281251ddd --not --all
+------------------------------------------------
+
+which does what it sounds like: it says that you want to see the commit
+history that is described by the dangling commit(s), but not the
+history that is described by all your existing branches and tags.  Thus
+you get exactly the history reachable from that commit that is lost.
+(And notice that it might not be just one commit: we only report the
+"tip of the line" as being dangling, but there might be a whole deep
+and complex commit history that was dropped.)
+
+If you decide you want the history back, you can always create a new
+reference pointing to it, for example, a new branch:
+
+------------------------------------------------
+$ git branch recovered-branch 7281251ddd
+------------------------------------------------
+
+Other types of dangling objects (blobs and trees) are also possible, and
+dangling objects can arise in other situations.
+
+
+[[sharing-development]]
+Sharing development with others
+===============================
+
+[[getting-updates-with-git-pull]]
+Getting updates with git pull
+-----------------------------
+
+After you clone a repository and make a few changes of your own, you
+may wish to check the original repository for updates and merge them
+into your own work.
+
+We have already seen <<Updating-a-repository-with-git-fetch,how to
+keep remote tracking branches up to date>> with gitlink:git-fetch[1],
+and how to merge two branches.  So you can merge in changes from the
+original repository's master branch with:
+
+-------------------------------------------------
+$ git fetch
+$ git merge origin/master
+-------------------------------------------------
+
+However, the gitlink:git-pull[1] command provides a way to do this in
+one step:
+
+-------------------------------------------------
+$ git pull origin master
+-------------------------------------------------
+
+In fact, if you have "master" checked out, then by default "git pull"
+merges from the HEAD branch of the origin repository.  So often you can
+accomplish the above with just a simple
+
+-------------------------------------------------
+$ git pull
+-------------------------------------------------
+
+More generally, a branch that is created from a remote branch will pull
+by default from that branch.  See the descriptions of the
+branch.<name>.remote and branch.<name>.merge options in
+gitlink:git-config[1], and the discussion of the `--track` option in
+gitlink:git-checkout[1], to learn how to control these defaults.
+
+In addition to saving you keystrokes, "git pull" also helps you by
+producing a default commit message documenting the branch and
+repository that you pulled from.
+
+(But note that no such commit will be created in the case of a
+<<fast-forwards,fast forward>>; instead, your branch will just be
+updated to point to the latest commit from the upstream branch.)
+
+The git-pull command can also be given "." as the "remote" repository,
+in which case it just merges in a branch from the current repository; so
+the commands
+
+-------------------------------------------------
+$ git pull . branch
+$ git merge branch
+-------------------------------------------------
+
+are roughly equivalent.  The former is actually very commonly used.
+
+[[submitting-patches]]
+Submitting patches to a project
+-------------------------------
+
+If you just have a few changes, the simplest way to submit them may
+just be to send them as patches in email:
+
+First, use gitlink:git-format-patch[1]; for example:
+
+-------------------------------------------------
+$ git format-patch origin
+-------------------------------------------------
+
+will produce a numbered series of files in the current directory, one
+for each patch in the current branch but not in origin/HEAD.
+
+You can then import these into your mail client and send them by
+hand.  However, if you have a lot to send at once, you may prefer to
+use the gitlink:git-send-email[1] script to automate the process.
+Consult the mailing list for your project first to determine how they
+prefer such patches be handled.
+
+[[importing-patches]]
+Importing patches to a project
+------------------------------
+
+Git also provides a tool called gitlink:git-am[1] (am stands for
+"apply mailbox"), for importing such an emailed series of patches.
+Just save all of the patch-containing messages, in order, into a
+single mailbox file, say "patches.mbox", then run
+
+-------------------------------------------------
+$ git am -3 patches.mbox
+-------------------------------------------------
+
+Git will apply each patch in order; if any conflicts are found, it
+will stop, and you can fix the conflicts as described in
+"<<resolving-a-merge,Resolving a merge>>".  (The "-3" option tells
+git to perform a merge; if you would prefer it just to abort and
+leave your tree and index untouched, you may omit that option.)
+
+Once the index is updated with the results of the conflict
+resolution, instead of creating a new commit, just run
+
+-------------------------------------------------
+$ git am --resolved
+-------------------------------------------------
+
+and git will create the commit for you and continue applying the
+remaining patches from the mailbox.
+
+The final result will be a series of commits, one for each patch in
+the original mailbox, with authorship and commit log message each
+taken from the message containing each patch.
+
+[[public-repositories]]
+Public git repositories
+-----------------------
+
+Another way to submit changes to a project is to tell the maintainer
+of that project to pull the changes from your repository using
+gitlink:git-pull[1].  In the section "<<getting-updates-with-git-pull,
+Getting updates with git pull>>" we described this as a way to get
+updates from the "main" repository, but it works just as well in the
+other direction.
+
+If you and the maintainer both have accounts on the same machine, then
+you can just pull changes from each other's repositories directly;
+commands that accept repository URLs as arguments will also accept a
+local directory name:
+
+-------------------------------------------------
+$ git clone /path/to/repository
+$ git pull /path/to/other/repository
+-------------------------------------------------
+
+or an ssh URL:
+
+-------------------------------------------------
+$ git clone ssh://yourhost/~you/repository
+-------------------------------------------------
+
+For projects with few developers, or for synchronizing a few private
+repositories, this may be all you need.
+
+However, the more common way to do this is to maintain a separate public
+repository (usually on a different host) for others to pull changes
+from.  This is usually more convenient, and allows you to cleanly
+separate private work in progress from publicly visible work.
+
+You will continue to do your day-to-day work in your personal
+repository, but periodically "push" changes from your personal
+repository into your public repository, allowing other developers to
+pull from that repository.  So the flow of changes, in a situation
+where there is one other developer with a public repository, looks
+like this:
+
+                        you push
+  your personal repo ------------------> your public repo
+       ^                                     |
+       |                                     |
+       | you pull                            | they pull
+       |                                     |
+       |                                     |
+        |               they push             V
+  their public repo <------------------- their repo
+
+We explain how to do this in the following sections.
+
+[[setting-up-a-public-repository]]
+Setting up a public repository
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Assume your personal repository is in the directory ~/proj.  We
+first create a new clone of the repository and tell git-daemon that it
+is meant to be public:
+
+-------------------------------------------------
+$ git clone --bare ~/proj proj.git
+$ touch proj.git/git-daemon-export-ok
+-------------------------------------------------
+
+The resulting directory proj.git contains a "bare" git repository--it is
+just the contents of the ".git" directory, without any files checked out
+around it.
+
+Next, copy proj.git to the server where you plan to host the
+public repository.  You can use scp, rsync, or whatever is most
+convenient.
+
+[[exporting-via-git]]
+Exporting a git repository via the git protocol
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is the preferred method.
+
+If someone else administers the server, they should tell you what
+directory to put the repository in, and what git:// URL it will appear
+at.  You can then skip to the section
+"<<pushing-changes-to-a-public-repository,Pushing changes to a public
+repository>>", below.
+
+Otherwise, all you need to do is start gitlink:git-daemon[1]; it will
+listen on port 9418.  By default, it will allow access to any directory
+that looks like a git directory and contains the magic file
+git-daemon-export-ok.  Passing some directory paths as git-daemon
+arguments will further restrict the exports to those paths.
+
+You can also run git-daemon as an inetd service; see the
+gitlink:git-daemon[1] man page for details.  (See especially the
+examples section.)
+
+[[exporting-via-http]]
+Exporting a git repository via http
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The git protocol gives better performance and reliability, but on a
+host with a web server set up, http exports may be simpler to set up.
+
+All you need to do is place the newly created bare git repository in
+a directory that is exported by the web server, and make some
+adjustments to give web clients some extra information they need:
+
+-------------------------------------------------
+$ mv proj.git /home/you/public_html/proj.git
+$ cd proj.git
+$ git --bare update-server-info
+$ chmod a+x hooks/post-update
+-------------------------------------------------
+
+(For an explanation of the last two lines, see
+gitlink:git-update-server-info[1], and the documentation
+link:hooks.html[Hooks used by git].)
+
+Advertise the URL of proj.git.  Anybody else should then be able to
+clone or pull from that URL, for example with a command line like:
+
+-------------------------------------------------
+$ git clone http://yourserver.com/~you/proj.git
+-------------------------------------------------
+
+(See also
+link:howto/setup-git-server-over-http.txt[setup-git-server-over-http]
+for a slightly more sophisticated setup using WebDAV which also
+allows pushing over http.)
+
+[[pushing-changes-to-a-public-repository]]
+Pushing changes to a public repository
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Note that the two techniques outlined above (exporting via
+<<exporting-via-http,http>> or <<exporting-via-git,git>>) allow other
+maintainers to fetch your latest changes, but they do not allow write
+access, which you will need to update the public repository with the
+latest changes created in your private repository.
+
+The simplest way to do this is using gitlink:git-push[1] and ssh; to
+update the remote branch named "master" with the latest state of your
+branch named "master", run
+
+-------------------------------------------------
+$ git push ssh://yourserver.com/~you/proj.git master:master
+-------------------------------------------------
+
+or just
+
+-------------------------------------------------
+$ git push ssh://yourserver.com/~you/proj.git master
+-------------------------------------------------
+
+As with git-fetch, git-push will complain if this does not result in a
+<<fast-forwards,fast forward>>; see the following section for details on
+handling this case.
+
+Note that the target of a "push" is normally a
+<<def_bare_repository,bare>> repository.  You can also push to a
+repository that has a checked-out working tree, but the working tree
+will not be updated by the push.  This may lead to unexpected results if
+the branch you push to is the currently checked-out branch!
+
+As with git-fetch, you may also set up configuration options to
+save typing; so, for example, after
+
+-------------------------------------------------
+$ cat >>.git/config <<EOF
+[remote "public-repo"]
+       url = ssh://yourserver.com/~you/proj.git
+EOF
+-------------------------------------------------
+
+you should be able to perform the above push with just
+
+-------------------------------------------------
+$ git push public-repo master
+-------------------------------------------------
+
+See the explanations of the remote.<name>.url, branch.<name>.remote,
+and remote.<name>.push options in gitlink:git-config[1] for
+details.
+
+[[forcing-push]]
+What to do when a push fails
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If a push would not result in a <<fast-forwards,fast forward>> of the
+remote branch, then it will fail with an error like:
+
+-------------------------------------------------
+error: remote 'refs/heads/master' is not an ancestor of
+ local  'refs/heads/master'.
+ Maybe you are not up-to-date and need to pull first?
+error: failed to push to 'ssh://yourserver.com/~you/proj.git'
+-------------------------------------------------
+
+This can happen, for example, if you:
+
+       - use `git reset --hard` to remove already-published commits, or
+       - use `git commit --amend` to replace already-published commits
+         (as in <<fixing-a-mistake-by-rewriting-history>>), or
+       - use `git rebase` to rebase any already-published commits (as
+         in <<using-git-rebase>>).
+
+You may force git-push to perform the update anyway by preceding the
+branch name with a plus sign:
+
+-------------------------------------------------
+$ git push ssh://yourserver.com/~you/proj.git +master
+-------------------------------------------------
+
+Normally whenever a branch head in a public repository is modified, it
+is modified to point to a descendent of the commit that it pointed to
+before.  By forcing a push in this situation, you break that convention.
+(See <<problems-with-rewriting-history>>.)
+
+Nevertheless, this is a common practice for people that need a simple
+way to publish a work-in-progress patch series, and it is an acceptable
+compromise as long as you warn other developers that this is how you
+intend to manage the branch.
+
+It's also possible for a push to fail in this way when other people have
+the right to push to the same repository.  In that case, the correct
+solution is to retry the push after first updating your work by either a
+pull or a fetch followed by a rebase; see the
+<<setting-up-a-shared-repository,next section>> and
+link:cvs-migration.html[git for CVS users] for more.
+
+[[setting-up-a-shared-repository]]
+Setting up a shared repository
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Another way to collaborate is by using a model similar to that
+commonly used in CVS, where several developers with special rights
+all push to and pull from a single shared repository.  See
+link:cvs-migration.html[git for CVS users] for instructions on how to
+set this up.
+
+However, while there is nothing wrong with git's support for shared
+repositories, this mode of operation is not generally recommended,
+simply because the mode of collaboration that git supports--by
+exchanging patches and pulling from public repositories--has so many
+advantages over the central shared repository:
+
+       - Git's ability to quickly import and merge patches allows a
+         single maintainer to process incoming changes even at very
+         high rates.  And when that becomes too much, git-pull provides
+         an easy way for that maintainer to delegate this job to other
+         maintainers while still allowing optional review of incoming
+         changes.
+       - Since every developer's repository has the same complete copy
+         of the project history, no repository is special, and it is
+         trivial for another developer to take over maintenance of a
+         project, either by mutual agreement, or because a maintainer
+         becomes unresponsive or difficult to work with.
+       - The lack of a central group of "committers" means there is
+         less need for formal decisions about who is "in" and who is
+         "out".
+
+[[setting-up-gitweb]]
+Allowing web browsing of a repository
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The gitweb cgi script provides users an easy way to browse your
+project's files and history without having to install git; see the file
+gitweb/INSTALL in the git source tree for instructions on setting it up.
+
+[[sharing-development-examples]]
+Examples
+--------
+
+[[maintaining-topic-branches]]
+Maintaining topic branches for a Linux subsystem maintainer
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This describes how Tony Luck uses git in his role as maintainer of the
+IA64 architecture for the Linux kernel.
+
+He uses two public branches:
+
+ - A "test" tree into which patches are initially placed so that they
+   can get some exposure when integrated with other ongoing development.
+   This tree is available to Andrew for pulling into -mm whenever he
+   wants.
+
+ - A "release" tree into which tested patches are moved for final sanity
+   checking, and as a vehicle to send them upstream to Linus (by sending
+   him a "please pull" request.)
+
+He also uses a set of temporary branches ("topic branches"), each
+containing a logical grouping of patches.
+
+To set this up, first create your work tree by cloning Linus's public
+tree:
+
+-------------------------------------------------
+$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git work
+$ cd work
+-------------------------------------------------
+
+Linus's tree will be stored in the remote branch named origin/master,
+and can be updated using gitlink:git-fetch[1]; you can track other
+public trees using gitlink:git-remote[1] to set up a "remote" and
+gitlink:git-fetch[1] to keep them up-to-date; see
+<<repositories-and-branches>>.
+
+Now create the branches in which you are going to work; these start out
+at the current tip of origin/master branch, and should be set up (using
+the --track option to gitlink:git-branch[1]) to merge changes in from
+Linus by default.
+
+-------------------------------------------------
+$ git branch --track test origin/master
+$ git branch --track release origin/master
+-------------------------------------------------
+
+These can be easily kept up to date using gitlink:git-pull[1].
+
+-------------------------------------------------
+$ git checkout test && git pull
+$ git checkout release && git pull
+-------------------------------------------------
+
+Important note!  If you have any local changes in these branches, then
+this merge will create a commit object in the history (with no local
+changes git will simply do a "Fast forward" merge).  Many people dislike
+the "noise" that this creates in the Linux history, so you should avoid
+doing this capriciously in the "release" branch, as these noisy commits
+will become part of the permanent history when you ask Linus to pull
+from the release branch.
+
+A few configuration variables (see gitlink:git-config[1]) can
+make it easy to push both branches to your public tree.  (See
+<<setting-up-a-public-repository>>.)
+
+-------------------------------------------------
+$ cat >> .git/config <<EOF
+[remote "mytree"]
+       url =  master.kernel.org:/pub/scm/linux/kernel/git/aegl/linux-2.6.git
+       push = release
+       push = test
+EOF
+-------------------------------------------------
+
+Then you can push both the test and release trees using
+gitlink:git-push[1]:
+
+-------------------------------------------------
+$ git push mytree
+-------------------------------------------------
+
+or push just one of the test and release branches using:
+
+-------------------------------------------------
+$ git push mytree test
+-------------------------------------------------
+
+or
+
+-------------------------------------------------
+$ git push mytree release
+-------------------------------------------------
+
+Now to apply some patches from the community.  Think of a short
+snappy name for a branch to hold this patch (or related group of
+patches), and create a new branch from the current tip of Linus's
+branch:
+
+-------------------------------------------------
+$ git checkout -b speed-up-spinlocks origin
+-------------------------------------------------
+
+Now you apply the patch(es), run some tests, and commit the change(s).  If
+the patch is a multi-part series, then you should apply each as a separate
+commit to this branch.
+
+-------------------------------------------------
+$ ... patch ... test  ... commit [ ... patch ... test ... commit ]*
+-------------------------------------------------
+
+When you are happy with the state of this change, you can pull it into the
+"test" branch in preparation to make it public:
+
+-------------------------------------------------
+$ git checkout test && git pull . speed-up-spinlocks
+-------------------------------------------------
+
+It is unlikely that you would have any conflicts here ... but you might if you
+spent a while on this step and had also pulled new versions from upstream.
+
+Some time later when enough time has passed and testing done, you can pull the
+same branch into the "release" tree ready to go upstream.  This is where you
+see the value of keeping each patch (or patch series) in its own branch.  It
+means that the patches can be moved into the "release" tree in any order.
+
+-------------------------------------------------
+$ git checkout release && git pull . speed-up-spinlocks
+-------------------------------------------------
+
+After a while, you will have a number of branches, and despite the
+well chosen names you picked for each of them, you may forget what
+they are for, or what status they are in.  To get a reminder of what
+changes are in a specific branch, use:
+
+-------------------------------------------------
+$ git log linux..branchname | git-shortlog
+-------------------------------------------------
+
+To see whether it has already been merged into the test or release branches,
+use:
+
+-------------------------------------------------
+$ git log test..branchname
+-------------------------------------------------
+
+or
+
+-------------------------------------------------
+$ git log release..branchname
+-------------------------------------------------
+
+(If this branch has not yet been merged, you will see some log entries.
+If it has been merged, then there will be no output.)
+
+Once a patch completes the great cycle (moving from test to release,
+then pulled by Linus, and finally coming back into your local
+"origin/master" branch), the branch for this change is no longer needed.
+You detect this when the output from:
+
+-------------------------------------------------
+$ git log origin..branchname
+-------------------------------------------------
+
+is empty.  At this point the branch can be deleted:
+
+-------------------------------------------------
+$ git branch -d branchname
+-------------------------------------------------
+
+Some changes are so trivial that it is not necessary to create a separate
+branch and then merge into each of the test and release branches.  For
+these changes, just apply directly to the "release" branch, and then
+merge that into the "test" branch.
+
+To create diffstat and shortlog summaries of changes to include in a "please
+pull" request to Linus you can use:
+
+-------------------------------------------------
+$ git diff --stat origin..release
+-------------------------------------------------
+
+and
+
+-------------------------------------------------
+$ git log -p origin..release | git shortlog
+-------------------------------------------------
+
+Here are some of the scripts that simplify all this even further.
+
+-------------------------------------------------
+==== update script ====
+# Update a branch in my GIT tree.  If the branch to be updated
+# is origin, then pull from kernel.org.  Otherwise merge
+# origin/master branch into test|release branch
+
+case "$1" in
+test|release)
+       git checkout $1 && git pull . origin
+       ;;
+origin)
+       before=$(git rev-parse refs/remotes/origin/master)
+       git fetch origin
+       after=$(git rev-parse refs/remotes/origin/master)
+       if [ $before != $after ]
+       then
+               git log $before..$after | git shortlog
+       fi
+       ;;
+*)
+       echo "Usage: $0 origin|test|release" 1>&2
+       exit 1
+       ;;
+esac
+-------------------------------------------------
+
+-------------------------------------------------
+==== merge script ====
+# Merge a branch into either the test or release branch
+
+pname=$0
+
+usage()
+{
+       echo "Usage: $pname branch test|release" 1>&2
+       exit 1
+}
+
+git show-ref -q --verify -- refs/heads/"$1" || {
+       echo "Can't see branch <$1>" 1>&2
+       usage
+}
+
+case "$2" in
+test|release)
+       if [ $(git log $2..$1 | wc -c) -eq 0 ]
+       then
+               echo $1 already merged into $2 1>&2
+               exit 1
+       fi
+       git checkout $2 && git pull . $1
+       ;;
+*)
+       usage
+       ;;
+esac
+-------------------------------------------------
+
+-------------------------------------------------
+==== status script ====
+# report on status of my ia64 GIT tree
+
+gb=$(tput setab 2)
+rb=$(tput setab 1)
+restore=$(tput setab 9)
+
+if [ `git rev-list test..release | wc -c` -gt 0 ]
+then
+       echo $rb Warning: commits in release that are not in test $restore
+       git log test..release
+fi
+
+for branch in `git show-ref --heads | sed 's|^.*/||'`
+do
+       if [ $branch = test -o $branch = release ]
+       then
+               continue
+       fi
+
+       echo -n $gb ======= $branch ====== $restore " "
+       status=
+       for ref in test release origin/master
+       do
+               if [ `git rev-list $ref..$branch | wc -c` -gt 0 ]
+               then
+                       status=$status${ref:0:1}
+               fi
+       done
+       case $status in
+       trl)
+               echo $rb Need to pull into test $restore
+               ;;
+       rl)
+               echo "In test"
+               ;;
+       l)
+               echo "Waiting for linus"
+               ;;
+       "")
+               echo $rb All done $restore
+               ;;
+       *)
+               echo $rb "<$status>" $restore
+               ;;
+       esac
+       git log origin/master..$branch | git shortlog
+done
+-------------------------------------------------
+
+
+[[cleaning-up-history]]
+Rewriting history and maintaining patch series
+==============================================
+
+Normally commits are only added to a project, never taken away or
+replaced.  Git is designed with this assumption, and violating it will
+cause git's merge machinery (for example) to do the wrong thing.
+
+However, there is a situation in which it can be useful to violate this
+assumption.
+
+[[patch-series]]
+Creating the perfect patch series
+---------------------------------
+
+Suppose you are a contributor to a large project, and you want to add a
+complicated feature, and to present it to the other developers in a way
+that makes it easy for them to read your changes, verify that they are
+correct, and understand why you made each change.
+
+If you present all of your changes as a single patch (or commit), they
+may find that it is too much to digest all at once.
+
+If you present them with the entire history of your work, complete with
+mistakes, corrections, and dead ends, they may be overwhelmed.
+
+So the ideal is usually to produce a series of patches such that:
+
+       1. Each patch can be applied in order.
+
+       2. Each patch includes a single logical change, together with a
+          message explaining the change.
+
+       3. No patch introduces a regression: after applying any initial
+          part of the series, the resulting project still compiles and
+          works, and has no bugs that it didn't have before.
+
+       4. The complete series produces the same end result as your own
+          (probably much messier!) development process did.
+
+We will introduce some tools that can help you do this, explain how to
+use them, and then explain some of the problems that can arise because
+you are rewriting history.
+
+[[using-git-rebase]]
+Keeping a patch series up to date using git-rebase
+--------------------------------------------------
+
+Suppose that you create a branch "mywork" on a remote-tracking branch
+"origin", and create some commits on top of it:
+
+-------------------------------------------------
+$ git checkout -b mywork origin
+$ vi file.txt
+$ git commit
+$ vi otherfile.txt
+$ git commit
+...
+-------------------------------------------------
+
+You have performed no merges into mywork, so it is just a simple linear
+sequence of patches on top of "origin":
+
+................................................
+ o--o--o <-- origin
+        \
+         o--o--o <-- mywork
+................................................
+
+Some more interesting work has been done in the upstream project, and
+"origin" has advanced:
+
+................................................
+ o--o--O--o--o--o <-- origin
+        \
+         a--b--c <-- mywork
+................................................
+
+At this point, you could use "pull" to merge your changes back in;
+the result would create a new merge commit, like this:
+
+................................................
+ o--o--O--o--o--o <-- origin
+        \        \
+         a--b--c--m <-- mywork
+................................................
+
+However, if you prefer to keep the history in mywork a simple series of
+commits without any merges, you may instead choose to use
+gitlink:git-rebase[1]:
+
+-------------------------------------------------
+$ git checkout mywork
+$ git rebase origin
+-------------------------------------------------
+
+This will remove each of your commits from mywork, temporarily saving
+them as patches (in a directory named ".dotest"), update mywork to
+point at the latest version of origin, then apply each of the saved
+patches to the new mywork.  The result will look like:
+
+
+................................................
+ o--o--O--o--o--o <-- origin
+                \
+                 a'--b'--c' <-- mywork
+................................................
+
+In the process, it may discover conflicts.  In that case it will stop
+and allow you to fix the conflicts; after fixing conflicts, use "git
+add" to update the index with those contents, and then, instead of
+running git-commit, just run
+
+-------------------------------------------------
+$ git rebase --continue
+-------------------------------------------------
+
+and git will continue applying the rest of the patches.
+
+At any point you may use the `--abort` option to abort this process and
+return mywork to the state it had before you started the rebase:
+
+-------------------------------------------------
+$ git rebase --abort
+-------------------------------------------------
+
+[[rewriting-one-commit]]
+Rewriting a single commit
+-------------------------
+
+We saw in <<fixing-a-mistake-by-rewriting-history>> that you can replace the
+most recent commit using
+
+-------------------------------------------------
+$ git commit --amend
+-------------------------------------------------
+
+which will replace the old commit by a new commit incorporating your
+changes, giving you a chance to edit the old commit message first.
+
+You can also use a combination of this and gitlink:git-rebase[1] to
+replace a commit further back in your history and recreate the
+intervening changes on top of it.  First, tag the problematic commit
+with
+
+-------------------------------------------------
+$ git tag bad mywork~5
+-------------------------------------------------
+
+(Either gitk or git-log may be useful for finding the commit.)
+
+Then check out that commit, edit it, and rebase the rest of the series
+on top of it (note that we could check out the commit on a temporary
+branch, but instead we're using a <<detached-head,detached head>>):
+
+-------------------------------------------------
+$ git checkout bad
+$ # make changes here and update the index
+$ git commit --amend
+$ git rebase --onto HEAD bad mywork
+-------------------------------------------------
+
+When you're done, you'll be left with mywork checked out, with the top
+patches on mywork reapplied on top of your modified commit.  You can
+then clean up with
+
+-------------------------------------------------
+$ git tag -d bad
+-------------------------------------------------
+
+Note that the immutable nature of git history means that you haven't really
+"modified" existing commits; instead, you have replaced the old commits with
+new commits having new object names.
+
+[[reordering-patch-series]]
+Reordering or selecting from a patch series
+-------------------------------------------
+
+Given one existing commit, the gitlink:git-cherry-pick[1] command
+allows you to apply the change introduced by that commit and create a
+new commit that records it.  So, for example, if "mywork" points to a
+series of patches on top of "origin", you might do something like:
+
+-------------------------------------------------
+$ git checkout -b mywork-new origin
+$ gitk origin..mywork &
+-------------------------------------------------
+
+and browse through the list of patches in the mywork branch using gitk,
+applying them (possibly in a different order) to mywork-new using
+cherry-pick, and possibly modifying them as you go using `commit --amend`.
+The gitlink:git-gui[1] command may also help as it allows you to
+individually select diff hunks for inclusion in the index (by
+right-clicking on the diff hunk and choosing "Stage Hunk for Commit").
+
+Another technique is to use git-format-patch to create a series of
+patches, then reset the state to before the patches:
+
+-------------------------------------------------
+$ git format-patch origin
+$ git reset --hard origin
+-------------------------------------------------
+
+Then modify, reorder, or eliminate patches as preferred before applying
+them again with gitlink:git-am[1].
+
+[[patch-series-tools]]
+Other tools
+-----------
+
+There are numerous other tools, such as StGIT, which exist for the
+purpose of maintaining a patch series.  These are outside of the scope of
+this manual.
+
+[[problems-with-rewriting-history]]
+Problems with rewriting history
+-------------------------------
+
+The primary problem with rewriting the history of a branch has to do
+with merging.  Suppose somebody fetches your branch and merges it into
+their branch, with a result something like this:
+
+................................................
+ o--o--O--o--o--o <-- origin
+        \        \
+         t--t--t--m <-- their branch:
+................................................
+
+Then suppose you modify the last three commits:
+
+................................................
+        o--o--o <-- new head of origin
+       /
+ o--o--O--o--o--o <-- old head of origin
+................................................
+
+If we examined all this history together in one repository, it will
+look like:
+
+................................................
+        o--o--o <-- new head of origin
+       /
+ o--o--O--o--o--o <-- old head of origin
+        \        \
+         t--t--t--m <-- their branch:
+................................................
+
+Git has no way of knowing that the new head is an updated version of
+the old head; it treats this situation exactly the same as it would if
+two developers had independently done the work on the old and new heads
+in parallel.  At this point, if someone attempts to merge the new head
+in to their branch, git will attempt to merge together the two (old and
+new) lines of development, instead of trying to replace the old by the
+new.  The results are likely to be unexpected.
+
+You may still choose to publish branches whose history is rewritten,
+and it may be useful for others to be able to fetch those branches in
+order to examine or test them, but they should not attempt to pull such
+branches into their own work.
+
+For true distributed development that supports proper merging,
+published branches should never be rewritten.
+
+[[bisect-merges]]
+Why bisecting merge commits can be harder than bisecting linear history
+-----------------------------------------------------------------------
+
+The gitlink:git-bisect[1] command correctly handles history that
+includes merge commits.  However, when the commit that it finds is a
+merge commit, the user may need to work harder than usual to figure out
+why that commit introduced a problem.
+
+Imagine this history:
+
+................................................
+      ---Z---o---X---...---o---A---C---D
+          \                       /
+           o---o---Y---...---o---B
+................................................
+
+Suppose that on the upper line of development, the meaning of one
+of the functions that exists at Z is changed at commit X.  The
+commits from Z leading to A change both the function's
+implementation and all calling sites that exist at Z, as well
+as new calling sites they add, to be consistent.  There is no
+bug at A.
+
+Suppose that in the meantime on the lower line of development somebody
+adds a new calling site for that function at commit Y.  The
+commits from Z leading to B all assume the old semantics of that
+function and the callers and the callee are consistent with each
+other.  There is no bug at B, either.
+
+Suppose further that the two development lines merge cleanly at C,
+so no conflict resolution is required.
+
+Nevertheless, the code at C is broken, because the callers added
+on the lower line of development have not been converted to the new
+semantics introduced on the upper line of development.  So if all
+you know is that D is bad, that Z is good, and that
+gitlink:git-bisect[1] identifies C as the culprit, how will you
+figure out that the problem is due to this change in semantics?
+
+When the result of a git-bisect is a non-merge commit, you should
+normally be able to discover the problem by examining just that commit.
+Developers can make this easy by breaking their changes into small
+self-contained commits.  That won't help in the case above, however,
+because the problem isn't obvious from examination of any single
+commit; instead, a global view of the development is required.  To
+make matters worse, the change in semantics in the problematic
+function may be just one small part of the changes in the upper
+line of development.
+
+On the other hand, if instead of merging at C you had rebased the
+history between Z to B on top of A, you would have gotten this
+linear history:
+
+................................................................
+    ---Z---o---X--...---o---A---o---o---Y*--...---o---B*--D*
+................................................................
+
+Bisecting between Z and D* would hit a single culprit commit Y*,
+and understanding why Y* was broken would probably be easier.
+
+Partly for this reason, many experienced git users, even when
+working on an otherwise merge-heavy project, keep the history
+linear by rebasing against the latest upstream version before
+publishing.
+
+[[advanced-branch-management]]
+Advanced branch management
+==========================
+
+[[fetching-individual-branches]]
+Fetching individual branches
+----------------------------
+
+Instead of using gitlink:git-remote[1], you can also choose just
+to update one branch at a time, and to store it locally under an
+arbitrary name:
+
+-------------------------------------------------
+$ git fetch origin todo:my-todo-work
+-------------------------------------------------
+
+The first argument, "origin", just tells git to fetch from the
+repository you originally cloned from.  The second argument tells git
+to fetch the branch named "todo" from the remote repository, and to
+store it locally under the name refs/heads/my-todo-work.
+
+You can also fetch branches from other repositories; so
+
+-------------------------------------------------
+$ git fetch git://example.com/proj.git master:example-master
+-------------------------------------------------
+
+will create a new branch named "example-master" and store in it the
+branch named "master" from the repository at the given URL.  If you
+already have a branch named example-master, it will attempt to
+<<fast-forwards,fast-forward>> to the commit given by example.com's
+master branch.  In more detail:
+
+[[fetch-fast-forwards]]
+git fetch and fast-forwards
+---------------------------
+
+In the previous example, when updating an existing branch, "git
+fetch" checks to make sure that the most recent commit on the remote
+branch is a descendant of the most recent commit on your copy of the
+branch before updating your copy of the branch to point at the new
+commit.  Git calls this process a <<fast-forwards,fast forward>>.
+
+A fast forward looks something like this:
+
+................................................
+ o--o--o--o <-- old head of the branch
+           \
+            o--o--o <-- new head of the branch
+................................................
+
+
+In some cases it is possible that the new head will *not* actually be
+a descendant of the old head.  For example, the developer may have
+realized she made a serious mistake, and decided to backtrack,
+resulting in a situation like:
+
+................................................
+ o--o--o--o--a--b <-- old head of the branch
+           \
+            o--o--o <-- new head of the branch
+................................................
+
+In this case, "git fetch" will fail, and print out a warning.
+
+In that case, you can still force git to update to the new head, as
+described in the following section.  However, note that in the
+situation above this may mean losing the commits labeled "a" and "b",
+unless you've already created a reference of your own pointing to
+them.
+
+[[forcing-fetch]]
+Forcing git fetch to do non-fast-forward updates
+------------------------------------------------
+
+If git fetch fails because the new head of a branch is not a
+descendant of the old head, you may force the update with:
+
+-------------------------------------------------
+$ git fetch git://example.com/proj.git +master:refs/remotes/example/master
+-------------------------------------------------
+
+Note the addition of the "+" sign.  Alternatively, you can use the "-f"
+flag to force updates of all the fetched branches, as in:
+
+-------------------------------------------------
+$ git fetch -f origin
+-------------------------------------------------
+
+Be aware that commits that the old version of example/master pointed at
+may be lost, as we saw in the previous section.
+
+[[remote-branch-configuration]]
+Configuring remote branches
+---------------------------
+
+We saw above that "origin" is just a shortcut to refer to the
+repository that you originally cloned from.  This information is
+stored in git configuration variables, which you can see using
+gitlink:git-config[1]:
+
+-------------------------------------------------
+$ git config -l
+core.repositoryformatversion=0
+core.filemode=true
+core.logallrefupdates=true
+remote.origin.url=git://git.kernel.org/pub/scm/git/git.git
+remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
+branch.master.remote=origin
+branch.master.merge=refs/heads/master
+-------------------------------------------------
+
+If there are other repositories that you also use frequently, you can
+create similar configuration options to save typing; for example,
+after
+
+-------------------------------------------------
+$ git config remote.example.url git://example.com/proj.git
+-------------------------------------------------
+
+then the following two commands will do the same thing:
+
+-------------------------------------------------
+$ git fetch git://example.com/proj.git master:refs/remotes/example/master
+$ git fetch example master:refs/remotes/example/master
+-------------------------------------------------
+
+Even better, if you add one more option:
+
+-------------------------------------------------
+$ git config remote.example.fetch master:refs/remotes/example/master
+-------------------------------------------------
+
+then the following commands will all do the same thing:
+
+-------------------------------------------------
+$ git fetch git://example.com/proj.git master:refs/remotes/example/master
+$ git fetch example master:refs/remotes/example/master
+$ git fetch example
+-------------------------------------------------
+
+You can also add a "+" to force the update each time:
+
+-------------------------------------------------
+$ git config remote.example.fetch +master:ref/remotes/example/master
+-------------------------------------------------
+
+Don't do this unless you're sure you won't mind "git fetch" possibly
+throwing away commits on mybranch.
+
+Also note that all of the above configuration can be performed by
+directly editing the file .git/config instead of using
+gitlink:git-config[1].
+
+See gitlink:git-config[1] for more details on the configuration
+options mentioned above.
+
+
+[[git-concepts]]
+Git concepts
+============
+
+Git is built on a small number of simple but powerful ideas.  While it
+is possible to get things done without understanding them, you will find
+git much more intuitive if you do.
+
+We start with the most important, the  <<def_object_database,object
+database>> and the <<def_index,index>>.
+
+[[the-object-database]]
+The Object Database
+-------------------
+
+
+We already saw in <<understanding-commits>> that all commits are stored
+under a 40-digit "object name".  In fact, all the information needed to
+represent the history of a project is stored in objects with such names.
+In each case the name is calculated by taking the SHA1 hash of the
+contents of the object.  The SHA1 hash is a cryptographic hash function.
+What that means to us is that it is impossible to find two different
+objects with the same name.  This has a number of advantages; among
+others:
+
+- Git can quickly determine whether two objects are identical or not,
+  just by comparing names.
+- Since object names are computed the same way in every repository, the
+  same content stored in two repositories will always be stored under
+  the same name.
+- Git can detect errors when it reads an object, by checking that the
+  object's name is still the SHA1 hash of its contents.
+
+(See <<object-details>> for the details of the object formatting and
+SHA1 calculation.)
+
+There are four different types of objects: "blob", "tree", "commit", and
+"tag".
+
+- A <<def_blob_object,"blob" object>> is used to store file data.
+- A <<def_tree_object,"tree" object>> is an object that ties one or more
+  "blob" objects into a directory structure. In addition, a tree object
+  can refer to other tree objects, thus creating a directory hierarchy.
+- A <<def_commit_object,"commit" object>> ties such directory hierarchies
+  together into a <<def_DAG,directed acyclic graph>> of revisions--each
+  commit contains the object name of exactly one tree designating the
+  directory hierarchy at the time of the commit. In addition, a commit
+  refers to "parent" commit objects that describe the history of how we
+  arrived at that directory hierarchy.
+- A <<def_tag_object,"tag" object>> symbolically identifies and can be
+  used to sign other objects. It contains the object name and type of
+  another object, a symbolic name (of course!) and, optionally, a
+  signature.
+
+The object types in some more detail:
+
+[[commit-object]]
+Commit Object
+~~~~~~~~~~~~~
+
+The "commit" object links a physical state of a tree with a description
+of how we got there and why.  Use the --pretty=raw option to
+gitlink:git-show[1] or gitlink:git-log[1] to examine your favorite
+commit:
+
+------------------------------------------------
+$ git show -s --pretty=raw 2be7fcb476
+commit 2be7fcb4764f2dbcee52635b91fedb1b3dcf7ab4
+tree fb3a8bdd0ceddd019615af4d57a53f43d8cee2bf
+parent 257a84d9d02e90447b149af58b271c19405edb6a
+author Dave Watson <dwatson@mimvista.com> 1187576872 -0400
+committer Junio C Hamano <gitster@pobox.com> 1187591163 -0700
+
+    Fix misspelling of 'suppress' in docs
+
+    Signed-off-by: Junio C Hamano <gitster@pobox.com>
+------------------------------------------------
+
+As you can see, a commit is defined by:
+
+- a tree: The SHA1 name of a tree object (as defined below), representing
+  the contents of a directory at a certain point in time.
+- parent(s): The SHA1 name of some number of commits which represent the
+  immediately prevoius step(s) in the history of the project.  The
+  example above has one parent; merge commits may have more than
+  one.  A commit with no parents is called a "root" commit, and
+  represents the initial revision of a project.  Each project must have
+  at least one root.  A project can also have multiple roots, though
+  that isn't common (or necessarily a good idea).
+- an author: The name of the person responsible for this change, together
+  with its date.
+- a committer: The name of the person who actually created the commit,
+  with the date it was done.  This may be different from the author, for
+  example, if the author was someone who wrote a patch and emailed it
+  to the person who used it to create the commit.
+- a comment describing this commit.
+
+Note that a commit does not itself contain any information about what
+actually changed; all changes are calculated by comparing the contents
+of the tree referred to by this commit with the trees associated with
+its parents.  In particular, git does not attempt to record file renames
+explicitly, though it can identify cases where the existence of the same
+file data at changing paths suggests a rename.  (See, for example, the
+-M option to gitlink:git-diff[1]).
+
+A commit is usually created by gitlink:git-commit[1], which creates a
+commit whose parent is normally the current HEAD, and whose tree is
+taken from the content currently stored in the index.
+
+[[tree-object]]
+Tree Object
+~~~~~~~~~~~
+
+The ever-versatile gitlink:git-show[1] command can also be used to
+examine tree objects, but gitlink:git-ls-tree[1] will give you more
+details:
+
+------------------------------------------------
+$ git ls-tree fb3a8bdd0ce
+100644 blob 63c918c667fa005ff12ad89437f2fdc80926e21c    .gitignore
+100644 blob 5529b198e8d14decbe4ad99db3f7fb632de0439d    .mailmap
+100644 blob 6ff87c4664981e4397625791c8ea3bbb5f2279a3    COPYING
+040000 tree 2fb783e477100ce076f6bf57e4a6f026013dc745    Documentation
+100755 blob 3c0032cec592a765692234f1cba47dfdcc3a9200    GIT-VERSION-GEN
+100644 blob 289b046a443c0647624607d471289b2c7dcd470b    INSTALL
+100644 blob 4eb463797adc693dc168b926b6932ff53f17d0b1    Makefile
+100644 blob 548142c327a6790ff8821d67c2ee1eff7a656b52    README
+...
+------------------------------------------------
+
+As you can see, a tree object contains a list of entries, each with a
+mode, object type, SHA1 name, and name, sorted by name.  It represents
+the contents of a single directory tree.
+
+The object type may be a blob, representing the contents of a file, or
+another tree, representing the contents of a subdirectory.  Since trees
+and blobs, like all other objects, are named by the SHA1 hash of their
+contents, two trees have the same SHA1 name if and only if their
+contents (including, recursively, the contents of all subdirectories)
+are identical.  This allows git to quickly determine the differences
+between two related tree objects, since it can ignore any entries with
+identical object names.
+
+(Note: in the presence of submodules, trees may also have commits as
+entries.  See <<submodules>> for documentation.)
+
+Note that the files all have mode 644 or 755: git actually only pays
+attention to the executable bit.
+
+[[blob-object]]
+Blob Object
+~~~~~~~~~~~
+
+You can use gitlink:git-show[1] to examine the contents of a blob; take,
+for example, the blob in the entry for "COPYING" from the tree above:
+
+------------------------------------------------
+$ git show 6ff87c4664
+
+ Note that the only valid version of the GPL as far as this project
+ is concerned is _this_ particular version of the license (ie v2, not
+ v2.2 or v3.x or whatever), unless explicitly otherwise stated.
+...
+------------------------------------------------
+
+A "blob" object is nothing but a binary blob of data.  It doesn't refer
+to anything else or have attributes of any kind.
+
+Since the blob is entirely defined by its data, if two files in a
+directory tree (or in multiple different versions of the repository)
+have the same contents, they will share the same blob object. The object
+is totally independent of its location in the directory tree, and
+renaming a file does not change the object that file is associated with.
+
+Note that any tree or blob object can be examined using
+gitlink:git-show[1] with the <revision>:<path> syntax.  This can
+sometimes be useful for browsing the contents of a tree that is not
+currently checked out.
+
+[[trust]]
+Trust
+~~~~~
+
+If you receive the SHA1 name of a blob from one source, and its contents
+from another (possibly untrusted) source, you can still trust that those
+contents are correct as long as the SHA1 name agrees.  This is because
+the SHA1 is designed so that it is infeasible to find different contents
+that produce the same hash.
+
+Similarly, you need only trust the SHA1 name of a top-level tree object
+to trust the contents of the entire directory that it refers to, and if
+you receive the SHA1 name of a commit from a trusted source, then you
+can easily verify the entire history of commits reachable through
+parents of that commit, and all of those contents of the trees referred
+to by those commits.
+
+So to introduce some real trust in the system, the only thing you need
+to do is to digitally sign just 'one' special note, which includes the
+name of a top-level commit.  Your digital signature shows others
+that you trust that commit, and the immutability of the history of
+commits tells others that they can trust the whole history.
+
+In other words, you can easily validate a whole archive by just
+sending out a single email that tells the people the name (SHA1 hash)
+of the top commit, and digitally sign that email using something
+like GPG/PGP.
+
+To assist in this, git also provides the tag object...
+
+[[tag-object]]
+Tag Object
+~~~~~~~~~~
+
+A tag object contains an object, object type, tag name, the name of the
+person ("tagger") who created the tag, and a message, which may contain
+a signature, as can be seen using the gitlink:git-cat-file[1]:
+
+------------------------------------------------
+$ git cat-file tag v1.5.0
+object 437b1b20df4b356c9342dac8d38849f24ef44f27
+type commit
+tag v1.5.0
+tagger Junio C Hamano <junkio@cox.net> 1171411200 +0000
+
+GIT 1.5.0
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+iD8DBQBF0lGqwMbZpPMRm5oRAuRiAJ9ohBLd7s2kqjkKlq1qqC57SbnmzQCdG4ui
+nLE/L9aUXdWeTFPron96DLA=
+=2E+0
+-----END PGP SIGNATURE-----
+------------------------------------------------
+
+See the gitlink:git-tag[1] command to learn how to create and verify tag
+objects.  (Note that gitlink:git-tag[1] can also be used to create
+"lightweight tags", which are not tag objects at all, but just simple
+references whose names begin with "refs/tags/").
+
+[[pack-files]]
+How git stores objects efficiently: pack files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Newly created objects are initially created in a file named after the
+object's SHA1 hash (stored in .git/objects).
+
+Unfortunately this system becomes inefficient once a project has a
+lot of objects.  Try this on an old project:
+
+------------------------------------------------
+$ git count-objects
+6930 objects, 47620 kilobytes
+------------------------------------------------
+
+The first number is the number of objects which are kept in
+individual files.  The second is the amount of space taken up by
+those "loose" objects.
+
+You can save space and make git faster by moving these loose objects in
+to a "pack file", which stores a group of objects in an efficient
+compressed format; the details of how pack files are formatted can be
+found in link:technical/pack-format.txt[technical/pack-format.txt].
+
+To put the loose objects into a pack, just run git repack:
+
+------------------------------------------------
+$ git repack
+Generating pack...
+Done counting 6020 objects.
+Deltifying 6020 objects.
+ 100% (6020/6020) done
+Writing 6020 objects.
+ 100% (6020/6020) done
+Total 6020, written 6020 (delta 4070), reused 0 (delta 0)
+Pack pack-3e54ad29d5b2e05838c75df582c65257b8d08e1c created.
+------------------------------------------------
+
+You can then run
+
+------------------------------------------------
+$ git prune
+------------------------------------------------
+
+to remove any of the "loose" objects that are now contained in the
+pack.  This will also remove any unreferenced objects (which may be
+created when, for example, you use "git reset" to remove a commit).
+You can verify that the loose objects are gone by looking at the
+.git/objects directory or by running
+
+------------------------------------------------
+$ git count-objects
+0 objects, 0 kilobytes
+------------------------------------------------
+
+Although the object files are gone, any commands that refer to those
+objects will work exactly as they did before.
+
+The gitlink:git-gc[1] command performs packing, pruning, and more for
+you, so is normally the only high-level command you need.
+
+[[dangling-objects]]
+Dangling objects
+~~~~~~~~~~~~~~~~
+
+The gitlink:git-fsck[1] command will sometimes complain about dangling
+objects.  They are not a problem.
+
+The most common cause of dangling objects is that you've rebased a
+branch, or you have pulled from somebody else who rebased a branch--see
+<<cleaning-up-history>>.  In that case, the old head of the original
+branch still exists, as does everything it pointed to. The branch
+pointer itself just doesn't, since you replaced it with another one.
+
+There are also other situations that cause dangling objects. For
+example, a "dangling blob" may arise because you did a "git add" of a
+file, but then, before you actually committed it and made it part of the
+bigger picture, you changed something else in that file and committed
+that *updated* thing--the old state that you added originally ends up
+not being pointed to by any commit or tree, so it's now a dangling blob
+object.
+
+Similarly, when the "recursive" merge strategy runs, and finds that
+there are criss-cross merges and thus more than one merge base (which is
+fairly unusual, but it does happen), it will generate one temporary
+midway tree (or possibly even more, if you had lots of criss-crossing
+merges and more than two merge bases) as a temporary internal merge
+base, and again, those are real objects, but the end result will not end
+up pointing to them, so they end up "dangling" in your repository.
+
+Generally, dangling objects aren't anything to worry about. They can
+even be very useful: if you screw something up, the dangling objects can
+be how you recover your old tree (say, you did a rebase, and realized
+that you really didn't want to--you can look at what dangling objects
+you have, and decide to reset your head to some old dangling state).
+
+For commits, you can just use:
+
+------------------------------------------------
+$ gitk <dangling-commit-sha-goes-here> --not --all
+------------------------------------------------
+
+This asks for all the history reachable from the given commit but not
+from any branch, tag, or other reference.  If you decide it's something
+you want, you can always create a new reference to it, e.g.,
+
+------------------------------------------------
+$ git branch recovered-branch <dangling-commit-sha-goes-here>
+------------------------------------------------
+
+For blobs and trees, you can't do the same, but you can still examine
+them.  You can just do
+
+------------------------------------------------
+$ git show <dangling-blob/tree-sha-goes-here>
+------------------------------------------------
+
+to show what the contents of the blob were (or, for a tree, basically
+what the "ls" for that directory was), and that may give you some idea
+of what the operation was that left that dangling object.
+
+Usually, dangling blobs and trees aren't very interesting. They're
+almost always the result of either being a half-way mergebase (the blob
+will often even have the conflict markers from a merge in it, if you
+have had conflicting merges that you fixed up by hand), or simply
+because you interrupted a "git fetch" with ^C or something like that,
+leaving _some_ of the new objects in the object database, but just
+dangling and useless.
+
+Anyway, once you are sure that you're not interested in any dangling
+state, you can just prune all unreachable objects:
+
+------------------------------------------------
+$ git prune
+------------------------------------------------
+
+and they'll be gone. But you should only run "git prune" on a quiescent
+repository--it's kind of like doing a filesystem fsck recovery: you
+don't want to do that while the filesystem is mounted.
+
+(The same is true of "git-fsck" itself, btw, but since
+git-fsck never actually *changes* the repository, it just reports
+on what it found, git-fsck itself is never "dangerous" to run.
+Running it while somebody is actually changing the repository can cause
+confusing and scary messages, but it won't actually do anything bad. In
+contrast, running "git prune" while somebody is actively changing the
+repository is a *BAD* idea).
+
+[[recovering-from-repository-corruption]]
+Recovering from repository corruption
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+By design, git treats data trusted to it with caution.  However, even in
+the absence of bugs in git itself, it is still possible that hardware or
+operating system errors could corrupt data.
+
+The first defense against such problems is backups.  You can back up a
+git directory using clone, or just using cp, tar, or any other backup
+mechanism.
+
+As a last resort, you can search for the corrupted objects and attempt
+to replace them by hand.  Back up your repository before attempting this
+in case you corrupt things even more in the process.
+
+We'll assume that the problem is a single missing or corrupted blob,
+which is sometimes a solveable problem.  (Recovering missing trees and
+especially commits is *much* harder).
+
+Before starting, verify that there is corruption, and figure out where
+it is with gitlink:git-fsck[1]; this may be time-consuming.
+
+Assume the output looks like this:
+
+------------------------------------------------
+$ git-fsck --full
+broken link from    tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
+              to    blob 4b9458b3786228369c63936db65827de3cc06200
+missing blob 4b9458b3786228369c63936db65827de3cc06200
+------------------------------------------------
+
+(Typically there will be some "dangling object" messages too, but they
+aren't interesting.)
+
+Now you know that blob 4b9458b3 is missing, and that the tree 2d9263c6
+points to it.  If you could find just one copy of that missing blob
+object, possibly in some other repository, you could move it into
+.git/objects/4b/9458b3... and be done.  Suppose you can't.  You can
+still examine the tree that pointed to it with gitlink:git-ls-tree[1],
+which might output something like:
+
+------------------------------------------------
+$ git ls-tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
+100644 blob 8d14531846b95bfa3564b58ccfb7913a034323b8   .gitignore
+100644 blob ebf9bf84da0aab5ed944264a5db2a65fe3a3e883   .mailmap
+100644 blob ca442d313d86dc67e0a2e5d584b465bd382cbf5c   COPYING
+...
+100644 blob 4b9458b3786228369c63936db65827de3cc06200   myfile
+...
+------------------------------------------------
+
+So now you know that the missing blob was the data for a file named
+"myfile".  And chances are you can also identify the directory--let's
+say it's in "somedirectory".  If you're lucky the missing copy might be
+the same as the copy you have checked out in your working tree at
+"somedirectory/myfile"; you can test whether that's right with
+gitlink:git-hash-object[1]:
+
+------------------------------------------------
+$ git hash-object -w somedirectory/myfile
+------------------------------------------------
+
+which will create and store a blob object with the contents of
+somedirectory/myfile, and output the sha1 of that object.  if you're
+extremely lucky it might be 4b9458b3786228369c63936db65827de3cc06200, in
+which case you've guessed right, and the corruption is fixed!
+
+Otherwise, you need more information.  How do you tell which version of
+the file has been lost?
+
+The easiest way to do this is with:
+
+------------------------------------------------
+$ git log --raw --all --full-history -- somedirectory/myfile
+------------------------------------------------
+
+Because you're asking for raw output, you'll now get something like
+
+------------------------------------------------
+commit abc
+Author:
+Date:
+...
+:100644 100644 4b9458b... newsha... M somedirectory/myfile
+
+
+commit xyz
+Author:
+Date:
+
+...
+:100644 100644 oldsha... 4b9458b... M somedirectory/myfile
+------------------------------------------------
+
+This tells you that the immediately preceding version of the file was
+"newsha", and that the immediately following version was "oldsha".
+You also know the commit messages that went with the change from oldsha
+to 4b9458b and with the change from 4b9458b to newsha.
+
+If you've been committing small enough changes, you may now have a good
+shot at reconstructing the contents of the in-between state 4b9458b.
+
+If you can do that, you can now recreate the missing object with
+
+------------------------------------------------
+$ git hash-object -w <recreated-file>
+------------------------------------------------
+
+and your repository is good again!
+
+(Btw, you could have ignored the fsck, and started with doing a
+
+------------------------------------------------
+$ git log --raw --all
+------------------------------------------------
+
+and just looked for the sha of the missing object (4b9458b..) in that
+whole thing. It's up to you - git does *have* a lot of information, it is
+just missing one particular blob version.
+
+[[the-index]]
+The index
+-----------
+
+The index is a binary file (generally kept in .git/index) containing a
+sorted list of path names, each with permissions and the SHA1 of a blob
+object; gitlink:git-ls-files[1] can show you the contents of the index:
+
+-------------------------------------------------
+$ git ls-files --stage
+100644 63c918c667fa005ff12ad89437f2fdc80926e21c 0      .gitignore
+100644 5529b198e8d14decbe4ad99db3f7fb632de0439d 0      .mailmap
+100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0      COPYING
+100644 a37b2152bd26be2c2289e1f57a292534a51a93c7 0      Documentation/.gitignore
+100644 fbefe9a45b00a54b58d94d06eca48b03d40a50e0 0      Documentation/Makefile
+...
+100644 2511aef8d89ab52be5ec6a5e46236b4b6bcd07ea 0      xdiff/xtypes.h
+100644 2ade97b2574a9f77e7ae4002a4e07a6a38e46d07 0      xdiff/xutils.c
+100644 d5de8292e05e7c36c4b68857c1cf9855e3d2f70a 0      xdiff/xutils.h
+-------------------------------------------------
+
+Note that in older documentation you may see the index called the
+"current directory cache" or just the "cache".  It has three important
+properties:
+
+1. The index contains all the information necessary to generate a single
+(uniquely determined) tree object.
++
+For example, running gitlink:git-commit[1] generates this tree object
+from the index, stores it in the object database, and uses it as the
+tree object associated with the new commit.
+
+2. The index enables fast comparisons between the tree object it defines
+and the working tree.
++
+It does this by storing some additional data for each entry (such as
+the last modified time).  This data is not displayed above, and is not
+stored in the created tree object, but it can be used to determine
+quickly which files in the working directory differ from what was
+stored in the index, and thus save git from having to read all of the
+data from such files to look for changes.
+
+3. It can efficiently represent information about merge conflicts
+between different tree objects, allowing each pathname to be
+associated with sufficient information about the trees involved that
+you can create a three-way merge between them.
++
+We saw in <<conflict-resolution>> that during a merge the index can
+store multiple versions of a single file (called "stages").  The third
+column in the gitlink:git-ls-files[1] output above is the stage
+number, and will take on values other than 0 for files with merge
+conflicts.
+
+The index is thus a sort of temporary staging area, which is filled with
+a tree which you are in the process of working on.
+
+If you blow the index away entirely, you generally haven't lost any
+information as long as you have the name of the tree that it described.
+
+[[submodules]]
+Submodules
+==========
+
+Large projects are often composed of smaller, self-contained modules.  For
+example, an embedded Linux distribution's source tree would include every
+piece of software in the distribution with some local modifications; a movie
+player might need to build against a specific, known-working version of a
+decompression library; several independent programs might all share the same
+build scripts.
+
+With centralized revision control systems this is often accomplished by
+including every module in one single repository.  Developers can check out
+all modules or only the modules they need to work with.  They can even modify
+files across several modules in a single commit while moving things around
+or updating APIs and translations.
+
+Git does not allow partial checkouts, so duplicating this approach in Git
+would force developers to keep a local copy of modules they are not
+interested in touching.  Commits in an enormous checkout would be slower
+than you'd expect as Git would have to scan every directory for changes.
+If modules have a lot of local history, clones would take forever.
+
+On the plus side, distributed revision control systems can much better
+integrate with external sources.  In a centralized model, a single arbitrary
+snapshot of the external project is exported from its own revision control
+and then imported into the local revision control on a vendor branch.  All
+the history is hidden.  With distributed revision control you can clone the
+entire external history and much more easily follow development and re-merge
+local changes.
+
+Git's submodule support allows a repository to contain, as a subdirectory, a
+checkout of an external project.  Submodules maintain their own identity;
+the submodule support just stores the submodule repository location and
+commit ID, so other developers who clone the containing project
+("superproject") can easily clone all the submodules at the same revision.
+Partial checkouts of the superproject are possible: you can tell Git to
+clone none, some or all of the submodules.
+
+The gitlink:git-submodule[1] command is available since Git 1.5.3.  Users
+with Git 1.5.2 can look up the submodule commits in the repository and
+manually check them out; earlier versions won't recognize the submodules at
+all.
+
+To see how submodule support works, create (for example) four example
+repositories that can be used later as a submodule:
+
+-------------------------------------------------
+$ mkdir ~/git
+$ cd ~/git
+$ for i in a b c d
+do
+       mkdir $i
+       cd $i
+       git init
+       echo "module $i" > $i.txt
+       git add $i.txt
+       git commit -m "Initial commit, submodule $i"
+       cd ..
+done
+-------------------------------------------------
+
+Now create the superproject and add all the submodules:
+
+-------------------------------------------------
+$ mkdir super
+$ cd super
+$ git init
+$ for i in a b c d
+do
+       git submodule add ~/git/$i
+done
+-------------------------------------------------
+
+NOTE: Do not use local URLs here if you plan to publish your superproject!
+
+See what files `git submodule` created:
+
+-------------------------------------------------
+$ ls -a
+.  ..  .git  .gitmodules  a  b  c  d
+-------------------------------------------------
+
+The `git submodule add` command does a couple of things:
+
+- It clones the submodule under the current directory and by default checks out
+  the master branch.
+- It adds the submodule's clone path to the gitlink:gitmodules[5] file and
+  adds this file to the index, ready to be committed.
+- It adds the submodule's current commit ID to the index, ready to be
+  committed.
+
+Commit the superproject:
+
+-------------------------------------------------
+$ git commit -m "Add submodules a, b, c and d."
+-------------------------------------------------
+
+Now clone the superproject:
+
+-------------------------------------------------
+$ cd ..
+$ git clone super cloned
+$ cd cloned
+-------------------------------------------------
+
+The submodule directories are there, but they're empty:
+
+-------------------------------------------------
+$ ls -a a
+.  ..
+$ git submodule status
+-d266b9873ad50488163457f025db7cdd9683d88b a
+-e81d457da15309b4fef4249aba9b50187999670d b
+-c1536a972b9affea0f16e0680ba87332dc059146 c
+-d96249ff5d57de5de093e6baff9e0aafa5276a74 d
+-------------------------------------------------
+
+NOTE: The commit object names shown above would be different for you, but they
+should match the HEAD commit object names of your repositories.  You can check
+it by running `git ls-remote ../a`.
+
+Pulling down the submodules is a two-step process. First run `git submodule
+init` to add the submodule repository URLs to `.git/config`:
+
+-------------------------------------------------
+$ git submodule init
+-------------------------------------------------
+
+Now use `git submodule update` to clone the repositories and check out the
+commits specified in the superproject:
+
+-------------------------------------------------
+$ git submodule update
+$ cd a
+$ ls -a
+.  ..  .git  a.txt
+-------------------------------------------------
+
+One major difference between `git submodule update` and `git submodule add` is
+that `git submodule update` checks out a specific commit, rather than the tip
+of a branch. It's like checking out a tag: the head is detached, so you're not
+working on a branch.
+
+-------------------------------------------------
+$ git branch
+* (no branch)
+  master
+-------------------------------------------------
+
+If you want to make a change within a submodule and you have a detached head,
+then you should create or checkout a branch, make your changes, publish the
+change within the submodule, and then update the superproject to reference the
+new commit:
+
+-------------------------------------------------
+$ git checkout master
+-------------------------------------------------
+
+or
+
+-------------------------------------------------
+$ git checkout -b fix-up
+-------------------------------------------------
+
+then
+
+-------------------------------------------------
+$ echo "adding a line again" >> a.txt
+$ git commit -a -m "Updated the submodule from within the superproject."
+$ git push
+$ cd ..
+$ git diff
+diff --git a/a b/a
+index d266b98..261dfac 160000
+--- a/a
++++ b/a
+@@ -1 +1 @@
+-Subproject commit d266b9873ad50488163457f025db7cdd9683d88b
++Subproject commit 261dfac35cb99d380eb966e102c1197139f7fa24
+$ git add a
+$ git commit -m "Updated submodule a."
+$ git push
+-------------------------------------------------
+
+You have to run `git submodule update` after `git pull` if you want to update
+submodules, too.
+
+Pitfalls with submodules
+------------------------
+
+Always publish the submodule change before publishing the change to the
+superproject that references it. If you forget to publish the submodule change,
+others won't be able to clone the repository:
+
+-------------------------------------------------
+$ cd ~/git/super/a
+$ echo i added another line to this file >> a.txt
+$ git commit -a -m "doing it wrong this time"
+$ cd ..
+$ git add a
+$ git commit -m "Updated submodule a again."
+$ git push
+$ cd ~/git/cloned
+$ git pull
+$ git submodule update
+error: pathspec '261dfac35cb99d380eb966e102c1197139f7fa24' did not match any file(s) known to git.
+Did you forget to 'git add'?
+Unable to checkout '261dfac35cb99d380eb966e102c1197139f7fa24' in submodule path 'a'
+-------------------------------------------------
+
+You also should not rewind branches in a submodule beyond commits that were
+ever recorded in any superproject.
+
+It's not safe to run `git submodule update` if you've made and committed
+changes within a submodule without checking out a branch first. They will be
+silently overwritten:
+
+-------------------------------------------------
+$ cat a.txt
+module a
+$ echo line added from private2 >> a.txt
+$ git commit -a -m "line added inside private2"
+$ cd ..
+$ git submodule update
+Submodule path 'a': checked out 'd266b9873ad50488163457f025db7cdd9683d88b'
+$ cd a
+$ cat a.txt
+module a
+-------------------------------------------------
+
+NOTE: The changes are still visible in the submodule's reflog.
+
+This is not the case if you did not commit your changes.
+
+[[low-level-operations]]
+Low-level git operations
+========================
+
+Many of the higher-level commands were originally implemented as shell
+scripts using a smaller core of low-level git commands.  These can still
+be useful when doing unusual things with git, or just as a way to
+understand its inner workings.
+
+[[object-manipulation]]
+Object access and manipulation
+------------------------------
+
+The gitlink:git-cat-file[1] command can show the contents of any object,
+though the higher-level gitlink:git-show[1] is usually more useful.
+
+The gitlink:git-commit-tree[1] command allows constructing commits with
+arbitrary parents and trees.
+
+A tree can be created with gitlink:git-write-tree[1] and its data can be
+accessed by gitlink:git-ls-tree[1].  Two trees can be compared with
+gitlink:git-diff-tree[1].
+
+A tag is created with gitlink:git-mktag[1], and the signature can be
+verified by gitlink:git-verify-tag[1], though it is normally simpler to
+use gitlink:git-tag[1] for both.
+
+[[the-workflow]]
+The Workflow
+------------
+
+High-level operations such as gitlink:git-commit[1],
+gitlink:git-checkout[1] and gitlink:git-reset[1] work by moving data
+between the working tree, the index, and the object database.  Git
+provides low-level operations which perform each of these steps
+individually.
+
+Generally, all "git" operations work on the index file. Some operations
+work *purely* on the index file (showing the current state of the
+index), but most operations move data between the index file and either
+the database or the working directory. Thus there are four main
+combinations:
+
+[[working-directory-to-index]]
+working directory -> index
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The gitlink:git-update-index[1] command updates the index with
+information from the working directory.  You generally update the
+index information by just specifying the filename you want to update,
+like so:
+
+-------------------------------------------------
+$ git update-index filename
+-------------------------------------------------
+
+but to avoid common mistakes with filename globbing etc, the command
+will not normally add totally new entries or remove old entries,
+i.e. it will normally just update existing cache entries.
+
+To tell git that yes, you really do realize that certain files no
+longer exist, or that new files should be added, you
+should use the `--remove` and `--add` flags respectively.
+
+NOTE! A `--remove` flag does 'not' mean that subsequent filenames will
+necessarily be removed: if the files still exist in your directory
+structure, the index will be updated with their new status, not
+removed. The only thing `--remove` means is that update-index will be
+considering a removed file to be a valid thing, and if the file really
+does not exist any more, it will update the index accordingly.
+
+As a special case, you can also do `git-update-index --refresh`, which
+will refresh the "stat" information of each index to match the current
+stat information. It will 'not' update the object status itself, and
+it will only update the fields that are used to quickly test whether
+an object still matches its old backing store object.
+
+The previously introduced gitlink:git-add[1] is just a wrapper for
+gitlink:git-update-index[1].
+
+[[index-to-object-database]]
+index -> object database
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+You write your current index file to a "tree" object with the program
+
+-------------------------------------------------
+$ git write-tree
+-------------------------------------------------
+
+that doesn't come with any options--it will just write out the
+current index into the set of tree objects that describe that state,
+and it will return the name of the resulting top-level tree. You can
+use that tree to re-generate the index at any time by going in the
+other direction:
+
+[[object-database-to-index]]
+object database -> index
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+You read a "tree" file from the object database, and use that to
+populate (and overwrite--don't do this if your index contains any
+unsaved state that you might want to restore later!) your current
+index.  Normal operation is just
+
+-------------------------------------------------
+$ git-read-tree <sha1 of tree>
+-------------------------------------------------
+
+and your index file will now be equivalent to the tree that you saved
+earlier. However, that is only your 'index' file: your working
+directory contents have not been modified.
+
+[[index-to-working-directory]]
+index -> working directory
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You update your working directory from the index by "checking out"
+files. This is not a very common operation, since normally you'd just
+keep your files updated, and rather than write to your working
+directory, you'd tell the index files about the changes in your
+working directory (i.e. `git-update-index`).
+
+However, if you decide to jump to a new version, or check out somebody
+else's version, or just restore a previous tree, you'd populate your
+index file with read-tree, and then you need to check out the result
+with
+
+-------------------------------------------------
+$ git-checkout-index filename
+-------------------------------------------------
+
+or, if you want to check out all of the index, use `-a`.
+
+NOTE! git-checkout-index normally refuses to overwrite old files, so
+if you have an old version of the tree already checked out, you will
+need to use the "-f" flag ('before' the "-a" flag or the filename) to
+'force' the checkout.
+
+
+Finally, there are a few odds and ends which are not purely moving
+from one representation to the other:
+
+[[tying-it-all-together]]
+Tying it all together
+~~~~~~~~~~~~~~~~~~~~~
+
+To commit a tree you have instantiated with "git-write-tree", you'd
+create a "commit" object that refers to that tree and the history
+behind it--most notably the "parent" commits that preceded it in
+history.
+
+Normally a "commit" has one parent: the previous state of the tree
+before a certain change was made. However, sometimes it can have two
+or more parent commits, in which case we call it a "merge", due to the
+fact that such a commit brings together ("merges") two or more
+previous states represented by other commits.
+
+In other words, while a "tree" represents a particular directory state
+of a working directory, a "commit" represents that state in "time",
+and explains how we got there.
+
+You create a commit object by giving it the tree that describes the
+state at the time of the commit, and a list of parents:
+
+-------------------------------------------------
+$ git-commit-tree <tree> -p <parent> [-p <parent2> ..]
+-------------------------------------------------
+
+and then giving the reason for the commit on stdin (either through
+redirection from a pipe or file, or by just typing it at the tty).
+
+git-commit-tree will return the name of the object that represents
+that commit, and you should save it away for later use. Normally,
+you'd commit a new `HEAD` state, and while git doesn't care where you
+save the note about that state, in practice we tend to just write the
+result to the file pointed at by `.git/HEAD`, so that we can always see
+what the last committed state was.
+
+Here is an ASCII art by Jon Loeliger that illustrates how
+various pieces fit together.
+
+------------
+
+                     commit-tree
+                      commit obj
+                       +----+
+                       |    |
+                       |    |
+                       V    V
+                    +-----------+
+                    | Object DB |
+                    |  Backing  |
+                    |   Store   |
+                    +-----------+
+                       ^
+           write-tree  |     |
+             tree obj  |     |
+                       |     |  read-tree
+                       |     |  tree obj
+                             V
+                    +-----------+
+                    |   Index   |
+                    |  "cache"  |
+                    +-----------+
+         update-index  ^
+             blob obj  |     |
+                       |     |
+    checkout-index -u  |     |  checkout-index
+             stat      |     |  blob obj
+                             V
+                    +-----------+
+                    |  Working  |
+                    | Directory |
+                    +-----------+
+
+------------
+
+
+[[examining-the-data]]
+Examining the data
+------------------
+
+You can examine the data represented in the object database and the
+index with various helper tools. For every object, you can use
+gitlink:git-cat-file[1] to examine details about the
+object:
+
+-------------------------------------------------
+$ git-cat-file -t <objectname>
+-------------------------------------------------
+
+shows the type of the object, and once you have the type (which is
+usually implicit in where you find the object), you can use
+
+-------------------------------------------------
+$ git-cat-file blob|tree|commit|tag <objectname>
+-------------------------------------------------
+
+to show its contents. NOTE! Trees have binary content, and as a result
+there is a special helper for showing that content, called
+`git-ls-tree`, which turns the binary content into a more easily
+readable form.
+
+It's especially instructive to look at "commit" objects, since those
+tend to be small and fairly self-explanatory. In particular, if you
+follow the convention of having the top commit name in `.git/HEAD`,
+you can do
+
+-------------------------------------------------
+$ git-cat-file commit HEAD
+-------------------------------------------------
+
+to see what the top commit was.
+
+[[merging-multiple-trees]]
+Merging multiple trees
+----------------------
+
+Git helps you do a three-way merge, which you can expand to n-way by
+repeating the merge procedure arbitrary times until you finally
+"commit" the state.  The normal situation is that you'd only do one
+three-way merge (two parents), and commit it, but if you like to, you
+can do multiple parents in one go.
+
+To do a three-way merge, you need the two sets of "commit" objects
+that you want to merge, use those to find the closest common parent (a
+third "commit" object), and then use those commit objects to find the
+state of the directory ("tree" object) at these points.
+
+To get the "base" for the merge, you first look up the common parent
+of two commits with
+
+-------------------------------------------------
+$ git-merge-base <commit1> <commit2>
+-------------------------------------------------
+
+which will return you the commit they are both based on.  You should
+now look up the "tree" objects of those commits, which you can easily
+do with (for example)
+
+-------------------------------------------------
+$ git-cat-file commit <commitname> | head -1
+-------------------------------------------------
+
+since the tree object information is always the first line in a commit
+object.
+
+Once you know the three trees you are going to merge (the one "original"
+tree, aka the common tree, and the two "result" trees, aka the branches
+you want to merge), you do a "merge" read into the index. This will
+complain if it has to throw away your old index contents, so you should
+make sure that you've committed those--in fact you would normally
+always do a merge against your last commit (which should thus match what
+you have in your current index anyway).
+
+To do the merge, do
+
+-------------------------------------------------
+$ git-read-tree -m -u <origtree> <yourtree> <targettree>
+-------------------------------------------------
+
+which will do all trivial merge operations for you directly in the
+index file, and you can just write the result out with
+`git-write-tree`.
+
+
+[[merging-multiple-trees-2]]
+Merging multiple trees, continued
+---------------------------------
+
+Sadly, many merges aren't trivial. If there are files that have
+been added, moved or removed, or if both branches have modified the
+same file, you will be left with an index tree that contains "merge
+entries" in it. Such an index tree can 'NOT' be written out to a tree
+object, and you will have to resolve any such merge clashes using
+other tools before you can write out the result.
+
+You can examine such index state with `git-ls-files --unmerged`
+command.  An example:
+
+------------------------------------------------
+$ git-read-tree -m $orig HEAD $target
+$ git-ls-files --unmerged
+100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1      hello.c
+100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2      hello.c
+100644 cc44c73eb783565da5831b4d820c962954019b69 3      hello.c
+------------------------------------------------
+
+Each line of the `git-ls-files --unmerged` output begins with
+the blob mode bits, blob SHA1, 'stage number', and the
+filename.  The 'stage number' is git's way to say which tree it
+came from: stage 1 corresponds to `$orig` tree, stage 2 `HEAD`
+tree, and stage3 `$target` tree.
+
+Earlier we said that trivial merges are done inside
+`git-read-tree -m`.  For example, if the file did not change
+from `$orig` to `HEAD` nor `$target`, or if the file changed
+from `$orig` to `HEAD` and `$orig` to `$target` the same way,
+obviously the final outcome is what is in `HEAD`.  What the
+above example shows is that file `hello.c` was changed from
+`$orig` to `HEAD` and `$orig` to `$target` in a different way.
+You could resolve this by running your favorite 3-way merge
+program, e.g.  `diff3`, `merge`, or git's own merge-file, on
+the blob objects from these three stages yourself, like this:
+
+------------------------------------------------
+$ git-cat-file blob 263414f... >hello.c~1
+$ git-cat-file blob 06fa6a2... >hello.c~2
+$ git-cat-file blob cc44c73... >hello.c~3
+$ git merge-file hello.c~2 hello.c~1 hello.c~3
+------------------------------------------------
+
+This would leave the merge result in `hello.c~2` file, along
+with conflict markers if there are conflicts.  After verifying
+the merge result makes sense, you can tell git what the final
+merge result for this file is by:
+
+-------------------------------------------------
+$ mv -f hello.c~2 hello.c
+$ git-update-index hello.c
+-------------------------------------------------
+
+When a path is in unmerged state, running `git-update-index` for
+that path tells git to mark the path resolved.
+
+The above is the description of a git merge at the lowest level,
+to help you understand what conceptually happens under the hood.
+In practice, nobody, not even git itself, uses three `git-cat-file`
+for this.  There is `git-merge-index` program that extracts the
+stages to temporary files and calls a "merge" script on it:
+
+-------------------------------------------------
+$ git-merge-index git-merge-one-file hello.c
+-------------------------------------------------
+
+and that is what higher level `git merge -s resolve` is implemented with.
+
+[[hacking-git]]
+Hacking git
+===========
+
+This chapter covers internal details of the git implementation which
+probably only git developers need to understand.
+
+[[object-details]]
+Object storage format
+---------------------
+
+All objects have a statically determined "type" which identifies the
+format of the object (i.e. how it is used, and how it can refer to other
+objects).  There are currently four different object types: "blob",
+"tree", "commit", and "tag".
+
+Regardless of object type, all objects share the following
+characteristics: they are all deflated with zlib, and have a header
+that not only specifies their type, but also provides size information
+about the data in the object.  It's worth noting that the SHA1 hash
+that is used to name the object is the hash of the original data
+plus this header, so `sha1sum` 'file' does not match the object name
+for 'file'.
+(Historical note: in the dawn of the age of git the hash
+was the sha1 of the 'compressed' object.)
+
+As a result, the general consistency of an object can always be tested
+independently of the contents or the type of the object: all objects can
+be validated by verifying that (a) their hashes match the content of the
+file and (b) the object successfully inflates to a stream of bytes that
+forms a sequence of <ascii type without space> {plus} <space> {plus} <ascii decimal
+size> {plus} <byte\0> {plus} <binary object data>.
+
+The structured objects can further have their structure and
+connectivity to other objects verified. This is generally done with
+the `git-fsck` program, which generates a full dependency graph
+of all objects, and verifies their internal consistency (in addition
+to just verifying their superficial consistency through the hash).
+
+[[birdview-on-the-source-code]]
+A birds-eye view of Git's source code
+-------------------------------------
+
+It is not always easy for new developers to find their way through Git's
+source code.  This section gives you a little guidance to show where to
+start.
+
+A good place to start is with the contents of the initial commit, with:
+
+----------------------------------------------------
+$ git checkout e83c5163
+----------------------------------------------------
+
+The initial revision lays the foundation for almost everything git has
+today, but is small enough to read in one sitting.
+
+Note that terminology has changed since that revision.  For example, the
+README in that revision uses the word "changeset" to describe what we
+now call a <<def_commit_object,commit>>.
+
+Also, we do not call it "cache" any more, but "index", however, the
+file is still called `cache.h`.  Remark: Not much reason to change it now,
+especially since there is no good single name for it anyway, because it is
+basically _the_ header file which is included by _all_ of Git's C sources.
+
+If you grasp the ideas in that initial commit, you should check out a
+more recent version and skim `cache.h`, `object.h` and `commit.h`.
+
+In the early days, Git (in the tradition of UNIX) was a bunch of programs
+which were extremely simple, and which you used in scripts, piping the
+output of one into another. This turned out to be good for initial
+development, since it was easier to test new things.  However, recently
+many of these parts have become builtins, and some of the core has been
+"libified", i.e. put into libgit.a for performance, portability reasons,
+and to avoid code duplication.
+
+By now, you know what the index is (and find the corresponding data
+structures in `cache.h`), and that there are just a couple of object types
+(blobs, trees, commits and tags) which inherit their common structure from
+`struct object`, which is their first member (and thus, you can cast e.g.
+`(struct object *)commit` to achieve the _same_ as `&commit->object`, i.e.
+get at the object name and flags).
+
+Now is a good point to take a break to let this information sink in.
+
+Next step: get familiar with the object naming.  Read <<naming-commits>>.
+There are quite a few ways to name an object (and not only revisions!).
+All of these are handled in `sha1_name.c`. Just have a quick look at
+the function `get_sha1()`. A lot of the special handling is done by
+functions like `get_sha1_basic()` or the likes.
+
+This is just to get you into the groove for the most libified part of Git:
+the revision walker.
+
+Basically, the initial version of `git log` was a shell script:
+
+----------------------------------------------------------------
+$ git-rev-list --pretty $(git-rev-parse --default HEAD "$@") | \
+       LESS=-S ${PAGER:-less}
+----------------------------------------------------------------
+
+What does this mean?
+
+`git-rev-list` is the original version of the revision walker, which
+_always_ printed a list of revisions to stdout.  It is still functional,
+and needs to, since most new Git programs start out as scripts using
+`git-rev-list`.
+
+`git-rev-parse` is not as important any more; it was only used to filter out
+options that were relevant for the different plumbing commands that were
+called by the script.
+
+Most of what `git-rev-list` did is contained in `revision.c` and
+`revision.h`.  It wraps the options in a struct named `rev_info`, which
+controls how and what revisions are walked, and more.
+
+The original job of `git-rev-parse` is now taken by the function
+`setup_revisions()`, which parses the revisions and the common command line
+options for the revision walker. This information is stored in the struct
+`rev_info` for later consumption. You can do your own command line option
+parsing after calling `setup_revisions()`. After that, you have to call
+`prepare_revision_walk()` for initialization, and then you can get the
+commits one by one with the function `get_revision()`.
+
+If you are interested in more details of the revision walking process,
+just have a look at the first implementation of `cmd_log()`; call
+`git-show v1.3.0~155^2~4` and scroll down to that function (note that you
+no longer need to call `setup_pager()` directly).
+
+Nowadays, `git log` is a builtin, which means that it is _contained_ in the
+command `git`.  The source side of a builtin is
+
+- a function called `cmd_<bla>`, typically defined in `builtin-<bla>.c`,
+  and declared in `builtin.h`,
+
+- an entry in the `commands[]` array in `git.c`, and
+
+- an entry in `BUILTIN_OBJECTS` in the `Makefile`.
+
+Sometimes, more than one builtin is contained in one source file.  For
+example, `cmd_whatchanged()` and `cmd_log()` both reside in `builtin-log.c`,
+since they share quite a bit of code.  In that case, the commands which are
+_not_ named like the `.c` file in which they live have to be listed in
+`BUILT_INS` in the `Makefile`.
+
+`git log` looks more complicated in C than it does in the original script,
+but that allows for a much greater flexibility and performance.
+
+Here again it is a good point to take a pause.
+
+Lesson three is: study the code.  Really, it is the best way to learn about
+the organization of Git (after you know the basic concepts).
+
+So, think about something which you are interested in, say, "how can I
+access a blob just knowing the object name of it?".  The first step is to
+find a Git command with which you can do it.  In this example, it is either
+`git show` or `git cat-file`.
+
+For the sake of clarity, let's stay with `git cat-file`, because it
+
+- is plumbing, and
+
+- was around even in the initial commit (it literally went only through
+  some 20 revisions as `cat-file.c`, was renamed to `builtin-cat-file.c`
+  when made a builtin, and then saw less than 10 versions).
+
+So, look into `builtin-cat-file.c`, search for `cmd_cat_file()` and look what
+it does.
+
+------------------------------------------------------------------
+        git_config(git_default_config);
+        if (argc != 3)
+                usage("git-cat-file [-t|-s|-e|-p|<type>] <sha1>");
+        if (get_sha1(argv[2], sha1))
+                die("Not a valid object name %s", argv[2]);
+------------------------------------------------------------------
+
+Let's skip over the obvious details; the only really interesting part
+here is the call to `get_sha1()`.  It tries to interpret `argv[2]` as an
+object name, and if it refers to an object which is present in the current
+repository, it writes the resulting SHA-1 into the variable `sha1`.
+
+Two things are interesting here:
+
+- `get_sha1()` returns 0 on _success_.  This might surprise some new
+  Git hackers, but there is a long tradition in UNIX to return different
+  negative numbers in case of different errors--and 0 on success.
+
+- the variable `sha1` in the function signature of `get_sha1()` is `unsigned
+  char \*`, but is actually expected to be a pointer to `unsigned
+  char[20]`.  This variable will contain the 160-bit SHA-1 of the given
+  commit.  Note that whenever a SHA-1 is passed as `unsigned char \*`, it
+  is the binary representation, as opposed to the ASCII representation in
+  hex characters, which is passed as `char *`.
+
+You will see both of these things throughout the code.
+
+Now, for the meat:
+
+-----------------------------------------------------------------------------
+        case 0:
+                buf = read_object_with_reference(sha1, argv[1], &size, NULL);
+-----------------------------------------------------------------------------
+
+This is how you read a blob (actually, not only a blob, but any type of
+object).  To know how the function `read_object_with_reference()` actually
+works, find the source code for it (something like `git grep
+read_object_with | grep ":[a-z]"` in the git repository), and read
+the source.
+
+To find out how the result can be used, just read on in `cmd_cat_file()`:
+
+-----------------------------------
+        write_or_die(1, buf, size);
+-----------------------------------
+
+Sometimes, you do not know where to look for a feature.  In many such cases,
+it helps to search through the output of `git log`, and then `git show` the
+corresponding commit.
+
+Example: If you know that there was some test case for `git bundle`, but
+do not remember where it was (yes, you _could_ `git grep bundle t/`, but that
+does not illustrate the point!):
+
+------------------------
+$ git log --no-merges t/
+------------------------
+
+In the pager (`less`), just search for "bundle", go a few lines back,
+and see that it is in commit 18449ab0...  Now just copy this object name,
+and paste it into the command line
+
+-------------------
+$ git show 18449ab0
+-------------------
+
+Voila.
+
+Another example: Find out what to do in order to make some script a
+builtin:
+
+-------------------------------------------------
+$ git log --no-merges --diff-filter=A builtin-*.c
+-------------------------------------------------
+
+You see, Git is actually the best tool to find out about the source of Git
+itself!
+
+[[glossary]]
+include::glossary.txt[]
+
+[[git-quick-start]]
+Appendix A: Git Quick Reference
+===============================
+
+This is a quick summary of the major commands; the previous chapters
+explain how these work in more detail.
+
+[[quick-creating-a-new-repository]]
+Creating a new repository
+-------------------------
+
+From a tarball:
+
+-----------------------------------------------
+$ tar xzf project.tar.gz
+$ cd project
+$ git init
+Initialized empty Git repository in .git/
+$ git add .
+$ git commit
+-----------------------------------------------
+
+From a remote repository:
+
+-----------------------------------------------
+$ git clone git://example.com/pub/project.git
+$ cd project
+-----------------------------------------------
+
+[[managing-branches]]
+Managing branches
+-----------------
+
+-----------------------------------------------
+$ git branch        # list all local branches in this repo
+$ git checkout test  # switch working directory to branch "test"
+$ git branch new     # create branch "new" starting at current HEAD
+$ git branch -d new  # delete branch "new"
+-----------------------------------------------
+
+Instead of basing a new branch on current HEAD (the default), use:
+
+-----------------------------------------------
+$ git branch new test    # branch named "test"
+$ git branch new v2.6.15 # tag named v2.6.15
+$ git branch new HEAD^   # commit before the most recent
+$ git branch new HEAD^^  # commit before that
+$ git branch new test~10 # ten commits before tip of branch "test"
+-----------------------------------------------
+
+Create and switch to a new branch at the same time:
+
+-----------------------------------------------
+$ git checkout -b new v2.6.15
+-----------------------------------------------
+
+Update and examine branches from the repository you cloned from:
+
+-----------------------------------------------
+$ git fetch            # update
+$ git branch -r                # list
+  origin/master
+  origin/next
+  ...
+$ git checkout -b masterwork origin/master
+-----------------------------------------------
+
+Fetch a branch from a different repository, and give it a new
+name in your repository:
+
+-----------------------------------------------
+$ git fetch git://example.com/project.git theirbranch:mybranch
+$ git fetch git://example.com/project.git v2.6.15:mybranch
+-----------------------------------------------
+
+Keep a list of repositories you work with regularly:
+
+-----------------------------------------------
+$ git remote add example git://example.com/project.git
+$ git remote                   # list remote repositories
+example
+origin
+$ git remote show example      # get details
+* remote example
+  URL: git://example.com/project.git
+  Tracked remote branches
+    master next ...
+$ git fetch example            # update branches from example
+$ git branch -r                        # list all remote branches
+-----------------------------------------------
+
+
+[[exploring-history]]
+Exploring history
+-----------------
+
+-----------------------------------------------
+$ gitk                     # visualize and browse history
+$ git log                  # list all commits
+$ git log src/             # ...modifying src/
+$ git log v2.6.15..v2.6.16  # ...in v2.6.16, not in v2.6.15
+$ git log master..test     # ...in branch test, not in branch master
+$ git log test..master     # ...in branch master, but not in test
+$ git log test...master            # ...in one branch, not in both
+$ git log -S'foo()'        # ...where difference contain "foo()"
+$ git log --since="2 weeks ago"
+$ git log -p               # show patches as well
+$ git show                 # most recent commit
+$ git diff v2.6.15..v2.6.16 # diff between two tagged versions
+$ git diff v2.6.15..HEAD    # diff with current head
+$ git grep "foo()"         # search working directory for "foo()"
+$ git grep v2.6.15 "foo()"  # search old tree for "foo()"
+$ git show v2.6.15:a.txt    # look at old version of a.txt
+-----------------------------------------------
+
+Search for regressions:
+
+-----------------------------------------------
+$ git bisect start
+$ git bisect bad               # current version is bad
+$ git bisect good v2.6.13-rc2  # last known good revision
+Bisecting: 675 revisions left to test after this
+                               # test here, then:
+$ git bisect good              # if this revision is good, or
+$ git bisect bad               # if this revision is bad.
+                               # repeat until done.
+-----------------------------------------------
+
+[[making-changes]]
+Making changes
+--------------
+
+Make sure git knows who to blame:
+
+------------------------------------------------
+$ cat >>~/.gitconfig <<\EOF
+[user]
+       name = Your Name Comes Here
+       email = you@yourdomain.example.com
+EOF
+------------------------------------------------
+
+Select file contents to include in the next commit, then make the
+commit:
+
+-----------------------------------------------
+$ git add a.txt    # updated file
+$ git add b.txt    # new file
+$ git rm c.txt     # old file
+$ git commit
+-----------------------------------------------
+
+Or, prepare and create the commit in one step:
+
+-----------------------------------------------
+$ git commit d.txt # use latest content only of d.txt
+$ git commit -a           # use latest content of all tracked files
+-----------------------------------------------
+
+[[merging]]
+Merging
+-------
+
+-----------------------------------------------
+$ git merge test   # merge branch "test" into the current branch
+$ git pull git://example.com/project.git master
+                  # fetch and merge in remote branch
+$ git pull . test  # equivalent to git merge test
+-----------------------------------------------
+
+[[sharing-your-changes]]
+Sharing your changes
+--------------------
+
+Importing or exporting patches:
+
+-----------------------------------------------
+$ git format-patch origin..HEAD # format a patch for each commit
+                               # in HEAD but not in origin
+$ git am mbox # import patches from the mailbox "mbox"
+-----------------------------------------------
+
+Fetch a branch in a different git repository, then merge into the
+current branch:
+
+-----------------------------------------------
+$ git pull git://example.com/project.git theirbranch
+-----------------------------------------------
+
+Store the fetched branch into a local branch before merging into the
+current branch:
+
+-----------------------------------------------
+$ git pull git://example.com/project.git theirbranch:mybranch
+-----------------------------------------------
+
+After creating commits on a local branch, update the remote
+branch with your commits:
+
+-----------------------------------------------
+$ git push ssh://example.com/project.git mybranch:theirbranch
+-----------------------------------------------
+
+When remote and local branch are both named "test":
+
+-----------------------------------------------
+$ git push ssh://example.com/project.git test
+-----------------------------------------------
+
+Shortcut version for a frequently used remote repository:
+
+-----------------------------------------------
+$ git remote add example ssh://example.com/project.git
+$ git push example test
+-----------------------------------------------
+
+[[repository-maintenance]]
+Repository maintenance
+----------------------
+
+Check for corruption:
+
+-----------------------------------------------
+$ git fsck
+-----------------------------------------------
+
+Recompress, remove unused cruft:
+
+-----------------------------------------------
+$ git gc
+-----------------------------------------------
+
+
+[[todo]]
+Appendix B: Notes and todo list for this manual
+===============================================
+
+This is a work in progress.
+
+The basic requirements:
+
+- It must be readable in order, from beginning to end, by someone
+  intelligent with a basic grasp of the UNIX command line, but without
+  any special knowledge of git.  If necessary, any other prerequisites
+  should be specifically mentioned as they arise.
+- Whenever possible, section headings should clearly describe the task
+  they explain how to do, in language that requires no more knowledge
+  than necessary: for example, "importing patches into a project" rather
+  than "the git-am command"
+
+Think about how to create a clear chapter dependency graph that will
+allow people to get to important topics without necessarily reading
+everything in between.
+
+Scan Documentation/ for other stuff left out; in particular:
+
+- howto's
+- some of technical/?
+- hooks
+- list of commands in gitlink:git[1]
+
+Scan email archives for other stuff left out
+
+Scan man pages to see if any assume more background than this manual
+provides.
+
+Simplify beginning by suggesting disconnected head instead of
+temporary branch creation?
+
+Add more good examples.  Entire sections of just cookbook examples
+might be a good idea; maybe make an "advanced examples" section a
+standard end-of-chapter section?
+
+Include cross-references to the glossary, where appropriate.
+
+Document shallow clones?  See draft 1.5.0 release notes for some
+documentation.
+
+Add a section on working with other version control systems, including
+CVS, Subversion, and just imports of series of release tarballs.
+
+More details on gitweb?
+
+Write a chapter on using plumbing and writing scripts.
+
+Alternates, clone -reference, etc.
+
+More on recovery from repository corruption.  See:
+       http://marc.theaimsgroup.com/?l=git&m=117263864820799&w=2
+       http://marc.theaimsgroup.com/?l=git&m=117147855503798&w=2
+       http://marc.theaimsgroup.com/?l=git&m=117147855503798&w=2
index cfe46a857e5fd319fa6eb49474ddbb37bd47c537..3c0032cec592a765692234f1cba47dfdcc3a9200 100755 (executable)
@@ -1,65 +1,30 @@
 #!/bin/sh
 
 GVF=GIT-VERSION-FILE
-DEF_VER=0.9.GITGUI
+DEF_VER=v1.5.3.GIT
 
 LF='
 '
 
-tree_search ()
-{
-       head=$1
-       tree=$2
-       for p in $(git rev-list --parents --max-count=1 $head 2>/dev/null)
-       do
-               test $tree = $(git rev-parse $p^{tree} 2>/dev/null) &&
-               vn=$(git describe --abbrev=4 $p 2>/dev/null) &&
-               case "$vn" in
-               gitgui-[0-9]*) echo $vn; break;;
-               esac
-       done
-}
-
-# Always use the tarball version file if found, just
-# in case we are somehow contained in a larger git
-# repository that doesn't actually track our state.
-# (At least one package manager is doing this.)
-#
-# We may be a subproject, so try looking for the merge
-# commit that supplied this directory content if we are
-# not at the toplevel.  We probably will always be the
-# second parent in the commit, but we shouldn't rely on
-# that fact.
-#
-# If we are at the toplevel or the merge assumption fails
-# try looking for a gitgui-* tag.
-
-if test -f version &&
-   VN=$(cat version)
-then
-       : happy
-elif prefix="$(git rev-parse --show-prefix 2>/dev/null)"
-   test -n "$prefix" &&
-   head=$(git rev-list --max-count=1 HEAD -- . 2>/dev/null) &&
-   tree=$(git rev-parse --verify "HEAD:$prefix" 2>/dev/null) &&
-   VN=$(tree_search $head $tree)
-   case "$VN" in
-   gitgui-[0-9]*) : happy ;;
-   *) (exit 1) ;;
-   esac
+# First see if there is a version file (included in release tarballs),
+# then try git-describe, then default.
+if test -f version
 then
-       VN=$(echo "$VN" | sed -e 's/^gitgui-//;s/-/./g');
-elif VN=$(git describe --abbrev=4 HEAD 2>/dev/null) &&
-   case "$VN" in
-   gitgui-[0-9]*) : happy ;;
-   *) (exit 1) ;;
-   esac
+       VN=$(cat version) || VN="$DEF_VER"
+elif test -d .git &&
+       VN=$(git describe --abbrev=4 HEAD 2>/dev/null) &&
+       case "$VN" in
+       *$LF*) (exit 1) ;;
+       v[0-9]*) : happy ;;
+       esac
 then
-       VN=$(echo "$VN" | sed -e 's/^gitgui-//;s/-/./g');
+       VN=$(echo "$VN" | sed -e 's/-/./g');
 else
        VN="$DEF_VER"
 fi
 
+VN=$(expr "$VN" : v*'\(.*\)')
+
 dirty=$(sh -c 'git diff-index --name-only HEAD' 2>/dev/null) || dirty=
 case "$dirty" in
 '')
@@ -70,11 +35,13 @@ esac
 
 if test -r $GVF
 then
-       VC=$(sed -e 's/^GITGUI_VERSION = //' <$GVF)
+       VC=$(sed -e 's/^GIT_VERSION = //' <$GVF)
 else
        VC=unset
 fi
 test "$VN" = "$VC" || {
-       echo >&2 "GITGUI_VERSION = $VN"
-       echo "GITGUI_VERSION = $VN" >$GVF
+       echo >&2 "GIT_VERSION = $VN"
+       echo "GIT_VERSION = $VN" >$GVF
 }
+
+
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..f1eb404
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,129 @@
+
+               Git installation
+
+Normally you can just do "make" followed by "make install", and that
+will install the git programs in your own ~/bin/ directory.  If you want
+to do a global install, you can do
+
+       $ make prefix=/usr all doc info ;# as yourself
+       # make prefix=/usr install install-doc install-info ;# as root
+
+(or prefix=/usr/local, of course).  Just like any program suite
+that uses $prefix, the built results have some paths encoded,
+which are derived from $prefix, so "make all; make prefix=/usr
+install" would not work.
+
+Alternatively you can use autoconf generated ./configure script to
+set up install paths (via config.mak.autogen), so you can write instead
+
+       $ make configure ;# as yourself
+       $ ./configure --prefix=/usr ;# as yourself
+       $ make all doc ;# as yourself
+       # make install install-doc ;# as root
+
+
+Issues of note:
+
+ - git normally installs a helper script wrapper called "git", which
+   conflicts with a similarly named "GNU interactive tools" program.
+
+   Tough.  Either don't use the wrapper script, or delete the old GNU
+   interactive tools.  None of the core git stuff needs the wrapper,
+   it's just a convenient shorthand and while it is documented in some
+   places, you can always replace "git commit" with "git-commit"
+   instead.
+
+   But let's face it, most of us don't have GNU interactive tools, and
+   even if we had it, we wouldn't know what it does.  I don't think it
+   has been actively developed since 1997, and people have moved over to
+   graphical file managers.
+
+ - You can use git after building but without installing if you
+   wanted to.  Various git commands need to find other git
+   commands and scripts to do their work, so you would need to
+   arrange a few environment variables to tell them that their
+   friends will be found in your built source area instead of at
+   their standard installation area.  Something like this works
+   for me:
+
+       GIT_EXEC_PATH=`pwd`
+       PATH=`pwd`:$PATH
+       GITPERLLIB=`pwd`/perl/blib/lib
+       export GIT_EXEC_PATH PATH GITPERLLIB
+
+ - Git is reasonably self-sufficient, but does depend on a few external
+   programs and libraries:
+
+       - "zlib", the compression library. Git won't build without it.
+
+       - "openssl".  Unless you specify otherwise, you'll get the SHA1
+         library from here.
+
+         If you don't have openssl, you can use one of the SHA1 libraries
+         that come with git (git includes the one from Mozilla, and has
+         its own PowerPC and ARM optimized ones too - see the Makefile).
+
+       - "libcurl" and "curl" executable.  git-http-fetch and
+         git-fetch use them.  If you do not use http
+         transfer, you are probably OK if you do not have
+         them.
+
+       - expat library; git-http-push uses it for remote lock
+         management over DAV.  Similar to "curl" above, this is optional.
+
+        - "wish", the Tcl/Tk windowing shell is used in gitk to show the
+          history graphically, and in git-gui.
+
+       - "ssh" is used to push and pull over the net
+
+       - "perl" and POSIX-compliant shells are needed to use most of
+         the barebone Porcelainish scripts.
+
+       - "cpio" is used by git-merge for saving and restoring the index,
+         and by git-clone when doing a local (possibly hardlinked) clone.
+
+ - Some platform specific issues are dealt with Makefile rules,
+   but depending on your specific installation, you may not
+   have all the libraries/tools needed, or you may have
+   necessary libraries at unusual locations.  Please look at the
+   top of the Makefile to see what can be adjusted for your needs.
+   You can place local settings in config.mak and the Makefile
+   will include them.  Note that config.mak is not distributed;
+   the name is reserved for local settings.
+
+ - To build and install documentation suite, you need to have
+   the asciidoc/xmlto toolchain.  Because not many people are
+   inclined to install the tools, the default build target
+   ("make all") does _not_ build them.
+
+   Building and installing the info file additionally requires
+   makeinfo and docbook2X.  Version 0.8.3 is known to work.
+
+   The documentation is written for AsciiDoc 7, but "make
+   ASCIIDOC8=YesPlease doc" will let you format with AsciiDoc 8.
+
+   Alternatively, pre-formatted documentation are available in
+   "html" and "man" branches of the git repository itself.  For
+   example, you could:
+
+       $ mkdir manual && cd manual
+       $ git init
+       $ git fetch-pack git://git.kernel.org/pub/scm/git/git.git man html |
+         while read a b
+         do
+           echo $a >.git/$b
+         done
+       $ cp .git/refs/heads/man .git/refs/heads/master
+       $ git checkout
+
+   to checkout the pre-built man pages.  Also in this repository:
+
+       $ git checkout html
+
+   would instead give you a copy of what you see at:
+
+       http://www.kernel.org/pub/software/scm/git/docs/
+
+   It has been reported that docbook-xsl version 1.72 and 1.73 are
+   buggy; 1.72 misformats manual pages for callouts, and 1.73 needs
+   the patch in contrib/patches/docbook-xsl-manpages-charmap.patch
index 26ac4b6bb0e974130ae84f4e35e1464f8b08e202..4dda3405bb6d456e8f4910e834affc6a82827f77 100644 (file)
--- a/Makefile
+++ b/Makefile
+# The default target of this Makefile is...
 all::
 
 # Define V=1 to have a more verbose compile.
 #
-# Define NO_MSGFMT if you do not have msgfmt from the GNU gettext
-# package and want to use our rough pure Tcl po->msg translator.
-# TCL_PATH must be vaild for this to work.
+# Define NO_OPENSSL environment variable if you do not have OpenSSL.
+# This also implies MOZILLA_SHA1.
+#
+# Define NO_CURL if you do not have curl installed.  git-http-pull and
+# git-http-push are not built, and you cannot use http:// and https://
+# transports.
+#
+# Define CURLDIR=/foo/bar if your curl header and library files are in
+# /foo/bar/include and /foo/bar/lib directories.
+#
+# Define NO_EXPAT if you do not have expat installed.  git-http-push is
+# not built, and you cannot push using http:// and https:// transports.
+#
+# Define NO_D_INO_IN_DIRENT if you don't have d_ino in your struct dirent.
+#
+# Define NO_D_TYPE_IN_DIRENT if your platform defines DT_UNKNOWN but lacks
+# d_type in struct dirent (latest Cygwin -- will be fixed soonish).
+#
+# Define NO_C99_FORMAT if your formatted IO functions (printf/scanf et.al.)
+# do not support the 'size specifiers' introduced by C99, namely ll, hh,
+# j, z, t. (representing long long int, char, intmax_t, size_t, ptrdiff_t).
+# some C compilers supported these specifiers prior to C99 as an extension.
+#
+# Define NO_STRCASESTR if you don't have strcasestr.
+#
+# Define NO_MEMMEM if you don't have memmem.
+#
+# Define NO_STRLCPY if you don't have strlcpy.
+#
+# Define NO_STRTOUMAX if you don't have strtoumax in the C library.
+# If your compiler also does not support long long or does not have
+# strtoull, define NO_STRTOULL.
+#
+# Define NO_SETENV if you don't have setenv in the C library.
+#
+# Define NO_MKDTEMP if you don't have mkdtemp in the C library.
+#
+# Define NO_SYMLINK_HEAD if you never want .git/HEAD to be a symbolic link.
+# Enable it on Windows.  By default, symrefs are still used.
+#
+# Define NO_SVN_TESTS if you want to skip time-consuming SVN interoperability
+# tests.  These tests take up a significant amount of the total test time
+# but are not needed unless you plan to talk to SVN repos.
+#
+# Define NO_FINK if you are building on Darwin/Mac OS X, have Fink
+# installed in /sw, but don't want GIT to link against any libraries
+# installed there.  If defined you may specify your own (or Fink's)
+# include directories and library directories by defining CFLAGS
+# and LDFLAGS appropriately.
+#
+# Define NO_DARWIN_PORTS if you are building on Darwin/Mac OS X,
+# have DarwinPorts installed in /opt/local, but don't want GIT to
+# link against any libraries installed there.  If defined you may
+# specify your own (or DarwinPort's) include directories and
+# library directories by defining CFLAGS and LDFLAGS appropriately.
+#
+# Define PPC_SHA1 environment variable when running make to make use of
+# a bundled SHA1 routine optimized for PowerPC.
+#
+# Define ARM_SHA1 environment variable when running make to make use of
+# a bundled SHA1 routine optimized for ARM.
+#
+# Define MOZILLA_SHA1 environment variable when running make to make use of
+# a bundled SHA1 routine coming from Mozilla. It is GPL'd and should be fast
+# on non-x86 architectures (e.g. PowerPC), while the OpenSSL version (default
+# choice) has very fast version optimized for i586.
+#
+# Define NEEDS_SSL_WITH_CRYPTO if you need -lcrypto with -lssl (Darwin).
+#
+# Define NEEDS_LIBICONV if linking with libc is not enough (Darwin).
+#
+# Define NEEDS_SOCKET if linking with libc is not enough (SunOS,
+# Patrick Mauritz).
+#
+# Define NO_MMAP if you want to avoid mmap.
+#
+# Define NO_PREAD if you have a problem with pread() system call (e.g.
+# cygwin.dll before v1.5.22).
+#
+# Define NO_FAST_WORKING_DIRECTORY if accessing objects in pack files is
+# generally faster on your platform than accessing the working directory.
+#
+# Define NO_TRUSTABLE_FILEMODE if your filesystem may claim to support
+# the executable mode bit, but doesn't really do so.
+#
+# Define NO_IPV6 if you lack IPv6 support and getaddrinfo().
+#
+# Define NO_SOCKADDR_STORAGE if your platform does not have struct
+# sockaddr_storage.
+#
+# Define NO_ICONV if your libc does not properly support iconv.
+#
+# Define OLD_ICONV if your library has an old iconv(), where the second
+# (input buffer pointer) parameter is declared with type (const char **).
+#
+# Define NO_DEFLATE_BOUND if your zlib does not have deflateBound.
+#
+# Define NO_R_TO_GCC_LINKER if your gcc does not like "-R/path/lib"
+# that tells runtime paths to dynamic libraries;
+# "-Wl,-rpath=/path/lib" is used instead.
+#
+# Define USE_NSEC below if you want git to care about sub-second file mtimes
+# and ctimes. Note that you need recent glibc (at least 2.2.4) for this, and
+# it will BREAK YOUR LOCAL DIFFS! show-diff and anything using it will likely
+# randomly break unless your underlying filesystem supports those sub-second
+# times (my ext3 doesn't).
+#
+# Define USE_STDEV below if you want git to care about the underlying device
+# change being considered an inode change from the update-index perspective.
+#
+# Define ASCIIDOC8 if you want to format documentation with AsciiDoc 8
+#
+# Define DOCBOOK_XSL_172 if you want to format man pages with DocBook XSL v1.72.
+#
+# Define NO_PERL_MAKEMAKER if you cannot use Makefiles generated by perl's
+# MakeMaker (e.g. using ActiveState under Cygwin).
+#
+# Define NO_TCLTK if you do not want Tcl/Tk GUI.
+#
+# The TCL_PATH variable governs the location of the Tcl interpreter
+# used to optimize git-gui for your system.  Only used if NO_TCLTK
+# is not set.  Defaults to the bare 'tclsh'.
+#
+# The TCLTK_PATH variable governs the location of the Tcl/Tk interpreter.
+# If not set it defaults to the bare 'wish'. If it is set to the empty
+# string then NO_TCLTK will be forced (this is used by configure script).
+#
+# Define THREADED_DELTA_SEARCH if you have pthreads and wish to exploit
+# parallel delta searching when packing objects.
 #
 
 GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
@@ -12,298 +139,1039 @@ GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
 -include GIT-VERSION-FILE
 
 uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
+uname_M := $(shell sh -c 'uname -m 2>/dev/null || echo not')
 uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not')
+uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not')
+uname_P := $(shell sh -c 'uname -p 2>/dev/null || echo not')
+
+# CFLAGS and LDFLAGS are for the users to override from the command line.
+
+CFLAGS = -g -O2 -Wall
+LDFLAGS =
+ALL_CFLAGS = $(CFLAGS)
+ALL_LDFLAGS = $(LDFLAGS)
+STRIP ?= strip
+
+prefix = $(HOME)
+bindir = $(prefix)/bin
+gitexecdir = $(bindir)
+sharedir = $(prefix)/share
+template_dir = $(sharedir)/git-core/templates
+ifeq ($(prefix),/usr)
+sysconfdir = /etc
+else
+sysconfdir = $(prefix)/etc
+endif
+lib = lib
+ETC_GITCONFIG = $(sysconfdir)/gitconfig
+# DESTDIR=
+
+# default configuration for gitweb
+GITWEB_CONFIG = gitweb_config.perl
+GITWEB_HOME_LINK_STR = projects
+GITWEB_SITENAME =
+GITWEB_PROJECTROOT = /pub/git
+GITWEB_PROJECT_MAXDEPTH = 2007
+GITWEB_EXPORT_OK =
+GITWEB_STRICT_EXPORT =
+GITWEB_BASE_URL =
+GITWEB_LIST =
+GITWEB_HOMETEXT = indextext.html
+GITWEB_CSS = gitweb.css
+GITWEB_LOGO = git-logo.png
+GITWEB_FAVICON = git-favicon.png
+GITWEB_SITE_HEADER =
+GITWEB_SITE_FOOTER =
+
+export prefix bindir gitexecdir sharedir template_dir sysconfdir
+
+CC = gcc
+AR = ar
+RM = rm -f
+TAR = tar
+FIND = find
+INSTALL = install
+RPMBUILD = rpmbuild
+TCL_PATH = tclsh
+TCLTK_PATH = wish
 
-SCRIPT_SH = git-gui.sh
-GITGUI_MAIN := git-gui
-GITGUI_BUILT_INS = git-citool
-ALL_LIBFILES = $(wildcard lib/*.tcl)
-PRELOAD_FILES = lib/class.tcl
-NONTCL_LIBFILES = \
-       lib/git-gui.ico \
-       $(wildcard lib/win32_*.js) \
-#end NONTCL_LIBFILES
+export TCL_PATH TCLTK_PATH
 
+# sparse is architecture-neutral, which means that we need to tell it
+# explicitly what architecture to check for. Fix this up for yours..
+SPARSE_FLAGS = -D__BIG_ENDIAN__ -D__powerpc__
+
+
+
+### --- END CONFIGURATION SECTION ---
+
+# Those must not be GNU-specific; they are shared with perl/ which may
+# be built by a different compiler. (Note that this is an artifact now
+# but it still might be nice to keep that distinction.)
+BASIC_CFLAGS =
+BASIC_LDFLAGS =
+
+SCRIPT_SH = \
+       git-bisect.sh git-checkout.sh \
+       git-clone.sh \
+       git-merge-one-file.sh git-mergetool.sh git-parse-remote.sh \
+       git-pull.sh git-rebase.sh git-rebase--interactive.sh \
+       git-repack.sh git-request-pull.sh \
+       git-sh-setup.sh \
+       git-am.sh \
+       git-merge.sh git-merge-stupid.sh git-merge-octopus.sh \
+       git-merge-resolve.sh \
+       git-lost-found.sh git-quiltimport.sh git-submodule.sh \
+       git-filter-branch.sh \
+       git-stash.sh
+
+SCRIPT_PERL = \
+       git-add--interactive.perl \
+       git-archimport.perl git-cvsimport.perl git-relink.perl \
+       git-cvsserver.perl git-remote.perl git-cvsexportcommit.perl \
+       git-send-email.perl git-svn.perl
+
+SCRIPTS = $(patsubst %.sh,%,$(SCRIPT_SH)) \
+         $(patsubst %.perl,%,$(SCRIPT_PERL)) \
+         git-instaweb
+
+# ... and all the rest that could be moved out of bindir to gitexecdir
+PROGRAMS = \
+       git-fetch-pack$X \
+       git-hash-object$X git-index-pack$X \
+       git-fast-import$X \
+       git-daemon$X \
+       git-merge-index$X git-mktag$X git-mktree$X git-patch-id$X \
+       git-receive-pack$X \
+       git-send-pack$X git-shell$X \
+       git-show-index$X \
+       git-unpack-file$X \
+       git-update-server-info$X \
+       git-upload-pack$X \
+       git-pack-redundant$X git-var$X \
+       git-merge-tree$X git-imap-send$X \
+       git-merge-recursive$X \
+       $(EXTRA_PROGRAMS)
+
+# Empty...
+EXTRA_PROGRAMS =
+
+BUILT_INS = \
+       git-format-patch$X git-show$X git-whatchanged$X git-cherry$X \
+       git-get-tar-commit-id$X git-init$X git-repo-config$X \
+       git-fsck-objects$X git-cherry-pick$X git-peek-remote$X git-status$X \
+       $(patsubst builtin-%.o,git-%$X,$(BUILTIN_OBJS))
+
+# what 'all' will build and 'install' will install, in gitexecdir
+ALL_PROGRAMS = $(PROGRAMS) $(SCRIPTS)
+
+ALL_PROGRAMS += git-merge-subtree$X
+
+# what 'all' will build but not install in gitexecdir
+OTHER_PROGRAMS = git$X gitweb/gitweb.cgi
+
+# Set paths to tools early so that they can be used for version tests.
 ifndef SHELL_PATH
        SHELL_PATH = /bin/sh
 endif
+ifndef PERL_PATH
+       PERL_PATH = /usr/bin/perl
+endif
+
+export PERL_PATH
+
+LIB_FILE=libgit.a
+XDIFF_LIB=xdiff/lib.a
+
+LIB_H = \
+       archive.h blob.h cache.h cache-tree.h commit.h csum-file.h delta.h grep.h \
+       diff.h object.h pack.h pkt-line.h quote.h refs.h list-objects.h sideband.h \
+       run-command.h strbuf.h tag.h tree.h git-compat-util.h revision.h \
+       tree-walk.h log-tree.h dir.h path-list.h unpack-trees.h builtin.h \
+       utf8.h reflog-walk.h patch-ids.h attr.h decorate.h progress.h \
+       mailmap.h remote.h parse-options.h transport.h diffcore.h hash.h
+
+DIFF_OBJS = \
+       diff.o diff-lib.o diffcore-break.o diffcore-order.o \
+       diffcore-pickaxe.o diffcore-rename.o tree-diff.o combine-diff.o \
+       diffcore-delta.o log-tree.o
+
+LIB_OBJS = \
+       blob.o commit.o connect.o csum-file.o cache-tree.o base85.o \
+       date.o diff-delta.o entry.o exec_cmd.o ident.o \
+       pretty.o interpolate.o hash.o \
+       lockfile.o \
+       patch-ids.o \
+       object.o pack-check.o pack-write.o patch-delta.o path.o pkt-line.o \
+       sideband.o reachable.o reflog-walk.o \
+       quote.o read-cache.o refs.o run-command.o dir.o object-refs.o \
+       server-info.o setup.o sha1_file.o sha1_name.o strbuf.o \
+       tag.o tree.o usage.o config.o environment.o ctype.o copy.o \
+       revision.o pager.o tree-walk.o xdiff-interface.o \
+       write_or_die.o trace.o list-objects.o grep.o match-trees.o \
+       alloc.o merge-file.o path-list.o help.o unpack-trees.o $(DIFF_OBJS) \
+       color.o wt-status.o archive-zip.o archive-tar.o shallow.o utf8.o \
+       convert.o attr.o decorate.o progress.o mailmap.o symlinks.o remote.o \
+       transport.o bundle.o walker.o parse-options.o
+
+BUILTIN_OBJS = \
+       builtin-add.o \
+       builtin-annotate.o \
+       builtin-apply.o \
+       builtin-archive.o \
+       builtin-blame.o \
+       builtin-branch.o \
+       builtin-bundle.o \
+       builtin-cat-file.o \
+       builtin-check-attr.o \
+       builtin-checkout-index.o \
+       builtin-check-ref-format.o \
+       builtin-clean.o \
+       builtin-commit.o \
+       builtin-commit-tree.o \
+       builtin-count-objects.o \
+       builtin-describe.o \
+       builtin-diff.o \
+       builtin-diff-files.o \
+       builtin-diff-index.o \
+       builtin-diff-tree.o \
+       builtin-fast-export.o \
+       builtin-fetch.o \
+       builtin-fetch-pack.o \
+       builtin-fetch--tool.o \
+       builtin-fmt-merge-msg.o \
+       builtin-for-each-ref.o \
+       builtin-fsck.o \
+       builtin-gc.o \
+       builtin-grep.o \
+       builtin-init-db.o \
+       builtin-log.o \
+       builtin-ls-files.o \
+       builtin-ls-tree.o \
+       builtin-ls-remote.o \
+       builtin-mailinfo.o \
+       builtin-mailsplit.o \
+       builtin-merge-base.o \
+       builtin-merge-file.o \
+       builtin-merge-ours.o \
+       builtin-mv.o \
+       builtin-name-rev.o \
+       builtin-pack-objects.o \
+       builtin-prune.o \
+       builtin-prune-packed.o \
+       builtin-push.o \
+       builtin-read-tree.o \
+       builtin-reflog.o \
+       builtin-send-pack.o \
+       builtin-config.o \
+       builtin-rerere.o \
+       builtin-reset.o \
+       builtin-rev-list.o \
+       builtin-rev-parse.o \
+       builtin-revert.o \
+       builtin-rm.o \
+       builtin-shortlog.o \
+       builtin-show-branch.o \
+       builtin-stripspace.o \
+       builtin-symbolic-ref.o \
+       builtin-tag.o \
+       builtin-tar-tree.o \
+       builtin-unpack-objects.o \
+       builtin-update-index.o \
+       builtin-update-ref.o \
+       builtin-upload-archive.o \
+       builtin-verify-pack.o \
+       builtin-verify-tag.o \
+       builtin-write-tree.o \
+       builtin-show-ref.o \
+       builtin-pack-refs.o
+
+GITLIBS = $(LIB_FILE) $(XDIFF_LIB)
+EXTLIBS =
+
+#
+# Platform specific tweaks
+#
+
+# We choose to avoid "if .. else if .. else .. endif endif"
+# because maintaining the nesting to match is a pain.  If
+# we had "elif" things would have been much nicer...
+
+ifeq ($(uname_S),Linux)
+       NO_STRLCPY = YesPlease
+endif
+ifeq ($(uname_S),GNU/kFreeBSD)
+       NO_STRLCPY = YesPlease
+endif
+ifeq ($(uname_S),Darwin)
+       NEEDS_SSL_WITH_CRYPTO = YesPlease
+       NEEDS_LIBICONV = YesPlease
+       ifneq ($(shell expr "$(uname_R)" : '9\.'),2)
+               OLD_ICONV = UnfortunatelyYes
+       endif
+       NO_STRLCPY = YesPlease
+       NO_MEMMEM = YesPlease
+endif
+ifeq ($(uname_S),SunOS)
+       NEEDS_SOCKET = YesPlease
+       NEEDS_NSL = YesPlease
+       SHELL_PATH = /bin/bash
+       NO_STRCASESTR = YesPlease
+       NO_MEMMEM = YesPlease
+       NO_HSTRERROR = YesPlease
+       NO_MKDTEMP = YesPlease
+       ifeq ($(uname_R),5.8)
+               NEEDS_LIBICONV = YesPlease
+               NO_UNSETENV = YesPlease
+               NO_SETENV = YesPlease
+               NO_C99_FORMAT = YesPlease
+               NO_STRTOUMAX = YesPlease
+       endif
+       ifeq ($(uname_R),5.9)
+               NO_UNSETENV = YesPlease
+               NO_SETENV = YesPlease
+               NO_C99_FORMAT = YesPlease
+               NO_STRTOUMAX = YesPlease
+       endif
+       INSTALL = ginstall
+       TAR = gtar
+       BASIC_CFLAGS += -D__EXTENSIONS__
+endif
+ifeq ($(uname_O),Cygwin)
+       NO_D_TYPE_IN_DIRENT = YesPlease
+       NO_D_INO_IN_DIRENT = YesPlease
+       NO_STRCASESTR = YesPlease
+       NO_MEMMEM = YesPlease
+       NO_SYMLINK_HEAD = YesPlease
+       NEEDS_LIBICONV = YesPlease
+       NO_FAST_WORKING_DIRECTORY = UnfortunatelyYes
+       NO_TRUSTABLE_FILEMODE = UnfortunatelyYes
+       OLD_ICONV = UnfortunatelyYes
+       # There are conflicting reports about this.
+       # On some boxes NO_MMAP is needed, and not so elsewhere.
+       # Try commenting this out if you suspect MMAP is more efficient
+       NO_MMAP = YesPlease
+       NO_IPV6 = YesPlease
+       X = .exe
+endif
+ifeq ($(uname_S),FreeBSD)
+       NEEDS_LIBICONV = YesPlease
+       NO_MEMMEM = YesPlease
+       BASIC_CFLAGS += -I/usr/local/include
+       BASIC_LDFLAGS += -L/usr/local/lib
+endif
+ifeq ($(uname_S),OpenBSD)
+       NO_STRCASESTR = YesPlease
+       NO_MEMMEM = YesPlease
+       NEEDS_LIBICONV = YesPlease
+       BASIC_CFLAGS += -I/usr/local/include
+       BASIC_LDFLAGS += -L/usr/local/lib
+endif
+ifeq ($(uname_S),NetBSD)
+       ifeq ($(shell expr "$(uname_R)" : '[01]\.'),2)
+               NEEDS_LIBICONV = YesPlease
+       endif
+       BASIC_CFLAGS += -I/usr/pkg/include
+       BASIC_LDFLAGS += -L/usr/pkg/lib
+       ALL_LDFLAGS += -Wl,-rpath,/usr/pkg/lib
+endif
+ifeq ($(uname_S),AIX)
+       NO_STRCASESTR=YesPlease
+       NO_MEMMEM = YesPlease
+       NO_STRLCPY = YesPlease
+       NEEDS_LIBICONV=YesPlease
+endif
+ifeq ($(uname_S),GNU)
+       # GNU/Hurd
+       NO_STRLCPY=YesPlease
+endif
+ifeq ($(uname_S),IRIX64)
+       NO_IPV6=YesPlease
+       NO_SETENV=YesPlease
+       NO_STRCASESTR=YesPlease
+       NO_MEMMEM = YesPlease
+       NO_STRLCPY = YesPlease
+       NO_SOCKADDR_STORAGE=YesPlease
+       SHELL_PATH=/usr/gnu/bin/bash
+       BASIC_CFLAGS += -DPATH_MAX=1024
+       # for now, build 32-bit version
+       BASIC_LDFLAGS += -L/usr/lib32
+endif
+ifneq (,$(findstring arm,$(uname_M)))
+       ARM_SHA1 = YesPlease
+endif
+
+-include config.mak.autogen
+-include config.mak
+
+ifeq ($(uname_S),Darwin)
+       ifndef NO_FINK
+               ifeq ($(shell test -d /sw/lib && echo y),y)
+                       BASIC_CFLAGS += -I/sw/include
+                       BASIC_LDFLAGS += -L/sw/lib
+               endif
+       endif
+       ifndef NO_DARWIN_PORTS
+               ifeq ($(shell test -d /opt/local/lib && echo y),y)
+                       BASIC_CFLAGS += -I/opt/local/include
+                       BASIC_LDFLAGS += -L/opt/local/lib
+               endif
+       endif
+endif
+
+ifdef NO_R_TO_GCC_LINKER
+       # Some gcc does not accept and pass -R to the linker to specify
+       # the runtime dynamic library path.
+       CC_LD_DYNPATH = -Wl,-rpath=
+else
+       CC_LD_DYNPATH = -R
+endif
+
+ifdef NO_CURL
+       BASIC_CFLAGS += -DNO_CURL
+else
+       ifdef CURLDIR
+               # Try "-Wl,-rpath=$(CURLDIR)/$(lib)" in such a case.
+               BASIC_CFLAGS += -I$(CURLDIR)/include
+               CURL_LIBCURL = -L$(CURLDIR)/$(lib) $(CC_LD_DYNPATH)$(CURLDIR)/$(lib) -lcurl
+       else
+               CURL_LIBCURL = -lcurl
+       endif
+       BUILTIN_OBJS += builtin-http-fetch.o
+       EXTLIBS += $(CURL_LIBCURL)
+       LIB_OBJS += http.o http-walker.o
+       curl_check := $(shell (echo 070908; curl-config --vernum) | sort -r | sed -ne 2p)
+       ifeq "$(curl_check)" "070908"
+               ifndef NO_EXPAT
+                       PROGRAMS += git-http-push$X
+               endif
+       endif
+       ifndef NO_EXPAT
+               EXPAT_LIBEXPAT = -lexpat
+       endif
+endif
+
+ifdef ZLIB_PATH
+       BASIC_CFLAGS += -I$(ZLIB_PATH)/include
+       EXTLIBS += -L$(ZLIB_PATH)/$(lib) $(CC_LD_DYNPATH)$(ZLIB_PATH)/$(lib)
+endif
+EXTLIBS += -lz
+
+ifndef NO_OPENSSL
+       OPENSSL_LIBSSL = -lssl
+       ifdef OPENSSLDIR
+               BASIC_CFLAGS += -I$(OPENSSLDIR)/include
+               OPENSSL_LINK = -L$(OPENSSLDIR)/$(lib) $(CC_LD_DYNPATH)$(OPENSSLDIR)/$(lib)
+       else
+               OPENSSL_LINK =
+       endif
+else
+       BASIC_CFLAGS += -DNO_OPENSSL
+       MOZILLA_SHA1 = 1
+       OPENSSL_LIBSSL =
+endif
+ifdef NEEDS_SSL_WITH_CRYPTO
+       LIB_4_CRYPTO = $(OPENSSL_LINK) -lcrypto -lssl
+else
+       LIB_4_CRYPTO = $(OPENSSL_LINK) -lcrypto
+endif
+ifdef NEEDS_LIBICONV
+       ifdef ICONVDIR
+               BASIC_CFLAGS += -I$(ICONVDIR)/include
+               ICONV_LINK = -L$(ICONVDIR)/$(lib) $(CC_LD_DYNPATH)$(ICONVDIR)/$(lib)
+       else
+               ICONV_LINK =
+       endif
+       EXTLIBS += $(ICONV_LINK) -liconv
+endif
+ifdef NEEDS_SOCKET
+       EXTLIBS += -lsocket
+endif
+ifdef NEEDS_NSL
+       EXTLIBS += -lnsl
+endif
+ifdef NO_D_TYPE_IN_DIRENT
+       BASIC_CFLAGS += -DNO_D_TYPE_IN_DIRENT
+endif
+ifdef NO_D_INO_IN_DIRENT
+       BASIC_CFLAGS += -DNO_D_INO_IN_DIRENT
+endif
+ifdef NO_C99_FORMAT
+       BASIC_CFLAGS += -DNO_C99_FORMAT
+endif
+ifdef NO_SYMLINK_HEAD
+       BASIC_CFLAGS += -DNO_SYMLINK_HEAD
+endif
+ifdef NO_STRCASESTR
+       COMPAT_CFLAGS += -DNO_STRCASESTR
+       COMPAT_OBJS += compat/strcasestr.o
+endif
+ifdef NO_STRLCPY
+       COMPAT_CFLAGS += -DNO_STRLCPY
+       COMPAT_OBJS += compat/strlcpy.o
+endif
+ifdef NO_STRTOUMAX
+       COMPAT_CFLAGS += -DNO_STRTOUMAX
+       COMPAT_OBJS += compat/strtoumax.o
+endif
+ifdef NO_STRTOULL
+       COMPAT_CFLAGS += -DNO_STRTOULL
+endif
+ifdef NO_SETENV
+       COMPAT_CFLAGS += -DNO_SETENV
+       COMPAT_OBJS += compat/setenv.o
+endif
+ifdef NO_MKDTEMP
+       COMPAT_CFLAGS += -DNO_MKDTEMP
+       COMPAT_OBJS += compat/mkdtemp.o
+endif
+ifdef NO_UNSETENV
+       COMPAT_CFLAGS += -DNO_UNSETENV
+       COMPAT_OBJS += compat/unsetenv.o
+endif
+ifdef NO_MMAP
+       COMPAT_CFLAGS += -DNO_MMAP
+       COMPAT_OBJS += compat/mmap.o
+endif
+ifdef NO_PREAD
+       COMPAT_CFLAGS += -DNO_PREAD
+       COMPAT_OBJS += compat/pread.o
+endif
+ifdef NO_FAST_WORKING_DIRECTORY
+       BASIC_CFLAGS += -DNO_FAST_WORKING_DIRECTORY
+endif
+ifdef NO_TRUSTABLE_FILEMODE
+       BASIC_CFLAGS += -DNO_TRUSTABLE_FILEMODE
+endif
+ifdef NO_IPV6
+       BASIC_CFLAGS += -DNO_IPV6
+endif
+ifdef NO_SOCKADDR_STORAGE
+ifdef NO_IPV6
+       BASIC_CFLAGS += -Dsockaddr_storage=sockaddr_in
+else
+       BASIC_CFLAGS += -Dsockaddr_storage=sockaddr_in6
+endif
+endif
+ifdef NO_INET_NTOP
+       LIB_OBJS += compat/inet_ntop.o
+endif
+ifdef NO_INET_PTON
+       LIB_OBJS += compat/inet_pton.o
+endif
 
-ifndef gitexecdir
-       gitexecdir := $(shell git --exec-path)
+ifdef NO_ICONV
+       BASIC_CFLAGS += -DNO_ICONV
 endif
 
-ifndef sharedir
-       sharedir := $(dir $(gitexecdir))share
+ifdef OLD_ICONV
+       BASIC_CFLAGS += -DOLD_ICONV
 endif
 
-ifndef INSTALL
-       INSTALL = install
+ifdef NO_DEFLATE_BOUND
+       BASIC_CFLAGS += -DNO_DEFLATE_BOUND
 endif
 
-RM_RF     ?= rm -rf
-RMDIR     ?= rmdir
+ifdef PPC_SHA1
+       SHA1_HEADER = "ppc/sha1.h"
+       LIB_OBJS += ppc/sha1.o ppc/sha1ppc.o
+else
+ifdef ARM_SHA1
+       SHA1_HEADER = "arm/sha1.h"
+       LIB_OBJS += arm/sha1.o arm/sha1_arm.o
+else
+ifdef MOZILLA_SHA1
+       SHA1_HEADER = "mozilla-sha1/sha1.h"
+       LIB_OBJS += mozilla-sha1/sha1.o
+else
+       SHA1_HEADER = <openssl/sha.h>
+       EXTLIBS += $(LIB_4_CRYPTO)
+endif
+endif
+endif
+ifdef NO_PERL_MAKEMAKER
+       export NO_PERL_MAKEMAKER
+endif
+ifdef NO_HSTRERROR
+       COMPAT_CFLAGS += -DNO_HSTRERROR
+       COMPAT_OBJS += compat/hstrerror.o
+endif
+ifdef NO_MEMMEM
+       COMPAT_CFLAGS += -DNO_MEMMEM
+       COMPAT_OBJS += compat/memmem.o
+endif
+
+ifdef THREADED_DELTA_SEARCH
+       BASIC_CFLAGS += -DTHREADED_DELTA_SEARCH
+       EXTLIBS += -lpthread
+endif
 
-INSTALL_D0 = $(INSTALL) -d -m 755 # space is required here
-INSTALL_D1 =
-INSTALL_R0 = $(INSTALL) -m 644 # space is required here
-INSTALL_R1 =
-INSTALL_X0 = $(INSTALL) -m 755 # space is required here
-INSTALL_X1 =
-INSTALL_A0 = find # space is required here
-INSTALL_A1 = | cpio -pud
-INSTALL_L0 = rm -f # space is required here
-INSTALL_L1 = && ln # space is required here
-INSTALL_L2 =
-INSTALL_L3 =
+ifeq ($(TCLTK_PATH),)
+NO_TCLTK=NoThanks
+endif
+
+QUIET_SUBDIR0  = +$(MAKE) -C # space to separate -C and subdir
+QUIET_SUBDIR1  =
 
-REMOVE_D0  = $(RMDIR) # space is required here
-REMOVE_D1  = || true
-REMOVE_F0  = $(RM_RF) # space is required here
-REMOVE_F1  =
-CLEAN_DST  = true
+ifneq ($(findstring $(MAKEFLAGS),w),w)
+PRINT_DIR = --no-print-directory
+else # "make -w"
+NO_SUBDIR = :
+endif
 
+ifneq ($(findstring $(MAKEFLAGS),s),s)
 ifndef V
-       QUIET          = @
-       QUIET_GEN      = $(QUIET)echo '   ' GEN '$@' &&
-       QUIET_INDEX    = $(QUIET)echo '   ' INDEX $(dir $@) &&
-       QUIET_MSGFMT0  = $(QUIET)printf '    MSGFMT %12s ' $@ && v=`
-       QUIET_MSGFMT1  = 2>&1` && echo "$$v" | sed -e 's/fuzzy translations/fuzzy/' | sed -e 's/ messages//g'
-       QUIET_2DEVNULL = 2>/dev/null
-
-       INSTALL_D0 = dir=
-       INSTALL_D1 = && echo ' ' DEST $$dir && $(INSTALL) -d -m 755 "$$dir"
-       INSTALL_R0 = src=
-       INSTALL_R1 = && echo '   ' INSTALL 644 `basename $$src` && $(INSTALL) -m 644 $$src
-       INSTALL_X0 = src=
-       INSTALL_X1 = && echo '   ' INSTALL 755 `basename $$src` && $(INSTALL) -m 755 $$src
-       INSTALL_A0 = src=
-       INSTALL_A1 = && echo '   ' INSTALL '   ' `basename "$$src"` && find "$$src" | cpio -pud
-
-       INSTALL_L0 = dst=
-       INSTALL_L1 = && src=
-       INSTALL_L2 = && dst=
-       INSTALL_L3 = && echo '   ' 'LINK       ' `basename "$$dst"` '->' `basename "$$src"` && rm -f "$$dst" && ln "$$src" "$$dst"
-
-       CLEAN_DST = echo ' ' UNINSTALL
-       REMOVE_D0 = dir=
-       REMOVE_D1 = && echo ' ' REMOVE $$dir && test -d "$$dir" && $(RMDIR) "$$dir" || true
-       REMOVE_F0 = dst=
-       REMOVE_F1 = && echo '   ' REMOVE `basename "$$dst"` && $(RM_RF) "$$dst"
-endif
-
-TCL_PATH   ?= tclsh
-TCLTK_PATH ?= wish
-TKFRAMEWORK = /Library/Frameworks/Tk.framework/Resources/Wish.app
-
-ifeq ($(findstring $(MAKEFLAGS),s),s)
-QUIET_GEN =
+       QUIET_CC       = @echo '   ' CC $@;
+       QUIET_AR       = @echo '   ' AR $@;
+       QUIET_LINK     = @echo '   ' LINK $@;
+       QUIET_BUILT_IN = @echo '   ' BUILTIN $@;
+       QUIET_GEN      = @echo '   ' GEN $@;
+       QUIET_SUBDIR0  = +@subdir=
+       QUIET_SUBDIR1  = ;$(NO_SUBDIR) echo '   ' SUBDIR $$subdir; \
+                        $(MAKE) $(PRINT_DIR) -C $$subdir
+       export V
+       export QUIET_GEN
+       export QUIET_BUILT_IN
+endif
 endif
 
--include config.mak
+ifdef ASCIIDOC8
+       export ASCIIDOC8
+endif
+
+# Shell quote (do not use $(call) to accommodate ancient setups);
+
+SHA1_HEADER_SQ = $(subst ','\'',$(SHA1_HEADER))
+ETC_GITCONFIG_SQ = $(subst ','\'',$(ETC_GITCONFIG))
 
 DESTDIR_SQ = $(subst ','\'',$(DESTDIR))
+bindir_SQ = $(subst ','\'',$(bindir))
 gitexecdir_SQ = $(subst ','\'',$(gitexecdir))
+template_dir_SQ = $(subst ','\'',$(template_dir))
+prefix_SQ = $(subst ','\'',$(prefix))
+
 SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
-TCL_PATH_SQ = $(subst ','\'',$(TCL_PATH))
+PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
 TCLTK_PATH_SQ = $(subst ','\'',$(TCLTK_PATH))
-TCLTK_PATH_SED = $(subst ','\'',$(subst \,\\,$(TCLTK_PATH)))
 
-gg_libdir ?= $(sharedir)/git-gui/lib
-libdir_SQ  = $(subst ','\'',$(gg_libdir))
-libdir_SED = $(subst ','\'',$(subst \,\\,$(gg_libdir)))
-exedir     = $(dir $(gitexecdir))share/git-gui/lib
+LIBS = $(GITLIBS) $(EXTLIBS)
 
-GITGUI_SCRIPT   := $$0
-GITGUI_RELATIVE :=
-GITGUI_MACOSXAPP :=
+BASIC_CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER_SQ)' \
+       $(COMPAT_CFLAGS)
+LIB_OBJS += $(COMPAT_OBJS)
 
-ifeq ($(exedir),$(gg_libdir))
-       GITGUI_RELATIVE := 1
-endif
+ALL_CFLAGS += $(BASIC_CFLAGS)
+ALL_LDFLAGS += $(BASIC_LDFLAGS)
 
-ifeq ($(uname_O),Cygwin)
-       GITGUI_SCRIPT := `cygpath --windows --absolute "$(GITGUI_SCRIPT)"`
-       ifeq ($(GITGUI_RELATIVE),)
-               gg_libdir := $(shell cygpath --windows --absolute "$(gg_libdir)")
-       endif
+export TAR INSTALL DESTDIR SHELL_PATH
+
+
+### Build rules
+
+all:: $(ALL_PROGRAMS) $(BUILT_INS) $(OTHER_PROGRAMS)
+ifneq (,$X)
+       $(foreach p,$(patsubst %$X,%,$(filter %$X,$(ALL_PROGRAMS) $(BUILT_INS) git$X)), $(RM) '$p';)
 endif
-ifeq ($(uname_S),Darwin)
-       ifeq ($(shell test -d $(TKFRAMEWORK) && echo y),y)
-               GITGUI_MACOSXAPP := YesPlease
-       endif
+
+all::
+ifndef NO_TCLTK
+       $(QUIET_SUBDIR0)git-gui $(QUIET_SUBDIR1) all
+       $(QUIET_SUBDIR0)gitk-git $(QUIET_SUBDIR1) all
 endif
-ifneq (,$(findstring MINGW,$(uname_S)))
-       NO_MSGFMT=1
-       GITGUI_WINDOWS_WRAPPER := YesPlease
-endif
-
-ifdef GITGUI_MACOSXAPP
-GITGUI_MAIN := git-gui.tcl
-
-git-gui: GIT-VERSION-FILE GIT-GUI-VARS
-       $(QUIET_GEN)rm -f $@ $@+ && \
-       echo '#!$(SHELL_PATH_SQ)' >$@+ && \
-       echo 'if test "z$$*" = zversion ||' >>$@+ && \
-       echo '   test "z$$*" = z--version' >>$@+ && \
-       echo then >>$@+ && \
-       echo '  'echo \'git-gui version '$(GITGUI_VERSION)'\' >>$@+ && \
-       echo else >>$@+ && \
-       echo '  'exec \''$(libdir_SQ)/Git Gui.app/Contents/MacOS/Wish'\' \
-               '"$$0" "$$@"' >>$@+ && \
-       echo fi >>$@+ && \
+       $(QUIET_SUBDIR0)perl $(QUIET_SUBDIR1) PERL_PATH='$(PERL_PATH_SQ)' prefix='$(prefix_SQ)' all
+       $(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1)
+
+strip: $(PROGRAMS) git$X
+       $(STRIP) $(STRIP_OPTS) $(PROGRAMS) git$X
+
+git.o: git.c common-cmds.h GIT-CFLAGS
+       $(QUIET_CC)$(CC) -DGIT_VERSION='"$(GIT_VERSION)"' \
+               $(ALL_CFLAGS) -c $(filter %.c,$^)
+
+git$X: git.o $(BUILTIN_OBJS) $(GITLIBS)
+       $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ git.o \
+               $(BUILTIN_OBJS) $(ALL_LDFLAGS) $(LIBS)
+
+help.o: common-cmds.h
+
+git-merge-subtree$X: git-merge-recursive$X
+       $(QUIET_BUILT_IN)$(RM) $@ && ln git-merge-recursive$X $@
+
+$(BUILT_INS): git$X
+       $(QUIET_BUILT_IN)$(RM) $@ && ln git$X $@
+
+common-cmds.h: ./generate-cmdlist.sh command-list.txt
+
+common-cmds.h: $(wildcard Documentation/git-*.txt)
+       $(QUIET_GEN)./generate-cmdlist.sh > $@+ && mv $@+ $@
+
+$(patsubst %.sh,%,$(SCRIPT_SH)) : % : %.sh
+       $(QUIET_GEN)$(RM) $@ $@+ && \
+       sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
+           -e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \
+           -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
+           -e 's/@@NO_CURL@@/$(NO_CURL)/g' \
+           $@.sh >$@+ && \
+       chmod +x $@+ && \
+       mv $@+ $@
+
+$(patsubst %.perl,%,$(SCRIPT_PERL)): perl/perl.mak
+
+perl/perl.mak: GIT-CFLAGS perl/Makefile perl/Makefile.PL
+       $(QUIET_SUBDIR0)perl $(QUIET_SUBDIR1) PERL_PATH='$(PERL_PATH_SQ)' prefix='$(prefix_SQ)' $(@F)
+
+$(patsubst %.perl,%,$(SCRIPT_PERL)): % : %.perl
+       $(QUIET_GEN)$(RM) $@ $@+ && \
+       INSTLIBDIR=`MAKEFLAGS= $(MAKE) -C perl -s --no-print-directory instlibdir` && \
+       sed -e '1{' \
+           -e '        s|#!.*perl|#!$(PERL_PATH_SQ)|' \
+           -e '        h' \
+           -e '        s=.*=use lib (split(/:/, $$ENV{GITPERLLIB} || "@@INSTLIBDIR@@"));=' \
+           -e '        H' \
+           -e '        x' \
+           -e '}' \
+           -e 's|@@INSTLIBDIR@@|'"$$INSTLIBDIR"'|g' \
+           -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
+           $@.perl >$@+ && \
+       chmod +x $@+ && \
+       mv $@+ $@
+
+gitweb/gitweb.cgi: gitweb/gitweb.perl
+       $(QUIET_GEN)$(RM) $@ $@+ && \
+       sed -e '1s|#!.*perl|#!$(PERL_PATH_SQ)|' \
+           -e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \
+           -e 's|++GIT_BINDIR++|$(bindir)|g' \
+           -e 's|++GITWEB_CONFIG++|$(GITWEB_CONFIG)|g' \
+           -e 's|++GITWEB_HOME_LINK_STR++|$(GITWEB_HOME_LINK_STR)|g' \
+           -e 's|++GITWEB_SITENAME++|$(GITWEB_SITENAME)|g' \
+           -e 's|++GITWEB_PROJECTROOT++|$(GITWEB_PROJECTROOT)|g' \
+           -e 's|"++GITWEB_PROJECT_MAXDEPTH++"|$(GITWEB_PROJECT_MAXDEPTH)|g' \
+           -e 's|++GITWEB_EXPORT_OK++|$(GITWEB_EXPORT_OK)|g' \
+           -e 's|++GITWEB_STRICT_EXPORT++|$(GITWEB_STRICT_EXPORT)|g' \
+           -e 's|++GITWEB_BASE_URL++|$(GITWEB_BASE_URL)|g' \
+           -e 's|++GITWEB_LIST++|$(GITWEB_LIST)|g' \
+           -e 's|++GITWEB_HOMETEXT++|$(GITWEB_HOMETEXT)|g' \
+           -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \
+           -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \
+           -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \
+           -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \
+           -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \
+           $< >$@+ && \
        chmod +x $@+ && \
        mv $@+ $@
 
-Git\ Gui.app: GIT-VERSION-FILE GIT-GUI-VARS \
-               macosx/Info.plist \
-               macosx/git-gui.icns \
-               macosx/AppMain.tcl \
-               $(TKFRAMEWORK)/Contents/MacOS/Wish
-       $(QUIET_GEN)rm -rf '$@' '$@'+ && \
-       mkdir -p '$@'+/Contents/MacOS && \
-       mkdir -p '$@'+/Contents/Resources/Scripts && \
-       cp '$(subst ','\'',$(TKFRAMEWORK))/Contents/MacOS/Wish' \
-               '$@'+/Contents/MacOS && \
-       cp macosx/git-gui.icns '$@'+/Contents/Resources && \
-       sed -e 's/@@GITGUI_VERSION@@/$(GITGUI_VERSION)/g' \
-               macosx/Info.plist \
-               >'$@'+/Contents/Info.plist && \
-       sed -e 's|@@gitexecdir@@|$(gitexecdir_SQ)|' \
-               -e 's|@@GITGUI_LIBDIR@@|$(libdir_SED)|' \
-               macosx/AppMain.tcl \
-               >'$@'+/Contents/Resources/Scripts/AppMain.tcl && \
-       mv '$@'+ '$@'
-endif
-
-ifdef GITGUI_WINDOWS_WRAPPER
-GITGUI_MAIN := git-gui.tcl
-
-git-gui: windows/git-gui.sh
-       cp $< $@
-endif
-
-$(GITGUI_MAIN): git-gui.sh GIT-VERSION-FILE GIT-GUI-VARS
-       $(QUIET_GEN)rm -f $@ $@+ && \
+git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css
+       $(QUIET_GEN)$(RM) $@ $@+ && \
        sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
-               -e '1,30s|^ argv0=$$0| argv0=$(GITGUI_SCRIPT)|' \
-               -e '1,30s|^ exec wish | exec '\''$(TCLTK_PATH_SED)'\'' |' \
-               -e 's/@@GITGUI_VERSION@@/$(GITGUI_VERSION)/g' \
-               -e 's|@@GITGUI_RELATIVE@@|$(GITGUI_RELATIVE)|' \
-               -e '$(GITGUI_RELATIVE)s|@@GITGUI_LIBDIR@@|$(libdir_SED)|' \
-               git-gui.sh >$@+ && \
+           -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
+           -e 's/@@NO_CURL@@/$(NO_CURL)/g' \
+           -e '/@@GITWEB_CGI@@/r gitweb/gitweb.cgi' \
+           -e '/@@GITWEB_CGI@@/d' \
+           -e '/@@GITWEB_CSS@@/r gitweb/gitweb.css' \
+           -e '/@@GITWEB_CSS@@/d' \
+           $@.sh > $@+ && \
        chmod +x $@+ && \
        mv $@+ $@
 
-XGETTEXT   ?= xgettext
-ifdef NO_MSGFMT
-       MSGFMT ?= $(TCL_PATH) po/po2msg.sh
-else
-       MSGFMT ?= msgfmt
-endif
-
-msgsdir     = $(gg_libdir)/msgs
-msgsdir_SQ  = $(subst ','\'',$(msgsdir))
-PO_TEMPLATE = po/git-gui.pot
-ALL_POFILES = $(wildcard po/*.po)
-ALL_MSGFILES = $(subst .po,.msg,$(ALL_POFILES))
-
-$(PO_TEMPLATE): $(SCRIPT_SH) $(ALL_LIBFILES)
-       $(XGETTEXT) -kmc -LTcl -o $@ $(SCRIPT_SH) $(ALL_LIBFILES)
-update-po:: $(PO_TEMPLATE)
-       $(foreach p, $(ALL_POFILES), echo Updating $p ; msgmerge -U $p $(PO_TEMPLATE) ; )
-$(ALL_MSGFILES): %.msg : %.po
-       $(QUIET_MSGFMT0)$(MSGFMT) --statistics --tcl -l $(basename $(notdir $<)) -d $(dir $@) $< $(QUIET_MSGFMT1)
-
-lib/tclIndex: $(ALL_LIBFILES) GIT-GUI-VARS
-       $(QUIET_INDEX)if echo \
-         $(foreach p,$(PRELOAD_FILES),source $p\;) \
-         auto_mkindex lib '*.tcl' \
-       | $(TCL_PATH) $(QUIET_2DEVNULL); then : ok; \
-       else \
-        echo 1>&2 "    * $(TCL_PATH) failed; using unoptimized loading"; \
-        rm -f $@ ; \
-        echo '# Autogenerated by git-gui Makefile' >$@ && \
-        echo >>$@ && \
-        $(foreach p,$(PRELOAD_FILES) $(ALL_LIBFILES),echo '$(subst lib/,,$p)' >>$@ &&) \
-        echo >>$@ ; \
-       fi
+configure: configure.ac
+       $(QUIET_GEN)$(RM) $@ $<+ && \
+       sed -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
+           $< > $<+ && \
+       autoconf -o $@ $<+ && \
+       $(RM) $<+
+
+# These can record GIT_VERSION
+git.o git.spec \
+       $(patsubst %.sh,%,$(SCRIPT_SH)) \
+       $(patsubst %.perl,%,$(SCRIPT_PERL)) \
+       : GIT-VERSION-FILE
 
-TRACK_VARS = \
-       $(subst ','\'',SHELL_PATH='$(SHELL_PATH_SQ)') \
-       $(subst ','\'',TCL_PATH='$(TCL_PATH_SQ)') \
-       $(subst ','\'',TCLTK_PATH='$(TCLTK_PATH_SQ)') \
-       $(subst ','\'',gitexecdir='$(gitexecdir_SQ)') \
-       $(subst ','\'',gg_libdir='$(libdir_SQ)') \
-       GITGUI_MACOSXAPP=$(GITGUI_MACOSXAPP) \
-#end TRACK_VARS
+%.o: %.c GIT-CFLAGS
+       $(QUIET_CC)$(CC) -o $*.o -c $(ALL_CFLAGS) $<
+%.s: %.c GIT-CFLAGS
+       $(QUIET_CC)$(CC) -S $(ALL_CFLAGS) $<
+%.o: %.S
+       $(QUIET_CC)$(CC) -o $*.o -c $(ALL_CFLAGS) $<
+
+exec_cmd.o: exec_cmd.c GIT-CFLAGS
+       $(QUIET_CC)$(CC) -o $*.o -c $(ALL_CFLAGS) '-DGIT_EXEC_PATH="$(gitexecdir_SQ)"' $<
+builtin-init-db.o: builtin-init-db.c GIT-CFLAGS
+       $(QUIET_CC)$(CC) -o $*.o -c $(ALL_CFLAGS) -DDEFAULT_GIT_TEMPLATE_DIR='"$(template_dir_SQ)"' $<
+
+config.o: config.c GIT-CFLAGS
+       $(QUIET_CC)$(CC) -o $*.o -c $(ALL_CFLAGS) -DETC_GITCONFIG='"$(ETC_GITCONFIG_SQ)"' $<
+
+http.o: http.c GIT-CFLAGS
+       $(QUIET_CC)$(CC) -o $*.o -c $(ALL_CFLAGS) -DGIT_USER_AGENT='"git/$(GIT_VERSION)"' $<
+
+ifdef NO_EXPAT
+http-walker.o: http-walker.c http.h GIT-CFLAGS
+       $(QUIET_CC)$(CC) -o $*.o -c $(ALL_CFLAGS) -DNO_EXPAT $<
+endif
+
+git-%$X: %.o $(GITLIBS)
+       $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)
+
+git-imap-send$X: imap-send.o $(LIB_FILE)
+
+http.o http-walker.o http-push.o: http.h
+
+git-http-push$X: revision.o http.o http-push.o $(GITLIBS)
+       $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
+               $(LIBS) $(CURL_LIBCURL) $(EXPAT_LIBEXPAT)
+
+$(LIB_OBJS) $(BUILTIN_OBJS): $(LIB_H)
+$(patsubst git-%$X,%.o,$(PROGRAMS)): $(LIB_H) $(wildcard */*.h)
+builtin-revert.o builtin-runstatus.o wt-status.o: wt-status.h
+
+$(LIB_FILE): $(LIB_OBJS)
+       $(QUIET_AR)$(RM) $@ && $(AR) rcs $@ $(LIB_OBJS)
+
+XDIFF_OBJS=xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o \
+       xdiff/xmerge.o
+$(XDIFF_OBJS): xdiff/xinclude.h xdiff/xmacros.h xdiff/xdiff.h xdiff/xtypes.h \
+       xdiff/xutils.h xdiff/xprepare.h xdiff/xdiffi.h xdiff/xemit.h
+
+$(XDIFF_LIB): $(XDIFF_OBJS)
+       $(QUIET_AR)$(RM) $@ && $(AR) rcs $@ $(XDIFF_OBJS)
+
+
+doc:
+       $(MAKE) -C Documentation all
+
+info:
+       $(MAKE) -C Documentation info
+
+TAGS:
+       $(RM) TAGS
+       $(FIND) . -name '*.[hcS]' -print | xargs etags -a
+
+tags:
+       $(RM) tags
+       $(FIND) . -name '*.[hcS]' -print | xargs ctags -a
+
+cscope:
+       $(RM) cscope*
+       $(FIND) . -name '*.[hcS]' -print | xargs cscope -b
+
+### Detect prefix changes
+TRACK_CFLAGS = $(subst ','\'',$(ALL_CFLAGS)):\
+             $(bindir_SQ):$(gitexecdir_SQ):$(template_dir_SQ):$(prefix_SQ)
+
+GIT-CFLAGS: .FORCE-GIT-CFLAGS
+       @FLAGS='$(TRACK_CFLAGS)'; \
+           if test x"$$FLAGS" != x"`cat GIT-CFLAGS 2>/dev/null`" ; then \
+               echo 1>&2 "    * new build flags or prefix"; \
+               echo "$$FLAGS" >GIT-CFLAGS; \
+            fi
+
+### Detect Tck/Tk interpreter path changes
+ifndef NO_TCLTK
+TRACK_VARS = $(subst ','\'',-DTCLTK_PATH='$(TCLTK_PATH_SQ)')
 
 GIT-GUI-VARS: .FORCE-GIT-GUI-VARS
        @VARS='$(TRACK_VARS)'; \
-       if test x"$$VARS" != x"`cat $@ 2>/dev/null`" ; then \
-               echo 1>&2 "    * new locations or Tcl/Tk interpreter"; \
-               echo 1>$@ "$$VARS"; \
-       fi
+           if test x"$$VARS" != x"`cat $@ 2>/dev/null`" ; then \
+               echo 1>&2 "    * new Tcl/Tk interpreter location"; \
+               echo "$$VARS" >$@; \
+            fi
 
-ifdef GITGUI_MACOSXAPP
-all:: git-gui Git\ Gui.app
-endif
-ifdef GITGUI_WINDOWS_WRAPPER
-all:: git-gui
+.PHONY: .FORCE-GIT-GUI-VARS
 endif
-all:: $(GITGUI_MAIN) lib/tclIndex $(ALL_MSGFILES)
+
+### Testing rules
+
+TEST_PROGRAMS = test-chmtime$X test-genrandom$X test-date$X test-delta$X test-sha1$X test-match-trees$X test-absolute-path$X test-parse-options$X
+
+all:: $(TEST_PROGRAMS)
+
+# GNU make supports exporting all variables by "export" without parameters.
+# However, the environment gets quite big, and some programs have problems
+# with that.
+
+export NO_SVN_TESTS
+
+test: all
+       $(MAKE) -C t/ all
+
+test-date$X: date.o ctype.o
+
+test-delta$X: diff-delta.o patch-delta.o
+
+test-parse-options$X: parse-options.o
+
+.PRECIOUS: $(patsubst test-%$X,test-%.o,$(TEST_PROGRAMS))
+
+test-%$X: test-%.o $(GITLIBS)
+       $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)
+
+check-sha1:: test-sha1$X
+       ./test-sha1.sh
+
+check: common-cmds.h
+       for i in *.c; do sparse $(ALL_CFLAGS) $(SPARSE_FLAGS) $$i || exit; done
+
+remove-dashes:
+       ./fixup-builtins $(BUILT_INS)
+
+### Installation rules
 
 install: all
-       $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(gitexecdir_SQ)' $(INSTALL_D1)
-       $(QUIET)$(INSTALL_X0)git-gui $(INSTALL_X1) '$(DESTDIR_SQ)$(gitexecdir_SQ)'
-       $(QUIET)$(foreach p,$(GITGUI_BUILT_INS), $(INSTALL_L0)'$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' $(INSTALL_L1)'$(DESTDIR_SQ)$(gitexecdir_SQ)/git-gui' $(INSTALL_L2)'$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' $(INSTALL_L3) &&) true
-ifdef GITGUI_WINDOWS_WRAPPER
-       $(QUIET)$(INSTALL_R0)git-gui.tcl $(INSTALL_R1) '$(DESTDIR_SQ)$(gitexecdir_SQ)'
-endif
-       $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(libdir_SQ)' $(INSTALL_D1)
-       $(QUIET)$(INSTALL_R0)lib/tclIndex $(INSTALL_R1) '$(DESTDIR_SQ)$(libdir_SQ)'
-ifdef GITGUI_MACOSXAPP
-       $(QUIET)$(INSTALL_A0)'Git Gui.app' $(INSTALL_A1) '$(DESTDIR_SQ)$(libdir_SQ)'
-       $(QUIET)$(INSTALL_X0)git-gui.tcl $(INSTALL_X1) '$(DESTDIR_SQ)$(libdir_SQ)'
-endif
-       $(QUIET)$(foreach p,$(ALL_LIBFILES) $(NONTCL_LIBFILES), $(INSTALL_R0)$p $(INSTALL_R1) '$(DESTDIR_SQ)$(libdir_SQ)' &&) true
-       $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(msgsdir_SQ)' $(INSTALL_D1)
-       $(QUIET)$(foreach p,$(ALL_MSGFILES), $(INSTALL_R0)$p $(INSTALL_R1) '$(DESTDIR_SQ)$(msgsdir_SQ)' &&) true
-
-uninstall:
-       $(QUIET)$(CLEAN_DST) '$(DESTDIR_SQ)$(gitexecdir_SQ)'
-       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/git-gui $(REMOVE_F1)
-       $(QUIET)$(foreach p,$(GITGUI_BUILT_INS), $(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/$p $(REMOVE_F1) &&) true
-ifdef GITGUI_WINDOWS_WRAPPER
-       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/git-gui.tcl $(REMOVE_F1)
-endif
-       $(QUIET)$(CLEAN_DST) '$(DESTDIR_SQ)$(libdir_SQ)'
-       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)'/tclIndex $(REMOVE_F1)
-ifdef GITGUI_MACOSXAPP
-       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)/Git Gui.app' $(REMOVE_F1)
-       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)'/git-gui.tcl $(REMOVE_F1)
-endif
-       $(QUIET)$(foreach p,$(ALL_LIBFILES) $(NONTCL_LIBFILES), $(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)'/$(notdir $p) $(REMOVE_F1) &&) true
-       $(QUIET)$(CLEAN_DST) '$(DESTDIR_SQ)$(msgsdir_SQ)'
-       $(QUIET)$(foreach p,$(ALL_MSGFILES), $(REMOVE_F0)'$(DESTDIR_SQ)$(msgsdir_SQ)'/$(notdir $p) $(REMOVE_F1) &&) true
-       $(QUIET)$(REMOVE_D0)'$(DESTDIR_SQ)$(gitexecdir_SQ)' $(REMOVE_D1)
-       $(QUIET)$(REMOVE_D0)'$(DESTDIR_SQ)$(msgsdir_SQ)' $(REMOVE_D1)
-       $(QUIET)$(REMOVE_D0)'$(DESTDIR_SQ)$(libdir_SQ)' $(REMOVE_D1)
-       $(QUIET)$(REMOVE_D0)`dirname '$(DESTDIR_SQ)$(libdir_SQ)'` $(REMOVE_D1)
-
-dist-version:
-       @mkdir -p $(TARDIR)
-       @echo $(GITGUI_VERSION) > $(TARDIR)/version
-
-clean::
-       $(RM_RF) $(GITGUI_MAIN) lib/tclIndex po/*.msg
-       $(RM_RF) GIT-VERSION-FILE GIT-GUI-VARS
-ifdef GITGUI_MACOSXAPP
-       $(RM_RF) 'Git Gui.app'* git-gui
-endif
-ifdef GITGUI_WINDOWS_WRAPPER
-       $(RM_RF) git-gui
-endif
-
-.PHONY: all install uninstall dist-version clean
-.PHONY: .FORCE-GIT-VERSION-FILE
-.PHONY: .FORCE-GIT-GUI-VARS
+       $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(bindir_SQ)'
+       $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitexecdir_SQ)'
+       $(INSTALL) $(ALL_PROGRAMS) '$(DESTDIR_SQ)$(gitexecdir_SQ)'
+       $(INSTALL) git$X '$(DESTDIR_SQ)$(bindir_SQ)'
+       $(MAKE) -C templates DESTDIR='$(DESTDIR_SQ)' install
+       $(MAKE) -C perl prefix='$(prefix_SQ)' install
+ifndef NO_TCLTK
+       $(MAKE) -C gitk-git install
+       $(MAKE) -C git-gui install
+endif
+       if test 'z$(bindir_SQ)' != 'z$(gitexecdir_SQ)'; \
+       then \
+               ln -f '$(DESTDIR_SQ)$(bindir_SQ)/git$X' \
+                       '$(DESTDIR_SQ)$(gitexecdir_SQ)/git$X' || \
+               cp '$(DESTDIR_SQ)$(bindir_SQ)/git$X' \
+                       '$(DESTDIR_SQ)$(gitexecdir_SQ)/git$X'; \
+       fi
+       $(foreach p,$(BUILT_INS), $(RM) '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' && ln '$(DESTDIR_SQ)$(gitexecdir_SQ)/git$X' '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' ;)
+ifneq (,$X)
+       $(foreach p,$(patsubst %$X,%,$(filter %$X,$(ALL_PROGRAMS) $(BUILT_INS) git$X)), $(RM) '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p';)
+endif
+
+install-doc:
+       $(MAKE) -C Documentation install
+
+install-info:
+       $(MAKE) -C Documentation install-info
+
+quick-install-doc:
+       $(MAKE) -C Documentation quick-install
+
+
+
+### Maintainer's dist rules
+
+git.spec: git.spec.in
+       sed -e 's/@@VERSION@@/$(GIT_VERSION)/g' < $< > $@+
+       mv $@+ $@
+
+GIT_TARNAME=git-$(GIT_VERSION)
+dist: git.spec git-archive configure
+       ./git-archive --format=tar \
+               --prefix=$(GIT_TARNAME)/ HEAD^{tree} > $(GIT_TARNAME).tar
+       @mkdir -p $(GIT_TARNAME)
+       @cp git.spec configure $(GIT_TARNAME)
+       @echo $(GIT_VERSION) > $(GIT_TARNAME)/version
+       @$(MAKE) -C git-gui TARDIR=../$(GIT_TARNAME)/git-gui dist-version
+       $(TAR) rf $(GIT_TARNAME).tar \
+               $(GIT_TARNAME)/git.spec \
+               $(GIT_TARNAME)/configure \
+               $(GIT_TARNAME)/version \
+               $(GIT_TARNAME)/git-gui/version
+       @$(RM) -r $(GIT_TARNAME)
+       gzip -f -9 $(GIT_TARNAME).tar
+
+rpm: dist
+       $(RPMBUILD) -ta $(GIT_TARNAME).tar.gz
+
+htmldocs = git-htmldocs-$(GIT_VERSION)
+manpages = git-manpages-$(GIT_VERSION)
+dist-doc:
+       $(RM) -r .doc-tmp-dir
+       mkdir .doc-tmp-dir
+       $(MAKE) -C Documentation WEBDOC_DEST=../.doc-tmp-dir install-webdoc
+       cd .doc-tmp-dir && $(TAR) cf ../$(htmldocs).tar .
+       gzip -n -9 -f $(htmldocs).tar
+       :
+       $(RM) -r .doc-tmp-dir
+       mkdir -p .doc-tmp-dir/man1 .doc-tmp-dir/man5 .doc-tmp-dir/man7
+       $(MAKE) -C Documentation DESTDIR=./ \
+               man1dir=../.doc-tmp-dir/man1 \
+               man5dir=../.doc-tmp-dir/man5 \
+               man7dir=../.doc-tmp-dir/man7 \
+               install
+       cd .doc-tmp-dir && $(TAR) cf ../$(manpages).tar .
+       gzip -n -9 -f $(manpages).tar
+       $(RM) -r .doc-tmp-dir
+
+### Cleaning rules
+
+distclean: clean
+       $(RM) configure
+
+clean:
+       $(RM) *.o mozilla-sha1/*.o arm/*.o ppc/*.o compat/*.o xdiff/*.o \
+               $(LIB_FILE) $(XDIFF_LIB)
+       $(RM) $(ALL_PROGRAMS) $(BUILT_INS) git$X
+       $(RM) $(TEST_PROGRAMS)
+       $(RM) *.spec *.pyc *.pyo */*.pyc */*.pyo common-cmds.h TAGS tags cscope*
+       $(RM) -r autom4te.cache
+       $(RM) config.log config.mak.autogen config.mak.append config.status config.cache
+       $(RM) -r $(GIT_TARNAME) .doc-tmp-dir
+       $(RM) $(GIT_TARNAME).tar.gz git-core_$(GIT_VERSION)-*.tar.gz
+       $(RM) $(htmldocs).tar.gz $(manpages).tar.gz
+       $(RM) gitweb/gitweb.cgi
+       $(MAKE) -C Documentation/ clean
+       $(MAKE) -C perl clean
+       $(MAKE) -C templates/ clean
+       $(MAKE) -C t/ clean
+ifndef NO_TCLTK
+       $(MAKE) -C gitk-git clean
+       $(MAKE) -C git-gui clean
+endif
+       $(RM) GIT-VERSION-FILE GIT-CFLAGS GIT-GUI-VARS
+
+.PHONY: all install clean strip
+.PHONY: .FORCE-GIT-VERSION-FILE TAGS tags cscope .FORCE-GIT-CFLAGS
+
+### Check documentation
+#
+check-docs::
+       @(for v in $(ALL_PROGRAMS) $(BUILT_INS) git gitk; \
+       do \
+               case "$$v" in \
+               git-merge-octopus | git-merge-ours | git-merge-recursive | \
+               git-merge-resolve | git-merge-stupid | git-merge-subtree | \
+               git-add--interactive | git-fsck-objects | git-init-db | \
+               git-rebase--interactive | \
+               git-repo-config | git-fetch--tool ) continue ;; \
+               esac ; \
+               test -f "Documentation/$$v.txt" || \
+               echo "no doc: $$v"; \
+               sed -e '/^#/d' command-list.txt | \
+               grep -q "^$$v[  ]" || \
+               case "$$v" in \
+               git) ;; \
+               *) echo "no link: $$v";; \
+               esac ; \
+       done; \
+       ( \
+               sed -e '/^#/d' \
+                   -e 's/[     ].*//' \
+                   -e 's/^/listed /' command-list.txt; \
+               ls -1 Documentation/git*txt | \
+               sed -e 's|Documentation/|documented |' \
+                   -e 's/\.txt//'; \
+       ) | while read how cmd; \
+       do \
+               case "$$how,$$cmd" in \
+               *,git-citool | \
+               *,git-gui | \
+               *,git-help | \
+               documented,gitattributes | \
+               documented,gitignore | \
+               documented,gitmodules | \
+               documented,git-tools | \
+               sentinel,not,matching,is,ok ) continue ;; \
+               esac; \
+               case " $(ALL_PROGRAMS) $(BUILT_INS) git gitk " in \
+               *" $$cmd "*)    ;; \
+               *) echo "removed but $$how: $$cmd" ;; \
+               esac; \
+       done ) | sort
+
+### Make sure built-ins do not have dups and listed in git.c
+#
+check-builtins::
+       ./check-builtins.sh
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..548142c
--- /dev/null
+++ b/README
@@ -0,0 +1,46 @@
+////////////////////////////////////////////////////////////////
+
+       GIT - the stupid content tracker
+
+////////////////////////////////////////////////////////////////
+
+"git" can mean anything, depending on your mood.
+
+ - random three-letter combination that is pronounceable, and not
+   actually used by any common UNIX command.  The fact that it is a
+   mispronunciation of "get" may or may not be relevant.
+ - stupid. contemptible and despicable. simple. Take your pick from the
+   dictionary of slang.
+ - "global information tracker": you're in a good mood, and it actually
+   works for you. Angels sing, and a light suddenly fills the room.
+ - "goddamn idiotic truckload of sh*t": when it breaks
+
+Git is a fast, scalable, distributed revision control system with an
+unusually rich command set that provides both high-level operations
+and full access to internals.
+
+Git is an Open Source project covered by the GNU General Public License.
+It was originally written by Linus Torvalds with help of a group of
+hackers around the net. It is currently maintained by Junio C Hamano.
+
+Please read the file INSTALL for installation instructions.
+See Documentation/tutorial.txt to get started, then see
+Documentation/everyday.txt for a useful minimum set of commands,
+and "man git-commandname" for documentation of each command.
+CVS users may also want to read Documentation/cvs-migration.txt.
+
+Many Git online resources are accessible from http://git.or.cz/
+including full documentation and Git related tools.
+
+The user discussion and development of Git take place on the Git
+mailing list -- everyone is welcome to post bug reports, feature
+requests, comments and patches to git@vger.kernel.org. To subscribe
+to the list, send an email with just "subscribe git" in the body to
+majordomo@vger.kernel.org. The mailing list archives are available at
+http://marc.theaimsgroup.com/?l=git and other archival sites.
+
+The messages titled "A note from the maintainer", "What's in
+git.git (stable)" and "What's cooking in git.git (topics)" and
+the discussion following them on the mailing list give a good
+reference for project status, development direction and
+remaining tasks.
diff --git a/RelNotes b/RelNotes
new file mode 120000 (symlink)
index 0000000..46308ce
--- /dev/null
+++ b/RelNotes
@@ -0,0 +1 @@
+Documentation/RelNotes-1.5.4.txt
\ No newline at end of file
diff --git a/alloc.c b/alloc.c
new file mode 100644 (file)
index 0000000..216c23a
--- /dev/null
+++ b/alloc.c
@@ -0,0 +1,76 @@
+/*
+ * alloc.c  - specialized allocator for internal objects
+ *
+ * Copyright (C) 2006 Linus Torvalds
+ *
+ * The standard malloc/free wastes too much space for objects, partly because
+ * it maintains all the allocation infrastructure (which isn't needed, since
+ * we never free an object descriptor anyway), but even more because it ends
+ * up with maximal alignment because it doesn't know what the object alignment
+ * for the new allocation is.
+ */
+#include "cache.h"
+#include "object.h"
+#include "blob.h"
+#include "tree.h"
+#include "commit.h"
+#include "tag.h"
+
+#define BLOCKING 1024
+
+#define DEFINE_ALLOCATOR(name, type)                           \
+static unsigned int name##_allocs;                             \
+void *alloc_##name##_node(void)                                        \
+{                                                              \
+       static int nr;                                          \
+       static type *block;                                     \
+       void *ret;                                              \
+                                                               \
+       if (!nr) {                                              \
+               nr = BLOCKING;                                  \
+               block = xmalloc(BLOCKING * sizeof(type));       \
+       }                                                       \
+       nr--;                                                   \
+       name##_allocs++;                                        \
+       ret = block++;                                          \
+       memset(ret, 0, sizeof(type));                           \
+       return ret;                                             \
+}
+
+union any_object {
+       struct object object;
+       struct blob blob;
+       struct tree tree;
+       struct commit commit;
+       struct tag tag;
+};
+
+DEFINE_ALLOCATOR(blob, struct blob)
+DEFINE_ALLOCATOR(tree, struct tree)
+DEFINE_ALLOCATOR(commit, struct commit)
+DEFINE_ALLOCATOR(tag, struct tag)
+DEFINE_ALLOCATOR(object, union any_object)
+
+#ifdef NO_C99_FORMAT
+#define SZ_FMT "%u"
+#else
+#define SZ_FMT "%zu"
+#endif
+
+static void report(const char* name, unsigned int count, size_t size)
+{
+    fprintf(stderr, "%10s: %8u (" SZ_FMT " kB)\n", name, count, size);
+}
+
+#undef SZ_FMT
+
+#define REPORT(name)   \
+    report(#name, name##_allocs, name##_allocs*sizeof(struct name) >> 10)
+
+void alloc_report(void)
+{
+       REPORT(blob);
+       REPORT(tree);
+       REPORT(commit);
+       REPORT(tag);
+}
diff --git a/archive-tar.c b/archive-tar.c
new file mode 100644 (file)
index 0000000..e1bced5
--- /dev/null
@@ -0,0 +1,293 @@
+/*
+ * Copyright (c) 2005, 2006 Rene Scharfe
+ */
+#include "cache.h"
+#include "commit.h"
+#include "tar.h"
+#include "builtin.h"
+#include "archive.h"
+
+#define RECORDSIZE     (512)
+#define BLOCKSIZE      (RECORDSIZE * 20)
+
+static char block[BLOCKSIZE];
+static unsigned long offset;
+
+static time_t archive_time;
+static int tar_umask = 002;
+static int verbose;
+static const struct commit *commit;
+
+/* writes out the whole block, but only if it is full */
+static void write_if_needed(void)
+{
+       if (offset == BLOCKSIZE) {
+               write_or_die(1, block, BLOCKSIZE);
+               offset = 0;
+       }
+}
+
+/*
+ * queues up writes, so that all our write(2) calls write exactly one
+ * full block; pads writes to RECORDSIZE
+ */
+static void write_blocked(const void *data, unsigned long size)
+{
+       const char *buf = data;
+       unsigned long tail;
+
+       if (offset) {
+               unsigned long chunk = BLOCKSIZE - offset;
+               if (size < chunk)
+                       chunk = size;
+               memcpy(block + offset, buf, chunk);
+               size -= chunk;
+               offset += chunk;
+               buf += chunk;
+               write_if_needed();
+       }
+       while (size >= BLOCKSIZE) {
+               write_or_die(1, buf, BLOCKSIZE);
+               size -= BLOCKSIZE;
+               buf += BLOCKSIZE;
+       }
+       if (size) {
+               memcpy(block + offset, buf, size);
+               offset += size;
+       }
+       tail = offset % RECORDSIZE;
+       if (tail)  {
+               memset(block + offset, 0, RECORDSIZE - tail);
+               offset += RECORDSIZE - tail;
+       }
+       write_if_needed();
+}
+
+/*
+ * The end of tar archives is marked by 2*512 nul bytes and after that
+ * follows the rest of the block (if any).
+ */
+static void write_trailer(void)
+{
+       int tail = BLOCKSIZE - offset;
+       memset(block + offset, 0, tail);
+       write_or_die(1, block, BLOCKSIZE);
+       if (tail < 2 * RECORDSIZE) {
+               memset(block, 0, offset);
+               write_or_die(1, block, BLOCKSIZE);
+       }
+}
+
+/*
+ * pax extended header records have the format "%u %s=%s\n".  %u contains
+ * the size of the whole string (including the %u), the first %s is the
+ * keyword, the second one is the value.  This function constructs such a
+ * string and appends it to a struct strbuf.
+ */
+static void strbuf_append_ext_header(struct strbuf *sb, const char *keyword,
+                                     const char *value, unsigned int valuelen)
+{
+       int len, tmp;
+
+       /* "%u %s=%s\n" */
+       len = 1 + 1 + strlen(keyword) + 1 + valuelen + 1;
+       for (tmp = len; tmp > 9; tmp /= 10)
+               len++;
+
+       strbuf_grow(sb, len);
+       strbuf_addf(sb, "%u %s=", len, keyword);
+       strbuf_add(sb, value, valuelen);
+       strbuf_addch(sb, '\n');
+}
+
+static unsigned int ustar_header_chksum(const struct ustar_header *header)
+{
+       char *p = (char *)header;
+       unsigned int chksum = 0;
+       while (p < header->chksum)
+               chksum += *p++;
+       chksum += sizeof(header->chksum) * ' ';
+       p += sizeof(header->chksum);
+       while (p < (char *)header + sizeof(struct ustar_header))
+               chksum += *p++;
+       return chksum;
+}
+
+static int get_path_prefix(const struct strbuf *path, int maxlen)
+{
+       int i = path->len;
+       if (i > maxlen)
+               i = maxlen;
+       do {
+               i--;
+       } while (i > 0 && path->buf[i] != '/');
+       return i;
+}
+
+static void write_entry(const unsigned char *sha1, struct strbuf *path,
+                        unsigned int mode, void *buffer, unsigned long size)
+{
+       struct ustar_header header;
+       struct strbuf ext_header;
+
+       memset(&header, 0, sizeof(header));
+       strbuf_init(&ext_header, 0);
+
+       if (!sha1) {
+               *header.typeflag = TYPEFLAG_GLOBAL_HEADER;
+               mode = 0100666;
+               strcpy(header.name, "pax_global_header");
+       } else if (!path) {
+               *header.typeflag = TYPEFLAG_EXT_HEADER;
+               mode = 0100666;
+               sprintf(header.name, "%s.paxheader", sha1_to_hex(sha1));
+       } else {
+               if (verbose)
+                       fprintf(stderr, "%.*s\n", (int)path->len, path->buf);
+               if (S_ISDIR(mode) || S_ISGITLINK(mode)) {
+                       *header.typeflag = TYPEFLAG_DIR;
+                       mode = (mode | 0777) & ~tar_umask;
+               } else if (S_ISLNK(mode)) {
+                       *header.typeflag = TYPEFLAG_LNK;
+                       mode |= 0777;
+               } else if (S_ISREG(mode)) {
+                       *header.typeflag = TYPEFLAG_REG;
+                       mode = (mode | ((mode & 0100) ? 0777 : 0666)) & ~tar_umask;
+               } else {
+                       error("unsupported file mode: 0%o (SHA1: %s)",
+                             mode, sha1_to_hex(sha1));
+                       return;
+               }
+               if (path->len > sizeof(header.name)) {
+                       int plen = get_path_prefix(path, sizeof(header.prefix));
+                       int rest = path->len - plen - 1;
+                       if (plen > 0 && rest <= sizeof(header.name)) {
+                               memcpy(header.prefix, path->buf, plen);
+                               memcpy(header.name, path->buf + plen + 1, rest);
+                       } else {
+                               sprintf(header.name, "%s.data",
+                                       sha1_to_hex(sha1));
+                               strbuf_append_ext_header(&ext_header, "path",
+                                                        path->buf, path->len);
+                       }
+               } else
+                       memcpy(header.name, path->buf, path->len);
+       }
+
+       if (S_ISLNK(mode) && buffer) {
+               if (size > sizeof(header.linkname)) {
+                       sprintf(header.linkname, "see %s.paxheader",
+                               sha1_to_hex(sha1));
+                       strbuf_append_ext_header(&ext_header, "linkpath",
+                                                buffer, size);
+               } else
+                       memcpy(header.linkname, buffer, size);
+       }
+
+       sprintf(header.mode, "%07o", mode & 07777);
+       sprintf(header.size, "%011lo", S_ISREG(mode) ? size : 0);
+       sprintf(header.mtime, "%011lo", archive_time);
+
+       sprintf(header.uid, "%07o", 0);
+       sprintf(header.gid, "%07o", 0);
+       strlcpy(header.uname, "root", sizeof(header.uname));
+       strlcpy(header.gname, "root", sizeof(header.gname));
+       sprintf(header.devmajor, "%07o", 0);
+       sprintf(header.devminor, "%07o", 0);
+
+       memcpy(header.magic, "ustar", 6);
+       memcpy(header.version, "00", 2);
+
+       sprintf(header.chksum, "%07o", ustar_header_chksum(&header));
+
+       if (ext_header.len > 0) {
+               write_entry(sha1, NULL, 0, ext_header.buf, ext_header.len);
+       }
+       strbuf_release(&ext_header);
+       write_blocked(&header, sizeof(header));
+       if (S_ISREG(mode) && buffer && size > 0)
+               write_blocked(buffer, size);
+}
+
+static void write_global_extended_header(const unsigned char *sha1)
+{
+       struct strbuf ext_header;
+
+       strbuf_init(&ext_header, 0);
+       strbuf_append_ext_header(&ext_header, "comment", sha1_to_hex(sha1), 40);
+       write_entry(NULL, NULL, 0, ext_header.buf, ext_header.len);
+       strbuf_release(&ext_header);
+}
+
+static int git_tar_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "tar.umask")) {
+               if (!strcmp(value, "user")) {
+                       tar_umask = umask(0);
+                       umask(tar_umask);
+               } else {
+                       tar_umask = git_config_int(var, value);
+               }
+               return 0;
+       }
+       return git_default_config(var, value);
+}
+
+static int write_tar_entry(const unsigned char *sha1,
+                           const char *base, int baselen,
+                           const char *filename, unsigned mode, int stage)
+{
+       static struct strbuf path = STRBUF_INIT;
+       void *buffer;
+       enum object_type type;
+       unsigned long size;
+
+       strbuf_reset(&path);
+       strbuf_grow(&path, PATH_MAX);
+       strbuf_add(&path, base, baselen);
+       strbuf_addstr(&path, filename);
+       if (S_ISDIR(mode) || S_ISGITLINK(mode)) {
+               strbuf_addch(&path, '/');
+               buffer = NULL;
+               size = 0;
+       } else {
+               buffer = sha1_file_to_archive(path.buf, sha1, mode, &type,
+                                             &size, commit);
+               if (!buffer)
+                       die("cannot read %s", sha1_to_hex(sha1));
+       }
+
+       write_entry(sha1, &path, mode, buffer, size);
+       free(buffer);
+
+       return READ_TREE_RECURSIVE;
+}
+
+int write_tar_archive(struct archiver_args *args)
+{
+       int plen = args->base ? strlen(args->base) : 0;
+
+       git_config(git_tar_config);
+
+       archive_time = args->time;
+       verbose = args->verbose;
+       commit = args->commit;
+
+       if (args->commit_sha1)
+               write_global_extended_header(args->commit_sha1);
+
+       if (args->base && plen > 0 && args->base[plen - 1] == '/') {
+               char *base = xstrdup(args->base);
+               int baselen = strlen(base);
+
+               while (baselen > 0 && base[baselen - 1] == '/')
+                       base[--baselen] = '\0';
+               write_tar_entry(args->tree->object.sha1, "", 0, base, 040777, 0);
+               free(base);
+       }
+       read_tree_recursive(args->tree, args->base, plen, 0,
+                           args->pathspec, write_tar_entry);
+       write_trailer();
+
+       return 0;
+}
diff --git a/archive-zip.c b/archive-zip.c
new file mode 100644 (file)
index 0000000..74e30f6
--- /dev/null
@@ -0,0 +1,354 @@
+/*
+ * Copyright (c) 2006 Rene Scharfe
+ */
+#include "cache.h"
+#include "commit.h"
+#include "blob.h"
+#include "tree.h"
+#include "quote.h"
+#include "builtin.h"
+#include "archive.h"
+
+static int verbose;
+static int zip_date;
+static int zip_time;
+static const struct commit *commit;
+
+static unsigned char *zip_dir;
+static unsigned int zip_dir_size;
+
+static unsigned int zip_offset;
+static unsigned int zip_dir_offset;
+static unsigned int zip_dir_entries;
+
+#define ZIP_DIRECTORY_MIN_SIZE (1024 * 1024)
+
+struct zip_local_header {
+       unsigned char magic[4];
+       unsigned char version[2];
+       unsigned char flags[2];
+       unsigned char compression_method[2];
+       unsigned char mtime[2];
+       unsigned char mdate[2];
+       unsigned char crc32[4];
+       unsigned char compressed_size[4];
+       unsigned char size[4];
+       unsigned char filename_length[2];
+       unsigned char extra_length[2];
+       unsigned char _end[1];
+};
+
+struct zip_dir_header {
+       unsigned char magic[4];
+       unsigned char creator_version[2];
+       unsigned char version[2];
+       unsigned char flags[2];
+       unsigned char compression_method[2];
+       unsigned char mtime[2];
+       unsigned char mdate[2];
+       unsigned char crc32[4];
+       unsigned char compressed_size[4];
+       unsigned char size[4];
+       unsigned char filename_length[2];
+       unsigned char extra_length[2];
+       unsigned char comment_length[2];
+       unsigned char disk[2];
+       unsigned char attr1[2];
+       unsigned char attr2[4];
+       unsigned char offset[4];
+       unsigned char _end[1];
+};
+
+struct zip_dir_trailer {
+       unsigned char magic[4];
+       unsigned char disk[2];
+       unsigned char directory_start_disk[2];
+       unsigned char entries_on_this_disk[2];
+       unsigned char entries[2];
+       unsigned char size[4];
+       unsigned char offset[4];
+       unsigned char comment_length[2];
+       unsigned char _end[1];
+};
+
+/*
+ * On ARM, padding is added at the end of the struct, so a simple
+ * sizeof(struct ...) reports two bytes more than the payload size
+ * we're interested in.
+ */
+#define ZIP_LOCAL_HEADER_SIZE  offsetof(struct zip_local_header, _end)
+#define ZIP_DIR_HEADER_SIZE    offsetof(struct zip_dir_header, _end)
+#define ZIP_DIR_TRAILER_SIZE   offsetof(struct zip_dir_trailer, _end)
+
+static void copy_le16(unsigned char *dest, unsigned int n)
+{
+       dest[0] = 0xff & n;
+       dest[1] = 0xff & (n >> 010);
+}
+
+static void copy_le32(unsigned char *dest, unsigned int n)
+{
+       dest[0] = 0xff & n;
+       dest[1] = 0xff & (n >> 010);
+       dest[2] = 0xff & (n >> 020);
+       dest[3] = 0xff & (n >> 030);
+}
+
+static void *zlib_deflate(void *data, unsigned long size,
+                          unsigned long *compressed_size)
+{
+       z_stream stream;
+       unsigned long maxsize;
+       void *buffer;
+       int result;
+
+       memset(&stream, 0, sizeof(stream));
+       deflateInit(&stream, zlib_compression_level);
+       maxsize = deflateBound(&stream, size);
+       buffer = xmalloc(maxsize);
+
+       stream.next_in = data;
+       stream.avail_in = size;
+       stream.next_out = buffer;
+       stream.avail_out = maxsize;
+
+       do {
+               result = deflate(&stream, Z_FINISH);
+       } while (result == Z_OK);
+
+       if (result != Z_STREAM_END) {
+               free(buffer);
+               return NULL;
+       }
+
+       deflateEnd(&stream);
+       *compressed_size = stream.total_out;
+
+       return buffer;
+}
+
+static char *construct_path(const char *base, int baselen,
+                            const char *filename, int isdir, int *pathlen)
+{
+       int filenamelen = strlen(filename);
+       int len = baselen + filenamelen;
+       char *path, *p;
+
+       if (isdir)
+               len++;
+       p = path = xmalloc(len + 1);
+
+       memcpy(p, base, baselen);
+       p += baselen;
+       memcpy(p, filename, filenamelen);
+       p += filenamelen;
+       if (isdir)
+               *p++ = '/';
+       *p = '\0';
+
+       *pathlen = len;
+
+       return path;
+}
+
+static int write_zip_entry(const unsigned char *sha1,
+                           const char *base, int baselen,
+                           const char *filename, unsigned mode, int stage)
+{
+       struct zip_local_header header;
+       struct zip_dir_header dirent;
+       unsigned long attr2;
+       unsigned long compressed_size;
+       unsigned long uncompressed_size;
+       unsigned long crc;
+       unsigned long direntsize;
+       unsigned long size;
+       int method;
+       int result = -1;
+       int pathlen;
+       unsigned char *out;
+       char *path;
+       enum object_type type;
+       void *buffer = NULL;
+       void *deflated = NULL;
+
+       crc = crc32(0, NULL, 0);
+
+       path = construct_path(base, baselen, filename, S_ISDIR(mode), &pathlen);
+       if (verbose)
+               fprintf(stderr, "%s\n", path);
+       if (pathlen > 0xffff) {
+               error("path too long (%d chars, SHA1: %s): %s", pathlen,
+                     sha1_to_hex(sha1), path);
+               goto out;
+       }
+
+       if (S_ISDIR(mode) || S_ISGITLINK(mode)) {
+               method = 0;
+               attr2 = 16;
+               result = (S_ISDIR(mode) ? READ_TREE_RECURSIVE : 0);
+               out = NULL;
+               uncompressed_size = 0;
+               compressed_size = 0;
+       } else if (S_ISREG(mode) || S_ISLNK(mode)) {
+               method = 0;
+               attr2 = S_ISLNK(mode) ? ((mode | 0777) << 16) :
+                       (mode & 0111) ? ((mode) << 16) : 0;
+               if (S_ISREG(mode) && zlib_compression_level != 0)
+                       method = 8;
+               result = 0;
+               buffer = sha1_file_to_archive(path, sha1, mode, &type, &size,
+                                             commit);
+               if (!buffer)
+                       die("cannot read %s", sha1_to_hex(sha1));
+               crc = crc32(crc, buffer, size);
+               out = buffer;
+               uncompressed_size = size;
+               compressed_size = size;
+       } else {
+               error("unsupported file mode: 0%o (SHA1: %s)", mode,
+                     sha1_to_hex(sha1));
+               goto out;
+       }
+
+       if (method == 8) {
+               deflated = zlib_deflate(buffer, size, &compressed_size);
+               if (deflated && compressed_size - 6 < size) {
+                       /* ZLIB --> raw compressed data (see RFC 1950) */
+                       /* CMF and FLG ... */
+                       out = (unsigned char *)deflated + 2;
+                       compressed_size -= 6;   /* ... and ADLER32 */
+               } else {
+                       method = 0;
+                       compressed_size = size;
+               }
+       }
+
+       /* make sure we have enough free space in the dictionary */
+       direntsize = ZIP_DIR_HEADER_SIZE + pathlen;
+       while (zip_dir_size < zip_dir_offset + direntsize) {
+               zip_dir_size += ZIP_DIRECTORY_MIN_SIZE;
+               zip_dir = xrealloc(zip_dir, zip_dir_size);
+       }
+
+       copy_le32(dirent.magic, 0x02014b50);
+       copy_le16(dirent.creator_version,
+               S_ISLNK(mode) || (S_ISREG(mode) && (mode & 0111)) ? 0x0317 : 0);
+       copy_le16(dirent.version, 10);
+       copy_le16(dirent.flags, 0);
+       copy_le16(dirent.compression_method, method);
+       copy_le16(dirent.mtime, zip_time);
+       copy_le16(dirent.mdate, zip_date);
+       copy_le32(dirent.crc32, crc);
+       copy_le32(dirent.compressed_size, compressed_size);
+       copy_le32(dirent.size, uncompressed_size);
+       copy_le16(dirent.filename_length, pathlen);
+       copy_le16(dirent.extra_length, 0);
+       copy_le16(dirent.comment_length, 0);
+       copy_le16(dirent.disk, 0);
+       copy_le16(dirent.attr1, 0);
+       copy_le32(dirent.attr2, attr2);
+       copy_le32(dirent.offset, zip_offset);
+       memcpy(zip_dir + zip_dir_offset, &dirent, ZIP_DIR_HEADER_SIZE);
+       zip_dir_offset += ZIP_DIR_HEADER_SIZE;
+       memcpy(zip_dir + zip_dir_offset, path, pathlen);
+       zip_dir_offset += pathlen;
+       zip_dir_entries++;
+
+       copy_le32(header.magic, 0x04034b50);
+       copy_le16(header.version, 10);
+       copy_le16(header.flags, 0);
+       copy_le16(header.compression_method, method);
+       copy_le16(header.mtime, zip_time);
+       copy_le16(header.mdate, zip_date);
+       copy_le32(header.crc32, crc);
+       copy_le32(header.compressed_size, compressed_size);
+       copy_le32(header.size, uncompressed_size);
+       copy_le16(header.filename_length, pathlen);
+       copy_le16(header.extra_length, 0);
+       write_or_die(1, &header, ZIP_LOCAL_HEADER_SIZE);
+       zip_offset += ZIP_LOCAL_HEADER_SIZE;
+       write_or_die(1, path, pathlen);
+       zip_offset += pathlen;
+       if (compressed_size > 0) {
+               write_or_die(1, out, compressed_size);
+               zip_offset += compressed_size;
+       }
+
+out:
+       free(buffer);
+       free(deflated);
+       free(path);
+
+       return result;
+}
+
+static void write_zip_trailer(const unsigned char *sha1)
+{
+       struct zip_dir_trailer trailer;
+
+       copy_le32(trailer.magic, 0x06054b50);
+       copy_le16(trailer.disk, 0);
+       copy_le16(trailer.directory_start_disk, 0);
+       copy_le16(trailer.entries_on_this_disk, zip_dir_entries);
+       copy_le16(trailer.entries, zip_dir_entries);
+       copy_le32(trailer.size, zip_dir_offset);
+       copy_le32(trailer.offset, zip_offset);
+       copy_le16(trailer.comment_length, sha1 ? 40 : 0);
+
+       write_or_die(1, zip_dir, zip_dir_offset);
+       write_or_die(1, &trailer, ZIP_DIR_TRAILER_SIZE);
+       if (sha1)
+               write_or_die(1, sha1_to_hex(sha1), 40);
+}
+
+static void dos_time(time_t *time, int *dos_date, int *dos_time)
+{
+       struct tm *t = localtime(time);
+
+       *dos_date = t->tm_mday + (t->tm_mon + 1) * 32 +
+                   (t->tm_year + 1900 - 1980) * 512;
+       *dos_time = t->tm_sec / 2 + t->tm_min * 32 + t->tm_hour * 2048;
+}
+
+int write_zip_archive(struct archiver_args *args)
+{
+       int plen = strlen(args->base);
+
+       dos_time(&args->time, &zip_date, &zip_time);
+
+       zip_dir = xmalloc(ZIP_DIRECTORY_MIN_SIZE);
+       zip_dir_size = ZIP_DIRECTORY_MIN_SIZE;
+       verbose = args->verbose;
+       commit = args->commit;
+
+       if (args->base && plen > 0 && args->base[plen - 1] == '/') {
+               char *base = xstrdup(args->base);
+               int baselen = strlen(base);
+
+               while (baselen > 0 && base[baselen - 1] == '/')
+                       base[--baselen] = '\0';
+               write_zip_entry(args->tree->object.sha1, "", 0, base, 040777, 0);
+               free(base);
+       }
+       read_tree_recursive(args->tree, args->base, plen, 0,
+                           args->pathspec, write_zip_entry);
+       write_zip_trailer(args->commit_sha1);
+
+       free(zip_dir);
+
+       return 0;
+}
+
+void *parse_extra_zip_args(int argc, const char **argv)
+{
+       for (; argc > 0; argc--, argv++) {
+               const char *arg = argv[0];
+
+               if (arg[0] == '-' && isdigit(arg[1]) && arg[2] == '\0')
+                       zlib_compression_level = arg[1] - '0';
+               else
+                       die("Unknown argument for zip format: %s", arg);
+       }
+       return NULL;
+}
diff --git a/archive.h b/archive.h
new file mode 100644 (file)
index 0000000..5791e65
--- /dev/null
+++ b/archive.h
@@ -0,0 +1,48 @@
+#ifndef ARCHIVE_H
+#define ARCHIVE_H
+
+#define MAX_EXTRA_ARGS 32
+#define MAX_ARGS       (MAX_EXTRA_ARGS + 32)
+
+struct archiver_args {
+       const char *base;
+       struct tree *tree;
+       const unsigned char *commit_sha1;
+       const struct commit *commit;
+       time_t time;
+       const char **pathspec;
+       unsigned int verbose : 1;
+       void *extra;
+};
+
+typedef int (*write_archive_fn_t)(struct archiver_args *);
+
+typedef void *(*parse_extra_args_fn_t)(int argc, const char **argv);
+
+struct archiver {
+       const char *name;
+       struct archiver_args args;
+       write_archive_fn_t write_archive;
+       parse_extra_args_fn_t parse_extra;
+};
+
+extern int parse_archive_args(int argc,
+                             const char **argv,
+                             struct archiver *ar);
+
+extern void parse_treeish_arg(const char **treeish,
+                             struct archiver_args *ar_args,
+                             const char *prefix);
+
+extern void parse_pathspec_arg(const char **pathspec,
+                              struct archiver_args *args);
+/*
+ * Archive-format specific backends.
+ */
+extern int write_tar_archive(struct archiver_args *);
+extern int write_zip_archive(struct archiver_args *);
+extern void *parse_extra_zip_args(int argc, const char **argv);
+
+extern void *sha1_file_to_archive(const char *path, const unsigned char *sha1, unsigned int mode, enum object_type *type, unsigned long *size, const struct commit *commit);
+
+#endif /* ARCHIVE_H */
diff --git a/arm/sha1.c b/arm/sha1.c
new file mode 100644 (file)
index 0000000..9e3ae03
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SHA-1 implementation optimized for ARM
+ *
+ * Copyright:   (C) 2005 by Nicolas Pitre <nico@cam.org>
+ * Created:     September 17, 2005
+ */
+
+#include <string.h>
+#include "sha1.h"
+
+extern void sha_transform(uint32_t *hash, const unsigned char *data, uint32_t *W);
+
+void SHA1_Init(SHA_CTX *c)
+{
+       c->len = 0;
+       c->hash[0] = 0x67452301;
+       c->hash[1] = 0xefcdab89;
+       c->hash[2] = 0x98badcfe;
+       c->hash[3] = 0x10325476;
+       c->hash[4] = 0xc3d2e1f0;
+}
+
+void SHA1_Update(SHA_CTX *c, const void *p, unsigned long n)
+{
+       uint32_t workspace[80];
+       unsigned int partial;
+       unsigned long done;
+
+       partial = c->len & 0x3f;
+       c->len += n;
+       if ((partial + n) >= 64) {
+               if (partial) {
+                       done = 64 - partial;
+                       memcpy(c->buffer + partial, p, done);
+                       sha_transform(c->hash, c->buffer, workspace);
+                       partial = 0;
+               } else
+                       done = 0;
+               while (n >= done + 64) {
+                       sha_transform(c->hash, p + done, workspace);
+                       done += 64;
+               }
+       } else
+               done = 0;
+       if (n - done)
+               memcpy(c->buffer + partial, p + done, n - done);
+}
+
+void SHA1_Final(unsigned char *hash, SHA_CTX *c)
+{
+       uint64_t bitlen;
+       uint32_t bitlen_hi, bitlen_lo;
+       unsigned int i, offset, padlen;
+       unsigned char bits[8];
+       static const unsigned char padding[64] = { 0x80, };
+
+       bitlen = c->len << 3;
+       offset = c->len & 0x3f;
+       padlen = ((offset < 56) ? 56 : (64 + 56)) - offset;
+       SHA1_Update(c, padding, padlen);
+
+       bitlen_hi = bitlen >> 32;
+       bitlen_lo = bitlen & 0xffffffff;
+       bits[0] = bitlen_hi >> 24;
+       bits[1] = bitlen_hi >> 16;
+       bits[2] = bitlen_hi >> 8;
+       bits[3] = bitlen_hi;
+       bits[4] = bitlen_lo >> 24;
+       bits[5] = bitlen_lo >> 16;
+       bits[6] = bitlen_lo >> 8;
+       bits[7] = bitlen_lo;
+       SHA1_Update(c, bits, 8);
+
+       for (i = 0; i < 5; i++) {
+               uint32_t v = c->hash[i];
+               hash[0] = v >> 24;
+               hash[1] = v >> 16;
+               hash[2] = v >> 8;
+               hash[3] = v;
+               hash += 4;
+       }
+}
diff --git a/arm/sha1.h b/arm/sha1.h
new file mode 100644 (file)
index 0000000..3952646
--- /dev/null
@@ -0,0 +1,18 @@
+/*
+ * SHA-1 implementation optimized for ARM
+ *
+ * Copyright:  (C) 2005 by Nicolas Pitre <nico@cam.org>
+ * Created:    September 17, 2005
+ */
+
+#include <stdint.h>
+
+typedef struct sha_context {
+       uint64_t len;
+       uint32_t hash[5];
+       unsigned char buffer[64];
+} SHA_CTX;
+
+void SHA1_Init(SHA_CTX *c);
+void SHA1_Update(SHA_CTX *c, const void *p, unsigned long n);
+void SHA1_Final(unsigned char *hash, SHA_CTX *c);
diff --git a/arm/sha1_arm.S b/arm/sha1_arm.S
new file mode 100644 (file)
index 0000000..8c1cb99
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ *  SHA transform optimized for ARM
+ *
+ *  Copyright: (C) 2005 by Nicolas Pitre <nico@cam.org>
+ *  Created:   September 17, 2005
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2 as
+ *  published by the Free Software Foundation.
+ */
+
+       .text
+       .globl  sha_transform
+
+/*
+ * void sha_transform(uint32_t *hash, const unsigned char *data, uint32_t *W);
+ *
+ * note: the "data" pointer may be unaligned.
+ */
+
+sha_transform:
+
+       stmfd   sp!, {r4 - r8, lr}
+
+       @ for (i = 0; i < 16; i++)
+       @         W[i] = ntohl(((uint32_t *)data)[i]);
+
+#ifdef __ARMEB__
+       mov     r4, r0
+       mov     r0, r2
+       mov     r2, #64
+       bl      memcpy
+       mov     r2, r0
+       mov     r0, r4
+#else
+       mov     r3, r2
+       mov     lr, #16
+1:     ldrb    r4, [r1], #1
+       ldrb    r5, [r1], #1
+       ldrb    r6, [r1], #1
+       ldrb    r7, [r1], #1
+       subs    lr, lr, #1
+       orr     r5, r5, r4, lsl #8
+       orr     r6, r6, r5, lsl #8
+       orr     r7, r7, r6, lsl #8
+       str     r7, [r3], #4
+       bne     1b
+#endif
+
+       @ for (i = 0; i < 64; i++)
+       @         W[i+16] = ror(W[i+13] ^ W[i+8] ^ W[i+2] ^ W[i], 31);
+
+       sub     r3, r2, #4
+       mov     lr, #64
+2:     ldr     r4, [r3, #4]!
+       subs    lr, lr, #1
+       ldr     r5, [r3, #8]
+       ldr     r6, [r3, #32]
+       ldr     r7, [r3, #52]
+       eor     r4, r4, r5
+       eor     r4, r4, r6
+       eor     r4, r4, r7
+       mov     r4, r4, ror #31
+       str     r4, [r3, #64]
+       bne     2b
+
+       /*
+        * The SHA functions are:
+        *
+        * f1(B,C,D) = (D ^ (B & (C ^ D)))
+        * f2(B,C,D) = (B ^ C ^ D)
+        * f3(B,C,D) = ((B & C) | (D & (B | C)))
+        *
+        * Then the sub-blocks are processed as follows:
+        *
+        * A' = ror(A, 27) + f(B,C,D) + E + K + *W++
+        * B' = A
+        * C' = ror(B, 2)
+        * D' = C
+        * E' = D
+        *
+        * We therefore unroll each loop 5 times to avoid register shuffling.
+        * Also the ror for C (and also D and E which are successivelyderived
+        * from it) is applied in place to cut on an additional mov insn for
+        * each round.
+        */
+
+       .macro  sha_f1, A, B, C, D, E
+       ldr     r3, [r2], #4
+       eor     ip, \C, \D
+       add     \E, r1, \E, ror #2
+       and     ip, \B, ip, ror #2
+       add     \E, \E, \A, ror #27
+       eor     ip, ip, \D, ror #2
+       add     \E, \E, r3
+       add     \E, \E, ip
+       .endm
+
+       .macro  sha_f2, A, B, C, D, E
+       ldr     r3, [r2], #4
+       add     \E, r1, \E, ror #2
+       eor     ip, \B, \C, ror #2
+       add     \E, \E, \A, ror #27
+       eor     ip, ip, \D, ror #2
+       add     \E, \E, r3
+       add     \E, \E, ip
+       .endm
+
+       .macro  sha_f3, A, B, C, D, E
+       ldr     r3, [r2], #4
+       add     \E, r1, \E, ror #2
+       orr     ip, \B, \C, ror #2
+       add     \E, \E, \A, ror #27
+       and     ip, ip, \D, ror #2
+       add     \E, \E, r3
+       and     r3, \B, \C, ror #2
+       orr     ip, ip, r3
+       add     \E, \E, ip
+       .endm
+
+       ldmia   r0, {r4 - r8}
+
+       mov     lr, #4
+       ldr     r1, .L_sha_K + 0
+
+       /* adjust initial values */
+       mov     r6, r6, ror #30
+       mov     r7, r7, ror #30
+       mov     r8, r8, ror #30
+
+3:     subs    lr, lr, #1
+       sha_f1  r4, r5, r6, r7, r8
+       sha_f1  r8, r4, r5, r6, r7
+       sha_f1  r7, r8, r4, r5, r6
+       sha_f1  r6, r7, r8, r4, r5
+       sha_f1  r5, r6, r7, r8, r4
+       bne     3b
+
+       ldr     r1, .L_sha_K + 4
+       mov     lr, #4
+
+4:     subs    lr, lr, #1
+       sha_f2  r4, r5, r6, r7, r8
+       sha_f2  r8, r4, r5, r6, r7
+       sha_f2  r7, r8, r4, r5, r6
+       sha_f2  r6, r7, r8, r4, r5
+       sha_f2  r5, r6, r7, r8, r4
+       bne     4b
+
+       ldr     r1, .L_sha_K + 8
+       mov     lr, #4
+
+5:     subs    lr, lr, #1
+       sha_f3  r4, r5, r6, r7, r8
+       sha_f3  r8, r4, r5, r6, r7
+       sha_f3  r7, r8, r4, r5, r6
+       sha_f3  r6, r7, r8, r4, r5
+       sha_f3  r5, r6, r7, r8, r4
+       bne     5b
+
+       ldr     r1, .L_sha_K + 12
+       mov     lr, #4
+
+6:     subs    lr, lr, #1
+       sha_f2  r4, r5, r6, r7, r8
+       sha_f2  r8, r4, r5, r6, r7
+       sha_f2  r7, r8, r4, r5, r6
+       sha_f2  r6, r7, r8, r4, r5
+       sha_f2  r5, r6, r7, r8, r4
+       bne     6b
+
+       ldmia   r0, {r1, r2, r3, ip, lr}
+       add     r4, r1, r4
+       add     r5, r2, r5
+       add     r6, r3, r6, ror #2
+       add     r7, ip, r7, ror #2
+       add     r8, lr, r8, ror #2
+       stmia   r0, {r4 - r8}
+
+       ldmfd   sp!, {r4 - r8, pc}
+
+.L_sha_K:
+       .word   0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6
diff --git a/attr.c b/attr.c
new file mode 100644 (file)
index 0000000..741db3b
--- /dev/null
+++ b/attr.c
@@ -0,0 +1,635 @@
+#include "cache.h"
+#include "attr.h"
+
+const char git_attr__true[] = "(builtin)true";
+const char git_attr__false[] = "\0(builtin)false";
+static const char git_attr__unknown[] = "(builtin)unknown";
+#define ATTR__TRUE git_attr__true
+#define ATTR__FALSE git_attr__false
+#define ATTR__UNSET NULL
+#define ATTR__UNKNOWN git_attr__unknown
+
+/*
+ * The basic design decision here is that we are not going to have
+ * insanely large number of attributes.
+ *
+ * This is a randomly chosen prime.
+ */
+#define HASHSIZE 257
+
+#ifndef DEBUG_ATTR
+#define DEBUG_ATTR 0
+#endif
+
+struct git_attr {
+       struct git_attr *next;
+       unsigned h;
+       int attr_nr;
+       char name[FLEX_ARRAY];
+};
+static int attr_nr;
+
+static struct git_attr_check *check_all_attr;
+static struct git_attr *(git_attr_hash[HASHSIZE]);
+
+static unsigned hash_name(const char *name, int namelen)
+{
+       unsigned val = 0;
+       unsigned char c;
+
+       while (namelen--) {
+               c = *name++;
+               val = ((val << 7) | (val >> 22)) ^ c;
+       }
+       return val;
+}
+
+static int invalid_attr_name(const char *name, int namelen)
+{
+       /*
+        * Attribute name cannot begin with '-' and from
+        * [-A-Za-z0-9_.].  We'd specifically exclude '=' for now,
+        * as we might later want to allow non-binary value for
+        * attributes, e.g. "*.svg      merge=special-merge-program-for-svg"
+        */
+       if (*name == '-')
+               return -1;
+       while (namelen--) {
+               char ch = *name++;
+               if (! (ch == '-' || ch == '.' || ch == '_' ||
+                      ('0' <= ch && ch <= '9') ||
+                      ('a' <= ch && ch <= 'z') ||
+                      ('A' <= ch && ch <= 'Z')) )
+                       return -1;
+       }
+       return 0;
+}
+
+struct git_attr *git_attr(const char *name, int len)
+{
+       unsigned hval = hash_name(name, len);
+       unsigned pos = hval % HASHSIZE;
+       struct git_attr *a;
+
+       for (a = git_attr_hash[pos]; a; a = a->next) {
+               if (a->h == hval &&
+                   !memcmp(a->name, name, len) && !a->name[len])
+                       return a;
+       }
+
+       if (invalid_attr_name(name, len))
+               return NULL;
+
+       a = xmalloc(sizeof(*a) + len + 1);
+       memcpy(a->name, name, len);
+       a->name[len] = 0;
+       a->h = hval;
+       a->next = git_attr_hash[pos];
+       a->attr_nr = attr_nr++;
+       git_attr_hash[pos] = a;
+
+       check_all_attr = xrealloc(check_all_attr,
+                                 sizeof(*check_all_attr) * attr_nr);
+       check_all_attr[a->attr_nr].attr = a;
+       check_all_attr[a->attr_nr].value = ATTR__UNKNOWN;
+       return a;
+}
+
+/*
+ * .gitattributes file is one line per record, each of which is
+ *
+ * (1) glob pattern.
+ * (2) whitespace
+ * (3) whitespace separated list of attribute names, each of which
+ *     could be prefixed with '-' to mean "set to false", '!' to mean
+ *     "unset".
+ */
+
+/* What does a matched pattern decide? */
+struct attr_state {
+       struct git_attr *attr;
+       const char *setto;
+};
+
+struct match_attr {
+       union {
+               char *pattern;
+               struct git_attr *attr;
+       } u;
+       char is_macro;
+       unsigned num_attr;
+       struct attr_state state[FLEX_ARRAY];
+};
+
+static const char blank[] = " \t\r\n";
+
+static const char *parse_attr(const char *src, int lineno, const char *cp,
+                             int *num_attr, struct match_attr *res)
+{
+       const char *ep, *equals;
+       int len;
+
+       ep = cp + strcspn(cp, blank);
+       equals = strchr(cp, '=');
+       if (equals && ep < equals)
+               equals = NULL;
+       if (equals)
+               len = equals - cp;
+       else
+               len = ep - cp;
+       if (!res) {
+               if (*cp == '-' || *cp == '!') {
+                       cp++;
+                       len--;
+               }
+               if (invalid_attr_name(cp, len)) {
+                       fprintf(stderr,
+                               "%.*s is not a valid attribute name: %s:%d\n",
+                               len, cp, src, lineno);
+                       return NULL;
+               }
+       } else {
+               struct attr_state *e;
+
+               e = &(res->state[*num_attr]);
+               if (*cp == '-' || *cp == '!') {
+                       e->setto = (*cp == '-') ? ATTR__FALSE : ATTR__UNSET;
+                       cp++;
+                       len--;
+               }
+               else if (!equals)
+                       e->setto = ATTR__TRUE;
+               else {
+                       e->setto = xmemdupz(equals + 1, ep - equals - 1);
+               }
+               e->attr = git_attr(cp, len);
+       }
+       (*num_attr)++;
+       return ep + strspn(ep, blank);
+}
+
+static struct match_attr *parse_attr_line(const char *line, const char *src,
+                                         int lineno, int macro_ok)
+{
+       int namelen;
+       int num_attr;
+       const char *cp, *name;
+       struct match_attr *res = NULL;
+       int pass;
+       int is_macro;
+
+       cp = line + strspn(line, blank);
+       if (!*cp || *cp == '#')
+               return NULL;
+       name = cp;
+       namelen = strcspn(name, blank);
+       if (strlen(ATTRIBUTE_MACRO_PREFIX) < namelen &&
+           !prefixcmp(name, ATTRIBUTE_MACRO_PREFIX)) {
+               if (!macro_ok) {
+                       fprintf(stderr, "%s not allowed: %s:%d\n",
+                               name, src, lineno);
+                       return NULL;
+               }
+               is_macro = 1;
+               name += strlen(ATTRIBUTE_MACRO_PREFIX);
+               name += strspn(name, blank);
+               namelen = strcspn(name, blank);
+               if (invalid_attr_name(name, namelen)) {
+                       fprintf(stderr,
+                               "%.*s is not a valid attribute name: %s:%d\n",
+                               namelen, name, src, lineno);
+                       return NULL;
+               }
+       }
+       else
+               is_macro = 0;
+
+       for (pass = 0; pass < 2; pass++) {
+               /* pass 0 counts and allocates, pass 1 fills */
+               num_attr = 0;
+               cp = name + namelen;
+               cp = cp + strspn(cp, blank);
+               while (*cp) {
+                       cp = parse_attr(src, lineno, cp, &num_attr, res);
+                       if (!cp)
+                               return NULL;
+               }
+               if (pass)
+                       break;
+               res = xcalloc(1,
+                             sizeof(*res) +
+                             sizeof(struct attr_state) * num_attr +
+                             (is_macro ? 0 : namelen + 1));
+               if (is_macro)
+                       res->u.attr = git_attr(name, namelen);
+               else {
+                       res->u.pattern = (char*)&(res->state[num_attr]);
+                       memcpy(res->u.pattern, name, namelen);
+                       res->u.pattern[namelen] = 0;
+               }
+               res->is_macro = is_macro;
+               res->num_attr = num_attr;
+       }
+       return res;
+}
+
+/*
+ * Like info/exclude and .gitignore, the attribute information can
+ * come from many places.
+ *
+ * (1) .gitattribute file of the same directory;
+ * (2) .gitattribute file of the parent directory if (1) does not have
+ *      any match; this goes recursively upwards, just like .gitignore.
+ * (3) $GIT_DIR/info/attributes, which overrides both of the above.
+ *
+ * In the same file, later entries override the earlier match, so in the
+ * global list, we would have entries from info/attributes the earliest
+ * (reading the file from top to bottom), .gitattribute of the root
+ * directory (again, reading the file from top to bottom) down to the
+ * current directory, and then scan the list backwards to find the first match.
+ * This is exactly the same as what excluded() does in dir.c to deal with
+ * .gitignore
+ */
+
+static struct attr_stack {
+       struct attr_stack *prev;
+       char *origin;
+       unsigned num_matches;
+       unsigned alloc;
+       struct match_attr **attrs;
+} *attr_stack;
+
+static void free_attr_elem(struct attr_stack *e)
+{
+       int i;
+       free(e->origin);
+       for (i = 0; i < e->num_matches; i++) {
+               struct match_attr *a = e->attrs[i];
+               int j;
+               for (j = 0; j < a->num_attr; j++) {
+                       const char *setto = a->state[j].setto;
+                       if (setto == ATTR__TRUE ||
+                           setto == ATTR__FALSE ||
+                           setto == ATTR__UNSET ||
+                           setto == ATTR__UNKNOWN)
+                               ;
+                       else
+                               free((char*) setto);
+               }
+               free(a);
+       }
+       free(e);
+}
+
+static const char *builtin_attr[] = {
+       "[attr]binary -diff -crlf",
+       NULL,
+};
+
+static void handle_attr_line(struct attr_stack *res,
+                            const char *line,
+                            const char *src,
+                            int lineno,
+                            int macro_ok)
+{
+       struct match_attr *a;
+
+       a = parse_attr_line(line, src, lineno, macro_ok);
+       if (!a)
+               return;
+       if (res->alloc <= res->num_matches) {
+               res->alloc = alloc_nr(res->num_matches);
+               res->attrs = xrealloc(res->attrs,
+                                     sizeof(struct match_attr *) *
+                                     res->alloc);
+       }
+       res->attrs[res->num_matches++] = a;
+}
+
+static struct attr_stack *read_attr_from_array(const char **list)
+{
+       struct attr_stack *res;
+       const char *line;
+       int lineno = 0;
+
+       res = xcalloc(1, sizeof(*res));
+       while ((line = *(list++)) != NULL)
+               handle_attr_line(res, line, "[builtin]", ++lineno, 1);
+       return res;
+}
+
+static struct attr_stack *read_attr_from_file(const char *path, int macro_ok)
+{
+       FILE *fp = fopen(path, "r");
+       struct attr_stack *res;
+       char buf[2048];
+       int lineno = 0;
+
+       if (!fp)
+               return NULL;
+       res = xcalloc(1, sizeof(*res));
+       while (fgets(buf, sizeof(buf), fp))
+               handle_attr_line(res, buf, path, ++lineno, macro_ok);
+       fclose(fp);
+       return res;
+}
+
+static void *read_index_data(const char *path)
+{
+       int pos, len;
+       unsigned long sz;
+       enum object_type type;
+       void *data;
+
+       len = strlen(path);
+       pos = cache_name_pos(path, len);
+       if (pos < 0) {
+               /*
+                * We might be in the middle of a merge, in which
+                * case we would read stage #2 (ours).
+                */
+               int i;
+               for (i = -pos - 1;
+                    (pos < 0 && i < active_nr &&
+                     !strcmp(active_cache[i]->name, path));
+                    i++)
+                       if (ce_stage(active_cache[i]) == 2)
+                               pos = i;
+       }
+       if (pos < 0)
+               return NULL;
+       data = read_sha1_file(active_cache[pos]->sha1, &type, &sz);
+       if (!data || type != OBJ_BLOB) {
+               free(data);
+               return NULL;
+       }
+       return data;
+}
+
+static struct attr_stack *read_attr(const char *path, int macro_ok)
+{
+       struct attr_stack *res;
+       char *buf, *sp;
+       int lineno = 0;
+
+       res = read_attr_from_file(path, macro_ok);
+       if (res)
+               return res;
+
+       res = xcalloc(1, sizeof(*res));
+
+       /*
+        * There is no checked out .gitattributes file there, but
+        * we might have it in the index.  We allow operation in a
+        * sparsely checked out work tree, so read from it.
+        */
+       buf = read_index_data(path);
+       if (!buf)
+               return res;
+
+       for (sp = buf; *sp; ) {
+               char *ep;
+               int more;
+               for (ep = sp; *ep && *ep != '\n'; ep++)
+                       ;
+               more = (*ep == '\n');
+               *ep = '\0';
+               handle_attr_line(res, sp, path, ++lineno, macro_ok);
+               sp = ep + more;
+       }
+       free(buf);
+       return res;
+}
+
+#if DEBUG_ATTR
+static void debug_info(const char *what, struct attr_stack *elem)
+{
+       fprintf(stderr, "%s: %s\n", what, elem->origin ? elem->origin : "()");
+}
+static void debug_set(const char *what, const char *match, struct git_attr *attr, void *v)
+{
+       const char *value = v;
+
+       if (ATTR_TRUE(value))
+               value = "set";
+       else if (ATTR_FALSE(value))
+               value = "unset";
+       else if (ATTR_UNSET(value))
+               value = "unspecified";
+
+       fprintf(stderr, "%s: %s => %s (%s)\n",
+               what, attr->name, (char *) value, match);
+}
+#define debug_push(a) debug_info("push", (a))
+#define debug_pop(a) debug_info("pop", (a))
+#else
+#define debug_push(a) do { ; } while (0)
+#define debug_pop(a) do { ; } while (0)
+#define debug_set(a,b,c,d) do { ; } while (0)
+#endif
+
+static void bootstrap_attr_stack(void)
+{
+       if (!attr_stack) {
+               struct attr_stack *elem;
+
+               elem = read_attr_from_array(builtin_attr);
+               elem->origin = NULL;
+               elem->prev = attr_stack;
+               attr_stack = elem;
+
+               elem = read_attr(GITATTRIBUTES_FILE, 1);
+               elem->origin = strdup("");
+               elem->prev = attr_stack;
+               attr_stack = elem;
+               debug_push(elem);
+
+               elem = read_attr_from_file(git_path(INFOATTRIBUTES_FILE), 1);
+               if (!elem)
+                       elem = xcalloc(1, sizeof(*elem));
+               elem->origin = NULL;
+               elem->prev = attr_stack;
+               attr_stack = elem;
+       }
+}
+
+static void prepare_attr_stack(const char *path, int dirlen)
+{
+       struct attr_stack *elem, *info;
+       int len;
+       char pathbuf[PATH_MAX];
+
+       /*
+        * At the bottom of the attribute stack is the built-in
+        * set of attribute definitions.  Then, contents from
+        * .gitattribute files from directories closer to the
+        * root to the ones in deeper directories are pushed
+        * to the stack.  Finally, at the very top of the stack
+        * we always keep the contents of $GIT_DIR/info/attributes.
+        *
+        * When checking, we use entries from near the top of the
+        * stack, preferring $GIT_DIR/info/attributes, then
+        * .gitattributes in deeper directories to shallower ones,
+        * and finally use the built-in set as the default.
+        */
+       if (!attr_stack)
+               bootstrap_attr_stack();
+
+       /*
+        * Pop the "info" one that is always at the top of the stack.
+        */
+       info = attr_stack;
+       attr_stack = info->prev;
+
+       /*
+        * Pop the ones from directories that are not the prefix of
+        * the path we are checking.
+        */
+       while (attr_stack && attr_stack->origin) {
+               int namelen = strlen(attr_stack->origin);
+
+               elem = attr_stack;
+               if (namelen <= dirlen &&
+                   !strncmp(elem->origin, path, namelen))
+                       break;
+
+               debug_pop(elem);
+               attr_stack = elem->prev;
+               free_attr_elem(elem);
+       }
+
+       /*
+        * Read from parent directories and push them down
+        */
+       while (1) {
+               char *cp;
+
+               len = strlen(attr_stack->origin);
+               if (dirlen <= len)
+                       break;
+               memcpy(pathbuf, path, dirlen);
+               memcpy(pathbuf + dirlen, "/", 2);
+               cp = strchr(pathbuf + len + 1, '/');
+               strcpy(cp + 1, GITATTRIBUTES_FILE);
+               elem = read_attr(pathbuf, 0);
+               *cp = '\0';
+               elem->origin = strdup(pathbuf);
+               elem->prev = attr_stack;
+               attr_stack = elem;
+               debug_push(elem);
+       }
+
+       /*
+        * Finally push the "info" one at the top of the stack.
+        */
+       info->prev = attr_stack;
+       attr_stack = info;
+}
+
+static int path_matches(const char *pathname, int pathlen,
+                       const char *pattern,
+                       const char *base, int baselen)
+{
+       if (!strchr(pattern, '/')) {
+               /* match basename */
+               const char *basename = strrchr(pathname, '/');
+               basename = basename ? basename + 1 : pathname;
+               return (fnmatch(pattern, basename, 0) == 0);
+       }
+       /*
+        * match with FNM_PATHNAME; the pattern has base implicitly
+        * in front of it.
+        */
+       if (*pattern == '/')
+               pattern++;
+       if (pathlen < baselen ||
+           (baselen && pathname[baselen - 1] != '/') ||
+           strncmp(pathname, base, baselen))
+               return 0;
+       return fnmatch(pattern, pathname + baselen, FNM_PATHNAME) == 0;
+}
+
+static int fill_one(const char *what, struct match_attr *a, int rem)
+{
+       struct git_attr_check *check = check_all_attr;
+       int i;
+
+       for (i = 0; 0 < rem && i < a->num_attr; i++) {
+               struct git_attr *attr = a->state[i].attr;
+               const char **n = &(check[attr->attr_nr].value);
+               const char *v = a->state[i].setto;
+
+               if (*n == ATTR__UNKNOWN) {
+                       debug_set(what, a->u.pattern, attr, v);
+                       *n = v;
+                       rem--;
+               }
+       }
+       return rem;
+}
+
+static int fill(const char *path, int pathlen, struct attr_stack *stk, int rem)
+{
+       int i;
+       const char *base = stk->origin ? stk->origin : "";
+
+       for (i = stk->num_matches - 1; 0 < rem && 0 <= i; i--) {
+               struct match_attr *a = stk->attrs[i];
+               if (a->is_macro)
+                       continue;
+               if (path_matches(path, pathlen,
+                                a->u.pattern, base, strlen(base)))
+                       rem = fill_one("fill", a, rem);
+       }
+       return rem;
+}
+
+static int macroexpand(struct attr_stack *stk, int rem)
+{
+       int i;
+       struct git_attr_check *check = check_all_attr;
+
+       for (i = stk->num_matches - 1; 0 < rem && 0 <= i; i--) {
+               struct match_attr *a = stk->attrs[i];
+               if (!a->is_macro)
+                       continue;
+               if (check[a->u.attr->attr_nr].value != ATTR__TRUE)
+                       continue;
+               rem = fill_one("expand", a, rem);
+       }
+       return rem;
+}
+
+int git_checkattr(const char *path, int num, struct git_attr_check *check)
+{
+       struct attr_stack *stk;
+       const char *cp;
+       int dirlen, pathlen, i, rem;
+
+       bootstrap_attr_stack();
+       for (i = 0; i < attr_nr; i++)
+               check_all_attr[i].value = ATTR__UNKNOWN;
+
+       pathlen = strlen(path);
+       cp = strrchr(path, '/');
+       if (!cp)
+               dirlen = 0;
+       else
+               dirlen = cp - path;
+       prepare_attr_stack(path, dirlen);
+       rem = attr_nr;
+       for (stk = attr_stack; 0 < rem && stk; stk = stk->prev)
+               rem = fill(path, pathlen, stk, rem);
+
+       for (stk = attr_stack; 0 < rem && stk; stk = stk->prev)
+               rem = macroexpand(stk, rem);
+
+       for (i = 0; i < num; i++) {
+               const char *value = check_all_attr[check[i].attr->attr_nr].value;
+               if (value == ATTR__UNKNOWN)
+                       value = ATTR__UNSET;
+               check[i].value = value;
+       }
+
+       return 0;
+}
diff --git a/attr.h b/attr.h
new file mode 100644 (file)
index 0000000..f1c2038
--- /dev/null
+++ b/attr.h
@@ -0,0 +1,34 @@
+#ifndef ATTR_H
+#define ATTR_H
+
+/* An attribute is a pointer to this opaque structure */
+struct git_attr;
+
+/*
+ * Given a string, return the gitattribute object that
+ * corresponds to it.
+ */
+struct git_attr *git_attr(const char *, int);
+
+/* Internal use */
+extern const char git_attr__true[];
+extern const char git_attr__false[];
+
+/* For public to check git_attr_check results */
+#define ATTR_TRUE(v) ((v) == git_attr__true)
+#define ATTR_FALSE(v) ((v) == git_attr__false)
+#define ATTR_UNSET(v) ((v) == NULL)
+
+/*
+ * Send one or more git_attr_check to git_checkattr(), and
+ * each 'value' member tells what its value is.
+ * Unset one is returned as NULL.
+ */
+struct git_attr_check {
+       struct git_attr *attr;
+       const char *value;
+};
+
+int git_checkattr(const char *path, int, struct git_attr_check *);
+
+#endif /* ATTR_H */
diff --git a/base85.c b/base85.c
new file mode 100644 (file)
index 0000000..b88270f
--- /dev/null
+++ b/base85.c
@@ -0,0 +1,140 @@
+#include "cache.h"
+
+#undef DEBUG_85
+
+#ifdef DEBUG_85
+#define say(a) fprintf(stderr, a)
+#define say1(a,b) fprintf(stderr, a, b)
+#define say2(a,b,c) fprintf(stderr, a, b, c)
+#else
+#define say(a) do {} while(0)
+#define say1(a,b) do {} while(0)
+#define say2(a,b,c) do {} while(0)
+#endif
+
+static const char en85[] = {
+       '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+       'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+       'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+       'U', 'V', 'W', 'X', 'Y', 'Z',
+       'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+       'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+       'u', 'v', 'w', 'x', 'y', 'z',
+       '!', '#', '$', '%', '&', '(', ')', '*', '+', '-',
+       ';', '<', '=', '>', '?', '@', '^', '_', '`', '{',
+       '|', '}', '~'
+};
+
+static char de85[256];
+static void prep_base85(void)
+{
+       int i;
+       if (de85['Z'])
+               return;
+       for (i = 0; i < ARRAY_SIZE(en85); i++) {
+               int ch = en85[i];
+               de85[ch] = i + 1;
+       }
+}
+
+int decode_85(char *dst, const char *buffer, int len)
+{
+       prep_base85();
+
+       say2("decode 85 <%.*s>", len/4*5, buffer);
+       while (len) {
+               unsigned acc = 0;
+               int de, cnt = 4;
+               unsigned char ch;
+               do {
+                       ch = *buffer++;
+                       de = de85[ch];
+                       if (--de < 0)
+                               return error("invalid base85 alphabet %c", ch);
+                       acc = acc * 85 + de;
+               } while (--cnt);
+               ch = *buffer++;
+               de = de85[ch];
+               if (--de < 0)
+                       return error("invalid base85 alphabet %c", ch);
+               /*
+                * Detect overflow.  The largest
+                * 5-letter possible is "|NsC0" to
+                * encode 0xffffffff, and "|NsC" gives
+                * 0x03030303 at this point (i.e.
+                * 0xffffffff = 0x03030303 * 85).
+                */
+               if (0x03030303 < acc ||
+                   0xffffffff - de < (acc *= 85))
+                       return error("invalid base85 sequence %.5s", buffer-5);
+               acc += de;
+               say1(" %08x", acc);
+
+               cnt = (len < 4) ? len : 4;
+               len -= cnt;
+               do {
+                       acc = (acc << 8) | (acc >> 24);
+                       *dst++ = acc;
+               } while (--cnt);
+       }
+       say("\n");
+
+       return 0;
+}
+
+void encode_85(char *buf, const unsigned char *data, int bytes)
+{
+       prep_base85();
+
+       say("encode 85");
+       while (bytes) {
+               unsigned acc = 0;
+               int cnt;
+               for (cnt = 24; cnt >= 0; cnt -= 8) {
+                       int ch = *data++;
+                       acc |= ch << cnt;
+                       if (--bytes == 0)
+                               break;
+               }
+               say1(" %08x", acc);
+               for (cnt = 4; cnt >= 0; cnt--) {
+                       int val = acc % 85;
+                       acc /= 85;
+                       buf[cnt] = en85[val];
+               }
+               buf += 5;
+       }
+       say("\n");
+
+       *buf = 0;
+}
+
+#ifdef DEBUG_85
+int main(int ac, char **av)
+{
+       char buf[1024];
+
+       if (!strcmp(av[1], "-e")) {
+               int len = strlen(av[2]);
+               encode_85(buf, av[2], len);
+               if (len <= 26) len = len + 'A' - 1;
+               else len = len + 'a' - 26 + 1;
+               printf("encoded: %c%s\n", len, buf);
+               return 0;
+       }
+       if (!strcmp(av[1], "-d")) {
+               int len = *av[2];
+               if ('A' <= len && len <= 'Z') len = len - 'A' + 1;
+               else len = len - 'a' + 26 + 1;
+               decode_85(buf, av[2]+1, len);
+               printf("decoded: %.*s\n", len, buf);
+               return 0;
+       }
+       if (!strcmp(av[1], "-t")) {
+               char t[4] = { -1,-1,-1,-1 };
+               encode_85(buf, t, 4);
+               printf("encoded: D%s\n", buf);
+               return 0;
+       }
+}
+#endif
diff --git a/blob.c b/blob.c
new file mode 100644 (file)
index 0000000..bd7d078
--- /dev/null
+++ b/blob.c
@@ -0,0 +1,46 @@
+#include "cache.h"
+#include "blob.h"
+
+const char *blob_type = "blob";
+
+struct blob *lookup_blob(const unsigned char *sha1)
+{
+       struct object *obj = lookup_object(sha1);
+       if (!obj)
+               return create_object(sha1, OBJ_BLOB, alloc_blob_node());
+       if (!obj->type)
+               obj->type = OBJ_BLOB;
+       if (obj->type != OBJ_BLOB) {
+               error("Object %s is a %s, not a blob",
+                     sha1_to_hex(sha1), typename(obj->type));
+               return NULL;
+       }
+       return (struct blob *) obj;
+}
+
+int parse_blob_buffer(struct blob *item, void *buffer, unsigned long size)
+{
+       item->object.parsed = 1;
+       return 0;
+}
+
+int parse_blob(struct blob *item)
+{
+        enum object_type type;
+        void *buffer;
+        unsigned long size;
+       int ret;
+
+        if (item->object.parsed)
+                return 0;
+        buffer = read_sha1_file(item->object.sha1, &type, &size);
+        if (!buffer)
+                return error("Could not read %s",
+                             sha1_to_hex(item->object.sha1));
+        if (type != OBJ_BLOB)
+                return error("Object %s not a blob",
+                             sha1_to_hex(item->object.sha1));
+       ret = parse_blob_buffer(item, buffer, size);
+       free(buffer);
+       return ret;
+}
diff --git a/blob.h b/blob.h
new file mode 100644 (file)
index 0000000..ea5d9e9
--- /dev/null
+++ b/blob.h
@@ -0,0 +1,18 @@
+#ifndef BLOB_H
+#define BLOB_H
+
+#include "object.h"
+
+extern const char *blob_type;
+
+struct blob {
+       struct object object;
+};
+
+struct blob *lookup_blob(const unsigned char *sha1);
+
+int parse_blob_buffer(struct blob *item, void *buffer, unsigned long size);
+
+int parse_blob(struct blob *item);
+
+#endif /* BLOB_H */
diff --git a/builtin-add.c b/builtin-add.c
new file mode 100644 (file)
index 0000000..5c29cc2
--- /dev/null
@@ -0,0 +1,267 @@
+/*
+ * "git add" builtin command
+ *
+ * Copyright (C) 2006 Linus Torvalds
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "dir.h"
+#include "exec_cmd.h"
+#include "cache-tree.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "commit.h"
+#include "revision.h"
+#include "run-command.h"
+#include "parse-options.h"
+
+static const char * const builtin_add_usage[] = {
+       "git-add [options] [--] <filepattern>...",
+       NULL
+};
+static int patch_interactive = 0, add_interactive = 0;
+static int take_worktree_changes;
+
+static void prune_directory(struct dir_struct *dir, const char **pathspec, int prefix)
+{
+       char *seen;
+       int i, specs;
+       struct dir_entry **src, **dst;
+
+       for (specs = 0; pathspec[specs];  specs++)
+               /* nothing */;
+       seen = xcalloc(specs, 1);
+
+       src = dst = dir->entries;
+       i = dir->nr;
+       while (--i >= 0) {
+               struct dir_entry *entry = *src++;
+               if (match_pathspec(pathspec, entry->name, entry->len,
+                                  prefix, seen))
+                       *dst++ = entry;
+       }
+       dir->nr = dst - dir->entries;
+
+       for (i = 0; i < specs; i++) {
+               if (!seen[i] && !file_exists(pathspec[i]))
+                       die("pathspec '%s' did not match any files",
+                                       pathspec[i]);
+       }
+        free(seen);
+}
+
+static void fill_directory(struct dir_struct *dir, const char **pathspec,
+               int ignored_too)
+{
+       const char *path, *base;
+       int baselen;
+
+       /* Set up the default git porcelain excludes */
+       memset(dir, 0, sizeof(*dir));
+       if (!ignored_too) {
+               dir->collect_ignored = 1;
+               setup_standard_excludes(dir);
+       }
+
+       /*
+        * Calculate common prefix for the pathspec, and
+        * use that to optimize the directory walk
+        */
+       baselen = common_prefix(pathspec);
+       path = ".";
+       base = "";
+       if (baselen)
+               path = base = xmemdupz(*pathspec, baselen);
+
+       /* Read the directory and prune it */
+       read_directory(dir, path, base, baselen, pathspec);
+       if (pathspec)
+               prune_directory(dir, pathspec, baselen);
+}
+
+static void update_callback(struct diff_queue_struct *q,
+                           struct diff_options *opt, void *cbdata)
+{
+       int i, verbose;
+
+       verbose = *((int *)cbdata);
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filepair *p = q->queue[i];
+               const char *path = p->one->path;
+               switch (p->status) {
+               default:
+                       die("unexpected diff status %c", p->status);
+               case DIFF_STATUS_UNMERGED:
+               case DIFF_STATUS_MODIFIED:
+               case DIFF_STATUS_TYPE_CHANGED:
+                       add_file_to_cache(path, verbose);
+                       break;
+               case DIFF_STATUS_DELETED:
+                       remove_file_from_cache(path);
+                       if (verbose)
+                               printf("remove '%s'\n", path);
+                       break;
+               }
+       }
+}
+
+void add_files_to_cache(int verbose, const char *prefix, const char **pathspec)
+{
+       struct rev_info rev;
+       init_revisions(&rev, prefix);
+       setup_revisions(0, NULL, &rev, NULL);
+       rev.prune_data = pathspec;
+       rev.diffopt.output_format = DIFF_FORMAT_CALLBACK;
+       rev.diffopt.format_callback = update_callback;
+       rev.diffopt.format_callback_data = &verbose;
+       run_diff_files(&rev, DIFF_RACY_IS_MODIFIED);
+}
+
+static void refresh(int verbose, const char **pathspec)
+{
+       char *seen;
+       int i, specs;
+
+       for (specs = 0; pathspec[specs];  specs++)
+               /* nothing */;
+       seen = xcalloc(specs, 1);
+       if (read_cache() < 0)
+               die("index file corrupt");
+       refresh_index(&the_index, verbose ? 0 : REFRESH_QUIET, pathspec, seen);
+       for (i = 0; i < specs; i++) {
+               if (!seen[i])
+                       die("pathspec '%s' did not match any files", pathspec[i]);
+       }
+        free(seen);
+}
+
+static const char **validate_pathspec(int argc, const char **argv, const char *prefix)
+{
+       const char **pathspec = get_pathspec(prefix, argv);
+
+       return pathspec;
+}
+
+int interactive_add(int argc, const char **argv, const char *prefix)
+{
+       int status, ac;
+       const char **args;
+       const char **pathspec = NULL;
+
+       if (argc) {
+               pathspec = validate_pathspec(argc, argv, prefix);
+               if (!pathspec)
+                       return -1;
+       }
+
+       args = xcalloc(sizeof(const char *), (argc + 4));
+       ac = 0;
+       args[ac++] = "add--interactive";
+       if (patch_interactive)
+               args[ac++] = "--patch";
+       args[ac++] = "--";
+       if (argc) {
+               memcpy(&(args[ac]), pathspec, sizeof(const char *) * argc);
+               ac += argc;
+       }
+       args[ac] = NULL;
+
+       status = run_command_v_opt(args, RUN_GIT_CMD);
+       free(args);
+       return status;
+}
+
+static struct lock_file lock_file;
+
+static const char ignore_error[] =
+"The following paths are ignored by one of your .gitignore files:\n";
+
+static int verbose = 0, show_only = 0, ignored_too = 0, refresh_only = 0;
+
+static struct option builtin_add_options[] = {
+       OPT__DRY_RUN(&show_only),
+       OPT__VERBOSE(&verbose),
+       OPT_GROUP(""),
+       OPT_BOOLEAN('i', "interactive", &add_interactive, "interactive picking"),
+       OPT_BOOLEAN('p', "patch", &patch_interactive, "interactive patching"),
+       OPT_BOOLEAN('f', NULL, &ignored_too, "allow adding otherwise ignored files"),
+       OPT_BOOLEAN('u', NULL, &take_worktree_changes, "update tracked files"),
+       OPT_BOOLEAN( 0 , "refresh", &refresh_only, "don't add, only refresh the index"),
+       OPT_END(),
+};
+
+int cmd_add(int argc, const char **argv, const char *prefix)
+{
+       int i, newfd;
+       const char **pathspec;
+       struct dir_struct dir;
+
+       argc = parse_options(argc, argv, builtin_add_options,
+                         builtin_add_usage, 0);
+       if (patch_interactive)
+               add_interactive = 1;
+       if (add_interactive)
+               exit(interactive_add(argc, argv, prefix));
+
+       git_config(git_default_config);
+
+       newfd = hold_locked_index(&lock_file, 1);
+
+       if (take_worktree_changes) {
+               const char **pathspec;
+               if (read_cache() < 0)
+                       die("index file corrupt");
+               pathspec = get_pathspec(prefix, argv);
+               add_files_to_cache(verbose, prefix, pathspec);
+               goto finish;
+       }
+
+       if (argc == 0) {
+               fprintf(stderr, "Nothing specified, nothing added.\n");
+               fprintf(stderr, "Maybe you wanted to say 'git add .'?\n");
+               return 0;
+       }
+       pathspec = get_pathspec(prefix, argv);
+
+       if (refresh_only) {
+               refresh(verbose, pathspec);
+               goto finish;
+       }
+
+       fill_directory(&dir, pathspec, ignored_too);
+
+       if (show_only) {
+               const char *sep = "", *eof = "";
+               for (i = 0; i < dir.nr; i++) {
+                       printf("%s%s", sep, dir.entries[i]->name);
+                       sep = " ";
+                       eof = "\n";
+               }
+               fputs(eof, stdout);
+               return 0;
+       }
+
+       if (read_cache() < 0)
+               die("index file corrupt");
+
+       if (dir.ignored_nr) {
+               fprintf(stderr, ignore_error);
+               for (i = 0; i < dir.ignored_nr; i++) {
+                       fprintf(stderr, "%s\n", dir.ignored[i]->name);
+               }
+               fprintf(stderr, "Use -f if you really want to add them.\n");
+               die("no files added");
+       }
+
+       for (i = 0; i < dir.nr; i++)
+               add_file_to_cache(dir.entries[i]->name, verbose);
+
+ finish:
+       if (active_cache_changed) {
+               if (write_cache(newfd, active_cache, active_nr) ||
+                   close(newfd) || commit_locked_index(&lock_file))
+                       die("Unable to write new index file");
+       }
+
+       return 0;
+}
diff --git a/builtin-annotate.c b/builtin-annotate.c
new file mode 100644 (file)
index 0000000..fc43eed
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * "git annotate" builtin alias
+ *
+ * Copyright (C) 2006 Ryan Anderson
+ */
+#include "git-compat-util.h"
+#include "builtin.h"
+
+int cmd_annotate(int argc, const char **argv, const char *prefix)
+{
+       const char **nargv;
+       int i;
+       nargv = xmalloc(sizeof(char *) * (argc + 2));
+
+       nargv[0] = "annotate";
+       nargv[1] = "-c";
+
+       for (i = 1; i < argc; i++) {
+               nargv[i+1] = argv[i];
+       }
+       nargv[argc + 1] = NULL;
+
+       return cmd_blame(argc + 1, nargv, prefix);
+}
diff --git a/builtin-apply.c b/builtin-apply.c
new file mode 100644 (file)
index 0000000..91f8752
--- /dev/null
@@ -0,0 +1,2892 @@
+/*
+ * apply.c
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ *
+ * This applies patches on top of some (arbitrary) version of the SCM.
+ *
+ */
+#include "cache.h"
+#include "cache-tree.h"
+#include "quote.h"
+#include "blob.h"
+#include "delta.h"
+#include "builtin.h"
+
+/*
+ *  --check turns on checking that the working tree matches the
+ *    files that are being modified, but doesn't apply the patch
+ *  --stat does just a diffstat, and doesn't actually apply
+ *  --numstat does numeric diffstat, and doesn't actually apply
+ *  --index-info shows the old and new index info for paths if available.
+ *  --index updates the cache as well.
+ *  --cached updates only the cache without ever touching the working tree.
+ */
+static const char *prefix;
+static int prefix_length = -1;
+static int newfd = -1;
+
+static int unidiff_zero;
+static int p_value = 1;
+static int p_value_known;
+static int check_index;
+static int update_index;
+static int cached;
+static int diffstat;
+static int numstat;
+static int summary;
+static int check;
+static int apply = 1;
+static int apply_in_reverse;
+static int apply_with_reject;
+static int apply_verbosely;
+static int no_add;
+static const char *fake_ancestor;
+static int line_termination = '\n';
+static unsigned long p_context = ULONG_MAX;
+static const char apply_usage[] =
+"git-apply [--stat] [--numstat] [--summary] [--check] [--index] [--cached] [--apply] [--no-add] [--index-info] [--allow-binary-replacement] [--reverse] [--reject] [--verbose] [-z] [-pNUM] [-CNUM] [--whitespace=<nowarn|warn|error|error-all|strip>] <patch>...";
+
+static enum whitespace_eol {
+       nowarn_whitespace,
+       warn_on_whitespace,
+       error_on_whitespace,
+       strip_whitespace,
+} new_whitespace = warn_on_whitespace;
+static int whitespace_error;
+static int squelch_whitespace_errors = 5;
+static int applied_after_fixing_ws;
+static const char *patch_input_file;
+
+static void parse_whitespace_option(const char *option)
+{
+       if (!option) {
+               new_whitespace = warn_on_whitespace;
+               return;
+       }
+       if (!strcmp(option, "warn")) {
+               new_whitespace = warn_on_whitespace;
+               return;
+       }
+       if (!strcmp(option, "nowarn")) {
+               new_whitespace = nowarn_whitespace;
+               return;
+       }
+       if (!strcmp(option, "error")) {
+               new_whitespace = error_on_whitespace;
+               return;
+       }
+       if (!strcmp(option, "error-all")) {
+               new_whitespace = error_on_whitespace;
+               squelch_whitespace_errors = 0;
+               return;
+       }
+       if (!strcmp(option, "strip")) {
+               new_whitespace = strip_whitespace;
+               return;
+       }
+       die("unrecognized whitespace option '%s'", option);
+}
+
+static void set_default_whitespace_mode(const char *whitespace_option)
+{
+       if (!whitespace_option && !apply_default_whitespace) {
+               new_whitespace = (apply
+                                 ? warn_on_whitespace
+                                 : nowarn_whitespace);
+       }
+}
+
+/*
+ * For "diff-stat" like behaviour, we keep track of the biggest change
+ * we've seen, and the longest filename. That allows us to do simple
+ * scaling.
+ */
+static int max_change, max_len;
+
+/*
+ * Various "current state", notably line numbers and what
+ * file (and how) we're patching right now.. The "is_xxxx"
+ * things are flags, where -1 means "don't know yet".
+ */
+static int linenr = 1;
+
+/*
+ * This represents one "hunk" from a patch, starting with
+ * "@@ -oldpos,oldlines +newpos,newlines @@" marker.  The
+ * patch text is pointed at by patch, and its byte length
+ * is stored in size.  leading and trailing are the number
+ * of context lines.
+ */
+struct fragment {
+       unsigned long leading, trailing;
+       unsigned long oldpos, oldlines;
+       unsigned long newpos, newlines;
+       const char *patch;
+       int size;
+       int rejected;
+       struct fragment *next;
+};
+
+/*
+ * When dealing with a binary patch, we reuse "leading" field
+ * to store the type of the binary hunk, either deflated "delta"
+ * or deflated "literal".
+ */
+#define binary_patch_method leading
+#define BINARY_DELTA_DEFLATED  1
+#define BINARY_LITERAL_DEFLATED 2
+
+struct patch {
+       char *new_name, *old_name, *def_name;
+       unsigned int old_mode, new_mode;
+       int is_new, is_delete;  /* -1 = unknown, 0 = false, 1 = true */
+       int rejected;
+       unsigned long deflate_origlen;
+       int lines_added, lines_deleted;
+       int score;
+       unsigned int is_toplevel_relative:1;
+       unsigned int inaccurate_eof:1;
+       unsigned int is_binary:1;
+       unsigned int is_copy:1;
+       unsigned int is_rename:1;
+       struct fragment *fragments;
+       char *result;
+       size_t resultsize;
+       char old_sha1_prefix[41];
+       char new_sha1_prefix[41];
+       struct patch *next;
+};
+
+static void say_patch_name(FILE *output, const char *pre, struct patch *patch, const char *post)
+{
+       fputs(pre, output);
+       if (patch->old_name && patch->new_name &&
+           strcmp(patch->old_name, patch->new_name)) {
+               quote_c_style(patch->old_name, NULL, output, 0);
+               fputs(" => ", output);
+               quote_c_style(patch->new_name, NULL, output, 0);
+       } else {
+               const char *n = patch->new_name;
+               if (!n)
+                       n = patch->old_name;
+               quote_c_style(n, NULL, output, 0);
+       }
+       fputs(post, output);
+}
+
+#define CHUNKSIZE (8192)
+#define SLOP (16)
+
+static void read_patch_file(struct strbuf *sb, int fd)
+{
+       if (strbuf_read(sb, fd, 0) < 0)
+               die("git-apply: read returned %s", strerror(errno));
+
+       /*
+        * Make sure that we have some slop in the buffer
+        * so that we can do speculative "memcmp" etc, and
+        * see to it that it is NUL-filled.
+        */
+       strbuf_grow(sb, SLOP);
+       memset(sb->buf + sb->len, 0, SLOP);
+}
+
+static unsigned long linelen(const char *buffer, unsigned long size)
+{
+       unsigned long len = 0;
+       while (size--) {
+               len++;
+               if (*buffer++ == '\n')
+                       break;
+       }
+       return len;
+}
+
+static int is_dev_null(const char *str)
+{
+       return !memcmp("/dev/null", str, 9) && isspace(str[9]);
+}
+
+#define TERM_SPACE     1
+#define TERM_TAB       2
+
+static int name_terminate(const char *name, int namelen, int c, int terminate)
+{
+       if (c == ' ' && !(terminate & TERM_SPACE))
+               return 0;
+       if (c == '\t' && !(terminate & TERM_TAB))
+               return 0;
+
+       return 1;
+}
+
+static char *find_name(const char *line, char *def, int p_value, int terminate)
+{
+       int len;
+       const char *start = line;
+
+       if (*line == '"') {
+               struct strbuf name;
+
+               /* Proposed "new-style" GNU patch/diff format; see
+                * http://marc.theaimsgroup.com/?l=git&m=112927316408690&w=2
+                */
+               strbuf_init(&name, 0);
+               if (!unquote_c_style(&name, line, NULL)) {
+                       char *cp;
+
+                       for (cp = name.buf; p_value; p_value--) {
+                               cp = strchr(cp, '/');
+                               if (!cp)
+                                       break;
+                               cp++;
+                       }
+                       if (cp) {
+                               /* name can later be freed, so we need
+                                * to memmove, not just return cp
+                                */
+                               strbuf_remove(&name, 0, cp - name.buf);
+                               free(def);
+                               return strbuf_detach(&name, NULL);
+                       }
+               }
+               strbuf_release(&name);
+       }
+
+       for (;;) {
+               char c = *line;
+
+               if (isspace(c)) {
+                       if (c == '\n')
+                               break;
+                       if (name_terminate(start, line-start, c, terminate))
+                               break;
+               }
+               line++;
+               if (c == '/' && !--p_value)
+                       start = line;
+       }
+       if (!start)
+               return def;
+       len = line - start;
+       if (!len)
+               return def;
+
+       /*
+        * Generally we prefer the shorter name, especially
+        * if the other one is just a variation of that with
+        * something else tacked on to the end (ie "file.orig"
+        * or "file~").
+        */
+       if (def) {
+               int deflen = strlen(def);
+               if (deflen < len && !strncmp(start, def, deflen))
+                       return def;
+               free(def);
+       }
+
+       return xmemdupz(start, len);
+}
+
+static int count_slashes(const char *cp)
+{
+       int cnt = 0;
+       char ch;
+
+       while ((ch = *cp++))
+               if (ch == '/')
+                       cnt++;
+       return cnt;
+}
+
+/*
+ * Given the string after "--- " or "+++ ", guess the appropriate
+ * p_value for the given patch.
+ */
+static int guess_p_value(const char *nameline)
+{
+       char *name, *cp;
+       int val = -1;
+
+       if (is_dev_null(nameline))
+               return -1;
+       name = find_name(nameline, NULL, 0, TERM_SPACE | TERM_TAB);
+       if (!name)
+               return -1;
+       cp = strchr(name, '/');
+       if (!cp)
+               val = 0;
+       else if (prefix) {
+               /*
+                * Does it begin with "a/$our-prefix" and such?  Then this is
+                * very likely to apply to our directory.
+                */
+               if (!strncmp(name, prefix, prefix_length))
+                       val = count_slashes(prefix);
+               else {
+                       cp++;
+                       if (!strncmp(cp, prefix, prefix_length))
+                               val = count_slashes(prefix) + 1;
+               }
+       }
+       free(name);
+       return val;
+}
+
+/*
+ * Get the name etc info from the --/+++ lines of a traditional patch header
+ *
+ * FIXME! The end-of-filename heuristics are kind of screwy. For existing
+ * files, we can happily check the index for a match, but for creating a
+ * new file we should try to match whatever "patch" does. I have no idea.
+ */
+static void parse_traditional_patch(const char *first, const char *second, struct patch *patch)
+{
+       char *name;
+
+       first += 4;     /* skip "--- " */
+       second += 4;    /* skip "+++ " */
+       if (!p_value_known) {
+               int p, q;
+               p = guess_p_value(first);
+               q = guess_p_value(second);
+               if (p < 0) p = q;
+               if (0 <= p && p == q) {
+                       p_value = p;
+                       p_value_known = 1;
+               }
+       }
+       if (is_dev_null(first)) {
+               patch->is_new = 1;
+               patch->is_delete = 0;
+               name = find_name(second, NULL, p_value, TERM_SPACE | TERM_TAB);
+               patch->new_name = name;
+       } else if (is_dev_null(second)) {
+               patch->is_new = 0;
+               patch->is_delete = 1;
+               name = find_name(first, NULL, p_value, TERM_SPACE | TERM_TAB);
+               patch->old_name = name;
+       } else {
+               name = find_name(first, NULL, p_value, TERM_SPACE | TERM_TAB);
+               name = find_name(second, name, p_value, TERM_SPACE | TERM_TAB);
+               patch->old_name = patch->new_name = name;
+       }
+       if (!name)
+               die("unable to find filename in patch at line %d", linenr);
+}
+
+static int gitdiff_hdrend(const char *line, struct patch *patch)
+{
+       return -1;
+}
+
+/*
+ * We're anal about diff header consistency, to make
+ * sure that we don't end up having strange ambiguous
+ * patches floating around.
+ *
+ * As a result, gitdiff_{old|new}name() will check
+ * their names against any previous information, just
+ * to make sure..
+ */
+static char *gitdiff_verify_name(const char *line, int isnull, char *orig_name, const char *oldnew)
+{
+       if (!orig_name && !isnull)
+               return find_name(line, NULL, p_value, TERM_TAB);
+
+       if (orig_name) {
+               int len;
+               const char *name;
+               char *another;
+               name = orig_name;
+               len = strlen(name);
+               if (isnull)
+                       die("git-apply: bad git-diff - expected /dev/null, got %s on line %d", name, linenr);
+               another = find_name(line, NULL, p_value, TERM_TAB);
+               if (!another || memcmp(another, name, len))
+                       die("git-apply: bad git-diff - inconsistent %s filename on line %d", oldnew, linenr);
+               free(another);
+               return orig_name;
+       }
+       else {
+               /* expect "/dev/null" */
+               if (memcmp("/dev/null", line, 9) || line[9] != '\n')
+                       die("git-apply: bad git-diff - expected /dev/null on line %d", linenr);
+               return NULL;
+       }
+}
+
+static int gitdiff_oldname(const char *line, struct patch *patch)
+{
+       patch->old_name = gitdiff_verify_name(line, patch->is_new, patch->old_name, "old");
+       return 0;
+}
+
+static int gitdiff_newname(const char *line, struct patch *patch)
+{
+       patch->new_name = gitdiff_verify_name(line, patch->is_delete, patch->new_name, "new");
+       return 0;
+}
+
+static int gitdiff_oldmode(const char *line, struct patch *patch)
+{
+       patch->old_mode = strtoul(line, NULL, 8);
+       return 0;
+}
+
+static int gitdiff_newmode(const char *line, struct patch *patch)
+{
+       patch->new_mode = strtoul(line, NULL, 8);
+       return 0;
+}
+
+static int gitdiff_delete(const char *line, struct patch *patch)
+{
+       patch->is_delete = 1;
+       patch->old_name = patch->def_name;
+       return gitdiff_oldmode(line, patch);
+}
+
+static int gitdiff_newfile(const char *line, struct patch *patch)
+{
+       patch->is_new = 1;
+       patch->new_name = patch->def_name;
+       return gitdiff_newmode(line, patch);
+}
+
+static int gitdiff_copysrc(const char *line, struct patch *patch)
+{
+       patch->is_copy = 1;
+       patch->old_name = find_name(line, NULL, 0, 0);
+       return 0;
+}
+
+static int gitdiff_copydst(const char *line, struct patch *patch)
+{
+       patch->is_copy = 1;
+       patch->new_name = find_name(line, NULL, 0, 0);
+       return 0;
+}
+
+static int gitdiff_renamesrc(const char *line, struct patch *patch)
+{
+       patch->is_rename = 1;
+       patch->old_name = find_name(line, NULL, 0, 0);
+       return 0;
+}
+
+static int gitdiff_renamedst(const char *line, struct patch *patch)
+{
+       patch->is_rename = 1;
+       patch->new_name = find_name(line, NULL, 0, 0);
+       return 0;
+}
+
+static int gitdiff_similarity(const char *line, struct patch *patch)
+{
+       if ((patch->score = strtoul(line, NULL, 10)) == ULONG_MAX)
+               patch->score = 0;
+       return 0;
+}
+
+static int gitdiff_dissimilarity(const char *line, struct patch *patch)
+{
+       if ((patch->score = strtoul(line, NULL, 10)) == ULONG_MAX)
+               patch->score = 0;
+       return 0;
+}
+
+static int gitdiff_index(const char *line, struct patch *patch)
+{
+       /* index line is N hexadecimal, "..", N hexadecimal,
+        * and optional space with octal mode.
+        */
+       const char *ptr, *eol;
+       int len;
+
+       ptr = strchr(line, '.');
+       if (!ptr || ptr[1] != '.' || 40 < ptr - line)
+               return 0;
+       len = ptr - line;
+       memcpy(patch->old_sha1_prefix, line, len);
+       patch->old_sha1_prefix[len] = 0;
+
+       line = ptr + 2;
+       ptr = strchr(line, ' ');
+       eol = strchr(line, '\n');
+
+       if (!ptr || eol < ptr)
+               ptr = eol;
+       len = ptr - line;
+
+       if (40 < len)
+               return 0;
+       memcpy(patch->new_sha1_prefix, line, len);
+       patch->new_sha1_prefix[len] = 0;
+       if (*ptr == ' ')
+               patch->new_mode = patch->old_mode = strtoul(ptr+1, NULL, 8);
+       return 0;
+}
+
+/*
+ * This is normal for a diff that doesn't change anything: we'll fall through
+ * into the next diff. Tell the parser to break out.
+ */
+static int gitdiff_unrecognized(const char *line, struct patch *patch)
+{
+       return -1;
+}
+
+static const char *stop_at_slash(const char *line, int llen)
+{
+       int i;
+
+       for (i = 0; i < llen; i++) {
+               int ch = line[i];
+               if (ch == '/')
+                       return line + i;
+       }
+       return NULL;
+}
+
+/* This is to extract the same name that appears on "diff --git"
+ * line.  We do not find and return anything if it is a rename
+ * patch, and it is OK because we will find the name elsewhere.
+ * We need to reliably find name only when it is mode-change only,
+ * creation or deletion of an empty file.  In any of these cases,
+ * both sides are the same name under a/ and b/ respectively.
+ */
+static char *git_header_name(char *line, int llen)
+{
+       const char *name;
+       const char *second = NULL;
+       size_t len;
+
+       line += strlen("diff --git ");
+       llen -= strlen("diff --git ");
+
+       if (*line == '"') {
+               const char *cp;
+               struct strbuf first;
+               struct strbuf sp;
+
+               strbuf_init(&first, 0);
+               strbuf_init(&sp, 0);
+
+               if (unquote_c_style(&first, line, &second))
+                       goto free_and_fail1;
+
+               /* advance to the first slash */
+               cp = stop_at_slash(first.buf, first.len);
+               /* we do not accept absolute paths */
+               if (!cp || cp == first.buf)
+                       goto free_and_fail1;
+               strbuf_remove(&first, 0, cp + 1 - first.buf);
+
+               /* second points at one past closing dq of name.
+                * find the second name.
+                */
+               while ((second < line + llen) && isspace(*second))
+                       second++;
+
+               if (line + llen <= second)
+                       goto free_and_fail1;
+               if (*second == '"') {
+                       if (unquote_c_style(&sp, second, NULL))
+                               goto free_and_fail1;
+                       cp = stop_at_slash(sp.buf, sp.len);
+                       if (!cp || cp == sp.buf)
+                               goto free_and_fail1;
+                       /* They must match, otherwise ignore */
+                       if (strcmp(cp + 1, first.buf))
+                               goto free_and_fail1;
+                       strbuf_release(&sp);
+                       return strbuf_detach(&first, NULL);
+               }
+
+               /* unquoted second */
+               cp = stop_at_slash(second, line + llen - second);
+               if (!cp || cp == second)
+                       goto free_and_fail1;
+               cp++;
+               if (line + llen - cp != first.len + 1 ||
+                   memcmp(first.buf, cp, first.len))
+                       goto free_and_fail1;
+               return strbuf_detach(&first, NULL);
+
+       free_and_fail1:
+               strbuf_release(&first);
+               strbuf_release(&sp);
+               return NULL;
+       }
+
+       /* unquoted first name */
+       name = stop_at_slash(line, llen);
+       if (!name || name == line)
+               return NULL;
+       name++;
+
+       /* since the first name is unquoted, a dq if exists must be
+        * the beginning of the second name.
+        */
+       for (second = name; second < line + llen; second++) {
+               if (*second == '"') {
+                       struct strbuf sp;
+                       const char *np;
+
+                       strbuf_init(&sp, 0);
+                       if (unquote_c_style(&sp, second, NULL))
+                               goto free_and_fail2;
+
+                       np = stop_at_slash(sp.buf, sp.len);
+                       if (!np || np == sp.buf)
+                               goto free_and_fail2;
+                       np++;
+
+                       len = sp.buf + sp.len - np;
+                       if (len < second - name &&
+                           !strncmp(np, name, len) &&
+                           isspace(name[len])) {
+                               /* Good */
+                               strbuf_remove(&sp, 0, np - sp.buf);
+                               return strbuf_detach(&sp, NULL);
+                       }
+
+               free_and_fail2:
+                       strbuf_release(&sp);
+                       return NULL;
+               }
+       }
+
+       /*
+        * Accept a name only if it shows up twice, exactly the same
+        * form.
+        */
+       for (len = 0 ; ; len++) {
+               switch (name[len]) {
+               default:
+                       continue;
+               case '\n':
+                       return NULL;
+               case '\t': case ' ':
+                       second = name+len;
+                       for (;;) {
+                               char c = *second++;
+                               if (c == '\n')
+                                       return NULL;
+                               if (c == '/')
+                                       break;
+                       }
+                       if (second[len] == '\n' && !memcmp(name, second, len)) {
+                               return xmemdupz(name, len);
+                       }
+               }
+       }
+}
+
+/* Verify that we recognize the lines following a git header */
+static int parse_git_header(char *line, int len, unsigned int size, struct patch *patch)
+{
+       unsigned long offset;
+
+       /* A git diff has explicit new/delete information, so we don't guess */
+       patch->is_new = 0;
+       patch->is_delete = 0;
+
+       /*
+        * Some things may not have the old name in the
+        * rest of the headers anywhere (pure mode changes,
+        * or removing or adding empty files), so we get
+        * the default name from the header.
+        */
+       patch->def_name = git_header_name(line, len);
+
+       line += len;
+       size -= len;
+       linenr++;
+       for (offset = len ; size > 0 ; offset += len, size -= len, line += len, linenr++) {
+               static const struct opentry {
+                       const char *str;
+                       int (*fn)(const char *, struct patch *);
+               } optable[] = {
+                       { "@@ -", gitdiff_hdrend },
+                       { "--- ", gitdiff_oldname },
+                       { "+++ ", gitdiff_newname },
+                       { "old mode ", gitdiff_oldmode },
+                       { "new mode ", gitdiff_newmode },
+                       { "deleted file mode ", gitdiff_delete },
+                       { "new file mode ", gitdiff_newfile },
+                       { "copy from ", gitdiff_copysrc },
+                       { "copy to ", gitdiff_copydst },
+                       { "rename old ", gitdiff_renamesrc },
+                       { "rename new ", gitdiff_renamedst },
+                       { "rename from ", gitdiff_renamesrc },
+                       { "rename to ", gitdiff_renamedst },
+                       { "similarity index ", gitdiff_similarity },
+                       { "dissimilarity index ", gitdiff_dissimilarity },
+                       { "index ", gitdiff_index },
+                       { "", gitdiff_unrecognized },
+               };
+               int i;
+
+               len = linelen(line, size);
+               if (!len || line[len-1] != '\n')
+                       break;
+               for (i = 0; i < ARRAY_SIZE(optable); i++) {
+                       const struct opentry *p = optable + i;
+                       int oplen = strlen(p->str);
+                       if (len < oplen || memcmp(p->str, line, oplen))
+                               continue;
+                       if (p->fn(line + oplen, patch) < 0)
+                               return offset;
+                       break;
+               }
+       }
+
+       return offset;
+}
+
+static int parse_num(const char *line, unsigned long *p)
+{
+       char *ptr;
+
+       if (!isdigit(*line))
+               return 0;
+       *p = strtoul(line, &ptr, 10);
+       return ptr - line;
+}
+
+static int parse_range(const char *line, int len, int offset, const char *expect,
+                       unsigned long *p1, unsigned long *p2)
+{
+       int digits, ex;
+
+       if (offset < 0 || offset >= len)
+               return -1;
+       line += offset;
+       len -= offset;
+
+       digits = parse_num(line, p1);
+       if (!digits)
+               return -1;
+
+       offset += digits;
+       line += digits;
+       len -= digits;
+
+       *p2 = 1;
+       if (*line == ',') {
+               digits = parse_num(line+1, p2);
+               if (!digits)
+                       return -1;
+
+               offset += digits+1;
+               line += digits+1;
+               len -= digits+1;
+       }
+
+       ex = strlen(expect);
+       if (ex > len)
+               return -1;
+       if (memcmp(line, expect, ex))
+               return -1;
+
+       return offset + ex;
+}
+
+/*
+ * Parse a unified diff fragment header of the
+ * form "@@ -a,b +c,d @@"
+ */
+static int parse_fragment_header(char *line, int len, struct fragment *fragment)
+{
+       int offset;
+
+       if (!len || line[len-1] != '\n')
+               return -1;
+
+       /* Figure out the number of lines in a fragment */
+       offset = parse_range(line, len, 4, " +", &fragment->oldpos, &fragment->oldlines);
+       offset = parse_range(line, len, offset, " @@", &fragment->newpos, &fragment->newlines);
+
+       return offset;
+}
+
+static int find_header(char *line, unsigned long size, int *hdrsize, struct patch *patch)
+{
+       unsigned long offset, len;
+
+       patch->is_toplevel_relative = 0;
+       patch->is_rename = patch->is_copy = 0;
+       patch->is_new = patch->is_delete = -1;
+       patch->old_mode = patch->new_mode = 0;
+       patch->old_name = patch->new_name = NULL;
+       for (offset = 0; size > 0; offset += len, size -= len, line += len, linenr++) {
+               unsigned long nextlen;
+
+               len = linelen(line, size);
+               if (!len)
+                       break;
+
+               /* Testing this early allows us to take a few shortcuts.. */
+               if (len < 6)
+                       continue;
+
+               /*
+                * Make sure we don't find any unconnected patch fragments.
+                * That's a sign that we didn't find a header, and that a
+                * patch has become corrupted/broken up.
+                */
+               if (!memcmp("@@ -", line, 4)) {
+                       struct fragment dummy;
+                       if (parse_fragment_header(line, len, &dummy) < 0)
+                               continue;
+                       die("patch fragment without header at line %d: %.*s",
+                           linenr, (int)len-1, line);
+               }
+
+               if (size < len + 6)
+                       break;
+
+               /*
+                * Git patch? It might not have a real patch, just a rename
+                * or mode change, so we handle that specially
+                */
+               if (!memcmp("diff --git ", line, 11)) {
+                       int git_hdr_len = parse_git_header(line, len, size, patch);
+                       if (git_hdr_len <= len)
+                               continue;
+                       if (!patch->old_name && !patch->new_name) {
+                               if (!patch->def_name)
+                                       die("git diff header lacks filename information (line %d)", linenr);
+                               patch->old_name = patch->new_name = patch->def_name;
+                       }
+                       patch->is_toplevel_relative = 1;
+                       *hdrsize = git_hdr_len;
+                       return offset;
+               }
+
+               /** --- followed by +++ ? */
+               if (memcmp("--- ", line,  4) || memcmp("+++ ", line + len, 4))
+                       continue;
+
+               /*
+                * We only accept unified patches, so we want it to
+                * at least have "@@ -a,b +c,d @@\n", which is 14 chars
+                * minimum
+                */
+               nextlen = linelen(line + len, size - len);
+               if (size < nextlen + 14 || memcmp("@@ -", line + len + nextlen, 4))
+                       continue;
+
+               /* Ok, we'll consider it a patch */
+               parse_traditional_patch(line, line+len, patch);
+               *hdrsize = len + nextlen;
+               linenr += 2;
+               return offset;
+       }
+       return -1;
+}
+
+static void check_whitespace(const char *line, int len)
+{
+       const char *err = "Adds trailing whitespace";
+       int seen_space = 0;
+       int i;
+
+       /*
+        * We know len is at least two, since we have a '+' and we
+        * checked that the last character was a '\n' before calling
+        * this function.  That is, an addition of an empty line would
+        * check the '+' here.  Sneaky...
+        */
+       if (isspace(line[len-2]))
+               goto error;
+
+       /*
+        * Make sure that there is no space followed by a tab in
+        * indentation.
+        */
+       err = "Space in indent is followed by a tab";
+       for (i = 1; i < len; i++) {
+               if (line[i] == '\t') {
+                       if (seen_space)
+                               goto error;
+               }
+               else if (line[i] == ' ')
+                       seen_space = 1;
+               else
+                       break;
+       }
+       return;
+
+ error:
+       whitespace_error++;
+       if (squelch_whitespace_errors &&
+           squelch_whitespace_errors < whitespace_error)
+               ;
+       else
+               fprintf(stderr, "%s.\n%s:%d:%.*s\n",
+                       err, patch_input_file, linenr, len-2, line+1);
+}
+
+
+/*
+ * Parse a unified diff. Note that this really needs to parse each
+ * fragment separately, since the only way to know the difference
+ * between a "---" that is part of a patch, and a "---" that starts
+ * the next patch is to look at the line counts..
+ */
+static int parse_fragment(char *line, unsigned long size, struct patch *patch, struct fragment *fragment)
+{
+       int added, deleted;
+       int len = linelen(line, size), offset;
+       unsigned long oldlines, newlines;
+       unsigned long leading, trailing;
+
+       offset = parse_fragment_header(line, len, fragment);
+       if (offset < 0)
+               return -1;
+       oldlines = fragment->oldlines;
+       newlines = fragment->newlines;
+       leading = 0;
+       trailing = 0;
+
+       /* Parse the thing.. */
+       line += len;
+       size -= len;
+       linenr++;
+       added = deleted = 0;
+       for (offset = len;
+            0 < size;
+            offset += len, size -= len, line += len, linenr++) {
+               if (!oldlines && !newlines)
+                       break;
+               len = linelen(line, size);
+               if (!len || line[len-1] != '\n')
+                       return -1;
+               switch (*line) {
+               default:
+                       return -1;
+               case '\n': /* newer GNU diff, an empty context line */
+               case ' ':
+                       oldlines--;
+                       newlines--;
+                       if (!deleted && !added)
+                               leading++;
+                       trailing++;
+                       break;
+               case '-':
+                       if (apply_in_reverse &&
+                                       new_whitespace != nowarn_whitespace)
+                               check_whitespace(line, len);
+                       deleted++;
+                       oldlines--;
+                       trailing = 0;
+                       break;
+               case '+':
+                       if (!apply_in_reverse &&
+                                       new_whitespace != nowarn_whitespace)
+                               check_whitespace(line, len);
+                       added++;
+                       newlines--;
+                       trailing = 0;
+                       break;
+
+                /* We allow "\ No newline at end of file". Depending
+                 * on locale settings when the patch was produced we
+                 * don't know what this line looks like. The only
+                 * thing we do know is that it begins with "\ ".
+                * Checking for 12 is just for sanity check -- any
+                * l10n of "\ No newline..." is at least that long.
+                */
+               case '\\':
+                       if (len < 12 || memcmp(line, "\\ ", 2))
+                               return -1;
+                       break;
+               }
+       }
+       if (oldlines || newlines)
+               return -1;
+       fragment->leading = leading;
+       fragment->trailing = trailing;
+
+       /* If a fragment ends with an incomplete line, we failed to include
+        * it in the above loop because we hit oldlines == newlines == 0
+        * before seeing it.
+        */
+       if (12 < size && !memcmp(line, "\\ ", 2))
+               offset += linelen(line, size);
+
+       patch->lines_added += added;
+       patch->lines_deleted += deleted;
+
+       if (0 < patch->is_new && oldlines)
+               return error("new file depends on old contents");
+       if (0 < patch->is_delete && newlines)
+               return error("deleted file still has contents");
+       return offset;
+}
+
+static int parse_single_patch(char *line, unsigned long size, struct patch *patch)
+{
+       unsigned long offset = 0;
+       unsigned long oldlines = 0, newlines = 0, context = 0;
+       struct fragment **fragp = &patch->fragments;
+
+       while (size > 4 && !memcmp(line, "@@ -", 4)) {
+               struct fragment *fragment;
+               int len;
+
+               fragment = xcalloc(1, sizeof(*fragment));
+               len = parse_fragment(line, size, patch, fragment);
+               if (len <= 0)
+                       die("corrupt patch at line %d", linenr);
+               fragment->patch = line;
+               fragment->size = len;
+               oldlines += fragment->oldlines;
+               newlines += fragment->newlines;
+               context += fragment->leading + fragment->trailing;
+
+               *fragp = fragment;
+               fragp = &fragment->next;
+
+               offset += len;
+               line += len;
+               size -= len;
+       }
+
+       /*
+        * If something was removed (i.e. we have old-lines) it cannot
+        * be creation, and if something was added it cannot be
+        * deletion.  However, the reverse is not true; --unified=0
+        * patches that only add are not necessarily creation even
+        * though they do not have any old lines, and ones that only
+        * delete are not necessarily deletion.
+        *
+        * Unfortunately, a real creation/deletion patch do _not_ have
+        * any context line by definition, so we cannot safely tell it
+        * apart with --unified=0 insanity.  At least if the patch has
+        * more than one hunk it is not creation or deletion.
+        */
+       if (patch->is_new < 0 &&
+           (oldlines || (patch->fragments && patch->fragments->next)))
+               patch->is_new = 0;
+       if (patch->is_delete < 0 &&
+           (newlines || (patch->fragments && patch->fragments->next)))
+               patch->is_delete = 0;
+       if (!unidiff_zero || context) {
+               /* If the user says the patch is not generated with
+                * --unified=0, or if we have seen context lines,
+                * then not having oldlines means the patch is creation,
+                * and not having newlines means the patch is deletion.
+                */
+               if (patch->is_new < 0 && !oldlines) {
+                       patch->is_new = 1;
+                       patch->old_name = NULL;
+               }
+               if (patch->is_delete < 0 && !newlines) {
+                       patch->is_delete = 1;
+                       patch->new_name = NULL;
+               }
+       }
+
+       if (0 < patch->is_new && oldlines)
+               die("new file %s depends on old contents", patch->new_name);
+       if (0 < patch->is_delete && newlines)
+               die("deleted file %s still has contents", patch->old_name);
+       if (!patch->is_delete && !newlines && context)
+               fprintf(stderr, "** warning: file %s becomes empty but "
+                       "is not deleted\n", patch->new_name);
+
+       return offset;
+}
+
+static inline int metadata_changes(struct patch *patch)
+{
+       return  patch->is_rename > 0 ||
+               patch->is_copy > 0 ||
+               patch->is_new > 0 ||
+               patch->is_delete ||
+               (patch->old_mode && patch->new_mode &&
+                patch->old_mode != patch->new_mode);
+}
+
+static char *inflate_it(const void *data, unsigned long size,
+                       unsigned long inflated_size)
+{
+       z_stream stream;
+       void *out;
+       int st;
+
+       memset(&stream, 0, sizeof(stream));
+
+       stream.next_in = (unsigned char *)data;
+       stream.avail_in = size;
+       stream.next_out = out = xmalloc(inflated_size);
+       stream.avail_out = inflated_size;
+       inflateInit(&stream);
+       st = inflate(&stream, Z_FINISH);
+       if ((st != Z_STREAM_END) || stream.total_out != inflated_size) {
+               free(out);
+               return NULL;
+       }
+       return out;
+}
+
+static struct fragment *parse_binary_hunk(char **buf_p,
+                                         unsigned long *sz_p,
+                                         int *status_p,
+                                         int *used_p)
+{
+       /* Expect a line that begins with binary patch method ("literal"
+        * or "delta"), followed by the length of data before deflating.
+        * a sequence of 'length-byte' followed by base-85 encoded data
+        * should follow, terminated by a newline.
+        *
+        * Each 5-byte sequence of base-85 encodes up to 4 bytes,
+        * and we would limit the patch line to 66 characters,
+        * so one line can fit up to 13 groups that would decode
+        * to 52 bytes max.  The length byte 'A'-'Z' corresponds
+        * to 1-26 bytes, and 'a'-'z' corresponds to 27-52 bytes.
+        */
+       int llen, used;
+       unsigned long size = *sz_p;
+       char *buffer = *buf_p;
+       int patch_method;
+       unsigned long origlen;
+       char *data = NULL;
+       int hunk_size = 0;
+       struct fragment *frag;
+
+       llen = linelen(buffer, size);
+       used = llen;
+
+       *status_p = 0;
+
+       if (!prefixcmp(buffer, "delta ")) {
+               patch_method = BINARY_DELTA_DEFLATED;
+               origlen = strtoul(buffer + 6, NULL, 10);
+       }
+       else if (!prefixcmp(buffer, "literal ")) {
+               patch_method = BINARY_LITERAL_DEFLATED;
+               origlen = strtoul(buffer + 8, NULL, 10);
+       }
+       else
+               return NULL;
+
+       linenr++;
+       buffer += llen;
+       while (1) {
+               int byte_length, max_byte_length, newsize;
+               llen = linelen(buffer, size);
+               used += llen;
+               linenr++;
+               if (llen == 1) {
+                       /* consume the blank line */
+                       buffer++;
+                       size--;
+                       break;
+               }
+               /* Minimum line is "A00000\n" which is 7-byte long,
+                * and the line length must be multiple of 5 plus 2.
+                */
+               if ((llen < 7) || (llen-2) % 5)
+                       goto corrupt;
+               max_byte_length = (llen - 2) / 5 * 4;
+               byte_length = *buffer;
+               if ('A' <= byte_length && byte_length <= 'Z')
+                       byte_length = byte_length - 'A' + 1;
+               else if ('a' <= byte_length && byte_length <= 'z')
+                       byte_length = byte_length - 'a' + 27;
+               else
+                       goto corrupt;
+               /* if the input length was not multiple of 4, we would
+                * have filler at the end but the filler should never
+                * exceed 3 bytes
+                */
+               if (max_byte_length < byte_length ||
+                   byte_length <= max_byte_length - 4)
+                       goto corrupt;
+               newsize = hunk_size + byte_length;
+               data = xrealloc(data, newsize);
+               if (decode_85(data + hunk_size, buffer + 1, byte_length))
+                       goto corrupt;
+               hunk_size = newsize;
+               buffer += llen;
+               size -= llen;
+       }
+
+       frag = xcalloc(1, sizeof(*frag));
+       frag->patch = inflate_it(data, hunk_size, origlen);
+       if (!frag->patch)
+               goto corrupt;
+       free(data);
+       frag->size = origlen;
+       *buf_p = buffer;
+       *sz_p = size;
+       *used_p = used;
+       frag->binary_patch_method = patch_method;
+       return frag;
+
+ corrupt:
+       free(data);
+       *status_p = -1;
+       error("corrupt binary patch at line %d: %.*s",
+             linenr-1, llen-1, buffer);
+       return NULL;
+}
+
+static int parse_binary(char *buffer, unsigned long size, struct patch *patch)
+{
+       /* We have read "GIT binary patch\n"; what follows is a line
+        * that says the patch method (currently, either "literal" or
+        * "delta") and the length of data before deflating; a
+        * sequence of 'length-byte' followed by base-85 encoded data
+        * follows.
+        *
+        * When a binary patch is reversible, there is another binary
+        * hunk in the same format, starting with patch method (either
+        * "literal" or "delta") with the length of data, and a sequence
+        * of length-byte + base-85 encoded data, terminated with another
+        * empty line.  This data, when applied to the postimage, produces
+        * the preimage.
+        */
+       struct fragment *forward;
+       struct fragment *reverse;
+       int status;
+       int used, used_1;
+
+       forward = parse_binary_hunk(&buffer, &size, &status, &used);
+       if (!forward && !status)
+               /* there has to be one hunk (forward hunk) */
+               return error("unrecognized binary patch at line %d", linenr-1);
+       if (status)
+               /* otherwise we already gave an error message */
+               return status;
+
+       reverse = parse_binary_hunk(&buffer, &size, &status, &used_1);
+       if (reverse)
+               used += used_1;
+       else if (status) {
+               /* not having reverse hunk is not an error, but having
+                * a corrupt reverse hunk is.
+                */
+               free((void*) forward->patch);
+               free(forward);
+               return status;
+       }
+       forward->next = reverse;
+       patch->fragments = forward;
+       patch->is_binary = 1;
+       return used;
+}
+
+static int parse_chunk(char *buffer, unsigned long size, struct patch *patch)
+{
+       int hdrsize, patchsize;
+       int offset = find_header(buffer, size, &hdrsize, patch);
+
+       if (offset < 0)
+               return offset;
+
+       patchsize = parse_single_patch(buffer + offset + hdrsize, size - offset - hdrsize, patch);
+
+       if (!patchsize) {
+               static const char *binhdr[] = {
+                       "Binary files ",
+                       "Files ",
+                       NULL,
+               };
+               static const char git_binary[] = "GIT binary patch\n";
+               int i;
+               int hd = hdrsize + offset;
+               unsigned long llen = linelen(buffer + hd, size - hd);
+
+               if (llen == sizeof(git_binary) - 1 &&
+                   !memcmp(git_binary, buffer + hd, llen)) {
+                       int used;
+                       linenr++;
+                       used = parse_binary(buffer + hd + llen,
+                                           size - hd - llen, patch);
+                       if (used)
+                               patchsize = used + llen;
+                       else
+                               patchsize = 0;
+               }
+               else if (!memcmp(" differ\n", buffer + hd + llen - 8, 8)) {
+                       for (i = 0; binhdr[i]; i++) {
+                               int len = strlen(binhdr[i]);
+                               if (len < size - hd &&
+                                   !memcmp(binhdr[i], buffer + hd, len)) {
+                                       linenr++;
+                                       patch->is_binary = 1;
+                                       patchsize = llen;
+                                       break;
+                               }
+                       }
+               }
+
+               /* Empty patch cannot be applied if it is a text patch
+                * without metadata change.  A binary patch appears
+                * empty to us here.
+                */
+               if ((apply || check) &&
+                   (!patch->is_binary && !metadata_changes(patch)))
+                       die("patch with only garbage at line %d", linenr);
+       }
+
+       return offset + hdrsize + patchsize;
+}
+
+#define swap(a,b) myswap((a),(b),sizeof(a))
+
+#define myswap(a, b, size) do {                \
+       unsigned char mytmp[size];      \
+       memcpy(mytmp, &a, size);                \
+       memcpy(&a, &b, size);           \
+       memcpy(&b, mytmp, size);                \
+} while (0)
+
+static void reverse_patches(struct patch *p)
+{
+       for (; p; p = p->next) {
+               struct fragment *frag = p->fragments;
+
+               swap(p->new_name, p->old_name);
+               swap(p->new_mode, p->old_mode);
+               swap(p->is_new, p->is_delete);
+               swap(p->lines_added, p->lines_deleted);
+               swap(p->old_sha1_prefix, p->new_sha1_prefix);
+
+               for (; frag; frag = frag->next) {
+                       swap(frag->newpos, frag->oldpos);
+                       swap(frag->newlines, frag->oldlines);
+               }
+       }
+}
+
+static const char pluses[] = "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++";
+static const char minuses[]= "----------------------------------------------------------------------";
+
+static void show_stats(struct patch *patch)
+{
+       struct strbuf qname;
+       char *cp = patch->new_name ? patch->new_name : patch->old_name;
+       int max, add, del;
+
+       strbuf_init(&qname, 0);
+       quote_c_style(cp, &qname, NULL, 0);
+
+       /*
+        * "scale" the filename
+        */
+       max = max_len;
+       if (max > 50)
+               max = 50;
+
+       if (qname.len > max) {
+               cp = strchr(qname.buf + qname.len + 3 - max, '/');
+               if (!cp)
+                       cp = qname.buf + qname.len + 3 - max;
+               strbuf_splice(&qname, 0, cp - qname.buf, "...", 3);
+       }
+
+       if (patch->is_binary) {
+               printf(" %-*s |  Bin\n", max, qname.buf);
+               strbuf_release(&qname);
+               return;
+       }
+
+       printf(" %-*s |", max, qname.buf);
+       strbuf_release(&qname);
+
+       /*
+        * scale the add/delete
+        */
+       max = max + max_change > 70 ? 70 - max : max_change;
+       add = patch->lines_added;
+       del = patch->lines_deleted;
+
+       if (max_change > 0) {
+               int total = ((add + del) * max + max_change / 2) / max_change;
+               add = (add * max + max_change / 2) / max_change;
+               del = total - add;
+       }
+       printf("%5d %.*s%.*s\n", patch->lines_added + patch->lines_deleted,
+               add, pluses, del, minuses);
+}
+
+static int read_old_data(struct stat *st, const char *path, struct strbuf *buf)
+{
+       switch (st->st_mode & S_IFMT) {
+       case S_IFLNK:
+               strbuf_grow(buf, st->st_size);
+               if (readlink(path, buf->buf, st->st_size) != st->st_size)
+                       return -1;
+               strbuf_setlen(buf, st->st_size);
+               return 0;
+       case S_IFREG:
+               if (strbuf_read_file(buf, path, st->st_size) != st->st_size)
+                       return error("unable to open or read %s", path);
+               convert_to_git(path, buf->buf, buf->len, buf);
+               return 0;
+       default:
+               return -1;
+       }
+}
+
+static int find_offset(const char *buf, unsigned long size, const char *fragment, unsigned long fragsize, int line, int *lines)
+{
+       int i;
+       unsigned long start, backwards, forwards;
+
+       if (fragsize > size)
+               return -1;
+
+       start = 0;
+       if (line > 1) {
+               unsigned long offset = 0;
+               i = line-1;
+               while (offset + fragsize <= size) {
+                       if (buf[offset++] == '\n') {
+                               start = offset;
+                               if (!--i)
+                                       break;
+                       }
+               }
+       }
+
+       /* Exact line number? */
+       if ((start + fragsize <= size) &&
+           !memcmp(buf + start, fragment, fragsize))
+               return start;
+
+       /*
+        * There's probably some smart way to do this, but I'll leave
+        * that to the smart and beautiful people. I'm simple and stupid.
+        */
+       backwards = start;
+       forwards = start;
+       for (i = 0; ; i++) {
+               unsigned long try;
+               int n;
+
+               /* "backward" */
+               if (i & 1) {
+                       if (!backwards) {
+                               if (forwards + fragsize > size)
+                                       break;
+                               continue;
+                       }
+                       do {
+                               --backwards;
+                       } while (backwards && buf[backwards-1] != '\n');
+                       try = backwards;
+               } else {
+                       while (forwards + fragsize <= size) {
+                               if (buf[forwards++] == '\n')
+                                       break;
+                       }
+                       try = forwards;
+               }
+
+               if (try + fragsize > size)
+                       continue;
+               if (memcmp(buf + try, fragment, fragsize))
+                       continue;
+               n = (i >> 1)+1;
+               if (i & 1)
+                       n = -n;
+               *lines = n;
+               return try;
+       }
+
+       /*
+        * We should start searching forward and backward.
+        */
+       return -1;
+}
+
+static void remove_first_line(const char **rbuf, int *rsize)
+{
+       const char *buf = *rbuf;
+       int size = *rsize;
+       unsigned long offset;
+       offset = 0;
+       while (offset <= size) {
+               if (buf[offset++] == '\n')
+                       break;
+       }
+       *rsize = size - offset;
+       *rbuf = buf + offset;
+}
+
+static void remove_last_line(const char **rbuf, int *rsize)
+{
+       const char *buf = *rbuf;
+       int size = *rsize;
+       unsigned long offset;
+       offset = size - 1;
+       while (offset > 0) {
+               if (buf[--offset] == '\n')
+                       break;
+       }
+       *rsize = offset + 1;
+}
+
+static int apply_line(char *output, const char *patch, int plen)
+{
+       /* plen is number of bytes to be copied from patch,
+        * starting at patch+1 (patch[0] is '+').  Typically
+        * patch[plen] is '\n', unless this is the incomplete
+        * last line.
+        */
+       int i;
+       int add_nl_to_tail = 0;
+       int fixed = 0;
+       int last_tab_in_indent = -1;
+       int last_space_in_indent = -1;
+       int need_fix_leading_space = 0;
+       char *buf;
+
+       if ((new_whitespace != strip_whitespace) || !whitespace_error ||
+           *patch != '+') {
+               memcpy(output, patch + 1, plen);
+               return plen;
+       }
+
+       if (1 < plen && isspace(patch[plen-1])) {
+               if (patch[plen] == '\n')
+                       add_nl_to_tail = 1;
+               plen--;
+               while (0 < plen && isspace(patch[plen]))
+                       plen--;
+               fixed = 1;
+       }
+
+       for (i = 1; i < plen; i++) {
+               char ch = patch[i];
+               if (ch == '\t') {
+                       last_tab_in_indent = i;
+                       if (0 <= last_space_in_indent)
+                               need_fix_leading_space = 1;
+               }
+               else if (ch == ' ')
+                       last_space_in_indent = i;
+               else
+                       break;
+       }
+
+       buf = output;
+       if (need_fix_leading_space) {
+               int consecutive_spaces = 0;
+               /* between patch[1..last_tab_in_indent] strip the
+                * funny spaces, updating them to tab as needed.
+                */
+               for (i = 1; i < last_tab_in_indent; i++, plen--) {
+                       char ch = patch[i];
+                       if (ch != ' ') {
+                               consecutive_spaces = 0;
+                               *output++ = ch;
+                       } else {
+                               consecutive_spaces++;
+                               if (consecutive_spaces == 8) {
+                                       *output++ = '\t';
+                                       consecutive_spaces = 0;
+                               }
+                       }
+               }
+               fixed = 1;
+               i = last_tab_in_indent;
+       }
+       else
+               i = 1;
+
+       memcpy(output, patch + i, plen);
+       if (add_nl_to_tail)
+               output[plen++] = '\n';
+       if (fixed)
+               applied_after_fixing_ws++;
+       return output + plen - buf;
+}
+
+static int apply_one_fragment(struct strbuf *buf, struct fragment *frag, int inaccurate_eof)
+{
+       int match_beginning, match_end;
+       const char *patch = frag->patch;
+       int offset, size = frag->size;
+       char *old = xmalloc(size);
+       char *new = xmalloc(size);
+       const char *oldlines, *newlines;
+       int oldsize = 0, newsize = 0;
+       int new_blank_lines_at_end = 0;
+       unsigned long leading, trailing;
+       int pos, lines;
+
+       while (size > 0) {
+               char first;
+               int len = linelen(patch, size);
+               int plen;
+               int added_blank_line = 0;
+
+               if (!len)
+                       break;
+
+               /*
+                * "plen" is how much of the line we should use for
+                * the actual patch data. Normally we just remove the
+                * first character on the line, but if the line is
+                * followed by "\ No newline", then we also remove the
+                * last one (which is the newline, of course).
+                */
+               plen = len-1;
+               if (len < size && patch[len] == '\\')
+                       plen--;
+               first = *patch;
+               if (apply_in_reverse) {
+                       if (first == '-')
+                               first = '+';
+                       else if (first == '+')
+                               first = '-';
+               }
+
+               switch (first) {
+               case '\n':
+                       /* Newer GNU diff, empty context line */
+                       if (plen < 0)
+                               /* ... followed by '\No newline'; nothing */
+                               break;
+                       old[oldsize++] = '\n';
+                       new[newsize++] = '\n';
+                       break;
+               case ' ':
+               case '-':
+                       memcpy(old + oldsize, patch + 1, plen);
+                       oldsize += plen;
+                       if (first == '-')
+                               break;
+               /* Fall-through for ' ' */
+               case '+':
+                       if (first != '+' || !no_add) {
+                               int added = apply_line(new + newsize, patch,
+                                                      plen);
+                               newsize += added;
+                               if (first == '+' &&
+                                   added == 1 && new[newsize-1] == '\n')
+                                       added_blank_line = 1;
+                       }
+                       break;
+               case '@': case '\\':
+                       /* Ignore it, we already handled it */
+                       break;
+               default:
+                       if (apply_verbosely)
+                               error("invalid start of line: '%c'", first);
+                       return -1;
+               }
+               if (added_blank_line)
+                       new_blank_lines_at_end++;
+               else
+                       new_blank_lines_at_end = 0;
+               patch += len;
+               size -= len;
+       }
+
+       if (inaccurate_eof && oldsize > 0 && old[oldsize - 1] == '\n' &&
+                       newsize > 0 && new[newsize - 1] == '\n') {
+               oldsize--;
+               newsize--;
+       }
+
+       oldlines = old;
+       newlines = new;
+       leading = frag->leading;
+       trailing = frag->trailing;
+
+       /*
+        * If we don't have any leading/trailing data in the patch,
+        * we want it to match at the beginning/end of the file.
+        *
+        * But that would break if the patch is generated with
+        * --unified=0; sane people wouldn't do that to cause us
+        * trouble, but we try to please not so sane ones as well.
+        */
+       if (unidiff_zero) {
+               match_beginning = (!leading && !frag->oldpos);
+               match_end = 0;
+       }
+       else {
+               match_beginning = !leading && (frag->oldpos == 1);
+               match_end = !trailing;
+       }
+
+       lines = 0;
+       pos = frag->newpos;
+       for (;;) {
+               offset = find_offset(buf->buf, buf->len,
+                                    oldlines, oldsize, pos, &lines);
+               if (match_end && offset + oldsize != buf->len)
+                       offset = -1;
+               if (match_beginning && offset)
+                       offset = -1;
+               if (offset >= 0) {
+                       if (new_whitespace == strip_whitespace &&
+                           (buf->len - oldsize - offset == 0)) /* end of file? */
+                               newsize -= new_blank_lines_at_end;
+
+                       /* Warn if it was necessary to reduce the number
+                        * of context lines.
+                        */
+                       if ((leading != frag->leading) ||
+                           (trailing != frag->trailing))
+                               fprintf(stderr, "Context reduced to (%ld/%ld)"
+                                       " to apply fragment at %d\n",
+                                       leading, trailing, pos + lines);
+
+                       strbuf_splice(buf, offset, oldsize, newlines, newsize);
+                       offset = 0;
+                       break;
+               }
+
+               /* Am I at my context limits? */
+               if ((leading <= p_context) && (trailing <= p_context))
+                       break;
+               if (match_beginning || match_end) {
+                       match_beginning = match_end = 0;
+                       continue;
+               }
+               /* Reduce the number of context lines
+                * Reduce both leading and trailing if they are equal
+                * otherwise just reduce the larger context.
+                */
+               if (leading >= trailing) {
+                       remove_first_line(&oldlines, &oldsize);
+                       remove_first_line(&newlines, &newsize);
+                       pos--;
+                       leading--;
+               }
+               if (trailing > leading) {
+                       remove_last_line(&oldlines, &oldsize);
+                       remove_last_line(&newlines, &newsize);
+                       trailing--;
+               }
+       }
+
+       if (offset && apply_verbosely)
+               error("while searching for:\n%.*s", oldsize, oldlines);
+
+       free(old);
+       free(new);
+       return offset;
+}
+
+static int apply_binary_fragment(struct strbuf *buf, struct patch *patch)
+{
+       struct fragment *fragment = patch->fragments;
+       unsigned long len;
+       void *dst;
+
+       /* Binary patch is irreversible without the optional second hunk */
+       if (apply_in_reverse) {
+               if (!fragment->next)
+                       return error("cannot reverse-apply a binary patch "
+                                    "without the reverse hunk to '%s'",
+                                    patch->new_name
+                                    ? patch->new_name : patch->old_name);
+               fragment = fragment->next;
+       }
+       switch (fragment->binary_patch_method) {
+       case BINARY_DELTA_DEFLATED:
+               dst = patch_delta(buf->buf, buf->len, fragment->patch,
+                                 fragment->size, &len);
+               if (!dst)
+                       return -1;
+               /* XXX patch_delta NUL-terminates */
+               strbuf_attach(buf, dst, len, len + 1);
+               return 0;
+       case BINARY_LITERAL_DEFLATED:
+               strbuf_reset(buf);
+               strbuf_add(buf, fragment->patch, fragment->size);
+               return 0;
+       }
+       return -1;
+}
+
+static int apply_binary(struct strbuf *buf, struct patch *patch)
+{
+       const char *name = patch->old_name ? patch->old_name : patch->new_name;
+       unsigned char sha1[20];
+
+       /* For safety, we require patch index line to contain
+        * full 40-byte textual SHA1 for old and new, at least for now.
+        */
+       if (strlen(patch->old_sha1_prefix) != 40 ||
+           strlen(patch->new_sha1_prefix) != 40 ||
+           get_sha1_hex(patch->old_sha1_prefix, sha1) ||
+           get_sha1_hex(patch->new_sha1_prefix, sha1))
+               return error("cannot apply binary patch to '%s' "
+                            "without full index line", name);
+
+       if (patch->old_name) {
+               /* See if the old one matches what the patch
+                * applies to.
+                */
+               hash_sha1_file(buf->buf, buf->len, blob_type, sha1);
+               if (strcmp(sha1_to_hex(sha1), patch->old_sha1_prefix))
+                       return error("the patch applies to '%s' (%s), "
+                                    "which does not match the "
+                                    "current contents.",
+                                    name, sha1_to_hex(sha1));
+       }
+       else {
+               /* Otherwise, the old one must be empty. */
+               if (buf->len)
+                       return error("the patch applies to an empty "
+                                    "'%s' but it is not empty", name);
+       }
+
+       get_sha1_hex(patch->new_sha1_prefix, sha1);
+       if (is_null_sha1(sha1)) {
+               strbuf_release(buf);
+               return 0; /* deletion patch */
+       }
+
+       if (has_sha1_file(sha1)) {
+               /* We already have the postimage */
+               enum object_type type;
+               unsigned long size;
+               char *result;
+
+               result = read_sha1_file(sha1, &type, &size);
+               if (!result)
+                       return error("the necessary postimage %s for "
+                                    "'%s' cannot be read",
+                                    patch->new_sha1_prefix, name);
+               /* XXX read_sha1_file NUL-terminates */
+               strbuf_attach(buf, result, size, size + 1);
+       } else {
+               /* We have verified buf matches the preimage;
+                * apply the patch data to it, which is stored
+                * in the patch->fragments->{patch,size}.
+                */
+               if (apply_binary_fragment(buf, patch))
+                       return error("binary patch does not apply to '%s'",
+                                    name);
+
+               /* verify that the result matches */
+               hash_sha1_file(buf->buf, buf->len, blob_type, sha1);
+               if (strcmp(sha1_to_hex(sha1), patch->new_sha1_prefix))
+                       return error("binary patch to '%s' creates incorrect result (expecting %s, got %s)",
+                               name, patch->new_sha1_prefix, sha1_to_hex(sha1));
+       }
+
+       return 0;
+}
+
+static int apply_fragments(struct strbuf *buf, struct patch *patch)
+{
+       struct fragment *frag = patch->fragments;
+       const char *name = patch->old_name ? patch->old_name : patch->new_name;
+
+       if (patch->is_binary)
+               return apply_binary(buf, patch);
+
+       while (frag) {
+               if (apply_one_fragment(buf, frag, patch->inaccurate_eof)) {
+                       error("patch failed: %s:%ld", name, frag->oldpos);
+                       if (!apply_with_reject)
+                               return -1;
+                       frag->rejected = 1;
+               }
+               frag = frag->next;
+       }
+       return 0;
+}
+
+static int read_file_or_gitlink(struct cache_entry *ce, struct strbuf *buf)
+{
+       if (!ce)
+               return 0;
+
+       if (S_ISGITLINK(ntohl(ce->ce_mode))) {
+               strbuf_grow(buf, 100);
+               strbuf_addf(buf, "Subproject commit %s\n", sha1_to_hex(ce->sha1));
+       } else {
+               enum object_type type;
+               unsigned long sz;
+               char *result;
+
+               result = read_sha1_file(ce->sha1, &type, &sz);
+               if (!result)
+                       return -1;
+               /* XXX read_sha1_file NUL-terminates */
+               strbuf_attach(buf, result, sz, sz + 1);
+       }
+       return 0;
+}
+
+static int apply_data(struct patch *patch, struct stat *st, struct cache_entry *ce)
+{
+       struct strbuf buf;
+
+       strbuf_init(&buf, 0);
+       if (cached) {
+               if (read_file_or_gitlink(ce, &buf))
+                       return error("read of %s failed", patch->old_name);
+       } else if (patch->old_name) {
+               if (S_ISGITLINK(patch->old_mode)) {
+                       if (ce) {
+                               read_file_or_gitlink(ce, &buf);
+                       } else {
+                               /*
+                                * There is no way to apply subproject
+                                * patch without looking at the index.
+                                */
+                               patch->fragments = NULL;
+                       }
+               } else {
+                       if (read_old_data(st, patch->old_name, &buf))
+                               return error("read of %s failed", patch->old_name);
+               }
+       }
+
+       if (apply_fragments(&buf, patch) < 0)
+               return -1; /* note with --reject this succeeds. */
+       patch->result = strbuf_detach(&buf, &patch->resultsize);
+
+       if (0 < patch->is_delete && patch->resultsize)
+               return error("removal patch leaves file contents");
+
+       return 0;
+}
+
+static int check_to_create_blob(const char *new_name, int ok_if_exists)
+{
+       struct stat nst;
+       if (!lstat(new_name, &nst)) {
+               if (S_ISDIR(nst.st_mode) || ok_if_exists)
+                       return 0;
+               /*
+                * A leading component of new_name might be a symlink
+                * that is going to be removed with this patch, but
+                * still pointing at somewhere that has the path.
+                * In such a case, path "new_name" does not exist as
+                * far as git is concerned.
+                */
+               if (has_symlink_leading_path(new_name, NULL))
+                       return 0;
+
+               return error("%s: already exists in working directory", new_name);
+       }
+       else if ((errno != ENOENT) && (errno != ENOTDIR))
+               return error("%s: %s", new_name, strerror(errno));
+       return 0;
+}
+
+static int verify_index_match(struct cache_entry *ce, struct stat *st)
+{
+       if (S_ISGITLINK(ntohl(ce->ce_mode))) {
+               if (!S_ISDIR(st->st_mode))
+                       return -1;
+               return 0;
+       }
+       return ce_match_stat(ce, st, CE_MATCH_IGNORE_VALID);
+}
+
+static int check_patch(struct patch *patch, struct patch *prev_patch)
+{
+       struct stat st;
+       const char *old_name = patch->old_name;
+       const char *new_name = patch->new_name;
+       const char *name = old_name ? old_name : new_name;
+       struct cache_entry *ce = NULL;
+       int ok_if_exists;
+
+       patch->rejected = 1; /* we will drop this after we succeed */
+
+       /*
+        * Make sure that we do not have local modifications from the
+        * index when we are looking at the index.  Also make sure
+        * we have the preimage file to be patched in the work tree,
+        * unless --cached, which tells git to apply only in the index.
+        */
+       if (old_name) {
+               int stat_ret = 0;
+               unsigned st_mode = 0;
+
+               if (!cached)
+                       stat_ret = lstat(old_name, &st);
+               if (check_index) {
+                       int pos = cache_name_pos(old_name, strlen(old_name));
+                       if (pos < 0)
+                               return error("%s: does not exist in index",
+                                            old_name);
+                       ce = active_cache[pos];
+                       if (stat_ret < 0) {
+                               struct checkout costate;
+                               if (errno != ENOENT)
+                                       return error("%s: %s", old_name,
+                                                    strerror(errno));
+                               /* checkout */
+                               costate.base_dir = "";
+                               costate.base_dir_len = 0;
+                               costate.force = 0;
+                               costate.quiet = 0;
+                               costate.not_new = 0;
+                               costate.refresh_cache = 1;
+                               if (checkout_entry(ce,
+                                                  &costate,
+                                                  NULL) ||
+                                   lstat(old_name, &st))
+                                       return -1;
+                       }
+                       if (!cached && verify_index_match(ce, &st))
+                               return error("%s: does not match index",
+                                            old_name);
+                       if (cached)
+                               st_mode = ntohl(ce->ce_mode);
+               } else if (stat_ret < 0)
+                       return error("%s: %s", old_name, strerror(errno));
+
+               if (!cached)
+                       st_mode = ntohl(ce_mode_from_stat(ce, st.st_mode));
+
+               if (patch->is_new < 0)
+                       patch->is_new = 0;
+               if (!patch->old_mode)
+                       patch->old_mode = st_mode;
+               if ((st_mode ^ patch->old_mode) & S_IFMT)
+                       return error("%s: wrong type", old_name);
+               if (st_mode != patch->old_mode)
+                       fprintf(stderr, "warning: %s has type %o, expected %o\n",
+                               old_name, st_mode, patch->old_mode);
+       }
+
+       if (new_name && prev_patch && 0 < prev_patch->is_delete &&
+           !strcmp(prev_patch->old_name, new_name))
+               /* A type-change diff is always split into a patch to
+                * delete old, immediately followed by a patch to
+                * create new (see diff.c::run_diff()); in such a case
+                * it is Ok that the entry to be deleted by the
+                * previous patch is still in the working tree and in
+                * the index.
+                */
+               ok_if_exists = 1;
+       else
+               ok_if_exists = 0;
+
+       if (new_name &&
+           ((0 < patch->is_new) | (0 < patch->is_rename) | patch->is_copy)) {
+               if (check_index &&
+                   cache_name_pos(new_name, strlen(new_name)) >= 0 &&
+                   !ok_if_exists)
+                       return error("%s: already exists in index", new_name);
+               if (!cached) {
+                       int err = check_to_create_blob(new_name, ok_if_exists);
+                       if (err)
+                               return err;
+               }
+               if (!patch->new_mode) {
+                       if (0 < patch->is_new)
+                               patch->new_mode = S_IFREG | 0644;
+                       else
+                               patch->new_mode = patch->old_mode;
+               }
+       }
+
+       if (new_name && old_name) {
+               int same = !strcmp(old_name, new_name);
+               if (!patch->new_mode)
+                       patch->new_mode = patch->old_mode;
+               if ((patch->old_mode ^ patch->new_mode) & S_IFMT)
+                       return error("new mode (%o) of %s does not match old mode (%o)%s%s",
+                               patch->new_mode, new_name, patch->old_mode,
+                               same ? "" : " of ", same ? "" : old_name);
+       }
+
+       if (apply_data(patch, &st, ce) < 0)
+               return error("%s: patch does not apply", name);
+       patch->rejected = 0;
+       return 0;
+}
+
+static int check_patch_list(struct patch *patch)
+{
+       struct patch *prev_patch = NULL;
+       int err = 0;
+
+       for (prev_patch = NULL; patch ; patch = patch->next) {
+               if (apply_verbosely)
+                       say_patch_name(stderr,
+                                      "Checking patch ", patch, "...\n");
+               err |= check_patch(patch, prev_patch);
+               prev_patch = patch;
+       }
+       return err;
+}
+
+/* This function tries to read the sha1 from the current index */
+static int get_current_sha1(const char *path, unsigned char *sha1)
+{
+       int pos;
+
+       if (read_cache() < 0)
+               return -1;
+       pos = cache_name_pos(path, strlen(path));
+       if (pos < 0)
+               return -1;
+       hashcpy(sha1, active_cache[pos]->sha1);
+       return 0;
+}
+
+/* Build an index that contains the just the files needed for a 3way merge */
+static void build_fake_ancestor(struct patch *list, const char *filename)
+{
+       struct patch *patch;
+       struct index_state result = { 0 };
+       int fd;
+
+       /* Once we start supporting the reverse patch, it may be
+        * worth showing the new sha1 prefix, but until then...
+        */
+       for (patch = list; patch; patch = patch->next) {
+               const unsigned char *sha1_ptr;
+               unsigned char sha1[20];
+               struct cache_entry *ce;
+               const char *name;
+
+               name = patch->old_name ? patch->old_name : patch->new_name;
+               if (0 < patch->is_new)
+                       continue;
+               else if (get_sha1(patch->old_sha1_prefix, sha1))
+                       /* git diff has no index line for mode/type changes */
+                       if (!patch->lines_added && !patch->lines_deleted) {
+                               if (get_current_sha1(patch->new_name, sha1) ||
+                                   get_current_sha1(patch->old_name, sha1))
+                                       die("mode change for %s, which is not "
+                                               "in current HEAD", name);
+                               sha1_ptr = sha1;
+                       } else
+                               die("sha1 information is lacking or useless "
+                                       "(%s).", name);
+               else
+                       sha1_ptr = sha1;
+
+               ce = make_cache_entry(patch->old_mode, sha1_ptr, name, 0, 0);
+               if (add_index_entry(&result, ce, ADD_CACHE_OK_TO_ADD))
+                       die ("Could not add %s to temporary index", name);
+       }
+
+       fd = open(filename, O_WRONLY | O_CREAT, 0666);
+       if (fd < 0 || write_index(&result, fd) || close(fd))
+               die ("Could not write temporary index to %s", filename);
+
+       discard_index(&result);
+}
+
+static void stat_patch_list(struct patch *patch)
+{
+       int files, adds, dels;
+
+       for (files = adds = dels = 0 ; patch ; patch = patch->next) {
+               files++;
+               adds += patch->lines_added;
+               dels += patch->lines_deleted;
+               show_stats(patch);
+       }
+
+       printf(" %d files changed, %d insertions(+), %d deletions(-)\n", files, adds, dels);
+}
+
+static void numstat_patch_list(struct patch *patch)
+{
+       for ( ; patch; patch = patch->next) {
+               const char *name;
+               name = patch->new_name ? patch->new_name : patch->old_name;
+               if (patch->is_binary)
+                       printf("-\t-\t");
+               else
+                       printf("%d\t%d\t", patch->lines_added, patch->lines_deleted);
+               write_name_quoted(name, stdout, line_termination);
+       }
+}
+
+static void show_file_mode_name(const char *newdelete, unsigned int mode, const char *name)
+{
+       if (mode)
+               printf(" %s mode %06o %s\n", newdelete, mode, name);
+       else
+               printf(" %s %s\n", newdelete, name);
+}
+
+static void show_mode_change(struct patch *p, int show_name)
+{
+       if (p->old_mode && p->new_mode && p->old_mode != p->new_mode) {
+               if (show_name)
+                       printf(" mode change %06o => %06o %s\n",
+                              p->old_mode, p->new_mode, p->new_name);
+               else
+                       printf(" mode change %06o => %06o\n",
+                              p->old_mode, p->new_mode);
+       }
+}
+
+static void show_rename_copy(struct patch *p)
+{
+       const char *renamecopy = p->is_rename ? "rename" : "copy";
+       const char *old, *new;
+
+       /* Find common prefix */
+       old = p->old_name;
+       new = p->new_name;
+       while (1) {
+               const char *slash_old, *slash_new;
+               slash_old = strchr(old, '/');
+               slash_new = strchr(new, '/');
+               if (!slash_old ||
+                   !slash_new ||
+                   slash_old - old != slash_new - new ||
+                   memcmp(old, new, slash_new - new))
+                       break;
+               old = slash_old + 1;
+               new = slash_new + 1;
+       }
+       /* p->old_name thru old is the common prefix, and old and new
+        * through the end of names are renames
+        */
+       if (old != p->old_name)
+               printf(" %s %.*s{%s => %s} (%d%%)\n", renamecopy,
+                      (int)(old - p->old_name), p->old_name,
+                      old, new, p->score);
+       else
+               printf(" %s %s => %s (%d%%)\n", renamecopy,
+                      p->old_name, p->new_name, p->score);
+       show_mode_change(p, 0);
+}
+
+static void summary_patch_list(struct patch *patch)
+{
+       struct patch *p;
+
+       for (p = patch; p; p = p->next) {
+               if (p->is_new)
+                       show_file_mode_name("create", p->new_mode, p->new_name);
+               else if (p->is_delete)
+                       show_file_mode_name("delete", p->old_mode, p->old_name);
+               else {
+                       if (p->is_rename || p->is_copy)
+                               show_rename_copy(p);
+                       else {
+                               if (p->score) {
+                                       printf(" rewrite %s (%d%%)\n",
+                                              p->new_name, p->score);
+                                       show_mode_change(p, 0);
+                               }
+                               else
+                                       show_mode_change(p, 1);
+                       }
+               }
+       }
+}
+
+static void patch_stats(struct patch *patch)
+{
+       int lines = patch->lines_added + patch->lines_deleted;
+
+       if (lines > max_change)
+               max_change = lines;
+       if (patch->old_name) {
+               int len = quote_c_style(patch->old_name, NULL, NULL, 0);
+               if (!len)
+                       len = strlen(patch->old_name);
+               if (len > max_len)
+                       max_len = len;
+       }
+       if (patch->new_name) {
+               int len = quote_c_style(patch->new_name, NULL, NULL, 0);
+               if (!len)
+                       len = strlen(patch->new_name);
+               if (len > max_len)
+                       max_len = len;
+       }
+}
+
+static void remove_file(struct patch *patch, int rmdir_empty)
+{
+       if (update_index) {
+               if (remove_file_from_cache(patch->old_name) < 0)
+                       die("unable to remove %s from index", patch->old_name);
+       }
+       if (!cached) {
+               if (S_ISGITLINK(patch->old_mode)) {
+                       if (rmdir(patch->old_name))
+                               warning("unable to remove submodule %s",
+                                       patch->old_name);
+               } else if (!unlink(patch->old_name) && rmdir_empty) {
+                       char *name = xstrdup(patch->old_name);
+                       char *end = strrchr(name, '/');
+                       while (end) {
+                               *end = 0;
+                               if (rmdir(name))
+                                       break;
+                               end = strrchr(name, '/');
+                       }
+                       free(name);
+               }
+       }
+}
+
+static void add_index_file(const char *path, unsigned mode, void *buf, unsigned long size)
+{
+       struct stat st;
+       struct cache_entry *ce;
+       int namelen = strlen(path);
+       unsigned ce_size = cache_entry_size(namelen);
+
+       if (!update_index)
+               return;
+
+       ce = xcalloc(1, ce_size);
+       memcpy(ce->name, path, namelen);
+       ce->ce_mode = create_ce_mode(mode);
+       ce->ce_flags = htons(namelen);
+       if (S_ISGITLINK(mode)) {
+               const char *s = buf;
+
+               if (get_sha1_hex(s + strlen("Subproject commit "), ce->sha1))
+                       die("corrupt patch for subproject %s", path);
+       } else {
+               if (!cached) {
+                       if (lstat(path, &st) < 0)
+                               die("unable to stat newly created file %s",
+                                   path);
+                       fill_stat_cache_info(ce, &st);
+               }
+               if (write_sha1_file(buf, size, blob_type, ce->sha1) < 0)
+                       die("unable to create backing store for newly created file %s", path);
+       }
+       if (add_cache_entry(ce, ADD_CACHE_OK_TO_ADD) < 0)
+               die("unable to add cache entry for %s", path);
+}
+
+static int try_create_file(const char *path, unsigned int mode, const char *buf, unsigned long size)
+{
+       int fd;
+       struct strbuf nbuf;
+
+       if (S_ISGITLINK(mode)) {
+               struct stat st;
+               if (!lstat(path, &st) && S_ISDIR(st.st_mode))
+                       return 0;
+               return mkdir(path, 0777);
+       }
+
+       if (has_symlinks && S_ISLNK(mode))
+               /* Although buf:size is counted string, it also is NUL
+                * terminated.
+                */
+               return symlink(buf, path);
+
+       fd = open(path, O_CREAT | O_EXCL | O_WRONLY, (mode & 0100) ? 0777 : 0666);
+       if (fd < 0)
+               return -1;
+
+       strbuf_init(&nbuf, 0);
+       if (convert_to_working_tree(path, buf, size, &nbuf)) {
+               size = nbuf.len;
+               buf  = nbuf.buf;
+       }
+       write_or_die(fd, buf, size);
+       strbuf_release(&nbuf);
+
+       if (close(fd) < 0)
+               die("closing file %s: %s", path, strerror(errno));
+       return 0;
+}
+
+/*
+ * We optimistically assume that the directories exist,
+ * which is true 99% of the time anyway. If they don't,
+ * we create them and try again.
+ */
+static void create_one_file(char *path, unsigned mode, const char *buf, unsigned long size)
+{
+       if (cached)
+               return;
+       if (!try_create_file(path, mode, buf, size))
+               return;
+
+       if (errno == ENOENT) {
+               if (safe_create_leading_directories(path))
+                       return;
+               if (!try_create_file(path, mode, buf, size))
+                       return;
+       }
+
+       if (errno == EEXIST || errno == EACCES) {
+               /* We may be trying to create a file where a directory
+                * used to be.
+                */
+               struct stat st;
+               if (!lstat(path, &st) && (!S_ISDIR(st.st_mode) || !rmdir(path)))
+                       errno = EEXIST;
+       }
+
+       if (errno == EEXIST) {
+               unsigned int nr = getpid();
+
+               for (;;) {
+                       const char *newpath;
+                       newpath = mkpath("%s~%u", path, nr);
+                       if (!try_create_file(newpath, mode, buf, size)) {
+                               if (!rename(newpath, path))
+                                       return;
+                               unlink(newpath);
+                               break;
+                       }
+                       if (errno != EEXIST)
+                               break;
+                       ++nr;
+               }
+       }
+       die("unable to write file %s mode %o", path, mode);
+}
+
+static void create_file(struct patch *patch)
+{
+       char *path = patch->new_name;
+       unsigned mode = patch->new_mode;
+       unsigned long size = patch->resultsize;
+       char *buf = patch->result;
+
+       if (!mode)
+               mode = S_IFREG | 0644;
+       create_one_file(path, mode, buf, size);
+       add_index_file(path, mode, buf, size);
+}
+
+/* phase zero is to remove, phase one is to create */
+static void write_out_one_result(struct patch *patch, int phase)
+{
+       if (patch->is_delete > 0) {
+               if (phase == 0)
+                       remove_file(patch, 1);
+               return;
+       }
+       if (patch->is_new > 0 || patch->is_copy) {
+               if (phase == 1)
+                       create_file(patch);
+               return;
+       }
+       /*
+        * Rename or modification boils down to the same
+        * thing: remove the old, write the new
+        */
+       if (phase == 0)
+               remove_file(patch, patch->is_rename);
+       if (phase == 1)
+               create_file(patch);
+}
+
+static int write_out_one_reject(struct patch *patch)
+{
+       FILE *rej;
+       char namebuf[PATH_MAX];
+       struct fragment *frag;
+       int cnt = 0;
+
+       for (cnt = 0, frag = patch->fragments; frag; frag = frag->next) {
+               if (!frag->rejected)
+                       continue;
+               cnt++;
+       }
+
+       if (!cnt) {
+               if (apply_verbosely)
+                       say_patch_name(stderr,
+                                      "Applied patch ", patch, " cleanly.\n");
+               return 0;
+       }
+
+       /* This should not happen, because a removal patch that leaves
+        * contents are marked "rejected" at the patch level.
+        */
+       if (!patch->new_name)
+               die("internal error");
+
+       /* Say this even without --verbose */
+       say_patch_name(stderr, "Applying patch ", patch, " with");
+       fprintf(stderr, " %d rejects...\n", cnt);
+
+       cnt = strlen(patch->new_name);
+       if (ARRAY_SIZE(namebuf) <= cnt + 5) {
+               cnt = ARRAY_SIZE(namebuf) - 5;
+               fprintf(stderr,
+                       "warning: truncating .rej filename to %.*s.rej",
+                       cnt - 1, patch->new_name);
+       }
+       memcpy(namebuf, patch->new_name, cnt);
+       memcpy(namebuf + cnt, ".rej", 5);
+
+       rej = fopen(namebuf, "w");
+       if (!rej)
+               return error("cannot open %s: %s", namebuf, strerror(errno));
+
+       /* Normal git tools never deal with .rej, so do not pretend
+        * this is a git patch by saying --git nor give extended
+        * headers.  While at it, maybe please "kompare" that wants
+        * the trailing TAB and some garbage at the end of line ;-).
+        */
+       fprintf(rej, "diff a/%s b/%s\t(rejected hunks)\n",
+               patch->new_name, patch->new_name);
+       for (cnt = 1, frag = patch->fragments;
+            frag;
+            cnt++, frag = frag->next) {
+               if (!frag->rejected) {
+                       fprintf(stderr, "Hunk #%d applied cleanly.\n", cnt);
+                       continue;
+               }
+               fprintf(stderr, "Rejected hunk #%d.\n", cnt);
+               fprintf(rej, "%.*s", frag->size, frag->patch);
+               if (frag->patch[frag->size-1] != '\n')
+                       fputc('\n', rej);
+       }
+       fclose(rej);
+       return -1;
+}
+
+static int write_out_results(struct patch *list, int skipped_patch)
+{
+       int phase;
+       int errs = 0;
+       struct patch *l;
+
+       if (!list && !skipped_patch)
+               return error("No changes");
+
+       for (phase = 0; phase < 2; phase++) {
+               l = list;
+               while (l) {
+                       if (l->rejected)
+                               errs = 1;
+                       else {
+                               write_out_one_result(l, phase);
+                               if (phase == 1 && write_out_one_reject(l))
+                                       errs = 1;
+                       }
+                       l = l->next;
+               }
+       }
+       return errs;
+}
+
+static struct lock_file lock_file;
+
+static struct excludes {
+       struct excludes *next;
+       const char *path;
+} *excludes;
+
+static int use_patch(struct patch *p)
+{
+       const char *pathname = p->new_name ? p->new_name : p->old_name;
+       struct excludes *x = excludes;
+       while (x) {
+               if (fnmatch(x->path, pathname, 0) == 0)
+                       return 0;
+               x = x->next;
+       }
+       if (0 < prefix_length) {
+               int pathlen = strlen(pathname);
+               if (pathlen <= prefix_length ||
+                   memcmp(prefix, pathname, prefix_length))
+                       return 0;
+       }
+       return 1;
+}
+
+static void prefix_one(char **name)
+{
+       char *old_name = *name;
+       if (!old_name)
+               return;
+       *name = xstrdup(prefix_filename(prefix, prefix_length, *name));
+       free(old_name);
+}
+
+static void prefix_patches(struct patch *p)
+{
+       if (!prefix || p->is_toplevel_relative)
+               return;
+       for ( ; p; p = p->next) {
+               if (p->new_name == p->old_name) {
+                       char *prefixed = p->new_name;
+                       prefix_one(&prefixed);
+                       p->new_name = p->old_name = prefixed;
+               }
+               else {
+                       prefix_one(&p->new_name);
+                       prefix_one(&p->old_name);
+               }
+       }
+}
+
+static int apply_patch(int fd, const char *filename, int inaccurate_eof)
+{
+       size_t offset;
+       struct strbuf buf;
+       struct patch *list = NULL, **listp = &list;
+       int skipped_patch = 0;
+
+       strbuf_init(&buf, 0);
+       patch_input_file = filename;
+       read_patch_file(&buf, fd);
+       offset = 0;
+       while (offset < buf.len) {
+               struct patch *patch;
+               int nr;
+
+               patch = xcalloc(1, sizeof(*patch));
+               patch->inaccurate_eof = inaccurate_eof;
+               nr = parse_chunk(buf.buf + offset, buf.len - offset, patch);
+               if (nr < 0)
+                       break;
+               if (apply_in_reverse)
+                       reverse_patches(patch);
+               if (prefix)
+                       prefix_patches(patch);
+               if (use_patch(patch)) {
+                       patch_stats(patch);
+                       *listp = patch;
+                       listp = &patch->next;
+               }
+               else {
+                       /* perhaps free it a bit better? */
+                       free(patch);
+                       skipped_patch++;
+               }
+               offset += nr;
+       }
+
+       if (whitespace_error && (new_whitespace == error_on_whitespace))
+               apply = 0;
+
+       update_index = check_index && apply;
+       if (update_index && newfd < 0)
+               newfd = hold_locked_index(&lock_file, 1);
+
+       if (check_index) {
+               if (read_cache() < 0)
+                       die("unable to read index file");
+       }
+
+       if ((check || apply) &&
+           check_patch_list(list) < 0 &&
+           !apply_with_reject)
+               exit(1);
+
+       if (apply && write_out_results(list, skipped_patch))
+               exit(1);
+
+       if (fake_ancestor)
+               build_fake_ancestor(list, fake_ancestor);
+
+       if (diffstat)
+               stat_patch_list(list);
+
+       if (numstat)
+               numstat_patch_list(list);
+
+       if (summary)
+               summary_patch_list(list);
+
+       strbuf_release(&buf);
+       return 0;
+}
+
+static int git_apply_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "apply.whitespace")) {
+               apply_default_whitespace = xstrdup(value);
+               return 0;
+       }
+       return git_default_config(var, value);
+}
+
+
+int cmd_apply(int argc, const char **argv, const char *unused_prefix)
+{
+       int i;
+       int read_stdin = 1;
+       int inaccurate_eof = 0;
+       int errs = 0;
+       int is_not_gitdir = 0;
+
+       const char *whitespace_option = NULL;
+
+       prefix = setup_git_directory_gently(&is_not_gitdir);
+       prefix_length = prefix ? strlen(prefix) : 0;
+       git_config(git_apply_config);
+       if (apply_default_whitespace)
+               parse_whitespace_option(apply_default_whitespace);
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               char *end;
+               int fd;
+
+               if (!strcmp(arg, "-")) {
+                       errs |= apply_patch(0, "<stdin>", inaccurate_eof);
+                       read_stdin = 0;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--exclude=")) {
+                       struct excludes *x = xmalloc(sizeof(*x));
+                       x->path = arg + 10;
+                       x->next = excludes;
+                       excludes = x;
+                       continue;
+               }
+               if (!prefixcmp(arg, "-p")) {
+                       p_value = atoi(arg + 2);
+                       p_value_known = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--no-add")) {
+                       no_add = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--stat")) {
+                       apply = 0;
+                       diffstat = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--allow-binary-replacement") ||
+                   !strcmp(arg, "--binary")) {
+                       continue; /* now no-op */
+               }
+               if (!strcmp(arg, "--numstat")) {
+                       apply = 0;
+                       numstat = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--summary")) {
+                       apply = 0;
+                       summary = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--check")) {
+                       apply = 0;
+                       check = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--index")) {
+                       if (is_not_gitdir)
+                               die("--index outside a repository");
+                       check_index = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--cached")) {
+                       if (is_not_gitdir)
+                               die("--cached outside a repository");
+                       check_index = 1;
+                       cached = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--apply")) {
+                       apply = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--build-fake-ancestor")) {
+                       apply = 0;
+                       if (++i >= argc)
+                               die ("need a filename");
+                       fake_ancestor = argv[i];
+                       continue;
+               }
+               if (!strcmp(arg, "-z")) {
+                       line_termination = 0;
+                       continue;
+               }
+               if (!prefixcmp(arg, "-C")) {
+                       p_context = strtoul(arg + 2, &end, 0);
+                       if (*end != '\0')
+                               die("unrecognized context count '%s'", arg + 2);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--whitespace=")) {
+                       whitespace_option = arg + 13;
+                       parse_whitespace_option(arg + 13);
+                       continue;
+               }
+               if (!strcmp(arg, "-R") || !strcmp(arg, "--reverse")) {
+                       apply_in_reverse = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--unidiff-zero")) {
+                       unidiff_zero = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--reject")) {
+                       apply = apply_with_reject = apply_verbosely = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-v") || !strcmp(arg, "--verbose")) {
+                       apply_verbosely = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--inaccurate-eof")) {
+                       inaccurate_eof = 1;
+                       continue;
+               }
+               if (0 < prefix_length)
+                       arg = prefix_filename(prefix, prefix_length, arg);
+
+               fd = open(arg, O_RDONLY);
+               if (fd < 0)
+                       usage(apply_usage);
+               read_stdin = 0;
+               set_default_whitespace_mode(whitespace_option);
+               errs |= apply_patch(fd, arg, inaccurate_eof);
+               close(fd);
+       }
+       set_default_whitespace_mode(whitespace_option);
+       if (read_stdin)
+               errs |= apply_patch(0, "<stdin>", inaccurate_eof);
+       if (whitespace_error) {
+               if (squelch_whitespace_errors &&
+                   squelch_whitespace_errors < whitespace_error) {
+                       int squelched =
+                               whitespace_error - squelch_whitespace_errors;
+                       fprintf(stderr, "warning: squelched %d "
+                               "whitespace error%s\n",
+                               squelched,
+                               squelched == 1 ? "" : "s");
+               }
+               if (new_whitespace == error_on_whitespace)
+                       die("%d line%s add%s whitespace errors.",
+                           whitespace_error,
+                           whitespace_error == 1 ? "" : "s",
+                           whitespace_error == 1 ? "s" : "");
+               if (applied_after_fixing_ws)
+                       fprintf(stderr, "warning: %d line%s applied after"
+                               " fixing whitespace errors.\n",
+                               applied_after_fixing_ws,
+                               applied_after_fixing_ws == 1 ? "" : "s");
+               else if (whitespace_error)
+                       fprintf(stderr, "warning: %d line%s add%s whitespace errors.\n",
+                               whitespace_error,
+                               whitespace_error == 1 ? "" : "s",
+                               whitespace_error == 1 ? "s" : "");
+       }
+
+       if (update_index) {
+               if (write_cache(newfd, active_cache, active_nr) ||
+                   close(newfd) || commit_locked_index(&lock_file))
+                       die("Unable to write new index file");
+       }
+
+       return !!errs;
+}
diff --git a/builtin-archive.c b/builtin-archive.c
new file mode 100644 (file)
index 0000000..14a1b30
--- /dev/null
@@ -0,0 +1,341 @@
+/*
+ * Copyright (c) 2006 Franck Bui-Huu
+ * Copyright (c) 2006 Rene Scharfe
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "archive.h"
+#include "commit.h"
+#include "tree-walk.h"
+#include "exec_cmd.h"
+#include "pkt-line.h"
+#include "sideband.h"
+#include "attr.h"
+
+static const char archive_usage[] = \
+"git-archive --format=<fmt> [--prefix=<prefix>/] [--verbose] [<extra>] <tree-ish> [path...]";
+
+static struct archiver_desc
+{
+       const char *name;
+       write_archive_fn_t write_archive;
+       parse_extra_args_fn_t parse_extra;
+} archivers[] = {
+       { "tar", write_tar_archive, NULL },
+       { "zip", write_zip_archive, parse_extra_zip_args },
+};
+
+static int run_remote_archiver(const char *remote, int argc,
+                              const char **argv)
+{
+       char *url, buf[LARGE_PACKET_MAX];
+       int fd[2], i, len, rv;
+       struct child_process *conn;
+       const char *exec = "git-upload-archive";
+       int exec_at = 0;
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (!prefixcmp(arg, "--exec=")) {
+                       if (exec_at)
+                               die("multiple --exec specified");
+                       exec = arg + 7;
+                       exec_at = i;
+                       break;
+               }
+       }
+
+       url = xstrdup(remote);
+       conn = git_connect(fd, url, exec, 0);
+
+       for (i = 1; i < argc; i++) {
+               if (i == exec_at)
+                       continue;
+               packet_write(fd[1], "argument %s\n", argv[i]);
+       }
+       packet_flush(fd[1]);
+
+       len = packet_read_line(fd[0], buf, sizeof(buf));
+       if (!len)
+               die("git-archive: expected ACK/NAK, got EOF");
+       if (buf[len-1] == '\n')
+               buf[--len] = 0;
+       if (strcmp(buf, "ACK")) {
+               if (len > 5 && !prefixcmp(buf, "NACK "))
+                       die("git-archive: NACK %s", buf + 5);
+               die("git-archive: protocol error");
+       }
+
+       len = packet_read_line(fd[0], buf, sizeof(buf));
+       if (len)
+               die("git-archive: expected a flush");
+
+       /* Now, start reading from fd[0] and spit it out to stdout */
+       rv = recv_sideband("archive", fd[0], 1, 2);
+       close(fd[0]);
+       close(fd[1]);
+       rv |= finish_connect(conn);
+
+       return !!rv;
+}
+
+static void format_subst(const struct commit *commit,
+                         const char *src, size_t len,
+                         struct strbuf *buf)
+{
+       char *to_free = NULL;
+       struct strbuf fmt;
+
+       if (src == buf->buf)
+               to_free = strbuf_detach(buf, NULL);
+       strbuf_init(&fmt, 0);
+       for (;;) {
+               const char *b, *c;
+
+               b = memmem(src, len, "$Format:", 8);
+               if (!b || src + len < b + 9)
+                       break;
+               c = memchr(b + 8, '$', len - 8);
+               if (!c)
+                       break;
+
+               strbuf_reset(&fmt);
+               strbuf_add(&fmt, b + 8, c - b - 8);
+
+               strbuf_add(buf, src, b - src);
+               format_commit_message(commit, fmt.buf, buf);
+               len -= c + 1 - src;
+               src  = c + 1;
+       }
+       strbuf_add(buf, src, len);
+       strbuf_release(&fmt);
+       free(to_free);
+}
+
+static int convert_to_archive(const char *path,
+                              const void *src, size_t len,
+                              struct strbuf *buf,
+                              const struct commit *commit)
+{
+       static struct git_attr *attr_export_subst;
+       struct git_attr_check check[1];
+
+       if (!commit)
+               return 0;
+
+       if (!attr_export_subst)
+               attr_export_subst = git_attr("export-subst", 12);
+
+       check[0].attr = attr_export_subst;
+       if (git_checkattr(path, ARRAY_SIZE(check), check))
+               return 0;
+       if (!ATTR_TRUE(check[0].value))
+               return 0;
+
+       format_subst(commit, src, len, buf);
+       return 1;
+}
+
+void *sha1_file_to_archive(const char *path, const unsigned char *sha1,
+                           unsigned int mode, enum object_type *type,
+                           unsigned long *sizep,
+                           const struct commit *commit)
+{
+       void *buffer;
+
+       buffer = read_sha1_file(sha1, type, sizep);
+       if (buffer && S_ISREG(mode)) {
+               struct strbuf buf;
+               size_t size = 0;
+
+               strbuf_init(&buf, 0);
+               strbuf_attach(&buf, buffer, *sizep, *sizep + 1);
+               convert_to_working_tree(path, buf.buf, buf.len, &buf);
+               convert_to_archive(path, buf.buf, buf.len, &buf, commit);
+               buffer = strbuf_detach(&buf, &size);
+               *sizep = size;
+       }
+
+       return buffer;
+}
+
+static int init_archiver(const char *name, struct archiver *ar)
+{
+       int rv = -1, i;
+
+       for (i = 0; i < ARRAY_SIZE(archivers); i++) {
+               if (!strcmp(name, archivers[i].name)) {
+                       memset(ar, 0, sizeof(*ar));
+                       ar->name = archivers[i].name;
+                       ar->write_archive = archivers[i].write_archive;
+                       ar->parse_extra = archivers[i].parse_extra;
+                       rv = 0;
+                       break;
+               }
+       }
+       return rv;
+}
+
+void parse_pathspec_arg(const char **pathspec, struct archiver_args *ar_args)
+{
+       ar_args->pathspec = get_pathspec(ar_args->base, pathspec);
+}
+
+void parse_treeish_arg(const char **argv, struct archiver_args *ar_args,
+                      const char *prefix)
+{
+       const char *name = argv[0];
+       const unsigned char *commit_sha1;
+       time_t archive_time;
+       struct tree *tree;
+       const struct commit *commit;
+       unsigned char sha1[20];
+
+       if (get_sha1(name, sha1))
+               die("Not a valid object name");
+
+       commit = lookup_commit_reference_gently(sha1, 1);
+       if (commit) {
+               commit_sha1 = commit->object.sha1;
+               archive_time = commit->date;
+       } else {
+               commit_sha1 = NULL;
+               archive_time = time(NULL);
+       }
+
+       tree = parse_tree_indirect(sha1);
+       if (tree == NULL)
+               die("not a tree object");
+
+       if (prefix) {
+               unsigned char tree_sha1[20];
+               unsigned int mode;
+               int err;
+
+               err = get_tree_entry(tree->object.sha1, prefix,
+                                    tree_sha1, &mode);
+               if (err || !S_ISDIR(mode))
+                       die("current working directory is untracked");
+
+               tree = parse_tree_indirect(tree_sha1);
+       }
+       ar_args->tree = tree;
+       ar_args->commit_sha1 = commit_sha1;
+       ar_args->commit = commit;
+       ar_args->time = archive_time;
+}
+
+int parse_archive_args(int argc, const char **argv, struct archiver *ar)
+{
+       const char *extra_argv[MAX_EXTRA_ARGS];
+       int extra_argc = 0;
+       const char *format = "tar";
+       const char *base = "";
+       int verbose = 0;
+       int i;
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (!strcmp(arg, "--list") || !strcmp(arg, "-l")) {
+                       for (i = 0; i < ARRAY_SIZE(archivers); i++)
+                               printf("%s\n", archivers[i].name);
+                       exit(0);
+               }
+               if (!strcmp(arg, "--verbose") || !strcmp(arg, "-v")) {
+                       verbose = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--format=")) {
+                       format = arg + 9;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--prefix=")) {
+                       base = arg + 9;
+                       continue;
+               }
+               if (!strcmp(arg, "--")) {
+                       i++;
+                       break;
+               }
+               if (arg[0] == '-') {
+                       if (extra_argc > MAX_EXTRA_ARGS - 1)
+                               die("Too many extra options");
+                       extra_argv[extra_argc++] = arg;
+                       continue;
+               }
+               break;
+       }
+
+       /* We need at least one parameter -- tree-ish */
+       if (argc - 1 < i)
+               usage(archive_usage);
+       if (init_archiver(format, ar) < 0)
+               die("Unknown archive format '%s'", format);
+
+       if (extra_argc) {
+               if (!ar->parse_extra)
+                       die("'%s' format does not handle %s",
+                           ar->name, extra_argv[0]);
+               ar->args.extra = ar->parse_extra(extra_argc, extra_argv);
+       }
+       ar->args.verbose = verbose;
+       ar->args.base = base;
+
+       return i;
+}
+
+static const char *extract_remote_arg(int *ac, const char **av)
+{
+       int ix, iy, cnt = *ac;
+       int no_more_options = 0;
+       const char *remote = NULL;
+
+       for (ix = iy = 1; ix < cnt; ix++) {
+               const char *arg = av[ix];
+               if (!strcmp(arg, "--"))
+                       no_more_options = 1;
+               if (!no_more_options) {
+                       if (!prefixcmp(arg, "--remote=")) {
+                               if (remote)
+                                       die("Multiple --remote specified");
+                               remote = arg + 9;
+                               continue;
+                       }
+                       if (arg[0] != '-')
+                               no_more_options = 1;
+               }
+               if (ix != iy)
+                       av[iy] = arg;
+               iy++;
+       }
+       if (remote) {
+               av[--cnt] = NULL;
+               *ac = cnt;
+       }
+       return remote;
+}
+
+int cmd_archive(int argc, const char **argv, const char *prefix)
+{
+       struct archiver ar;
+       int tree_idx;
+       const char *remote = NULL;
+
+       remote = extract_remote_arg(&argc, argv);
+       if (remote)
+               return run_remote_archiver(remote, argc, argv);
+
+       setvbuf(stderr, NULL, _IOLBF, BUFSIZ);
+
+       memset(&ar, 0, sizeof(ar));
+       tree_idx = parse_archive_args(argc, argv, &ar);
+       if (prefix == NULL)
+               prefix = setup_git_directory();
+
+       argv += tree_idx;
+       parse_treeish_arg(argv, &ar.args, prefix);
+       parse_pathspec_arg(argv + 1, &ar.args);
+
+       return ar.write_archive(&ar.args);
+}
diff --git a/builtin-blame.c b/builtin-blame.c
new file mode 100644 (file)
index 0000000..c158d31
--- /dev/null
@@ -0,0 +1,2440 @@
+/*
+ * Pickaxe
+ *
+ * Copyright (c) 2006, Junio C Hamano
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "blob.h"
+#include "commit.h"
+#include "tag.h"
+#include "tree-walk.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "revision.h"
+#include "quote.h"
+#include "xdiff-interface.h"
+#include "cache-tree.h"
+#include "path-list.h"
+#include "mailmap.h"
+
+static char blame_usage[] =
+"git-blame [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-p] [-w] [-L n,m] [-S <revs-file>] [-M] [-C] [-C] [--contents <filename>] [--incremental] [commit] [--] file\n"
+"  -c                  Use the same output mode as git-annotate (Default: off)\n"
+"  -b                  Show blank SHA-1 for boundary commits (Default: off)\n"
+"  -l                  Show long commit SHA1 (Default: off)\n"
+"  --root              Do not treat root commits as boundaries (Default: off)\n"
+"  -t                  Show raw timestamp (Default: off)\n"
+"  -f, --show-name     Show original filename (Default: auto)\n"
+"  -n, --show-number   Show original linenumber (Default: off)\n"
+"  -s                  Suppress author name and timestamp (Default: off)\n"
+"  -p, --porcelain     Show in a format designed for machine consumption\n"
+"  -w                  Ignore whitespace differences\n"
+"  -L n,m              Process only line range n,m, counting from 1\n"
+"  -M, -C              Find line movements within and across files\n"
+"  --incremental       Show blame entries as we find them, incrementally\n"
+"  --contents file     Use <file>'s contents as the final image\n"
+"  -S revs-file        Use revisions from revs-file instead of calling git-rev-list\n";
+
+static int longest_file;
+static int longest_author;
+static int max_orig_digits;
+static int max_digits;
+static int max_score_digits;
+static int show_root;
+static int blank_boundary;
+static int incremental;
+static int cmd_is_annotate;
+static int xdl_opts = XDF_NEED_MINIMAL;
+static struct path_list mailmap;
+
+#ifndef DEBUG
+#define DEBUG 0
+#endif
+
+/* stats */
+static int num_read_blob;
+static int num_get_patch;
+static int num_commits;
+
+#define PICKAXE_BLAME_MOVE             01
+#define PICKAXE_BLAME_COPY             02
+#define PICKAXE_BLAME_COPY_HARDER      04
+#define PICKAXE_BLAME_COPY_HARDEST     010
+
+/*
+ * blame for a blame_entry with score lower than these thresholds
+ * is not passed to the parent using move/copy logic.
+ */
+static unsigned blame_move_score;
+static unsigned blame_copy_score;
+#define BLAME_DEFAULT_MOVE_SCORE       20
+#define BLAME_DEFAULT_COPY_SCORE       40
+
+/* bits #0..7 in revision.h, #8..11 used for merge_bases() in commit.c */
+#define METAINFO_SHOWN         (1u<<12)
+#define MORE_THAN_ONE_PATH     (1u<<13)
+
+/*
+ * One blob in a commit that is being suspected
+ */
+struct origin {
+       int refcnt;
+       struct commit *commit;
+       mmfile_t file;
+       unsigned char blob_sha1[20];
+       char path[FLEX_ARRAY];
+};
+
+/*
+ * Given an origin, prepare mmfile_t structure to be used by the
+ * diff machinery
+ */
+static char *fill_origin_blob(struct origin *o, mmfile_t *file)
+{
+       if (!o->file.ptr) {
+               enum object_type type;
+               num_read_blob++;
+               file->ptr = read_sha1_file(o->blob_sha1, &type,
+                                          (unsigned long *)(&(file->size)));
+               if (!file->ptr)
+                       die("Cannot read blob %s for path %s",
+                           sha1_to_hex(o->blob_sha1),
+                           o->path);
+               o->file = *file;
+       }
+       else
+               *file = o->file;
+       return file->ptr;
+}
+
+/*
+ * Origin is refcounted and usually we keep the blob contents to be
+ * reused.
+ */
+static inline struct origin *origin_incref(struct origin *o)
+{
+       if (o)
+               o->refcnt++;
+       return o;
+}
+
+static void origin_decref(struct origin *o)
+{
+       if (o && --o->refcnt <= 0) {
+               if (o->file.ptr)
+                       free(o->file.ptr);
+               memset(o, 0, sizeof(*o));
+               free(o);
+       }
+}
+
+/*
+ * Each group of lines is described by a blame_entry; it can be split
+ * as we pass blame to the parents.  They form a linked list in the
+ * scoreboard structure, sorted by the target line number.
+ */
+struct blame_entry {
+       struct blame_entry *prev;
+       struct blame_entry *next;
+
+       /* the first line of this group in the final image;
+        * internally all line numbers are 0 based.
+        */
+       int lno;
+
+       /* how many lines this group has */
+       int num_lines;
+
+       /* the commit that introduced this group into the final image */
+       struct origin *suspect;
+
+       /* true if the suspect is truly guilty; false while we have not
+        * checked if the group came from one of its parents.
+        */
+       char guilty;
+
+       /* the line number of the first line of this group in the
+        * suspect's file; internally all line numbers are 0 based.
+        */
+       int s_lno;
+
+       /* how significant this entry is -- cached to avoid
+        * scanning the lines over and over.
+        */
+       unsigned score;
+};
+
+/*
+ * The current state of the blame assignment.
+ */
+struct scoreboard {
+       /* the final commit (i.e. where we started digging from) */
+       struct commit *final;
+
+       const char *path;
+
+       /*
+        * The contents in the final image.
+        * Used by many functions to obtain contents of the nth line,
+        * indexed with scoreboard.lineno[blame_entry.lno].
+        */
+       const char *final_buf;
+       unsigned long final_buf_size;
+
+       /* linked list of blames */
+       struct blame_entry *ent;
+
+       /* look-up a line in the final buffer */
+       int num_lines;
+       int *lineno;
+};
+
+static inline int same_suspect(struct origin *a, struct origin *b)
+{
+       if (a == b)
+               return 1;
+       if (a->commit != b->commit)
+               return 0;
+       return !strcmp(a->path, b->path);
+}
+
+static void sanity_check_refcnt(struct scoreboard *);
+
+/*
+ * If two blame entries that are next to each other came from
+ * contiguous lines in the same origin (i.e. <commit, path> pair),
+ * merge them together.
+ */
+static void coalesce(struct scoreboard *sb)
+{
+       struct blame_entry *ent, *next;
+
+       for (ent = sb->ent; ent && (next = ent->next); ent = next) {
+               if (same_suspect(ent->suspect, next->suspect) &&
+                   ent->guilty == next->guilty &&
+                   ent->s_lno + ent->num_lines == next->s_lno) {
+                       ent->num_lines += next->num_lines;
+                       ent->next = next->next;
+                       if (ent->next)
+                               ent->next->prev = ent;
+                       origin_decref(next->suspect);
+                       free(next);
+                       ent->score = 0;
+                       next = ent; /* again */
+               }
+       }
+
+       if (DEBUG) /* sanity */
+               sanity_check_refcnt(sb);
+}
+
+/*
+ * Given a commit and a path in it, create a new origin structure.
+ * The callers that add blame to the scoreboard should use
+ * get_origin() to obtain shared, refcounted copy instead of calling
+ * this function directly.
+ */
+static struct origin *make_origin(struct commit *commit, const char *path)
+{
+       struct origin *o;
+       o = xcalloc(1, sizeof(*o) + strlen(path) + 1);
+       o->commit = commit;
+       o->refcnt = 1;
+       strcpy(o->path, path);
+       return o;
+}
+
+/*
+ * Locate an existing origin or create a new one.
+ */
+static struct origin *get_origin(struct scoreboard *sb,
+                                struct commit *commit,
+                                const char *path)
+{
+       struct blame_entry *e;
+
+       for (e = sb->ent; e; e = e->next) {
+               if (e->suspect->commit == commit &&
+                   !strcmp(e->suspect->path, path))
+                       return origin_incref(e->suspect);
+       }
+       return make_origin(commit, path);
+}
+
+/*
+ * Fill the blob_sha1 field of an origin if it hasn't, so that later
+ * call to fill_origin_blob() can use it to locate the data.  blob_sha1
+ * for an origin is also used to pass the blame for the entire file to
+ * the parent to detect the case where a child's blob is identical to
+ * that of its parent's.
+ */
+static int fill_blob_sha1(struct origin *origin)
+{
+       unsigned mode;
+
+       if (!is_null_sha1(origin->blob_sha1))
+               return 0;
+       if (get_tree_entry(origin->commit->object.sha1,
+                          origin->path,
+                          origin->blob_sha1, &mode))
+               goto error_out;
+       if (sha1_object_info(origin->blob_sha1, NULL) != OBJ_BLOB)
+               goto error_out;
+       return 0;
+ error_out:
+       hashclr(origin->blob_sha1);
+       return -1;
+}
+
+/*
+ * We have an origin -- check if the same path exists in the
+ * parent and return an origin structure to represent it.
+ */
+static struct origin *find_origin(struct scoreboard *sb,
+                                 struct commit *parent,
+                                 struct origin *origin)
+{
+       struct origin *porigin = NULL;
+       struct diff_options diff_opts;
+       const char *paths[2];
+
+       if (parent->util) {
+               /*
+                * Each commit object can cache one origin in that
+                * commit.  This is a freestanding copy of origin and
+                * not refcounted.
+                */
+               struct origin *cached = parent->util;
+               if (!strcmp(cached->path, origin->path)) {
+                       /*
+                        * The same path between origin and its parent
+                        * without renaming -- the most common case.
+                        */
+                       porigin = get_origin(sb, parent, cached->path);
+
+                       /*
+                        * If the origin was newly created (i.e. get_origin
+                        * would call make_origin if none is found in the
+                        * scoreboard), it does not know the blob_sha1,
+                        * so copy it.  Otherwise porigin was in the
+                        * scoreboard and already knows blob_sha1.
+                        */
+                       if (porigin->refcnt == 1)
+                               hashcpy(porigin->blob_sha1, cached->blob_sha1);
+                       return porigin;
+               }
+               /* otherwise it was not very useful; free it */
+               free(parent->util);
+               parent->util = NULL;
+       }
+
+       /* See if the origin->path is different between parent
+        * and origin first.  Most of the time they are the
+        * same and diff-tree is fairly efficient about this.
+        */
+       diff_setup(&diff_opts);
+       DIFF_OPT_SET(&diff_opts, RECURSIVE);
+       diff_opts.detect_rename = 0;
+       diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+       paths[0] = origin->path;
+       paths[1] = NULL;
+
+       diff_tree_setup_paths(paths, &diff_opts);
+       if (diff_setup_done(&diff_opts) < 0)
+               die("diff-setup");
+
+       if (is_null_sha1(origin->commit->object.sha1))
+               do_diff_cache(parent->tree->object.sha1, &diff_opts);
+       else
+               diff_tree_sha1(parent->tree->object.sha1,
+                              origin->commit->tree->object.sha1,
+                              "", &diff_opts);
+       diffcore_std(&diff_opts);
+
+       /* It is either one entry that says "modified", or "created",
+        * or nothing.
+        */
+       if (!diff_queued_diff.nr) {
+               /* The path is the same as parent */
+               porigin = get_origin(sb, parent, origin->path);
+               hashcpy(porigin->blob_sha1, origin->blob_sha1);
+       }
+       else if (diff_queued_diff.nr != 1)
+               die("internal error in blame::find_origin");
+       else {
+               struct diff_filepair *p = diff_queued_diff.queue[0];
+               switch (p->status) {
+               default:
+                       die("internal error in blame::find_origin (%c)",
+                           p->status);
+               case 'M':
+                       porigin = get_origin(sb, parent, origin->path);
+                       hashcpy(porigin->blob_sha1, p->one->sha1);
+                       break;
+               case 'A':
+               case 'T':
+                       /* Did not exist in parent, or type changed */
+                       break;
+               }
+       }
+       diff_flush(&diff_opts);
+       if (porigin) {
+               /*
+                * Create a freestanding copy that is not part of
+                * the refcounted origin found in the scoreboard, and
+                * cache it in the commit.
+                */
+               struct origin *cached;
+
+               cached = make_origin(porigin->commit, porigin->path);
+               hashcpy(cached->blob_sha1, porigin->blob_sha1);
+               parent->util = cached;
+       }
+       return porigin;
+}
+
+/*
+ * We have an origin -- find the path that corresponds to it in its
+ * parent and return an origin structure to represent it.
+ */
+static struct origin *find_rename(struct scoreboard *sb,
+                                 struct commit *parent,
+                                 struct origin *origin)
+{
+       struct origin *porigin = NULL;
+       struct diff_options diff_opts;
+       int i;
+       const char *paths[2];
+
+       diff_setup(&diff_opts);
+       DIFF_OPT_SET(&diff_opts, RECURSIVE);
+       diff_opts.detect_rename = DIFF_DETECT_RENAME;
+       diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+       diff_opts.single_follow = origin->path;
+       paths[0] = NULL;
+       diff_tree_setup_paths(paths, &diff_opts);
+       if (diff_setup_done(&diff_opts) < 0)
+               die("diff-setup");
+
+       if (is_null_sha1(origin->commit->object.sha1))
+               do_diff_cache(parent->tree->object.sha1, &diff_opts);
+       else
+               diff_tree_sha1(parent->tree->object.sha1,
+                              origin->commit->tree->object.sha1,
+                              "", &diff_opts);
+       diffcore_std(&diff_opts);
+
+       for (i = 0; i < diff_queued_diff.nr; i++) {
+               struct diff_filepair *p = diff_queued_diff.queue[i];
+               if ((p->status == 'R' || p->status == 'C') &&
+                   !strcmp(p->two->path, origin->path)) {
+                       porigin = get_origin(sb, parent, p->one->path);
+                       hashcpy(porigin->blob_sha1, p->one->sha1);
+                       break;
+               }
+       }
+       diff_flush(&diff_opts);
+       return porigin;
+}
+
+/*
+ * Parsing of patch chunks...
+ */
+struct chunk {
+       /* line number in postimage; up to but not including this
+        * line is the same as preimage
+        */
+       int same;
+
+       /* preimage line number after this chunk */
+       int p_next;
+
+       /* postimage line number after this chunk */
+       int t_next;
+};
+
+struct patch {
+       struct chunk *chunks;
+       int num;
+};
+
+struct blame_diff_state {
+       struct xdiff_emit_state xm;
+       struct patch *ret;
+       unsigned hunk_post_context;
+       unsigned hunk_in_pre_context : 1;
+};
+
+static void process_u_diff(void *state_, char *line, unsigned long len)
+{
+       struct blame_diff_state *state = state_;
+       struct chunk *chunk;
+       int off1, off2, len1, len2, num;
+
+       num = state->ret->num;
+       if (len < 4 || line[0] != '@' || line[1] != '@') {
+               if (state->hunk_in_pre_context && line[0] == ' ')
+                       state->ret->chunks[num - 1].same++;
+               else {
+                       state->hunk_in_pre_context = 0;
+                       if (line[0] == ' ')
+                               state->hunk_post_context++;
+                       else
+                               state->hunk_post_context = 0;
+               }
+               return;
+       }
+
+       if (num && state->hunk_post_context) {
+               chunk = &state->ret->chunks[num - 1];
+               chunk->p_next -= state->hunk_post_context;
+               chunk->t_next -= state->hunk_post_context;
+       }
+       state->ret->num = ++num;
+       state->ret->chunks = xrealloc(state->ret->chunks,
+                                     sizeof(struct chunk) * num);
+       chunk = &state->ret->chunks[num - 1];
+       if (parse_hunk_header(line, len, &off1, &len1, &off2, &len2)) {
+               state->ret->num--;
+               return;
+       }
+
+       /* Line numbers in patch output are one based. */
+       off1--;
+       off2--;
+
+       chunk->same = len2 ? off2 : (off2 + 1);
+
+       chunk->p_next = off1 + (len1 ? len1 : 1);
+       chunk->t_next = chunk->same + len2;
+       state->hunk_in_pre_context = 1;
+       state->hunk_post_context = 0;
+}
+
+static struct patch *compare_buffer(mmfile_t *file_p, mmfile_t *file_o,
+                                   int context)
+{
+       struct blame_diff_state state;
+       xpparam_t xpp;
+       xdemitconf_t xecfg;
+       xdemitcb_t ecb;
+
+       xpp.flags = xdl_opts;
+       memset(&xecfg, 0, sizeof(xecfg));
+       xecfg.ctxlen = context;
+       ecb.outf = xdiff_outf;
+       ecb.priv = &state;
+       memset(&state, 0, sizeof(state));
+       state.xm.consume = process_u_diff;
+       state.ret = xmalloc(sizeof(struct patch));
+       state.ret->chunks = NULL;
+       state.ret->num = 0;
+
+       xdl_diff(file_p, file_o, &xpp, &xecfg, &ecb);
+
+       if (state.ret->num) {
+               struct chunk *chunk;
+               chunk = &state.ret->chunks[state.ret->num - 1];
+               chunk->p_next -= state.hunk_post_context;
+               chunk->t_next -= state.hunk_post_context;
+       }
+       return state.ret;
+}
+
+/*
+ * Run diff between two origins and grab the patch output, so that
+ * we can pass blame for lines origin is currently suspected for
+ * to its parent.
+ */
+static struct patch *get_patch(struct origin *parent, struct origin *origin)
+{
+       mmfile_t file_p, file_o;
+       struct patch *patch;
+
+       fill_origin_blob(parent, &file_p);
+       fill_origin_blob(origin, &file_o);
+       if (!file_p.ptr || !file_o.ptr)
+               return NULL;
+       patch = compare_buffer(&file_p, &file_o, 0);
+       num_get_patch++;
+       return patch;
+}
+
+static void free_patch(struct patch *p)
+{
+       free(p->chunks);
+       free(p);
+}
+
+/*
+ * Link in a new blame entry to the scoreboard.  Entries that cover the
+ * same line range have been removed from the scoreboard previously.
+ */
+static void add_blame_entry(struct scoreboard *sb, struct blame_entry *e)
+{
+       struct blame_entry *ent, *prev = NULL;
+
+       origin_incref(e->suspect);
+
+       for (ent = sb->ent; ent && ent->lno < e->lno; ent = ent->next)
+               prev = ent;
+
+       /* prev, if not NULL, is the last one that is below e */
+       e->prev = prev;
+       if (prev) {
+               e->next = prev->next;
+               prev->next = e;
+       }
+       else {
+               e->next = sb->ent;
+               sb->ent = e;
+       }
+       if (e->next)
+               e->next->prev = e;
+}
+
+/*
+ * src typically is on-stack; we want to copy the information in it to
+ * an malloced blame_entry that is already on the linked list of the
+ * scoreboard.  The origin of dst loses a refcnt while the origin of src
+ * gains one.
+ */
+static void dup_entry(struct blame_entry *dst, struct blame_entry *src)
+{
+       struct blame_entry *p, *n;
+
+       p = dst->prev;
+       n = dst->next;
+       origin_incref(src->suspect);
+       origin_decref(dst->suspect);
+       memcpy(dst, src, sizeof(*src));
+       dst->prev = p;
+       dst->next = n;
+       dst->score = 0;
+}
+
+static const char *nth_line(struct scoreboard *sb, int lno)
+{
+       return sb->final_buf + sb->lineno[lno];
+}
+
+/*
+ * It is known that lines between tlno to same came from parent, and e
+ * has an overlap with that range.  it also is known that parent's
+ * line plno corresponds to e's line tlno.
+ *
+ *                <---- e ----->
+ *                   <------>
+ *                   <------------>
+ *             <------------>
+ *             <------------------>
+ *
+ * Split e into potentially three parts; before this chunk, the chunk
+ * to be blamed for the parent, and after that portion.
+ */
+static void split_overlap(struct blame_entry *split,
+                         struct blame_entry *e,
+                         int tlno, int plno, int same,
+                         struct origin *parent)
+{
+       int chunk_end_lno;
+       memset(split, 0, sizeof(struct blame_entry [3]));
+
+       if (e->s_lno < tlno) {
+               /* there is a pre-chunk part not blamed on parent */
+               split[0].suspect = origin_incref(e->suspect);
+               split[0].lno = e->lno;
+               split[0].s_lno = e->s_lno;
+               split[0].num_lines = tlno - e->s_lno;
+               split[1].lno = e->lno + tlno - e->s_lno;
+               split[1].s_lno = plno;
+       }
+       else {
+               split[1].lno = e->lno;
+               split[1].s_lno = plno + (e->s_lno - tlno);
+       }
+
+       if (same < e->s_lno + e->num_lines) {
+               /* there is a post-chunk part not blamed on parent */
+               split[2].suspect = origin_incref(e->suspect);
+               split[2].lno = e->lno + (same - e->s_lno);
+               split[2].s_lno = e->s_lno + (same - e->s_lno);
+               split[2].num_lines = e->s_lno + e->num_lines - same;
+               chunk_end_lno = split[2].lno;
+       }
+       else
+               chunk_end_lno = e->lno + e->num_lines;
+       split[1].num_lines = chunk_end_lno - split[1].lno;
+
+       /*
+        * if it turns out there is nothing to blame the parent for,
+        * forget about the splitting.  !split[1].suspect signals this.
+        */
+       if (split[1].num_lines < 1)
+               return;
+       split[1].suspect = origin_incref(parent);
+}
+
+/*
+ * split_overlap() divided an existing blame e into up to three parts
+ * in split.  Adjust the linked list of blames in the scoreboard to
+ * reflect the split.
+ */
+static void split_blame(struct scoreboard *sb,
+                       struct blame_entry *split,
+                       struct blame_entry *e)
+{
+       struct blame_entry *new_entry;
+
+       if (split[0].suspect && split[2].suspect) {
+               /* The first part (reuse storage for the existing entry e) */
+               dup_entry(e, &split[0]);
+
+               /* The last part -- me */
+               new_entry = xmalloc(sizeof(*new_entry));
+               memcpy(new_entry, &(split[2]), sizeof(struct blame_entry));
+               add_blame_entry(sb, new_entry);
+
+               /* ... and the middle part -- parent */
+               new_entry = xmalloc(sizeof(*new_entry));
+               memcpy(new_entry, &(split[1]), sizeof(struct blame_entry));
+               add_blame_entry(sb, new_entry);
+       }
+       else if (!split[0].suspect && !split[2].suspect)
+               /*
+                * The parent covers the entire area; reuse storage for
+                * e and replace it with the parent.
+                */
+               dup_entry(e, &split[1]);
+       else if (split[0].suspect) {
+               /* me and then parent */
+               dup_entry(e, &split[0]);
+
+               new_entry = xmalloc(sizeof(*new_entry));
+               memcpy(new_entry, &(split[1]), sizeof(struct blame_entry));
+               add_blame_entry(sb, new_entry);
+       }
+       else {
+               /* parent and then me */
+               dup_entry(e, &split[1]);
+
+               new_entry = xmalloc(sizeof(*new_entry));
+               memcpy(new_entry, &(split[2]), sizeof(struct blame_entry));
+               add_blame_entry(sb, new_entry);
+       }
+
+       if (DEBUG) { /* sanity */
+               struct blame_entry *ent;
+               int lno = sb->ent->lno, corrupt = 0;
+
+               for (ent = sb->ent; ent; ent = ent->next) {
+                       if (lno != ent->lno)
+                               corrupt = 1;
+                       if (ent->s_lno < 0)
+                               corrupt = 1;
+                       lno += ent->num_lines;
+               }
+               if (corrupt) {
+                       lno = sb->ent->lno;
+                       for (ent = sb->ent; ent; ent = ent->next) {
+                               printf("L %8d l %8d n %8d\n",
+                                      lno, ent->lno, ent->num_lines);
+                               lno = ent->lno + ent->num_lines;
+                       }
+                       die("oops");
+               }
+       }
+}
+
+/*
+ * After splitting the blame, the origins used by the
+ * on-stack blame_entry should lose one refcnt each.
+ */
+static void decref_split(struct blame_entry *split)
+{
+       int i;
+
+       for (i = 0; i < 3; i++)
+               origin_decref(split[i].suspect);
+}
+
+/*
+ * Helper for blame_chunk().  blame_entry e is known to overlap with
+ * the patch hunk; split it and pass blame to the parent.
+ */
+static void blame_overlap(struct scoreboard *sb, struct blame_entry *e,
+                         int tlno, int plno, int same,
+                         struct origin *parent)
+{
+       struct blame_entry split[3];
+
+       split_overlap(split, e, tlno, plno, same, parent);
+       if (split[1].suspect)
+               split_blame(sb, split, e);
+       decref_split(split);
+}
+
+/*
+ * Find the line number of the last line the target is suspected for.
+ */
+static int find_last_in_target(struct scoreboard *sb, struct origin *target)
+{
+       struct blame_entry *e;
+       int last_in_target = -1;
+
+       for (e = sb->ent; e; e = e->next) {
+               if (e->guilty || !same_suspect(e->suspect, target))
+                       continue;
+               if (last_in_target < e->s_lno + e->num_lines)
+                       last_in_target = e->s_lno + e->num_lines;
+       }
+       return last_in_target;
+}
+
+/*
+ * Process one hunk from the patch between the current suspect for
+ * blame_entry e and its parent.  Find and split the overlap, and
+ * pass blame to the overlapping part to the parent.
+ */
+static void blame_chunk(struct scoreboard *sb,
+                       int tlno, int plno, int same,
+                       struct origin *target, struct origin *parent)
+{
+       struct blame_entry *e;
+
+       for (e = sb->ent; e; e = e->next) {
+               if (e->guilty || !same_suspect(e->suspect, target))
+                       continue;
+               if (same <= e->s_lno)
+                       continue;
+               if (tlno < e->s_lno + e->num_lines)
+                       blame_overlap(sb, e, tlno, plno, same, parent);
+       }
+}
+
+/*
+ * We are looking at the origin 'target' and aiming to pass blame
+ * for the lines it is suspected to its parent.  Run diff to find
+ * which lines came from parent and pass blame for them.
+ */
+static int pass_blame_to_parent(struct scoreboard *sb,
+                               struct origin *target,
+                               struct origin *parent)
+{
+       int i, last_in_target, plno, tlno;
+       struct patch *patch;
+
+       last_in_target = find_last_in_target(sb, target);
+       if (last_in_target < 0)
+               return 1; /* nothing remains for this target */
+
+       patch = get_patch(parent, target);
+       plno = tlno = 0;
+       for (i = 0; i < patch->num; i++) {
+               struct chunk *chunk = &patch->chunks[i];
+
+               blame_chunk(sb, tlno, plno, chunk->same, target, parent);
+               plno = chunk->p_next;
+               tlno = chunk->t_next;
+       }
+       /* The rest (i.e. anything after tlno) are the same as the parent */
+       blame_chunk(sb, tlno, plno, last_in_target, target, parent);
+
+       free_patch(patch);
+       return 0;
+}
+
+/*
+ * The lines in blame_entry after splitting blames many times can become
+ * very small and trivial, and at some point it becomes pointless to
+ * blame the parents.  E.g. "\t\t}\n\t}\n\n" appears everywhere in any
+ * ordinary C program, and it is not worth to say it was copied from
+ * totally unrelated file in the parent.
+ *
+ * Compute how trivial the lines in the blame_entry are.
+ */
+static unsigned ent_score(struct scoreboard *sb, struct blame_entry *e)
+{
+       unsigned score;
+       const char *cp, *ep;
+
+       if (e->score)
+               return e->score;
+
+       score = 1;
+       cp = nth_line(sb, e->lno);
+       ep = nth_line(sb, e->lno + e->num_lines);
+       while (cp < ep) {
+               unsigned ch = *((unsigned char *)cp);
+               if (isalnum(ch))
+                       score++;
+               cp++;
+       }
+       e->score = score;
+       return score;
+}
+
+/*
+ * best_so_far[] and this[] are both a split of an existing blame_entry
+ * that passes blame to the parent.  Maintain best_so_far the best split
+ * so far, by comparing this and best_so_far and copying this into
+ * bst_so_far as needed.
+ */
+static void copy_split_if_better(struct scoreboard *sb,
+                                struct blame_entry *best_so_far,
+                                struct blame_entry *this)
+{
+       int i;
+
+       if (!this[1].suspect)
+               return;
+       if (best_so_far[1].suspect) {
+               if (ent_score(sb, &this[1]) < ent_score(sb, &best_so_far[1]))
+                       return;
+       }
+
+       for (i = 0; i < 3; i++)
+               origin_incref(this[i].suspect);
+       decref_split(best_so_far);
+       memcpy(best_so_far, this, sizeof(struct blame_entry [3]));
+}
+
+/*
+ * We are looking at a part of the final image represented by
+ * ent (tlno and same are offset by ent->s_lno).
+ * tlno is where we are looking at in the final image.
+ * up to (but not including) same match preimage.
+ * plno is where we are looking at in the preimage.
+ *
+ * <-------------- final image ---------------------->
+ *       <------ent------>
+ *         ^tlno ^same
+ *    <---------preimage----->
+ *         ^plno
+ *
+ * All line numbers are 0-based.
+ */
+static void handle_split(struct scoreboard *sb,
+                        struct blame_entry *ent,
+                        int tlno, int plno, int same,
+                        struct origin *parent,
+                        struct blame_entry *split)
+{
+       if (ent->num_lines <= tlno)
+               return;
+       if (tlno < same) {
+               struct blame_entry this[3];
+               tlno += ent->s_lno;
+               same += ent->s_lno;
+               split_overlap(this, ent, tlno, plno, same, parent);
+               copy_split_if_better(sb, split, this);
+               decref_split(this);
+       }
+}
+
+/*
+ * Find the lines from parent that are the same as ent so that
+ * we can pass blames to it.  file_p has the blob contents for
+ * the parent.
+ */
+static void find_copy_in_blob(struct scoreboard *sb,
+                             struct blame_entry *ent,
+                             struct origin *parent,
+                             struct blame_entry *split,
+                             mmfile_t *file_p)
+{
+       const char *cp;
+       int cnt;
+       mmfile_t file_o;
+       struct patch *patch;
+       int i, plno, tlno;
+
+       /*
+        * Prepare mmfile that contains only the lines in ent.
+        */
+       cp = nth_line(sb, ent->lno);
+       file_o.ptr = (char*) cp;
+       cnt = ent->num_lines;
+
+       while (cnt && cp < sb->final_buf + sb->final_buf_size) {
+               if (*cp++ == '\n')
+                       cnt--;
+       }
+       file_o.size = cp - file_o.ptr;
+
+       patch = compare_buffer(file_p, &file_o, 1);
+
+       /*
+        * file_o is a part of final image we are annotating.
+        * file_p partially may match that image.
+        */
+       memset(split, 0, sizeof(struct blame_entry [3]));
+       plno = tlno = 0;
+       for (i = 0; i < patch->num; i++) {
+               struct chunk *chunk = &patch->chunks[i];
+
+               handle_split(sb, ent, tlno, plno, chunk->same, parent, split);
+               plno = chunk->p_next;
+               tlno = chunk->t_next;
+       }
+       /* remainder, if any, all match the preimage */
+       handle_split(sb, ent, tlno, plno, ent->num_lines, parent, split);
+       free_patch(patch);
+}
+
+/*
+ * See if lines currently target is suspected for can be attributed to
+ * parent.
+ */
+static int find_move_in_parent(struct scoreboard *sb,
+                              struct origin *target,
+                              struct origin *parent)
+{
+       int last_in_target, made_progress;
+       struct blame_entry *e, split[3];
+       mmfile_t file_p;
+
+       last_in_target = find_last_in_target(sb, target);
+       if (last_in_target < 0)
+               return 1; /* nothing remains for this target */
+
+       fill_origin_blob(parent, &file_p);
+       if (!file_p.ptr)
+               return 0;
+
+       made_progress = 1;
+       while (made_progress) {
+               made_progress = 0;
+               for (e = sb->ent; e; e = e->next) {
+                       if (e->guilty || !same_suspect(e->suspect, target))
+                               continue;
+                       find_copy_in_blob(sb, e, parent, split, &file_p);
+                       if (split[1].suspect &&
+                           blame_move_score < ent_score(sb, &split[1])) {
+                               split_blame(sb, split, e);
+                               made_progress = 1;
+                       }
+                       decref_split(split);
+               }
+       }
+       return 0;
+}
+
+struct blame_list {
+       struct blame_entry *ent;
+       struct blame_entry split[3];
+};
+
+/*
+ * Count the number of entries the target is suspected for,
+ * and prepare a list of entry and the best split.
+ */
+static struct blame_list *setup_blame_list(struct scoreboard *sb,
+                                          struct origin *target,
+                                          int *num_ents_p)
+{
+       struct blame_entry *e;
+       int num_ents, i;
+       struct blame_list *blame_list = NULL;
+
+       for (e = sb->ent, num_ents = 0; e; e = e->next)
+               if (!e->guilty && same_suspect(e->suspect, target))
+                       num_ents++;
+       if (num_ents) {
+               blame_list = xcalloc(num_ents, sizeof(struct blame_list));
+               for (e = sb->ent, i = 0; e; e = e->next)
+                       if (!e->guilty && same_suspect(e->suspect, target))
+                               blame_list[i++].ent = e;
+       }
+       *num_ents_p = num_ents;
+       return blame_list;
+}
+
+/*
+ * For lines target is suspected for, see if we can find code movement
+ * across file boundary from the parent commit.  porigin is the path
+ * in the parent we already tried.
+ */
+static int find_copy_in_parent(struct scoreboard *sb,
+                              struct origin *target,
+                              struct commit *parent,
+                              struct origin *porigin,
+                              int opt)
+{
+       struct diff_options diff_opts;
+       const char *paths[1];
+       int i, j;
+       int retval;
+       struct blame_list *blame_list;
+       int num_ents;
+
+       blame_list = setup_blame_list(sb, target, &num_ents);
+       if (!blame_list)
+               return 1; /* nothing remains for this target */
+
+       diff_setup(&diff_opts);
+       DIFF_OPT_SET(&diff_opts, RECURSIVE);
+       diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+
+       paths[0] = NULL;
+       diff_tree_setup_paths(paths, &diff_opts);
+       if (diff_setup_done(&diff_opts) < 0)
+               die("diff-setup");
+
+       /* Try "find copies harder" on new path if requested;
+        * we do not want to use diffcore_rename() actually to
+        * match things up; find_copies_harder is set only to
+        * force diff_tree_sha1() to feed all filepairs to diff_queue,
+        * and this code needs to be after diff_setup_done(), which
+        * usually makes find-copies-harder imply copy detection.
+        */
+       if ((opt & PICKAXE_BLAME_COPY_HARDEST)
+           || ((opt & PICKAXE_BLAME_COPY_HARDER)
+               && (!porigin || strcmp(target->path, porigin->path))))
+               DIFF_OPT_SET(&diff_opts, FIND_COPIES_HARDER);
+
+       if (is_null_sha1(target->commit->object.sha1))
+               do_diff_cache(parent->tree->object.sha1, &diff_opts);
+       else
+               diff_tree_sha1(parent->tree->object.sha1,
+                              target->commit->tree->object.sha1,
+                              "", &diff_opts);
+
+       if (!DIFF_OPT_TST(&diff_opts, FIND_COPIES_HARDER))
+               diffcore_std(&diff_opts);
+
+       retval = 0;
+       while (1) {
+               int made_progress = 0;
+
+               for (i = 0; i < diff_queued_diff.nr; i++) {
+                       struct diff_filepair *p = diff_queued_diff.queue[i];
+                       struct origin *norigin;
+                       mmfile_t file_p;
+                       struct blame_entry this[3];
+
+                       if (!DIFF_FILE_VALID(p->one))
+                               continue; /* does not exist in parent */
+                       if (porigin && !strcmp(p->one->path, porigin->path))
+                               /* find_move already dealt with this path */
+                               continue;
+
+                       norigin = get_origin(sb, parent, p->one->path);
+                       hashcpy(norigin->blob_sha1, p->one->sha1);
+                       fill_origin_blob(norigin, &file_p);
+                       if (!file_p.ptr)
+                               continue;
+
+                       for (j = 0; j < num_ents; j++) {
+                               find_copy_in_blob(sb, blame_list[j].ent,
+                                                 norigin, this, &file_p);
+                               copy_split_if_better(sb, blame_list[j].split,
+                                                    this);
+                               decref_split(this);
+                       }
+                       origin_decref(norigin);
+               }
+
+               for (j = 0; j < num_ents; j++) {
+                       struct blame_entry *split = blame_list[j].split;
+                       if (split[1].suspect &&
+                           blame_copy_score < ent_score(sb, &split[1])) {
+                               split_blame(sb, split, blame_list[j].ent);
+                               made_progress = 1;
+                       }
+                       decref_split(split);
+               }
+               free(blame_list);
+
+               if (!made_progress)
+                       break;
+               blame_list = setup_blame_list(sb, target, &num_ents);
+               if (!blame_list) {
+                       retval = 1;
+                       break;
+               }
+       }
+       diff_flush(&diff_opts);
+
+       return retval;
+}
+
+/*
+ * The blobs of origin and porigin exactly match, so everything
+ * origin is suspected for can be blamed on the parent.
+ */
+static void pass_whole_blame(struct scoreboard *sb,
+                            struct origin *origin, struct origin *porigin)
+{
+       struct blame_entry *e;
+
+       if (!porigin->file.ptr && origin->file.ptr) {
+               /* Steal its file */
+               porigin->file = origin->file;
+               origin->file.ptr = NULL;
+       }
+       for (e = sb->ent; e; e = e->next) {
+               if (!same_suspect(e->suspect, origin))
+                       continue;
+               origin_incref(porigin);
+               origin_decref(e->suspect);
+               e->suspect = porigin;
+       }
+}
+
+#define MAXPARENT 16
+
+static void pass_blame(struct scoreboard *sb, struct origin *origin, int opt)
+{
+       int i, pass;
+       struct commit *commit = origin->commit;
+       struct commit_list *parent;
+       struct origin *parent_origin[MAXPARENT], *porigin;
+
+       memset(parent_origin, 0, sizeof(parent_origin));
+
+       /* The first pass looks for unrenamed path to optimize for
+        * common cases, then we look for renames in the second pass.
+        */
+       for (pass = 0; pass < 2; pass++) {
+               struct origin *(*find)(struct scoreboard *,
+                                      struct commit *, struct origin *);
+               find = pass ? find_rename : find_origin;
+
+               for (i = 0, parent = commit->parents;
+                    i < MAXPARENT && parent;
+                    parent = parent->next, i++) {
+                       struct commit *p = parent->item;
+                       int j, same;
+
+                       if (parent_origin[i])
+                               continue;
+                       if (parse_commit(p))
+                               continue;
+                       porigin = find(sb, p, origin);
+                       if (!porigin)
+                               continue;
+                       if (!hashcmp(porigin->blob_sha1, origin->blob_sha1)) {
+                               pass_whole_blame(sb, origin, porigin);
+                               origin_decref(porigin);
+                               goto finish;
+                       }
+                       for (j = same = 0; j < i; j++)
+                               if (parent_origin[j] &&
+                                   !hashcmp(parent_origin[j]->blob_sha1,
+                                            porigin->blob_sha1)) {
+                                       same = 1;
+                                       break;
+                               }
+                       if (!same)
+                               parent_origin[i] = porigin;
+                       else
+                               origin_decref(porigin);
+               }
+       }
+
+       num_commits++;
+       for (i = 0, parent = commit->parents;
+            i < MAXPARENT && parent;
+            parent = parent->next, i++) {
+               struct origin *porigin = parent_origin[i];
+               if (!porigin)
+                       continue;
+               if (pass_blame_to_parent(sb, origin, porigin))
+                       goto finish;
+       }
+
+       /*
+        * Optionally find moves in parents' files.
+        */
+       if (opt & PICKAXE_BLAME_MOVE)
+               for (i = 0, parent = commit->parents;
+                    i < MAXPARENT && parent;
+                    parent = parent->next, i++) {
+                       struct origin *porigin = parent_origin[i];
+                       if (!porigin)
+                               continue;
+                       if (find_move_in_parent(sb, origin, porigin))
+                               goto finish;
+               }
+
+       /*
+        * Optionally find copies from parents' files.
+        */
+       if (opt & PICKAXE_BLAME_COPY)
+               for (i = 0, parent = commit->parents;
+                    i < MAXPARENT && parent;
+                    parent = parent->next, i++) {
+                       struct origin *porigin = parent_origin[i];
+                       if (find_copy_in_parent(sb, origin, parent->item,
+                                               porigin, opt))
+                               goto finish;
+               }
+
+ finish:
+       for (i = 0; i < MAXPARENT; i++)
+               origin_decref(parent_origin[i]);
+}
+
+/*
+ * Information on commits, used for output.
+ */
+struct commit_info
+{
+       const char *author;
+       const char *author_mail;
+       unsigned long author_time;
+       const char *author_tz;
+
+       /* filled only when asked for details */
+       const char *committer;
+       const char *committer_mail;
+       unsigned long committer_time;
+       const char *committer_tz;
+
+       const char *summary;
+};
+
+/*
+ * Parse author/committer line in the commit object buffer
+ */
+static void get_ac_line(const char *inbuf, const char *what,
+                       int bufsz, char *person, const char **mail,
+                       unsigned long *time, const char **tz)
+{
+       int len, tzlen, maillen;
+       char *tmp, *endp, *timepos;
+
+       tmp = strstr(inbuf, what);
+       if (!tmp)
+               goto error_out;
+       tmp += strlen(what);
+       endp = strchr(tmp, '\n');
+       if (!endp)
+               len = strlen(tmp);
+       else
+               len = endp - tmp;
+       if (bufsz <= len) {
+       error_out:
+               /* Ugh */
+               *mail = *tz = "(unknown)";
+               *time = 0;
+               return;
+       }
+       memcpy(person, tmp, len);
+
+       tmp = person;
+       tmp += len;
+       *tmp = 0;
+       while (*tmp != ' ')
+               tmp--;
+       *tz = tmp+1;
+       tzlen = (person+len)-(tmp+1);
+
+       *tmp = 0;
+       while (*tmp != ' ')
+               tmp--;
+       *time = strtoul(tmp, NULL, 10);
+       timepos = tmp;
+
+       *tmp = 0;
+       while (*tmp != ' ')
+               tmp--;
+       *mail = tmp + 1;
+       *tmp = 0;
+       maillen = timepos - tmp;
+
+       if (!mailmap.nr)
+               return;
+
+       /*
+        * mailmap expansion may make the name longer.
+        * make room by pushing stuff down.
+        */
+       tmp = person + bufsz - (tzlen + 1);
+       memmove(tmp, *tz, tzlen);
+       tmp[tzlen] = 0;
+       *tz = tmp;
+
+       tmp = tmp - (maillen + 1);
+       memmove(tmp, *mail, maillen);
+       tmp[maillen] = 0;
+       *mail = tmp;
+
+       /*
+        * Now, convert e-mail using mailmap
+        */
+       map_email(&mailmap, tmp + 1, person, tmp-person-1);
+}
+
+static void get_commit_info(struct commit *commit,
+                           struct commit_info *ret,
+                           int detailed)
+{
+       int len;
+       char *tmp, *endp;
+       static char author_buf[1024];
+       static char committer_buf[1024];
+       static char summary_buf[1024];
+
+       /*
+        * We've operated without save_commit_buffer, so
+        * we now need to populate them for output.
+        */
+       if (!commit->buffer) {
+               enum object_type type;
+               unsigned long size;
+               commit->buffer =
+                       read_sha1_file(commit->object.sha1, &type, &size);
+               if (!commit->buffer)
+                       die("Cannot read commit %s",
+                           sha1_to_hex(commit->object.sha1));
+       }
+       ret->author = author_buf;
+       get_ac_line(commit->buffer, "\nauthor ",
+                   sizeof(author_buf), author_buf, &ret->author_mail,
+                   &ret->author_time, &ret->author_tz);
+
+       if (!detailed)
+               return;
+
+       ret->committer = committer_buf;
+       get_ac_line(commit->buffer, "\ncommitter ",
+                   sizeof(committer_buf), committer_buf, &ret->committer_mail,
+                   &ret->committer_time, &ret->committer_tz);
+
+       ret->summary = summary_buf;
+       tmp = strstr(commit->buffer, "\n\n");
+       if (!tmp) {
+       error_out:
+               sprintf(summary_buf, "(%s)", sha1_to_hex(commit->object.sha1));
+               return;
+       }
+       tmp += 2;
+       endp = strchr(tmp, '\n');
+       if (!endp)
+               endp = tmp + strlen(tmp);
+       len = endp - tmp;
+       if (len >= sizeof(summary_buf) || len == 0)
+               goto error_out;
+       memcpy(summary_buf, tmp, len);
+       summary_buf[len] = 0;
+}
+
+/*
+ * To allow LF and other nonportable characters in pathnames,
+ * they are c-style quoted as needed.
+ */
+static void write_filename_info(const char *path)
+{
+       printf("filename ");
+       write_name_quoted(path, stdout, '\n');
+}
+
+/*
+ * The blame_entry is found to be guilty for the range.  Mark it
+ * as such, and show it in incremental output.
+ */
+static void found_guilty_entry(struct blame_entry *ent)
+{
+       if (ent->guilty)
+               return;
+       ent->guilty = 1;
+       if (incremental) {
+               struct origin *suspect = ent->suspect;
+
+               printf("%s %d %d %d\n",
+                      sha1_to_hex(suspect->commit->object.sha1),
+                      ent->s_lno + 1, ent->lno + 1, ent->num_lines);
+               if (!(suspect->commit->object.flags & METAINFO_SHOWN)) {
+                       struct commit_info ci;
+                       suspect->commit->object.flags |= METAINFO_SHOWN;
+                       get_commit_info(suspect->commit, &ci, 1);
+                       printf("author %s\n", ci.author);
+                       printf("author-mail %s\n", ci.author_mail);
+                       printf("author-time %lu\n", ci.author_time);
+                       printf("author-tz %s\n", ci.author_tz);
+                       printf("committer %s\n", ci.committer);
+                       printf("committer-mail %s\n", ci.committer_mail);
+                       printf("committer-time %lu\n", ci.committer_time);
+                       printf("committer-tz %s\n", ci.committer_tz);
+                       printf("summary %s\n", ci.summary);
+                       if (suspect->commit->object.flags & UNINTERESTING)
+                               printf("boundary\n");
+               }
+               write_filename_info(suspect->path);
+               maybe_flush_or_die(stdout, "stdout");
+       }
+}
+
+/*
+ * The main loop -- while the scoreboard has lines whose true origin
+ * is still unknown, pick one blame_entry, and allow its current
+ * suspect to pass blames to its parents.
+ */
+static void assign_blame(struct scoreboard *sb, struct rev_info *revs, int opt)
+{
+       while (1) {
+               struct blame_entry *ent;
+               struct commit *commit;
+               struct origin *suspect = NULL;
+
+               /* find one suspect to break down */
+               for (ent = sb->ent; !suspect && ent; ent = ent->next)
+                       if (!ent->guilty)
+                               suspect = ent->suspect;
+               if (!suspect)
+                       return; /* all done */
+
+               /*
+                * We will use this suspect later in the loop,
+                * so hold onto it in the meantime.
+                */
+               origin_incref(suspect);
+               commit = suspect->commit;
+               if (!commit->object.parsed)
+                       parse_commit(commit);
+               if (!(commit->object.flags & UNINTERESTING) &&
+                   !(revs->max_age != -1 && commit->date < revs->max_age))
+                       pass_blame(sb, suspect, opt);
+               else {
+                       commit->object.flags |= UNINTERESTING;
+                       if (commit->object.parsed)
+                               mark_parents_uninteresting(commit);
+               }
+               /* treat root commit as boundary */
+               if (!commit->parents && !show_root)
+                       commit->object.flags |= UNINTERESTING;
+
+               /* Take responsibility for the remaining entries */
+               for (ent = sb->ent; ent; ent = ent->next)
+                       if (same_suspect(ent->suspect, suspect))
+                               found_guilty_entry(ent);
+               origin_decref(suspect);
+
+               if (DEBUG) /* sanity */
+                       sanity_check_refcnt(sb);
+       }
+}
+
+static const char *format_time(unsigned long time, const char *tz_str,
+                              int show_raw_time)
+{
+       static char time_buf[128];
+       time_t t = time;
+       int minutes, tz;
+       struct tm *tm;
+
+       if (show_raw_time) {
+               sprintf(time_buf, "%lu %s", time, tz_str);
+               return time_buf;
+       }
+
+       tz = atoi(tz_str);
+       minutes = tz < 0 ? -tz : tz;
+       minutes = (minutes / 100)*60 + (minutes % 100);
+       minutes = tz < 0 ? -minutes : minutes;
+       t = time + minutes * 60;
+       tm = gmtime(&t);
+
+       strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S ", tm);
+       strcat(time_buf, tz_str);
+       return time_buf;
+}
+
+#define OUTPUT_ANNOTATE_COMPAT 001
+#define OUTPUT_LONG_OBJECT_NAME        002
+#define OUTPUT_RAW_TIMESTAMP   004
+#define OUTPUT_PORCELAIN       010
+#define OUTPUT_SHOW_NAME       020
+#define OUTPUT_SHOW_NUMBER     040
+#define OUTPUT_SHOW_SCORE      0100
+#define OUTPUT_NO_AUTHOR       0200
+
+static void emit_porcelain(struct scoreboard *sb, struct blame_entry *ent)
+{
+       int cnt;
+       const char *cp;
+       struct origin *suspect = ent->suspect;
+       char hex[41];
+
+       strcpy(hex, sha1_to_hex(suspect->commit->object.sha1));
+       printf("%s%c%d %d %d\n",
+              hex,
+              ent->guilty ? ' ' : '*', // purely for debugging
+              ent->s_lno + 1,
+              ent->lno + 1,
+              ent->num_lines);
+       if (!(suspect->commit->object.flags & METAINFO_SHOWN)) {
+               struct commit_info ci;
+               suspect->commit->object.flags |= METAINFO_SHOWN;
+               get_commit_info(suspect->commit, &ci, 1);
+               printf("author %s\n", ci.author);
+               printf("author-mail %s\n", ci.author_mail);
+               printf("author-time %lu\n", ci.author_time);
+               printf("author-tz %s\n", ci.author_tz);
+               printf("committer %s\n", ci.committer);
+               printf("committer-mail %s\n", ci.committer_mail);
+               printf("committer-time %lu\n", ci.committer_time);
+               printf("committer-tz %s\n", ci.committer_tz);
+               write_filename_info(suspect->path);
+               printf("summary %s\n", ci.summary);
+               if (suspect->commit->object.flags & UNINTERESTING)
+                       printf("boundary\n");
+       }
+       else if (suspect->commit->object.flags & MORE_THAN_ONE_PATH)
+               write_filename_info(suspect->path);
+
+       cp = nth_line(sb, ent->lno);
+       for (cnt = 0; cnt < ent->num_lines; cnt++) {
+               char ch;
+               if (cnt)
+                       printf("%s %d %d\n", hex,
+                              ent->s_lno + 1 + cnt,
+                              ent->lno + 1 + cnt);
+               putchar('\t');
+               do {
+                       ch = *cp++;
+                       putchar(ch);
+               } while (ch != '\n' &&
+                        cp < sb->final_buf + sb->final_buf_size);
+       }
+}
+
+static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt)
+{
+       int cnt;
+       const char *cp;
+       struct origin *suspect = ent->suspect;
+       struct commit_info ci;
+       char hex[41];
+       int show_raw_time = !!(opt & OUTPUT_RAW_TIMESTAMP);
+
+       get_commit_info(suspect->commit, &ci, 1);
+       strcpy(hex, sha1_to_hex(suspect->commit->object.sha1));
+
+       cp = nth_line(sb, ent->lno);
+       for (cnt = 0; cnt < ent->num_lines; cnt++) {
+               char ch;
+               int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? 40 : 8;
+
+               if (suspect->commit->object.flags & UNINTERESTING) {
+                       if (blank_boundary)
+                               memset(hex, ' ', length);
+                       else if (!cmd_is_annotate) {
+                               length--;
+                               putchar('^');
+                       }
+               }
+
+               printf("%.*s", length, hex);
+               if (opt & OUTPUT_ANNOTATE_COMPAT)
+                       printf("\t(%10s\t%10s\t%d)", ci.author,
+                              format_time(ci.author_time, ci.author_tz,
+                                          show_raw_time),
+                              ent->lno + 1 + cnt);
+               else {
+                       if (opt & OUTPUT_SHOW_SCORE)
+                               printf(" %*d %02d",
+                                      max_score_digits, ent->score,
+                                      ent->suspect->refcnt);
+                       if (opt & OUTPUT_SHOW_NAME)
+                               printf(" %-*.*s", longest_file, longest_file,
+                                      suspect->path);
+                       if (opt & OUTPUT_SHOW_NUMBER)
+                               printf(" %*d", max_orig_digits,
+                                      ent->s_lno + 1 + cnt);
+
+                       if (!(opt & OUTPUT_NO_AUTHOR))
+                               printf(" (%-*.*s %10s",
+                                      longest_author, longest_author,
+                                      ci.author,
+                                      format_time(ci.author_time,
+                                                  ci.author_tz,
+                                                  show_raw_time));
+                       printf(" %*d) ",
+                              max_digits, ent->lno + 1 + cnt);
+               }
+               do {
+                       ch = *cp++;
+                       putchar(ch);
+               } while (ch != '\n' &&
+                        cp < sb->final_buf + sb->final_buf_size);
+       }
+}
+
+static void output(struct scoreboard *sb, int option)
+{
+       struct blame_entry *ent;
+
+       if (option & OUTPUT_PORCELAIN) {
+               for (ent = sb->ent; ent; ent = ent->next) {
+                       struct blame_entry *oth;
+                       struct origin *suspect = ent->suspect;
+                       struct commit *commit = suspect->commit;
+                       if (commit->object.flags & MORE_THAN_ONE_PATH)
+                               continue;
+                       for (oth = ent->next; oth; oth = oth->next) {
+                               if ((oth->suspect->commit != commit) ||
+                                   !strcmp(oth->suspect->path, suspect->path))
+                                       continue;
+                               commit->object.flags |= MORE_THAN_ONE_PATH;
+                               break;
+                       }
+               }
+       }
+
+       for (ent = sb->ent; ent; ent = ent->next) {
+               if (option & OUTPUT_PORCELAIN)
+                       emit_porcelain(sb, ent);
+               else {
+                       emit_other(sb, ent, option);
+               }
+       }
+}
+
+/*
+ * To allow quick access to the contents of nth line in the
+ * final image, prepare an index in the scoreboard.
+ */
+static int prepare_lines(struct scoreboard *sb)
+{
+       const char *buf = sb->final_buf;
+       unsigned long len = sb->final_buf_size;
+       int num = 0, incomplete = 0, bol = 1;
+
+       if (len && buf[len-1] != '\n')
+               incomplete++; /* incomplete line at the end */
+       while (len--) {
+               if (bol) {
+                       sb->lineno = xrealloc(sb->lineno,
+                                             sizeof(int* ) * (num + 1));
+                       sb->lineno[num] = buf - sb->final_buf;
+                       bol = 0;
+               }
+               if (*buf++ == '\n') {
+                       num++;
+                       bol = 1;
+               }
+       }
+       sb->lineno = xrealloc(sb->lineno,
+                             sizeof(int* ) * (num + incomplete + 1));
+       sb->lineno[num + incomplete] = buf - sb->final_buf;
+       sb->num_lines = num + incomplete;
+       return sb->num_lines;
+}
+
+/*
+ * Add phony grafts for use with -S; this is primarily to
+ * support git-cvsserver that wants to give a linear history
+ * to its clients.
+ */
+static int read_ancestry(const char *graft_file)
+{
+       FILE *fp = fopen(graft_file, "r");
+       char buf[1024];
+       if (!fp)
+               return -1;
+       while (fgets(buf, sizeof(buf), fp)) {
+               /* The format is just "Commit Parent1 Parent2 ...\n" */
+               int len = strlen(buf);
+               struct commit_graft *graft = read_graft_line(buf, len);
+               if (graft)
+                       register_commit_graft(graft, 0);
+       }
+       fclose(fp);
+       return 0;
+}
+
+/*
+ * How many columns do we need to show line numbers in decimal?
+ */
+static int lineno_width(int lines)
+{
+       int i, width;
+
+       for (width = 1, i = 10; i <= lines + 1; width++)
+               i *= 10;
+       return width;
+}
+
+/*
+ * How many columns do we need to show line numbers, authors,
+ * and filenames?
+ */
+static void find_alignment(struct scoreboard *sb, int *option)
+{
+       int longest_src_lines = 0;
+       int longest_dst_lines = 0;
+       unsigned largest_score = 0;
+       struct blame_entry *e;
+
+       for (e = sb->ent; e; e = e->next) {
+               struct origin *suspect = e->suspect;
+               struct commit_info ci;
+               int num;
+
+               if (strcmp(suspect->path, sb->path))
+                       *option |= OUTPUT_SHOW_NAME;
+               num = strlen(suspect->path);
+               if (longest_file < num)
+                       longest_file = num;
+               if (!(suspect->commit->object.flags & METAINFO_SHOWN)) {
+                       suspect->commit->object.flags |= METAINFO_SHOWN;
+                       get_commit_info(suspect->commit, &ci, 1);
+                       num = strlen(ci.author);
+                       if (longest_author < num)
+                               longest_author = num;
+               }
+               num = e->s_lno + e->num_lines;
+               if (longest_src_lines < num)
+                       longest_src_lines = num;
+               num = e->lno + e->num_lines;
+               if (longest_dst_lines < num)
+                       longest_dst_lines = num;
+               if (largest_score < ent_score(sb, e))
+                       largest_score = ent_score(sb, e);
+       }
+       max_orig_digits = lineno_width(longest_src_lines);
+       max_digits = lineno_width(longest_dst_lines);
+       max_score_digits = lineno_width(largest_score);
+}
+
+/*
+ * For debugging -- origin is refcounted, and this asserts that
+ * we do not underflow.
+ */
+static void sanity_check_refcnt(struct scoreboard *sb)
+{
+       int baa = 0;
+       struct blame_entry *ent;
+
+       for (ent = sb->ent; ent; ent = ent->next) {
+               /* Nobody should have zero or negative refcnt */
+               if (ent->suspect->refcnt <= 0) {
+                       fprintf(stderr, "%s in %s has negative refcnt %d\n",
+                               ent->suspect->path,
+                               sha1_to_hex(ent->suspect->commit->object.sha1),
+                               ent->suspect->refcnt);
+                       baa = 1;
+               }
+       }
+       for (ent = sb->ent; ent; ent = ent->next) {
+               /* Mark the ones that haven't been checked */
+               if (0 < ent->suspect->refcnt)
+                       ent->suspect->refcnt = -ent->suspect->refcnt;
+       }
+       for (ent = sb->ent; ent; ent = ent->next) {
+               /*
+                * ... then pick each and see if they have the the
+                * correct refcnt.
+                */
+               int found;
+               struct blame_entry *e;
+               struct origin *suspect = ent->suspect;
+
+               if (0 < suspect->refcnt)
+                       continue;
+               suspect->refcnt = -suspect->refcnt; /* Unmark */
+               for (found = 0, e = sb->ent; e; e = e->next) {
+                       if (e->suspect != suspect)
+                               continue;
+                       found++;
+               }
+               if (suspect->refcnt != found) {
+                       fprintf(stderr, "%s in %s has refcnt %d, not %d\n",
+                               ent->suspect->path,
+                               sha1_to_hex(ent->suspect->commit->object.sha1),
+                               ent->suspect->refcnt, found);
+                       baa = 2;
+               }
+       }
+       if (baa) {
+               int opt = 0160;
+               find_alignment(sb, &opt);
+               output(sb, opt);
+               die("Baa %d!", baa);
+       }
+}
+
+/*
+ * Used for the command line parsing; check if the path exists
+ * in the working tree.
+ */
+static int has_path_in_work_tree(const char *path)
+{
+       struct stat st;
+       return !lstat(path, &st);
+}
+
+static unsigned parse_score(const char *arg)
+{
+       char *end;
+       unsigned long score = strtoul(arg, &end, 10);
+       if (*end)
+               return 0;
+       return score;
+}
+
+static const char *add_prefix(const char *prefix, const char *path)
+{
+       if (!prefix || !prefix[0])
+               return path;
+       return prefix_path(prefix, strlen(prefix), path);
+}
+
+/*
+ * Parsing of (comma separated) one item in the -L option
+ */
+static const char *parse_loc(const char *spec,
+                            struct scoreboard *sb, long lno,
+                            long begin, long *ret)
+{
+       char *term;
+       const char *line;
+       long num;
+       int reg_error;
+       regex_t regexp;
+       regmatch_t match[1];
+
+       /* Allow "-L <something>,+20" to mean starting at <something>
+        * for 20 lines, or "-L <something>,-5" for 5 lines ending at
+        * <something>.
+        */
+       if (1 < begin && (spec[0] == '+' || spec[0] == '-')) {
+               num = strtol(spec + 1, &term, 10);
+               if (term != spec + 1) {
+                       if (spec[0] == '-')
+                               num = 0 - num;
+                       if (0 < num)
+                               *ret = begin + num - 2;
+                       else if (!num)
+                               *ret = begin;
+                       else
+                               *ret = begin + num;
+                       return term;
+               }
+               return spec;
+       }
+       num = strtol(spec, &term, 10);
+       if (term != spec) {
+               *ret = num;
+               return term;
+       }
+       if (spec[0] != '/')
+               return spec;
+
+       /* it could be a regexp of form /.../ */
+       for (term = (char*) spec + 1; *term && *term != '/'; term++) {
+               if (*term == '\\')
+                       term++;
+       }
+       if (*term != '/')
+               return spec;
+
+       /* try [spec+1 .. term-1] as regexp */
+       *term = 0;
+       begin--; /* input is in human terms */
+       line = nth_line(sb, begin);
+
+       if (!(reg_error = regcomp(&regexp, spec + 1, REG_NEWLINE)) &&
+           !(reg_error = regexec(&regexp, line, 1, match, 0))) {
+               const char *cp = line + match[0].rm_so;
+               const char *nline;
+
+               while (begin++ < lno) {
+                       nline = nth_line(sb, begin);
+                       if (line <= cp && cp < nline)
+                               break;
+                       line = nline;
+               }
+               *ret = begin;
+               regfree(&regexp);
+               *term++ = '/';
+               return term;
+       }
+       else {
+               char errbuf[1024];
+               regerror(reg_error, &regexp, errbuf, 1024);
+               die("-L parameter '%s': %s", spec + 1, errbuf);
+       }
+}
+
+/*
+ * Parsing of -L option
+ */
+static void prepare_blame_range(struct scoreboard *sb,
+                               const char *bottomtop,
+                               long lno,
+                               long *bottom, long *top)
+{
+       const char *term;
+
+       term = parse_loc(bottomtop, sb, lno, 1, bottom);
+       if (*term == ',') {
+               term = parse_loc(term + 1, sb, lno, *bottom + 1, top);
+               if (*term)
+                       usage(blame_usage);
+       }
+       if (*term)
+               usage(blame_usage);
+}
+
+static int git_blame_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "blame.showroot")) {
+               show_root = git_config_bool(var, value);
+               return 0;
+       }
+       if (!strcmp(var, "blame.blankboundary")) {
+               blank_boundary = git_config_bool(var, value);
+               return 0;
+       }
+       return git_default_config(var, value);
+}
+
+static struct commit *fake_working_tree_commit(const char *path, const char *contents_from)
+{
+       struct commit *commit;
+       struct origin *origin;
+       unsigned char head_sha1[20];
+       struct strbuf buf;
+       const char *ident;
+       time_t now;
+       int size, len;
+       struct cache_entry *ce;
+       unsigned mode;
+
+       if (get_sha1("HEAD", head_sha1))
+               die("No such ref: HEAD");
+
+       time(&now);
+       commit = xcalloc(1, sizeof(*commit));
+       commit->parents = xcalloc(1, sizeof(*commit->parents));
+       commit->parents->item = lookup_commit_reference(head_sha1);
+       commit->object.parsed = 1;
+       commit->date = now;
+       commit->object.type = OBJ_COMMIT;
+
+       origin = make_origin(commit, path);
+
+       strbuf_init(&buf, 0);
+       if (!contents_from || strcmp("-", contents_from)) {
+               struct stat st;
+               const char *read_from;
+               unsigned long fin_size;
+
+               if (contents_from) {
+                       if (stat(contents_from, &st) < 0)
+                               die("Cannot stat %s", contents_from);
+                       read_from = contents_from;
+               }
+               else {
+                       if (lstat(path, &st) < 0)
+                               die("Cannot lstat %s", path);
+                       read_from = path;
+               }
+               fin_size = xsize_t(st.st_size);
+               mode = canon_mode(st.st_mode);
+               switch (st.st_mode & S_IFMT) {
+               case S_IFREG:
+                       if (strbuf_read_file(&buf, read_from, st.st_size) != st.st_size)
+                               die("cannot open or read %s", read_from);
+                       break;
+               case S_IFLNK:
+                       if (readlink(read_from, buf.buf, buf.alloc) != fin_size)
+                               die("cannot readlink %s", read_from);
+                       buf.len = fin_size;
+                       break;
+               default:
+                       die("unsupported file type %s", read_from);
+               }
+       }
+       else {
+               /* Reading from stdin */
+               contents_from = "standard input";
+               mode = 0;
+               if (strbuf_read(&buf, 0, 0) < 0)
+                       die("read error %s from stdin", strerror(errno));
+       }
+       convert_to_git(path, buf.buf, buf.len, &buf);
+       origin->file.ptr = buf.buf;
+       origin->file.size = buf.len;
+       pretend_sha1_file(buf.buf, buf.len, OBJ_BLOB, origin->blob_sha1);
+       commit->util = origin;
+
+       /*
+        * Read the current index, replace the path entry with
+        * origin->blob_sha1 without mucking with its mode or type
+        * bits; we are not going to write this index out -- we just
+        * want to run "diff-index --cached".
+        */
+       discard_cache();
+       read_cache();
+
+       len = strlen(path);
+       if (!mode) {
+               int pos = cache_name_pos(path, len);
+               if (0 <= pos)
+                       mode = ntohl(active_cache[pos]->ce_mode);
+               else
+                       /* Let's not bother reading from HEAD tree */
+                       mode = S_IFREG | 0644;
+       }
+       size = cache_entry_size(len);
+       ce = xcalloc(1, size);
+       hashcpy(ce->sha1, origin->blob_sha1);
+       memcpy(ce->name, path, len);
+       ce->ce_flags = create_ce_flags(len, 0);
+       ce->ce_mode = create_ce_mode(mode);
+       add_cache_entry(ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE);
+
+       /*
+        * We are not going to write this out, so this does not matter
+        * right now, but someday we might optimize diff-index --cached
+        * with cache-tree information.
+        */
+       cache_tree_invalidate_path(active_cache_tree, path);
+
+       commit->buffer = xmalloc(400);
+       ident = fmt_ident("Not Committed Yet", "not.committed.yet", NULL, 0);
+       snprintf(commit->buffer, 400,
+               "tree 0000000000000000000000000000000000000000\n"
+               "parent %s\n"
+               "author %s\n"
+               "committer %s\n\n"
+               "Version of %s from %s\n",
+               sha1_to_hex(head_sha1),
+               ident, ident, path, contents_from ? contents_from : path);
+       return commit;
+}
+
+int cmd_blame(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info revs;
+       const char *path;
+       struct scoreboard sb;
+       struct origin *o;
+       struct blame_entry *ent;
+       int i, seen_dashdash, unk, opt;
+       long bottom, top, lno;
+       int output_option = 0;
+       int show_stats = 0;
+       const char *revs_file = NULL;
+       const char *final_commit_name = NULL;
+       enum object_type type;
+       const char *bottomtop = NULL;
+       const char *contents_from = NULL;
+
+       cmd_is_annotate = !strcmp(argv[0], "annotate");
+
+       git_config(git_blame_config);
+       save_commit_buffer = 0;
+
+       opt = 0;
+       seen_dashdash = 0;
+       for (unk = i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (*arg != '-')
+                       break;
+               else if (!strcmp("-b", arg))
+                       blank_boundary = 1;
+               else if (!strcmp("--root", arg))
+                       show_root = 1;
+               else if (!strcmp(arg, "--show-stats"))
+                       show_stats = 1;
+               else if (!strcmp("-c", arg))
+                       output_option |= OUTPUT_ANNOTATE_COMPAT;
+               else if (!strcmp("-t", arg))
+                       output_option |= OUTPUT_RAW_TIMESTAMP;
+               else if (!strcmp("-l", arg))
+                       output_option |= OUTPUT_LONG_OBJECT_NAME;
+               else if (!strcmp("-s", arg))
+                       output_option |= OUTPUT_NO_AUTHOR;
+               else if (!strcmp("-w", arg))
+                       xdl_opts |= XDF_IGNORE_WHITESPACE;
+               else if (!strcmp("-S", arg) && ++i < argc)
+                       revs_file = argv[i];
+               else if (!prefixcmp(arg, "-M")) {
+                       opt |= PICKAXE_BLAME_MOVE;
+                       blame_move_score = parse_score(arg+2);
+               }
+               else if (!prefixcmp(arg, "-C")) {
+                       /*
+                        * -C enables copy from removed files;
+                        * -C -C enables copy from existing files, but only
+                        *       when blaming a new file;
+                        * -C -C -C enables copy from existing files for
+                        *          everybody
+                        */
+                       if (opt & PICKAXE_BLAME_COPY_HARDER)
+                               opt |= PICKAXE_BLAME_COPY_HARDEST;
+                       if (opt & PICKAXE_BLAME_COPY)
+                               opt |= PICKAXE_BLAME_COPY_HARDER;
+                       opt |= PICKAXE_BLAME_COPY | PICKAXE_BLAME_MOVE;
+                       blame_copy_score = parse_score(arg+2);
+               }
+               else if (!prefixcmp(arg, "-L")) {
+                       if (!arg[2]) {
+                               if (++i >= argc)
+                                       usage(blame_usage);
+                               arg = argv[i];
+                       }
+                       else
+                               arg += 2;
+                       if (bottomtop)
+                               die("More than one '-L n,m' option given");
+                       bottomtop = arg;
+               }
+               else if (!strcmp("--contents", arg)) {
+                       if (++i >= argc)
+                               usage(blame_usage);
+                       contents_from = argv[i];
+               }
+               else if (!strcmp("--incremental", arg))
+                       incremental = 1;
+               else if (!strcmp("--score-debug", arg))
+                       output_option |= OUTPUT_SHOW_SCORE;
+               else if (!strcmp("-f", arg) ||
+                        !strcmp("--show-name", arg))
+                       output_option |= OUTPUT_SHOW_NAME;
+               else if (!strcmp("-n", arg) ||
+                        !strcmp("--show-number", arg))
+                       output_option |= OUTPUT_SHOW_NUMBER;
+               else if (!strcmp("-p", arg) ||
+                        !strcmp("--porcelain", arg))
+                       output_option |= OUTPUT_PORCELAIN;
+               else if (!strcmp("--", arg)) {
+                       seen_dashdash = 1;
+                       i++;
+                       break;
+               }
+               else
+                       argv[unk++] = arg;
+       }
+
+       if (!blame_move_score)
+               blame_move_score = BLAME_DEFAULT_MOVE_SCORE;
+       if (!blame_copy_score)
+               blame_copy_score = BLAME_DEFAULT_COPY_SCORE;
+
+       /*
+        * We have collected options unknown to us in argv[1..unk]
+        * which are to be passed to revision machinery if we are
+        * going to do the "bottom" processing.
+        *
+        * The remaining are:
+        *
+        * (1) if seen_dashdash, its either
+        *     "-options -- <path>" or
+        *     "-options -- <path> <rev>".
+        *     but the latter is allowed only if there is no
+        *     options that we passed to revision machinery.
+        *
+        * (2) otherwise, we may have "--" somewhere later and
+        *     might be looking at the first one of multiple 'rev'
+        *     parameters (e.g. " master ^next ^maint -- path").
+        *     See if there is a dashdash first, and give the
+        *     arguments before that to revision machinery.
+        *     After that there must be one 'path'.
+        *
+        * (3) otherwise, its one of the three:
+        *     "-options <path> <rev>"
+        *     "-options <rev> <path>"
+        *     "-options <path>"
+        *     but again the first one is allowed only if
+        *     there is no options that we passed to revision
+        *     machinery.
+        */
+
+       if (seen_dashdash) {
+               /* (1) */
+               if (argc <= i)
+                       usage(blame_usage);
+               path = add_prefix(prefix, argv[i]);
+               if (i + 1 == argc - 1) {
+                       if (unk != 1)
+                               usage(blame_usage);
+                       argv[unk++] = argv[i + 1];
+               }
+               else if (i + 1 != argc)
+                       /* garbage at end */
+                       usage(blame_usage);
+       }
+       else {
+               int j;
+               for (j = i; !seen_dashdash && j < argc; j++)
+                       if (!strcmp(argv[j], "--"))
+                               seen_dashdash = j;
+               if (seen_dashdash) {
+                       /* (2) */
+                       if (seen_dashdash + 1 != argc - 1)
+                               usage(blame_usage);
+                       path = add_prefix(prefix, argv[seen_dashdash + 1]);
+                       for (j = i; j < seen_dashdash; j++)
+                               argv[unk++] = argv[j];
+               }
+               else {
+                       /* (3) */
+                       if (argc <= i)
+                               usage(blame_usage);
+                       path = add_prefix(prefix, argv[i]);
+                       if (i + 1 == argc - 1) {
+                               final_commit_name = argv[i + 1];
+
+                               /* if (unk == 1) we could be getting
+                                * old-style
+                                */
+                               if (unk == 1 && !has_path_in_work_tree(path)) {
+                                       path = add_prefix(prefix, argv[i + 1]);
+                                       final_commit_name = argv[i];
+                               }
+                       }
+                       else if (i != argc - 1)
+                               usage(blame_usage); /* garbage at end */
+
+                       setup_work_tree();
+                       if (!has_path_in_work_tree(path))
+                               die("cannot stat path %s: %s",
+                                   path, strerror(errno));
+               }
+       }
+
+       if (final_commit_name)
+               argv[unk++] = final_commit_name;
+
+       /*
+        * Now we got rev and path.  We do not want the path pruning
+        * but we may want "bottom" processing.
+        */
+       argv[unk++] = "--"; /* terminate the rev name */
+       argv[unk] = NULL;
+
+       init_revisions(&revs, NULL);
+       setup_revisions(unk, argv, &revs, NULL);
+       memset(&sb, 0, sizeof(sb));
+
+       /*
+        * There must be one and only one positive commit in the
+        * revs->pending array.
+        */
+       for (i = 0; i < revs.pending.nr; i++) {
+               struct object *obj = revs.pending.objects[i].item;
+               if (obj->flags & UNINTERESTING)
+                       continue;
+               while (obj->type == OBJ_TAG)
+                       obj = deref_tag(obj, NULL, 0);
+               if (obj->type != OBJ_COMMIT)
+                       die("Non commit %s?",
+                           revs.pending.objects[i].name);
+               if (sb.final)
+                       die("More than one commit to dig from %s and %s?",
+                           revs.pending.objects[i].name,
+                           final_commit_name);
+               sb.final = (struct commit *) obj;
+               final_commit_name = revs.pending.objects[i].name;
+       }
+
+       if (!sb.final) {
+               /*
+                * "--not A B -- path" without anything positive;
+                * do not default to HEAD, but use the working tree
+                * or "--contents".
+                */
+               setup_work_tree();
+               sb.final = fake_working_tree_commit(path, contents_from);
+               add_pending_object(&revs, &(sb.final->object), ":");
+       }
+       else if (contents_from)
+               die("Cannot use --contents with final commit object name");
+
+       /*
+        * If we have bottom, this will mark the ancestors of the
+        * bottom commits we would reach while traversing as
+        * uninteresting.
+        */
+       prepare_revision_walk(&revs);
+
+       if (is_null_sha1(sb.final->object.sha1)) {
+               char *buf;
+               o = sb.final->util;
+               buf = xmalloc(o->file.size + 1);
+               memcpy(buf, o->file.ptr, o->file.size + 1);
+               sb.final_buf = buf;
+               sb.final_buf_size = o->file.size;
+       }
+       else {
+               o = get_origin(&sb, sb.final, path);
+               if (fill_blob_sha1(o))
+                       die("no such path %s in %s", path, final_commit_name);
+
+               sb.final_buf = read_sha1_file(o->blob_sha1, &type,
+                                             &sb.final_buf_size);
+               if (!sb.final_buf)
+                       die("Cannot read blob %s for path %s",
+                           sha1_to_hex(o->blob_sha1),
+                           path);
+       }
+       num_read_blob++;
+       lno = prepare_lines(&sb);
+
+       bottom = top = 0;
+       if (bottomtop)
+               prepare_blame_range(&sb, bottomtop, lno, &bottom, &top);
+       if (bottom && top && top < bottom) {
+               long tmp;
+               tmp = top; top = bottom; bottom = tmp;
+       }
+       if (bottom < 1)
+               bottom = 1;
+       if (top < 1)
+               top = lno;
+       bottom--;
+       if (lno < top)
+               die("file %s has only %lu lines", path, lno);
+
+       ent = xcalloc(1, sizeof(*ent));
+       ent->lno = bottom;
+       ent->num_lines = top - bottom;
+       ent->suspect = o;
+       ent->s_lno = bottom;
+
+       sb.ent = ent;
+       sb.path = path;
+
+       if (revs_file && read_ancestry(revs_file))
+               die("reading graft file %s failed: %s",
+                   revs_file, strerror(errno));
+
+       read_mailmap(&mailmap, ".mailmap", NULL);
+
+       if (!incremental)
+               setup_pager();
+
+       assign_blame(&sb, &revs, opt);
+
+       if (incremental)
+               return 0;
+
+       coalesce(&sb);
+
+       if (!(output_option & OUTPUT_PORCELAIN))
+               find_alignment(&sb, &output_option);
+
+       output(&sb, output_option);
+       free((void *)sb.final_buf);
+       for (ent = sb.ent; ent; ) {
+               struct blame_entry *e = ent->next;
+               free(ent);
+               ent = e;
+       }
+
+       if (show_stats) {
+               printf("num read blob: %d\n", num_read_blob);
+               printf("num get patch: %d\n", num_get_patch);
+               printf("num commits: %d\n", num_commits);
+       }
+       return 0;
+}
diff --git a/builtin-branch.c b/builtin-branch.c
new file mode 100644 (file)
index 0000000..089cae5
--- /dev/null
@@ -0,0 +1,620 @@
+/*
+ * Builtin "git branch"
+ *
+ * Copyright (c) 2006 Kristian Høgsberg <krh@redhat.com>
+ * Based on git-branch.sh by Junio C Hamano.
+ */
+
+#include "cache.h"
+#include "color.h"
+#include "refs.h"
+#include "commit.h"
+#include "builtin.h"
+#include "remote.h"
+#include "parse-options.h"
+
+static const char * const builtin_branch_usage[] = {
+       "git-branch [options] [-r | -a]",
+       "git-branch [options] [-l] [-f] <branchname> [<start-point>]",
+       "git-branch [options] [-r] (-d | -D) <branchname>",
+       "git-branch [options] (-m | -M) [<oldbranch>] <newbranch>",
+       NULL
+};
+
+#define REF_UNKNOWN_TYPE    0x00
+#define REF_LOCAL_BRANCH    0x01
+#define REF_REMOTE_BRANCH   0x02
+#define REF_TAG             0x04
+
+static const char *head;
+static unsigned char head_sha1[20];
+
+static int branch_track = 1;
+
+static int branch_use_color;
+static char branch_colors[][COLOR_MAXLEN] = {
+       "\033[m",       /* reset */
+       "",             /* PLAIN (normal) */
+       "\033[31m",     /* REMOTE (red) */
+       "",             /* LOCAL (normal) */
+       "\033[32m",     /* CURRENT (green) */
+};
+enum color_branch {
+       COLOR_BRANCH_RESET = 0,
+       COLOR_BRANCH_PLAIN = 1,
+       COLOR_BRANCH_REMOTE = 2,
+       COLOR_BRANCH_LOCAL = 3,
+       COLOR_BRANCH_CURRENT = 4,
+};
+
+static int parse_branch_color_slot(const char *var, int ofs)
+{
+       if (!strcasecmp(var+ofs, "plain"))
+               return COLOR_BRANCH_PLAIN;
+       if (!strcasecmp(var+ofs, "reset"))
+               return COLOR_BRANCH_RESET;
+       if (!strcasecmp(var+ofs, "remote"))
+               return COLOR_BRANCH_REMOTE;
+       if (!strcasecmp(var+ofs, "local"))
+               return COLOR_BRANCH_LOCAL;
+       if (!strcasecmp(var+ofs, "current"))
+               return COLOR_BRANCH_CURRENT;
+       die("bad config variable '%s'", var);
+}
+
+static int git_branch_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "color.branch")) {
+               branch_use_color = git_config_colorbool(var, value, -1);
+               return 0;
+       }
+       if (!prefixcmp(var, "color.branch.")) {
+               int slot = parse_branch_color_slot(var, 13);
+               color_parse(value, var, branch_colors[slot]);
+               return 0;
+       }
+       if (!strcmp(var, "branch.autosetupmerge"))
+                       branch_track = git_config_bool(var, value);
+
+       return git_default_config(var, value);
+}
+
+static const char *branch_get_color(enum color_branch ix)
+{
+       if (branch_use_color)
+               return branch_colors[ix];
+       return "";
+}
+
+static int delete_branches(int argc, const char **argv, int force, int kinds)
+{
+       struct commit *rev, *head_rev = head_rev;
+       unsigned char sha1[20];
+       char *name = NULL;
+       const char *fmt, *remote;
+       char section[PATH_MAX];
+       int i;
+       int ret = 0;
+
+       switch (kinds) {
+       case REF_REMOTE_BRANCH:
+               fmt = "refs/remotes/%s";
+               remote = "remote ";
+               force = 1;
+               break;
+       case REF_LOCAL_BRANCH:
+               fmt = "refs/heads/%s";
+               remote = "";
+               break;
+       default:
+               die("cannot use -a with -d");
+       }
+
+       if (!force) {
+               head_rev = lookup_commit_reference(head_sha1);
+               if (!head_rev)
+                       die("Couldn't look up commit object for HEAD");
+       }
+       for (i = 0; i < argc; i++) {
+               if (kinds == REF_LOCAL_BRANCH && !strcmp(head, argv[i])) {
+                       error("Cannot delete the branch '%s' "
+                               "which you are currently on.", argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               if (name)
+                       free(name);
+
+               name = xstrdup(mkpath(fmt, argv[i]));
+               if (!resolve_ref(name, sha1, 1, NULL)) {
+                       error("%sbranch '%s' not found.",
+                                       remote, argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               rev = lookup_commit_reference(sha1);
+               if (!rev) {
+                       error("Couldn't look up commit object for '%s'", name);
+                       ret = 1;
+                       continue;
+               }
+
+               /* This checks whether the merge bases of branch and
+                * HEAD contains branch -- which means that the HEAD
+                * contains everything in both.
+                */
+
+               if (!force &&
+                   !in_merge_bases(rev, &head_rev, 1)) {
+                       error("The branch '%s' is not an ancestor of "
+                               "your current HEAD.\n"
+                               "If you are sure you want to delete it, "
+                               "run 'git branch -D %s'.", argv[i], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               if (delete_ref(name, sha1)) {
+                       error("Error deleting %sbranch '%s'", remote,
+                              argv[i]);
+                       ret = 1;
+               } else {
+                       printf("Deleted %sbranch %s.\n", remote, argv[i]);
+                       snprintf(section, sizeof(section), "branch.%s",
+                                argv[i]);
+                       if (git_config_rename_section(section, NULL) < 0)
+                               warning("Update of config-file failed");
+               }
+       }
+
+       if (name)
+               free(name);
+
+       return(ret);
+}
+
+struct ref_item {
+       char *name;
+       unsigned int kind;
+       unsigned char sha1[20];
+};
+
+struct ref_list {
+       int index, alloc, maxwidth;
+       struct ref_item *list;
+       struct commit_list *with_commit;
+       int kinds;
+};
+
+static int has_commit(const unsigned char *sha1, struct commit_list *with_commit)
+{
+       struct commit *commit;
+
+       if (!with_commit)
+               return 1;
+       commit = lookup_commit_reference_gently(sha1, 1);
+       if (!commit)
+               return 0;
+       while (with_commit) {
+               struct commit *other;
+
+               other = with_commit->item;
+               with_commit = with_commit->next;
+               if (in_merge_bases(other, &commit, 1))
+                       return 1;
+       }
+       return 0;
+}
+
+static int append_ref(const char *refname, const unsigned char *sha1, int flags, void *cb_data)
+{
+       struct ref_list *ref_list = (struct ref_list*)(cb_data);
+       struct ref_item *newitem;
+       int kind = REF_UNKNOWN_TYPE;
+       int len;
+
+       /* Detect kind */
+       if (!prefixcmp(refname, "refs/heads/")) {
+               kind = REF_LOCAL_BRANCH;
+               refname += 11;
+       } else if (!prefixcmp(refname, "refs/remotes/")) {
+               kind = REF_REMOTE_BRANCH;
+               refname += 13;
+       } else if (!prefixcmp(refname, "refs/tags/")) {
+               kind = REF_TAG;
+               refname += 10;
+       }
+
+       /* Filter with with_commit if specified */
+       if (!has_commit(sha1, ref_list->with_commit))
+               return 0;
+
+       /* Don't add types the caller doesn't want */
+       if ((kind & ref_list->kinds) == 0)
+               return 0;
+
+       /* Resize buffer */
+       if (ref_list->index >= ref_list->alloc) {
+               ref_list->alloc = alloc_nr(ref_list->alloc);
+               ref_list->list = xrealloc(ref_list->list,
+                               ref_list->alloc * sizeof(struct ref_item));
+       }
+
+       /* Record the new item */
+       newitem = &(ref_list->list[ref_list->index++]);
+       newitem->name = xstrdup(refname);
+       newitem->kind = kind;
+       hashcpy(newitem->sha1, sha1);
+       len = strlen(newitem->name);
+       if (len > ref_list->maxwidth)
+               ref_list->maxwidth = len;
+
+       return 0;
+}
+
+static void free_ref_list(struct ref_list *ref_list)
+{
+       int i;
+
+       for (i = 0; i < ref_list->index; i++)
+               free(ref_list->list[i].name);
+       free(ref_list->list);
+}
+
+static int ref_cmp(const void *r1, const void *r2)
+{
+       struct ref_item *c1 = (struct ref_item *)(r1);
+       struct ref_item *c2 = (struct ref_item *)(r2);
+
+       if (c1->kind != c2->kind)
+               return c1->kind - c2->kind;
+       return strcmp(c1->name, c2->name);
+}
+
+static void print_ref_item(struct ref_item *item, int maxwidth, int verbose,
+                          int abbrev, int current)
+{
+       char c;
+       int color;
+       struct commit *commit;
+
+       switch (item->kind) {
+       case REF_LOCAL_BRANCH:
+               color = COLOR_BRANCH_LOCAL;
+               break;
+       case REF_REMOTE_BRANCH:
+               color = COLOR_BRANCH_REMOTE;
+               break;
+       default:
+               color = COLOR_BRANCH_PLAIN;
+               break;
+       }
+
+       c = ' ';
+       if (current) {
+               c = '*';
+               color = COLOR_BRANCH_CURRENT;
+       }
+
+       if (verbose) {
+               struct strbuf subject;
+               const char *sub = " **** invalid ref ****";
+
+               strbuf_init(&subject, 0);
+
+               commit = lookup_commit(item->sha1);
+               if (commit && !parse_commit(commit)) {
+                       pretty_print_commit(CMIT_FMT_ONELINE, commit,
+                                           &subject, 0, NULL, NULL, 0, 0);
+                       sub = subject.buf;
+               }
+               printf("%c %s%-*s%s %s %s\n", c, branch_get_color(color),
+                      maxwidth, item->name,
+                      branch_get_color(COLOR_BRANCH_RESET),
+                      find_unique_abbrev(item->sha1, abbrev), sub);
+               strbuf_release(&subject);
+       } else {
+               printf("%c %s%s%s\n", c, branch_get_color(color), item->name,
+                      branch_get_color(COLOR_BRANCH_RESET));
+       }
+}
+
+static void print_ref_list(int kinds, int detached, int verbose, int abbrev, struct commit_list *with_commit)
+{
+       int i;
+       struct ref_list ref_list;
+
+       memset(&ref_list, 0, sizeof(ref_list));
+       ref_list.kinds = kinds;
+       ref_list.with_commit = with_commit;
+       for_each_ref(append_ref, &ref_list);
+
+       qsort(ref_list.list, ref_list.index, sizeof(struct ref_item), ref_cmp);
+
+       detached = (detached && (kinds & REF_LOCAL_BRANCH));
+       if (detached && has_commit(head_sha1, with_commit)) {
+               struct ref_item item;
+               item.name = xstrdup("(no branch)");
+               item.kind = REF_LOCAL_BRANCH;
+               hashcpy(item.sha1, head_sha1);
+               if (strlen(item.name) > ref_list.maxwidth)
+                             ref_list.maxwidth = strlen(item.name);
+               print_ref_item(&item, ref_list.maxwidth, verbose, abbrev, 1);
+               free(item.name);
+       }
+
+       for (i = 0; i < ref_list.index; i++) {
+               int current = !detached &&
+                       (ref_list.list[i].kind == REF_LOCAL_BRANCH) &&
+                       !strcmp(ref_list.list[i].name, head);
+               print_ref_item(&ref_list.list[i], ref_list.maxwidth, verbose,
+                              abbrev, current);
+       }
+
+       free_ref_list(&ref_list);
+}
+
+struct tracking {
+       struct refspec spec;
+       char *src;
+       const char *remote;
+       int matches;
+};
+
+static int find_tracked_branch(struct remote *remote, void *priv)
+{
+       struct tracking *tracking = priv;
+
+       if (!remote_find_tracking(remote, &tracking->spec)) {
+               if (++tracking->matches == 1) {
+                       tracking->src = tracking->spec.src;
+                       tracking->remote = remote->name;
+               } else {
+                       free(tracking->spec.src);
+                       if (tracking->src) {
+                               free(tracking->src);
+                               tracking->src = NULL;
+                       }
+               }
+               tracking->spec.src = NULL;
+       }
+
+       return 0;
+}
+
+
+/*
+ * This is called when new_ref is branched off of orig_ref, and tries
+ * to infer the settings for branch.<new_ref>.{remote,merge} from the
+ * config.
+ */
+static int setup_tracking(const char *new_ref, const char *orig_ref)
+{
+       char key[1024];
+       struct tracking tracking;
+
+       if (strlen(new_ref) > 1024 - 7 - 7 - 1)
+               return error("Tracking not set up: name too long: %s",
+                               new_ref);
+
+       memset(&tracking, 0, sizeof(tracking));
+       tracking.spec.dst = (char *)orig_ref;
+       if (for_each_remote(find_tracked_branch, &tracking) ||
+                       !tracking.matches)
+               return 1;
+
+       if (tracking.matches > 1)
+               return error("Not tracking: ambiguous information for ref %s",
+                               orig_ref);
+
+       if (tracking.matches == 1) {
+               sprintf(key, "branch.%s.remote", new_ref);
+               git_config_set(key, tracking.remote ?  tracking.remote : ".");
+               sprintf(key, "branch.%s.merge", new_ref);
+               git_config_set(key, tracking.src);
+               free(tracking.src);
+               printf("Branch %s set up to track remote branch %s.\n",
+                              new_ref, orig_ref);
+       }
+
+       return 0;
+}
+
+static void create_branch(const char *name, const char *start_name,
+                         int force, int reflog, int track)
+{
+       struct ref_lock *lock;
+       struct commit *commit;
+       unsigned char sha1[20];
+       char *real_ref, ref[PATH_MAX], msg[PATH_MAX + 20];
+       int forcing = 0;
+
+       snprintf(ref, sizeof ref, "refs/heads/%s", name);
+       if (check_ref_format(ref))
+               die("'%s' is not a valid branch name.", name);
+
+       if (resolve_ref(ref, sha1, 1, NULL)) {
+               if (!force)
+                       die("A branch named '%s' already exists.", name);
+               else if (!is_bare_repository() && !strcmp(head, name))
+                       die("Cannot force update the current branch.");
+               forcing = 1;
+       }
+
+       real_ref = NULL;
+       if (get_sha1(start_name, sha1))
+               die("Not a valid object name: '%s'.", start_name);
+
+       switch (dwim_ref(start_name, strlen(start_name), sha1, &real_ref)) {
+       case 0:
+               /* Not branching from any existing branch */
+               real_ref = NULL;
+               break;
+       case 1:
+               /* Unique completion -- good */
+               break;
+       default:
+               die("Ambiguous object name: '%s'.", start_name);
+               break;
+       }
+
+       if ((commit = lookup_commit_reference(sha1)) == NULL)
+               die("Not a valid branch point: '%s'.", start_name);
+       hashcpy(sha1, commit->object.sha1);
+
+       lock = lock_any_ref_for_update(ref, NULL, 0);
+       if (!lock)
+               die("Failed to lock ref for update: %s.", strerror(errno));
+
+       if (reflog)
+               log_all_ref_updates = 1;
+
+       if (forcing)
+               snprintf(msg, sizeof msg, "branch: Reset from %s",
+                        start_name);
+       else
+               snprintf(msg, sizeof msg, "branch: Created from %s",
+                        start_name);
+
+       /* When branching off a remote branch, set up so that git-pull
+          automatically merges from there.  So far, this is only done for
+          remotes registered via .git/config.  */
+       if (real_ref && track)
+               setup_tracking(name, real_ref);
+
+       if (write_ref_sha1(lock, sha1, msg) < 0)
+               die("Failed to write ref: %s.", strerror(errno));
+
+       if (real_ref)
+               free(real_ref);
+}
+
+static void rename_branch(const char *oldname, const char *newname, int force)
+{
+       char oldref[PATH_MAX], newref[PATH_MAX], logmsg[PATH_MAX*2 + 100];
+       unsigned char sha1[20];
+       char oldsection[PATH_MAX], newsection[PATH_MAX];
+
+       if (!oldname)
+               die("cannot rename the current branch while not on any.");
+
+       if (snprintf(oldref, sizeof(oldref), "refs/heads/%s", oldname) > sizeof(oldref))
+               die("Old branchname too long");
+
+       if (check_ref_format(oldref))
+               die("Invalid branch name: %s", oldref);
+
+       if (snprintf(newref, sizeof(newref), "refs/heads/%s", newname) > sizeof(newref))
+               die("New branchname too long");
+
+       if (check_ref_format(newref))
+               die("Invalid branch name: %s", newref);
+
+       if (resolve_ref(newref, sha1, 1, NULL) && !force)
+               die("A branch named '%s' already exists.", newname);
+
+       snprintf(logmsg, sizeof(logmsg), "Branch: renamed %s to %s",
+                oldref, newref);
+
+       if (rename_ref(oldref, newref, logmsg))
+               die("Branch rename failed");
+
+       /* no need to pass logmsg here as HEAD didn't really move */
+       if (!strcmp(oldname, head) && create_symref("HEAD", newref, NULL))
+               die("Branch renamed to %s, but HEAD is not updated!", newname);
+
+       snprintf(oldsection, sizeof(oldsection), "branch.%s", oldref + 11);
+       snprintf(newsection, sizeof(newsection), "branch.%s", newref + 11);
+       if (git_config_rename_section(oldsection, newsection) < 0)
+               die("Branch is renamed, but update of config-file failed");
+}
+
+static int opt_parse_with_commit(const struct option *opt, const char *arg, int unset)
+{
+       unsigned char sha1[20];
+       struct commit *commit;
+
+       if (!arg)
+               return -1;
+       if (get_sha1(arg, sha1))
+               die("malformed object name %s", arg);
+       commit = lookup_commit_reference(sha1);
+       if (!commit)
+               die("no such commit %s", arg);
+       commit_list_insert(commit, opt->value);
+       return 0;
+}
+
+int cmd_branch(int argc, const char **argv, const char *prefix)
+{
+       int delete = 0, rename = 0, force_create = 0;
+       int verbose = 0, abbrev = DEFAULT_ABBREV, detached = 0;
+       int reflog = 0, track;
+       int kinds = REF_LOCAL_BRANCH;
+       struct commit_list *with_commit = NULL;
+
+       struct option options[] = {
+               OPT_GROUP("Generic options"),
+               OPT__VERBOSE(&verbose),
+               OPT_BOOLEAN( 0 , "track",  &track, "set up tracking mode (see git-pull(1))"),
+               OPT_BOOLEAN( 0 , "color",  &branch_use_color, "use colored output"),
+               OPT_SET_INT('r', NULL,     &kinds, "act on remote-tracking branches",
+                       REF_REMOTE_BRANCH),
+               OPT_CALLBACK(0, "contains", &with_commit, "commit",
+                            "print only branches that contain the commit",
+                            opt_parse_with_commit),
+               {
+                       OPTION_CALLBACK, 0, "with", &with_commit, "commit",
+                       "print only branches that contain the commit",
+                       PARSE_OPT_HIDDEN, opt_parse_with_commit,
+               },
+               OPT__ABBREV(&abbrev),
+
+               OPT_GROUP("Specific git-branch actions:"),
+               OPT_SET_INT('a', NULL, &kinds, "list both remote-tracking and local branches",
+                       REF_REMOTE_BRANCH | REF_LOCAL_BRANCH),
+               OPT_BIT('d', NULL, &delete, "delete fully merged branch", 1),
+               OPT_BIT('D', NULL, &delete, "delete branch (even if not merged)", 2),
+               OPT_BIT('m', NULL, &rename, "move/rename a branch and its reflog", 1),
+               OPT_BIT('M', NULL, &rename, "move/rename a branch, even if target exists", 2),
+               OPT_BOOLEAN('l', NULL, &reflog, "create the branch's reflog"),
+               OPT_BOOLEAN('f', NULL, &force_create, "force creation (when already exists)"),
+               OPT_END(),
+       };
+
+       git_config(git_branch_config);
+       track = branch_track;
+       argc = parse_options(argc, argv, options, builtin_branch_usage, 0);
+       if (!!delete + !!rename + !!force_create > 1)
+               usage_with_options(builtin_branch_usage, options);
+
+       head = resolve_ref("HEAD", head_sha1, 0, NULL);
+       if (!head)
+               die("Failed to resolve HEAD as a valid ref.");
+       head = xstrdup(head);
+       if (!strcmp(head, "HEAD")) {
+               detached = 1;
+       } else {
+               if (prefixcmp(head, "refs/heads/"))
+                       die("HEAD not found below refs/heads!");
+               head += 11;
+       }
+
+       if (delete)
+               return delete_branches(argc, argv, delete > 1, kinds);
+       else if (argc == 0)
+               print_ref_list(kinds, detached, verbose, abbrev, with_commit);
+       else if (rename && (argc == 1))
+               rename_branch(head, argv[0], rename > 1);
+       else if (rename && (argc == 2))
+               rename_branch(argv[0], argv[1], rename > 1);
+       else if (argc <= 2)
+               create_branch(argv[0], (argc == 2) ? argv[1] : head,
+                             force_create, reflog, track);
+       else
+               usage_with_options(builtin_branch_usage, options);
+
+       return 0;
+}
diff --git a/builtin-bundle.c b/builtin-bundle.c
new file mode 100644 (file)
index 0000000..9f38e21
--- /dev/null
@@ -0,0 +1,63 @@
+#include "builtin.h"
+#include "cache.h"
+#include "bundle.h"
+
+/*
+ * Basic handler for bundle files to connect repositories via sneakernet.
+ * Invocation must include action.
+ * This function can create a bundle or provide information on an existing
+ * bundle supporting git-fetch, git-pull, and git-ls-remote
+ */
+
+static const char *bundle_usage="git-bundle (create <bundle> <git-rev-list args> | verify <bundle> | list-heads <bundle> [refname]... | unbundle <bundle> [refname]... )";
+
+int cmd_bundle(int argc, const char **argv, const char *prefix)
+{
+       struct bundle_header header;
+       int nongit = 0;
+       const char *cmd, *bundle_file;
+       int bundle_fd = -1;
+       char buffer[PATH_MAX];
+
+       if (argc < 3)
+               usage(bundle_usage);
+
+       cmd = argv[1];
+       bundle_file = argv[2];
+       argc -= 2;
+       argv += 2;
+
+       prefix = setup_git_directory_gently(&nongit);
+       if (prefix && bundle_file[0] != '/') {
+               snprintf(buffer, sizeof(buffer), "%s/%s", prefix, bundle_file);
+               bundle_file = buffer;
+       }
+
+       memset(&header, 0, sizeof(header));
+       if (strcmp(cmd, "create") && (bundle_fd =
+                               read_bundle_header(bundle_file, &header)) < 0)
+               return 1;
+
+       if (!strcmp(cmd, "verify")) {
+               close(bundle_fd);
+               if (verify_bundle(&header, 1))
+                       return 1;
+               fprintf(stderr, "%s is okay\n", bundle_file);
+               return 0;
+       }
+       if (!strcmp(cmd, "list-heads")) {
+               close(bundle_fd);
+               return !!list_bundle_refs(&header, argc, argv);
+       }
+       if (!strcmp(cmd, "create")) {
+               if (nongit)
+                       die("Need a repository to create a bundle.");
+               return !!create_bundle(&header, bundle_file, argc, argv);
+       } else if (!strcmp(cmd, "unbundle")) {
+               if (nongit)
+                       die("Need a repository to unbundle.");
+               return !!unbundle(&header, bundle_fd) ||
+                       list_bundle_refs(&header, argc, argv);
+       } else
+               usage(bundle_usage);
+}
diff --git a/builtin-cat-file.c b/builtin-cat-file.c
new file mode 100644 (file)
index 0000000..f132d58
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "cache.h"
+#include "exec_cmd.h"
+#include "tag.h"
+#include "tree.h"
+#include "builtin.h"
+
+static void pprint_tag(const unsigned char *sha1, const char *buf, unsigned long size)
+{
+       /* the parser in tag.c is useless here. */
+       const char *endp = buf + size;
+       const char *cp = buf;
+
+       while (cp < endp) {
+               char c = *cp++;
+               if (c != '\n')
+                       continue;
+               if (7 <= endp - cp && !memcmp("tagger ", cp, 7)) {
+                       const char *tagger = cp;
+
+                       /* Found the tagger line.  Copy out the contents
+                        * of the buffer so far.
+                        */
+                       write_or_die(1, buf, cp - buf);
+
+                       /*
+                        * Do something intelligent, like pretty-printing
+                        * the date.
+                        */
+                       while (cp < endp) {
+                               if (*cp++ == '\n') {
+                                       /* tagger to cp is a line
+                                        * that has ident and time.
+                                        */
+                                       const char *sp = tagger;
+                                       char *ep;
+                                       unsigned long date;
+                                       long tz;
+                                       while (sp < cp && *sp != '>')
+                                               sp++;
+                                       if (sp == cp) {
+                                               /* give up */
+                                               write_or_die(1, tagger,
+                                                            cp - tagger);
+                                               break;
+                                       }
+                                       while (sp < cp &&
+                                              !('0' <= *sp && *sp <= '9'))
+                                               sp++;
+                                       write_or_die(1, tagger, sp - tagger);
+                                       date = strtoul(sp, &ep, 10);
+                                       tz = strtol(ep, NULL, 10);
+                                       sp = show_date(date, tz, 0);
+                                       write_or_die(1, sp, strlen(sp));
+                                       xwrite(1, "\n", 1);
+                                       break;
+                               }
+                       }
+                       break;
+               }
+               if (cp < endp && *cp == '\n')
+                       /* end of header */
+                       break;
+       }
+       /* At this point, we have copied out the header up to the end of
+        * the tagger line and cp points at one past \n.  It could be the
+        * next header line after the tagger line, or it could be another
+        * \n that marks the end of the headers.  We need to copy out the
+        * remainder as is.
+        */
+       if (cp < endp)
+               write_or_die(1, cp, endp - cp);
+}
+
+int cmd_cat_file(int argc, const char **argv, const char *prefix)
+{
+       unsigned char sha1[20];
+       enum object_type type;
+       void *buf;
+       unsigned long size;
+       int opt;
+       const char *exp_type, *obj_name;
+
+       git_config(git_default_config);
+       if (argc != 3)
+               usage("git-cat-file [-t|-s|-e|-p|<type>] <sha1>");
+       exp_type = argv[1];
+       obj_name = argv[2];
+
+       if (get_sha1(obj_name, sha1))
+               die("Not a valid object name %s", obj_name);
+
+       opt = 0;
+       if ( exp_type[0] == '-' ) {
+               opt = exp_type[1];
+               if ( !opt || exp_type[2] )
+                       opt = -1; /* Not a single character option */
+       }
+
+       buf = NULL;
+       switch (opt) {
+       case 't':
+               type = sha1_object_info(sha1, NULL);
+               if (type > 0) {
+                       printf("%s\n", typename(type));
+                       return 0;
+               }
+               break;
+
+       case 's':
+               type = sha1_object_info(sha1, &size);
+               if (type > 0) {
+                       printf("%lu\n", size);
+                       return 0;
+               }
+               break;
+
+       case 'e':
+               return !has_sha1_file(sha1);
+
+       case 'p':
+               type = sha1_object_info(sha1, NULL);
+               if (type < 0)
+                       die("Not a valid object name %s", obj_name);
+
+               /* custom pretty-print here */
+               if (type == OBJ_TREE) {
+                       const char *ls_args[3] = {"ls-tree", obj_name, NULL};
+                       return cmd_ls_tree(2, ls_args, NULL);
+               }
+
+               buf = read_sha1_file(sha1, &type, &size);
+               if (!buf)
+                       die("Cannot read object %s", obj_name);
+               if (type == OBJ_TAG) {
+                       pprint_tag(sha1, buf, size);
+                       return 0;
+               }
+
+               /* otherwise just spit out the data */
+               break;
+       case 0:
+               buf = read_object_with_reference(sha1, exp_type, &size, NULL);
+               break;
+
+       default:
+               die("git-cat-file: unknown option: %s\n", exp_type);
+       }
+
+       if (!buf)
+               die("git-cat-file %s: bad file", obj_name);
+
+       write_or_die(1, buf, size);
+       return 0;
+}
diff --git a/builtin-check-attr.c b/builtin-check-attr.c
new file mode 100644 (file)
index 0000000..6afdfa1
--- /dev/null
@@ -0,0 +1,64 @@
+#include "builtin.h"
+#include "cache.h"
+#include "attr.h"
+#include "quote.h"
+
+static const char check_attr_usage[] =
+"git-check-attr attr... [--] pathname...";
+
+int cmd_check_attr(int argc, const char **argv, const char *prefix)
+{
+       struct git_attr_check *check;
+       int cnt, i, doubledash;
+
+       if (read_cache() < 0) {
+               die("invalid cache");
+       }
+
+       doubledash = -1;
+       for (i = 1; doubledash < 0 && i < argc; i++) {
+               if (!strcmp(argv[i], "--"))
+                       doubledash = i;
+       }
+
+       /* If there is no double dash, we handle only one attribute */
+       if (doubledash < 0) {
+               cnt = 1;
+               doubledash = 1;
+       } else
+               cnt = doubledash - 1;
+       doubledash++;
+
+       if (cnt <= 0 || argc < doubledash)
+               usage(check_attr_usage);
+       check = xcalloc(cnt, sizeof(*check));
+       for (i = 0; i < cnt; i++) {
+               const char *name;
+               struct git_attr *a;
+               name = argv[i + 1];
+               a = git_attr(name, strlen(name));
+               if (!a)
+                       return error("%s: not a valid attribute name", name);
+               check[i].attr = a;
+       }
+
+       for (i = doubledash; i < argc; i++) {
+               int j;
+               if (git_checkattr(argv[i], cnt, check))
+                       die("git_checkattr died");
+               for (j = 0; j < cnt; j++) {
+                       const char *value = check[j].value;
+
+                       if (ATTR_TRUE(value))
+                               value = "set";
+                       else if (ATTR_FALSE(value))
+                               value = "unset";
+                       else if (ATTR_UNSET(value))
+                               value = "unspecified";
+
+                       quote_c_style(argv[i], NULL, stdout, 0);
+                       printf(": %s: %s\n", argv[j+1], value);
+               }
+       }
+       return 0;
+}
diff --git a/builtin-check-ref-format.c b/builtin-check-ref-format.c
new file mode 100644 (file)
index 0000000..fe04be7
--- /dev/null
@@ -0,0 +1,14 @@
+/*
+ * GIT - The information manager from hell
+ */
+
+#include "cache.h"
+#include "refs.h"
+#include "builtin.h"
+
+int cmd_check_ref_format(int argc, const char **argv, const char *prefix)
+{
+       if (argc != 2)
+               usage("git-check-ref-format refname");
+       return !!check_ref_format(argv[1]);
+}
diff --git a/builtin-checkout-index.c b/builtin-checkout-index.c
new file mode 100644 (file)
index 0000000..70d619d
--- /dev/null
@@ -0,0 +1,303 @@
+/*
+ * Check-out files from the "current cache directory"
+ *
+ * Copyright (C) 2005 Linus Torvalds
+ *
+ * Careful: order of argument flags does matter. For example,
+ *
+ *     git-checkout-index -a -f file.c
+ *
+ * Will first check out all files listed in the cache (but not
+ * overwrite any old ones), and then force-checkout "file.c" a
+ * second time (ie that one _will_ overwrite any old contents
+ * with the same filename).
+ *
+ * Also, just doing "git-checkout-index" does nothing. You probably
+ * meant "git-checkout-index -a". And if you want to force it, you
+ * want "git-checkout-index -f -a".
+ *
+ * Intuitiveness is not the goal here. Repeatability is. The
+ * reason for the "no arguments means no work" thing is that
+ * from scripts you are supposed to be able to do things like
+ *
+ *     find . -name '*.h' -print0 | xargs -0 git-checkout-index -f --
+ *
+ * or:
+ *
+ *     find . -name '*.h' -print0 | git-checkout-index -f -z --stdin
+ *
+ * which will force all existing *.h files to be replaced with
+ * their cached copies. If an empty command line implied "all",
+ * then this would force-refresh everything in the cache, which
+ * was not the point.
+ *
+ * Oh, and the "--" is just a good idea when you know the rest
+ * will be filenames. Just so that you wouldn't have a filename
+ * of "-a" causing problems (not possible in the above example,
+ * but get used to it in scripting!).
+ */
+#include "builtin.h"
+#include "cache.h"
+#include "quote.h"
+#include "cache-tree.h"
+
+#define CHECKOUT_ALL 4
+static int line_termination = '\n';
+static int checkout_stage; /* default to checkout stage0 */
+static int to_tempfile;
+static char topath[4][PATH_MAX + 1];
+
+static struct checkout state;
+
+static void write_tempfile_record(const char *name, int prefix_length)
+{
+       int i;
+
+       if (CHECKOUT_ALL == checkout_stage) {
+               for (i = 1; i < 4; i++) {
+                       if (i > 1)
+                               putchar(' ');
+                       if (topath[i][0])
+                               fputs(topath[i], stdout);
+                       else
+                               putchar('.');
+               }
+       } else
+               fputs(topath[checkout_stage], stdout);
+
+       putchar('\t');
+       write_name_quoted(name + prefix_length, stdout, line_termination);
+
+       for (i = 0; i < 4; i++) {
+               topath[i][0] = 0;
+       }
+}
+
+static int checkout_file(const char *name, int prefix_length)
+{
+       int namelen = strlen(name);
+       int pos = cache_name_pos(name, namelen);
+       int has_same_name = 0;
+       int did_checkout = 0;
+       int errs = 0;
+
+       if (pos < 0)
+               pos = -pos - 1;
+
+       while (pos < active_nr) {
+               struct cache_entry *ce = active_cache[pos];
+               if (ce_namelen(ce) != namelen ||
+                   memcmp(ce->name, name, namelen))
+                       break;
+               has_same_name = 1;
+               pos++;
+               if (ce_stage(ce) != checkout_stage
+                   && (CHECKOUT_ALL != checkout_stage || !ce_stage(ce)))
+                       continue;
+               did_checkout = 1;
+               if (checkout_entry(ce, &state,
+                   to_tempfile ? topath[ce_stage(ce)] : NULL) < 0)
+                       errs++;
+       }
+
+       if (did_checkout) {
+               if (to_tempfile)
+                       write_tempfile_record(name, prefix_length);
+               return errs > 0 ? -1 : 0;
+       }
+
+       if (!state.quiet) {
+               fprintf(stderr, "git-checkout-index: %s ", name);
+               if (!has_same_name)
+                       fprintf(stderr, "is not in the cache");
+               else if (checkout_stage)
+                       fprintf(stderr, "does not exist at stage %d",
+                               checkout_stage);
+               else
+                       fprintf(stderr, "is unmerged");
+               fputc('\n', stderr);
+       }
+       return -1;
+}
+
+static void checkout_all(const char *prefix, int prefix_length)
+{
+       int i, errs = 0;
+       struct cache_entry* last_ce = NULL;
+
+       for (i = 0; i < active_nr ; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (ce_stage(ce) != checkout_stage
+                   && (CHECKOUT_ALL != checkout_stage || !ce_stage(ce)))
+                       continue;
+               if (prefix && *prefix &&
+                   (ce_namelen(ce) <= prefix_length ||
+                    memcmp(prefix, ce->name, prefix_length)))
+                       continue;
+               if (last_ce && to_tempfile) {
+                       if (ce_namelen(last_ce) != ce_namelen(ce)
+                           || memcmp(last_ce->name, ce->name, ce_namelen(ce)))
+                               write_tempfile_record(last_ce->name, prefix_length);
+               }
+               if (checkout_entry(ce, &state,
+                   to_tempfile ? topath[ce_stage(ce)] : NULL) < 0)
+                       errs++;
+               last_ce = ce;
+       }
+       if (last_ce && to_tempfile)
+               write_tempfile_record(last_ce->name, prefix_length);
+       if (errs)
+               /* we have already done our error reporting.
+                * exit with the same code as die().
+                */
+               exit(128);
+}
+
+static const char checkout_cache_usage[] =
+"git-checkout-index [-u] [-q] [-a] [-f] [-n] [--stage=[123]|all] [--prefix=<string>] [--temp] [--] <file>...";
+
+static struct lock_file lock_file;
+
+int cmd_checkout_index(int argc, const char **argv, const char *prefix)
+{
+       int i;
+       int newfd = -1;
+       int all = 0;
+       int read_from_stdin = 0;
+       int prefix_length;
+
+       git_config(git_default_config);
+       state.base_dir = "";
+       prefix_length = prefix ? strlen(prefix) : 0;
+
+       if (read_cache() < 0) {
+               die("invalid cache");
+       }
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (!strcmp(arg, "--")) {
+                       i++;
+                       break;
+               }
+               if (!strcmp(arg, "-a") || !strcmp(arg, "--all")) {
+                       all = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-f") || !strcmp(arg, "--force")) {
+                       state.force = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-q") || !strcmp(arg, "--quiet")) {
+                       state.quiet = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-n") || !strcmp(arg, "--no-create")) {
+                       state.not_new = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-u") || !strcmp(arg, "--index")) {
+                       state.refresh_cache = 1;
+                       if (newfd < 0)
+                               newfd = hold_locked_index(&lock_file, 1);
+                       continue;
+               }
+               if (!strcmp(arg, "-z")) {
+                       line_termination = 0;
+                       continue;
+               }
+               if (!strcmp(arg, "--stdin")) {
+                       if (i != argc - 1)
+                               die("--stdin must be at the end");
+                       read_from_stdin = 1;
+                       i++; /* do not consider arg as a file name */
+                       break;
+               }
+               if (!strcmp(arg, "--temp")) {
+                       to_tempfile = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--prefix=")) {
+                       state.base_dir = arg+9;
+                       state.base_dir_len = strlen(state.base_dir);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--stage=")) {
+                       if (!strcmp(arg + 8, "all")) {
+                               to_tempfile = 1;
+                               checkout_stage = CHECKOUT_ALL;
+                       } else {
+                               int ch = arg[8];
+                               if ('1' <= ch && ch <= '3')
+                                       checkout_stage = arg[8] - '0';
+                               else
+                                       die("stage should be between 1 and 3 or all");
+                       }
+                       continue;
+               }
+               if (arg[0] == '-')
+                       usage(checkout_cache_usage);
+               break;
+       }
+
+       if (state.base_dir_len || to_tempfile) {
+               /* when --prefix is specified we do not
+                * want to update cache.
+                */
+               if (state.refresh_cache) {
+                       close(newfd); newfd = -1;
+                       rollback_lock_file(&lock_file);
+               }
+               state.refresh_cache = 0;
+       }
+
+       /* Check out named files first */
+       for ( ; i < argc; i++) {
+               const char *arg = argv[i];
+               const char *p;
+
+               if (all)
+                       die("git-checkout-index: don't mix '--all' and explicit filenames");
+               if (read_from_stdin)
+                       die("git-checkout-index: don't mix '--stdin' and explicit filenames");
+               p = prefix_path(prefix, prefix_length, arg);
+               checkout_file(p, prefix_length);
+               if (p < arg || p > arg + strlen(arg))
+                       free((char*)p);
+       }
+
+       if (read_from_stdin) {
+               struct strbuf buf, nbuf;
+
+               if (all)
+                       die("git-checkout-index: don't mix '--all' and '--stdin'");
+
+               strbuf_init(&buf, 0);
+               strbuf_init(&nbuf, 0);
+               while (strbuf_getline(&buf, stdin, line_termination) != EOF) {
+                       const char *p;
+                       if (line_termination && buf.buf[0] == '"') {
+                               strbuf_reset(&nbuf);
+                               if (unquote_c_style(&nbuf, buf.buf, NULL))
+                                       die("line is badly quoted");
+                               strbuf_swap(&buf, &nbuf);
+                       }
+                       p = prefix_path(prefix, prefix_length, buf.buf);
+                       checkout_file(p, prefix_length);
+                       if (p < buf.buf || p > buf.buf + buf.len)
+                               free((char *)p);
+               }
+               strbuf_release(&nbuf);
+               strbuf_release(&buf);
+       }
+
+       if (all)
+               checkout_all(prefix, prefix_length);
+
+       if (0 <= newfd &&
+           (write_cache(newfd, active_cache, active_nr) ||
+            close(newfd) || commit_locked_index(&lock_file)))
+               die("Unable to write new index file");
+       return 0;
+}
diff --git a/builtin-clean.c b/builtin-clean.c
new file mode 100644 (file)
index 0000000..ae30d4e
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * "git clean" builtin command
+ *
+ * Copyright (C) 2007 Shawn Bohrer
+ *
+ * Based on git-clean.sh by Pavel Roskin
+ */
+
+#include "builtin.h"
+#include "cache.h"
+#include "dir.h"
+#include "parse-options.h"
+
+static int force = -1; /* unset */
+
+static const char *const builtin_clean_usage[] = {
+       "git-clean [-d] [-f] [-n] [-q] [-x | -X] [--] <paths>...",
+       NULL
+};
+
+static int git_clean_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "clean.requireforce"))
+               force = !git_config_bool(var, value);
+       return git_default_config(var, value);
+}
+
+int cmd_clean(int argc, const char **argv, const char *prefix)
+{
+       int i;
+       int show_only = 0, remove_directories = 0, quiet = 0, ignored = 0;
+       int ignored_only = 0, baselen = 0, config_set = 0;
+       struct strbuf directory;
+       struct dir_struct dir;
+       const char *path, *base;
+       static const char **pathspec;
+       char *seen = NULL;
+       struct option options[] = {
+               OPT__QUIET(&quiet),
+               OPT__DRY_RUN(&show_only),
+               OPT_BOOLEAN('f', NULL, &force, "force"),
+               OPT_BOOLEAN('d', NULL, &remove_directories,
+                               "remove whole directories"),
+               OPT_BOOLEAN('x', NULL, &ignored, "remove ignored files, too"),
+               OPT_BOOLEAN('X', NULL, &ignored_only,
+                               "remove only ignored files"),
+               OPT_END()
+       };
+
+       git_config(git_clean_config);
+       if (force < 0)
+               force = 0;
+       else
+               config_set = 1;
+
+       argc = parse_options(argc, argv, options, builtin_clean_usage, 0);
+
+       memset(&dir, 0, sizeof(dir));
+       if (ignored_only)
+               dir.show_ignored = 1;
+
+       if (ignored && ignored_only)
+               die("-x and -X cannot be used together");
+
+       if (!show_only && !force)
+               die("clean.requireForce%s set and -n or -f not given; "
+                   "refusing to clean", config_set ? "" : " not");
+
+       dir.show_other_directories = 1;
+
+       if (!ignored)
+               setup_standard_excludes(&dir);
+
+       pathspec = get_pathspec(prefix, argv);
+       read_cache();
+
+       /*
+        * Calculate common prefix for the pathspec, and
+        * use that to optimize the directory walk
+        */
+       baselen = common_prefix(pathspec);
+       path = ".";
+       base = "";
+       if (baselen)
+               path = base = xmemdupz(*pathspec, baselen);
+       read_directory(&dir, path, base, baselen, pathspec);
+       strbuf_init(&directory, 0);
+
+       if (pathspec)
+               seen = xmalloc(argc);
+
+       for (i = 0; i < dir.nr; i++) {
+               struct dir_entry *ent = dir.entries[i];
+               int len, pos, matches;
+               struct cache_entry *ce;
+               struct stat st;
+
+               /*
+                * Remove the '/' at the end that directory
+                * walking adds for directory entries.
+                */
+               len = ent->len;
+               if (len && ent->name[len-1] == '/')
+                       len--;
+               pos = cache_name_pos(ent->name, len);
+               if (0 <= pos)
+                       continue;       /* exact match */
+               pos = -pos - 1;
+               if (pos < active_nr) {
+                       ce = active_cache[pos];
+                       if (ce_namelen(ce) == len &&
+                           !memcmp(ce->name, ent->name, len))
+                               continue; /* Yup, this one exists unmerged */
+               }
+
+               /*
+                * we might have removed this as part of earlier
+                * recursive directory removal, so lstat() here could
+                * fail with ENOENT.
+                */
+               if (lstat(ent->name, &st))
+                       continue;
+
+               if (pathspec) {
+                       memset(seen, 0, argc);
+                       matches = match_pathspec(pathspec, ent->name, ent->len,
+                                                baselen, seen);
+               } else {
+                       matches = 0;
+               }
+
+               if (S_ISDIR(st.st_mode)) {
+                       strbuf_addstr(&directory, ent->name);
+                       if (show_only && (remove_directories || matches)) {
+                               printf("Would remove %s\n", directory.buf);
+                       } else if (quiet && (remove_directories || matches)) {
+                               remove_dir_recursively(&directory, 0);
+                       } else if (remove_directories || matches) {
+                               printf("Removing %s\n", directory.buf);
+                               remove_dir_recursively(&directory, 0);
+                       } else if (show_only) {
+                               printf("Would not remove %s\n", directory.buf);
+                       } else {
+                               printf("Not removing %s\n", directory.buf);
+                       }
+                       strbuf_reset(&directory);
+               } else {
+                       if (pathspec && !matches)
+                               continue;
+                       if (show_only) {
+                               printf("Would remove %s\n", ent->name);
+                               continue;
+                       } else if (!quiet) {
+                               printf("Removing %s\n", ent->name);
+                       }
+                       unlink(ent->name);
+               }
+       }
+       free(seen);
+
+       strbuf_release(&directory);
+       return 0;
+}
diff --git a/builtin-commit-tree.c b/builtin-commit-tree.c
new file mode 100644 (file)
index 0000000..88b0ab3
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "cache.h"
+#include "commit.h"
+#include "tree.h"
+#include "builtin.h"
+#include "utf8.h"
+
+#define BLOCKING (1ul << 14)
+
+/*
+ * FIXME! Share the code with "write-tree.c"
+ */
+static void check_valid(unsigned char *sha1, enum object_type expect)
+{
+       enum object_type type = sha1_object_info(sha1, NULL);
+       if (type < 0)
+               die("%s is not a valid object", sha1_to_hex(sha1));
+       if (type != expect)
+               die("%s is not a valid '%s' object", sha1_to_hex(sha1),
+                   typename(expect));
+}
+
+/*
+ * Having more than two parents is not strange at all, and this is
+ * how multi-way merges are represented.
+ */
+#define MAXPARENT (16)
+static unsigned char parent_sha1[MAXPARENT][20];
+
+static const char commit_tree_usage[] = "git-commit-tree <sha1> [-p <sha1>]* < changelog";
+
+static int new_parent(int idx)
+{
+       int i;
+       unsigned char *sha1 = parent_sha1[idx];
+       for (i = 0; i < idx; i++) {
+               if (!hashcmp(parent_sha1[i], sha1)) {
+                       error("duplicate parent %s ignored", sha1_to_hex(sha1));
+                       return 0;
+               }
+       }
+       return 1;
+}
+
+static const char commit_utf8_warn[] =
+"Warning: commit message does not conform to UTF-8.\n"
+"You may want to amend it after fixing the message, or set the config\n"
+"variable i18n.commitencoding to the encoding your project uses.\n";
+
+int cmd_commit_tree(int argc, const char **argv, const char *prefix)
+{
+       int i;
+       int parents = 0;
+       unsigned char tree_sha1[20];
+       unsigned char commit_sha1[20];
+       struct strbuf buffer;
+       int encoding_is_utf8;
+
+       git_config(git_default_config);
+
+       if (argc < 2)
+               usage(commit_tree_usage);
+       if (get_sha1(argv[1], tree_sha1))
+               die("Not a valid object name %s", argv[1]);
+
+       check_valid(tree_sha1, OBJ_TREE);
+       for (i = 2; i < argc; i += 2) {
+               const char *a, *b;
+               a = argv[i]; b = argv[i+1];
+               if (!b || strcmp(a, "-p"))
+                       usage(commit_tree_usage);
+
+               if (parents >= MAXPARENT)
+                       die("Too many parents (%d max)", MAXPARENT);
+               if (get_sha1(b, parent_sha1[parents]))
+                       die("Not a valid object name %s", b);
+               check_valid(parent_sha1[parents], OBJ_COMMIT);
+               if (new_parent(parents))
+                       parents++;
+       }
+
+       /* Not having i18n.commitencoding is the same as having utf-8 */
+       encoding_is_utf8 = is_encoding_utf8(git_commit_encoding);
+
+       strbuf_init(&buffer, 8192); /* should avoid reallocs for the headers */
+       strbuf_addf(&buffer, "tree %s\n", sha1_to_hex(tree_sha1));
+
+       /*
+        * NOTE! This ordering means that the same exact tree merged with a
+        * different order of parents will be a _different_ changeset even
+        * if everything else stays the same.
+        */
+       for (i = 0; i < parents; i++)
+               strbuf_addf(&buffer, "parent %s\n", sha1_to_hex(parent_sha1[i]));
+
+       /* Person/date information */
+       strbuf_addf(&buffer, "author %s\n", git_author_info(1));
+       strbuf_addf(&buffer, "committer %s\n", git_committer_info(1));
+       if (!encoding_is_utf8)
+               strbuf_addf(&buffer, "encoding %s\n", git_commit_encoding);
+       strbuf_addch(&buffer, '\n');
+
+       /* And add the comment */
+       if (strbuf_read(&buffer, 0, 0) < 0)
+               die("git-commit-tree: read returned %s", strerror(errno));
+
+       /* And check the encoding */
+       if (encoding_is_utf8 && !is_utf8(buffer.buf))
+               fprintf(stderr, commit_utf8_warn);
+
+       if (!write_sha1_file(buffer.buf, buffer.len, commit_type, commit_sha1)) {
+               printf("%s\n", sha1_to_hex(commit_sha1));
+               return 0;
+       }
+       else
+               return 1;
+}
diff --git a/builtin-commit.c b/builtin-commit.c
new file mode 100644 (file)
index 0000000..2ec8223
--- /dev/null
@@ -0,0 +1,851 @@
+/*
+ * Builtin "git commit"
+ *
+ * Copyright (c) 2007 Kristian Høgsberg <krh@redhat.com>
+ * Based on git-commit.sh by Junio C Hamano and Linus Torvalds
+ */
+
+#include "cache.h"
+#include "cache-tree.h"
+#include "dir.h"
+#include "builtin.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "commit.h"
+#include "revision.h"
+#include "wt-status.h"
+#include "run-command.h"
+#include "refs.h"
+#include "log-tree.h"
+#include "strbuf.h"
+#include "utf8.h"
+#include "parse-options.h"
+#include "path-list.h"
+
+static const char * const builtin_commit_usage[] = {
+       "git-commit [options] [--] <filepattern>...",
+       NULL
+};
+
+static const char * const builtin_status_usage[] = {
+       "git-status [options] [--] <filepattern>...",
+       NULL
+};
+
+static unsigned char head_sha1[20], merge_head_sha1[20];
+static char *use_message_buffer;
+static const char commit_editmsg[] = "COMMIT_EDITMSG";
+static struct lock_file index_lock; /* real index */
+static struct lock_file false_lock; /* used only for partial commits */
+static enum {
+       COMMIT_AS_IS = 1,
+       COMMIT_NORMAL,
+       COMMIT_PARTIAL,
+} commit_style;
+
+static char *logfile, *force_author, *template_file;
+static char *edit_message, *use_message;
+static int all, edit_flag, also, interactive, only, amend, signoff;
+static int quiet, verbose, untracked_files, no_verify, allow_empty;
+
+static int no_edit, initial_commit, in_merge;
+const char *only_include_assumed;
+struct strbuf message;
+
+static int opt_parse_m(const struct option *opt, const char *arg, int unset)
+{
+       struct strbuf *buf = opt->value;
+       if (unset)
+               strbuf_setlen(buf, 0);
+       else {
+               strbuf_addstr(buf, arg);
+               strbuf_addch(buf, '\n');
+               strbuf_addch(buf, '\n');
+       }
+       return 0;
+}
+
+static struct option builtin_commit_options[] = {
+       OPT__QUIET(&quiet),
+       OPT__VERBOSE(&verbose),
+       OPT_GROUP("Commit message options"),
+
+       OPT_STRING('F', "file", &logfile, "FILE", "read log from file"),
+       OPT_STRING(0, "author", &force_author, "AUTHOR", "override author for commit"),
+       OPT_CALLBACK('m', "message", &message, "MESSAGE", "specify commit message", opt_parse_m),
+       OPT_STRING('c', "reedit-message", &edit_message, "COMMIT", "reuse and edit message from specified commit "),
+       OPT_STRING('C', "reuse-message", &use_message, "COMMIT", "reuse message from specified commit"),
+       OPT_BOOLEAN('s', "signoff", &signoff, "add Signed-off-by: header"),
+       OPT_STRING('t', "template", &template_file, "FILE", "use specified template file"),
+       OPT_BOOLEAN('e', "edit", &edit_flag, "force edit of commit"),
+
+       OPT_GROUP("Commit contents options"),
+       OPT_BOOLEAN('a', "all", &all, "commit all changed files"),
+       OPT_BOOLEAN('i', "include", &also, "add specified files to index for commit"),
+       OPT_BOOLEAN(0, "interactive", &interactive, "interactively add files"),
+       OPT_BOOLEAN('o', "only", &only, ""),
+       OPT_BOOLEAN('n', "no-verify", &no_verify, "bypass pre-commit hook"),
+       OPT_BOOLEAN(0, "amend", &amend, "amend previous commit"),
+       OPT_BOOLEAN(0, "untracked-files", &untracked_files, "show all untracked files"),
+       OPT_BOOLEAN(0, "allow-empty", &allow_empty, "ok to record an empty change"),
+
+       OPT_END()
+};
+
+static void rollback_index_files(void)
+{
+       switch (commit_style) {
+       case COMMIT_AS_IS:
+               break; /* nothing to do */
+       case COMMIT_NORMAL:
+               rollback_lock_file(&index_lock);
+               break;
+       case COMMIT_PARTIAL:
+               rollback_lock_file(&index_lock);
+               rollback_lock_file(&false_lock);
+               break;
+       }
+}
+
+static void commit_index_files(void)
+{
+       switch (commit_style) {
+       case COMMIT_AS_IS:
+               break; /* nothing to do */
+       case COMMIT_NORMAL:
+               commit_lock_file(&index_lock);
+               break;
+       case COMMIT_PARTIAL:
+               commit_lock_file(&index_lock);
+               rollback_lock_file(&false_lock);
+               break;
+       }
+}
+
+/*
+ * Take a union of paths in the index and the named tree (typically, "HEAD"),
+ * and return the paths that match the given pattern in list.
+ */
+static int list_paths(struct path_list *list, const char *with_tree,
+                     const char *prefix, const char **pattern)
+{
+       int i;
+       char *m;
+
+       for (i = 0; pattern[i]; i++)
+               ;
+       m = xcalloc(1, i);
+
+       if (with_tree)
+               overlay_tree_on_cache(with_tree, prefix);
+
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (ce->ce_flags & htons(CE_UPDATE))
+                       continue;
+               if (!pathspec_match(pattern, m, ce->name, 0))
+                       continue;
+               path_list_insert(ce->name, list);
+       }
+
+       return report_path_error(m, pattern, prefix ? strlen(prefix) : 0);
+}
+
+static void add_remove_files(struct path_list *list)
+{
+       int i;
+       for (i = 0; i < list->nr; i++) {
+               struct path_list_item *p = &(list->items[i]);
+               if (file_exists(p->path))
+                       add_file_to_cache(p->path, 0);
+               else
+                       remove_file_from_cache(p->path);
+       }
+}
+
+static char *prepare_index(int argc, const char **argv, const char *prefix)
+{
+       int fd;
+       struct tree *tree;
+       struct path_list partial;
+       const char **pathspec = NULL;
+
+       if (interactive) {
+               interactive_add(argc, argv, prefix);
+               commit_style = COMMIT_AS_IS;
+               return get_index_file();
+       }
+
+       if (read_cache() < 0)
+               die("index file corrupt");
+
+       if (*argv)
+               pathspec = get_pathspec(prefix, argv);
+
+       /*
+        * Non partial, non as-is commit.
+        *
+        * (1) get the real index;
+        * (2) update the_index as necessary;
+        * (3) write the_index out to the real index (still locked);
+        * (4) return the name of the locked index file.
+        *
+        * The caller should run hooks on the locked real index, and
+        * (A) if all goes well, commit the real index;
+        * (B) on failure, rollback the real index.
+        */
+       if (all || (also && pathspec && *pathspec)) {
+               int fd = hold_locked_index(&index_lock, 1);
+               add_files_to_cache(0, also ? prefix : NULL, pathspec);
+               refresh_cache(REFRESH_QUIET);
+               if (write_cache(fd, active_cache, active_nr) || close(fd))
+                       die("unable to write new_index file");
+               commit_style = COMMIT_NORMAL;
+               return index_lock.filename;
+       }
+
+       /*
+        * As-is commit.
+        *
+        * (1) return the name of the real index file.
+        *
+        * The caller should run hooks on the real index, and run
+        * hooks on the real index, and create commit from the_index.
+        * We still need to refresh the index here.
+        */
+       if (!pathspec || !*pathspec) {
+               fd = hold_locked_index(&index_lock, 1);
+               refresh_cache(REFRESH_QUIET);
+               if (write_cache(fd, active_cache, active_nr) ||
+                   close(fd) || commit_locked_index(&index_lock))
+                       die("unable to write new_index file");
+               commit_style = COMMIT_AS_IS;
+               return get_index_file();
+       }
+
+       /*
+        * A partial commit.
+        *
+        * (0) find the set of affected paths;
+        * (1) get lock on the real index file;
+        * (2) update the_index with the given paths;
+        * (3) write the_index out to the real index (still locked);
+        * (4) get lock on the false index file;
+        * (5) reset the_index from HEAD;
+        * (6) update the_index the same way as (2);
+        * (7) write the_index out to the false index file;
+        * (8) return the name of the false index file (still locked);
+        *
+        * The caller should run hooks on the locked false index, and
+        * create commit from it.  Then
+        * (A) if all goes well, commit the real index;
+        * (B) on failure, rollback the real index;
+        * In either case, rollback the false index.
+        */
+       commit_style = COMMIT_PARTIAL;
+
+       if (file_exists(git_path("MERGE_HEAD")))
+               die("cannot do a partial commit during a merge.");
+
+       memset(&partial, 0, sizeof(partial));
+       partial.strdup_paths = 1;
+       if (list_paths(&partial, initial_commit ? NULL : "HEAD", prefix, pathspec))
+               exit(1);
+
+       discard_cache();
+       if (read_cache() < 0)
+               die("cannot read the index");
+
+       fd = hold_locked_index(&index_lock, 1);
+       add_remove_files(&partial);
+       refresh_cache(REFRESH_QUIET);
+       if (write_cache(fd, active_cache, active_nr) || close(fd))
+               die("unable to write new_index file");
+
+       fd = hold_lock_file_for_update(&false_lock,
+                                      git_path("next-index-%d", getpid()), 1);
+       discard_cache();
+       if (!initial_commit) {
+               tree = parse_tree_indirect(head_sha1);
+               if (!tree)
+                       die("failed to unpack HEAD tree object");
+               if (read_tree(tree, 0, NULL))
+                       die("failed to read HEAD tree object");
+       }
+       add_remove_files(&partial);
+       refresh_cache(REFRESH_QUIET);
+
+       if (write_cache(fd, active_cache, active_nr) || close(fd))
+               die("unable to write temporary index file");
+       return false_lock.filename;
+}
+
+static int run_status(FILE *fp, const char *index_file, const char *prefix)
+{
+       struct wt_status s;
+
+       wt_status_prepare(&s);
+       s.prefix = prefix;
+
+       if (amend) {
+               s.amend = 1;
+               s.reference = "HEAD^1";
+       }
+       s.verbose = verbose;
+       s.untracked = untracked_files;
+       s.index_file = index_file;
+       s.fp = fp;
+
+       wt_status_print(&s);
+
+       return s.commitable;
+}
+
+static const char sign_off_header[] = "Signed-off-by: ";
+
+static int prepare_log_message(const char *index_file, const char *prefix)
+{
+       struct stat statbuf;
+       int commitable, saved_color_setting;
+       struct strbuf sb;
+       char *buffer;
+       FILE *fp;
+
+       strbuf_init(&sb, 0);
+       if (message.len) {
+               strbuf_addbuf(&sb, &message);
+       } else if (logfile && !strcmp(logfile, "-")) {
+               if (isatty(0))
+                       fprintf(stderr, "(reading log message from standard input)\n");
+               if (strbuf_read(&sb, 0, 0) < 0)
+                       die("could not read log from standard input");
+       } else if (logfile) {
+               if (strbuf_read_file(&sb, logfile, 0) < 0)
+                       die("could not read log file '%s': %s",
+                           logfile, strerror(errno));
+       } else if (use_message) {
+               buffer = strstr(use_message_buffer, "\n\n");
+               if (!buffer || buffer[2] == '\0')
+                       die("commit has empty message");
+               strbuf_add(&sb, buffer + 2, strlen(buffer + 2));
+       } else if (!stat(git_path("MERGE_MSG"), &statbuf)) {
+               if (strbuf_read_file(&sb, git_path("MERGE_MSG"), 0) < 0)
+                       die("could not read MERGE_MSG: %s", strerror(errno));
+       } else if (!stat(git_path("SQUASH_MSG"), &statbuf)) {
+               if (strbuf_read_file(&sb, git_path("SQUASH_MSG"), 0) < 0)
+                       die("could not read SQUASH_MSG: %s", strerror(errno));
+       } else if (template_file && !stat(template_file, &statbuf)) {
+               if (strbuf_read_file(&sb, template_file, 0) < 0)
+                       die("could not read %s: %s",
+                           template_file, strerror(errno));
+       }
+
+       fp = fopen(git_path(commit_editmsg), "w");
+       if (fp == NULL)
+               die("could not open %s", git_path(commit_editmsg));
+
+       stripspace(&sb, 0);
+
+       if (signoff) {
+               struct strbuf sob;
+               int i;
+
+               strbuf_init(&sob, 0);
+               strbuf_addstr(&sob, sign_off_header);
+               strbuf_addstr(&sob, fmt_name(getenv("GIT_COMMITTER_NAME"),
+                                            getenv("GIT_COMMITTER_EMAIL")));
+               strbuf_addch(&sob, '\n');
+               for (i = sb.len - 1; i > 0 && sb.buf[i - 1] != '\n'; i--)
+                       ; /* do nothing */
+               if (prefixcmp(sb.buf + i, sob.buf)) {
+                       if (prefixcmp(sb.buf + i, sign_off_header))
+                               strbuf_addch(&sb, '\n');
+                       strbuf_addbuf(&sb, &sob);
+               }
+               strbuf_release(&sob);
+       }
+
+       if (fwrite(sb.buf, 1, sb.len, fp) < sb.len)
+               die("could not write commit template: %s", strerror(errno));
+
+       strbuf_release(&sb);
+
+       if (no_edit) {
+               struct rev_info rev;
+               unsigned char sha1[40];
+
+               fclose(fp);
+
+               if (!active_nr && read_cache() < 0)
+                       die("Cannot read index");
+
+               if (get_sha1("HEAD", sha1) != 0)
+                       return !!active_nr;
+
+               init_revisions(&rev, "");
+               rev.abbrev = 0;
+               setup_revisions(0, NULL, &rev, "HEAD");
+               DIFF_OPT_SET(&rev.diffopt, QUIET);
+               DIFF_OPT_SET(&rev.diffopt, EXIT_WITH_STATUS);
+               run_diff_index(&rev, 1 /* cached */);
+
+               return !!DIFF_OPT_TST(&rev.diffopt, HAS_CHANGES);
+       }
+
+       if (in_merge && !no_edit)
+               fprintf(fp,
+                       "#\n"
+                       "# It looks like you may be committing a MERGE.\n"
+                       "# If this is not correct, please remove the file\n"
+                       "#      %s\n"
+                       "# and try again.\n"
+                       "#\n",
+                       git_path("MERGE_HEAD"));
+
+       fprintf(fp,
+               "\n"
+               "# Please enter the commit message for your changes.\n"
+               "# (Comment lines starting with '#' will not be included)\n");
+       if (only_include_assumed)
+               fprintf(fp, "# %s\n", only_include_assumed);
+
+       saved_color_setting = wt_status_use_color;
+       wt_status_use_color = 0;
+       commitable = run_status(fp, index_file, prefix);
+       wt_status_use_color = saved_color_setting;
+
+       fclose(fp);
+
+       return commitable;
+}
+
+/*
+ * Find out if the message starting at position 'start' in the strbuf
+ * contains only whitespace and Signed-off-by lines.
+ */
+static int message_is_empty(struct strbuf *sb, int start)
+{
+       struct strbuf tmpl;
+       const char *nl;
+       int eol, i;
+
+       /* See if the template is just a prefix of the message. */
+       strbuf_init(&tmpl, 0);
+       if (template_file && strbuf_read_file(&tmpl, template_file, 0) > 0) {
+               stripspace(&tmpl, 1);
+               if (start + tmpl.len <= sb->len &&
+                   memcmp(tmpl.buf, sb->buf + start, tmpl.len) == 0)
+                       start += tmpl.len;
+       }
+       strbuf_release(&tmpl);
+
+       /* Check if the rest is just whitespace and Signed-of-by's. */
+       for (i = start; i < sb->len; i++) {
+               nl = memchr(sb->buf + i, '\n', sb->len - i);
+               if (nl)
+                       eol = nl - sb->buf;
+               else
+                       eol = sb->len;
+
+               if (strlen(sign_off_header) <= eol - i &&
+                   !prefixcmp(sb->buf + i, sign_off_header)) {
+                       i = eol;
+                       continue;
+               }
+               while (i < eol)
+                       if (!isspace(sb->buf[i++]))
+                               return 0;
+       }
+
+       return 1;
+}
+
+static void determine_author_info(struct strbuf *sb)
+{
+       char *name, *email, *date;
+
+       name = getenv("GIT_AUTHOR_NAME");
+       email = getenv("GIT_AUTHOR_EMAIL");
+       date = getenv("GIT_AUTHOR_DATE");
+
+       if (use_message) {
+               const char *a, *lb, *rb, *eol;
+
+               a = strstr(use_message_buffer, "\nauthor ");
+               if (!a)
+                       die("invalid commit: %s", use_message);
+
+               lb = strstr(a + 8, " <");
+               rb = strstr(a + 8, "> ");
+               eol = strchr(a + 8, '\n');
+               if (!lb || !rb || !eol)
+                       die("invalid commit: %s", use_message);
+
+               name = xstrndup(a + 8, lb - (a + 8));
+               email = xstrndup(lb + 2, rb - (lb + 2));
+               date = xstrndup(rb + 2, eol - (rb + 2));
+       }
+
+       if (force_author) {
+               const char *lb = strstr(force_author, " <");
+               const char *rb = strchr(force_author, '>');
+
+               if (!lb || !rb)
+                       die("malformed --author parameter");
+               name = xstrndup(force_author, lb - force_author);
+               email = xstrndup(lb + 2, rb - (lb + 2));
+       }
+
+       strbuf_addf(sb, "author %s\n", fmt_ident(name, email, date, 1));
+}
+
+static int parse_and_validate_options(int argc, const char *argv[],
+                                     const char * const usage[])
+{
+       int f = 0;
+
+       argc = parse_options(argc, argv, builtin_commit_options, usage, 0);
+
+       if (logfile || message.len || use_message)
+               no_edit = 1;
+       if (edit_flag)
+               no_edit = 0;
+
+       if (get_sha1("HEAD", head_sha1))
+               initial_commit = 1;
+
+       if (!get_sha1("MERGE_HEAD", merge_head_sha1))
+               in_merge = 1;
+
+       /* Sanity check options */
+       if (amend && initial_commit)
+               die("You have nothing to amend.");
+       if (amend && in_merge)
+               die("You are in the middle of a merge -- cannot amend.");
+
+       if (use_message)
+               f++;
+       if (edit_message)
+               f++;
+       if (logfile)
+               f++;
+       if (f > 1)
+               die("Only one of -c/-C/-F can be used.");
+       if (message.len && f > 0)
+               die("Option -m cannot be combined with -c/-C/-F.");
+       if (edit_message)
+               use_message = edit_message;
+       if (amend)
+               use_message = "HEAD";
+       if (use_message) {
+               unsigned char sha1[20];
+               static char utf8[] = "UTF-8";
+               const char *out_enc;
+               char *enc, *end;
+               struct commit *commit;
+
+               if (get_sha1(use_message, sha1))
+                       die("could not lookup commit %s", use_message);
+               commit = lookup_commit(sha1);
+               if (!commit || parse_commit(commit))
+                       die("could not parse commit %s", use_message);
+
+               enc = strstr(commit->buffer, "\nencoding");
+               if (enc) {
+                       end = strchr(enc + 10, '\n');
+                       enc = xstrndup(enc + 10, end - (enc + 10));
+               } else {
+                       enc = utf8;
+               }
+               out_enc = git_commit_encoding ? git_commit_encoding : utf8;
+
+               if (strcmp(out_enc, enc))
+                       use_message_buffer =
+                               reencode_string(commit->buffer, out_enc, enc);
+
+               /*
+                * If we failed to reencode the buffer, just copy it
+                * byte for byte so the user can try to fix it up.
+                * This also handles the case where input and output
+                * encodings are identical.
+                */
+               if (use_message_buffer == NULL)
+                       use_message_buffer = xstrdup(commit->buffer);
+               if (enc != utf8)
+                       free(enc);
+       }
+
+       if (!!also + !!only + !!all + !!interactive > 1)
+               die("Only one of --include/--only/--all/--interactive can be used.");
+       if (argc == 0 && (also || (only && !amend)))
+               die("No paths with --include/--only does not make sense.");
+       if (argc == 0 && only && amend)
+               only_include_assumed = "Clever... amending the last one with dirty index.";
+       if (argc > 0 && !also && !only) {
+               only_include_assumed = "Explicit paths specified without -i nor -o; assuming --only paths...";
+               also = 0;
+       }
+
+       if (all && argc > 0)
+               die("Paths with -a does not make sense.");
+       else if (interactive && argc > 0)
+               die("Paths with --interactive does not make sense.");
+
+       return argc;
+}
+
+int cmd_status(int argc, const char **argv, const char *prefix)
+{
+       const char *index_file;
+       int commitable;
+
+       git_config(git_status_config);
+
+       argc = parse_and_validate_options(argc, argv, builtin_status_usage);
+
+       index_file = prepare_index(argc, argv, prefix);
+
+       commitable = run_status(stdout, index_file, prefix);
+
+       rollback_index_files();
+
+       return commitable ? 0 : 1;
+}
+
+static int run_hook(const char *index_file, const char *name, const char *arg)
+{
+       struct child_process hook;
+       const char *argv[3], *env[2];
+       char index[PATH_MAX];
+
+       argv[0] = git_path("hooks/%s", name);
+       argv[1] = arg;
+       argv[2] = NULL;
+       snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", index_file);
+       env[0] = index;
+       env[1] = NULL;
+
+       if (access(argv[0], X_OK) < 0)
+               return 0;
+
+       memset(&hook, 0, sizeof(hook));
+       hook.argv = argv;
+       hook.no_stdin = 1;
+       hook.stdout_to_stderr = 1;
+       hook.env = env;
+
+       return run_command(&hook);
+}
+
+static void print_summary(const char *prefix, const unsigned char *sha1)
+{
+       struct rev_info rev;
+       struct commit *commit;
+
+       commit = lookup_commit(sha1);
+       if (!commit)
+               die("couldn't look up newly created commit");
+       if (!commit || parse_commit(commit))
+               die("could not parse newly created commit");
+
+       init_revisions(&rev, prefix);
+       setup_revisions(0, NULL, &rev, NULL);
+
+       rev.abbrev = 0;
+       rev.diff = 1;
+       rev.diffopt.output_format =
+               DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_SUMMARY;
+
+       rev.verbose_header = 1;
+       rev.show_root_diff = 1;
+       rev.commit_format = get_commit_format("format:%h: %s");
+       rev.always_show_header = 1;
+
+       printf("Created %scommit ", initial_commit ? "initial " : "");
+
+       log_tree_commit(&rev, commit);
+       printf("\n");
+}
+
+int git_commit_config(const char *k, const char *v)
+{
+       if (!strcmp(k, "commit.template")) {
+               template_file = xstrdup(v);
+               return 0;
+       }
+
+       return git_status_config(k, v);
+}
+
+static int is_a_merge(const unsigned char *sha1)
+{
+       struct commit *commit = lookup_commit(sha1);
+       if (!commit || parse_commit(commit))
+               die("could not parse HEAD commit");
+       return !!(commit->parents && commit->parents->next);
+}
+
+static const char commit_utf8_warn[] =
+"Warning: commit message does not conform to UTF-8.\n"
+"You may want to amend it after fixing the message, or set the config\n"
+"variable i18n.commitencoding to the encoding your project uses.\n";
+
+int cmd_commit(int argc, const char **argv, const char *prefix)
+{
+       int header_len;
+       struct strbuf sb;
+       const char *index_file, *reflog_msg;
+       char *nl, *p;
+       unsigned char commit_sha1[20];
+       struct ref_lock *ref_lock;
+
+       git_config(git_commit_config);
+
+       argc = parse_and_validate_options(argc, argv, builtin_commit_usage);
+
+       index_file = prepare_index(argc, argv, prefix);
+
+       if (!no_verify && run_hook(index_file, "pre-commit", NULL)) {
+               rollback_index_files();
+               return 1;
+       }
+
+       if (!prepare_log_message(index_file, prefix) && !in_merge &&
+           !allow_empty && !(amend && is_a_merge(head_sha1))) {
+               run_status(stdout, index_file, prefix);
+               rollback_index_files();
+               unlink(commit_editmsg);
+               return 1;
+       }
+
+       /*
+        * Re-read the index as pre-commit hook could have updated it,
+        * and write it out as a tree.
+        */
+       discard_cache();
+       read_cache_from(index_file);
+       if (!active_cache_tree)
+               active_cache_tree = cache_tree();
+       if (cache_tree_update(active_cache_tree,
+                             active_cache, active_nr, 0, 0) < 0) {
+               rollback_index_files();
+               die("Error building trees");
+       }
+
+       /*
+        * The commit object
+        */
+       strbuf_init(&sb, 0);
+       strbuf_addf(&sb, "tree %s\n",
+                   sha1_to_hex(active_cache_tree->sha1));
+
+       /* Determine parents */
+       if (initial_commit) {
+               reflog_msg = "commit (initial)";
+       } else if (amend) {
+               struct commit_list *c;
+               struct commit *commit;
+
+               reflog_msg = "commit (amend)";
+               commit = lookup_commit(head_sha1);
+               if (!commit || parse_commit(commit))
+                       die("could not parse HEAD commit");
+
+               for (c = commit->parents; c; c = c->next)
+                       strbuf_addf(&sb, "parent %s\n",
+                                     sha1_to_hex(c->item->object.sha1));
+       } else if (in_merge) {
+               struct strbuf m;
+               FILE *fp;
+
+               reflog_msg = "commit (merge)";
+               strbuf_addf(&sb, "parent %s\n", sha1_to_hex(head_sha1));
+               strbuf_init(&m, 0);
+               fp = fopen(git_path("MERGE_HEAD"), "r");
+               if (fp == NULL)
+                       die("could not open %s for reading: %s",
+                           git_path("MERGE_HEAD"), strerror(errno));
+               while (strbuf_getline(&m, fp, '\n') != EOF)
+                       strbuf_addf(&sb, "parent %s\n", m.buf);
+               fclose(fp);
+               strbuf_release(&m);
+       } else {
+               reflog_msg = "commit";
+               strbuf_addf(&sb, "parent %s\n", sha1_to_hex(head_sha1));
+       }
+
+       determine_author_info(&sb);
+       strbuf_addf(&sb, "committer %s\n", git_committer_info(1));
+       if (!is_encoding_utf8(git_commit_encoding))
+               strbuf_addf(&sb, "encoding %s\n", git_commit_encoding);
+       strbuf_addch(&sb, '\n');
+
+       /* Get the commit message and validate it */
+       header_len = sb.len;
+       if (!no_edit) {
+               char index[PATH_MAX];
+               const char *env[2] = { index, NULL };
+               snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", index_file);
+               launch_editor(git_path(commit_editmsg), &sb, env);
+       } else if (strbuf_read_file(&sb, git_path(commit_editmsg), 0) < 0) {
+               rollback_index_files();
+               die("could not read commit message");
+       }
+       if (run_hook(index_file, "commit-msg", git_path(commit_editmsg))) {
+               rollback_index_files();
+               exit(1);
+       }
+
+       /* Truncate the message just before the diff, if any. */
+       p = strstr(sb.buf, "\ndiff --git a/");
+       if (p != NULL)
+               strbuf_setlen(&sb, p - sb.buf + 1);
+
+       stripspace(&sb, 1);
+       if (sb.len < header_len || message_is_empty(&sb, header_len)) {
+               rollback_index_files();
+               die("no commit message?  aborting commit.");
+       }
+       strbuf_addch(&sb, '\0');
+       if (is_encoding_utf8(git_commit_encoding) && !is_utf8(sb.buf))
+               fprintf(stderr, commit_utf8_warn);
+
+       if (write_sha1_file(sb.buf, sb.len - 1, commit_type, commit_sha1)) {
+               rollback_index_files();
+               die("failed to write commit object");
+       }
+
+       ref_lock = lock_any_ref_for_update("HEAD",
+                                          initial_commit ? NULL : head_sha1,
+                                          0);
+
+       nl = strchr(sb.buf + header_len, '\n');
+       if (nl)
+               strbuf_setlen(&sb, nl + 1 - sb.buf);
+       else
+               strbuf_addch(&sb, '\n');
+       strbuf_remove(&sb, 0, header_len);
+       strbuf_insert(&sb, 0, reflog_msg, strlen(reflog_msg));
+       strbuf_insert(&sb, strlen(reflog_msg), ": ", 2);
+
+       if (!ref_lock) {
+               rollback_index_files();
+               die("cannot lock HEAD ref");
+       }
+       if (write_ref_sha1(ref_lock, commit_sha1, sb.buf) < 0) {
+               rollback_index_files();
+               die("cannot update HEAD ref");
+       }
+
+       unlink(git_path("MERGE_HEAD"));
+       unlink(git_path("MERGE_MSG"));
+
+       commit_index_files();
+
+       rerere();
+       run_hook(get_index_file(), "post-commit", NULL);
+       if (!quiet)
+               print_summary(prefix, commit_sha1);
+
+       return 0;
+}
diff --git a/builtin-config.c b/builtin-config.c
new file mode 100644 (file)
index 0000000..e4a12e3
--- /dev/null
@@ -0,0 +1,402 @@
+#include "builtin.h"
+#include "cache.h"
+#include "color.h"
+
+static const char git_config_set_usage[] =
+"git-config [ --global | --system | [ -f | --file ] config-file ] [ --bool | --int ] [ -z | --null ] [--get | --get-all | --get-regexp | --replace-all | --add | --unset | --unset-all] name [value [value_regex]] | --rename-section old_name new_name | --remove-section name | --list | --get-color var [default] | --get-colorbool name [stdout-is-tty]";
+
+static char *key;
+static regex_t *key_regexp;
+static regex_t *regexp;
+static int show_keys;
+static int use_key_regexp;
+static int do_all;
+static int do_not_match;
+static int seen;
+static char delim = '=';
+static char key_delim = ' ';
+static char term = '\n';
+static enum { T_RAW, T_INT, T_BOOL } type = T_RAW;
+
+static int show_all_config(const char *key_, const char *value_)
+{
+       if (value_)
+               printf("%s%c%s%c", key_, delim, value_, term);
+       else
+               printf("%s%c", key_, term);
+       return 0;
+}
+
+static int show_config(const char* key_, const char* value_)
+{
+       char value[256];
+       const char *vptr = value;
+       int dup_error = 0;
+
+       if (!use_key_regexp && strcmp(key_, key))
+               return 0;
+       if (use_key_regexp && regexec(key_regexp, key_, 0, NULL, 0))
+               return 0;
+       if (regexp != NULL &&
+           (do_not_match ^ !!regexec(regexp, (value_?value_:""), 0, NULL, 0)))
+               return 0;
+
+       if (show_keys) {
+               if (value_)
+                       printf("%s%c", key_, key_delim);
+               else
+                       printf("%s", key_);
+       }
+       if (seen && !do_all)
+               dup_error = 1;
+       if (type == T_INT)
+               sprintf(value, "%d", git_config_int(key_, value_?value_:""));
+       else if (type == T_BOOL)
+               vptr = git_config_bool(key_, value_) ? "true" : "false";
+       else
+               vptr = value_?value_:"";
+       seen++;
+       if (dup_error) {
+               error("More than one value for the key %s: %s",
+                               key_, vptr);
+       }
+       else
+               printf("%s%c", vptr, term);
+
+       return 0;
+}
+
+static int get_value(const char* key_, const char* regex_)
+{
+       int ret = -1;
+       char *tl;
+       char *global = NULL, *repo_config = NULL;
+       const char *system_wide = NULL, *local;
+
+       local = getenv(CONFIG_ENVIRONMENT);
+       if (!local) {
+               const char *home = getenv("HOME");
+               local = getenv(CONFIG_LOCAL_ENVIRONMENT);
+               if (!local)
+                       local = repo_config = xstrdup(git_path("config"));
+               if (home)
+                       global = xstrdup(mkpath("%s/.gitconfig", home));
+               system_wide = git_etc_gitconfig();
+       }
+
+       key = xstrdup(key_);
+       for (tl=key+strlen(key)-1; tl >= key && *tl != '.'; --tl)
+               *tl = tolower(*tl);
+       for (tl=key; *tl && *tl != '.'; ++tl)
+               *tl = tolower(*tl);
+
+       if (use_key_regexp) {
+               key_regexp = (regex_t*)xmalloc(sizeof(regex_t));
+               if (regcomp(key_regexp, key, REG_EXTENDED)) {
+                       fprintf(stderr, "Invalid key pattern: %s\n", key_);
+                       goto free_strings;
+               }
+       }
+
+       if (regex_) {
+               if (regex_[0] == '!') {
+                       do_not_match = 1;
+                       regex_++;
+               }
+
+               regexp = (regex_t*)xmalloc(sizeof(regex_t));
+               if (regcomp(regexp, regex_, REG_EXTENDED)) {
+                       fprintf(stderr, "Invalid pattern: %s\n", regex_);
+                       goto free_strings;
+               }
+       }
+
+       if (do_all && system_wide)
+               git_config_from_file(show_config, system_wide);
+       if (do_all && global)
+               git_config_from_file(show_config, global);
+       git_config_from_file(show_config, local);
+       if (!do_all && !seen && global)
+               git_config_from_file(show_config, global);
+       if (!do_all && !seen && system_wide)
+               git_config_from_file(show_config, system_wide);
+
+       free(key);
+       if (regexp) {
+               regfree(regexp);
+               free(regexp);
+       }
+
+       if (do_all)
+               ret = !seen;
+       else
+               ret = (seen == 1) ? 0 : seen > 1 ? 2 : 1;
+
+free_strings:
+       free(repo_config);
+       free(global);
+       return ret;
+}
+
+char *normalize_value(const char *key, const char *value)
+{
+       char *normalized;
+
+       if (!value)
+               return NULL;
+
+       if (type == T_RAW)
+               normalized = xstrdup(value);
+       else {
+               normalized = xmalloc(64);
+               if (type == T_INT) {
+                       int v = git_config_int(key, value);
+                       sprintf(normalized, "%d", v);
+               }
+               else if (type == T_BOOL)
+                       sprintf(normalized, "%s",
+                               git_config_bool(key, value) ? "true" : "false");
+       }
+
+       return normalized;
+}
+
+static int get_color_found;
+static const char *get_color_slot;
+static char parsed_color[COLOR_MAXLEN];
+
+static int git_get_color_config(const char *var, const char *value)
+{
+       if (!strcmp(var, get_color_slot)) {
+               color_parse(value, var, parsed_color);
+               get_color_found = 1;
+       }
+       return 0;
+}
+
+static int get_color(int argc, const char **argv)
+{
+       /*
+        * grab the color setting for the given slot from the configuration,
+        * or parse the default value if missing, and return ANSI color
+        * escape sequence.
+        *
+        * e.g.
+        * git config --get-color color.diff.whitespace "blue reverse"
+        */
+       const char *def_color = NULL;
+
+       switch (argc) {
+       default:
+               usage(git_config_set_usage);
+       case 2:
+               def_color = argv[1];
+               /* fallthru */
+       case 1:
+               get_color_slot = argv[0];
+               break;
+       }
+
+       get_color_found = 0;
+       parsed_color[0] = '\0';
+       git_config(git_get_color_config);
+
+       if (!get_color_found && def_color)
+               color_parse(def_color, "command line", parsed_color);
+
+       fputs(parsed_color, stdout);
+       return 0;
+}
+
+static int stdout_is_tty;
+static int get_colorbool_found;
+static int get_diff_color_found;
+static int git_get_colorbool_config(const char *var, const char *value)
+{
+       if (!strcmp(var, get_color_slot)) {
+               get_colorbool_found =
+                       git_config_colorbool(var, value, stdout_is_tty);
+       }
+       if (!strcmp(var, "diff.color")) {
+               get_diff_color_found =
+                       git_config_colorbool(var, value, stdout_is_tty);
+       }
+       return 0;
+}
+
+static int get_colorbool(int argc, const char **argv)
+{
+       /*
+        * git config --get-colorbool <slot> [<stdout-is-tty>]
+        *
+        * returns "true" or "false" depending on how <slot>
+        * is configured.
+        */
+
+       if (argc == 2)
+               stdout_is_tty = git_config_bool("command line", argv[1]);
+       else if (argc == 1)
+               stdout_is_tty = isatty(1);
+       else
+               usage(git_config_set_usage);
+       get_colorbool_found = -1;
+       get_diff_color_found = -1;
+       get_color_slot = argv[0];
+       git_config(git_get_colorbool_config);
+
+       if (get_colorbool_found < 0) {
+               if (!strcmp(get_color_slot, "color.diff"))
+                       get_colorbool_found = get_diff_color_found;
+               if (get_colorbool_found < 0)
+                       get_colorbool_found = 0;
+       }
+
+       if (argc == 1) {
+               return get_colorbool_found ? 0 : 1;
+       } else {
+               printf("%s\n", get_colorbool_found ? "true" : "false");
+               return 0;
+       }
+}
+
+int cmd_config(int argc, const char **argv, const char *prefix)
+{
+       int nongit = 0;
+       char* value;
+       const char *file = setup_git_directory_gently(&nongit);
+
+       while (1 < argc) {
+               if (!strcmp(argv[1], "--int"))
+                       type = T_INT;
+               else if (!strcmp(argv[1], "--bool"))
+                       type = T_BOOL;
+               else if (!strcmp(argv[1], "--list") || !strcmp(argv[1], "-l")) {
+                       if (argc != 2)
+                               usage(git_config_set_usage);
+                       if (git_config(show_all_config) < 0 && file && errno)
+                               die("unable to read config file %s: %s", file,
+                                   strerror(errno));
+                       return 0;
+               }
+               else if (!strcmp(argv[1], "--global")) {
+                       char *home = getenv("HOME");
+                       if (home) {
+                               char *user_config = xstrdup(mkpath("%s/.gitconfig", home));
+                               setenv(CONFIG_ENVIRONMENT, user_config, 1);
+                               free(user_config);
+                       } else {
+                               die("$HOME not set");
+                       }
+               }
+               else if (!strcmp(argv[1], "--system"))
+                       setenv(CONFIG_ENVIRONMENT, git_etc_gitconfig(), 1);
+               else if (!strcmp(argv[1], "--file") || !strcmp(argv[1], "-f")) {
+                       if (argc < 3)
+                               usage(git_config_set_usage);
+                       if (!is_absolute_path(argv[2]) && file)
+                               file = prefix_filename(file, strlen(file),
+                                                      argv[2]);
+                       else
+                               file = argv[2];
+                       setenv(CONFIG_ENVIRONMENT, file, 1);
+                       argc--;
+                       argv++;
+               }
+               else if (!strcmp(argv[1], "--null") || !strcmp(argv[1], "-z")) {
+                       term = '\0';
+                       delim = '\n';
+                       key_delim = '\n';
+               }
+               else if (!strcmp(argv[1], "--rename-section")) {
+                       int ret;
+                       if (argc != 4)
+                               usage(git_config_set_usage);
+                       ret = git_config_rename_section(argv[2], argv[3]);
+                       if (ret < 0)
+                               return ret;
+                       if (ret == 0) {
+                               fprintf(stderr, "No such section!\n");
+                               return 1;
+                       }
+                       return 0;
+               }
+               else if (!strcmp(argv[1], "--remove-section")) {
+                       int ret;
+                       if (argc != 3)
+                               usage(git_config_set_usage);
+                       ret = git_config_rename_section(argv[2], NULL);
+                       if (ret < 0)
+                               return ret;
+                       if (ret == 0) {
+                               fprintf(stderr, "No such section!\n");
+                               return 1;
+                       }
+                       return 0;
+               } else if (!strcmp(argv[1], "--get-color")) {
+                       return get_color(argc-2, argv+2);
+               } else if (!strcmp(argv[1], "--get-colorbool")) {
+                       return get_colorbool(argc-2, argv+2);
+               } else
+                       break;
+               argc--;
+               argv++;
+       }
+
+       switch (argc) {
+       case 2:
+               return get_value(argv[1], NULL);
+       case 3:
+               if (!strcmp(argv[1], "--unset"))
+                       return git_config_set(argv[2], NULL);
+               else if (!strcmp(argv[1], "--unset-all"))
+                       return git_config_set_multivar(argv[2], NULL, NULL, 1);
+               else if (!strcmp(argv[1], "--get"))
+                       return get_value(argv[2], NULL);
+               else if (!strcmp(argv[1], "--get-all")) {
+                       do_all = 1;
+                       return get_value(argv[2], NULL);
+               } else if (!strcmp(argv[1], "--get-regexp")) {
+                       show_keys = 1;
+                       use_key_regexp = 1;
+                       do_all = 1;
+                       return get_value(argv[2], NULL);
+               } else {
+                       value = normalize_value(argv[1], argv[2]);
+                       return git_config_set(argv[1], value);
+               }
+       case 4:
+               if (!strcmp(argv[1], "--unset"))
+                       return git_config_set_multivar(argv[2], NULL, argv[3], 0);
+               else if (!strcmp(argv[1], "--unset-all"))
+                       return git_config_set_multivar(argv[2], NULL, argv[3], 1);
+               else if (!strcmp(argv[1], "--get"))
+                       return get_value(argv[2], argv[3]);
+               else if (!strcmp(argv[1], "--get-all")) {
+                       do_all = 1;
+                       return get_value(argv[2], argv[3]);
+               } else if (!strcmp(argv[1], "--get-regexp")) {
+                       show_keys = 1;
+                       use_key_regexp = 1;
+                       do_all = 1;
+                       return get_value(argv[2], argv[3]);
+               } else if (!strcmp(argv[1], "--add")) {
+                       value = normalize_value(argv[2], argv[3]);
+                       return git_config_set_multivar(argv[2], value, "^$", 0);
+               } else if (!strcmp(argv[1], "--replace-all")) {
+                       value = normalize_value(argv[2], argv[3]);
+                       return git_config_set_multivar(argv[2], value, NULL, 1);
+               } else {
+                       value = normalize_value(argv[1], argv[2]);
+                       return git_config_set_multivar(argv[1], value, argv[3], 0);
+               }
+       case 5:
+               if (!strcmp(argv[1], "--replace-all")) {
+                       value = normalize_value(argv[2], argv[3]);
+                       return git_config_set_multivar(argv[2], value, argv[4], 1);
+               }
+       case 1:
+       default:
+               usage(git_config_set_usage);
+       }
+       return 0;
+}
diff --git a/builtin-count-objects.c b/builtin-count-objects.c
new file mode 100644 (file)
index 0000000..f00306f
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * Builtin "git count-objects".
+ *
+ * Copyright (c) 2006 Junio C Hamano
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "parse-options.h"
+
+static void count_objects(DIR *d, char *path, int len, int verbose,
+                         unsigned long *loose,
+                         unsigned long *loose_size,
+                         unsigned long *packed_loose,
+                         unsigned long *garbage)
+{
+       struct dirent *ent;
+       while ((ent = readdir(d)) != NULL) {
+               char hex[41];
+               unsigned char sha1[20];
+               const char *cp;
+               int bad = 0;
+
+               if ((ent->d_name[0] == '.') &&
+                   (ent->d_name[1] == 0 ||
+                    ((ent->d_name[1] == '.') && (ent->d_name[2] == 0))))
+                       continue;
+               for (cp = ent->d_name; *cp; cp++) {
+                       int ch = *cp;
+                       if (('0' <= ch && ch <= '9') ||
+                           ('a' <= ch && ch <= 'f'))
+                               continue;
+                       bad = 1;
+                       break;
+               }
+               if (cp - ent->d_name != 38)
+                       bad = 1;
+               else {
+                       struct stat st;
+                       memcpy(path + len + 3, ent->d_name, 38);
+                       path[len + 2] = '/';
+                       path[len + 41] = 0;
+                       if (lstat(path, &st) || !S_ISREG(st.st_mode))
+                               bad = 1;
+                       else
+                               (*loose_size) += xsize_t(st.st_blocks);
+               }
+               if (bad) {
+                       if (verbose) {
+                               error("garbage found: %.*s/%s",
+                                     len + 2, path, ent->d_name);
+                               (*garbage)++;
+                       }
+                       continue;
+               }
+               (*loose)++;
+               if (!verbose)
+                       continue;
+               memcpy(hex, path+len, 2);
+               memcpy(hex+2, ent->d_name, 38);
+               hex[40] = 0;
+               if (get_sha1_hex(hex, sha1))
+                       die("internal error");
+               if (has_sha1_pack(sha1, NULL))
+                       (*packed_loose)++;
+       }
+}
+
+static char const * const count_objects_usage[] = {
+       "git-count-objects [-v]",
+       NULL
+};
+
+int cmd_count_objects(int argc, const char **argv, const char *prefix)
+{
+       int i, verbose = 0;
+       const char *objdir = get_object_directory();
+       int len = strlen(objdir);
+       char *path = xmalloc(len + 50);
+       unsigned long loose = 0, packed = 0, packed_loose = 0, garbage = 0;
+       unsigned long loose_size = 0;
+       struct option opts[] = {
+               OPT__VERBOSE(&verbose),
+               OPT_END(),
+       };
+
+       argc = parse_options(argc, argv, opts, count_objects_usage, 0);
+       /* we do not take arguments other than flags for now */
+       if (argc)
+               usage_with_options(count_objects_usage, opts);
+       memcpy(path, objdir, len);
+       if (len && objdir[len-1] != '/')
+               path[len++] = '/';
+       for (i = 0; i < 256; i++) {
+               DIR *d;
+               sprintf(path + len, "%02x", i);
+               d = opendir(path);
+               if (!d)
+                       continue;
+               count_objects(d, path, len, verbose,
+                             &loose, &loose_size, &packed_loose, &garbage);
+               closedir(d);
+       }
+       if (verbose) {
+               struct packed_git *p;
+               unsigned long num_pack = 0;
+               if (!packed_git)
+                       prepare_packed_git();
+               for (p = packed_git; p; p = p->next) {
+                       if (!p->pack_local)
+                               continue;
+                       if (open_pack_index(p))
+                               continue;
+                       packed += p->num_objects;
+                       num_pack++;
+               }
+               printf("count: %lu\n", loose);
+               printf("size: %lu\n", loose_size / 2);
+               printf("in-pack: %lu\n", packed);
+               printf("packs: %lu\n", num_pack);
+               printf("prune-packable: %lu\n", packed_loose);
+               printf("garbage: %lu\n", garbage);
+       }
+       else
+               printf("%lu objects, %lu kilobytes\n",
+                      loose, loose_size / 2);
+       return 0;
+}
diff --git a/builtin-describe.c b/builtin-describe.c
new file mode 100644 (file)
index 0000000..6eeb9b5
--- /dev/null
@@ -0,0 +1,286 @@
+#include "cache.h"
+#include "commit.h"
+#include "tag.h"
+#include "refs.h"
+#include "builtin.h"
+#include "exec_cmd.h"
+#include "parse-options.h"
+
+#define SEEN           (1u<<0)
+#define MAX_TAGS       (FLAG_BITS - 1)
+
+static const char * const describe_usage[] = {
+       "git-describe [options] <committish>*",
+       NULL
+};
+
+static int debug;      /* Display lots of verbose info */
+static int all;        /* Default to annotated tags only */
+static int tags;       /* But allow any tags if --tags is specified */
+static int abbrev = DEFAULT_ABBREV;
+static int max_candidates = 10;
+
+struct commit_name {
+       int prio; /* annotated tag = 2, tag = 1, head = 0 */
+       char path[FLEX_ARRAY]; /* more */
+};
+static const char *prio_names[] = {
+       "head", "lightweight", "annotated",
+};
+
+static void add_to_known_names(const char *path,
+                              struct commit *commit,
+                              int prio)
+{
+       struct commit_name *e = commit->util;
+       if (!e || e->prio < prio) {
+               size_t len = strlen(path)+1;
+               free(e);
+               e = xmalloc(sizeof(struct commit_name) + len);
+               e->prio = prio;
+               memcpy(e->path, path, len);
+               commit->util = e;
+       }
+}
+
+static int get_name(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct commit *commit = lookup_commit_reference_gently(sha1, 1);
+       struct object *object;
+       int prio;
+
+       if (!commit)
+               return 0;
+       object = parse_object(sha1);
+       /* If --all, then any refs are used.
+        * If --tags, then any tags are used.
+        * Otherwise only annotated tags are used.
+        */
+       if (!prefixcmp(path, "refs/tags/")) {
+               if (object->type == OBJ_TAG)
+                       prio = 2;
+               else
+                       prio = 1;
+       }
+       else
+               prio = 0;
+
+       if (!all) {
+               if (!prio)
+                       return 0;
+               if (!tags && prio < 2)
+                       return 0;
+       }
+       add_to_known_names(all ? path + 5 : path + 10, commit, prio);
+       return 0;
+}
+
+struct possible_tag {
+       struct commit_name *name;
+       int depth;
+       int found_order;
+       unsigned flag_within;
+};
+
+static int compare_pt(const void *a_, const void *b_)
+{
+       struct possible_tag *a = (struct possible_tag *)a_;
+       struct possible_tag *b = (struct possible_tag *)b_;
+       if (a->name->prio != b->name->prio)
+               return b->name->prio - a->name->prio;
+       if (a->depth != b->depth)
+               return a->depth - b->depth;
+       if (a->found_order != b->found_order)
+               return a->found_order - b->found_order;
+       return 0;
+}
+
+static unsigned long finish_depth_computation(
+       struct commit_list **list,
+       struct possible_tag *best)
+{
+       unsigned long seen_commits = 0;
+       while (*list) {
+               struct commit *c = pop_commit(list);
+               struct commit_list *parents = c->parents;
+               seen_commits++;
+               if (c->object.flags & best->flag_within) {
+                       struct commit_list *a = *list;
+                       while (a) {
+                               struct commit *i = a->item;
+                               if (!(i->object.flags & best->flag_within))
+                                       break;
+                               a = a->next;
+                       }
+                       if (!a)
+                               break;
+               } else
+                       best->depth++;
+               while (parents) {
+                       struct commit *p = parents->item;
+                       parse_commit(p);
+                       if (!(p->object.flags & SEEN))
+                               insert_by_date(p, list);
+                       p->object.flags |= c->object.flags;
+                       parents = parents->next;
+               }
+       }
+       return seen_commits;
+}
+
+static void describe(const char *arg, int last_one)
+{
+       unsigned char sha1[20];
+       struct commit *cmit, *gave_up_on = NULL;
+       struct commit_list *list;
+       static int initialized = 0;
+       struct commit_name *n;
+       struct possible_tag all_matches[MAX_TAGS];
+       unsigned int match_cnt = 0, annotated_cnt = 0, cur_match;
+       unsigned long seen_commits = 0;
+
+       if (get_sha1(arg, sha1))
+               die("Not a valid object name %s", arg);
+       cmit = lookup_commit_reference(sha1);
+       if (!cmit)
+               die("%s is not a valid '%s' object", arg, commit_type);
+
+       if (!initialized) {
+               initialized = 1;
+               for_each_ref(get_name, NULL);
+       }
+
+       n = cmit->util;
+       if (n) {
+               printf("%s\n", n->path);
+               return;
+       }
+
+       if (debug)
+               fprintf(stderr, "searching to describe %s\n", arg);
+
+       list = NULL;
+       cmit->object.flags = SEEN;
+       commit_list_insert(cmit, &list);
+       while (list) {
+               struct commit *c = pop_commit(&list);
+               struct commit_list *parents = c->parents;
+               seen_commits++;
+               n = c->util;
+               if (n) {
+                       if (match_cnt < max_candidates) {
+                               struct possible_tag *t = &all_matches[match_cnt++];
+                               t->name = n;
+                               t->depth = seen_commits - 1;
+                               t->flag_within = 1u << match_cnt;
+                               t->found_order = match_cnt;
+                               c->object.flags |= t->flag_within;
+                               if (n->prio == 2)
+                                       annotated_cnt++;
+                       }
+                       else {
+                               gave_up_on = c;
+                               break;
+                       }
+               }
+               for (cur_match = 0; cur_match < match_cnt; cur_match++) {
+                       struct possible_tag *t = &all_matches[cur_match];
+                       if (!(c->object.flags & t->flag_within))
+                               t->depth++;
+               }
+               if (annotated_cnt && !list) {
+                       if (debug)
+                               fprintf(stderr, "finished search at %s\n",
+                                       sha1_to_hex(c->object.sha1));
+                       break;
+               }
+               while (parents) {
+                       struct commit *p = parents->item;
+                       parse_commit(p);
+                       if (!(p->object.flags & SEEN))
+                               insert_by_date(p, &list);
+                       p->object.flags |= c->object.flags;
+                       parents = parents->next;
+               }
+       }
+
+       if (!match_cnt)
+               die("cannot describe '%s'", sha1_to_hex(cmit->object.sha1));
+
+       qsort(all_matches, match_cnt, sizeof(all_matches[0]), compare_pt);
+
+       if (gave_up_on) {
+               insert_by_date(gave_up_on, &list);
+               seen_commits--;
+       }
+       seen_commits += finish_depth_computation(&list, &all_matches[0]);
+       free_commit_list(list);
+
+       if (debug) {
+               for (cur_match = 0; cur_match < match_cnt; cur_match++) {
+                       struct possible_tag *t = &all_matches[cur_match];
+                       fprintf(stderr, " %-11s %8d %s\n",
+                               prio_names[t->name->prio],
+                               t->depth, t->name->path);
+               }
+               fprintf(stderr, "traversed %lu commits\n", seen_commits);
+               if (gave_up_on) {
+                       fprintf(stderr,
+                               "more than %i tags found; listed %i most recent\n"
+                               "gave up search at %s\n",
+                               max_candidates, max_candidates,
+                               sha1_to_hex(gave_up_on->object.sha1));
+               }
+       }
+       if (abbrev == 0)
+               printf("%s\n", all_matches[0].name->path );
+       else
+               printf("%s-%d-g%s\n", all_matches[0].name->path,
+                      all_matches[0].depth,
+                      find_unique_abbrev(cmit->object.sha1, abbrev));
+
+       if (!last_one)
+               clear_commit_marks(cmit, -1);
+}
+
+int cmd_describe(int argc, const char **argv, const char *prefix)
+{
+       int contains = 0;
+       struct option options[] = {
+               OPT_BOOLEAN(0, "contains",   &contains, "find the tag that comes after the commit"),
+               OPT_BOOLEAN(0, "debug",      &debug, "debug search strategy on stderr"),
+               OPT_BOOLEAN(0, "all",        &all, "use any ref in .git/refs"),
+               OPT_BOOLEAN(0, "tags",       &tags, "use any tag in .git/refs/tags"),
+               OPT__ABBREV(&abbrev),
+               OPT_INTEGER(0, "candidates", &max_candidates,
+                                       "consider <n> most recent tags (default: 10)"),
+               OPT_END(),
+       };
+
+       argc = parse_options(argc, argv, options, describe_usage, 0);
+       if (max_candidates < 1)
+               max_candidates = 1;
+       else if (max_candidates > MAX_TAGS)
+               max_candidates = MAX_TAGS;
+
+       save_commit_buffer = 0;
+
+       if (contains) {
+               const char **args = xmalloc((4 + argc) * sizeof(char*));
+               args[0] = "name-rev";
+               args[1] = "--name-only";
+               args[2] = "--tags";
+               memcpy(args + 3, argv, argc * sizeof(char*));
+               args[3 + argc] = NULL;
+               return cmd_name_rev(3 + argc, args, prefix);
+       }
+
+       if (argc == 0) {
+               describe("HEAD", 1);
+       } else {
+               while (argc-- > 0) {
+                       describe(*argv++, argc == 0);
+               }
+       }
+       return 0;
+}
diff --git a/builtin-diff-files.c b/builtin-diff-files.c
new file mode 100644 (file)
index 0000000..046b7e3
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "cache.h"
+#include "diff.h"
+#include "commit.h"
+#include "revision.h"
+#include "builtin.h"
+
+static const char diff_files_usage[] =
+"git-diff-files [-q] [-0/-1/2/3 |-c|--cc|--no-index] [<common diff options>] [<path>...]"
+COMMON_DIFF_OPTIONS_HELP;
+
+int cmd_diff_files(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info rev;
+       int nongit = 0;
+       int result;
+
+       prefix = setup_git_directory_gently(&nongit);
+       init_revisions(&rev, prefix);
+       git_config(git_default_config); /* no "diff" UI options */
+       rev.abbrev = 0;
+
+       if (!setup_diff_no_index(&rev, argc, argv, nongit, prefix))
+               argc = 0;
+       else
+               argc = setup_revisions(argc, argv, &rev, NULL);
+       if (!rev.diffopt.output_format)
+               rev.diffopt.output_format = DIFF_FORMAT_RAW;
+       result = run_diff_files_cmd(&rev, argc, argv);
+       if (DIFF_OPT_TST(&rev.diffopt, EXIT_WITH_STATUS))
+               return DIFF_OPT_TST(&rev.diffopt, HAS_CHANGES) != 0;
+       return result;
+}
diff --git a/builtin-diff-index.c b/builtin-diff-index.c
new file mode 100644 (file)
index 0000000..556c506
--- /dev/null
@@ -0,0 +1,50 @@
+#include "cache.h"
+#include "diff.h"
+#include "commit.h"
+#include "revision.h"
+#include "builtin.h"
+
+static const char diff_cache_usage[] =
+"git-diff-index [-m] [--cached] "
+"[<common diff options>] <tree-ish> [<path>...]"
+COMMON_DIFF_OPTIONS_HELP;
+
+int cmd_diff_index(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info rev;
+       int cached = 0;
+       int i;
+       int result;
+
+       init_revisions(&rev, prefix);
+       git_config(git_default_config); /* no "diff" UI options */
+       rev.abbrev = 0;
+
+       argc = setup_revisions(argc, argv, &rev, NULL);
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (!strcmp(arg, "--cached"))
+                       cached = 1;
+               else
+                       usage(diff_cache_usage);
+       }
+       if (!rev.diffopt.output_format)
+               rev.diffopt.output_format = DIFF_FORMAT_RAW;
+
+       /*
+        * Make sure there is one revision (i.e. pending object),
+        * and there is no revision filtering parameters.
+        */
+       if (rev.pending.nr != 1 ||
+           rev.max_count != -1 || rev.min_age != -1 || rev.max_age != -1)
+               usage(diff_cache_usage);
+       if (read_cache() < 0) {
+               perror("read_cache");
+               return -1;
+       }
+       result = run_diff_index(&rev, cached);
+       if (DIFF_OPT_TST(&rev.diffopt, EXIT_WITH_STATUS))
+               return DIFF_OPT_TST(&rev.diffopt, HAS_CHANGES) != 0;
+       return result;
+}
diff --git a/builtin-diff-tree.c b/builtin-diff-tree.c
new file mode 100644 (file)
index 0000000..2e13716
--- /dev/null
@@ -0,0 +1,139 @@
+#include "cache.h"
+#include "diff.h"
+#include "commit.h"
+#include "log-tree.h"
+#include "builtin.h"
+
+static struct rev_info log_tree_opt;
+
+static int diff_tree_commit_sha1(const unsigned char *sha1)
+{
+       struct commit *commit = lookup_commit_reference(sha1);
+       if (!commit)
+               return -1;
+       return log_tree_commit(&log_tree_opt, commit);
+}
+
+static int diff_tree_stdin(char *line)
+{
+       int len = strlen(line);
+       unsigned char sha1[20];
+       struct commit *commit;
+
+       if (!len || line[len-1] != '\n')
+               return -1;
+       line[len-1] = 0;
+       if (get_sha1_hex(line, sha1))
+               return -1;
+       commit = lookup_commit(sha1);
+       if (!commit || parse_commit(commit))
+               return -1;
+       if (isspace(line[40]) && !get_sha1_hex(line+41, sha1)) {
+               /* Graft the fake parents locally to the commit */
+               int pos = 41;
+               struct commit_list **pptr, *parents;
+
+               /* Free the real parent list */
+               for (parents = commit->parents; parents; ) {
+                       struct commit_list *tmp = parents->next;
+                       free(parents);
+                       parents = tmp;
+               }
+               commit->parents = NULL;
+               pptr = &(commit->parents);
+               while (line[pos] && !get_sha1_hex(line + pos, sha1)) {
+                       struct commit *parent = lookup_commit(sha1);
+                       if (parent) {
+                               pptr = &commit_list_insert(parent, pptr)->next;
+                       }
+                       pos += 41;
+               }
+       }
+       return log_tree_commit(&log_tree_opt, commit);
+}
+
+static const char diff_tree_usage[] =
+"git-diff-tree [--stdin] [-m] [-c] [--cc] [-s] [-v] [--pretty] [-t] [-r] [--root] "
+"[<common diff options>] <tree-ish> [<tree-ish>] [<path>...]\n"
+"  -r            diff recursively\n"
+"  --root        include the initial commit as diff against /dev/null\n"
+COMMON_DIFF_OPTIONS_HELP;
+
+int cmd_diff_tree(int argc, const char **argv, const char *prefix)
+{
+       int nr_sha1;
+       char line[1000];
+       struct object *tree1, *tree2;
+       static struct rev_info *opt = &log_tree_opt;
+       int read_stdin = 0;
+
+       init_revisions(opt, prefix);
+       git_config(git_default_config); /* no "diff" UI options */
+       nr_sha1 = 0;
+       opt->abbrev = 0;
+       opt->diff = 1;
+       argc = setup_revisions(argc, argv, opt, NULL);
+
+       while (--argc > 0) {
+               const char *arg = *++argv;
+
+               if (!strcmp(arg, "--stdin")) {
+                       read_stdin = 1;
+                       continue;
+               }
+               usage(diff_tree_usage);
+       }
+
+       if (!opt->diffopt.output_format)
+               opt->diffopt.output_format = DIFF_FORMAT_RAW;
+
+       /*
+        * NOTE! We expect "a ^b" to be equal to "a..b", so we
+        * reverse the order of the objects if the second one
+        * is marked UNINTERESTING.
+        */
+       nr_sha1 = opt->pending.nr;
+       switch (nr_sha1) {
+       case 0:
+               if (!read_stdin)
+                       usage(diff_tree_usage);
+               break;
+       case 1:
+               tree1 = opt->pending.objects[0].item;
+               diff_tree_commit_sha1(tree1->sha1);
+               break;
+       case 2:
+               tree1 = opt->pending.objects[0].item;
+               tree2 = opt->pending.objects[1].item;
+               if (tree2->flags & UNINTERESTING) {
+                       struct object *tmp = tree2;
+                       tree2 = tree1;
+                       tree1 = tmp;
+               }
+               diff_tree_sha1(tree1->sha1,
+                              tree2->sha1,
+                              "", &opt->diffopt);
+               log_tree_diff_flush(opt);
+               break;
+       }
+
+       if (!read_stdin)
+               return DIFF_OPT_TST(&opt->diffopt, EXIT_WITH_STATUS)
+                       && DIFF_OPT_TST(&opt->diffopt, HAS_CHANGES);
+
+       if (opt->diffopt.detect_rename)
+               opt->diffopt.setup |= (DIFF_SETUP_USE_SIZE_CACHE |
+                                      DIFF_SETUP_USE_CACHE);
+       while (fgets(line, sizeof(line), stdin)) {
+               unsigned char sha1[20];
+
+               if (get_sha1_hex(line, sha1)) {
+                       fputs(line, stdout);
+                       fflush(stdout);
+               }
+               else
+                       diff_tree_stdin(line);
+       }
+       return DIFF_OPT_TST(&opt->diffopt, EXIT_WITH_STATUS)
+               && DIFF_OPT_TST(&opt->diffopt, HAS_CHANGES);
+}
diff --git a/builtin-diff.c b/builtin-diff.c
new file mode 100644 (file)
index 0000000..1b61599
--- /dev/null
@@ -0,0 +1,372 @@
+/*
+ * Builtin "git diff"
+ *
+ * Copyright (c) 2006 Junio C Hamano
+ */
+#include "cache.h"
+#include "commit.h"
+#include "blob.h"
+#include "tag.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "revision.h"
+#include "log-tree.h"
+#include "builtin.h"
+
+struct blobinfo {
+       unsigned char sha1[20];
+       const char *name;
+       unsigned mode;
+};
+
+static const char builtin_diff_usage[] =
+"git-diff <options> <rev>{0,2} -- <path>*";
+
+static void stuff_change(struct diff_options *opt,
+                        unsigned old_mode, unsigned new_mode,
+                        const unsigned char *old_sha1,
+                        const unsigned char *new_sha1,
+                        const char *old_name,
+                        const char *new_name)
+{
+       struct diff_filespec *one, *two;
+
+       if (!is_null_sha1(old_sha1) && !is_null_sha1(new_sha1) &&
+           !hashcmp(old_sha1, new_sha1) && (old_mode == new_mode))
+               return;
+
+       if (DIFF_OPT_TST(opt, REVERSE_DIFF)) {
+               unsigned tmp;
+               const unsigned char *tmp_u;
+               const char *tmp_c;
+               tmp = old_mode; old_mode = new_mode; new_mode = tmp;
+               tmp_u = old_sha1; old_sha1 = new_sha1; new_sha1 = tmp_u;
+               tmp_c = old_name; old_name = new_name; new_name = tmp_c;
+       }
+       one = alloc_filespec(old_name);
+       two = alloc_filespec(new_name);
+       fill_filespec(one, old_sha1, old_mode);
+       fill_filespec(two, new_sha1, new_mode);
+
+       /* NEEDSWORK: shouldn't this part of diffopt??? */
+       diff_queue(&diff_queued_diff, one, two);
+}
+
+static int builtin_diff_b_f(struct rev_info *revs,
+                           int argc, const char **argv,
+                           struct blobinfo *blob,
+                           const char *path)
+{
+       /* Blob vs file in the working tree*/
+       struct stat st;
+
+       if (argc > 1)
+               usage(builtin_diff_usage);
+
+       if (lstat(path, &st))
+               die("'%s': %s", path, strerror(errno));
+       if (!(S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)))
+               die("'%s': not a regular file or symlink", path);
+
+       if (blob[0].mode == S_IFINVALID)
+               blob[0].mode = canon_mode(st.st_mode);
+
+       stuff_change(&revs->diffopt,
+                    blob[0].mode, canon_mode(st.st_mode),
+                    blob[0].sha1, null_sha1,
+                    path, path);
+       diffcore_std(&revs->diffopt);
+       diff_flush(&revs->diffopt);
+       return 0;
+}
+
+static int builtin_diff_blobs(struct rev_info *revs,
+                             int argc, const char **argv,
+                             struct blobinfo *blob)
+{
+       unsigned mode = canon_mode(S_IFREG | 0644);
+
+       if (argc > 1)
+               usage(builtin_diff_usage);
+
+       if (blob[0].mode == S_IFINVALID)
+               blob[0].mode = mode;
+
+       if (blob[1].mode == S_IFINVALID)
+               blob[1].mode = mode;
+
+       stuff_change(&revs->diffopt,
+                    blob[0].mode, blob[1].mode,
+                    blob[0].sha1, blob[1].sha1,
+                    blob[0].name, blob[1].name);
+       diffcore_std(&revs->diffopt);
+       diff_flush(&revs->diffopt);
+       return 0;
+}
+
+static int builtin_diff_index(struct rev_info *revs,
+                             int argc, const char **argv)
+{
+       int cached = 0;
+       while (1 < argc) {
+               const char *arg = argv[1];
+               if (!strcmp(arg, "--cached"))
+                       cached = 1;
+               else
+                       usage(builtin_diff_usage);
+               argv++; argc--;
+       }
+       /*
+        * Make sure there is one revision (i.e. pending object),
+        * and there is no revision filtering parameters.
+        */
+       if (revs->pending.nr != 1 ||
+           revs->max_count != -1 || revs->min_age != -1 ||
+           revs->max_age != -1)
+               usage(builtin_diff_usage);
+       if (read_cache() < 0) {
+               perror("read_cache");
+               return -1;
+       }
+       return run_diff_index(revs, cached);
+}
+
+static int builtin_diff_tree(struct rev_info *revs,
+                            int argc, const char **argv,
+                            struct object_array_entry *ent)
+{
+       const unsigned char *(sha1[2]);
+       int swap = 0;
+
+       if (argc > 1)
+               usage(builtin_diff_usage);
+
+       /* We saw two trees, ent[0] and ent[1].
+        * if ent[1] is uninteresting, they are swapped
+        */
+       if (ent[1].item->flags & UNINTERESTING)
+               swap = 1;
+       sha1[swap] = ent[0].item->sha1;
+       sha1[1-swap] = ent[1].item->sha1;
+       diff_tree_sha1(sha1[0], sha1[1], "", &revs->diffopt);
+       log_tree_diff_flush(revs);
+       return 0;
+}
+
+static int builtin_diff_combined(struct rev_info *revs,
+                                int argc, const char **argv,
+                                struct object_array_entry *ent,
+                                int ents)
+{
+       const unsigned char (*parent)[20];
+       int i;
+
+       if (argc > 1)
+               usage(builtin_diff_usage);
+
+       if (!revs->dense_combined_merges && !revs->combine_merges)
+               revs->dense_combined_merges = revs->combine_merges = 1;
+       parent = xmalloc(ents * sizeof(*parent));
+       /* Again, the revs are all reverse */
+       for (i = 0; i < ents; i++)
+               hashcpy((unsigned char *)(parent + i),
+                       ent[ents - 1 - i].item->sha1);
+       diff_tree_combined(parent[0], parent + 1, ents - 1,
+                          revs->dense_combined_merges, revs);
+       return 0;
+}
+
+void add_head(struct rev_info *revs)
+{
+       unsigned char sha1[20];
+       struct object *obj;
+       if (get_sha1("HEAD", sha1))
+               return;
+       obj = parse_object(sha1);
+       if (!obj)
+               return;
+       add_pending_object(revs, obj, "HEAD");
+}
+
+static void refresh_index_quietly(void)
+{
+       struct lock_file *lock_file;
+       int fd;
+
+       lock_file = xcalloc(1, sizeof(struct lock_file));
+       fd = hold_locked_index(lock_file, 0);
+       if (fd < 0)
+               return;
+       discard_cache();
+       read_cache();
+       refresh_cache(REFRESH_QUIET|REFRESH_UNMERGED);
+
+       if (active_cache_changed &&
+           !write_cache(fd, active_cache, active_nr) && !close(fd))
+               commit_locked_index(lock_file);
+
+       rollback_lock_file(lock_file);
+}
+
+int cmd_diff(int argc, const char **argv, const char *prefix)
+{
+       int i;
+       struct rev_info rev;
+       struct object_array_entry ent[100];
+       int ents = 0, blobs = 0, paths = 0;
+       const char *path = NULL;
+       struct blobinfo blob[2];
+       int nongit = 0;
+       int result = 0;
+
+       /*
+        * We could get N tree-ish in the rev.pending_objects list.
+        * Also there could be M blobs there, and P pathspecs.
+        *
+        * N=0, M=0:
+        *      cache vs files (diff-files)
+        * N=0, M=2:
+        *      compare two random blobs.  P must be zero.
+        * N=0, M=1, P=1:
+        *      compare a blob with a working tree file.
+        *
+        * N=1, M=0:
+        *      tree vs cache (diff-index --cached)
+        *
+        * N=2, M=0:
+        *      tree vs tree (diff-tree)
+        *
+        * Other cases are errors.
+        */
+
+       prefix = setup_git_directory_gently(&nongit);
+       git_config(git_diff_ui_config);
+       init_revisions(&rev, prefix);
+       rev.diffopt.skip_stat_unmatch = !!diff_auto_refresh_index;
+
+       if (!setup_diff_no_index(&rev, argc, argv, nongit, prefix))
+               argc = 0;
+       else
+               argc = setup_revisions(argc, argv, &rev, NULL);
+       if (!rev.diffopt.output_format) {
+               rev.diffopt.output_format = DIFF_FORMAT_PATCH;
+               if (diff_setup_done(&rev.diffopt) < 0)
+                       die("diff_setup_done failed");
+       }
+       DIFF_OPT_SET(&rev.diffopt, ALLOW_EXTERNAL);
+       DIFF_OPT_SET(&rev.diffopt, RECURSIVE);
+
+       /* If the user asked for our exit code then don't start a
+        * pager or we would end up reporting its exit code instead.
+        */
+       if (!DIFF_OPT_TST(&rev.diffopt, EXIT_WITH_STATUS))
+               setup_pager();
+
+       /* Do we have --cached and not have a pending object, then
+        * default to HEAD by hand.  Eek.
+        */
+       if (!rev.pending.nr) {
+               int i;
+               for (i = 1; i < argc; i++) {
+                       const char *arg = argv[i];
+                       if (!strcmp(arg, "--"))
+                               break;
+                       else if (!strcmp(arg, "--cached")) {
+                               add_head(&rev);
+                               if (!rev.pending.nr)
+                                       die("No HEAD commit to compare with (yet)");
+                               break;
+                       }
+               }
+       }
+
+       for (i = 0; i < rev.pending.nr; i++) {
+               struct object_array_entry *list = rev.pending.objects+i;
+               struct object *obj = list->item;
+               const char *name = list->name;
+               int flags = (obj->flags & UNINTERESTING);
+               if (!obj->parsed)
+                       obj = parse_object(obj->sha1);
+               obj = deref_tag(obj, NULL, 0);
+               if (!obj)
+                       die("invalid object '%s' given.", name);
+               if (obj->type == OBJ_COMMIT)
+                       obj = &((struct commit *)obj)->tree->object;
+               if (obj->type == OBJ_TREE) {
+                       if (ARRAY_SIZE(ent) <= ents)
+                               die("more than %d trees given: '%s'",
+                                   (int) ARRAY_SIZE(ent), name);
+                       obj->flags |= flags;
+                       ent[ents].item = obj;
+                       ent[ents].name = name;
+                       ents++;
+                       continue;
+               }
+               if (obj->type == OBJ_BLOB) {
+                       if (2 <= blobs)
+                               die("more than two blobs given: '%s'", name);
+                       hashcpy(blob[blobs].sha1, obj->sha1);
+                       blob[blobs].name = name;
+                       blob[blobs].mode = list->mode;
+                       blobs++;
+                       continue;
+
+               }
+               die("unhandled object '%s' given.", name);
+       }
+       if (rev.prune_data) {
+               const char **pathspec = rev.prune_data;
+               while (*pathspec) {
+                       if (!path)
+                               path = *pathspec;
+                       paths++;
+                       pathspec++;
+               }
+       }
+
+       /*
+        * Now, do the arguments look reasonable?
+        */
+       if (!ents) {
+               switch (blobs) {
+               case 0:
+                       result = run_diff_files_cmd(&rev, argc, argv);
+                       break;
+               case 1:
+                       if (paths != 1)
+                               usage(builtin_diff_usage);
+                       result = builtin_diff_b_f(&rev, argc, argv, blob, path);
+                       break;
+               case 2:
+                       if (paths)
+                               usage(builtin_diff_usage);
+                       result = builtin_diff_blobs(&rev, argc, argv, blob);
+                       break;
+               default:
+                       usage(builtin_diff_usage);
+               }
+       }
+       else if (blobs)
+               usage(builtin_diff_usage);
+       else if (ents == 1)
+               result = builtin_diff_index(&rev, argc, argv);
+       else if (ents == 2)
+               result = builtin_diff_tree(&rev, argc, argv, ent);
+       else if ((ents == 3) && (ent[0].item->flags & UNINTERESTING)) {
+               /* diff A...B where there is one sane merge base between
+                * A and B.  We have ent[0] == merge-base, ent[1] == A,
+                * and ent[2] == B.  Show diff between the base and B.
+                */
+               ent[1] = ent[2];
+               result = builtin_diff_tree(&rev, argc, argv, ent);
+       }
+       else
+               result = builtin_diff_combined(&rev, argc, argv,
+                                            ent, ents);
+       if (DIFF_OPT_TST(&rev.diffopt, EXIT_WITH_STATUS))
+               result = DIFF_OPT_TST(&rev.diffopt, HAS_CHANGES) != 0;
+
+       if (1 < rev.diffopt.skip_stat_unmatch)
+               refresh_index_quietly();
+       return result;
+}
diff --git a/builtin-fast-export.c b/builtin-fast-export.c
new file mode 100755 (executable)
index 0000000..2136aad
--- /dev/null
@@ -0,0 +1,406 @@
+/*
+ * "git fast-export" builtin command
+ *
+ * Copyright (C) 2007 Johannes E. Schindelin
+ */
+#include "builtin.h"
+#include "cache.h"
+#include "commit.h"
+#include "object.h"
+#include "tag.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "log-tree.h"
+#include "revision.h"
+#include "decorate.h"
+#include "path-list.h"
+#include "utf8.h"
+#include "parse-options.h"
+
+static const char *fast_export_usage[] = {
+       "git-fast-export [rev-list-opts]",
+       NULL
+};
+
+static int progress;
+static enum { VERBATIM, WARN, STRIP, ABORT } signed_tag_mode = ABORT;
+
+static int parse_opt_signed_tag_mode(const struct option *opt,
+                                    const char *arg, int unset)
+{
+       if (unset || !strcmp(arg, "abort"))
+               signed_tag_mode = ABORT;
+       else if (!strcmp(arg, "verbatim") || !strcmp(arg, "ignore"))
+               signed_tag_mode = VERBATIM;
+       else if (!strcmp(arg, "warn"))
+               signed_tag_mode = WARN;
+       else if (!strcmp(arg, "strip"))
+               signed_tag_mode = STRIP;
+       else
+               return error("Unknown signed-tag mode: %s", arg);
+       return 0;
+}
+
+static struct decoration idnums;
+static uint32_t last_idnum;
+
+static int has_unshown_parent(struct commit *commit)
+{
+       struct commit_list *parent;
+
+       for (parent = commit->parents; parent; parent = parent->next)
+               if (!(parent->item->object.flags & SHOWN) &&
+                   !(parent->item->object.flags & UNINTERESTING))
+                       return 1;
+       return 0;
+}
+
+/* Since intptr_t is C99, we do not use it here */
+static void mark_object(struct object *object)
+{
+       last_idnum++;
+       add_decoration(&idnums, object, ((uint32_t *)NULL) + last_idnum);
+}
+
+static int get_object_mark(struct object *object)
+{
+       void *decoration = lookup_decoration(&idnums, object);
+       if (!decoration)
+               return 0;
+       return (uint32_t *)decoration - (uint32_t *)NULL;
+}
+
+static void show_progress(void)
+{
+       static int counter = 0;
+       if (!progress)
+               return;
+       if ((++counter % progress) == 0)
+               printf("progress %d objects\n", counter);
+}
+
+static void handle_object(const unsigned char *sha1)
+{
+       unsigned long size;
+       enum object_type type;
+       char *buf;
+       struct object *object;
+
+       if (is_null_sha1(sha1))
+               return;
+
+       object = parse_object(sha1);
+       if (!object)
+               die ("Could not read blob %s", sha1_to_hex(sha1));
+
+       if (object->flags & SHOWN)
+               return;
+
+       buf = read_sha1_file(sha1, &type, &size);
+       if (!buf)
+               die ("Could not read blob %s", sha1_to_hex(sha1));
+
+       mark_object(object);
+
+       printf("blob\nmark :%d\ndata %lu\n", last_idnum, size);
+       if (fwrite(buf, size, 1, stdout) != 1)
+               die ("Could not write blob %s", sha1_to_hex(sha1));
+       printf("\n");
+
+       show_progress();
+
+       object->flags |= SHOWN;
+       free(buf);
+}
+
+static void show_filemodify(struct diff_queue_struct *q,
+                           struct diff_options *options, void *data)
+{
+       int i;
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filespec *spec = q->queue[i]->two;
+               if (is_null_sha1(spec->sha1))
+                       printf("D %s\n", spec->path);
+               else {
+                       struct object *object = lookup_object(spec->sha1);
+                       printf("M 0%06o :%d %s\n", spec->mode,
+                              get_object_mark(object), spec->path);
+               }
+       }
+}
+
+static const char *find_encoding(const char *begin, const char *end)
+{
+       const char *needle = "\nencoding ";
+       char *bol, *eol;
+
+       bol = memmem(begin, end ? end - begin : strlen(begin),
+                    needle, strlen(needle));
+       if (!bol)
+               return git_commit_encoding;
+       bol += strlen(needle);
+       eol = strchrnul(bol, '\n');
+       *eol = '\0';
+       return bol;
+}
+
+static void handle_commit(struct commit *commit, struct rev_info *rev)
+{
+       int saved_output_format = rev->diffopt.output_format;
+       const char *author, *author_end, *committer, *committer_end;
+       const char *encoding, *message;
+       char *reencoded = NULL;
+       struct commit_list *p;
+       int i;
+
+       rev->diffopt.output_format = DIFF_FORMAT_CALLBACK;
+
+       parse_commit(commit);
+       author = strstr(commit->buffer, "\nauthor ");
+       if (!author)
+               die ("Could not find author in commit %s",
+                    sha1_to_hex(commit->object.sha1));
+       author++;
+       author_end = strchrnul(author, '\n');
+       committer = strstr(author_end, "\ncommitter ");
+       if (!committer)
+               die ("Could not find committer in commit %s",
+                    sha1_to_hex(commit->object.sha1));
+       committer++;
+       committer_end = strchrnul(committer, '\n');
+       message = strstr(committer_end, "\n\n");
+       encoding = find_encoding(committer_end, message);
+       if (message)
+               message += 2;
+
+       if (commit->parents) {
+               parse_commit(commit->parents->item);
+               diff_tree_sha1(commit->parents->item->tree->object.sha1,
+                              commit->tree->object.sha1, "", &rev->diffopt);
+       }
+       else
+               diff_root_tree_sha1(commit->tree->object.sha1,
+                                   "", &rev->diffopt);
+
+       for (i = 0; i < diff_queued_diff.nr; i++)
+               handle_object(diff_queued_diff.queue[i]->two->sha1);
+
+       mark_object(&commit->object);
+       if (!is_encoding_utf8(encoding))
+               reencoded = reencode_string(message, "UTF-8", encoding);
+       printf("commit %s\nmark :%d\n%.*s\n%.*s\ndata %u\n%s",
+              (const char *)commit->util, last_idnum,
+              (int)(author_end - author), author,
+              (int)(committer_end - committer), committer,
+              (unsigned)(reencoded
+                         ? strlen(reencoded) : message
+                         ? strlen(message) : 0),
+              reencoded ? reencoded : message ? message : "");
+       if (reencoded)
+               free(reencoded);
+
+       for (i = 0, p = commit->parents; p; p = p->next) {
+               int mark = get_object_mark(&p->item->object);
+               if (!mark)
+                       continue;
+               if (i == 0)
+                       printf("from :%d\n", mark);
+               else if (i == 1)
+                       printf("merge :%d", mark);
+               else
+                       printf(" :%d", mark);
+               i++;
+       }
+       if (i > 1)
+               printf("\n");
+
+       log_tree_diff_flush(rev);
+       rev->diffopt.output_format = saved_output_format;
+
+       printf("\n");
+
+       show_progress();
+}
+
+static void handle_tail(struct object_array *commits, struct rev_info *revs)
+{
+       struct commit *commit;
+       while (commits->nr) {
+               commit = (struct commit *)commits->objects[commits->nr - 1].item;
+               if (has_unshown_parent(commit))
+                       return;
+               handle_commit(commit, revs);
+               commits->nr--;
+       }
+}
+
+static void handle_tag(const char *name, struct tag *tag)
+{
+       unsigned long size;
+       enum object_type type;
+       char *buf;
+       const char *tagger, *tagger_end, *message;
+       size_t message_size = 0;
+
+       buf = read_sha1_file(tag->object.sha1, &type, &size);
+       if (!buf)
+               die ("Could not read tag %s", sha1_to_hex(tag->object.sha1));
+       message = memmem(buf, size, "\n\n", 2);
+       if (message) {
+               message += 2;
+               message_size = strlen(message);
+       }
+       tagger = memmem(buf, message ? message - buf : size, "\ntagger ", 8);
+       if (!tagger)
+               die ("No tagger for tag %s", sha1_to_hex(tag->object.sha1));
+       tagger++;
+       tagger_end = strchrnul(tagger, '\n');
+
+       /* handle signed tags */
+       if (message) {
+               const char *signature = strstr(message,
+                                              "\n-----BEGIN PGP SIGNATURE-----\n");
+               if (signature)
+                       switch(signed_tag_mode) {
+                       case ABORT:
+                               die ("Encountered signed tag %s; use "
+                                    "--signed-tag=<mode> to handle it.",
+                                    sha1_to_hex(tag->object.sha1));
+                       case WARN:
+                               warning ("Exporting signed tag %s",
+                                        sha1_to_hex(tag->object.sha1));
+                               /* fallthru */
+                       case VERBATIM:
+                               break;
+                       case STRIP:
+                               message_size = signature + 1 - message;
+                               break;
+                       }
+       }
+
+       if (!prefixcmp(name, "refs/tags/"))
+               name += 10;
+       printf("tag %s\nfrom :%d\n%.*s\ndata %d\n%.*s\n",
+              name, get_object_mark(tag->tagged),
+              (int)(tagger_end - tagger), tagger,
+              (int)message_size, (int)message_size, message ? message : "");
+}
+
+static void get_tags_and_duplicates(struct object_array *pending,
+                                   struct path_list *extra_refs)
+{
+       struct tag *tag;
+       int i;
+
+       for (i = 0; i < pending->nr; i++) {
+               struct object_array_entry *e = pending->objects + i;
+               unsigned char sha1[20];
+               struct commit *commit = commit;
+               char *full_name;
+
+               if (dwim_ref(e->name, strlen(e->name), sha1, &full_name) != 1)
+                       continue;
+
+               switch (e->item->type) {
+               case OBJ_COMMIT:
+                       commit = (struct commit *)e->item;
+                       break;
+               case OBJ_TAG:
+                       tag = (struct tag *)e->item;
+                       while (tag && tag->object.type == OBJ_TAG) {
+                               path_list_insert(full_name, extra_refs)->util = tag;
+                               tag = (struct tag *)tag->tagged;
+                       }
+                       if (!tag)
+                               die ("Tag %s points nowhere?", e->name);
+                       switch(tag->object.type) {
+                       case OBJ_COMMIT:
+                               commit = (struct commit *)tag;
+                               break;
+                       case OBJ_BLOB:
+                               handle_object(tag->object.sha1);
+                               continue;
+                       }
+                       break;
+               default:
+                       die ("Unexpected object of type %s",
+                            typename(e->item->type));
+               }
+               if (commit->util)
+                       /* more than one name for the same object */
+                       path_list_insert(full_name, extra_refs)->util = commit;
+               else
+                       commit->util = full_name;
+       }
+}
+
+static void handle_tags_and_duplicates(struct path_list *extra_refs)
+{
+       struct commit *commit;
+       int i;
+
+       for (i = extra_refs->nr - 1; i >= 0; i--) {
+               const char *name = extra_refs->items[i].path;
+               struct object *object = extra_refs->items[i].util;
+               switch (object->type) {
+               case OBJ_TAG:
+                       handle_tag(name, (struct tag *)object);
+                       break;
+               case OBJ_COMMIT:
+                       /* create refs pointing to already seen commits */
+                       commit = (struct commit *)object;
+                       printf("reset %s\nfrom :%d\n\n", name,
+                              get_object_mark(&commit->object));
+                       show_progress();
+                       break;
+               }
+       }
+}
+
+int cmd_fast_export(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info revs;
+       struct object_array commits = { 0, 0, NULL };
+       struct path_list extra_refs = { NULL, 0, 0, 0 };
+       struct commit *commit;
+       struct option options[] = {
+               OPT_INTEGER(0, "progress", &progress,
+                           "show progress after <n> objects"),
+               OPT_CALLBACK(0, "signed-tags", &signed_tag_mode, "mode",
+                            "select handling of signed tags",
+                            parse_opt_signed_tag_mode),
+               OPT_END()
+       };
+
+       /* we handle encodings */
+       git_config(git_default_config);
+
+       init_revisions(&revs, prefix);
+       argc = setup_revisions(argc, argv, &revs, NULL);
+       argc = parse_options(argc, argv, options, fast_export_usage, 0);
+       if (argc > 1)
+               usage_with_options (fast_export_usage, options);
+
+       get_tags_and_duplicates(&revs.pending, &extra_refs);
+
+       prepare_revision_walk(&revs);
+       revs.diffopt.format_callback = show_filemodify;
+       DIFF_OPT_SET(&revs.diffopt, RECURSIVE);
+       while ((commit = get_revision(&revs))) {
+               if (has_unshown_parent(commit)) {
+                       struct commit_list *parent = commit->parents;
+                       add_object_array(&commit->object, NULL, &commits);
+                       for (; parent; parent = parent->next)
+                               if (!parent->item->util)
+                                       parent->item->util = commit->util;
+               }
+               else {
+                       handle_commit(commit, &revs);
+                       handle_tail(&commits, &revs);
+               }
+       }
+
+       handle_tags_and_duplicates(&extra_refs);
+
+       return 0;
+}
diff --git a/builtin-fetch--tool.c b/builtin-fetch--tool.c
new file mode 100644 (file)
index 0000000..7460ab7
--- /dev/null
@@ -0,0 +1,574 @@
+#include "builtin.h"
+#include "cache.h"
+#include "refs.h"
+#include "commit.h"
+
+static char *get_stdin(void)
+{
+       struct strbuf buf;
+       strbuf_init(&buf, 0);
+       if (strbuf_read(&buf, 0, 1024) < 0) {
+               die("error reading standard input: %s", strerror(errno));
+       }
+       return strbuf_detach(&buf, NULL);
+}
+
+static void show_new(enum object_type type, unsigned char *sha1_new)
+{
+       fprintf(stderr, "  %s: %s\n", typename(type),
+               find_unique_abbrev(sha1_new, DEFAULT_ABBREV));
+}
+
+static int update_ref_env(const char *action,
+                     const char *refname,
+                     unsigned char *sha1,
+                     unsigned char *oldval)
+{
+       char msg[1024];
+       const char *rla = getenv("GIT_REFLOG_ACTION");
+
+       if (!rla)
+               rla = "(reflog update)";
+       if (snprintf(msg, sizeof(msg), "%s: %s", rla, action) >= sizeof(msg))
+               warning("reflog message too long: %.*s...", 50, msg);
+       return update_ref(msg, refname, sha1, oldval, 0, QUIET_ON_ERR);
+}
+
+static int update_local_ref(const char *name,
+                           const char *new_head,
+                           const char *note,
+                           int verbose, int force)
+{
+       unsigned char sha1_old[20], sha1_new[20];
+       char oldh[41], newh[41];
+       struct commit *current, *updated;
+       enum object_type type;
+
+       if (get_sha1_hex(new_head, sha1_new))
+               die("malformed object name %s", new_head);
+
+       type = sha1_object_info(sha1_new, NULL);
+       if (type < 0)
+               die("object %s not found", new_head);
+
+       if (!*name) {
+               /* Not storing */
+               if (verbose) {
+                       fprintf(stderr, "* fetched %s\n", note);
+                       show_new(type, sha1_new);
+               }
+               return 0;
+       }
+
+       if (get_sha1(name, sha1_old)) {
+               const char *msg;
+       just_store:
+               /* new ref */
+               if (!strncmp(name, "refs/tags/", 10))
+                       msg = "storing tag";
+               else
+                       msg = "storing head";
+               fprintf(stderr, "* %s: storing %s\n",
+                       name, note);
+               show_new(type, sha1_new);
+               return update_ref_env(msg, name, sha1_new, NULL);
+       }
+
+       if (!hashcmp(sha1_old, sha1_new)) {
+               if (verbose) {
+                       fprintf(stderr, "* %s: same as %s\n", name, note);
+                       show_new(type, sha1_new);
+               }
+               return 0;
+       }
+
+       if (!strncmp(name, "refs/tags/", 10)) {
+               fprintf(stderr, "* %s: updating with %s\n", name, note);
+               show_new(type, sha1_new);
+               return update_ref_env("updating tag", name, sha1_new, NULL);
+       }
+
+       current = lookup_commit_reference(sha1_old);
+       updated = lookup_commit_reference(sha1_new);
+       if (!current || !updated)
+               goto just_store;
+
+       strcpy(oldh, find_unique_abbrev(current->object.sha1, DEFAULT_ABBREV));
+       strcpy(newh, find_unique_abbrev(sha1_new, DEFAULT_ABBREV));
+
+       if (in_merge_bases(current, &updated, 1)) {
+               fprintf(stderr, "* %s: fast forward to %s\n",
+                       name, note);
+               fprintf(stderr, "  old..new: %s..%s\n", oldh, newh);
+               return update_ref_env("fast forward", name, sha1_new, sha1_old);
+       }
+       if (!force) {
+               fprintf(stderr,
+                       "* %s: not updating to non-fast forward %s\n",
+                       name, note);
+               fprintf(stderr,
+                       "  old...new: %s...%s\n", oldh, newh);
+               return 1;
+       }
+       fprintf(stderr,
+               "* %s: forcing update to non-fast forward %s\n",
+               name, note);
+       fprintf(stderr, "  old...new: %s...%s\n", oldh, newh);
+       return update_ref_env("forced-update", name, sha1_new, sha1_old);
+}
+
+static int append_fetch_head(FILE *fp,
+                            const char *head, const char *remote,
+                            const char *remote_name, const char *remote_nick,
+                            const char *local_name, int not_for_merge,
+                            int verbose, int force)
+{
+       struct commit *commit;
+       int remote_len, i, note_len;
+       unsigned char sha1[20];
+       char note[1024];
+       const char *what, *kind;
+
+       if (get_sha1(head, sha1))
+               return error("Not a valid object name: %s", head);
+       commit = lookup_commit_reference_gently(sha1, 1);
+       if (!commit)
+               not_for_merge = 1;
+
+       if (!strcmp(remote_name, "HEAD")) {
+               kind = "";
+               what = "";
+       }
+       else if (!strncmp(remote_name, "refs/heads/", 11)) {
+               kind = "branch";
+               what = remote_name + 11;
+       }
+       else if (!strncmp(remote_name, "refs/tags/", 10)) {
+               kind = "tag";
+               what = remote_name + 10;
+       }
+       else if (!strncmp(remote_name, "refs/remotes/", 13)) {
+               kind = "remote branch";
+               what = remote_name + 13;
+       }
+       else {
+               kind = "";
+               what = remote_name;
+       }
+
+       remote_len = strlen(remote);
+       for (i = remote_len - 1; remote[i] == '/' && 0 <= i; i--)
+               ;
+       remote_len = i + 1;
+       if (4 < i && !strncmp(".git", remote + i - 3, 4))
+               remote_len = i - 3;
+
+       note_len = 0;
+       if (*what) {
+               if (*kind)
+                       note_len += sprintf(note + note_len, "%s ", kind);
+               note_len += sprintf(note + note_len, "'%s' of ", what);
+       }
+       note_len += sprintf(note + note_len, "%.*s", remote_len, remote);
+       fprintf(fp, "%s\t%s\t%s\n",
+               sha1_to_hex(commit ? commit->object.sha1 : sha1),
+               not_for_merge ? "not-for-merge" : "",
+               note);
+       return update_local_ref(local_name, head, note, verbose, force);
+}
+
+static char *keep;
+static void remove_keep(void)
+{
+       if (keep && *keep)
+               unlink(keep);
+}
+
+static void remove_keep_on_signal(int signo)
+{
+       remove_keep();
+       signal(SIGINT, SIG_DFL);
+       raise(signo);
+}
+
+static char *find_local_name(const char *remote_name, const char *refs,
+                            int *force_p, int *not_for_merge_p)
+{
+       const char *ref = refs;
+       int len = strlen(remote_name);
+
+       while (ref) {
+               const char *next;
+               int single_force, not_for_merge;
+
+               while (*ref == '\n')
+                       ref++;
+               if (!*ref)
+                       break;
+               next = strchr(ref, '\n');
+
+               single_force = not_for_merge = 0;
+               if (*ref == '+') {
+                       single_force = 1;
+                       ref++;
+               }
+               if (*ref == '.') {
+                       not_for_merge = 1;
+                       ref++;
+                       if (*ref == '+') {
+                               single_force = 1;
+                               ref++;
+                       }
+               }
+               if (!strncmp(remote_name, ref, len) && ref[len] == ':') {
+                       const char *local_part = ref + len + 1;
+                       int retlen;
+
+                       if (!next)
+                               retlen = strlen(local_part);
+                       else
+                               retlen = next - local_part;
+                       *force_p = single_force;
+                       *not_for_merge_p = not_for_merge;
+                       return xmemdupz(local_part, retlen);
+               }
+               ref = next;
+       }
+       return NULL;
+}
+
+static int fetch_native_store(FILE *fp,
+                             const char *remote,
+                             const char *remote_nick,
+                             const char *refs,
+                             int verbose, int force)
+{
+       char buffer[1024];
+       int err = 0;
+
+       signal(SIGINT, remove_keep_on_signal);
+       atexit(remove_keep);
+
+       while (fgets(buffer, sizeof(buffer), stdin)) {
+               int len;
+               char *cp;
+               char *local_name;
+               int single_force, not_for_merge;
+
+               for (cp = buffer; *cp && !isspace(*cp); cp++)
+                       ;
+               if (*cp)
+                       *cp++ = 0;
+               len = strlen(cp);
+               if (len && cp[len-1] == '\n')
+                       cp[--len] = 0;
+               if (!strcmp(buffer, "failed"))
+                       die("Fetch failure: %s", remote);
+               if (!strcmp(buffer, "pack"))
+                       continue;
+               if (!strcmp(buffer, "keep")) {
+                       char *od = get_object_directory();
+                       int len = strlen(od) + strlen(cp) + 50;
+                       keep = xmalloc(len);
+                       sprintf(keep, "%s/pack/pack-%s.keep", od, cp);
+                       continue;
+               }
+
+               local_name = find_local_name(cp, refs,
+                                            &single_force, &not_for_merge);
+               if (!local_name)
+                       continue;
+               err |= append_fetch_head(fp,
+                                        buffer, remote, cp, remote_nick,
+                                        local_name, not_for_merge,
+                                        verbose, force || single_force);
+       }
+       return err;
+}
+
+static int parse_reflist(const char *reflist)
+{
+       const char *ref;
+
+       printf("refs='");
+       for (ref = reflist; ref; ) {
+               const char *next;
+               while (*ref && isspace(*ref))
+                       ref++;
+               if (!*ref)
+                       break;
+               for (next = ref; *next && !isspace(*next); next++)
+                       ;
+               printf("\n%.*s", (int)(next - ref), ref);
+               ref = next;
+       }
+       printf("'\n");
+
+       printf("rref='");
+       for (ref = reflist; ref; ) {
+               const char *next, *colon;
+               while (*ref && isspace(*ref))
+                       ref++;
+               if (!*ref)
+                       break;
+               for (next = ref; *next && !isspace(*next); next++)
+                       ;
+               if (*ref == '.')
+                       ref++;
+               if (*ref == '+')
+                       ref++;
+               colon = strchr(ref, ':');
+               putchar('\n');
+               printf("%.*s", (int)((colon ? colon : next) - ref), ref);
+               ref = next;
+       }
+       printf("'\n");
+       return 0;
+}
+
+static int expand_refs_wildcard(const char *ls_remote_result, int numrefs,
+                               const char **refs)
+{
+       int i, matchlen, replacelen;
+       int found_one = 0;
+       const char *remote = *refs++;
+       numrefs--;
+
+       if (numrefs == 0) {
+               fprintf(stderr, "Nothing specified for fetching with remote.%s.fetch\n",
+                       remote);
+               printf("empty\n");
+       }
+
+       for (i = 0; i < numrefs; i++) {
+               const char *ref = refs[i];
+               const char *lref = ref;
+               const char *colon;
+               const char *tail;
+               const char *ls;
+               const char *next;
+
+               if (*lref == '+')
+                       lref++;
+               colon = strchr(lref, ':');
+               tail = lref + strlen(lref);
+               if (!(colon &&
+                     2 < colon - lref &&
+                     colon[-1] == '*' &&
+                     colon[-2] == '/' &&
+                     2 < tail - (colon + 1) &&
+                     tail[-1] == '*' &&
+                     tail[-2] == '/')) {
+                       /* not a glob */
+                       if (!found_one++)
+                               printf("explicit\n");
+                       printf("%s\n", ref);
+                       continue;
+               }
+
+               /* glob */
+               if (!found_one++)
+                       printf("glob\n");
+
+               /* lref to colon-2 is remote hierarchy name;
+                * colon+1 to tail-2 is local.
+                */
+               matchlen = (colon-1) - lref;
+               replacelen = (tail-1) - (colon+1);
+               for (ls = ls_remote_result; ls; ls = next) {
+                       const char *eol;
+                       unsigned char sha1[20];
+                       int namelen;
+
+                       while (*ls && isspace(*ls))
+                               ls++;
+                       next = strchr(ls, '\n');
+                       eol = !next ? (ls + strlen(ls)) : next;
+                       if (!memcmp("^{}", eol-3, 3))
+                               continue;
+                       if (eol - ls < 40)
+                               continue;
+                       if (get_sha1_hex(ls, sha1))
+                               continue;
+                       ls += 40;
+                       while (ls < eol && isspace(*ls))
+                               ls++;
+                       /* ls to next (or eol) is the name.
+                        * is it identical to lref to colon-2?
+                        */
+                       if ((eol - ls) <= matchlen ||
+                           strncmp(ls, lref, matchlen))
+                               continue;
+
+                       /* Yes, it is a match */
+                       namelen = eol - ls;
+                       if (lref != ref)
+                               putchar('+');
+                       printf("%.*s:%.*s%.*s\n",
+                              namelen, ls,
+                              replacelen, colon + 1,
+                              namelen - matchlen, ls + matchlen);
+               }
+       }
+       return 0;
+}
+
+static int pick_rref(int sha1_only, const char *rref, const char *ls_remote_result)
+{
+       int err = 0;
+       int lrr_count = lrr_count, i, pass;
+       const char *cp;
+       struct lrr {
+               const char *line;
+               const char *name;
+               int namelen;
+               int shown;
+       } *lrr_list = lrr_list;
+
+       for (pass = 0; pass < 2; pass++) {
+               /* pass 0 counts and allocates, pass 1 fills... */
+               cp = ls_remote_result;
+               i = 0;
+               while (1) {
+                       const char *np;
+                       while (*cp && isspace(*cp))
+                               cp++;
+                       if (!*cp)
+                               break;
+                       np = strchrnul(cp, '\n');
+                       if (pass) {
+                               lrr_list[i].line = cp;
+                               lrr_list[i].name = cp + 41;
+                               lrr_list[i].namelen = np - (cp + 41);
+                       }
+                       i++;
+                       cp = np;
+               }
+               if (!pass) {
+                       lrr_count = i;
+                       lrr_list = xcalloc(lrr_count, sizeof(*lrr_list));
+               }
+       }
+
+       while (1) {
+               const char *next;
+               int rreflen;
+               int i;
+
+               while (*rref && isspace(*rref))
+                       rref++;
+               if (!*rref)
+                       break;
+               next = strchrnul(rref, '\n');
+               rreflen = next - rref;
+
+               for (i = 0; i < lrr_count; i++) {
+                       struct lrr *lrr = &(lrr_list[i]);
+
+                       if (rreflen == lrr->namelen &&
+                           !memcmp(lrr->name, rref, rreflen)) {
+                               if (!lrr->shown)
+                                       printf("%.*s\n",
+                                              sha1_only ? 40 : lrr->namelen + 41,
+                                              lrr->line);
+                               lrr->shown = 1;
+                               break;
+                       }
+               }
+               if (lrr_count <= i) {
+                       error("pick-rref: %.*s not found", rreflen, rref);
+                       err = 1;
+               }
+               rref = next;
+       }
+       free(lrr_list);
+       return err;
+}
+
+int cmd_fetch__tool(int argc, const char **argv, const char *prefix)
+{
+       int verbose = 0;
+       int force = 0;
+       int sopt = 0;
+
+       while (1 < argc) {
+               const char *arg = argv[1];
+               if (!strcmp("-v", arg))
+                       verbose = 1;
+               else if (!strcmp("-f", arg))
+                       force = 1;
+               else if (!strcmp("-s", arg))
+                       sopt = 1;
+               else
+                       break;
+               argc--;
+               argv++;
+       }
+
+       if (argc <= 1)
+               return error("Missing subcommand");
+
+       if (!strcmp("append-fetch-head", argv[1])) {
+               int result;
+               FILE *fp;
+               char *filename;
+
+               if (argc != 8)
+                       return error("append-fetch-head takes 6 args");
+               filename = git_path("FETCH_HEAD");
+               fp = fopen(filename, "a");
+               if (!fp)
+                       return error("cannot open %s: %s\n", filename, strerror(errno));
+               result = append_fetch_head(fp, argv[2], argv[3],
+                                          argv[4], argv[5],
+                                          argv[6], !!argv[7][0],
+                                          verbose, force);
+               fclose(fp);
+               return result;
+       }
+       if (!strcmp("native-store", argv[1])) {
+               int result;
+               FILE *fp;
+               char *filename;
+
+               if (argc != 5)
+                       return error("fetch-native-store takes 3 args");
+               filename = git_path("FETCH_HEAD");
+               fp = fopen(filename, "a");
+               if (!fp)
+                       return error("cannot open %s: %s\n", filename, strerror(errno));
+               result = fetch_native_store(fp, argv[2], argv[3], argv[4],
+                                           verbose, force);
+               fclose(fp);
+               return result;
+       }
+       if (!strcmp("parse-reflist", argv[1])) {
+               const char *reflist;
+               if (argc != 3)
+                       return error("parse-reflist takes 1 arg");
+               reflist = argv[2];
+               if (!strcmp(reflist, "-"))
+                       reflist = get_stdin();
+               return parse_reflist(reflist);
+       }
+       if (!strcmp("pick-rref", argv[1])) {
+               const char *ls_remote_result;
+               if (argc != 4)
+                       return error("pick-rref takes 2 args");
+               ls_remote_result = argv[3];
+               if (!strcmp(ls_remote_result, "-"))
+                       ls_remote_result = get_stdin();
+               return pick_rref(sopt, argv[2], ls_remote_result);
+       }
+       if (!strcmp("expand-refs-wildcard", argv[1])) {
+               const char *reflist;
+               if (argc < 4)
+                       return error("expand-refs-wildcard takes at least 2 args");
+               reflist = argv[2];
+               if (!strcmp(reflist, "-"))
+                       reflist = get_stdin();
+               return expand_refs_wildcard(reflist, argc - 3, argv + 3);
+       }
+
+       return error("Unknown subcommand: %s", argv[1]);
+}
diff --git a/builtin-fetch-pack.c b/builtin-fetch-pack.c
new file mode 100644 (file)
index 0000000..807fa93
--- /dev/null
@@ -0,0 +1,795 @@
+#include "cache.h"
+#include "refs.h"
+#include "pkt-line.h"
+#include "commit.h"
+#include "tag.h"
+#include "exec_cmd.h"
+#include "pack.h"
+#include "sideband.h"
+#include "fetch-pack.h"
+#include "run-command.h"
+
+static int transfer_unpack_limit = -1;
+static int fetch_unpack_limit = -1;
+static int unpack_limit = 100;
+static struct fetch_pack_args args = {
+       /* .uploadpack = */ "git-upload-pack",
+};
+
+static const char fetch_pack_usage[] =
+"git-fetch-pack [--all] [--quiet|-q] [--keep|-k] [--thin] [--upload-pack=<git-upload-pack>] [--depth=<n>] [--no-progress] [-v] [<host>:]<directory> [<refs>...]";
+
+#define COMPLETE       (1U << 0)
+#define COMMON         (1U << 1)
+#define COMMON_REF     (1U << 2)
+#define SEEN           (1U << 3)
+#define POPPED         (1U << 4)
+
+/*
+ * After sending this many "have"s if we do not get any new ACK , we
+ * give up traversing our history.
+ */
+#define MAX_IN_VAIN 256
+
+static struct commit_list *rev_list;
+static int non_common_revs, multi_ack, use_sideband;
+
+static void rev_list_push(struct commit *commit, int mark)
+{
+       if (!(commit->object.flags & mark)) {
+               commit->object.flags |= mark;
+
+               if (!(commit->object.parsed))
+                       parse_commit(commit);
+
+               insert_by_date(commit, &rev_list);
+
+               if (!(commit->object.flags & COMMON))
+                       non_common_revs++;
+       }
+}
+
+static int rev_list_insert_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct object *o = deref_tag(parse_object(sha1), path, 0);
+
+       if (o && o->type == OBJ_COMMIT)
+               rev_list_push((struct commit *)o, SEEN);
+
+       return 0;
+}
+
+/*
+   This function marks a rev and its ancestors as common.
+   In some cases, it is desirable to mark only the ancestors (for example
+   when only the server does not yet know that they are common).
+*/
+
+static void mark_common(struct commit *commit,
+               int ancestors_only, int dont_parse)
+{
+       if (commit != NULL && !(commit->object.flags & COMMON)) {
+               struct object *o = (struct object *)commit;
+
+               if (!ancestors_only)
+                       o->flags |= COMMON;
+
+               if (!(o->flags & SEEN))
+                       rev_list_push(commit, SEEN);
+               else {
+                       struct commit_list *parents;
+
+                       if (!ancestors_only && !(o->flags & POPPED))
+                               non_common_revs--;
+                       if (!o->parsed && !dont_parse)
+                               parse_commit(commit);
+
+                       for (parents = commit->parents;
+                                       parents;
+                                       parents = parents->next)
+                               mark_common(parents->item, 0, dont_parse);
+               }
+       }
+}
+
+/*
+  Get the next rev to send, ignoring the common.
+*/
+
+static const unsigned char* get_rev(void)
+{
+       struct commit *commit = NULL;
+
+       while (commit == NULL) {
+               unsigned int mark;
+               struct commit_list* parents;
+
+               if (rev_list == NULL || non_common_revs == 0)
+                       return NULL;
+
+               commit = rev_list->item;
+               if (!(commit->object.parsed))
+                       parse_commit(commit);
+               commit->object.flags |= POPPED;
+               if (!(commit->object.flags & COMMON))
+                       non_common_revs--;
+
+               parents = commit->parents;
+
+               if (commit->object.flags & COMMON) {
+                       /* do not send "have", and ignore ancestors */
+                       commit = NULL;
+                       mark = COMMON | SEEN;
+               } else if (commit->object.flags & COMMON_REF)
+                       /* send "have", and ignore ancestors */
+                       mark = COMMON | SEEN;
+               else
+                       /* send "have", also for its ancestors */
+                       mark = SEEN;
+
+               while (parents) {
+                       if (!(parents->item->object.flags & SEEN))
+                               rev_list_push(parents->item, mark);
+                       if (mark & COMMON)
+                               mark_common(parents->item, 1, 0);
+                       parents = parents->next;
+               }
+
+               rev_list = rev_list->next;
+       }
+
+       return commit->object.sha1;
+}
+
+static int find_common(int fd[2], unsigned char *result_sha1,
+                      struct ref *refs)
+{
+       int fetching;
+       int count = 0, flushes = 0, retval;
+       const unsigned char *sha1;
+       unsigned in_vain = 0;
+       int got_continue = 0;
+
+       for_each_ref(rev_list_insert_ref, NULL);
+
+       fetching = 0;
+       for ( ; refs ; refs = refs->next) {
+               unsigned char *remote = refs->old_sha1;
+               struct object *o;
+
+               /*
+                * If that object is complete (i.e. it is an ancestor of a
+                * local ref), we tell them we have it but do not have to
+                * tell them about its ancestors, which they already know
+                * about.
+                *
+                * We use lookup_object here because we are only
+                * interested in the case we *know* the object is
+                * reachable and we have already scanned it.
+                */
+               if (((o = lookup_object(remote)) != NULL) &&
+                               (o->flags & COMPLETE)) {
+                       continue;
+               }
+
+               if (!fetching)
+                       packet_write(fd[1], "want %s%s%s%s%s%s%s\n",
+                                    sha1_to_hex(remote),
+                                    (multi_ack ? " multi_ack" : ""),
+                                    (use_sideband == 2 ? " side-band-64k" : ""),
+                                    (use_sideband == 1 ? " side-band" : ""),
+                                    (args.use_thin_pack ? " thin-pack" : ""),
+                                    (args.no_progress ? " no-progress" : ""),
+                                    " ofs-delta");
+               else
+                       packet_write(fd[1], "want %s\n", sha1_to_hex(remote));
+               fetching++;
+       }
+       if (is_repository_shallow())
+               write_shallow_commits(fd[1], 1);
+       if (args.depth > 0)
+               packet_write(fd[1], "deepen %d", args.depth);
+       packet_flush(fd[1]);
+       if (!fetching)
+               return 1;
+
+       if (args.depth > 0) {
+               char line[1024];
+               unsigned char sha1[20];
+               int len;
+
+               while ((len = packet_read_line(fd[0], line, sizeof(line)))) {
+                       if (!prefixcmp(line, "shallow ")) {
+                               if (get_sha1_hex(line + 8, sha1))
+                                       die("invalid shallow line: %s", line);
+                               register_shallow(sha1);
+                               continue;
+                       }
+                       if (!prefixcmp(line, "unshallow ")) {
+                               if (get_sha1_hex(line + 10, sha1))
+                                       die("invalid unshallow line: %s", line);
+                               if (!lookup_object(sha1))
+                                       die("object not found: %s", line);
+                               /* make sure that it is parsed as shallow */
+                               parse_object(sha1);
+                               if (unregister_shallow(sha1))
+                                       die("no shallow found: %s", line);
+                               continue;
+                       }
+                       die("expected shallow/unshallow, got %s", line);
+               }
+       }
+
+       flushes = 0;
+       retval = -1;
+       while ((sha1 = get_rev())) {
+               packet_write(fd[1], "have %s\n", sha1_to_hex(sha1));
+               if (args.verbose)
+                       fprintf(stderr, "have %s\n", sha1_to_hex(sha1));
+               in_vain++;
+               if (!(31 & ++count)) {
+                       int ack;
+
+                       packet_flush(fd[1]);
+                       flushes++;
+
+                       /*
+                        * We keep one window "ahead" of the other side, and
+                        * will wait for an ACK only on the next one
+                        */
+                       if (count == 32)
+                               continue;
+
+                       do {
+                               ack = get_ack(fd[0], result_sha1);
+                               if (args.verbose && ack)
+                                       fprintf(stderr, "got ack %d %s\n", ack,
+                                                       sha1_to_hex(result_sha1));
+                               if (ack == 1) {
+                                       flushes = 0;
+                                       multi_ack = 0;
+                                       retval = 0;
+                                       goto done;
+                               } else if (ack == 2) {
+                                       struct commit *commit =
+                                               lookup_commit(result_sha1);
+                                       mark_common(commit, 0, 1);
+                                       retval = 0;
+                                       in_vain = 0;
+                                       got_continue = 1;
+                               }
+                       } while (ack);
+                       flushes--;
+                       if (got_continue && MAX_IN_VAIN < in_vain) {
+                               if (args.verbose)
+                                       fprintf(stderr, "giving up\n");
+                               break; /* give up */
+                       }
+               }
+       }
+done:
+       packet_write(fd[1], "done\n");
+       if (args.verbose)
+               fprintf(stderr, "done\n");
+       if (retval != 0) {
+               multi_ack = 0;
+               flushes++;
+       }
+       while (flushes || multi_ack) {
+               int ack = get_ack(fd[0], result_sha1);
+               if (ack) {
+                       if (args.verbose)
+                               fprintf(stderr, "got ack (%d) %s\n", ack,
+                                       sha1_to_hex(result_sha1));
+                       if (ack == 1)
+                               return 0;
+                       multi_ack = 1;
+                       continue;
+               }
+               flushes--;
+       }
+       return retval;
+}
+
+static struct commit_list *complete;
+
+static int mark_complete(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct object *o = parse_object(sha1);
+
+       while (o && o->type == OBJ_TAG) {
+               struct tag *t = (struct tag *) o;
+               if (!t->tagged)
+                       break; /* broken repository */
+               o->flags |= COMPLETE;
+               o = parse_object(t->tagged->sha1);
+       }
+       if (o && o->type == OBJ_COMMIT) {
+               struct commit *commit = (struct commit *)o;
+               commit->object.flags |= COMPLETE;
+               insert_by_date(commit, &complete);
+       }
+       return 0;
+}
+
+static void mark_recent_complete_commits(unsigned long cutoff)
+{
+       while (complete && cutoff <= complete->item->date) {
+               if (args.verbose)
+                       fprintf(stderr, "Marking %s as complete\n",
+                               sha1_to_hex(complete->item->object.sha1));
+               pop_most_recent_commit(&complete, COMPLETE);
+       }
+}
+
+static void filter_refs(struct ref **refs, int nr_match, char **match)
+{
+       struct ref **return_refs;
+       struct ref *newlist = NULL;
+       struct ref **newtail = &newlist;
+       struct ref *ref, *next;
+       struct ref *fastarray[32];
+
+       if (nr_match && !args.fetch_all) {
+               if (ARRAY_SIZE(fastarray) < nr_match)
+                       return_refs = xcalloc(nr_match, sizeof(struct ref *));
+               else {
+                       return_refs = fastarray;
+                       memset(return_refs, 0, sizeof(struct ref *) * nr_match);
+               }
+       }
+       else
+               return_refs = NULL;
+
+       for (ref = *refs; ref; ref = next) {
+               next = ref->next;
+               if (!memcmp(ref->name, "refs/", 5) &&
+                   check_ref_format(ref->name + 5))
+                       ; /* trash */
+               else if (args.fetch_all &&
+                        (!args.depth || prefixcmp(ref->name, "refs/tags/") )) {
+                       *newtail = ref;
+                       ref->next = NULL;
+                       newtail = &ref->next;
+                       continue;
+               }
+               else {
+                       int order = path_match(ref->name, nr_match, match);
+                       if (order) {
+                               return_refs[order-1] = ref;
+                               continue; /* we will link it later */
+                       }
+               }
+               free(ref);
+       }
+
+       if (!args.fetch_all) {
+               int i;
+               for (i = 0; i < nr_match; i++) {
+                       ref = return_refs[i];
+                       if (ref) {
+                               *newtail = ref;
+                               ref->next = NULL;
+                               newtail = &ref->next;
+                       }
+               }
+               if (return_refs != fastarray)
+                       free(return_refs);
+       }
+       *refs = newlist;
+}
+
+static int everything_local(struct ref **refs, int nr_match, char **match)
+{
+       struct ref *ref;
+       int retval;
+       unsigned long cutoff = 0;
+
+       track_object_refs = 0;
+       save_commit_buffer = 0;
+
+       for (ref = *refs; ref; ref = ref->next) {
+               struct object *o;
+
+               o = parse_object(ref->old_sha1);
+               if (!o)
+                       continue;
+
+               /* We already have it -- which may mean that we were
+                * in sync with the other side at some time after
+                * that (it is OK if we guess wrong here).
+                */
+               if (o->type == OBJ_COMMIT) {
+                       struct commit *commit = (struct commit *)o;
+                       if (!cutoff || cutoff < commit->date)
+                               cutoff = commit->date;
+               }
+       }
+
+       if (!args.depth) {
+               for_each_ref(mark_complete, NULL);
+               if (cutoff)
+                       mark_recent_complete_commits(cutoff);
+       }
+
+       /*
+        * Mark all complete remote refs as common refs.
+        * Don't mark them common yet; the server has to be told so first.
+        */
+       for (ref = *refs; ref; ref = ref->next) {
+               struct object *o = deref_tag(lookup_object(ref->old_sha1),
+                                            NULL, 0);
+
+               if (!o || o->type != OBJ_COMMIT || !(o->flags & COMPLETE))
+                       continue;
+
+               if (!(o->flags & SEEN)) {
+                       rev_list_push((struct commit *)o, COMMON_REF | SEEN);
+
+                       mark_common((struct commit *)o, 1, 1);
+               }
+       }
+
+       filter_refs(refs, nr_match, match);
+
+       for (retval = 1, ref = *refs; ref ; ref = ref->next) {
+               const unsigned char *remote = ref->old_sha1;
+               unsigned char local[20];
+               struct object *o;
+
+               o = lookup_object(remote);
+               if (!o || !(o->flags & COMPLETE)) {
+                       retval = 0;
+                       if (!args.verbose)
+                               continue;
+                       fprintf(stderr,
+                               "want %s (%s)\n", sha1_to_hex(remote),
+                               ref->name);
+                       continue;
+               }
+
+               hashcpy(ref->new_sha1, local);
+               if (!args.verbose)
+                       continue;
+               fprintf(stderr,
+                       "already have %s (%s)\n", sha1_to_hex(remote),
+                       ref->name);
+       }
+       return retval;
+}
+
+static int sideband_demux(int fd, void *data)
+{
+       int *xd = data;
+
+       return recv_sideband("fetch-pack", xd[0], fd, 2);
+}
+
+static int get_pack(int xd[2], char **pack_lockfile)
+{
+       struct async demux;
+       const char *argv[20];
+       char keep_arg[256];
+       char hdr_arg[256];
+       const char **av;
+       int do_keep = args.keep_pack;
+       struct child_process cmd;
+
+       memset(&demux, 0, sizeof(demux));
+       if (use_sideband) {
+               /* xd[] is talking with upload-pack; subprocess reads from
+                * xd[0], spits out band#2 to stderr, and feeds us band#1
+                * through demux->out.
+                */
+               demux.proc = sideband_demux;
+               demux.data = xd;
+               if (start_async(&demux))
+                       die("fetch-pack: unable to fork off sideband"
+                           " demultiplexer");
+       }
+       else
+               demux.out = xd[0];
+
+       memset(&cmd, 0, sizeof(cmd));
+       cmd.argv = argv;
+       av = argv;
+       *hdr_arg = 0;
+       if (!args.keep_pack && unpack_limit) {
+               struct pack_header header;
+
+               if (read_pack_header(demux.out, &header))
+                       die("protocol error: bad pack header");
+               snprintf(hdr_arg, sizeof(hdr_arg), "--pack_header=%u,%u",
+                        ntohl(header.hdr_version), ntohl(header.hdr_entries));
+               if (ntohl(header.hdr_entries) < unpack_limit)
+                       do_keep = 0;
+               else
+                       do_keep = 1;
+       }
+
+       if (do_keep) {
+               if (pack_lockfile)
+                       cmd.out = -1;
+               *av++ = "index-pack";
+               *av++ = "--stdin";
+               if (!args.quiet && !args.no_progress)
+                       *av++ = "-v";
+               if (args.use_thin_pack)
+                       *av++ = "--fix-thin";
+               if (args.lock_pack || unpack_limit) {
+                       int s = sprintf(keep_arg,
+                                       "--keep=fetch-pack %d on ", getpid());
+                       if (gethostname(keep_arg + s, sizeof(keep_arg) - s))
+                               strcpy(keep_arg + s, "localhost");
+                       *av++ = keep_arg;
+               }
+       }
+       else {
+               *av++ = "unpack-objects";
+               if (args.quiet)
+                       *av++ = "-q";
+       }
+       if (*hdr_arg)
+               *av++ = hdr_arg;
+       *av++ = NULL;
+
+       cmd.in = demux.out;
+       cmd.git_cmd = 1;
+       if (start_command(&cmd))
+               die("fetch-pack: unable to fork off %s", argv[0]);
+       if (do_keep && pack_lockfile)
+               *pack_lockfile = index_pack_lockfile(cmd.out);
+
+       if (finish_command(&cmd))
+               die("%s failed", argv[0]);
+       if (use_sideband && finish_async(&demux))
+               die("error in sideband demultiplexer");
+       return 0;
+}
+
+static struct ref *do_fetch_pack(int fd[2],
+               int nr_match,
+               char **match,
+               char **pack_lockfile)
+{
+       struct ref *ref;
+       unsigned char sha1[20];
+
+       get_remote_heads(fd[0], &ref, 0, NULL, 0);
+       if (is_repository_shallow() && !server_supports("shallow"))
+               die("Server does not support shallow clients");
+       if (server_supports("multi_ack")) {
+               if (args.verbose)
+                       fprintf(stderr, "Server supports multi_ack\n");
+               multi_ack = 1;
+       }
+       if (server_supports("side-band-64k")) {
+               if (args.verbose)
+                       fprintf(stderr, "Server supports side-band-64k\n");
+               use_sideband = 2;
+       }
+       else if (server_supports("side-band")) {
+               if (args.verbose)
+                       fprintf(stderr, "Server supports side-band\n");
+               use_sideband = 1;
+       }
+       if (!ref) {
+               packet_flush(fd[1]);
+               die("no matching remote head");
+       }
+       if (everything_local(&ref, nr_match, match)) {
+               packet_flush(fd[1]);
+               goto all_done;
+       }
+       if (find_common(fd, sha1, ref) < 0)
+               if (!args.keep_pack)
+                       /* When cloning, it is not unusual to have
+                        * no common commit.
+                        */
+                       fprintf(stderr, "warning: no common commits\n");
+
+       if (get_pack(fd, pack_lockfile))
+               die("git-fetch-pack: fetch failed.");
+
+ all_done:
+       return ref;
+}
+
+static int remove_duplicates(int nr_heads, char **heads)
+{
+       int src, dst;
+
+       for (src = dst = 0; src < nr_heads; src++) {
+               /* If heads[src] is different from any of
+                * heads[0..dst], push it in.
+                */
+               int i;
+               for (i = 0; i < dst; i++) {
+                       if (!strcmp(heads[i], heads[src]))
+                               break;
+               }
+               if (i < dst)
+                       continue;
+               if (src != dst)
+                       heads[dst] = heads[src];
+               dst++;
+       }
+       return dst;
+}
+
+static int fetch_pack_config(const char *var, const char *value)
+{
+       if (strcmp(var, "fetch.unpacklimit") == 0) {
+               fetch_unpack_limit = git_config_int(var, value);
+               return 0;
+       }
+
+       if (strcmp(var, "transfer.unpacklimit") == 0) {
+               transfer_unpack_limit = git_config_int(var, value);
+               return 0;
+       }
+
+       return git_default_config(var, value);
+}
+
+static struct lock_file lock;
+
+static void fetch_pack_setup(void)
+{
+       static int did_setup;
+       if (did_setup)
+               return;
+       git_config(fetch_pack_config);
+       if (0 <= transfer_unpack_limit)
+               unpack_limit = transfer_unpack_limit;
+       else if (0 <= fetch_unpack_limit)
+               unpack_limit = fetch_unpack_limit;
+       did_setup = 1;
+}
+
+int cmd_fetch_pack(int argc, const char **argv, const char *prefix)
+{
+       int i, ret, nr_heads;
+       struct ref *ref;
+       char *dest = NULL, **heads;
+
+       nr_heads = 0;
+       heads = NULL;
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (*arg == '-') {
+                       if (!prefixcmp(arg, "--upload-pack=")) {
+                               args.uploadpack = arg + 14;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--exec=")) {
+                               args.uploadpack = arg + 7;
+                               continue;
+                       }
+                       if (!strcmp("--quiet", arg) || !strcmp("-q", arg)) {
+                               args.quiet = 1;
+                               continue;
+                       }
+                       if (!strcmp("--keep", arg) || !strcmp("-k", arg)) {
+                               args.lock_pack = args.keep_pack;
+                               args.keep_pack = 1;
+                               continue;
+                       }
+                       if (!strcmp("--thin", arg)) {
+                               args.use_thin_pack = 1;
+                               continue;
+                       }
+                       if (!strcmp("--all", arg)) {
+                               args.fetch_all = 1;
+                               continue;
+                       }
+                       if (!strcmp("-v", arg)) {
+                               args.verbose = 1;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--depth=")) {
+                               args.depth = strtol(arg + 8, NULL, 0);
+                               continue;
+                       }
+                       if (!strcmp("--no-progress", arg)) {
+                               args.no_progress = 1;
+                               continue;
+                       }
+                       usage(fetch_pack_usage);
+               }
+               dest = (char *)arg;
+               heads = (char **)(argv + i + 1);
+               nr_heads = argc - i - 1;
+               break;
+       }
+       if (!dest)
+               usage(fetch_pack_usage);
+
+       ref = fetch_pack(&args, dest, nr_heads, heads, NULL);
+       ret = !ref;
+
+       while (ref) {
+               printf("%s %s\n",
+                      sha1_to_hex(ref->old_sha1), ref->name);
+               ref = ref->next;
+       }
+
+       return ret;
+}
+
+struct ref *fetch_pack(struct fetch_pack_args *my_args,
+               const char *dest,
+               int nr_heads,
+               char **heads,
+               char **pack_lockfile)
+{
+       int i, ret;
+       int fd[2];
+       struct child_process *conn;
+       struct ref *ref;
+       struct stat st;
+
+       fetch_pack_setup();
+       memcpy(&args, my_args, sizeof(args));
+       if (args.depth > 0) {
+               if (stat(git_path("shallow"), &st))
+                       st.st_mtime = 0;
+       }
+
+       conn = git_connect(fd, (char *)dest, args.uploadpack,
+                          args.verbose ? CONNECT_VERBOSE : 0);
+       if (heads && nr_heads)
+               nr_heads = remove_duplicates(nr_heads, heads);
+       ref = do_fetch_pack(fd, nr_heads, heads, pack_lockfile);
+       close(fd[0]);
+       close(fd[1]);
+       ret = finish_connect(conn);
+
+       if (!ret && nr_heads) {
+               /* If the heads to pull were given, we should have
+                * consumed all of them by matching the remote.
+                * Otherwise, 'git-fetch remote no-such-ref' would
+                * silently succeed without issuing an error.
+                */
+               for (i = 0; i < nr_heads; i++)
+                       if (heads[i] && heads[i][0]) {
+                               error("no such remote ref %s", heads[i]);
+                               ret = 1;
+                       }
+       }
+
+       if (!ret && args.depth > 0) {
+               struct cache_time mtime;
+               char *shallow = git_path("shallow");
+               int fd;
+
+               mtime.sec = st.st_mtime;
+#ifdef USE_NSEC
+               mtime.usec = st.st_mtim.usec;
+#endif
+               if (stat(shallow, &st)) {
+                       if (mtime.sec)
+                               die("shallow file was removed during fetch");
+               } else if (st.st_mtime != mtime.sec
+#ifdef USE_NSEC
+                               || st.st_mtim.usec != mtime.usec
+#endif
+                         )
+                       die("shallow file was changed during fetch");
+
+               fd = hold_lock_file_for_update(&lock, shallow, 1);
+               if (!write_shallow_commits(fd, 0)) {
+                       unlink(shallow);
+                       rollback_lock_file(&lock);
+               } else {
+                       close(fd);
+                       commit_lock_file(&lock);
+               }
+       }
+
+       if (ret)
+               ref = NULL;
+
+       return ref;
+}
diff --git a/builtin-fetch.c b/builtin-fetch.c
new file mode 100644 (file)
index 0000000..320e235
--- /dev/null
@@ -0,0 +1,632 @@
+/*
+ * "git fetch"
+ */
+#include "cache.h"
+#include "refs.h"
+#include "commit.h"
+#include "builtin.h"
+#include "path-list.h"
+#include "remote.h"
+#include "transport.h"
+#include "run-command.h"
+#include "parse-options.h"
+
+static const char * const builtin_fetch_usage[] = {
+       "git-fetch [options] [<repository> <refspec>...]",
+       NULL
+};
+
+enum {
+       TAGS_UNSET = 0,
+       TAGS_DEFAULT = 1,
+       TAGS_SET = 2
+};
+
+static int append, force, keep, update_head_ok, verbose, quiet;
+static int tags = TAGS_DEFAULT;
+static const char *depth;
+static const char *upload_pack;
+static struct strbuf default_rla = STRBUF_INIT;
+static struct transport *transport;
+
+static struct option builtin_fetch_options[] = {
+       OPT__QUIET(&quiet),
+       OPT__VERBOSE(&verbose),
+       OPT_BOOLEAN('a', "append", &append,
+                   "append to .git/FETCH_HEAD instead of overwriting"),
+       OPT_STRING(0, "upload-pack", &upload_pack, "PATH",
+                  "path to upload pack on remote end"),
+       OPT_BOOLEAN('f', "force", &force,
+                   "force overwrite of local branch"),
+       OPT_SET_INT('t', "tags", &tags,
+                   "fetch all tags and associated objects", TAGS_SET),
+       OPT_BOOLEAN('k', "keep", &keep, "keep downloaded pack"),
+       OPT_BOOLEAN('u', "update-head-ok", &update_head_ok,
+                   "allow updating of HEAD ref"),
+       OPT_STRING(0, "depth", &depth, "DEPTH",
+                  "deepen history of shallow clone"),
+       OPT_END()
+};
+
+static void unlock_pack(void)
+{
+       if (transport)
+               transport_unlock_pack(transport);
+}
+
+static void unlock_pack_on_signal(int signo)
+{
+       unlock_pack();
+       signal(SIGINT, SIG_DFL);
+       raise(signo);
+}
+
+static void add_merge_config(struct ref **head,
+                          const struct ref *remote_refs,
+                          struct branch *branch,
+                          struct ref ***tail)
+{
+       int i;
+
+       for (i = 0; i < branch->merge_nr; i++) {
+               struct ref *rm, **old_tail = *tail;
+               struct refspec refspec;
+
+               for (rm = *head; rm; rm = rm->next) {
+                       if (branch_merge_matches(branch, i, rm->name)) {
+                               rm->merge = 1;
+                               break;
+                       }
+               }
+               if (rm)
+                       continue;
+
+               /*
+                * Not fetched to a tracking branch?  We need to fetch
+                * it anyway to allow this branch's "branch.$name.merge"
+                * to be honored by git-pull, but we do not have to
+                * fail if branch.$name.merge is misconfigured to point
+                * at a nonexisting branch.  If we were indeed called by
+                * git-pull, it will notice the misconfiguration because
+                * there is no entry in the resulting FETCH_HEAD marked
+                * for merging.
+                */
+               refspec.src = branch->merge[i]->src;
+               refspec.dst = NULL;
+               refspec.pattern = 0;
+               refspec.force = 0;
+               get_fetch_map(remote_refs, &refspec, tail, 1);
+               for (rm = *old_tail; rm; rm = rm->next)
+                       rm->merge = 1;
+       }
+}
+
+static struct ref *get_ref_map(struct transport *transport,
+                              struct refspec *refs, int ref_count, int tags,
+                              int *autotags)
+{
+       int i;
+       struct ref *rm;
+       struct ref *ref_map = NULL;
+       struct ref **tail = &ref_map;
+
+       const struct ref *remote_refs = transport_get_remote_refs(transport);
+
+       if (ref_count || tags == TAGS_SET) {
+               for (i = 0; i < ref_count; i++) {
+                       get_fetch_map(remote_refs, &refs[i], &tail, 0);
+                       if (refs[i].dst && refs[i].dst[0])
+                               *autotags = 1;
+               }
+               /* Merge everything on the command line, but not --tags */
+               for (rm = ref_map; rm; rm = rm->next)
+                       rm->merge = 1;
+               if (tags == TAGS_SET) {
+                       struct refspec refspec;
+                       refspec.src = "refs/tags/";
+                       refspec.dst = "refs/tags/";
+                       refspec.pattern = 1;
+                       refspec.force = 0;
+                       get_fetch_map(remote_refs, &refspec, &tail, 0);
+               }
+       } else {
+               /* Use the defaults */
+               struct remote *remote = transport->remote;
+               struct branch *branch = branch_get(NULL);
+               int has_merge = branch_has_merge_config(branch);
+               if (remote && (remote->fetch_refspec_nr || has_merge)) {
+                       for (i = 0; i < remote->fetch_refspec_nr; i++) {
+                               get_fetch_map(remote_refs, &remote->fetch[i], &tail, 0);
+                               if (remote->fetch[i].dst &&
+                                   remote->fetch[i].dst[0])
+                                       *autotags = 1;
+                               if (!i && !has_merge && ref_map &&
+                                   !remote->fetch[0].pattern)
+                                       ref_map->merge = 1;
+                       }
+                       /*
+                        * if the remote we're fetching from is the same
+                        * as given in branch.<name>.remote, we add the
+                        * ref given in branch.<name>.merge, too.
+                        */
+                       if (has_merge &&
+                           !strcmp(branch->remote_name, remote->name))
+                               add_merge_config(&ref_map, remote_refs, branch, &tail);
+               } else {
+                       ref_map = get_remote_ref(remote_refs, "HEAD");
+                       if (!ref_map)
+                               die("Couldn't find remote ref HEAD");
+                       ref_map->merge = 1;
+               }
+       }
+       ref_remove_duplicates(ref_map);
+
+       return ref_map;
+}
+
+static int s_update_ref(const char *action,
+                       struct ref *ref,
+                       int check_old)
+{
+       char msg[1024];
+       char *rla = getenv("GIT_REFLOG_ACTION");
+       static struct ref_lock *lock;
+
+       if (!rla)
+               rla = default_rla.buf;
+       snprintf(msg, sizeof(msg), "%s: %s", rla, action);
+       lock = lock_any_ref_for_update(ref->name,
+                                      check_old ? ref->old_sha1 : NULL, 0);
+       if (!lock)
+               return 1;
+       if (write_ref_sha1(lock, ref->new_sha1, msg) < 0)
+               return 1;
+       return 0;
+}
+
+#define SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
+#define REFCOL_WIDTH  10
+
+static int update_local_ref(struct ref *ref,
+                           const char *remote,
+                           int verbose,
+                           char *display)
+{
+       struct commit *current = NULL, *updated;
+       enum object_type type;
+       struct branch *current_branch = branch_get(NULL);
+       const char *pretty_ref = ref->name + (
+               !prefixcmp(ref->name, "refs/heads/") ? 11 :
+               !prefixcmp(ref->name, "refs/tags/") ? 10 :
+               !prefixcmp(ref->name, "refs/remotes/") ? 13 :
+               0);
+
+       *display = 0;
+       type = sha1_object_info(ref->new_sha1, NULL);
+       if (type < 0)
+               die("object %s not found", sha1_to_hex(ref->new_sha1));
+
+       if (!*ref->name) {
+               /* Not storing */
+               if (verbose)
+                       sprintf(display, "* branch %s -> FETCH_HEAD", remote);
+               return 0;
+       }
+
+       if (!hashcmp(ref->old_sha1, ref->new_sha1)) {
+               if (verbose)
+                       sprintf(display, "= %-*s %-*s -> %s", SUMMARY_WIDTH,
+                               "[up to date]", REFCOL_WIDTH, remote,
+                               pretty_ref);
+               return 0;
+       }
+
+       if (current_branch &&
+           !strcmp(ref->name, current_branch->name) &&
+           !(update_head_ok || is_bare_repository()) &&
+           !is_null_sha1(ref->old_sha1)) {
+               /*
+                * If this is the head, and it's not okay to update
+                * the head, and the old value of the head isn't empty...
+                */
+               sprintf(display, "! %-*s %-*s -> %s  (can't fetch in current branch)",
+                       SUMMARY_WIDTH, "[rejected]", REFCOL_WIDTH, remote,
+                       pretty_ref);
+               return 1;
+       }
+
+       if (!is_null_sha1(ref->old_sha1) &&
+           !prefixcmp(ref->name, "refs/tags/")) {
+               sprintf(display, "- %-*s %-*s -> %s",
+                       SUMMARY_WIDTH, "[tag update]", REFCOL_WIDTH, remote,
+                       pretty_ref);
+               return s_update_ref("updating tag", ref, 0);
+       }
+
+       current = lookup_commit_reference_gently(ref->old_sha1, 1);
+       updated = lookup_commit_reference_gently(ref->new_sha1, 1);
+       if (!current || !updated) {
+               const char *msg;
+               const char *what;
+               if (!strncmp(ref->name, "refs/tags/", 10)) {
+                       msg = "storing tag";
+                       what = "[new tag]";
+               }
+               else {
+                       msg = "storing head";
+                       what = "[new branch]";
+               }
+
+               sprintf(display, "* %-*s %-*s -> %s", SUMMARY_WIDTH, what,
+                       REFCOL_WIDTH, remote, pretty_ref);
+               return s_update_ref(msg, ref, 0);
+       }
+
+       if (in_merge_bases(current, &updated, 1)) {
+               char quickref[83];
+               strcpy(quickref, find_unique_abbrev(current->object.sha1, DEFAULT_ABBREV));
+               strcat(quickref, "..");
+               strcat(quickref, find_unique_abbrev(ref->new_sha1, DEFAULT_ABBREV));
+               sprintf(display, "  %-*s %-*s -> %s", SUMMARY_WIDTH, quickref,
+                       REFCOL_WIDTH, remote, pretty_ref);
+               return s_update_ref("fast forward", ref, 1);
+       } else if (force || ref->force) {
+               char quickref[84];
+               strcpy(quickref, find_unique_abbrev(current->object.sha1, DEFAULT_ABBREV));
+               strcat(quickref, "...");
+               strcat(quickref, find_unique_abbrev(ref->new_sha1, DEFAULT_ABBREV));
+               sprintf(display, "+ %-*s %-*s -> %s  (forced update)",
+                       SUMMARY_WIDTH, quickref, REFCOL_WIDTH, remote, pretty_ref);
+               return s_update_ref("forced-update", ref, 1);
+       } else {
+               sprintf(display, "! %-*s %-*s -> %s  (non fast forward)",
+                       SUMMARY_WIDTH, "[rejected]", REFCOL_WIDTH, remote,
+                       pretty_ref);
+               return 1;
+       }
+}
+
+static int store_updated_refs(const char *url, struct ref *ref_map)
+{
+       FILE *fp;
+       struct commit *commit;
+       int url_len, i, note_len, shown_url = 0;
+       char note[1024];
+       const char *what, *kind;
+       struct ref *rm;
+       char *filename = git_path("FETCH_HEAD");
+
+       fp = fopen(filename, "a");
+       if (!fp)
+               return error("cannot open %s: %s\n", filename, strerror(errno));
+       for (rm = ref_map; rm; rm = rm->next) {
+               struct ref *ref = NULL;
+
+               if (rm->peer_ref) {
+                       ref = xcalloc(1, sizeof(*ref) + strlen(rm->peer_ref->name) + 1);
+                       strcpy(ref->name, rm->peer_ref->name);
+                       hashcpy(ref->old_sha1, rm->peer_ref->old_sha1);
+                       hashcpy(ref->new_sha1, rm->old_sha1);
+                       ref->force = rm->peer_ref->force;
+               }
+
+               commit = lookup_commit_reference_gently(rm->old_sha1, 1);
+               if (!commit)
+                       rm->merge = 0;
+
+               if (!strcmp(rm->name, "HEAD")) {
+                       kind = "";
+                       what = "";
+               }
+               else if (!prefixcmp(rm->name, "refs/heads/")) {
+                       kind = "branch";
+                       what = rm->name + 11;
+               }
+               else if (!prefixcmp(rm->name, "refs/tags/")) {
+                       kind = "tag";
+                       what = rm->name + 10;
+               }
+               else if (!prefixcmp(rm->name, "refs/remotes/")) {
+                       kind = "remote branch";
+                       what = rm->name + 13;
+               }
+               else {
+                       kind = "";
+                       what = rm->name;
+               }
+
+               url_len = strlen(url);
+               for (i = url_len - 1; url[i] == '/' && 0 <= i; i--)
+                       ;
+               url_len = i + 1;
+               if (4 < i && !strncmp(".git", url + i - 3, 4))
+                       url_len = i - 3;
+
+               note_len = 0;
+               if (*what) {
+                       if (*kind)
+                               note_len += sprintf(note + note_len, "%s ",
+                                                   kind);
+                       note_len += sprintf(note + note_len, "'%s' of ", what);
+               }
+               note_len += sprintf(note + note_len, "%.*s", url_len, url);
+               fprintf(fp, "%s\t%s\t%s\n",
+                       sha1_to_hex(commit ? commit->object.sha1 :
+                                   rm->old_sha1),
+                       rm->merge ? "" : "not-for-merge",
+                       note);
+
+               if (ref) {
+                       update_local_ref(ref, what, verbose, note);
+                       if (*note) {
+                               if (!shown_url) {
+                                       fprintf(stderr, "From %.*s\n",
+                                                       url_len, url);
+                                       shown_url = 1;
+                               }
+                               fprintf(stderr, " %s\n", note);
+                       }
+               }
+       }
+       fclose(fp);
+       return 0;
+}
+
+/*
+ * We would want to bypass the object transfer altogether if
+ * everything we are going to fetch already exists and connected
+ * locally.
+ *
+ * The refs we are going to fetch are in to_fetch (nr_heads in
+ * total).  If running
+ *
+ *  $ git-rev-list --objects to_fetch[0] to_fetch[1] ... --not --all
+ *
+ * does not error out, that means everything reachable from the
+ * refs we are going to fetch exists and is connected to some of
+ * our existing refs.
+ */
+static int quickfetch(struct ref *ref_map)
+{
+       struct child_process revlist;
+       struct ref *ref;
+       char **argv;
+       int i, err;
+
+       /*
+        * If we are deepening a shallow clone we already have these
+        * objects reachable.  Running rev-list here will return with
+        * a good (0) exit status and we'll bypass the fetch that we
+        * really need to perform.  Claiming failure now will ensure
+        * we perform the network exchange to deepen our history.
+        */
+       if (depth)
+               return -1;
+
+       for (i = 0, ref = ref_map; ref; ref = ref->next)
+               i++;
+       if (!i)
+               return 0;
+
+       argv = xmalloc(sizeof(*argv) * (i + 6));
+       i = 0;
+       argv[i++] = xstrdup("rev-list");
+       argv[i++] = xstrdup("--quiet");
+       argv[i++] = xstrdup("--objects");
+       for (ref = ref_map; ref; ref = ref->next)
+               argv[i++] = xstrdup(sha1_to_hex(ref->old_sha1));
+       argv[i++] = xstrdup("--not");
+       argv[i++] = xstrdup("--all");
+       argv[i++] = NULL;
+
+       memset(&revlist, 0, sizeof(revlist));
+       revlist.argv = (const char**)argv;
+       revlist.git_cmd = 1;
+       revlist.no_stdin = 1;
+       revlist.no_stdout = 1;
+       revlist.no_stderr = 1;
+       err = run_command(&revlist);
+
+       for (i = 0; argv[i]; i++)
+               free(argv[i]);
+       free(argv);
+       return err;
+}
+
+static int fetch_refs(struct transport *transport, struct ref *ref_map)
+{
+       int ret = quickfetch(ref_map);
+       if (ret)
+               ret = transport_fetch_refs(transport, ref_map);
+       if (!ret)
+               ret |= store_updated_refs(transport->url, ref_map);
+       transport_unlock_pack(transport);
+       return ret;
+}
+
+static int add_existing(const char *refname, const unsigned char *sha1,
+                       int flag, void *cbdata)
+{
+       struct path_list *list = (struct path_list *)cbdata;
+       path_list_insert(refname, list);
+       return 0;
+}
+
+static struct ref *find_non_local_tags(struct transport *transport,
+                                      struct ref *fetch_map)
+{
+       static struct path_list existing_refs = { NULL, 0, 0, 0 };
+       struct path_list new_refs = { NULL, 0, 0, 1 };
+       char *ref_name;
+       int ref_name_len;
+       const unsigned char *ref_sha1;
+       const struct ref *tag_ref;
+       struct ref *rm = NULL;
+       struct ref *ref_map = NULL;
+       struct ref **tail = &ref_map;
+       const struct ref *ref;
+
+       for_each_ref(add_existing, &existing_refs);
+       for (ref = transport_get_remote_refs(transport); ref; ref = ref->next) {
+               if (prefixcmp(ref->name, "refs/tags"))
+                       continue;
+
+               ref_name = xstrdup(ref->name);
+               ref_name_len = strlen(ref_name);
+               ref_sha1 = ref->old_sha1;
+
+               if (!strcmp(ref_name + ref_name_len - 3, "^{}")) {
+                       ref_name[ref_name_len - 3] = 0;
+                       tag_ref = transport_get_remote_refs(transport);
+                       while (tag_ref) {
+                               if (!strcmp(tag_ref->name, ref_name)) {
+                                       ref_sha1 = tag_ref->old_sha1;
+                                       break;
+                               }
+                               tag_ref = tag_ref->next;
+                       }
+               }
+
+               if (!path_list_has_path(&existing_refs, ref_name) &&
+                   !path_list_has_path(&new_refs, ref_name) &&
+                   has_sha1_file(ref->old_sha1)) {
+                       path_list_insert(ref_name, &new_refs);
+
+                       rm = alloc_ref(strlen(ref_name) + 1);
+                       strcpy(rm->name, ref_name);
+                       rm->peer_ref = alloc_ref(strlen(ref_name) + 1);
+                       strcpy(rm->peer_ref->name, ref_name);
+                       hashcpy(rm->old_sha1, ref_sha1);
+
+                       *tail = rm;
+                       tail = &rm->next;
+               }
+               free(ref_name);
+       }
+
+       return ref_map;
+}
+
+static int do_fetch(struct transport *transport,
+                   struct refspec *refs, int ref_count)
+{
+       struct ref *ref_map, *fetch_map;
+       struct ref *rm;
+       int autotags = (transport->remote->fetch_tags == 1);
+       if (transport->remote->fetch_tags == 2 && tags != TAGS_UNSET)
+               tags = TAGS_SET;
+       if (transport->remote->fetch_tags == -1)
+               tags = TAGS_UNSET;
+
+       if (!transport->get_refs_list || !transport->fetch)
+               die("Don't know how to fetch from %s", transport->url);
+
+       /* if not appending, truncate FETCH_HEAD */
+       if (!append) {
+               char *filename = git_path("FETCH_HEAD");
+               FILE *fp = fopen(filename, "w");
+               if (!fp)
+                       return error("cannot open %s: %s\n", filename, strerror(errno));
+               fclose(fp);
+       }
+
+       ref_map = get_ref_map(transport, refs, ref_count, tags, &autotags);
+
+       for (rm = ref_map; rm; rm = rm->next) {
+               if (rm->peer_ref)
+                       read_ref(rm->peer_ref->name, rm->peer_ref->old_sha1);
+       }
+
+       if (fetch_refs(transport, ref_map)) {
+               free_refs(ref_map);
+               return 1;
+       }
+
+       fetch_map = ref_map;
+
+       /* if neither --no-tags nor --tags was specified, do automated tag
+        * following ... */
+       if (tags == TAGS_DEFAULT && autotags) {
+               ref_map = find_non_local_tags(transport, fetch_map);
+               if (ref_map) {
+                       transport_set_option(transport, TRANS_OPT_DEPTH, "0");
+                       fetch_refs(transport, ref_map);
+               }
+               free_refs(ref_map);
+       }
+
+       free_refs(fetch_map);
+
+       return 0;
+}
+
+static void set_option(const char *name, const char *value)
+{
+       int r = transport_set_option(transport, name, value);
+       if (r < 0)
+               die("Option \"%s\" value \"%s\" is not valid for %s\n",
+                       name, value, transport->url);
+       if (r > 0)
+               warning("Option \"%s\" is ignored for %s\n",
+                       name, transport->url);
+}
+
+int cmd_fetch(int argc, const char **argv, const char *prefix)
+{
+       struct remote *remote;
+       int i;
+       static const char **refs = NULL;
+       int ref_nr = 0;
+
+       /* Record the command line for the reflog */
+       strbuf_addstr(&default_rla, "fetch");
+       for (i = 1; i < argc; i++)
+               strbuf_addf(&default_rla, " %s", argv[i]);
+
+       argc = parse_options(argc, argv,
+                            builtin_fetch_options, builtin_fetch_usage, 0);
+
+       if (argc == 0)
+               remote = remote_get(NULL);
+       else
+               remote = remote_get(argv[0]);
+
+       transport = transport_get(remote, remote->url[0]);
+       if (verbose >= 2)
+               transport->verbose = 1;
+       if (quiet)
+               transport->verbose = -1;
+       if (upload_pack)
+               set_option(TRANS_OPT_UPLOADPACK, upload_pack);
+       if (keep)
+               set_option(TRANS_OPT_KEEP, "yes");
+       if (depth)
+               set_option(TRANS_OPT_DEPTH, depth);
+
+       if (!transport->url)
+               die("Where do you want to fetch from today?");
+
+       if (argc > 1) {
+               int j = 0;
+               refs = xcalloc(argc + 1, sizeof(const char *));
+               for (i = 1; i < argc; i++) {
+                       if (!strcmp(argv[i], "tag")) {
+                               char *ref;
+                               i++;
+                               ref = xmalloc(strlen(argv[i]) * 2 + 22);
+                               strcpy(ref, "refs/tags/");
+                               strcat(ref, argv[i]);
+                               strcat(ref, ":refs/tags/");
+                               strcat(ref, argv[i]);
+                               refs[j++] = ref;
+                       } else
+                               refs[j++] = argv[i];
+               }
+               refs[j] = NULL;
+               ref_nr = j;
+       }
+
+       signal(SIGINT, unlock_pack_on_signal);
+       atexit(unlock_pack);
+       return do_fetch(transport, parse_ref_spec(ref_nr, refs), ref_nr);
+}
diff --git a/builtin-fmt-merge-msg.c b/builtin-fmt-merge-msg.c
new file mode 100644 (file)
index 0000000..6163bd4
--- /dev/null
@@ -0,0 +1,354 @@
+#include "builtin.h"
+#include "cache.h"
+#include "commit.h"
+#include "diff.h"
+#include "revision.h"
+#include "tag.h"
+
+static const char *fmt_merge_msg_usage =
+       "git-fmt-merge-msg [--summary] [--no-summary] [--file <file>]";
+
+static int merge_summary;
+
+static int fmt_merge_msg_config(const char *key, const char *value)
+{
+       if (!strcmp("merge.summary", key))
+               merge_summary = git_config_bool(key, value);
+       return 0;
+}
+
+struct list {
+       char **list;
+       void **payload;
+       unsigned nr, alloc;
+};
+
+static void append_to_list(struct list *list, char *value, void *payload)
+{
+       if (list->nr == list->alloc) {
+               list->alloc += 32;
+               list->list = xrealloc(list->list, sizeof(char *) * list->alloc);
+               list->payload = xrealloc(list->payload,
+                               sizeof(char *) * list->alloc);
+       }
+       list->payload[list->nr] = payload;
+       list->list[list->nr++] = value;
+}
+
+static int find_in_list(struct list *list, char *value)
+{
+       int i;
+
+       for (i = 0; i < list->nr; i++)
+               if (!strcmp(list->list[i], value))
+                       return i;
+
+       return -1;
+}
+
+static void free_list(struct list *list)
+{
+       int i;
+
+       if (list->alloc == 0)
+               return;
+
+       for (i = 0; i < list->nr; i++) {
+               free(list->list[i]);
+               free(list->payload[i]);
+       }
+       free(list->list);
+       free(list->payload);
+       list->nr = list->alloc = 0;
+}
+
+struct src_data {
+       struct list branch, tag, r_branch, generic;
+       int head_status;
+};
+
+static struct list srcs = { NULL, NULL, 0, 0};
+static struct list origins = { NULL, NULL, 0, 0};
+
+static int handle_line(char *line)
+{
+       int i, len = strlen(line);
+       unsigned char *sha1;
+       char *src, *origin;
+       struct src_data *src_data;
+       int pulling_head = 0;
+
+       if (len < 43 || line[40] != '\t')
+               return 1;
+
+       if (!prefixcmp(line + 41, "not-for-merge"))
+               return 0;
+
+       if (line[41] != '\t')
+               return 2;
+
+       line[40] = 0;
+       sha1 = xmalloc(20);
+       i = get_sha1(line, sha1);
+       line[40] = '\t';
+       if (i)
+               return 3;
+
+       if (line[len - 1] == '\n')
+               line[len - 1] = 0;
+       line += 42;
+
+       src = strstr(line, " of ");
+       if (src) {
+               *src = 0;
+               src += 4;
+               pulling_head = 0;
+       } else {
+               src = line;
+               pulling_head = 1;
+       }
+
+       i = find_in_list(&srcs, src);
+       if (i < 0) {
+               i = srcs.nr;
+               append_to_list(&srcs, xstrdup(src),
+                               xcalloc(1, sizeof(struct src_data)));
+       }
+       src_data = srcs.payload[i];
+
+       if (pulling_head) {
+               origin = xstrdup(src);
+               src_data->head_status |= 1;
+       } else if (!prefixcmp(line, "branch ")) {
+               origin = xstrdup(line + 7);
+               append_to_list(&src_data->branch, origin, NULL);
+               src_data->head_status |= 2;
+       } else if (!prefixcmp(line, "tag ")) {
+               origin = line;
+               append_to_list(&src_data->tag, xstrdup(origin + 4), NULL);
+               src_data->head_status |= 2;
+       } else if (!prefixcmp(line, "remote branch ")) {
+               origin = xstrdup(line + 14);
+               append_to_list(&src_data->r_branch, origin, NULL);
+               src_data->head_status |= 2;
+       } else {
+               origin = xstrdup(src);
+               append_to_list(&src_data->generic, xstrdup(line), NULL);
+               src_data->head_status |= 2;
+       }
+
+       if (!strcmp(".", src) || !strcmp(src, origin)) {
+               int len = strlen(origin);
+               if (origin[0] == '\'' && origin[len - 1] == '\'') {
+                       origin = xmemdupz(origin + 1, len - 2);
+               } else {
+                       origin = xstrdup(origin);
+               }
+       } else {
+               char *new_origin = xmalloc(strlen(origin) + strlen(src) + 5);
+               sprintf(new_origin, "%s of %s", origin, src);
+               origin = new_origin;
+       }
+       append_to_list(&origins, origin, sha1);
+       return 0;
+}
+
+static void print_joined(const char *singular, const char *plural,
+               struct list *list)
+{
+       if (list->nr == 0)
+               return;
+       if (list->nr == 1) {
+               printf("%s%s", singular, list->list[0]);
+       } else {
+               int i;
+               printf("%s", plural);
+               for (i = 0; i < list->nr - 1; i++)
+                       printf("%s%s", i > 0 ? ", " : "", list->list[i]);
+               printf(" and %s", list->list[list->nr - 1]);
+       }
+}
+
+static void shortlog(const char *name, unsigned char *sha1,
+               struct commit *head, struct rev_info *rev, int limit)
+{
+       int i, count = 0;
+       struct commit *commit;
+       struct object *branch;
+       struct list subjects = { NULL, NULL, 0, 0 };
+       int flags = UNINTERESTING | TREESAME | SEEN | SHOWN | ADDED;
+
+       branch = deref_tag(parse_object(sha1), sha1_to_hex(sha1), 40);
+       if (!branch || branch->type != OBJ_COMMIT)
+               return;
+
+       setup_revisions(0, NULL, rev, NULL);
+       rev->ignore_merges = 1;
+       add_pending_object(rev, branch, name);
+       add_pending_object(rev, &head->object, "^HEAD");
+       head->object.flags |= UNINTERESTING;
+       prepare_revision_walk(rev);
+       while ((commit = get_revision(rev)) != NULL) {
+               char *oneline, *bol, *eol;
+
+               /* ignore merges */
+               if (commit->parents && commit->parents->next)
+                       continue;
+
+               count++;
+               if (subjects.nr > limit)
+                       continue;
+
+               bol = strstr(commit->buffer, "\n\n");
+               if (!bol) {
+                       append_to_list(&subjects, xstrdup(sha1_to_hex(
+                                                       commit->object.sha1)),
+                                       NULL);
+                       continue;
+               }
+
+               bol += 2;
+               eol = strchr(bol, '\n');
+               if (eol) {
+                       oneline = xmemdupz(bol, eol - bol);
+               } else {
+                       oneline = xstrdup(bol);
+               }
+               append_to_list(&subjects, oneline, NULL);
+       }
+
+       if (count > limit)
+               printf("\n* %s: (%d commits)\n", name, count);
+       else
+               printf("\n* %s:\n", name);
+
+       for (i = 0; i < subjects.nr; i++)
+               if (i >= limit)
+                       printf("  ...\n");
+               else
+                       printf("  %s\n", subjects.list[i]);
+
+       clear_commit_marks((struct commit *)branch, flags);
+       clear_commit_marks(head, flags);
+       free_commit_list(rev->commits);
+       rev->commits = NULL;
+       rev->pending.nr = 0;
+
+       free_list(&subjects);
+}
+
+int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)
+{
+       int limit = 20, i = 0;
+       char line[1024];
+       FILE *in = stdin;
+       const char *sep = "";
+       unsigned char head_sha1[20];
+       const char *current_branch;
+
+       git_config(fmt_merge_msg_config);
+
+       while (argc > 1) {
+               if (!strcmp(argv[1], "--summary"))
+                       merge_summary = 1;
+               else if (!strcmp(argv[1], "--no-summary"))
+                       merge_summary = 0;
+               else if (!strcmp(argv[1], "-F") || !strcmp(argv[1], "--file")) {
+                       if (argc < 3)
+                               die ("Which file?");
+                       if (!strcmp(argv[2], "-"))
+                               in = stdin;
+                       else {
+                               fclose(in);
+                               in = fopen(argv[2], "r");
+                               if (!in)
+                                       die("cannot open %s", argv[2]);
+                       }
+                       argc--; argv++;
+               } else
+                       break;
+               argc--; argv++;
+       }
+
+       if (argc > 1)
+               usage(fmt_merge_msg_usage);
+
+       /* get current branch */
+       current_branch = resolve_ref("HEAD", head_sha1, 1, NULL);
+       if (!current_branch)
+               die("No current branch");
+       if (!prefixcmp(current_branch, "refs/heads/"))
+               current_branch += 11;
+
+       while (fgets(line, sizeof(line), in)) {
+               i++;
+               if (line[0] == 0)
+                       continue;
+               if (handle_line(line))
+                       die ("Error in line %d: %s", i, line);
+       }
+
+       printf("Merge ");
+       for (i = 0; i < srcs.nr; i++) {
+               struct src_data *src_data = srcs.payload[i];
+               const char *subsep = "";
+
+               printf(sep);
+               sep = "; ";
+
+               if (src_data->head_status == 1) {
+                       printf(srcs.list[i]);
+                       continue;
+               }
+               if (src_data->head_status == 3) {
+                       subsep = ", ";
+                       printf("HEAD");
+               }
+               if (src_data->branch.nr) {
+                       printf(subsep);
+                       subsep = ", ";
+                       print_joined("branch ", "branches ", &src_data->branch);
+               }
+               if (src_data->r_branch.nr) {
+                       printf(subsep);
+                       subsep = ", ";
+                       print_joined("remote branch ", "remote branches ",
+                                       &src_data->r_branch);
+               }
+               if (src_data->tag.nr) {
+                       printf(subsep);
+                       subsep = ", ";
+                       print_joined("tag ", "tags ", &src_data->tag);
+               }
+               if (src_data->generic.nr) {
+                       printf(subsep);
+                       print_joined("commit ", "commits ", &src_data->generic);
+               }
+               if (strcmp(".", srcs.list[i]))
+                       printf(" of %s", srcs.list[i]);
+       }
+
+       if (!strcmp("master", current_branch))
+               putchar('\n');
+       else
+               printf(" into %s\n", current_branch);
+
+       if (merge_summary) {
+               struct commit *head;
+               struct rev_info rev;
+
+               head = lookup_commit(head_sha1);
+               init_revisions(&rev, prefix);
+               rev.commit_format = CMIT_FMT_ONELINE;
+               rev.ignore_merges = 1;
+               rev.limited = 1;
+
+               for (i = 0; i < origins.nr; i++)
+                       shortlog(origins.list[i], origins.payload[i],
+                                       head, &rev, limit);
+       }
+
+       /* No cleanup yet; is standalone anyway */
+
+       return 0;
+}
diff --git a/builtin-for-each-ref.c b/builtin-for-each-ref.c
new file mode 100644 (file)
index 0000000..f36a43c
--- /dev/null
@@ -0,0 +1,894 @@
+#include "builtin.h"
+#include "cache.h"
+#include "refs.h"
+#include "object.h"
+#include "tag.h"
+#include "commit.h"
+#include "tree.h"
+#include "blob.h"
+#include "quote.h"
+#include "parse-options.h"
+
+/* Quoting styles */
+#define QUOTE_NONE 0
+#define QUOTE_SHELL 1
+#define QUOTE_PERL 2
+#define QUOTE_PYTHON 4
+#define QUOTE_TCL 8
+
+typedef enum { FIELD_STR, FIELD_ULONG, FIELD_TIME } cmp_type;
+
+struct atom_value {
+       const char *s;
+       unsigned long ul; /* used for sorting when not FIELD_STR */
+};
+
+struct ref_sort {
+       struct ref_sort *next;
+       int atom; /* index into used_atom array */
+       unsigned reverse : 1;
+};
+
+struct refinfo {
+       char *refname;
+       unsigned char objectname[20];
+       struct atom_value *value;
+};
+
+static struct {
+       const char *name;
+       cmp_type cmp_type;
+} valid_atom[] = {
+       { "refname" },
+       { "objecttype" },
+       { "objectsize", FIELD_ULONG },
+       { "objectname" },
+       { "tree" },
+       { "parent" },
+       { "numparent", FIELD_ULONG },
+       { "object" },
+       { "type" },
+       { "tag" },
+       { "author" },
+       { "authorname" },
+       { "authoremail" },
+       { "authordate", FIELD_TIME },
+       { "committer" },
+       { "committername" },
+       { "committeremail" },
+       { "committerdate", FIELD_TIME },
+       { "tagger" },
+       { "taggername" },
+       { "taggeremail" },
+       { "taggerdate", FIELD_TIME },
+       { "creator" },
+       { "creatordate", FIELD_TIME },
+       { "subject" },
+       { "body" },
+       { "contents" },
+};
+
+/*
+ * An atom is a valid field atom listed above, possibly prefixed with
+ * a "*" to denote deref_tag().
+ *
+ * We parse given format string and sort specifiers, and make a list
+ * of properties that we need to extract out of objects.  refinfo
+ * structure will hold an array of values extracted that can be
+ * indexed with the "atom number", which is an index into this
+ * array.
+ */
+static const char **used_atom;
+static cmp_type *used_atom_type;
+static int used_atom_cnt, sort_atom_limit, need_tagged;
+
+/*
+ * Used to parse format string and sort specifiers
+ */
+static int parse_atom(const char *atom, const char *ep)
+{
+       const char *sp;
+       int i, at;
+
+       sp = atom;
+       if (*sp == '*' && sp < ep)
+               sp++; /* deref */
+       if (ep <= sp)
+               die("malformed field name: %.*s", (int)(ep-atom), atom);
+
+       /* Do we have the atom already used elsewhere? */
+       for (i = 0; i < used_atom_cnt; i++) {
+               int len = strlen(used_atom[i]);
+               if (len == ep - atom && !memcmp(used_atom[i], atom, len))
+                       return i;
+       }
+
+       /* Is the atom a valid one? */
+       for (i = 0; i < ARRAY_SIZE(valid_atom); i++) {
+               int len = strlen(valid_atom[i].name);
+               /*
+                * If the atom name has a colon, strip it and everything after
+                * it off - it specifies the format for this entry, and
+                * shouldn't be used for checking against the valid_atom
+                * table.
+                */
+               const char *formatp = strchr(sp, ':');
+               if (!formatp || ep < formatp)
+                       formatp = ep;
+               if (len == formatp - sp && !memcmp(valid_atom[i].name, sp, len))
+                       break;
+       }
+
+       if (ARRAY_SIZE(valid_atom) <= i)
+               die("unknown field name: %.*s", (int)(ep-atom), atom);
+
+       /* Add it in, including the deref prefix */
+       at = used_atom_cnt;
+       used_atom_cnt++;
+       used_atom = xrealloc(used_atom,
+                            (sizeof *used_atom) * used_atom_cnt);
+       used_atom_type = xrealloc(used_atom_type,
+                                 (sizeof(*used_atom_type) * used_atom_cnt));
+       used_atom[at] = xmemdupz(atom, ep - atom);
+       used_atom_type[at] = valid_atom[i].cmp_type;
+       return at;
+}
+
+/*
+ * In a format string, find the next occurrence of %(atom).
+ */
+static const char *find_next(const char *cp)
+{
+       while (*cp) {
+               if (*cp == '%') {
+                       /* %( is the start of an atom;
+                        * %% is a quoted per-cent.
+                        */
+                       if (cp[1] == '(')
+                               return cp;
+                       else if (cp[1] == '%')
+                               cp++; /* skip over two % */
+                       /* otherwise this is a singleton, literal % */
+               }
+               cp++;
+       }
+       return NULL;
+}
+
+/*
+ * Make sure the format string is well formed, and parse out
+ * the used atoms.
+ */
+static int verify_format(const char *format)
+{
+       const char *cp, *sp;
+       for (cp = format; *cp && (sp = find_next(cp)); ) {
+               const char *ep = strchr(sp, ')');
+               if (!ep)
+                       return error("malformatted format string %s", sp);
+               /* sp points at "%(" and ep points at the closing ")" */
+               parse_atom(sp + 2, ep);
+               cp = ep + 1;
+       }
+       return 0;
+}
+
+/*
+ * Given an object name, read the object data and size, and return a
+ * "struct object".  If the object data we are returning is also borrowed
+ * by the "struct object" representation, set *eaten as well---it is a
+ * signal from parse_object_buffer to us not to free the buffer.
+ */
+static void *get_obj(const unsigned char *sha1, struct object **obj, unsigned long *sz, int *eaten)
+{
+       enum object_type type;
+       void *buf = read_sha1_file(sha1, &type, sz);
+
+       if (buf)
+               *obj = parse_object_buffer(sha1, type, *sz, buf, eaten);
+       else
+               *obj = NULL;
+       return buf;
+}
+
+/* See grab_values */
+static void grab_common_values(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz)
+{
+       int i;
+
+       for (i = 0; i < used_atom_cnt; i++) {
+               const char *name = used_atom[i];
+               struct atom_value *v = &val[i];
+               if (!!deref != (*name == '*'))
+                       continue;
+               if (deref)
+                       name++;
+               if (!strcmp(name, "objecttype"))
+                       v->s = typename(obj->type);
+               else if (!strcmp(name, "objectsize")) {
+                       char *s = xmalloc(40);
+                       sprintf(s, "%lu", sz);
+                       v->ul = sz;
+                       v->s = s;
+               }
+               else if (!strcmp(name, "objectname")) {
+                       char *s = xmalloc(41);
+                       strcpy(s, sha1_to_hex(obj->sha1));
+                       v->s = s;
+               }
+       }
+}
+
+/* See grab_values */
+static void grab_tag_values(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz)
+{
+       int i;
+       struct tag *tag = (struct tag *) obj;
+
+       for (i = 0; i < used_atom_cnt; i++) {
+               const char *name = used_atom[i];
+               struct atom_value *v = &val[i];
+               if (!!deref != (*name == '*'))
+                       continue;
+               if (deref)
+                       name++;
+               if (!strcmp(name, "tag"))
+                       v->s = tag->tag;
+       }
+}
+
+static int num_parents(struct commit *commit)
+{
+       struct commit_list *parents;
+       int i;
+
+       for (i = 0, parents = commit->parents;
+            parents;
+            parents = parents->next)
+               i++;
+       return i;
+}
+
+/* See grab_values */
+static void grab_commit_values(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz)
+{
+       int i;
+       struct commit *commit = (struct commit *) obj;
+
+       for (i = 0; i < used_atom_cnt; i++) {
+               const char *name = used_atom[i];
+               struct atom_value *v = &val[i];
+               if (!!deref != (*name == '*'))
+                       continue;
+               if (deref)
+                       name++;
+               if (!strcmp(name, "tree")) {
+                       char *s = xmalloc(41);
+                       strcpy(s, sha1_to_hex(commit->tree->object.sha1));
+                       v->s = s;
+               }
+               if (!strcmp(name, "numparent")) {
+                       char *s = xmalloc(40);
+                       v->ul = num_parents(commit);
+                       sprintf(s, "%lu", v->ul);
+                       v->s = s;
+               }
+               else if (!strcmp(name, "parent")) {
+                       int num = num_parents(commit);
+                       int i;
+                       struct commit_list *parents;
+                       char *s = xmalloc(41 * num + 1);
+                       v->s = s;
+                       for (i = 0, parents = commit->parents;
+                            parents;
+                            parents = parents->next, i = i + 41) {
+                               struct commit *parent = parents->item;
+                               strcpy(s+i, sha1_to_hex(parent->object.sha1));
+                               if (parents->next)
+                                       s[i+40] = ' ';
+                       }
+                       if (!i)
+                               *s = '\0';
+               }
+       }
+}
+
+static const char *find_wholine(const char *who, int wholen, const char *buf, unsigned long sz)
+{
+       const char *eol;
+       while (*buf) {
+               if (!strncmp(buf, who, wholen) &&
+                   buf[wholen] == ' ')
+                       return buf + wholen + 1;
+               eol = strchr(buf, '\n');
+               if (!eol)
+                       return "";
+               eol++;
+               if (*eol == '\n')
+                       return ""; /* end of header */
+               buf = eol;
+       }
+       return "";
+}
+
+static const char *copy_line(const char *buf)
+{
+       const char *eol = strchr(buf, '\n');
+       if (!eol)
+               return "";
+       return xmemdupz(buf, eol - buf);
+}
+
+static const char *copy_name(const char *buf)
+{
+       const char *cp;
+       for (cp = buf; *cp && *cp != '\n'; cp++) {
+               if (!strncmp(cp, " <", 2))
+                       return xmemdupz(buf, cp - buf);
+       }
+       return "";
+}
+
+static const char *copy_email(const char *buf)
+{
+       const char *email = strchr(buf, '<');
+       const char *eoemail = strchr(email, '>');
+       if (!email || !eoemail)
+               return "";
+       return xmemdupz(email, eoemail + 1 - email);
+}
+
+static void grab_date(const char *buf, struct atom_value *v, const char *atomname)
+{
+       const char *eoemail = strstr(buf, "> ");
+       char *zone;
+       unsigned long timestamp;
+       long tz;
+       enum date_mode date_mode = DATE_NORMAL;
+       const char *formatp;
+
+       /*
+        * We got here because atomname ends in "date" or "date<something>";
+        * it's not possible that <something> is not ":<format>" because
+        * parse_atom() wouldn't have allowed it, so we can assume that no
+        * ":" means no format is specified, and use the default.
+        */
+       formatp = strchr(atomname, ':');
+       if (formatp != NULL) {
+               formatp++;
+               date_mode = parse_date_format(formatp);
+       }
+
+       if (!eoemail)
+               goto bad;
+       timestamp = strtoul(eoemail + 2, &zone, 10);
+       if (timestamp == ULONG_MAX)
+               goto bad;
+       tz = strtol(zone, NULL, 10);
+       if ((tz == LONG_MIN || tz == LONG_MAX) && errno == ERANGE)
+               goto bad;
+       v->s = xstrdup(show_date(timestamp, tz, date_mode));
+       v->ul = timestamp;
+       return;
+ bad:
+       v->s = "";
+       v->ul = 0;
+}
+
+/* See grab_values */
+static void grab_person(const char *who, struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz)
+{
+       int i;
+       int wholen = strlen(who);
+       const char *wholine = NULL;
+
+       for (i = 0; i < used_atom_cnt; i++) {
+               const char *name = used_atom[i];
+               struct atom_value *v = &val[i];
+               if (!!deref != (*name == '*'))
+                       continue;
+               if (deref)
+                       name++;
+               if (strncmp(who, name, wholen))
+                       continue;
+               if (name[wholen] != 0 &&
+                   strcmp(name + wholen, "name") &&
+                   strcmp(name + wholen, "email") &&
+                   prefixcmp(name + wholen, "date"))
+                       continue;
+               if (!wholine)
+                       wholine = find_wholine(who, wholen, buf, sz);
+               if (!wholine)
+                       return; /* no point looking for it */
+               if (name[wholen] == 0)
+                       v->s = copy_line(wholine);
+               else if (!strcmp(name + wholen, "name"))
+                       v->s = copy_name(wholine);
+               else if (!strcmp(name + wholen, "email"))
+                       v->s = copy_email(wholine);
+               else if (!prefixcmp(name + wholen, "date"))
+                       grab_date(wholine, v, name);
+       }
+
+       /* For a tag or a commit object, if "creator" or "creatordate" is
+        * requested, do something special.
+        */
+       if (strcmp(who, "tagger") && strcmp(who, "committer"))
+               return; /* "author" for commit object is not wanted */
+       if (!wholine)
+               wholine = find_wholine(who, wholen, buf, sz);
+       if (!wholine)
+               return;
+       for (i = 0; i < used_atom_cnt; i++) {
+               const char *name = used_atom[i];
+               struct atom_value *v = &val[i];
+               if (!!deref != (*name == '*'))
+                       continue;
+               if (deref)
+                       name++;
+
+               if (!prefixcmp(name, "creatordate"))
+                       grab_date(wholine, v, name);
+               else if (!strcmp(name, "creator"))
+                       v->s = copy_line(wholine);
+       }
+}
+
+static void find_subpos(const char *buf, unsigned long sz, const char **sub, const char **body)
+{
+       while (*buf) {
+               const char *eol = strchr(buf, '\n');
+               if (!eol)
+                       return;
+               if (eol[1] == '\n') {
+                       buf = eol + 1;
+                       break; /* found end of header */
+               }
+               buf = eol + 1;
+       }
+       while (*buf == '\n')
+               buf++;
+       if (!*buf)
+               return;
+       *sub = buf; /* first non-empty line */
+       buf = strchr(buf, '\n');
+       if (!buf)
+               return; /* no body */
+       while (*buf == '\n')
+               buf++; /* skip blank between subject and body */
+       *body = buf;
+}
+
+/* See grab_values */
+static void grab_sub_body_contents(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz)
+{
+       int i;
+       const char *subpos = NULL, *bodypos = NULL;
+
+       for (i = 0; i < used_atom_cnt; i++) {
+               const char *name = used_atom[i];
+               struct atom_value *v = &val[i];
+               if (!!deref != (*name == '*'))
+                       continue;
+               if (deref)
+                       name++;
+               if (strcmp(name, "subject") &&
+                   strcmp(name, "body") &&
+                   strcmp(name, "contents"))
+                       continue;
+               if (!subpos)
+                       find_subpos(buf, sz, &subpos, &bodypos);
+               if (!subpos)
+                       return;
+
+               if (!strcmp(name, "subject"))
+                       v->s = copy_line(subpos);
+               else if (!strcmp(name, "body"))
+                       v->s = xstrdup(bodypos);
+               else if (!strcmp(name, "contents"))
+                       v->s = xstrdup(subpos);
+       }
+}
+
+/* We want to have empty print-string for field requests
+ * that do not apply (e.g. "authordate" for a tag object)
+ */
+static void fill_missing_values(struct atom_value *val)
+{
+       int i;
+       for (i = 0; i < used_atom_cnt; i++) {
+               struct atom_value *v = &val[i];
+               if (v->s == NULL)
+                       v->s = "";
+       }
+}
+
+/*
+ * val is a list of atom_value to hold returned values.  Extract
+ * the values for atoms in used_atom array out of (obj, buf, sz).
+ * when deref is false, (obj, buf, sz) is the object that is
+ * pointed at by the ref itself; otherwise it is the object the
+ * ref (which is a tag) refers to.
+ */
+static void grab_values(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz)
+{
+       grab_common_values(val, deref, obj, buf, sz);
+       switch (obj->type) {
+       case OBJ_TAG:
+               grab_tag_values(val, deref, obj, buf, sz);
+               grab_sub_body_contents(val, deref, obj, buf, sz);
+               grab_person("tagger", val, deref, obj, buf, sz);
+               break;
+       case OBJ_COMMIT:
+               grab_commit_values(val, deref, obj, buf, sz);
+               grab_sub_body_contents(val, deref, obj, buf, sz);
+               grab_person("author", val, deref, obj, buf, sz);
+               grab_person("committer", val, deref, obj, buf, sz);
+               break;
+       case OBJ_TREE:
+               // grab_tree_values(val, deref, obj, buf, sz);
+               break;
+       case OBJ_BLOB:
+               // grab_blob_values(val, deref, obj, buf, sz);
+               break;
+       default:
+               die("Eh?  Object of type %d?", obj->type);
+       }
+}
+
+/*
+ * Parse the object referred by ref, and grab needed value.
+ */
+static void populate_value(struct refinfo *ref)
+{
+       void *buf;
+       struct object *obj;
+       int eaten, i;
+       unsigned long size;
+       const unsigned char *tagged;
+
+       ref->value = xcalloc(sizeof(struct atom_value), used_atom_cnt);
+
+       buf = get_obj(ref->objectname, &obj, &size, &eaten);
+       if (!buf)
+               die("missing object %s for %s",
+                   sha1_to_hex(ref->objectname), ref->refname);
+       if (!obj)
+               die("parse_object_buffer failed on %s for %s",
+                   sha1_to_hex(ref->objectname), ref->refname);
+
+       /* Fill in specials first */
+       for (i = 0; i < used_atom_cnt; i++) {
+               const char *name = used_atom[i];
+               struct atom_value *v = &ref->value[i];
+               if (!strcmp(name, "refname"))
+                       v->s = ref->refname;
+               else if (!strcmp(name, "*refname")) {
+                       int len = strlen(ref->refname);
+                       char *s = xmalloc(len + 4);
+                       sprintf(s, "%s^{}", ref->refname);
+                       v->s = s;
+               }
+       }
+
+       grab_values(ref->value, 0, obj, buf, size);
+       if (!eaten)
+               free(buf);
+
+       /* If there is no atom that wants to know about tagged
+        * object, we are done.
+        */
+       if (!need_tagged || (obj->type != OBJ_TAG))
+               return;
+
+       /* If it is a tag object, see if we use a value that derefs
+        * the object, and if we do grab the object it refers to.
+        */
+       tagged = ((struct tag *)obj)->tagged->sha1;
+
+       /* NEEDSWORK: This derefs tag only once, which
+        * is good to deal with chains of trust, but
+        * is not consistent with what deref_tag() does
+        * which peels the onion to the core.
+        */
+       buf = get_obj(tagged, &obj, &size, &eaten);
+       if (!buf)
+               die("missing object %s for %s",
+                   sha1_to_hex(tagged), ref->refname);
+       if (!obj)
+               die("parse_object_buffer failed on %s for %s",
+                   sha1_to_hex(tagged), ref->refname);
+       grab_values(ref->value, 1, obj, buf, size);
+       if (!eaten)
+               free(buf);
+}
+
+/*
+ * Given a ref, return the value for the atom.  This lazily gets value
+ * out of the object by calling populate value.
+ */
+static void get_value(struct refinfo *ref, int atom, struct atom_value **v)
+{
+       if (!ref->value) {
+               populate_value(ref);
+               fill_missing_values(ref->value);
+       }
+       *v = &ref->value[atom];
+}
+
+struct grab_ref_cbdata {
+       struct refinfo **grab_array;
+       const char **grab_pattern;
+       int grab_cnt;
+};
+
+/*
+ * A call-back given to for_each_ref().  It is unfortunate that we
+ * need to use global variables to pass extra information to this
+ * function.
+ */
+static int grab_single_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct grab_ref_cbdata *cb = cb_data;
+       struct refinfo *ref;
+       int cnt;
+
+       if (*cb->grab_pattern) {
+               const char **pattern;
+               int namelen = strlen(refname);
+               for (pattern = cb->grab_pattern; *pattern; pattern++) {
+                       const char *p = *pattern;
+                       int plen = strlen(p);
+
+                       if ((plen <= namelen) &&
+                           !strncmp(refname, p, plen) &&
+                           (refname[plen] == '\0' ||
+                            refname[plen] == '/'))
+                               break;
+                       if (!fnmatch(p, refname, FNM_PATHNAME))
+                               break;
+               }
+               if (!*pattern)
+                       return 0;
+       }
+
+       /* We do not open the object yet; sort may only need refname
+        * to do its job and the resulting list may yet to be pruned
+        * by maxcount logic.
+        */
+       ref = xcalloc(1, sizeof(*ref));
+       ref->refname = xstrdup(refname);
+       hashcpy(ref->objectname, sha1);
+
+       cnt = cb->grab_cnt;
+       cb->grab_array = xrealloc(cb->grab_array,
+                                 sizeof(*cb->grab_array) * (cnt + 1));
+       cb->grab_array[cnt++] = ref;
+       cb->grab_cnt = cnt;
+       return 0;
+}
+
+static int cmp_ref_sort(struct ref_sort *s, struct refinfo *a, struct refinfo *b)
+{
+       struct atom_value *va, *vb;
+       int cmp;
+       cmp_type cmp_type = used_atom_type[s->atom];
+
+       get_value(a, s->atom, &va);
+       get_value(b, s->atom, &vb);
+       switch (cmp_type) {
+       case FIELD_STR:
+               cmp = strcmp(va->s, vb->s);
+               break;
+       default:
+               if (va->ul < vb->ul)
+                       cmp = -1;
+               else if (va->ul == vb->ul)
+                       cmp = 0;
+               else
+                       cmp = 1;
+               break;
+       }
+       return (s->reverse) ? -cmp : cmp;
+}
+
+static struct ref_sort *ref_sort;
+static int compare_refs(const void *a_, const void *b_)
+{
+       struct refinfo *a = *((struct refinfo **)a_);
+       struct refinfo *b = *((struct refinfo **)b_);
+       struct ref_sort *s;
+
+       for (s = ref_sort; s; s = s->next) {
+               int cmp = cmp_ref_sort(s, a, b);
+               if (cmp)
+                       return cmp;
+       }
+       return 0;
+}
+
+static void sort_refs(struct ref_sort *sort, struct refinfo **refs, int num_refs)
+{
+       ref_sort = sort;
+       qsort(refs, num_refs, sizeof(struct refinfo *), compare_refs);
+}
+
+static void print_value(struct refinfo *ref, int atom, int quote_style)
+{
+       struct atom_value *v;
+       get_value(ref, atom, &v);
+       switch (quote_style) {
+       case QUOTE_NONE:
+               fputs(v->s, stdout);
+               break;
+       case QUOTE_SHELL:
+               sq_quote_print(stdout, v->s);
+               break;
+       case QUOTE_PERL:
+               perl_quote_print(stdout, v->s);
+               break;
+       case QUOTE_PYTHON:
+               python_quote_print(stdout, v->s);
+               break;
+       case QUOTE_TCL:
+               tcl_quote_print(stdout, v->s);
+               break;
+       }
+}
+
+static int hex1(char ch)
+{
+       if ('0' <= ch && ch <= '9')
+               return ch - '0';
+       else if ('a' <= ch && ch <= 'f')
+               return ch - 'a' + 10;
+       else if ('A' <= ch && ch <= 'F')
+               return ch - 'A' + 10;
+       return -1;
+}
+static int hex2(const char *cp)
+{
+       if (cp[0] && cp[1])
+               return (hex1(cp[0]) << 4) | hex1(cp[1]);
+       else
+               return -1;
+}
+
+static void emit(const char *cp, const char *ep)
+{
+       while (*cp && (!ep || cp < ep)) {
+               if (*cp == '%') {
+                       if (cp[1] == '%')
+                               cp++;
+                       else {
+                               int ch = hex2(cp + 1);
+                               if (0 <= ch) {
+                                       putchar(ch);
+                                       cp += 3;
+                                       continue;
+                               }
+                       }
+               }
+               putchar(*cp);
+               cp++;
+       }
+}
+
+static void show_ref(struct refinfo *info, const char *format, int quote_style)
+{
+       const char *cp, *sp, *ep;
+
+       for (cp = format; *cp && (sp = find_next(cp)); cp = ep + 1) {
+               ep = strchr(sp, ')');
+               if (cp < sp)
+                       emit(cp, sp);
+               print_value(info, parse_atom(sp + 2, ep), quote_style);
+       }
+       if (*cp) {
+               sp = cp + strlen(cp);
+               emit(cp, sp);
+       }
+       putchar('\n');
+}
+
+static struct ref_sort *default_sort(void)
+{
+       static const char cstr_name[] = "refname";
+
+       struct ref_sort *sort = xcalloc(1, sizeof(*sort));
+
+       sort->next = NULL;
+       sort->atom = parse_atom(cstr_name, cstr_name + strlen(cstr_name));
+       return sort;
+}
+
+int opt_parse_sort(const struct option *opt, const char *arg, int unset)
+{
+       struct ref_sort **sort_tail = opt->value;
+       struct ref_sort *s;
+       int len;
+
+       if (!arg) /* should --no-sort void the list ? */
+               return -1;
+
+       *sort_tail = s = xcalloc(1, sizeof(*s));
+       sort_tail = &s->next;
+
+       if (*arg == '-') {
+               s->reverse = 1;
+               arg++;
+       }
+       len = strlen(arg);
+       s->atom = parse_atom(arg, arg+len);
+       return 0;
+}
+
+static char const * const for_each_ref_usage[] = {
+       "git-for-each-ref [options] [<pattern>]",
+       NULL
+};
+
+int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
+{
+       int i, num_refs;
+       const char *format = "%(objectname) %(objecttype)\t%(refname)";
+       struct ref_sort *sort = NULL, **sort_tail = &sort;
+       int maxcount = 0, quote_style = 0;
+       struct refinfo **refs;
+       struct grab_ref_cbdata cbdata;
+
+       struct option opts[] = {
+               OPT_BIT('s', "shell", &quote_style,
+                       "quote placeholders suitably for shells", QUOTE_SHELL),
+               OPT_BIT('p', "perl",  &quote_style,
+                       "quote placeholders suitably for perl", QUOTE_PERL),
+               OPT_BIT(0 , "python", &quote_style,
+                       "quote placeholders suitably for python", QUOTE_PYTHON),
+               OPT_BIT(0 , "tcl",  &quote_style,
+                       "quote placeholders suitably for tcl", QUOTE_TCL),
+
+               OPT_GROUP(""),
+               OPT_INTEGER( 0 , "count", &maxcount, "show only <n> matched refs"),
+               OPT_STRING(  0 , "format", &format, "format", "format to use for the output"),
+               OPT_CALLBACK(0 , "sort", sort_tail, "key",
+                           "field name to sort on", &opt_parse_sort),
+               OPT_END(),
+       };
+
+       parse_options(argc, argv, opts, for_each_ref_usage, 0);
+       if (maxcount < 0) {
+               error("invalid --count argument: `%d'", maxcount);
+               usage_with_options(for_each_ref_usage, opts);
+       }
+       if (HAS_MULTI_BITS(quote_style)) {
+               error("more than one quoting style?");
+               usage_with_options(for_each_ref_usage, opts);
+       }
+       if (verify_format(format))
+               usage_with_options(for_each_ref_usage, opts);
+
+       if (!sort)
+               sort = default_sort();
+       sort_atom_limit = used_atom_cnt;
+
+       memset(&cbdata, 0, sizeof(cbdata));
+       cbdata.grab_pattern = argv;
+       for_each_ref(grab_single_ref, &cbdata);
+       refs = cbdata.grab_array;
+       num_refs = cbdata.grab_cnt;
+
+       for (i = 0; i < used_atom_cnt; i++) {
+               if (used_atom[i][0] == '*') {
+                       need_tagged = 1;
+                       break;
+               }
+       }
+
+       sort_refs(sort, refs, num_refs);
+
+       if (!maxcount || num_refs < maxcount)
+               maxcount = num_refs;
+       for (i = 0; i < maxcount; i++)
+               show_ref(refs[i], format, quote_style);
+       return 0;
+}
diff --git a/builtin-fsck.c b/builtin-fsck.c
new file mode 100644 (file)
index 0000000..e4874f6
--- /dev/null
@@ -0,0 +1,781 @@
+#include "builtin.h"
+#include "cache.h"
+#include "commit.h"
+#include "tree.h"
+#include "blob.h"
+#include "tag.h"
+#include "refs.h"
+#include "pack.h"
+#include "cache-tree.h"
+#include "tree-walk.h"
+#include "parse-options.h"
+
+#define REACHABLE 0x0001
+#define SEEN      0x0002
+
+static int show_root;
+static int show_tags;
+static int show_unreachable;
+static int include_reflogs = 1;
+static int check_full;
+static int check_strict;
+static int keep_cache_objects;
+static unsigned char head_sha1[20];
+static int errors_found;
+static int write_lost_and_found;
+static int verbose;
+#define ERROR_OBJECT 01
+#define ERROR_REACHABLE 02
+
+#ifdef NO_D_INO_IN_DIRENT
+#define SORT_DIRENT 0
+#define DIRENT_SORT_HINT(de) 0
+#else
+#define SORT_DIRENT 1
+#define DIRENT_SORT_HINT(de) ((de)->d_ino)
+#endif
+
+static void objreport(struct object *obj, const char *severity,
+                      const char *err, va_list params)
+{
+       fprintf(stderr, "%s in %s %s: ",
+               severity, typename(obj->type), sha1_to_hex(obj->sha1));
+       vfprintf(stderr, err, params);
+       fputs("\n", stderr);
+}
+
+static int objerror(struct object *obj, const char *err, ...)
+{
+       va_list params;
+       va_start(params, err);
+       errors_found |= ERROR_OBJECT;
+       objreport(obj, "error", err, params);
+       va_end(params);
+       return -1;
+}
+
+static int objwarning(struct object *obj, const char *err, ...)
+{
+       va_list params;
+       va_start(params, err);
+       objreport(obj, "warning", err, params);
+       va_end(params);
+       return -1;
+}
+
+/*
+ * Check a single reachable object
+ */
+static void check_reachable_object(struct object *obj)
+{
+       const struct object_refs *refs;
+
+       /*
+        * We obviously want the object to be parsed,
+        * except if it was in a pack-file and we didn't
+        * do a full fsck
+        */
+       if (!obj->parsed) {
+               if (has_sha1_pack(obj->sha1, NULL))
+                       return; /* it is in pack - forget about it */
+               printf("missing %s %s\n", typename(obj->type), sha1_to_hex(obj->sha1));
+               errors_found |= ERROR_REACHABLE;
+               return;
+       }
+
+       /*
+        * Check that everything that we try to reference is also good.
+        */
+       refs = lookup_object_refs(obj);
+       if (refs) {
+               unsigned j;
+               for (j = 0; j < refs->count; j++) {
+                       struct object *ref = refs->ref[j];
+                       if (ref->parsed ||
+                           (has_sha1_file(ref->sha1)))
+                               continue;
+                       printf("broken link from %7s %s\n",
+                              typename(obj->type), sha1_to_hex(obj->sha1));
+                       printf("              to %7s %s\n",
+                              typename(ref->type), sha1_to_hex(ref->sha1));
+                       errors_found |= ERROR_REACHABLE;
+               }
+       }
+}
+
+/*
+ * Check a single unreachable object
+ */
+static void check_unreachable_object(struct object *obj)
+{
+       /*
+        * Missing unreachable object? Ignore it. It's not like
+        * we miss it (since it can't be reached), nor do we want
+        * to complain about it being unreachable (since it does
+        * not exist).
+        */
+       if (!obj->parsed)
+               return;
+
+       /*
+        * Unreachable object that exists? Show it if asked to,
+        * since this is something that is prunable.
+        */
+       if (show_unreachable) {
+               printf("unreachable %s %s\n", typename(obj->type), sha1_to_hex(obj->sha1));
+               return;
+       }
+
+       /*
+        * "!used" means that nothing at all points to it, including
+        * other unreachable objects. In other words, it's the "tip"
+        * of some set of unreachable objects, usually a commit that
+        * got dropped.
+        *
+        * Such starting points are more interesting than some random
+        * set of unreachable objects, so we show them even if the user
+        * hasn't asked for _all_ unreachable objects. If you have
+        * deleted a branch by mistake, this is a prime candidate to
+        * start looking at, for example.
+        */
+       if (!obj->used) {
+               printf("dangling %s %s\n", typename(obj->type),
+                      sha1_to_hex(obj->sha1));
+               if (write_lost_and_found) {
+                       char *filename = git_path("lost-found/%s/%s",
+                               obj->type == OBJ_COMMIT ? "commit" : "other",
+                               sha1_to_hex(obj->sha1));
+                       FILE *f;
+
+                       if (safe_create_leading_directories(filename)) {
+                               error("Could not create lost-found");
+                               return;
+                       }
+                       if (!(f = fopen(filename, "w")))
+                               die("Could not open %s", filename);
+                       if (obj->type == OBJ_BLOB) {
+                               enum object_type type;
+                               unsigned long size;
+                               char *buf = read_sha1_file(obj->sha1,
+                                               &type, &size);
+                               if (buf) {
+                                       fwrite(buf, size, 1, f);
+                                       free(buf);
+                               }
+                       } else
+                               fprintf(f, "%s\n", sha1_to_hex(obj->sha1));
+                       fclose(f);
+               }
+               return;
+       }
+
+       /*
+        * Otherwise? It's there, it's unreachable, and some other unreachable
+        * object points to it. Ignore it - it's not interesting, and we showed
+        * all the interesting cases above.
+        */
+}
+
+static void check_object(struct object *obj)
+{
+       if (verbose)
+               fprintf(stderr, "Checking %s\n", sha1_to_hex(obj->sha1));
+
+       if (obj->flags & REACHABLE)
+               check_reachable_object(obj);
+       else
+               check_unreachable_object(obj);
+}
+
+static void check_connectivity(void)
+{
+       int i, max;
+
+       /* Look up all the requirements, warn about missing objects.. */
+       max = get_max_object_index();
+       if (verbose)
+               fprintf(stderr, "Checking connectivity (%d objects)\n", max);
+
+       for (i = 0; i < max; i++) {
+               struct object *obj = get_indexed_object(i);
+
+               if (obj)
+                       check_object(obj);
+       }
+}
+
+/*
+ * The entries in a tree are ordered in the _path_ order,
+ * which means that a directory entry is ordered by adding
+ * a slash to the end of it.
+ *
+ * So a directory called "a" is ordered _after_ a file
+ * called "a.c", because "a/" sorts after "a.c".
+ */
+#define TREE_UNORDERED (-1)
+#define TREE_HAS_DUPS  (-2)
+
+static int verify_ordered(unsigned mode1, const char *name1, unsigned mode2, const char *name2)
+{
+       int len1 = strlen(name1);
+       int len2 = strlen(name2);
+       int len = len1 < len2 ? len1 : len2;
+       unsigned char c1, c2;
+       int cmp;
+
+       cmp = memcmp(name1, name2, len);
+       if (cmp < 0)
+               return 0;
+       if (cmp > 0)
+               return TREE_UNORDERED;
+
+       /*
+        * Ok, the first <len> characters are the same.
+        * Now we need to order the next one, but turn
+        * a '\0' into a '/' for a directory entry.
+        */
+       c1 = name1[len];
+       c2 = name2[len];
+       if (!c1 && !c2)
+               /*
+                * git-write-tree used to write out a nonsense tree that has
+                * entries with the same name, one blob and one tree.  Make
+                * sure we do not have duplicate entries.
+                */
+               return TREE_HAS_DUPS;
+       if (!c1 && S_ISDIR(mode1))
+               c1 = '/';
+       if (!c2 && S_ISDIR(mode2))
+               c2 = '/';
+       return c1 < c2 ? 0 : TREE_UNORDERED;
+}
+
+static int fsck_tree(struct tree *item)
+{
+       int retval;
+       int has_full_path = 0;
+       int has_empty_name = 0;
+       int has_zero_pad = 0;
+       int has_bad_modes = 0;
+       int has_dup_entries = 0;
+       int not_properly_sorted = 0;
+       struct tree_desc desc;
+       unsigned o_mode;
+       const char *o_name;
+       const unsigned char *o_sha1;
+
+       if (verbose)
+               fprintf(stderr, "Checking tree %s\n",
+                               sha1_to_hex(item->object.sha1));
+
+       init_tree_desc(&desc, item->buffer, item->size);
+
+       o_mode = 0;
+       o_name = NULL;
+       o_sha1 = NULL;
+       while (desc.size) {
+               unsigned mode;
+               const char *name;
+               const unsigned char *sha1;
+
+               sha1 = tree_entry_extract(&desc, &name, &mode);
+
+               if (strchr(name, '/'))
+                       has_full_path = 1;
+               if (!*name)
+                       has_empty_name = 1;
+               has_zero_pad |= *(char *)desc.buffer == '0';
+               update_tree_entry(&desc);
+
+               switch (mode) {
+               /*
+                * Standard modes..
+                */
+               case S_IFREG | 0755:
+               case S_IFREG | 0644:
+               case S_IFLNK:
+               case S_IFDIR:
+               case S_IFGITLINK:
+                       break;
+               /*
+                * This is nonstandard, but we had a few of these
+                * early on when we honored the full set of mode
+                * bits..
+                */
+               case S_IFREG | 0664:
+                       if (!check_strict)
+                               break;
+               default:
+                       has_bad_modes = 1;
+               }
+
+               if (o_name) {
+                       switch (verify_ordered(o_mode, o_name, mode, name)) {
+                       case TREE_UNORDERED:
+                               not_properly_sorted = 1;
+                               break;
+                       case TREE_HAS_DUPS:
+                               has_dup_entries = 1;
+                               break;
+                       default:
+                               break;
+                       }
+               }
+
+               o_mode = mode;
+               o_name = name;
+               o_sha1 = sha1;
+       }
+       free(item->buffer);
+       item->buffer = NULL;
+
+       retval = 0;
+       if (has_full_path) {
+               objwarning(&item->object, "contains full pathnames");
+       }
+       if (has_empty_name) {
+               objwarning(&item->object, "contains empty pathname");
+       }
+       if (has_zero_pad) {
+               objwarning(&item->object, "contains zero-padded file modes");
+       }
+       if (has_bad_modes) {
+               objwarning(&item->object, "contains bad file modes");
+       }
+       if (has_dup_entries) {
+               retval = objerror(&item->object, "contains duplicate file entries");
+       }
+       if (not_properly_sorted) {
+               retval = objerror(&item->object, "not properly sorted");
+       }
+       return retval;
+}
+
+static int fsck_commit(struct commit *commit)
+{
+       char *buffer = commit->buffer;
+       unsigned char tree_sha1[20], sha1[20];
+
+       if (verbose)
+               fprintf(stderr, "Checking commit %s\n",
+                       sha1_to_hex(commit->object.sha1));
+
+       if (memcmp(buffer, "tree ", 5))
+               return objerror(&commit->object, "invalid format - expected 'tree' line");
+       if (get_sha1_hex(buffer+5, tree_sha1) || buffer[45] != '\n')
+               return objerror(&commit->object, "invalid 'tree' line format - bad sha1");
+       buffer += 46;
+       while (!memcmp(buffer, "parent ", 7)) {
+               if (get_sha1_hex(buffer+7, sha1) || buffer[47] != '\n')
+                       return objerror(&commit->object, "invalid 'parent' line format - bad sha1");
+               buffer += 48;
+       }
+       if (memcmp(buffer, "author ", 7))
+               return objerror(&commit->object, "invalid format - expected 'author' line");
+       free(commit->buffer);
+       commit->buffer = NULL;
+       if (!commit->tree)
+               return objerror(&commit->object, "could not load commit's tree %s", tree_sha1);
+       if (!commit->parents && show_root)
+               printf("root %s\n", sha1_to_hex(commit->object.sha1));
+       if (!commit->date)
+               printf("bad commit date in %s\n",
+                      sha1_to_hex(commit->object.sha1));
+       return 0;
+}
+
+static int fsck_tag(struct tag *tag)
+{
+       struct object *tagged = tag->tagged;
+
+       if (verbose)
+               fprintf(stderr, "Checking tag %s\n",
+                       sha1_to_hex(tag->object.sha1));
+
+       if (!tagged) {
+               return objerror(&tag->object, "could not load tagged object");
+       }
+       if (!show_tags)
+               return 0;
+
+       printf("tagged %s %s", typename(tagged->type), sha1_to_hex(tagged->sha1));
+       printf(" (%s) in %s\n", tag->tag, sha1_to_hex(tag->object.sha1));
+       return 0;
+}
+
+static int fsck_sha1(const unsigned char *sha1)
+{
+       struct object *obj = parse_object(sha1);
+       if (!obj) {
+               errors_found |= ERROR_OBJECT;
+               return error("%s: object corrupt or missing",
+                            sha1_to_hex(sha1));
+       }
+       if (obj->flags & SEEN)
+               return 0;
+       obj->flags |= SEEN;
+       if (obj->type == OBJ_BLOB)
+               return 0;
+       if (obj->type == OBJ_TREE)
+               return fsck_tree((struct tree *) obj);
+       if (obj->type == OBJ_COMMIT)
+               return fsck_commit((struct commit *) obj);
+       if (obj->type == OBJ_TAG)
+               return fsck_tag((struct tag *) obj);
+
+       /* By now, parse_object() would've returned NULL instead. */
+       return objerror(obj, "unknown type '%d' (internal fsck error)",
+                       obj->type);
+}
+
+/*
+ * This is the sorting chunk size: make it reasonably
+ * big so that we can sort well..
+ */
+#define MAX_SHA1_ENTRIES (1024)
+
+struct sha1_entry {
+       unsigned long ino;
+       unsigned char sha1[20];
+};
+
+static struct {
+       unsigned long nr;
+       struct sha1_entry *entry[MAX_SHA1_ENTRIES];
+} sha1_list;
+
+static int ino_compare(const void *_a, const void *_b)
+{
+       const struct sha1_entry *a = _a, *b = _b;
+       unsigned long ino1 = a->ino, ino2 = b->ino;
+       return ino1 < ino2 ? -1 : ino1 > ino2 ? 1 : 0;
+}
+
+static void fsck_sha1_list(void)
+{
+       int i, nr = sha1_list.nr;
+
+       if (SORT_DIRENT)
+               qsort(sha1_list.entry, nr,
+                     sizeof(struct sha1_entry *), ino_compare);
+       for (i = 0; i < nr; i++) {
+               struct sha1_entry *entry = sha1_list.entry[i];
+               unsigned char *sha1 = entry->sha1;
+
+               sha1_list.entry[i] = NULL;
+               fsck_sha1(sha1);
+               free(entry);
+       }
+       sha1_list.nr = 0;
+}
+
+static void add_sha1_list(unsigned char *sha1, unsigned long ino)
+{
+       struct sha1_entry *entry = xmalloc(sizeof(*entry));
+       int nr;
+
+       entry->ino = ino;
+       hashcpy(entry->sha1, sha1);
+       nr = sha1_list.nr;
+       if (nr == MAX_SHA1_ENTRIES) {
+               fsck_sha1_list();
+               nr = 0;
+       }
+       sha1_list.entry[nr] = entry;
+       sha1_list.nr = ++nr;
+}
+
+static void fsck_dir(int i, char *path)
+{
+       DIR *dir = opendir(path);
+       struct dirent *de;
+
+       if (!dir)
+               return;
+
+       if (verbose)
+               fprintf(stderr, "Checking directory %s\n", path);
+
+       while ((de = readdir(dir)) != NULL) {
+               char name[100];
+               unsigned char sha1[20];
+               int len = strlen(de->d_name);
+
+               switch (len) {
+               case 2:
+                       if (de->d_name[1] != '.')
+                               break;
+               case 1:
+                       if (de->d_name[0] != '.')
+                               break;
+                       continue;
+               case 38:
+                       sprintf(name, "%02x", i);
+                       memcpy(name+2, de->d_name, len+1);
+                       if (get_sha1_hex(name, sha1) < 0)
+                               break;
+                       add_sha1_list(sha1, DIRENT_SORT_HINT(de));
+                       continue;
+               }
+               fprintf(stderr, "bad sha1 file: %s/%s\n", path, de->d_name);
+       }
+       closedir(dir);
+}
+
+static int default_refs;
+
+static int fsck_handle_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
+               const char *email, unsigned long timestamp, int tz,
+               const char *message, void *cb_data)
+{
+       struct object *obj;
+
+       if (verbose)
+               fprintf(stderr, "Checking reflog %s->%s\n",
+                       sha1_to_hex(osha1), sha1_to_hex(nsha1));
+
+       if (!is_null_sha1(osha1)) {
+               obj = lookup_object(osha1);
+               if (obj) {
+                       obj->used = 1;
+                       mark_reachable(obj, REACHABLE);
+               }
+       }
+       obj = lookup_object(nsha1);
+       if (obj) {
+               obj->used = 1;
+               mark_reachable(obj, REACHABLE);
+       }
+       return 0;
+}
+
+static int fsck_handle_reflog(const char *logname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       for_each_reflog_ent(logname, fsck_handle_reflog_ent, NULL);
+       return 0;
+}
+
+static int fsck_handle_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct object *obj;
+
+       obj = lookup_object(sha1);
+       if (!obj) {
+               if (has_sha1_file(sha1)) {
+                       default_refs++;
+                       return 0; /* it is in a pack */
+               }
+               error("%s: invalid sha1 pointer %s", refname, sha1_to_hex(sha1));
+               /* We'll continue with the rest despite the error.. */
+               return 0;
+       }
+       default_refs++;
+       obj->used = 1;
+       mark_reachable(obj, REACHABLE);
+
+       return 0;
+}
+
+static void get_default_heads(void)
+{
+       for_each_ref(fsck_handle_ref, NULL);
+       if (include_reflogs)
+               for_each_reflog(fsck_handle_reflog, NULL);
+
+       /*
+        * Not having any default heads isn't really fatal, but
+        * it does mean that "--unreachable" no longer makes any
+        * sense (since in this case everything will obviously
+        * be unreachable by definition.
+        *
+        * Showing dangling objects is valid, though (as those
+        * dangling objects are likely lost heads).
+        *
+        * So we just print a warning about it, and clear the
+        * "show_unreachable" flag.
+        */
+       if (!default_refs) {
+               fprintf(stderr, "notice: No default references\n");
+               show_unreachable = 0;
+       }
+}
+
+static void fsck_object_dir(const char *path)
+{
+       int i;
+
+       if (verbose)
+               fprintf(stderr, "Checking object directory\n");
+
+       for (i = 0; i < 256; i++) {
+               static char dir[4096];
+               sprintf(dir, "%s/%02x", path, i);
+               fsck_dir(i, dir);
+       }
+       fsck_sha1_list();
+}
+
+static int fsck_head_link(void)
+{
+       unsigned char sha1[20];
+       int flag;
+       int null_is_error = 0;
+       const char *head_points_at = resolve_ref("HEAD", sha1, 0, &flag);
+
+       if (verbose)
+               fprintf(stderr, "Checking HEAD link\n");
+
+       if (!head_points_at)
+               return error("Invalid HEAD");
+       if (!strcmp(head_points_at, "HEAD"))
+               /* detached HEAD */
+               null_is_error = 1;
+       else if (prefixcmp(head_points_at, "refs/heads/"))
+               return error("HEAD points to something strange (%s)",
+                            head_points_at);
+       if (is_null_sha1(sha1)) {
+               if (null_is_error)
+                       return error("HEAD: detached HEAD points at nothing");
+               fprintf(stderr, "notice: HEAD points to an unborn branch (%s)\n",
+                       head_points_at + 11);
+       }
+       return 0;
+}
+
+static int fsck_cache_tree(struct cache_tree *it)
+{
+       int i;
+       int err = 0;
+
+       if (verbose)
+               fprintf(stderr, "Checking cache tree\n");
+
+       if (0 <= it->entry_count) {
+               struct object *obj = parse_object(it->sha1);
+               if (!obj) {
+                       error("%s: invalid sha1 pointer in cache-tree",
+                             sha1_to_hex(it->sha1));
+                       return 1;
+               }
+               mark_reachable(obj, REACHABLE);
+               obj->used = 1;
+               if (obj->type != OBJ_TREE)
+                       err |= objerror(obj, "non-tree in cache-tree");
+       }
+       for (i = 0; i < it->subtree_nr; i++)
+               err |= fsck_cache_tree(it->down[i]->cache_tree);
+       return err;
+}
+
+static char const * const fsck_usage[] = {
+       "git-fsck [options] [<object>...]",
+       NULL
+};
+
+static struct option fsck_opts[] = {
+       OPT__VERBOSE(&verbose),
+       OPT_BOOLEAN(0, "unreachable", &show_unreachable, "show unreachable objects"),
+       OPT_BOOLEAN(0, "tags", &show_tags, "report tags"),
+       OPT_BOOLEAN(0, "root", &show_root, "report root nodes"),
+       OPT_BOOLEAN(0, "cache", &keep_cache_objects, "make index objects head nodes"),
+       OPT_BOOLEAN(0, "reflogs", &include_reflogs, "make reflogs head nodes (default)"),
+       OPT_BOOLEAN(0, "full", &check_full, "also consider alternate objects"),
+       OPT_BOOLEAN(0, "strict", &check_strict, "enable more strict checking"),
+       OPT_BOOLEAN(0, "lost-found", &write_lost_and_found,
+                               "write dangling objects in .git/lost-found"),
+       OPT_END(),
+};
+
+int cmd_fsck(int argc, const char **argv, const char *prefix)
+{
+       int i, heads;
+
+       track_object_refs = 1;
+       errors_found = 0;
+
+       argc = parse_options(argc, argv, fsck_opts, fsck_usage, 0);
+       if (write_lost_and_found) {
+               check_full = 1;
+               include_reflogs = 0;
+       }
+
+       fsck_head_link();
+       fsck_object_dir(get_object_directory());
+       if (check_full) {
+               struct alternate_object_database *alt;
+               struct packed_git *p;
+               prepare_alt_odb();
+               for (alt = alt_odb_list; alt; alt = alt->next) {
+                       char namebuf[PATH_MAX];
+                       int namelen = alt->name - alt->base;
+                       memcpy(namebuf, alt->base, namelen);
+                       namebuf[namelen - 1] = 0;
+                       fsck_object_dir(namebuf);
+               }
+               prepare_packed_git();
+               for (p = packed_git; p; p = p->next)
+                       /* verify gives error messages itself */
+                       verify_pack(p, 0);
+
+               for (p = packed_git; p; p = p->next) {
+                       uint32_t j, num;
+                       if (open_pack_index(p))
+                               continue;
+                       num = p->num_objects;
+                       for (j = 0; j < num; j++)
+                               fsck_sha1(nth_packed_object_sha1(p, j));
+               }
+       }
+
+       heads = 0;
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (!get_sha1(arg, head_sha1)) {
+                       struct object *obj = lookup_object(head_sha1);
+
+                       /* Error is printed by lookup_object(). */
+                       if (!obj)
+                               continue;
+
+                       obj->used = 1;
+                       mark_reachable(obj, REACHABLE);
+                       heads++;
+                       continue;
+               }
+               error("invalid parameter: expected sha1, got '%s'", arg);
+       }
+
+       /*
+        * If we've not been given any explicit head information, do the
+        * default ones from .git/refs. We also consider the index file
+        * in this case (ie this implies --cache).
+        */
+       if (!heads) {
+               get_default_heads();
+               keep_cache_objects = 1;
+       }
+
+       if (keep_cache_objects) {
+               read_cache();
+               for (i = 0; i < active_nr; i++) {
+                       unsigned int mode;
+                       struct blob *blob;
+                       struct object *obj;
+
+                       mode = ntohl(active_cache[i]->ce_mode);
+                       if (S_ISGITLINK(mode))
+                               continue;
+                       blob = lookup_blob(active_cache[i]->sha1);
+                       if (!blob)
+                               continue;
+                       obj = &blob->object;
+                       obj->used = 1;
+                       mark_reachable(obj, REACHABLE);
+               }
+               if (active_cache_tree)
+                       fsck_cache_tree(active_cache_tree);
+       }
+
+       check_connectivity();
+       return errors_found;
+}
diff --git a/builtin-gc.c b/builtin-gc.c
new file mode 100644 (file)
index 0000000..799c263
--- /dev/null
@@ -0,0 +1,244 @@
+/*
+ * git gc builtin command
+ *
+ * Cleanup unreachable files and optimize the repository.
+ *
+ * Copyright (c) 2007 James Bowes
+ *
+ * Based on git-gc.sh, which is
+ *
+ * Copyright (c) 2006 Shawn O. Pearce
+ */
+
+#include "builtin.h"
+#include "cache.h"
+#include "parse-options.h"
+#include "run-command.h"
+
+#define FAILED_RUN "failed to run %s"
+
+static const char * const builtin_gc_usage[] = {
+       "git-gc [options]",
+       NULL
+};
+
+static int pack_refs = 1;
+static int aggressive_window = -1;
+static int gc_auto_threshold = 6700;
+static int gc_auto_pack_limit = 20;
+
+#define MAX_ADD 10
+static const char *argv_pack_refs[] = {"pack-refs", "--all", "--prune", NULL};
+static const char *argv_reflog[] = {"reflog", "expire", "--all", NULL};
+static const char *argv_repack[MAX_ADD] = {"repack", "-d", "-l", NULL};
+static const char *argv_prune[] = {"prune", NULL};
+static const char *argv_rerere[] = {"rerere", "gc", NULL};
+
+static int gc_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "gc.packrefs")) {
+               if (!strcmp(value, "notbare"))
+                       pack_refs = -1;
+               else
+                       pack_refs = git_config_bool(var, value);
+               return 0;
+       }
+       if (!strcmp(var, "gc.aggressivewindow")) {
+               aggressive_window = git_config_int(var, value);
+               return 0;
+       }
+       if (!strcmp(var, "gc.auto")) {
+               gc_auto_threshold = git_config_int(var, value);
+               return 0;
+       }
+       if (!strcmp(var, "gc.autopacklimit")) {
+               gc_auto_pack_limit = git_config_int(var, value);
+               return 0;
+       }
+       return git_default_config(var, value);
+}
+
+static void append_option(const char **cmd, const char *opt, int max_length)
+{
+       int i;
+
+       for (i = 0; cmd[i]; i++)
+               ;
+
+       if (i + 2 >= max_length)
+               die("Too many options specified");
+       cmd[i++] = opt;
+       cmd[i] = NULL;
+}
+
+static int too_many_loose_objects(void)
+{
+       /*
+        * Quickly check if a "gc" is needed, by estimating how
+        * many loose objects there are.  Because SHA-1 is evenly
+        * distributed, we can check only one and get a reasonable
+        * estimate.
+        */
+       char path[PATH_MAX];
+       const char *objdir = get_object_directory();
+       DIR *dir;
+       struct dirent *ent;
+       int auto_threshold;
+       int num_loose = 0;
+       int needed = 0;
+
+       if (gc_auto_threshold <= 0)
+               return 0;
+
+       if (sizeof(path) <= snprintf(path, sizeof(path), "%s/17", objdir)) {
+               warning("insanely long object directory %.*s", 50, objdir);
+               return 0;
+       }
+       dir = opendir(path);
+       if (!dir)
+               return 0;
+
+       auto_threshold = (gc_auto_threshold + 255) / 256;
+       while ((ent = readdir(dir)) != NULL) {
+               if (strspn(ent->d_name, "0123456789abcdef") != 38 ||
+                   ent->d_name[38] != '\0')
+                       continue;
+               if (++num_loose > auto_threshold) {
+                       needed = 1;
+                       break;
+               }
+       }
+       closedir(dir);
+       return needed;
+}
+
+static int too_many_packs(void)
+{
+       struct packed_git *p;
+       int cnt;
+
+       if (gc_auto_pack_limit <= 0)
+               return 0;
+
+       prepare_packed_git();
+       for (cnt = 0, p = packed_git; p; p = p->next) {
+               char path[PATH_MAX];
+               size_t len;
+               int keep;
+
+               if (!p->pack_local)
+                       continue;
+               len = strlen(p->pack_name);
+               if (PATH_MAX <= len + 1)
+                       continue; /* oops, give up */
+               memcpy(path, p->pack_name, len-5);
+               memcpy(path + len - 5, ".keep", 6);
+               keep = access(p->pack_name, F_OK) && (errno == ENOENT);
+               if (keep)
+                       continue;
+               /*
+                * Perhaps check the size of the pack and count only
+                * very small ones here?
+                */
+               cnt++;
+       }
+       return gc_auto_pack_limit <= cnt;
+}
+
+static int need_to_gc(void)
+{
+       /*
+        * Setting gc.auto and gc.autopacklimit to 0 or negative can
+        * disable the automatic gc.
+        */
+       if (gc_auto_threshold <= 0 && gc_auto_pack_limit <= 0)
+               return 0;
+
+       /*
+        * If there are too many loose objects, but not too many
+        * packs, we run "repack -d -l".  If there are too many packs,
+        * we run "repack -A -d -l".  Otherwise we tell the caller
+        * there is no need.
+        */
+       if (too_many_packs())
+               append_option(argv_repack, "-A", MAX_ADD);
+       else if (!too_many_loose_objects())
+               return 0;
+       return 1;
+}
+
+int cmd_gc(int argc, const char **argv, const char *prefix)
+{
+       int prune = 0;
+       int aggressive = 0;
+       int auto_gc = 0;
+       char buf[80];
+
+       struct option builtin_gc_options[] = {
+               OPT_BOOLEAN(0, "prune", &prune, "prune unreferenced objects"),
+               OPT_BOOLEAN(0, "aggressive", &aggressive, "be more thorough (increased runtime)"),
+               OPT_BOOLEAN(0, "auto", &auto_gc, "enable auto-gc mode"),
+               OPT_END()
+       };
+
+       git_config(gc_config);
+
+       if (pack_refs < 0)
+               pack_refs = !is_bare_repository();
+
+       argc = parse_options(argc, argv, builtin_gc_options, builtin_gc_usage, 0);
+       if (argc > 0)
+               usage_with_options(builtin_gc_usage, builtin_gc_options);
+
+       if (aggressive) {
+               append_option(argv_repack, "-f", MAX_ADD);
+               if (aggressive_window > 0) {
+                       sprintf(buf, "--window=%d", aggressive_window);
+                       append_option(argv_repack, buf, MAX_ADD);
+               }
+       }
+
+       if (auto_gc) {
+               /*
+                * Auto-gc should be least intrusive as possible.
+                */
+               prune = 0;
+               if (!need_to_gc())
+                       return 0;
+               fprintf(stderr, "Packing your repository for optimum "
+                       "performance. You may also\n"
+                       "run \"git gc\" manually. See "
+                       "\"git help gc\" for more information.\n");
+       } else {
+               /*
+                * Use safer (for shared repos) "-A" option to
+                * repack when not pruning. Auto-gc makes its
+                * own decision.
+                */
+               if (prune)
+                       append_option(argv_repack, "-a", MAX_ADD);
+               else
+                       append_option(argv_repack, "-A", MAX_ADD);
+       }
+
+       if (pack_refs && run_command_v_opt(argv_pack_refs, RUN_GIT_CMD))
+               return error(FAILED_RUN, argv_pack_refs[0]);
+
+       if (run_command_v_opt(argv_reflog, RUN_GIT_CMD))
+               return error(FAILED_RUN, argv_reflog[0]);
+
+       if (run_command_v_opt(argv_repack, RUN_GIT_CMD))
+               return error(FAILED_RUN, argv_repack[0]);
+
+       if (prune && run_command_v_opt(argv_prune, RUN_GIT_CMD))
+               return error(FAILED_RUN, argv_prune[0]);
+
+       if (run_command_v_opt(argv_rerere, RUN_GIT_CMD))
+               return error(FAILED_RUN, argv_rerere[0]);
+
+       if (auto_gc && too_many_loose_objects())
+               warning("There are too many unreachable loose objects; "
+                       "run 'git prune' to remove them.");
+
+       return 0;
+}
diff --git a/builtin-grep.c b/builtin-grep.c
new file mode 100644 (file)
index 0000000..f1ff8dc
--- /dev/null
@@ -0,0 +1,788 @@
+/*
+ * Builtin "git grep"
+ *
+ * Copyright (c) 2006 Junio C Hamano
+ */
+#include "cache.h"
+#include "blob.h"
+#include "tree.h"
+#include "commit.h"
+#include "tag.h"
+#include "tree-walk.h"
+#include "builtin.h"
+#include "grep.h"
+
+/*
+ * git grep pathspecs are somewhat different from diff-tree pathspecs;
+ * pathname wildcards are allowed.
+ */
+static int pathspec_matches(const char **paths, const char *name)
+{
+       int namelen, i;
+       if (!paths || !*paths)
+               return 1;
+       namelen = strlen(name);
+       for (i = 0; paths[i]; i++) {
+               const char *match = paths[i];
+               int matchlen = strlen(match);
+               const char *cp, *meta;
+
+               if (!matchlen ||
+                   ((matchlen <= namelen) &&
+                    !strncmp(name, match, matchlen) &&
+                    (match[matchlen-1] == '/' ||
+                     name[matchlen] == '\0' || name[matchlen] == '/')))
+                       return 1;
+               if (!fnmatch(match, name, 0))
+                       return 1;
+               if (name[namelen-1] != '/')
+                       continue;
+
+               /* We are being asked if the directory ("name") is worth
+                * descending into.
+                *
+                * Find the longest leading directory name that does
+                * not have metacharacter in the pathspec; the name
+                * we are looking at must overlap with that directory.
+                */
+               for (cp = match, meta = NULL; cp - match < matchlen; cp++) {
+                       char ch = *cp;
+                       if (ch == '*' || ch == '[' || ch == '?') {
+                               meta = cp;
+                               break;
+                       }
+               }
+               if (!meta)
+                       meta = cp; /* fully literal */
+
+               if (namelen <= meta - match) {
+                       /* Looking at "Documentation/" and
+                        * the pattern says "Documentation/howto/", or
+                        * "Documentation/diff*.txt".  The name we
+                        * have should match prefix.
+                        */
+                       if (!memcmp(match, name, namelen))
+                               return 1;
+                       continue;
+               }
+
+               if (meta - match < namelen) {
+                       /* Looking at "Documentation/howto/" and
+                        * the pattern says "Documentation/h*";
+                        * match up to "Do.../h"; this avoids descending
+                        * into "Documentation/technical/".
+                        */
+                       if (!memcmp(match, name, meta - match))
+                               return 1;
+                       continue;
+               }
+       }
+       return 0;
+}
+
+static int grep_sha1(struct grep_opt *opt, const unsigned char *sha1, const char *name, int tree_name_len)
+{
+       unsigned long size;
+       char *data;
+       enum object_type type;
+       char *to_free = NULL;
+       int hit;
+
+       data = read_sha1_file(sha1, &type, &size);
+       if (!data) {
+               error("'%s': unable to read %s", name, sha1_to_hex(sha1));
+               return 0;
+       }
+       if (opt->relative && opt->prefix_length) {
+               static char name_buf[PATH_MAX];
+               char *cp;
+               int name_len = strlen(name) - opt->prefix_length + 1;
+
+               if (!tree_name_len)
+                       name += opt->prefix_length;
+               else {
+                       if (ARRAY_SIZE(name_buf) <= name_len)
+                               cp = to_free = xmalloc(name_len);
+                       else
+                               cp = name_buf;
+                       memcpy(cp, name, tree_name_len);
+                       strcpy(cp + tree_name_len,
+                              name + tree_name_len + opt->prefix_length);
+                       name = cp;
+               }
+       }
+       hit = grep_buffer(opt, name, data, size);
+       free(data);
+       free(to_free);
+       return hit;
+}
+
+static int grep_file(struct grep_opt *opt, const char *filename)
+{
+       struct stat st;
+       int i;
+       char *data;
+       size_t sz;
+
+       if (lstat(filename, &st) < 0) {
+       err_ret:
+               if (errno != ENOENT)
+                       error("'%s': %s", filename, strerror(errno));
+               return 0;
+       }
+       if (!st.st_size)
+               return 0; /* empty file -- no grep hit */
+       if (!S_ISREG(st.st_mode))
+               return 0;
+       sz = xsize_t(st.st_size);
+       i = open(filename, O_RDONLY);
+       if (i < 0)
+               goto err_ret;
+       data = xmalloc(sz + 1);
+       if (st.st_size != read_in_full(i, data, sz)) {
+               error("'%s': short read %s", filename, strerror(errno));
+               close(i);
+               free(data);
+               return 0;
+       }
+       close(i);
+       if (opt->relative && opt->prefix_length)
+               filename += opt->prefix_length;
+       i = grep_buffer(opt, filename, data, sz);
+       free(data);
+       return i;
+}
+
+#ifdef __unix__
+static int exec_grep(int argc, const char **argv)
+{
+       pid_t pid;
+       int status;
+
+       argv[argc] = NULL;
+       pid = fork();
+       if (pid < 0)
+               return pid;
+       if (!pid) {
+               execvp("grep", (char **) argv);
+               exit(255);
+       }
+       while (waitpid(pid, &status, 0) < 0) {
+               if (errno == EINTR)
+                       continue;
+               return -1;
+       }
+       if (WIFEXITED(status)) {
+               if (!WEXITSTATUS(status))
+                       return 1;
+               return 0;
+       }
+       return -1;
+}
+
+#define MAXARGS 1000
+#define ARGBUF 4096
+#define push_arg(a) do { \
+       if (nr < MAXARGS) argv[nr++] = (a); \
+       else die("maximum number of args exceeded"); \
+       } while (0)
+
+/*
+ * If you send a singleton filename to grep, it does not give
+ * the name of the file.  GNU grep has "-H" but we would want
+ * that behaviour in a portable way.
+ *
+ * So we keep two pathnames in argv buffer unsent to grep in
+ * the main loop if we need to do more than one grep.
+ */
+static int flush_grep(struct grep_opt *opt,
+                     int argc, int arg0, const char **argv, int *kept)
+{
+       int status;
+       int count = argc - arg0;
+       const char *kept_0 = NULL;
+
+       if (count <= 2) {
+               /*
+                * Because we keep at least 2 paths in the call from
+                * the main loop (i.e. kept != NULL), and MAXARGS is
+                * far greater than 2, this usually is a call to
+                * conclude the grep.  However, the user could attempt
+                * to overflow the argv buffer by giving too many
+                * options to leave very small number of real
+                * arguments even for the call in the main loop.
+                */
+               if (kept)
+                       die("insanely many options to grep");
+
+               /*
+                * If we have two or more paths, we do not have to do
+                * anything special, but we need to push /dev/null to
+                * get "-H" behaviour of GNU grep portably but when we
+                * are not doing "-l" nor "-L" nor "-c".
+                */
+               if (count == 1 &&
+                   !opt->name_only &&
+                   !opt->unmatch_name_only &&
+                   !opt->count) {
+                       argv[argc++] = "/dev/null";
+                       argv[argc] = NULL;
+               }
+       }
+
+       else if (kept) {
+               /*
+                * Called because we found many paths and haven't finished
+                * iterating over the cache yet.  We keep two paths
+                * for the concluding call.  argv[argc-2] and argv[argc-1]
+                * has the last two paths, so save the first one away,
+                * replace it with NULL while sending the list to grep,
+                * and recover them after we are done.
+                */
+               *kept = 2;
+               kept_0 = argv[argc-2];
+               argv[argc-2] = NULL;
+               argc -= 2;
+       }
+
+       status = exec_grep(argc, argv);
+
+       if (kept_0) {
+               /*
+                * Then recover them.  Now the last arg is beyond the
+                * terminating NULL which is at argc, and the second
+                * from the last is what we saved away in kept_0
+                */
+               argv[arg0++] = kept_0;
+               argv[arg0] = argv[argc+1];
+       }
+       return status;
+}
+
+static int external_grep(struct grep_opt *opt, const char **paths, int cached)
+{
+       int i, nr, argc, hit, len, status;
+       const char *argv[MAXARGS+1];
+       char randarg[ARGBUF];
+       char *argptr = randarg;
+       struct grep_pat *p;
+
+       if (opt->extended || (opt->relative && opt->prefix_length))
+               return -1;
+       len = nr = 0;
+       push_arg("grep");
+       if (opt->fixed)
+               push_arg("-F");
+       if (opt->linenum)
+               push_arg("-n");
+       if (!opt->pathname)
+               push_arg("-h");
+       if (opt->regflags & REG_EXTENDED)
+               push_arg("-E");
+       if (opt->regflags & REG_ICASE)
+               push_arg("-i");
+       if (opt->word_regexp)
+               push_arg("-w");
+       if (opt->name_only)
+               push_arg("-l");
+       if (opt->unmatch_name_only)
+               push_arg("-L");
+       if (opt->count)
+               push_arg("-c");
+       if (opt->post_context || opt->pre_context) {
+               if (opt->post_context != opt->pre_context) {
+                       if (opt->pre_context) {
+                               push_arg("-B");
+                               len += snprintf(argptr, sizeof(randarg)-len,
+                                               "%u", opt->pre_context) + 1;
+                               if (sizeof(randarg) <= len)
+                                       die("maximum length of args exceeded");
+                               push_arg(argptr);
+                               argptr += len;
+                       }
+                       if (opt->post_context) {
+                               push_arg("-A");
+                               len += snprintf(argptr, sizeof(randarg)-len,
+                                               "%u", opt->post_context) + 1;
+                               if (sizeof(randarg) <= len)
+                                       die("maximum length of args exceeded");
+                               push_arg(argptr);
+                               argptr += len;
+                       }
+               }
+               else {
+                       push_arg("-C");
+                       len += snprintf(argptr, sizeof(randarg)-len,
+                                       "%u", opt->post_context) + 1;
+                       if (sizeof(randarg) <= len)
+                               die("maximum length of args exceeded");
+                       push_arg(argptr);
+                       argptr += len;
+               }
+       }
+       for (p = opt->pattern_list; p; p = p->next) {
+               push_arg("-e");
+               push_arg(p->pattern);
+       }
+
+       hit = 0;
+       argc = nr;
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               char *name;
+               int kept;
+               if (!S_ISREG(ntohl(ce->ce_mode)))
+                       continue;
+               if (!pathspec_matches(paths, ce->name))
+                       continue;
+               name = ce->name;
+               if (name[0] == '-') {
+                       int len = ce_namelen(ce);
+                       name = xmalloc(len + 3);
+                       memcpy(name, "./", 2);
+                       memcpy(name + 2, ce->name, len + 1);
+               }
+               argv[argc++] = name;
+               if (MAXARGS <= argc) {
+                       status = flush_grep(opt, argc, nr, argv, &kept);
+                       if (0 < status)
+                               hit = 1;
+                       argc = nr + kept;
+               }
+               if (ce_stage(ce)) {
+                       do {
+                               i++;
+                       } while (i < active_nr &&
+                                !strcmp(ce->name, active_cache[i]->name));
+                       i--; /* compensate for loop control */
+               }
+       }
+       if (argc > nr) {
+               status = flush_grep(opt, argc, nr, argv, NULL);
+               if (0 < status)
+                       hit = 1;
+       }
+       return hit;
+}
+#endif
+
+static int grep_cache(struct grep_opt *opt, const char **paths, int cached)
+{
+       int hit = 0;
+       int nr;
+       read_cache();
+
+#ifdef __unix__
+       /*
+        * Use the external "grep" command for the case where
+        * we grep through the checked-out files. It tends to
+        * be a lot more optimized
+        */
+       if (!cached) {
+               hit = external_grep(opt, paths, cached);
+               if (hit >= 0)
+                       return hit;
+       }
+#endif
+
+       for (nr = 0; nr < active_nr; nr++) {
+               struct cache_entry *ce = active_cache[nr];
+               if (!S_ISREG(ntohl(ce->ce_mode)))
+                       continue;
+               if (!pathspec_matches(paths, ce->name))
+                       continue;
+               if (cached) {
+                       if (ce_stage(ce))
+                               continue;
+                       hit |= grep_sha1(opt, ce->sha1, ce->name, 0);
+               }
+               else
+                       hit |= grep_file(opt, ce->name);
+               if (ce_stage(ce)) {
+                       do {
+                               nr++;
+                       } while (nr < active_nr &&
+                                !strcmp(ce->name, active_cache[nr]->name));
+                       nr--; /* compensate for loop control */
+               }
+       }
+       free_grep_patterns(opt);
+       return hit;
+}
+
+static int grep_tree(struct grep_opt *opt, const char **paths,
+                    struct tree_desc *tree,
+                    const char *tree_name, const char *base)
+{
+       int len;
+       int hit = 0;
+       struct name_entry entry;
+       char *down;
+       int tn_len = strlen(tree_name);
+       char *path_buf = xmalloc(PATH_MAX + tn_len + 100);
+
+       if (tn_len) {
+               tn_len = sprintf(path_buf, "%s:", tree_name);
+               down = path_buf + tn_len;
+               strcat(down, base);
+       }
+       else {
+               down = path_buf;
+               strcpy(down, base);
+       }
+       len = strlen(path_buf);
+
+       while (tree_entry(tree, &entry)) {
+               strcpy(path_buf + len, entry.path);
+
+               if (S_ISDIR(entry.mode))
+                       /* Match "abc/" against pathspec to
+                        * decide if we want to descend into "abc"
+                        * directory.
+                        */
+                       strcpy(path_buf + len + tree_entry_len(entry.path, entry.sha1), "/");
+
+               if (!pathspec_matches(paths, down))
+                       ;
+               else if (S_ISREG(entry.mode))
+                       hit |= grep_sha1(opt, entry.sha1, path_buf, tn_len);
+               else if (S_ISDIR(entry.mode)) {
+                       enum object_type type;
+                       struct tree_desc sub;
+                       void *data;
+                       unsigned long size;
+
+                       data = read_sha1_file(entry.sha1, &type, &size);
+                       if (!data)
+                               die("unable to read tree (%s)",
+                                   sha1_to_hex(entry.sha1));
+                       init_tree_desc(&sub, data, size);
+                       hit |= grep_tree(opt, paths, &sub, tree_name, down);
+                       free(data);
+               }
+       }
+       return hit;
+}
+
+static int grep_object(struct grep_opt *opt, const char **paths,
+                      struct object *obj, const char *name)
+{
+       if (obj->type == OBJ_BLOB)
+               return grep_sha1(opt, obj->sha1, name, 0);
+       if (obj->type == OBJ_COMMIT || obj->type == OBJ_TREE) {
+               struct tree_desc tree;
+               void *data;
+               unsigned long size;
+               int hit;
+               data = read_object_with_reference(obj->sha1, tree_type,
+                                                 &size, NULL);
+               if (!data)
+                       die("unable to read tree (%s)", sha1_to_hex(obj->sha1));
+               init_tree_desc(&tree, data, size);
+               hit = grep_tree(opt, paths, &tree, name, "");
+               free(data);
+               return hit;
+       }
+       die("unable to grep from object of type %s", typename(obj->type));
+}
+
+static const char builtin_grep_usage[] =
+"git-grep <option>* <rev>* [-e] <pattern> [<path>...]";
+
+static const char emsg_invalid_context_len[] =
+"%s: invalid context length argument";
+static const char emsg_missing_context_len[] =
+"missing context length argument";
+static const char emsg_missing_argument[] =
+"option requires an argument -%s";
+
+int cmd_grep(int argc, const char **argv, const char *prefix)
+{
+       int hit = 0;
+       int cached = 0;
+       int seen_dashdash = 0;
+       struct grep_opt opt;
+       struct object_array list = { 0, 0, NULL };
+       const char **paths = NULL;
+       int i;
+
+       memset(&opt, 0, sizeof(opt));
+       opt.prefix_length = (prefix && *prefix) ? strlen(prefix) : 0;
+       opt.relative = 1;
+       opt.pathname = 1;
+       opt.pattern_tail = &opt.pattern_list;
+       opt.regflags = REG_NEWLINE;
+
+       /*
+        * If there is no -- then the paths must exist in the working
+        * tree.  If there is no explicit pattern specified with -e or
+        * -f, we take the first unrecognized non option to be the
+        * pattern, but then what follows it must be zero or more
+        * valid refs up to the -- (if exists), and then existing
+        * paths.  If there is an explicit pattern, then the first
+        * unrecognized non option is the beginning of the refs list
+        * that continues up to the -- (if exists), and then paths.
+        */
+
+       while (1 < argc) {
+               const char *arg = argv[1];
+               argc--; argv++;
+               if (!strcmp("--cached", arg)) {
+                       cached = 1;
+                       continue;
+               }
+               if (!strcmp("-a", arg) ||
+                   !strcmp("--text", arg)) {
+                       opt.binary = GREP_BINARY_TEXT;
+                       continue;
+               }
+               if (!strcmp("-i", arg) ||
+                   !strcmp("--ignore-case", arg)) {
+                       opt.regflags |= REG_ICASE;
+                       continue;
+               }
+               if (!strcmp("-I", arg)) {
+                       opt.binary = GREP_BINARY_NOMATCH;
+                       continue;
+               }
+               if (!strcmp("-v", arg) ||
+                   !strcmp("--invert-match", arg)) {
+                       opt.invert = 1;
+                       continue;
+               }
+               if (!strcmp("-E", arg) ||
+                   !strcmp("--extended-regexp", arg)) {
+                       opt.regflags |= REG_EXTENDED;
+                       continue;
+               }
+               if (!strcmp("-F", arg) ||
+                   !strcmp("--fixed-strings", arg)) {
+                       opt.fixed = 1;
+                       continue;
+               }
+               if (!strcmp("-G", arg) ||
+                   !strcmp("--basic-regexp", arg)) {
+                       opt.regflags &= ~REG_EXTENDED;
+                       continue;
+               }
+               if (!strcmp("-n", arg)) {
+                       opt.linenum = 1;
+                       continue;
+               }
+               if (!strcmp("-h", arg)) {
+                       opt.pathname = 0;
+                       continue;
+               }
+               if (!strcmp("-H", arg)) {
+                       opt.pathname = 1;
+                       continue;
+               }
+               if (!strcmp("-l", arg) ||
+                   !strcmp("--files-with-matches", arg)) {
+                       opt.name_only = 1;
+                       continue;
+               }
+               if (!strcmp("-L", arg) ||
+                   !strcmp("--files-without-match", arg)) {
+                       opt.unmatch_name_only = 1;
+                       continue;
+               }
+               if (!strcmp("-c", arg) ||
+                   !strcmp("--count", arg)) {
+                       opt.count = 1;
+                       continue;
+               }
+               if (!strcmp("-w", arg) ||
+                   !strcmp("--word-regexp", arg)) {
+                       opt.word_regexp = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "-A") ||
+                   !prefixcmp(arg, "-B") ||
+                   !prefixcmp(arg, "-C") ||
+                   (arg[0] == '-' && '1' <= arg[1] && arg[1] <= '9')) {
+                       unsigned num;
+                       const char *scan;
+                       switch (arg[1]) {
+                       case 'A': case 'B': case 'C':
+                               if (!arg[2]) {
+                                       if (argc <= 1)
+                                               die(emsg_missing_context_len);
+                                       scan = *++argv;
+                                       argc--;
+                               }
+                               else
+                                       scan = arg + 2;
+                               break;
+                       default:
+                               scan = arg + 1;
+                               break;
+                       }
+                       if (strtoul_ui(scan, 10, &num))
+                               die(emsg_invalid_context_len, scan);
+                       switch (arg[1]) {
+                       case 'A':
+                               opt.post_context = num;
+                               break;
+                       default:
+                       case 'C':
+                               opt.post_context = num;
+                       case 'B':
+                               opt.pre_context = num;
+                               break;
+                       }
+                       continue;
+               }
+               if (!strcmp("-f", arg)) {
+                       FILE *patterns;
+                       int lno = 0;
+                       char buf[1024];
+                       if (argc <= 1)
+                               die(emsg_missing_argument, arg);
+                       patterns = fopen(argv[1], "r");
+                       if (!patterns)
+                               die("'%s': %s", argv[1], strerror(errno));
+                       while (fgets(buf, sizeof(buf), patterns)) {
+                               int len = strlen(buf);
+                               if (buf[len-1] == '\n')
+                                       buf[len-1] = 0;
+                               /* ignore empty line like grep does */
+                               if (!buf[0])
+                                       continue;
+                               append_grep_pattern(&opt, xstrdup(buf),
+                                                   argv[1], ++lno,
+                                                   GREP_PATTERN);
+                       }
+                       fclose(patterns);
+                       argv++;
+                       argc--;
+                       continue;
+               }
+               if (!strcmp("--not", arg)) {
+                       append_grep_pattern(&opt, arg, "command line", 0,
+                                           GREP_NOT);
+                       continue;
+               }
+               if (!strcmp("--and", arg)) {
+                       append_grep_pattern(&opt, arg, "command line", 0,
+                                           GREP_AND);
+                       continue;
+               }
+               if (!strcmp("--or", arg))
+                       continue; /* no-op */
+               if (!strcmp("(", arg)) {
+                       append_grep_pattern(&opt, arg, "command line", 0,
+                                           GREP_OPEN_PAREN);
+                       continue;
+               }
+               if (!strcmp(")", arg)) {
+                       append_grep_pattern(&opt, arg, "command line", 0,
+                                           GREP_CLOSE_PAREN);
+                       continue;
+               }
+               if (!strcmp("--all-match", arg)) {
+                       opt.all_match = 1;
+                       continue;
+               }
+               if (!strcmp("-e", arg)) {
+                       if (1 < argc) {
+                               append_grep_pattern(&opt, argv[1],
+                                                   "-e option", 0,
+                                                   GREP_PATTERN);
+                               argv++;
+                               argc--;
+                               continue;
+                       }
+                       die(emsg_missing_argument, arg);
+               }
+               if (!strcmp("--full-name", arg)) {
+                       opt.relative = 0;
+                       continue;
+               }
+               if (!strcmp("--", arg)) {
+                       /* later processing wants to have this at argv[1] */
+                       argv--;
+                       argc++;
+                       break;
+               }
+               if (*arg == '-')
+                       usage(builtin_grep_usage);
+
+               /* First unrecognized non-option token */
+               if (!opt.pattern_list) {
+                       append_grep_pattern(&opt, arg, "command line", 0,
+                                           GREP_PATTERN);
+                       break;
+               }
+               else {
+                       /* We are looking at the first path or rev;
+                        * it is found at argv[1] after leaving the
+                        * loop.
+                        */
+                       argc++; argv--;
+                       break;
+               }
+       }
+
+       if (!opt.pattern_list)
+               die("no pattern given.");
+       if ((opt.regflags != REG_NEWLINE) && opt.fixed)
+               die("cannot mix --fixed-strings and regexp");
+       compile_grep_patterns(&opt);
+
+       /* Check revs and then paths */
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               unsigned char sha1[20];
+               /* Is it a rev? */
+               if (!get_sha1(arg, sha1)) {
+                       struct object *object = parse_object(sha1);
+                       if (!object)
+                               die("bad object %s", arg);
+                       add_object_array(object, arg, &list);
+                       continue;
+               }
+               if (!strcmp(arg, "--")) {
+                       i++;
+                       seen_dashdash = 1;
+               }
+               break;
+       }
+
+       /* The rest are paths */
+       if (!seen_dashdash) {
+               int j;
+               for (j = i; j < argc; j++)
+                       verify_filename(prefix, argv[j]);
+       }
+
+       if (i < argc) {
+               paths = get_pathspec(prefix, argv + i);
+               if (opt.prefix_length && opt.relative) {
+                       /* Make sure we do not get outside of paths */
+                       for (i = 0; paths[i]; i++)
+                               if (strncmp(prefix, paths[i], opt.prefix_length))
+                                       die("git-grep: cannot generate relative filenames containing '..'");
+               }
+       }
+       else if (prefix) {
+               paths = xcalloc(2, sizeof(const char *));
+               paths[0] = prefix;
+               paths[1] = NULL;
+       }
+
+       if (!list.nr)
+               return !grep_cache(&opt, paths, cached);
+
+       if (cached)
+               die("both --cached and trees are given.");
+
+       for (i = 0; i < list.nr; i++) {
+               struct object *real_obj;
+               real_obj = deref_tag(list.objects[i].item, NULL, 0);
+               if (grep_object(&opt, paths, real_obj, list.objects[i].name))
+                       hit = 1;
+       }
+       free_grep_patterns(&opt);
+       return !hit;
+}
diff --git a/builtin-http-fetch.c b/builtin-http-fetch.c
new file mode 100644 (file)
index 0000000..4a50dbd
--- /dev/null
@@ -0,0 +1,77 @@
+#include "cache.h"
+#include "walker.h"
+
+int cmd_http_fetch(int argc, const char **argv, const char *prefix)
+{
+       struct walker *walker;
+       int commits_on_stdin = 0;
+       int commits;
+       const char **write_ref = NULL;
+       char **commit_id;
+       const char *url;
+       int arg = 1;
+       int rc = 0;
+       int get_tree = 0;
+       int get_history = 0;
+       int get_all = 0;
+       int get_verbosely = 0;
+       int get_recover = 0;
+
+       git_config(git_default_config);
+
+       while (arg < argc && argv[arg][0] == '-') {
+               if (argv[arg][1] == 't') {
+                       get_tree = 1;
+               } else if (argv[arg][1] == 'c') {
+                       get_history = 1;
+               } else if (argv[arg][1] == 'a') {
+                       get_all = 1;
+                       get_tree = 1;
+                       get_history = 1;
+               } else if (argv[arg][1] == 'v') {
+                       get_verbosely = 1;
+               } else if (argv[arg][1] == 'w') {
+                       write_ref = &argv[arg + 1];
+                       arg++;
+               } else if (!strcmp(argv[arg], "--recover")) {
+                       get_recover = 1;
+               } else if (!strcmp(argv[arg], "--stdin")) {
+                       commits_on_stdin = 1;
+               }
+               arg++;
+       }
+       if (argc < arg + 2 - commits_on_stdin) {
+               usage("git-http-fetch [-c] [-t] [-a] [-v] [--recover] [-w ref] [--stdin] commit-id url");
+               return 1;
+       }
+       if (commits_on_stdin) {
+               commits = walker_targets_stdin(&commit_id, &write_ref);
+       } else {
+               commit_id = (char **) &argv[arg++];
+               commits = 1;
+       }
+       url = argv[arg];
+
+       walker = get_http_walker(url);
+       walker->get_tree = get_tree;
+       walker->get_history = get_history;
+       walker->get_all = get_all;
+       walker->get_verbosely = get_verbosely;
+       walker->get_recover = get_recover;
+
+       rc = walker_fetch(walker, commits, commit_id, write_ref, url);
+
+       if (commits_on_stdin)
+               walker_targets_free(commits, commit_id, write_ref);
+
+       if (walker->corrupt_object_found) {
+               fprintf(stderr,
+"Some loose object were found to be corrupt, but they might be just\n"
+"a false '404 Not Found' error message sent with incorrect HTTP\n"
+"status code.  Suggest running git-fsck.\n");
+       }
+
+       walker_free(walker);
+
+       return rc;
+}
diff --git a/builtin-init-db.c b/builtin-init-db.c
new file mode 100644 (file)
index 0000000..e1393b8
--- /dev/null
@@ -0,0 +1,437 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "exec_cmd.h"
+
+#ifndef DEFAULT_GIT_TEMPLATE_DIR
+#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates"
+#endif
+
+#ifdef NO_TRUSTABLE_FILEMODE
+#define TEST_FILEMODE 0
+#else
+#define TEST_FILEMODE 1
+#endif
+
+static void safe_create_dir(const char *dir, int share)
+{
+       if (mkdir(dir, 0777) < 0) {
+               if (errno != EEXIST) {
+                       perror(dir);
+                       exit(1);
+               }
+       }
+       else if (share && adjust_shared_perm(dir))
+               die("Could not make %s writable by group\n", dir);
+}
+
+static int copy_file(const char *dst, const char *src, int mode)
+{
+       int fdi, fdo, status;
+
+       mode = (mode & 0111) ? 0777 : 0666;
+       if ((fdi = open(src, O_RDONLY)) < 0)
+               return fdi;
+       if ((fdo = open(dst, O_WRONLY | O_CREAT | O_EXCL, mode)) < 0) {
+               close(fdi);
+               return fdo;
+       }
+       status = copy_fd(fdi, fdo);
+       if (close(fdo) != 0)
+               return error("%s: write error: %s", dst, strerror(errno));
+
+       if (!status && adjust_shared_perm(dst))
+               return -1;
+
+       return status;
+}
+
+static void copy_templates_1(char *path, int baselen,
+                            char *template, int template_baselen,
+                            DIR *dir)
+{
+       struct dirent *de;
+
+       /* Note: if ".git/hooks" file exists in the repository being
+        * re-initialized, /etc/core-git/templates/hooks/update would
+        * cause git-init to fail here.  I think this is sane but
+        * it means that the set of templates we ship by default, along
+        * with the way the namespace under .git/ is organized, should
+        * be really carefully chosen.
+        */
+       safe_create_dir(path, 1);
+       while ((de = readdir(dir)) != NULL) {
+               struct stat st_git, st_template;
+               int namelen;
+               int exists = 0;
+
+               if (de->d_name[0] == '.')
+                       continue;
+               namelen = strlen(de->d_name);
+               if ((PATH_MAX <= baselen + namelen) ||
+                   (PATH_MAX <= template_baselen + namelen))
+                       die("insanely long template name %s", de->d_name);
+               memcpy(path + baselen, de->d_name, namelen+1);
+               memcpy(template + template_baselen, de->d_name, namelen+1);
+               if (lstat(path, &st_git)) {
+                       if (errno != ENOENT)
+                               die("cannot stat %s", path);
+               }
+               else
+                       exists = 1;
+
+               if (lstat(template, &st_template))
+                       die("cannot stat template %s", template);
+
+               if (S_ISDIR(st_template.st_mode)) {
+                       DIR *subdir = opendir(template);
+                       int baselen_sub = baselen + namelen;
+                       int template_baselen_sub = template_baselen + namelen;
+                       if (!subdir)
+                               die("cannot opendir %s", template);
+                       path[baselen_sub++] =
+                               template[template_baselen_sub++] = '/';
+                       path[baselen_sub] =
+                               template[template_baselen_sub] = 0;
+                       copy_templates_1(path, baselen_sub,
+                                        template, template_baselen_sub,
+                                        subdir);
+                       closedir(subdir);
+               }
+               else if (exists)
+                       continue;
+               else if (S_ISLNK(st_template.st_mode)) {
+                       char lnk[256];
+                       int len;
+                       len = readlink(template, lnk, sizeof(lnk));
+                       if (len < 0)
+                               die("cannot readlink %s", template);
+                       if (sizeof(lnk) <= len)
+                               die("insanely long symlink %s", template);
+                       lnk[len] = 0;
+                       if (symlink(lnk, path))
+                               die("cannot symlink %s %s", lnk, path);
+               }
+               else if (S_ISREG(st_template.st_mode)) {
+                       if (copy_file(path, template, st_template.st_mode))
+                               die("cannot copy %s to %s", template, path);
+               }
+               else
+                       error("ignoring template %s", template);
+       }
+}
+
+static void copy_templates(const char *git_dir, int len, const char *template_dir)
+{
+       char path[PATH_MAX];
+       char template_path[PATH_MAX];
+       int template_len;
+       DIR *dir;
+
+       if (!template_dir)
+               template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT);
+       if (!template_dir) {
+               /*
+                * if the hard-coded template is relative, it is
+                * interpreted relative to the exec_dir
+                */
+               template_dir = DEFAULT_GIT_TEMPLATE_DIR;
+               if (!is_absolute_path(template_dir)) {
+                       const char *exec_path = git_exec_path();
+                       template_dir = prefix_path(exec_path, strlen(exec_path),
+                                                  template_dir);
+               }
+       }
+       strcpy(template_path, template_dir);
+       template_len = strlen(template_path);
+       if (template_path[template_len-1] != '/') {
+               template_path[template_len++] = '/';
+               template_path[template_len] = 0;
+       }
+       dir = opendir(template_path);
+       if (!dir) {
+               fprintf(stderr, "warning: templates not found %s\n",
+                       template_dir);
+               return;
+       }
+
+       /* Make sure that template is from the correct vintage */
+       strcpy(template_path + template_len, "config");
+       repository_format_version = 0;
+       git_config_from_file(check_repository_format_version,
+                            template_path);
+       template_path[template_len] = 0;
+
+       if (repository_format_version &&
+           repository_format_version != GIT_REPO_VERSION) {
+               fprintf(stderr, "warning: not copying templates of "
+                       "a wrong format version %d from '%s'\n",
+                       repository_format_version,
+                       template_dir);
+               closedir(dir);
+               return;
+       }
+
+       memcpy(path, git_dir, len);
+       path[len] = 0;
+       copy_templates_1(path, len,
+                        template_path, template_len,
+                        dir);
+       closedir(dir);
+}
+
+static int create_default_files(const char *git_dir, const char *template_path)
+{
+       unsigned len = strlen(git_dir);
+       static char path[PATH_MAX];
+       unsigned char sha1[20];
+       struct stat st1;
+       char repo_version_string[10];
+       int reinit;
+       int filemode;
+
+       if (len > sizeof(path)-50)
+               die("insane git directory %s", git_dir);
+       memcpy(path, git_dir, len);
+
+       if (len && path[len-1] != '/')
+               path[len++] = '/';
+
+       /*
+        * Create .git/refs/{heads,tags}
+        */
+       strcpy(path + len, "refs");
+       safe_create_dir(path, 1);
+       strcpy(path + len, "refs/heads");
+       safe_create_dir(path, 1);
+       strcpy(path + len, "refs/tags");
+       safe_create_dir(path, 1);
+
+       /* First copy the templates -- we might have the default
+        * config file there, in which case we would want to read
+        * from it after installing.
+        */
+       path[len] = 0;
+       copy_templates(path, len, template_path);
+
+       git_config(git_default_config);
+
+       /*
+        * We would have created the above under user's umask -- under
+        * shared-repository settings, we would need to fix them up.
+        */
+       if (shared_repository) {
+               path[len] = 0;
+               adjust_shared_perm(path);
+               strcpy(path + len, "refs");
+               adjust_shared_perm(path);
+               strcpy(path + len, "refs/heads");
+               adjust_shared_perm(path);
+               strcpy(path + len, "refs/tags");
+               adjust_shared_perm(path);
+       }
+
+       /*
+        * Create the default symlink from ".git/HEAD" to the "master"
+        * branch, if it does not exist yet.
+        */
+       strcpy(path + len, "HEAD");
+       reinit = !read_ref("HEAD", sha1);
+       if (!reinit) {
+               if (create_symref("HEAD", "refs/heads/master", NULL) < 0)
+                       exit(1);
+       }
+
+       /* This forces creation of new config file */
+       sprintf(repo_version_string, "%d", GIT_REPO_VERSION);
+       git_config_set("core.repositoryformatversion", repo_version_string);
+
+       path[len] = 0;
+       strcpy(path + len, "config");
+
+       /* Check filemode trustability */
+       filemode = TEST_FILEMODE;
+       if (TEST_FILEMODE && !lstat(path, &st1)) {
+               struct stat st2;
+               filemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&
+                               !lstat(path, &st2) &&
+                               st1.st_mode != st2.st_mode);
+       }
+       git_config_set("core.filemode", filemode ? "true" : "false");
+
+       if (is_bare_repository())
+               git_config_set("core.bare", "true");
+       else {
+               const char *work_tree = get_git_work_tree();
+               git_config_set("core.bare", "false");
+               /* allow template config file to override the default */
+               if (log_all_ref_updates == -1)
+                   git_config_set("core.logallrefupdates", "true");
+               if (work_tree != git_work_tree_cfg)
+                       git_config_set("core.worktree", work_tree);
+       }
+
+       /* Check if symlink is supported in the work tree */
+       if (!reinit) {
+               path[len] = 0;
+               strcpy(path + len, "tXXXXXX");
+               if (!close(xmkstemp(path)) &&
+                   !unlink(path) &&
+                   !symlink("testing", path) &&
+                   !lstat(path, &st1) &&
+                   S_ISLNK(st1.st_mode))
+                       unlink(path); /* good */
+               else
+                       git_config_set("core.symlinks", "false");
+       }
+
+       return reinit;
+}
+
+static void guess_repository_type(const char *git_dir)
+{
+       char cwd[PATH_MAX];
+       const char *slash;
+
+       if (0 <= is_bare_repository_cfg)
+               return;
+       if (!git_dir)
+               return;
+
+       /*
+        * "GIT_DIR=. git init" is always bare.
+        * "GIT_DIR=`pwd` git init" too.
+        */
+       if (!strcmp(".", git_dir))
+               goto force_bare;
+       if (!getcwd(cwd, sizeof(cwd)))
+               die("cannot tell cwd");
+       if (!strcmp(git_dir, cwd))
+               goto force_bare;
+       /*
+        * "GIT_DIR=.git or GIT_DIR=something/.git is usually not.
+        */
+       if (!strcmp(git_dir, ".git"))
+               return;
+       slash = strrchr(git_dir, '/');
+       if (slash && !strcmp(slash, "/.git"))
+               return;
+
+       /*
+        * Otherwise it is often bare.  At this point
+        * we are just guessing.
+        */
+ force_bare:
+       is_bare_repository_cfg = 1;
+       return;
+}
+
+static const char init_db_usage[] =
+"git-init [-q | --quiet] [--template=<template-directory>] [--shared]";
+
+/*
+ * If you want to, you can share the DB area with any number of branches.
+ * That has advantages: you can save space by sharing all the SHA1 objects.
+ * On the other hand, it might just make lookup slower and messier. You
+ * be the judge.  The default case is to have one DB per managed directory.
+ */
+int cmd_init_db(int argc, const char **argv, const char *prefix)
+{
+       const char *git_dir;
+       const char *sha1_dir;
+       const char *template_dir = NULL;
+       char *path;
+       int len, i, reinit;
+       int quiet = 0;
+
+       for (i = 1; i < argc; i++, argv++) {
+               const char *arg = argv[1];
+               if (!prefixcmp(arg, "--template="))
+                       template_dir = arg+11;
+               else if (!strcmp(arg, "--shared"))
+                       shared_repository = PERM_GROUP;
+               else if (!prefixcmp(arg, "--shared="))
+                       shared_repository = git_config_perm("arg", arg+9);
+               else if (!strcmp(arg, "-q") || !strcmp(arg, "--quiet"))
+                       quiet = 1;
+               else
+                       usage(init_db_usage);
+       }
+
+       /*
+        * GIT_WORK_TREE makes sense only in conjunction with GIT_DIR
+        * without --bare.  Catch the error early.
+        */
+       git_dir = getenv(GIT_DIR_ENVIRONMENT);
+       if ((!git_dir || is_bare_repository_cfg == 1)
+           && getenv(GIT_WORK_TREE_ENVIRONMENT))
+               die("%s (or --work-tree=<directory>) not allowed without "
+                   "specifying %s (or --git-dir=<directory>)",
+                   GIT_WORK_TREE_ENVIRONMENT,
+                   GIT_DIR_ENVIRONMENT);
+
+       guess_repository_type(git_dir);
+
+       if (is_bare_repository_cfg <= 0) {
+               git_work_tree_cfg = xcalloc(PATH_MAX, 1);
+               if (!getcwd(git_work_tree_cfg, PATH_MAX))
+                       die ("Cannot access current working directory.");
+               if (access(get_git_work_tree(), X_OK))
+                       die ("Cannot access work tree '%s'",
+                            get_git_work_tree());
+       }
+
+       /*
+        * Set up the default .git directory contents
+        */
+       git_dir = getenv(GIT_DIR_ENVIRONMENT);
+       if (!git_dir)
+               git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
+       safe_create_dir(git_dir, 0);
+
+       /* Check to see if the repository version is right.
+        * Note that a newly created repository does not have
+        * config file, so this will not fail.  What we are catching
+        * is an attempt to reinitialize new repository with an old tool.
+        */
+       check_repository_format();
+
+       reinit = create_default_files(git_dir, template_dir);
+
+       /*
+        * And set up the object store.
+        */
+       sha1_dir = get_object_directory();
+       len = strlen(sha1_dir);
+       path = xmalloc(len + 40);
+       memcpy(path, sha1_dir, len);
+
+       safe_create_dir(sha1_dir, 1);
+       strcpy(path+len, "/pack");
+       safe_create_dir(path, 1);
+       strcpy(path+len, "/info");
+       safe_create_dir(path, 1);
+
+       if (shared_repository) {
+               char buf[10];
+               /* We do not spell "group" and such, so that
+                * the configuration can be read by older version
+                * of git.
+                */
+               sprintf(buf, "%d", shared_repository);
+               git_config_set("core.sharedrepository", buf);
+               git_config_set("receive.denyNonFastforwards", "true");
+       }
+
+       if (!quiet)
+               printf("%s%s Git repository in %s/\n",
+                      reinit ? "Reinitialized existing" : "Initialized empty",
+                      shared_repository ? " shared" : "",
+                      git_dir);
+
+       return 0;
+}
diff --git a/builtin-log.c b/builtin-log.c
new file mode 100644 (file)
index 0000000..e1f1cf6
--- /dev/null
@@ -0,0 +1,941 @@
+/*
+ * Builtin "git log" and related commands (show, whatchanged)
+ *
+ * (C) Copyright 2006 Linus Torvalds
+ *              2006 Junio Hamano
+ */
+#include "cache.h"
+#include "commit.h"
+#include "diff.h"
+#include "revision.h"
+#include "log-tree.h"
+#include "builtin.h"
+#include "tag.h"
+#include "reflog-walk.h"
+#include "patch-ids.h"
+#include "refs.h"
+
+static int default_show_root = 1;
+static const char *fmt_patch_subject_prefix = "PATCH";
+
+/* this is in builtin-diff.c */
+void add_head(struct rev_info *revs);
+
+static void add_name_decoration(const char *prefix, const char *name, struct object *obj)
+{
+       int plen = strlen(prefix);
+       int nlen = strlen(name);
+       struct name_decoration *res = xmalloc(sizeof(struct name_decoration) + plen + nlen);
+       memcpy(res->name, prefix, plen);
+       memcpy(res->name + plen, name, nlen + 1);
+       res->next = add_decoration(&name_decoration, obj, res);
+}
+
+static int add_ref_decoration(const char *refname, const unsigned char *sha1, int flags, void *cb_data)
+{
+       struct object *obj = parse_object(sha1);
+       if (!obj)
+               return 0;
+       add_name_decoration("", refname, obj);
+       while (obj->type == OBJ_TAG) {
+               obj = ((struct tag *)obj)->tagged;
+               if (!obj)
+                       break;
+               add_name_decoration("tag: ", refname, obj);
+       }
+       return 0;
+}
+
+static void cmd_log_init(int argc, const char **argv, const char *prefix,
+                     struct rev_info *rev)
+{
+       int i;
+       int decorate = 0;
+
+       rev->abbrev = DEFAULT_ABBREV;
+       rev->commit_format = CMIT_FMT_DEFAULT;
+       rev->verbose_header = 1;
+       DIFF_OPT_SET(&rev->diffopt, RECURSIVE);
+       rev->show_root_diff = default_show_root;
+       rev->subject_prefix = fmt_patch_subject_prefix;
+       argc = setup_revisions(argc, argv, rev, "HEAD");
+       if (rev->diffopt.pickaxe || rev->diffopt.filter)
+               rev->always_show_header = 0;
+       if (DIFF_OPT_TST(&rev->diffopt, FOLLOW_RENAMES)) {
+               rev->always_show_header = 0;
+               if (rev->diffopt.nr_paths != 1)
+                       usage("git logs can only follow renames on one pathname at a time");
+       }
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (!strcmp(arg, "--decorate")) {
+                       if (!decorate)
+                               for_each_ref(add_ref_decoration, NULL);
+                       decorate = 1;
+               } else
+                       die("unrecognized argument: %s", arg);
+       }
+}
+
+/*
+ * This gives a rough estimate for how many commits we
+ * will print out in the list.
+ */
+static int estimate_commit_count(struct rev_info *rev, struct commit_list *list)
+{
+       int n = 0;
+
+       while (list) {
+               struct commit *commit = list->item;
+               unsigned int flags = commit->object.flags;
+               list = list->next;
+               if (!(flags & (TREESAME | UNINTERESTING)))
+                       n++;
+       }
+       return n;
+}
+
+static void show_early_header(struct rev_info *rev, const char *stage, int nr)
+{
+       if (rev->shown_one) {
+               rev->shown_one = 0;
+               if (rev->commit_format != CMIT_FMT_ONELINE)
+                       putchar(rev->diffopt.line_termination);
+       }
+       printf("Final output: %d %s\n", nr, stage);
+}
+
+struct itimerval early_output_timer;
+
+static void log_show_early(struct rev_info *revs, struct commit_list *list)
+{
+       int i = revs->early_output;
+       int show_header = 1;
+
+       sort_in_topological_order(&list, revs->lifo);
+       while (list && i) {
+               struct commit *commit = list->item;
+               switch (simplify_commit(revs, commit)) {
+               case commit_show:
+                       if (show_header) {
+                               int n = estimate_commit_count(revs, list);
+                               show_early_header(revs, "incomplete", n);
+                               show_header = 0;
+                       }
+                       log_tree_commit(revs, commit);
+                       i--;
+                       break;
+               case commit_ignore:
+                       break;
+               case commit_error:
+                       return;
+               }
+               list = list->next;
+       }
+
+       /* Did we already get enough commits for the early output? */
+       if (!i)
+               return;
+
+       /*
+        * ..if no, then repeat it twice a second until we
+        * do.
+        *
+        * NOTE! We don't use "it_interval", because if the
+        * reader isn't listening, we want our output to be
+        * throttled by the writing, and not have the timer
+        * trigger every second even if we're blocked on a
+        * reader!
+        */
+       early_output_timer.it_value.tv_sec = 0;
+       early_output_timer.it_value.tv_usec = 500000;
+       setitimer(ITIMER_REAL, &early_output_timer, NULL);
+}
+
+static void early_output(int signal)
+{
+       show_early_output = log_show_early;
+}
+
+static void setup_early_output(struct rev_info *rev)
+{
+       struct sigaction sa;
+
+       /*
+        * Set up the signal handler, minimally intrusively:
+        * we only set a single volatile integer word (not
+        * using sigatomic_t - trying to avoid unnecessary
+        * system dependencies and headers), and using
+        * SA_RESTART.
+        */
+       memset(&sa, 0, sizeof(sa));
+       sa.sa_handler = early_output;
+       sigemptyset(&sa.sa_mask);
+       sa.sa_flags = SA_RESTART;
+       sigaction(SIGALRM, &sa, NULL);
+
+       /*
+        * If we can get the whole output in less than a
+        * tenth of a second, don't even bother doing the
+        * early-output thing..
+        *
+        * This is a one-time-only trigger.
+        */
+       early_output_timer.it_value.tv_sec = 0;
+       early_output_timer.it_value.tv_usec = 100000;
+       setitimer(ITIMER_REAL, &early_output_timer, NULL);
+}
+
+static void finish_early_output(struct rev_info *rev)
+{
+       int n = estimate_commit_count(rev, rev->commits);
+       signal(SIGALRM, SIG_IGN);
+       show_early_header(rev, "done", n);
+}
+
+static int cmd_log_walk(struct rev_info *rev)
+{
+       struct commit *commit;
+
+       if (rev->early_output)
+               setup_early_output(rev);
+
+       prepare_revision_walk(rev);
+
+       if (rev->early_output)
+               finish_early_output(rev);
+
+       while ((commit = get_revision(rev)) != NULL) {
+               log_tree_commit(rev, commit);
+               if (!rev->reflog_info) {
+                       /* we allow cycles in reflog ancestry */
+                       free(commit->buffer);
+                       commit->buffer = NULL;
+               }
+               free_commit_list(commit->parents);
+               commit->parents = NULL;
+       }
+       return 0;
+}
+
+static int git_log_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "format.subjectprefix")) {
+               if (!value)
+                       die("format.subjectprefix without value");
+               fmt_patch_subject_prefix = xstrdup(value);
+               return 0;
+       }
+       if (!strcmp(var, "log.showroot")) {
+               default_show_root = git_config_bool(var, value);
+               return 0;
+       }
+       return git_diff_ui_config(var, value);
+}
+
+int cmd_whatchanged(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info rev;
+
+       git_config(git_log_config);
+       init_revisions(&rev, prefix);
+       rev.diff = 1;
+       rev.simplify_history = 0;
+       cmd_log_init(argc, argv, prefix, &rev);
+       if (!rev.diffopt.output_format)
+               rev.diffopt.output_format = DIFF_FORMAT_RAW;
+       return cmd_log_walk(&rev);
+}
+
+static int show_object(const unsigned char *sha1, int suppress_header)
+{
+       unsigned long size;
+       enum object_type type;
+       char *buf = read_sha1_file(sha1, &type, &size);
+       int offset = 0;
+
+       if (!buf)
+               return error("Could not read object %s", sha1_to_hex(sha1));
+
+       if (suppress_header)
+               while (offset < size && buf[offset++] != '\n') {
+                       int new_offset = offset;
+                       while (new_offset < size && buf[new_offset++] != '\n')
+                               ; /* do nothing */
+                       offset = new_offset;
+               }
+
+       if (offset < size)
+               fwrite(buf + offset, size - offset, 1, stdout);
+       free(buf);
+       return 0;
+}
+
+static int show_tree_object(const unsigned char *sha1,
+               const char *base, int baselen,
+               const char *pathname, unsigned mode, int stage)
+{
+       printf("%s%s\n", pathname, S_ISDIR(mode) ? "/" : "");
+       return 0;
+}
+
+int cmd_show(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info rev;
+       struct object_array_entry *objects;
+       int i, count, ret = 0;
+
+       git_config(git_log_config);
+       init_revisions(&rev, prefix);
+       rev.diff = 1;
+       rev.combine_merges = 1;
+       rev.dense_combined_merges = 1;
+       rev.always_show_header = 1;
+       rev.ignore_merges = 0;
+       rev.no_walk = 1;
+       cmd_log_init(argc, argv, prefix, &rev);
+
+       count = rev.pending.nr;
+       objects = rev.pending.objects;
+       for (i = 0; i < count && !ret; i++) {
+               struct object *o = objects[i].item;
+               const char *name = objects[i].name;
+               switch (o->type) {
+               case OBJ_BLOB:
+                       ret = show_object(o->sha1, 0);
+                       break;
+               case OBJ_TAG: {
+                       struct tag *t = (struct tag *)o;
+
+                       printf("%stag %s%s\n\n",
+                                       diff_get_color_opt(&rev.diffopt, DIFF_COMMIT),
+                                       t->tag,
+                                       diff_get_color_opt(&rev.diffopt, DIFF_RESET));
+                       ret = show_object(o->sha1, 1);
+                       objects[i].item = (struct object *)t->tagged;
+                       i--;
+                       break;
+               }
+               case OBJ_TREE:
+                       printf("%stree %s%s\n\n",
+                                       diff_get_color_opt(&rev.diffopt, DIFF_COMMIT),
+                                       name,
+                                       diff_get_color_opt(&rev.diffopt, DIFF_RESET));
+                       read_tree_recursive((struct tree *)o, "", 0, 0, NULL,
+                                       show_tree_object);
+                       break;
+               case OBJ_COMMIT:
+                       rev.pending.nr = rev.pending.alloc = 0;
+                       rev.pending.objects = NULL;
+                       add_object_array(o, name, &rev.pending);
+                       ret = cmd_log_walk(&rev);
+                       break;
+               default:
+                       ret = error("Unknown type: %d", o->type);
+               }
+       }
+       free(objects);
+       return ret;
+}
+
+/*
+ * This is equivalent to "git log -g --abbrev-commit --pretty=oneline"
+ */
+int cmd_log_reflog(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info rev;
+
+       git_config(git_log_config);
+       init_revisions(&rev, prefix);
+       init_reflog_walk(&rev.reflog_info);
+       rev.abbrev_commit = 1;
+       rev.verbose_header = 1;
+       cmd_log_init(argc, argv, prefix, &rev);
+
+       /*
+        * This means that we override whatever commit format the user gave
+        * on the cmd line.  Sad, but cmd_log_init() currently doesn't
+        * allow us to set a different default.
+        */
+       rev.commit_format = CMIT_FMT_ONELINE;
+       rev.always_show_header = 1;
+
+       /*
+        * We get called through "git reflog", so unlike the other log
+        * routines, we need to set up our pager manually..
+        */
+       setup_pager();
+
+       return cmd_log_walk(&rev);
+}
+
+int cmd_log(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info rev;
+
+       git_config(git_log_config);
+       init_revisions(&rev, prefix);
+       rev.always_show_header = 1;
+       cmd_log_init(argc, argv, prefix, &rev);
+       return cmd_log_walk(&rev);
+}
+
+/* format-patch */
+#define FORMAT_PATCH_NAME_MAX 64
+
+static int istitlechar(char c)
+{
+       return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+               (c >= '0' && c <= '9') || c == '.' || c == '_';
+}
+
+static char *extra_headers = NULL;
+static int extra_headers_size = 0;
+static const char *fmt_patch_suffix = ".patch";
+static int numbered = 0;
+static int auto_number = 0;
+
+static int git_format_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "format.headers")) {
+               int len;
+
+               if (!value)
+                       die("format.headers without value");
+               len = strlen(value);
+               extra_headers_size += len + 1;
+               extra_headers = xrealloc(extra_headers, extra_headers_size);
+               extra_headers[extra_headers_size - len - 1] = 0;
+               strcat(extra_headers, value);
+               return 0;
+       }
+       if (!strcmp(var, "format.suffix")) {
+               if (!value)
+                       die("format.suffix without value");
+               fmt_patch_suffix = xstrdup(value);
+               return 0;
+       }
+       if (!strcmp(var, "diff.color") || !strcmp(var, "color.diff")) {
+               return 0;
+       }
+       if (!strcmp(var, "format.numbered")) {
+               if (!strcasecmp(value, "auto")) {
+                       auto_number = 1;
+                       return 0;
+               }
+
+               numbered = git_config_bool(var, value);
+               return 0;
+       }
+
+       return git_log_config(var, value);
+}
+
+
+static FILE *realstdout = NULL;
+static const char *output_directory = NULL;
+
+static int reopen_stdout(struct commit *commit, int nr, int keep_subject,
+                        int numbered_files)
+{
+       char filename[PATH_MAX];
+       char *sol;
+       int len = 0;
+       int suffix_len = strlen(fmt_patch_suffix) + 1;
+
+       if (output_directory) {
+               if (strlen(output_directory) >=
+                   sizeof(filename) - FORMAT_PATCH_NAME_MAX - suffix_len)
+                       return error("name of output directory is too long");
+               strlcpy(filename, output_directory, sizeof(filename) - suffix_len);
+               len = strlen(filename);
+               if (filename[len - 1] != '/')
+                       filename[len++] = '/';
+       }
+
+       if (numbered_files) {
+               sprintf(filename + len, "%d", nr);
+               len = strlen(filename);
+
+       } else {
+               sprintf(filename + len, "%04d", nr);
+               len = strlen(filename);
+
+               sol = strstr(commit->buffer, "\n\n");
+               if (sol) {
+                       int j, space = 1;
+
+                       sol += 2;
+                       /* strip [PATCH] or [PATCH blabla] */
+                       if (!keep_subject && !prefixcmp(sol, "[PATCH")) {
+                               char *eos = strchr(sol + 6, ']');
+                               if (eos) {
+                                       while (isspace(*eos))
+                                               eos++;
+                                       sol = eos;
+                               }
+                       }
+
+                       for (j = 0;
+                            j < FORMAT_PATCH_NAME_MAX - suffix_len - 5 &&
+                                    len < sizeof(filename) - suffix_len &&
+                                    sol[j] && sol[j] != '\n';
+                            j++) {
+                               if (istitlechar(sol[j])) {
+                                       if (space) {
+                                               filename[len++] = '-';
+                                               space = 0;
+                                       }
+                                       filename[len++] = sol[j];
+                                       if (sol[j] == '.')
+                                               while (sol[j + 1] == '.')
+                                                       j++;
+                               } else
+                                       space = 1;
+                       }
+                       while (filename[len - 1] == '.'
+                              || filename[len - 1] == '-')
+                               len--;
+                       filename[len] = 0;
+               }
+               if (len + suffix_len >= sizeof(filename))
+                       return error("Patch pathname too long");
+               strcpy(filename + len, fmt_patch_suffix);
+       }
+
+       fprintf(realstdout, "%s\n", filename);
+       if (freopen(filename, "w", stdout) == NULL)
+               return error("Cannot open patch file %s",filename);
+
+       return 0;
+}
+
+static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids, const char *prefix)
+{
+       struct rev_info check_rev;
+       struct commit *commit;
+       struct object *o1, *o2;
+       unsigned flags1, flags2;
+
+       if (rev->pending.nr != 2)
+               die("Need exactly one range.");
+
+       o1 = rev->pending.objects[0].item;
+       flags1 = o1->flags;
+       o2 = rev->pending.objects[1].item;
+       flags2 = o2->flags;
+
+       if ((flags1 & UNINTERESTING) == (flags2 & UNINTERESTING))
+               die("Not a range.");
+
+       init_patch_ids(ids);
+
+       /* given a range a..b get all patch ids for b..a */
+       init_revisions(&check_rev, prefix);
+       o1->flags ^= UNINTERESTING;
+       o2->flags ^= UNINTERESTING;
+       add_pending_object(&check_rev, o1, "o1");
+       add_pending_object(&check_rev, o2, "o2");
+       prepare_revision_walk(&check_rev);
+
+       while ((commit = get_revision(&check_rev)) != NULL) {
+               /* ignore merges */
+               if (commit->parents && commit->parents->next)
+                       continue;
+
+               add_commit_patch_id(commit, ids);
+       }
+
+       /* reset for next revision walk */
+       clear_commit_marks((struct commit *)o1,
+                       SEEN | UNINTERESTING | SHOWN | ADDED);
+       clear_commit_marks((struct commit *)o2,
+                       SEEN | UNINTERESTING | SHOWN | ADDED);
+       o1->flags = flags1;
+       o2->flags = flags2;
+}
+
+static void gen_message_id(char *dest, unsigned int length, char *base)
+{
+       const char *committer = git_committer_info(-1);
+       const char *email_start = strrchr(committer, '<');
+       const char *email_end = strrchr(committer, '>');
+       if(!email_start || !email_end || email_start > email_end - 1)
+               die("Could not extract email from committer identity.");
+       snprintf(dest, length, "%s.%lu.git.%.*s", base,
+                (unsigned long) time(NULL),
+                (int)(email_end - email_start - 1), email_start + 1);
+}
+
+static const char *clean_message_id(const char *msg_id)
+{
+       char ch;
+       const char *a, *z, *m;
+
+       m = msg_id;
+       while ((ch = *m) && (isspace(ch) || (ch == '<')))
+               m++;
+       a = m;
+       z = NULL;
+       while ((ch = *m)) {
+               if (!isspace(ch) && (ch != '>'))
+                       z = m;
+               m++;
+       }
+       if (!z)
+               die("insane in-reply-to: %s", msg_id);
+       if (++z == m)
+               return a;
+       return xmemdupz(a, z - a);
+}
+
+int cmd_format_patch(int argc, const char **argv, const char *prefix)
+{
+       struct commit *commit;
+       struct commit **list = NULL;
+       struct rev_info rev;
+       int nr = 0, total, i, j;
+       int use_stdout = 0;
+       int start_number = -1;
+       int keep_subject = 0;
+       int numbered_files = 0;         /* _just_ numbers */
+       int subject_prefix = 0;
+       int ignore_if_in_upstream = 0;
+       int thread = 0;
+       const char *in_reply_to = NULL;
+       struct patch_ids ids;
+       char *add_signoff = NULL;
+       char message_id[1024];
+       char ref_message_id[1024];
+
+       git_config(git_format_config);
+       init_revisions(&rev, prefix);
+       rev.commit_format = CMIT_FMT_EMAIL;
+       rev.verbose_header = 1;
+       rev.diff = 1;
+       rev.combine_merges = 0;
+       rev.ignore_merges = 1;
+       rev.diffopt.msg_sep = "";
+       DIFF_OPT_SET(&rev.diffopt, RECURSIVE);
+
+       rev.subject_prefix = fmt_patch_subject_prefix;
+       rev.extra_headers = extra_headers;
+
+       /*
+        * Parse the arguments before setup_revisions(), or something
+        * like "git format-patch -o a123 HEAD^.." may fail; a123 is
+        * possibly a valid SHA1.
+        */
+       for (i = 1, j = 1; i < argc; i++) {
+               if (!strcmp(argv[i], "--stdout"))
+                       use_stdout = 1;
+               else if (!strcmp(argv[i], "-n") ||
+                               !strcmp(argv[i], "--numbered"))
+                       numbered = 1;
+               else if (!strcmp(argv[i], "-N") ||
+                               !strcmp(argv[i], "--no-numbered")) {
+                       numbered = 0;
+                       auto_number = 0;
+               }
+               else if (!prefixcmp(argv[i], "--start-number="))
+                       start_number = strtol(argv[i] + 15, NULL, 10);
+               else if (!strcmp(argv[i], "--numbered-files"))
+                       numbered_files = 1;
+               else if (!strcmp(argv[i], "--start-number")) {
+                       i++;
+                       if (i == argc)
+                               die("Need a number for --start-number");
+                       start_number = strtol(argv[i], NULL, 10);
+               }
+               else if (!strcmp(argv[i], "-k") ||
+                               !strcmp(argv[i], "--keep-subject")) {
+                       keep_subject = 1;
+                       rev.total = -1;
+               }
+               else if (!strcmp(argv[i], "--output-directory") ||
+                        !strcmp(argv[i], "-o")) {
+                       i++;
+                       if (argc <= i)
+                               die("Which directory?");
+                       if (output_directory)
+                               die("Two output directories?");
+                       output_directory = argv[i];
+               }
+               else if (!strcmp(argv[i], "--signoff") ||
+                        !strcmp(argv[i], "-s")) {
+                       const char *committer;
+                       const char *endpos;
+                       committer = git_committer_info(1);
+                       endpos = strchr(committer, '>');
+                       if (!endpos)
+                               die("bogos committer info %s\n", committer);
+                       add_signoff = xmemdupz(committer, endpos - committer + 1);
+               }
+               else if (!strcmp(argv[i], "--attach")) {
+                       rev.mime_boundary = git_version_string;
+                       rev.no_inline = 1;
+               }
+               else if (!prefixcmp(argv[i], "--attach=")) {
+                       rev.mime_boundary = argv[i] + 9;
+                       rev.no_inline = 1;
+               }
+               else if (!strcmp(argv[i], "--inline")) {
+                       rev.mime_boundary = git_version_string;
+                       rev.no_inline = 0;
+               }
+               else if (!prefixcmp(argv[i], "--inline=")) {
+                       rev.mime_boundary = argv[i] + 9;
+                       rev.no_inline = 0;
+               }
+               else if (!strcmp(argv[i], "--ignore-if-in-upstream"))
+                       ignore_if_in_upstream = 1;
+               else if (!strcmp(argv[i], "--thread"))
+                       thread = 1;
+               else if (!prefixcmp(argv[i], "--in-reply-to="))
+                       in_reply_to = argv[i] + 14;
+               else if (!strcmp(argv[i], "--in-reply-to")) {
+                       i++;
+                       if (i == argc)
+                               die("Need a Message-Id for --in-reply-to");
+                       in_reply_to = argv[i];
+               } else if (!prefixcmp(argv[i], "--subject-prefix=")) {
+                       subject_prefix = 1;
+                       rev.subject_prefix = argv[i] + 17;
+               } else if (!prefixcmp(argv[i], "--suffix="))
+                       fmt_patch_suffix = argv[i] + 9;
+               else
+                       argv[j++] = argv[i];
+       }
+       argc = j;
+
+       if (start_number < 0)
+               start_number = 1;
+       if (numbered && keep_subject)
+               die ("-n and -k are mutually exclusive.");
+       if (keep_subject && subject_prefix)
+               die ("--subject-prefix and -k are mutually exclusive.");
+       if (numbered_files && use_stdout)
+               die ("--numbered-files and --stdout are mutually exclusive.");
+
+       argc = setup_revisions(argc, argv, &rev, "HEAD");
+       if (argc > 1)
+               die ("unrecognized argument: %s", argv[1]);
+
+       if (!rev.diffopt.output_format)
+               rev.diffopt.output_format = DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_SUMMARY | DIFF_FORMAT_PATCH;
+
+       if (!DIFF_OPT_TST(&rev.diffopt, TEXT))
+               DIFF_OPT_SET(&rev.diffopt, BINARY);
+
+       if (!output_directory && !use_stdout)
+               output_directory = prefix;
+
+       if (output_directory) {
+               if (use_stdout)
+                       die("standard output, or directory, which one?");
+               if (mkdir(output_directory, 0777) < 0 && errno != EEXIST)
+                       die("Could not create directory %s",
+                           output_directory);
+       }
+
+       if (rev.pending.nr == 1) {
+               if (rev.max_count < 0 && !rev.show_root_diff) {
+                       /*
+                        * This is traditional behaviour of "git format-patch
+                        * origin" that prepares what the origin side still
+                        * does not have.
+                        */
+                       rev.pending.objects[0].item->flags |= UNINTERESTING;
+                       add_head(&rev);
+               }
+               /*
+                * Otherwise, it is "format-patch -22 HEAD", and/or
+                * "format-patch --root HEAD".  The user wants
+                * get_revision() to do the usual traversal.
+                */
+       }
+
+       if (ignore_if_in_upstream)
+               get_patch_ids(&rev, &ids, prefix);
+
+       if (!use_stdout)
+               realstdout = xfdopen(xdup(1), "w");
+
+       prepare_revision_walk(&rev);
+       while ((commit = get_revision(&rev)) != NULL) {
+               /* ignore merges */
+               if (commit->parents && commit->parents->next)
+                       continue;
+
+               if (ignore_if_in_upstream &&
+                               has_commit_patch_id(commit, &ids))
+                       continue;
+
+               nr++;
+               list = xrealloc(list, nr * sizeof(list[0]));
+               list[nr - 1] = commit;
+       }
+       total = nr;
+       if (!keep_subject && auto_number && total > 1)
+               numbered = 1;
+       if (numbered)
+               rev.total = total + start_number - 1;
+       rev.add_signoff = add_signoff;
+       if (in_reply_to)
+               rev.ref_message_id = clean_message_id(in_reply_to);
+       while (0 <= --nr) {
+               int shown;
+               commit = list[nr];
+               rev.nr = total - nr + (start_number - 1);
+               /* Make the second and subsequent mails replies to the first */
+               if (thread) {
+                       if (nr == (total - 2)) {
+                               strncpy(ref_message_id, message_id,
+                                       sizeof(ref_message_id));
+                               ref_message_id[sizeof(ref_message_id)-1]='\0';
+                               rev.ref_message_id = ref_message_id;
+                       }
+                       gen_message_id(message_id, sizeof(message_id),
+                                      sha1_to_hex(commit->object.sha1));
+                       rev.message_id = message_id;
+               }
+               if (!use_stdout)
+                       if (reopen_stdout(commit, rev.nr, keep_subject,
+                                         numbered_files))
+                               die("Failed to create output files");
+               shown = log_tree_commit(&rev, commit);
+               free(commit->buffer);
+               commit->buffer = NULL;
+
+               /* We put one extra blank line between formatted
+                * patches and this flag is used by log-tree code
+                * to see if it needs to emit a LF before showing
+                * the log; when using one file per patch, we do
+                * not want the extra blank line.
+                */
+               if (!use_stdout)
+                       rev.shown_one = 0;
+               if (shown) {
+                       if (rev.mime_boundary)
+                               printf("\n--%s%s--\n\n\n",
+                                      mime_boundary_leader,
+                                      rev.mime_boundary);
+                       else
+                               printf("-- \n%s\n\n", git_version_string);
+               }
+               if (!use_stdout)
+                       fclose(stdout);
+       }
+       free(list);
+       if (ignore_if_in_upstream)
+               free_patch_ids(&ids);
+       return 0;
+}
+
+static int add_pending_commit(const char *arg, struct rev_info *revs, int flags)
+{
+       unsigned char sha1[20];
+       if (get_sha1(arg, sha1) == 0) {
+               struct commit *commit = lookup_commit_reference(sha1);
+               if (commit) {
+                       commit->object.flags |= flags;
+                       add_pending_object(revs, &commit->object, arg);
+                       return 0;
+               }
+       }
+       return -1;
+}
+
+static const char cherry_usage[] =
+"git-cherry [-v] <upstream> [<head>] [<limit>]";
+int cmd_cherry(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info revs;
+       struct patch_ids ids;
+       struct commit *commit;
+       struct commit_list *list = NULL;
+       const char *upstream;
+       const char *head = "HEAD";
+       const char *limit = NULL;
+       int verbose = 0;
+
+       if (argc > 1 && !strcmp(argv[1], "-v")) {
+               verbose = 1;
+               argc--;
+               argv++;
+       }
+
+       switch (argc) {
+       case 4:
+               limit = argv[3];
+               /* FALLTHROUGH */
+       case 3:
+               head = argv[2];
+               /* FALLTHROUGH */
+       case 2:
+               upstream = argv[1];
+               break;
+       default:
+               usage(cherry_usage);
+       }
+
+       init_revisions(&revs, prefix);
+       revs.diff = 1;
+       revs.combine_merges = 0;
+       revs.ignore_merges = 1;
+       DIFF_OPT_SET(&revs.diffopt, RECURSIVE);
+
+       if (add_pending_commit(head, &revs, 0))
+               die("Unknown commit %s", head);
+       if (add_pending_commit(upstream, &revs, UNINTERESTING))
+               die("Unknown commit %s", upstream);
+
+       /* Don't say anything if head and upstream are the same. */
+       if (revs.pending.nr == 2) {
+               struct object_array_entry *o = revs.pending.objects;
+               if (hashcmp(o[0].item->sha1, o[1].item->sha1) == 0)
+                       return 0;
+       }
+
+       get_patch_ids(&revs, &ids, prefix);
+
+       if (limit && add_pending_commit(limit, &revs, UNINTERESTING))
+               die("Unknown commit %s", limit);
+
+       /* reverse the list of commits */
+       prepare_revision_walk(&revs);
+       while ((commit = get_revision(&revs)) != NULL) {
+               /* ignore merges */
+               if (commit->parents && commit->parents->next)
+                       continue;
+
+               commit_list_insert(commit, &list);
+       }
+
+       while (list) {
+               char sign = '+';
+
+               commit = list->item;
+               if (has_commit_patch_id(commit, &ids))
+                       sign = '-';
+
+               if (verbose) {
+                       struct strbuf buf;
+                       strbuf_init(&buf, 0);
+                       pretty_print_commit(CMIT_FMT_ONELINE, commit,
+                                           &buf, 0, NULL, NULL, 0, 0);
+                       printf("%c %s %s\n", sign,
+                              sha1_to_hex(commit->object.sha1), buf.buf);
+                       strbuf_release(&buf);
+               }
+               else {
+                       printf("%c %s\n", sign,
+                              sha1_to_hex(commit->object.sha1));
+               }
+
+               list = list->next;
+       }
+
+       free_patch_ids(&ids);
+       return 0;
+}
diff --git a/builtin-ls-files.c b/builtin-ls-files.c
new file mode 100644 (file)
index 0000000..0f0ab2d
--- /dev/null
@@ -0,0 +1,621 @@
+/*
+ * This merges the file listing in the directory cache index
+ * with the actual working directory list, and shows different
+ * combinations of the two.
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "cache.h"
+#include "quote.h"
+#include "dir.h"
+#include "builtin.h"
+#include "tree.h"
+
+static int abbrev;
+static int show_deleted;
+static int show_cached;
+static int show_others;
+static int show_stage;
+static int show_unmerged;
+static int show_modified;
+static int show_killed;
+static int show_valid_bit;
+static int line_terminator = '\n';
+
+static int prefix_len;
+static int prefix_offset;
+static const char **pathspec;
+static int error_unmatch;
+static char *ps_matched;
+static const char *with_tree;
+
+static const char *tag_cached = "";
+static const char *tag_unmerged = "";
+static const char *tag_removed = "";
+static const char *tag_other = "";
+static const char *tag_killed = "";
+static const char *tag_modified = "";
+
+
+/*
+ * Match a pathspec against a filename. The first "skiplen" characters
+ * are the common prefix
+ */
+int pathspec_match(const char **spec, char *ps_matched,
+                  const char *filename, int skiplen)
+{
+       const char *m;
+
+       while ((m = *spec++) != NULL) {
+               int matchlen = strlen(m + skiplen);
+
+               if (!matchlen)
+                       goto matched;
+               if (!strncmp(m + skiplen, filename + skiplen, matchlen)) {
+                       if (m[skiplen + matchlen - 1] == '/')
+                               goto matched;
+                       switch (filename[skiplen + matchlen]) {
+                       case '/': case '\0':
+                               goto matched;
+                       }
+               }
+               if (!fnmatch(m + skiplen, filename + skiplen, 0))
+                       goto matched;
+               if (ps_matched)
+                       ps_matched++;
+               continue;
+       matched:
+               if (ps_matched)
+                       *ps_matched = 1;
+               return 1;
+       }
+       return 0;
+}
+
+static void show_dir_entry(const char *tag, struct dir_entry *ent)
+{
+       int len = prefix_len;
+       int offset = prefix_offset;
+
+       if (len >= ent->len)
+               die("git-ls-files: internal error - directory entry not superset of prefix");
+
+       if (pathspec && !pathspec_match(pathspec, ps_matched, ent->name, len))
+               return;
+
+       fputs(tag, stdout);
+       write_name_quoted(ent->name + offset, stdout, line_terminator);
+}
+
+static void show_other_files(struct dir_struct *dir)
+{
+       int i;
+
+
+       /*
+        * Skip matching and unmerged entries for the paths,
+        * since we want just "others".
+        *
+        * (Matching entries are normally pruned during
+        * the directory tree walk, but will show up for
+        * gitlinks because we don't necessarily have
+        * dir->show_other_directories set to suppress
+        * them).
+        */
+       for (i = 0; i < dir->nr; i++) {
+               struct dir_entry *ent = dir->entries[i];
+               int len, pos;
+               struct cache_entry *ce;
+
+               /*
+                * Remove the '/' at the end that directory
+                * walking adds for directory entries.
+                */
+               len = ent->len;
+               if (len && ent->name[len-1] == '/')
+                       len--;
+               pos = cache_name_pos(ent->name, len);
+               if (0 <= pos)
+                       continue;       /* exact match */
+               pos = -pos - 1;
+               if (pos < active_nr) {
+                       ce = active_cache[pos];
+                       if (ce_namelen(ce) == len &&
+                           !memcmp(ce->name, ent->name, len))
+                               continue; /* Yup, this one exists unmerged */
+               }
+               show_dir_entry(tag_other, ent);
+       }
+}
+
+static void show_killed_files(struct dir_struct *dir)
+{
+       int i;
+       for (i = 0; i < dir->nr; i++) {
+               struct dir_entry *ent = dir->entries[i];
+               char *cp, *sp;
+               int pos, len, killed = 0;
+
+               for (cp = ent->name; cp - ent->name < ent->len; cp = sp + 1) {
+                       sp = strchr(cp, '/');
+                       if (!sp) {
+                               /* If ent->name is prefix of an entry in the
+                                * cache, it will be killed.
+                                */
+                               pos = cache_name_pos(ent->name, ent->len);
+                               if (0 <= pos)
+                                       die("bug in show-killed-files");
+                               pos = -pos - 1;
+                               while (pos < active_nr &&
+                                      ce_stage(active_cache[pos]))
+                                       pos++; /* skip unmerged */
+                               if (active_nr <= pos)
+                                       break;
+                               /* pos points at a name immediately after
+                                * ent->name in the cache.  Does it expect
+                                * ent->name to be a directory?
+                                */
+                               len = ce_namelen(active_cache[pos]);
+                               if ((ent->len < len) &&
+                                   !strncmp(active_cache[pos]->name,
+                                            ent->name, ent->len) &&
+                                   active_cache[pos]->name[ent->len] == '/')
+                                       killed = 1;
+                               break;
+                       }
+                       if (0 <= cache_name_pos(ent->name, sp - ent->name)) {
+                               /* If any of the leading directories in
+                                * ent->name is registered in the cache,
+                                * ent->name will be killed.
+                                */
+                               killed = 1;
+                               break;
+                       }
+               }
+               if (killed)
+                       show_dir_entry(tag_killed, dir->entries[i]);
+       }
+}
+
+static void show_ce_entry(const char *tag, struct cache_entry *ce)
+{
+       int len = prefix_len;
+       int offset = prefix_offset;
+
+       if (len >= ce_namelen(ce))
+               die("git-ls-files: internal error - cache entry not superset of prefix");
+
+       if (pathspec && !pathspec_match(pathspec, ps_matched, ce->name, len))
+               return;
+
+       if (tag && *tag && show_valid_bit &&
+           (ce->ce_flags & htons(CE_VALID))) {
+               static char alttag[4];
+               memcpy(alttag, tag, 3);
+               if (isalpha(tag[0]))
+                       alttag[0] = tolower(tag[0]);
+               else if (tag[0] == '?')
+                       alttag[0] = '!';
+               else {
+                       alttag[0] = 'v';
+                       alttag[1] = tag[0];
+                       alttag[2] = ' ';
+                       alttag[3] = 0;
+               }
+               tag = alttag;
+       }
+
+       if (!show_stage) {
+               fputs(tag, stdout);
+       } else {
+               printf("%s%06o %s %d\t",
+                      tag,
+                      ntohl(ce->ce_mode),
+                      abbrev ? find_unique_abbrev(ce->sha1,abbrev)
+                               : sha1_to_hex(ce->sha1),
+                      ce_stage(ce));
+       }
+       write_name_quoted(ce->name + offset, stdout, line_terminator);
+}
+
+static void show_files(struct dir_struct *dir, const char *prefix)
+{
+       int i;
+
+       /* For cached/deleted files we don't need to even do the readdir */
+       if (show_others || show_killed) {
+               const char *path = ".", *base = "";
+               int baselen = prefix_len;
+
+               if (baselen)
+                       path = base = prefix;
+               read_directory(dir, path, base, baselen, pathspec);
+               if (show_others)
+                       show_other_files(dir);
+               if (show_killed)
+                       show_killed_files(dir);
+       }
+       if (show_cached | show_stage) {
+               for (i = 0; i < active_nr; i++) {
+                       struct cache_entry *ce = active_cache[i];
+                       if (excluded(dir, ce->name) != dir->show_ignored)
+                               continue;
+                       if (show_unmerged && !ce_stage(ce))
+                               continue;
+                       if (ce->ce_flags & htons(CE_UPDATE))
+                               continue;
+                       show_ce_entry(ce_stage(ce) ? tag_unmerged : tag_cached, ce);
+               }
+       }
+       if (show_deleted | show_modified) {
+               for (i = 0; i < active_nr; i++) {
+                       struct cache_entry *ce = active_cache[i];
+                       struct stat st;
+                       int err;
+                       if (excluded(dir, ce->name) != dir->show_ignored)
+                               continue;
+                       err = lstat(ce->name, &st);
+                       if (show_deleted && err)
+                               show_ce_entry(tag_removed, ce);
+                       if (show_modified && ce_modified(ce, &st, 0))
+                               show_ce_entry(tag_modified, ce);
+               }
+       }
+}
+
+/*
+ * Prune the index to only contain stuff starting with "prefix"
+ */
+static void prune_cache(const char *prefix)
+{
+       int pos = cache_name_pos(prefix, prefix_len);
+       unsigned int first, last;
+
+       if (pos < 0)
+               pos = -pos-1;
+       memmove(active_cache, active_cache + pos,
+               (active_nr - pos) * sizeof(struct cache_entry *));
+       active_nr -= pos;
+       first = 0;
+       last = active_nr;
+       while (last > first) {
+               int next = (last + first) >> 1;
+               struct cache_entry *ce = active_cache[next];
+               if (!strncmp(ce->name, prefix, prefix_len)) {
+                       first = next+1;
+                       continue;
+               }
+               last = next;
+       }
+       active_nr = last;
+}
+
+static const char *verify_pathspec(const char *prefix)
+{
+       const char **p, *n, *prev;
+       unsigned long max;
+
+       prev = NULL;
+       max = PATH_MAX;
+       for (p = pathspec; (n = *p) != NULL; p++) {
+               int i, len = 0;
+               for (i = 0; i < max; i++) {
+                       char c = n[i];
+                       if (prev && prev[i] != c)
+                               break;
+                       if (!c || c == '*' || c == '?')
+                               break;
+                       if (c == '/')
+                               len = i+1;
+               }
+               prev = n;
+               if (len < max) {
+                       max = len;
+                       if (!max)
+                               break;
+               }
+       }
+
+       if (prefix_offset > max || memcmp(prev, prefix, prefix_offset))
+               die("git-ls-files: cannot generate relative filenames containing '..'");
+
+       prefix_len = max;
+       return max ? xmemdupz(prev, max) : NULL;
+}
+
+/*
+ * Read the tree specified with --with-tree option
+ * (typically, HEAD) into stage #1 and then
+ * squash them down to stage #0.  This is used for
+ * --error-unmatch to list and check the path patterns
+ * that were given from the command line.  We are not
+ * going to write this index out.
+ */
+void overlay_tree_on_cache(const char *tree_name, const char *prefix)
+{
+       struct tree *tree;
+       unsigned char sha1[20];
+       const char **match;
+       struct cache_entry *last_stage0 = NULL;
+       int i;
+
+       if (get_sha1(tree_name, sha1))
+               die("tree-ish %s not found.", tree_name);
+       tree = parse_tree_indirect(sha1);
+       if (!tree)
+               die("bad tree-ish %s", tree_name);
+
+       /* Hoist the unmerged entries up to stage #3 to make room */
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (!ce_stage(ce))
+                       continue;
+               ce->ce_flags |= htons(CE_STAGEMASK);
+       }
+
+       if (prefix) {
+               static const char *(matchbuf[2]);
+               matchbuf[0] = prefix;
+               matchbuf [1] = NULL;
+               match = matchbuf;
+       } else
+               match = NULL;
+       if (read_tree(tree, 1, match))
+               die("unable to read tree entries %s", tree_name);
+
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               switch (ce_stage(ce)) {
+               case 0:
+                       last_stage0 = ce;
+                       /* fallthru */
+               default:
+                       continue;
+               case 1:
+                       /*
+                        * If there is stage #0 entry for this, we do not
+                        * need to show it.  We use CE_UPDATE bit to mark
+                        * such an entry.
+                        */
+                       if (last_stage0 &&
+                           !strcmp(last_stage0->name, ce->name))
+                               ce->ce_flags |= htons(CE_UPDATE);
+               }
+       }
+}
+
+int report_path_error(const char *ps_matched, const char **pathspec, int prefix_offset)
+{
+       /*
+        * Make sure all pathspec matched; otherwise it is an error.
+        */
+       int num, errors = 0;
+       for (num = 0; pathspec[num]; num++) {
+               int other, found_dup;
+
+               if (ps_matched[num])
+                       continue;
+               /*
+                * The caller might have fed identical pathspec
+                * twice.  Do not barf on such a mistake.
+                */
+               for (found_dup = other = 0;
+                    !found_dup && pathspec[other];
+                    other++) {
+                       if (other == num || !ps_matched[other])
+                               continue;
+                       if (!strcmp(pathspec[other], pathspec[num]))
+                               /*
+                                * Ok, we have a match already.
+                                */
+                               found_dup = 1;
+               }
+               if (found_dup)
+                       continue;
+
+               error("pathspec '%s' did not match any file(s) known to git.",
+                     pathspec[num] + prefix_offset);
+               errors++;
+       }
+       return errors;
+}
+
+static const char ls_files_usage[] =
+       "git-ls-files [-z] [-t] [-v] (--[cached|deleted|others|stage|unmerged|killed|modified])* "
+       "[ --ignored ] [--exclude=<pattern>] [--exclude-from=<file>] "
+       "[ --exclude-per-directory=<filename> ] [--exclude-standard] "
+       "[--full-name] [--abbrev] [--] [<file>]*";
+
+int cmd_ls_files(int argc, const char **argv, const char *prefix)
+{
+       int i;
+       int exc_given = 0, require_work_tree = 0;
+       struct dir_struct dir;
+
+       memset(&dir, 0, sizeof(dir));
+       if (prefix)
+               prefix_offset = strlen(prefix);
+       git_config(git_default_config);
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (!strcmp(arg, "--")) {
+                       i++;
+                       break;
+               }
+               if (!strcmp(arg, "-z")) {
+                       line_terminator = 0;
+                       continue;
+               }
+               if (!strcmp(arg, "-t") || !strcmp(arg, "-v")) {
+                       tag_cached = "H ";
+                       tag_unmerged = "M ";
+                       tag_removed = "R ";
+                       tag_modified = "C ";
+                       tag_other = "? ";
+                       tag_killed = "K ";
+                       if (arg[1] == 'v')
+                               show_valid_bit = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-c") || !strcmp(arg, "--cached")) {
+                       show_cached = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-d") || !strcmp(arg, "--deleted")) {
+                       show_deleted = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-m") || !strcmp(arg, "--modified")) {
+                       show_modified = 1;
+                       require_work_tree = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-o") || !strcmp(arg, "--others")) {
+                       show_others = 1;
+                       require_work_tree = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-i") || !strcmp(arg, "--ignored")) {
+                       dir.show_ignored = 1;
+                       require_work_tree = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-s") || !strcmp(arg, "--stage")) {
+                       show_stage = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-k") || !strcmp(arg, "--killed")) {
+                       show_killed = 1;
+                       require_work_tree = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--directory")) {
+                       dir.show_other_directories = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--no-empty-directory")) {
+                       dir.hide_empty_directories = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-u") || !strcmp(arg, "--unmerged")) {
+                       /* There's no point in showing unmerged unless
+                        * you also show the stage information.
+                        */
+                       show_stage = 1;
+                       show_unmerged = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-x") && i+1 < argc) {
+                       exc_given = 1;
+                       add_exclude(argv[++i], "", 0, &dir.exclude_list[EXC_CMDL]);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--exclude=")) {
+                       exc_given = 1;
+                       add_exclude(arg+10, "", 0, &dir.exclude_list[EXC_CMDL]);
+                       continue;
+               }
+               if (!strcmp(arg, "-X") && i+1 < argc) {
+                       exc_given = 1;
+                       add_excludes_from_file(&dir, argv[++i]);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--exclude-from=")) {
+                       exc_given = 1;
+                       add_excludes_from_file(&dir, arg+15);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--exclude-per-directory=")) {
+                       exc_given = 1;
+                       dir.exclude_per_dir = arg + 24;
+                       continue;
+               }
+               if (!strcmp(arg, "--exclude-standard")) {
+                       exc_given = 1;
+                       setup_standard_excludes(&dir);
+                       continue;
+               }
+               if (!strcmp(arg, "--full-name")) {
+                       prefix_offset = 0;
+                       continue;
+               }
+               if (!strcmp(arg, "--error-unmatch")) {
+                       error_unmatch = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--with-tree=")) {
+                       with_tree = arg + 12;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--abbrev=")) {
+                       abbrev = strtoul(arg+9, NULL, 10);
+                       if (abbrev && abbrev < MINIMUM_ABBREV)
+                               abbrev = MINIMUM_ABBREV;
+                       else if (abbrev > 40)
+                               abbrev = 40;
+                       continue;
+               }
+               if (!strcmp(arg, "--abbrev")) {
+                       abbrev = DEFAULT_ABBREV;
+                       continue;
+               }
+               if (*arg == '-')
+                       usage(ls_files_usage);
+               break;
+       }
+
+       if (require_work_tree && !is_inside_work_tree())
+               setup_work_tree();
+
+       pathspec = get_pathspec(prefix, argv + i);
+
+       /* Verify that the pathspec matches the prefix */
+       if (pathspec)
+               prefix = verify_pathspec(prefix);
+
+       /* Treat unmatching pathspec elements as errors */
+       if (pathspec && error_unmatch) {
+               int num;
+               for (num = 0; pathspec[num]; num++)
+                       ;
+               ps_matched = xcalloc(1, num);
+       }
+
+       if (dir.show_ignored && !exc_given) {
+               fprintf(stderr, "%s: --ignored needs some exclude pattern\n",
+                       argv[0]);
+               exit(1);
+       }
+
+       /* With no flags, we default to showing the cached files */
+       if (!(show_stage | show_deleted | show_others | show_unmerged |
+             show_killed | show_modified))
+               show_cached = 1;
+
+       read_cache();
+       if (prefix)
+               prune_cache(prefix);
+       if (with_tree) {
+               /*
+                * Basic sanity check; show-stages and show-unmerged
+                * would not make any sense with this option.
+                */
+               if (show_stage || show_unmerged)
+                       die("ls-files --with-tree is incompatible with -s or -u");
+               overlay_tree_on_cache(with_tree, prefix);
+       }
+       show_files(&dir, prefix);
+
+       if (ps_matched) {
+               int bad;
+               bad = report_path_error(ps_matched, pathspec, prefix_offset);
+               if (bad)
+                       fprintf(stderr, "Did you forget to 'git add'?\n");
+
+               return bad ? 1 : 0;
+       }
+
+       return 0;
+}
diff --git a/builtin-ls-remote.c b/builtin-ls-remote.c
new file mode 100644 (file)
index 0000000..56f3f88
--- /dev/null
@@ -0,0 +1,74 @@
+#include "builtin.h"
+#include "cache.h"
+#include "transport.h"
+#include "remote.h"
+
+static const char ls_remote_usage[] =
+"git-ls-remote [--upload-pack=<git-upload-pack>] [<host>:]<directory>";
+
+int cmd_ls_remote(int argc, const char **argv, const char *prefix)
+{
+       int i;
+       const char *dest = NULL;
+       int nongit = 0;
+       unsigned flags = 0;
+       const char *uploadpack = NULL;
+
+       struct remote *remote;
+       struct transport *transport;
+       const struct ref *ref;
+
+       setup_git_directory_gently(&nongit);
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (*arg == '-') {
+                       if (!prefixcmp(arg, "--upload-pack=")) {
+                               uploadpack = arg + 14;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--exec=")) {
+                               uploadpack = arg + 7;
+                               continue;
+                       }
+                       if (!strcmp("--tags", arg)) {
+                               flags |= REF_TAGS;
+                               continue;
+                       }
+                       if (!strcmp("--heads", arg)) {
+                               flags |= REF_HEADS;
+                               continue;
+                       }
+                       if (!strcmp("--refs", arg)) {
+                               flags |= REF_NORMAL;
+                               continue;
+                       }
+                       usage(ls_remote_usage);
+               }
+               dest = arg;
+               break;
+       }
+
+       if (!dest || i != argc - 1)
+               usage(ls_remote_usage);
+
+       remote = nongit ? NULL : remote_get(dest);
+       if (remote && !remote->url_nr)
+               die("remote %s has no configured URL", dest);
+       transport = transport_get(remote, remote ? remote->url[0] : dest);
+       if (uploadpack != NULL)
+               transport_set_option(transport, TRANS_OPT_UPLOADPACK, uploadpack);
+
+       ref = transport_get_remote_refs(transport);
+
+       if (!ref)
+               return 1;
+
+       while (ref) {
+               if (check_ref_type(ref, flags))
+                       printf("%s      %s\n", sha1_to_hex(ref->old_sha1), ref->name);
+               ref = ref->next;
+       }
+       return 0;
+}
diff --git a/builtin-ls-tree.c b/builtin-ls-tree.c
new file mode 100644 (file)
index 0000000..7abe333
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "cache.h"
+#include "blob.h"
+#include "tree.h"
+#include "commit.h"
+#include "quote.h"
+#include "builtin.h"
+
+static int line_termination = '\n';
+#define LS_RECURSIVE 1
+#define LS_TREE_ONLY 2
+#define LS_SHOW_TREES 4
+#define LS_NAME_ONLY 8
+#define LS_SHOW_SIZE 16
+static int abbrev;
+static int ls_options;
+static const char **pathspec;
+static int chomp_prefix;
+static const char *ls_tree_prefix;
+
+static const char ls_tree_usage[] =
+       "git-ls-tree [-d] [-r] [-t] [-l] [-z] [--name-only] [--name-status] [--full-name] [--abbrev[=<n>]] <tree-ish> [path...]";
+
+static int show_recursive(const char *base, int baselen, const char *pathname)
+{
+       const char **s;
+
+       if (ls_options & LS_RECURSIVE)
+               return 1;
+
+       s = pathspec;
+       if (!s)
+               return 0;
+
+       for (;;) {
+               const char *spec = *s++;
+               int len, speclen;
+
+               if (!spec)
+                       return 0;
+               if (strncmp(base, spec, baselen))
+                       continue;
+               len = strlen(pathname);
+               spec += baselen;
+               speclen = strlen(spec);
+               if (speclen <= len)
+                       continue;
+               if (memcmp(pathname, spec, len))
+                       continue;
+               return 1;
+       }
+}
+
+static int show_tree(const unsigned char *sha1, const char *base, int baselen,
+                    const char *pathname, unsigned mode, int stage)
+{
+       int retval = 0;
+       const char *type = blob_type;
+       unsigned long size;
+
+       if (S_ISGITLINK(mode)) {
+               /*
+                * Maybe we want to have some recursive version here?
+                *
+                * Something like:
+                *
+               if (show_subprojects(base, baselen, pathname)) {
+                       if (fork()) {
+                               chdir(base);
+                               exec ls-tree;
+                       }
+                       waitpid();
+               }
+                *
+                * ..or similar..
+                */
+               type = commit_type;
+       } else if (S_ISDIR(mode)) {
+               if (show_recursive(base, baselen, pathname)) {
+                       retval = READ_TREE_RECURSIVE;
+                       if (!(ls_options & LS_SHOW_TREES))
+                               return retval;
+               }
+               type = tree_type;
+       }
+       else if (ls_options & LS_TREE_ONLY)
+               return 0;
+
+       if (chomp_prefix &&
+           (baselen < chomp_prefix || memcmp(ls_tree_prefix, base, chomp_prefix)))
+               return 0;
+
+       if (!(ls_options & LS_NAME_ONLY)) {
+               if (ls_options & LS_SHOW_SIZE) {
+                       if (!strcmp(type, blob_type)) {
+                               sha1_object_info(sha1, &size);
+                               printf("%06o %s %s %7lu\t", mode, type,
+                                      abbrev ? find_unique_abbrev(sha1, abbrev)
+                                             : sha1_to_hex(sha1),
+                                      size);
+                       } else
+                               printf("%06o %s %s %7c\t", mode, type,
+                                      abbrev ? find_unique_abbrev(sha1, abbrev)
+                                             : sha1_to_hex(sha1),
+                                      '-');
+               } else
+                       printf("%06o %s %s\t", mode, type,
+                              abbrev ? find_unique_abbrev(sha1, abbrev)
+                                     : sha1_to_hex(sha1));
+       }
+       write_name_quotedpfx(base + chomp_prefix, baselen - chomp_prefix,
+                         pathname, stdout, line_termination);
+       return retval;
+}
+
+int cmd_ls_tree(int argc, const char **argv, const char *prefix)
+{
+       unsigned char sha1[20];
+       struct tree *tree;
+
+       git_config(git_default_config);
+       ls_tree_prefix = prefix;
+       if (prefix && *prefix)
+               chomp_prefix = strlen(prefix);
+       while (1 < argc && argv[1][0] == '-') {
+               switch (argv[1][1]) {
+               case 'z':
+                       line_termination = 0;
+                       break;
+               case 'r':
+                       ls_options |= LS_RECURSIVE;
+                       break;
+               case 'd':
+                       ls_options |= LS_TREE_ONLY;
+                       break;
+               case 't':
+                       ls_options |= LS_SHOW_TREES;
+                       break;
+               case 'l':
+                       ls_options |= LS_SHOW_SIZE;
+                       break;
+               case '-':
+                       if (!strcmp(argv[1]+2, "name-only") ||
+                           !strcmp(argv[1]+2, "name-status")) {
+                               ls_options |= LS_NAME_ONLY;
+                               break;
+                       }
+                       if (!strcmp(argv[1]+2, "long")) {
+                               ls_options |= LS_SHOW_SIZE;
+                               break;
+                       }
+                       if (!strcmp(argv[1]+2, "full-name")) {
+                               chomp_prefix = 0;
+                               break;
+                       }
+                       if (!prefixcmp(argv[1]+2, "abbrev=")) {
+                               abbrev = strtoul(argv[1]+9, NULL, 10);
+                               if (abbrev && abbrev < MINIMUM_ABBREV)
+                                       abbrev = MINIMUM_ABBREV;
+                               else if (abbrev > 40)
+                                       abbrev = 40;
+                               break;
+                       }
+                       if (!strcmp(argv[1]+2, "abbrev")) {
+                               abbrev = DEFAULT_ABBREV;
+                               break;
+                       }
+                       /* otherwise fallthru */
+               default:
+                       usage(ls_tree_usage);
+               }
+               argc--; argv++;
+       }
+       /* -d -r should imply -t, but -d by itself should not have to. */
+       if ( (LS_TREE_ONLY|LS_RECURSIVE) ==
+           ((LS_TREE_ONLY|LS_RECURSIVE) & ls_options))
+               ls_options |= LS_SHOW_TREES;
+
+       if (argc < 2)
+               usage(ls_tree_usage);
+       if (get_sha1(argv[1], sha1))
+               die("Not a valid object name %s", argv[1]);
+
+       pathspec = get_pathspec(prefix, argv + 2);
+       tree = parse_tree_indirect(sha1);
+       if (!tree)
+               die("not a tree object");
+       read_tree_recursive(tree, "", 0, 0, pathspec, show_tree);
+
+       return 0;
+}
diff --git a/builtin-mailinfo.c b/builtin-mailinfo.c
new file mode 100644 (file)
index 0000000..2600847
--- /dev/null
@@ -0,0 +1,987 @@
+/*
+ * Another stupid program, this one parsing the headers of an
+ * email to figure out authorship and subject
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "utf8.h"
+
+static FILE *cmitmsg, *patchfile, *fin, *fout;
+
+static int keep_subject;
+static const char *metainfo_charset;
+static char line[1000];
+static char name[1000];
+static char email[1000];
+
+static enum  {
+       TE_DONTCARE, TE_QP, TE_BASE64,
+} transfer_encoding;
+static enum  {
+       TYPE_TEXT, TYPE_OTHER,
+} message_type;
+
+static char charset[256];
+static int patch_lines;
+static char **p_hdr_data, **s_hdr_data;
+
+#define MAX_HDR_PARSED 10
+#define MAX_BOUNDARIES 5
+
+static char *sanity_check(char *name, char *email)
+{
+       int len = strlen(name);
+       if (len < 3 || len > 60)
+               return email;
+       if (strchr(name, '@') || strchr(name, '<') || strchr(name, '>'))
+               return email;
+       return name;
+}
+
+static int bogus_from(char *line)
+{
+       /* John Doe <johndoe> */
+       char *bra, *ket, *dst, *cp;
+
+       /* This is fallback, so do not bother if we already have an
+        * e-mail address.
+        */
+       if (*email)
+               return 0;
+
+       bra = strchr(line, '<');
+       if (!bra)
+               return 0;
+       ket = strchr(bra, '>');
+       if (!ket)
+               return 0;
+
+       for (dst = email, cp = bra+1; cp < ket; )
+               *dst++ = *cp++;
+       *dst = 0;
+       for (cp = line; isspace(*cp); cp++)
+               ;
+       for (bra--; isspace(*bra); bra--)
+               *bra = 0;
+       cp = sanity_check(cp, email);
+       strcpy(name, cp);
+       return 1;
+}
+
+static int handle_from(char *in_line)
+{
+       char line[1000];
+       char *at;
+       char *dst;
+
+       strcpy(line, in_line);
+       at = strchr(line, '@');
+       if (!at)
+               return bogus_from(line);
+
+       /*
+        * If we already have one email, don't take any confusing lines
+        */
+       if (*email && strchr(at+1, '@'))
+               return 0;
+
+       /* Pick up the string around '@', possibly delimited with <>
+        * pair; that is the email part.  White them out while copying.
+        */
+       while (at > line) {
+               char c = at[-1];
+               if (isspace(c))
+                       break;
+               if (c == '<') {
+                       at[-1] = ' ';
+                       break;
+               }
+               at--;
+       }
+       dst = email;
+       for (;;) {
+               unsigned char c = *at;
+               if (!c || c == '>' || isspace(c)) {
+                       if (c == '>')
+                               *at = ' ';
+                       break;
+               }
+               *at++ = ' ';
+               *dst++ = c;
+       }
+       *dst++ = 0;
+
+       /* The remainder is name.  It could be "John Doe <john.doe@xz>"
+        * or "john.doe@xz (John Doe)", but we have whited out the
+        * email part, so trim from both ends, possibly removing
+        * the () pair at the end.
+        */
+       at = line + strlen(line);
+       while (at > line) {
+               unsigned char c = *--at;
+               if (!isspace(c)) {
+                       at[(c == ')') ? 0 : 1] = 0;
+                       break;
+               }
+       }
+
+       at = line;
+       for (;;) {
+               unsigned char c = *at;
+               if (!c || !isspace(c)) {
+                       if (c == '(')
+                               at++;
+                       break;
+               }
+               at++;
+       }
+       at = sanity_check(at, email);
+       strcpy(name, at);
+       return 1;
+}
+
+static int handle_header(char *line, char *data, int ofs)
+{
+       if (!line || !data)
+               return 1;
+
+       strcpy(data, line+ofs);
+
+       return 0;
+}
+
+/* NOTE NOTE NOTE.  We do not claim we do full MIME.  We just attempt
+ * to have enough heuristics to grok MIME encoded patches often found
+ * on our mailing lists.  For example, we do not even treat header lines
+ * case insensitively.
+ */
+
+static int slurp_attr(const char *line, const char *name, char *attr)
+{
+       const char *ends, *ap = strcasestr(line, name);
+       size_t sz;
+
+       if (!ap) {
+               *attr = 0;
+               return 0;
+       }
+       ap += strlen(name);
+       if (*ap == '"') {
+               ap++;
+               ends = "\"";
+       }
+       else
+               ends = "; \t";
+       sz = strcspn(ap, ends);
+       memcpy(attr, ap, sz);
+       attr[sz] = 0;
+       return 1;
+}
+
+struct content_type {
+       char *boundary;
+       int boundary_len;
+};
+
+static struct content_type content[MAX_BOUNDARIES];
+
+static struct content_type *content_top = content;
+
+static int handle_content_type(char *line)
+{
+       char boundary[256];
+
+       if (strcasestr(line, "text/") == NULL)
+                message_type = TYPE_OTHER;
+       if (slurp_attr(line, "boundary=", boundary + 2)) {
+               memcpy(boundary, "--", 2);
+               if (content_top++ >= &content[MAX_BOUNDARIES]) {
+                       fprintf(stderr, "Too many boundaries to handle\n");
+                       exit(1);
+               }
+               content_top->boundary_len = strlen(boundary);
+               content_top->boundary = xmalloc(content_top->boundary_len+1);
+               strcpy(content_top->boundary, boundary);
+       }
+       if (slurp_attr(line, "charset=", charset)) {
+               int i, c;
+               for (i = 0; (c = charset[i]) != 0; i++)
+                       charset[i] = tolower(c);
+       }
+       return 0;
+}
+
+static int handle_content_transfer_encoding(char *line)
+{
+       if (strcasestr(line, "base64"))
+               transfer_encoding = TE_BASE64;
+       else if (strcasestr(line, "quoted-printable"))
+               transfer_encoding = TE_QP;
+       else
+               transfer_encoding = TE_DONTCARE;
+       return 0;
+}
+
+static int is_multipart_boundary(const char *line)
+{
+       return (!memcmp(line, content_top->boundary, content_top->boundary_len));
+}
+
+static int eatspace(char *line)
+{
+       int len = strlen(line);
+       while (len > 0 && isspace(line[len-1]))
+               line[--len] = 0;
+       return len;
+}
+
+static char *cleanup_subject(char *subject)
+{
+       for (;;) {
+               char *p;
+               int len, remove;
+               switch (*subject) {
+               case 'r': case 'R':
+                       if (!memcmp("e:", subject+1, 2)) {
+                               subject += 3;
+                               continue;
+                       }
+                       break;
+               case ' ': case '\t': case ':':
+                       subject++;
+                       continue;
+
+               case '[':
+                       p = strchr(subject, ']');
+                       if (!p) {
+                               subject++;
+                               continue;
+                       }
+                       len = strlen(p);
+                       remove = p - subject;
+                       if (remove <= len *2) {
+                               subject = p+1;
+                               continue;
+                       }
+                       break;
+               }
+               eatspace(subject);
+               return subject;
+       }
+}
+
+static void cleanup_space(char *buf)
+{
+       unsigned char c;
+       while ((c = *buf) != 0) {
+               buf++;
+               if (isspace(c)) {
+                       buf[-1] = ' ';
+                       c = *buf;
+                       while (isspace(c)) {
+                               int len = strlen(buf);
+                               memmove(buf, buf+1, len);
+                               c = *buf;
+                       }
+               }
+       }
+}
+
+static void decode_header(char *it, unsigned itsize);
+static const char *header[MAX_HDR_PARSED] = {
+       "From","Subject","Date",
+};
+
+static int check_header(char *line, unsigned linesize, char **hdr_data, int overwrite)
+{
+       int i;
+
+       /* search for the interesting parts */
+       for (i = 0; header[i]; i++) {
+               int len = strlen(header[i]);
+               if ((!hdr_data[i] || overwrite) &&
+                   !strncasecmp(line, header[i], len) &&
+                   line[len] == ':' && isspace(line[len + 1])) {
+                       /* Unwrap inline B and Q encoding, and optionally
+                        * normalize the meta information to utf8.
+                        */
+                       decode_header(line + len + 2, linesize - len - 2);
+                       hdr_data[i] = xmalloc(1000 * sizeof(char));
+                       if (! handle_header(line, hdr_data[i], len + 2)) {
+                               return 1;
+                       }
+               }
+       }
+
+       /* Content stuff */
+       if (!strncasecmp(line, "Content-Type", 12) &&
+               line[12] == ':' && isspace(line[12 + 1])) {
+               decode_header(line + 12 + 2, linesize - 12 - 2);
+               if (! handle_content_type(line)) {
+                       return 1;
+               }
+       }
+       if (!strncasecmp(line, "Content-Transfer-Encoding", 25) &&
+               line[25] == ':' && isspace(line[25 + 1])) {
+               decode_header(line + 25 + 2, linesize - 25 - 2);
+               if (! handle_content_transfer_encoding(line)) {
+                       return 1;
+               }
+       }
+
+       /* for inbody stuff */
+       if (!memcmp(">From", line, 5) && isspace(line[5]))
+               return 1;
+       if (!memcmp("[PATCH]", line, 7) && isspace(line[7])) {
+               for (i = 0; header[i]; i++) {
+                       if (!memcmp("Subject: ", header[i], 9)) {
+                               if (! handle_header(line, hdr_data[i], 0)) {
+                                       return 1;
+                               }
+                       }
+               }
+       }
+
+       /* no match */
+       return 0;
+}
+
+static int is_rfc2822_header(char *line)
+{
+       /*
+        * The section that defines the loosest possible
+        * field name is "3.6.8 Optional fields".
+        *
+        * optional-field = field-name ":" unstructured CRLF
+        * field-name = 1*ftext
+        * ftext = %d33-57 / %59-126
+        */
+       int ch;
+       char *cp = line;
+
+       /* Count mbox From headers as headers */
+       if (!memcmp(line, "From ", 5) || !memcmp(line, ">From ", 6))
+               return 1;
+
+       while ((ch = *cp++)) {
+               if (ch == ':')
+                       return cp != line;
+               if ((33 <= ch && ch <= 57) ||
+                   (59 <= ch && ch <= 126))
+                       continue;
+               break;
+       }
+       return 0;
+}
+
+/*
+ * sz is size of 'line' buffer in bytes.  Must be reasonably
+ * long enough to hold one physical real-world e-mail line.
+ */
+static int read_one_header_line(char *line, int sz, FILE *in)
+{
+       int len;
+
+       /*
+        * We will read at most (sz-1) bytes and then potentially
+        * re-add NUL after it.  Accessing line[sz] after this is safe
+        * and we can allow len to grow up to and including sz.
+        */
+       sz--;
+
+       /* Get the first part of the line. */
+       if (!fgets(line, sz, in))
+               return 0;
+
+       /*
+        * Is it an empty line or not a valid rfc2822 header?
+        * If so, stop here, and return false ("not a header")
+        */
+       len = eatspace(line);
+       if (!len || !is_rfc2822_header(line)) {
+               /* Re-add the newline */
+               line[len] = '\n';
+               line[len + 1] = '\0';
+               return 0;
+       }
+
+       /*
+        * Now we need to eat all the continuation lines..
+        * Yuck, 2822 header "folding"
+        */
+       for (;;) {
+               int peek, addlen;
+               static char continuation[1000];
+
+               peek = fgetc(in); ungetc(peek, in);
+               if (peek != ' ' && peek != '\t')
+                       break;
+               if (!fgets(continuation, sizeof(continuation), in))
+                       break;
+               addlen = eatspace(continuation);
+               if (len < sz - 1) {
+                       if (addlen >= sz - len)
+                               addlen = sz - len - 1;
+                       memcpy(line + len, continuation, addlen);
+                       line[len] = '\n';
+                       len += addlen;
+               }
+       }
+       line[len] = 0;
+
+       return 1;
+}
+
+static int decode_q_segment(char *in, char *ot, unsigned otsize, char *ep, int rfc2047)
+{
+       char *otend = ot + otsize;
+       int c;
+       while ((c = *in++) != 0 && (in <= ep)) {
+               if (ot == otend) {
+                       *--ot = '\0';
+                       return -1;
+               }
+               if (c == '=') {
+                       int d = *in++;
+                       if (d == '\n' || !d)
+                               break; /* drop trailing newline */
+                       *ot++ = ((hexval(d) << 4) | hexval(*in++));
+                       continue;
+               }
+               if (rfc2047 && c == '_') /* rfc2047 4.2 (2) */
+                       c = 0x20;
+               *ot++ = c;
+       }
+       *ot = 0;
+       return 0;
+}
+
+static int decode_b_segment(char *in, char *ot, unsigned otsize, char *ep)
+{
+       /* Decode in..ep, possibly in-place to ot */
+       int c, pos = 0, acc = 0;
+       char *otend = ot + otsize;
+
+       while ((c = *in++) != 0 && (in <= ep)) {
+               if (ot == otend) {
+                       *--ot = '\0';
+                       return -1;
+               }
+               if (c == '+')
+                       c = 62;
+               else if (c == '/')
+                       c = 63;
+               else if ('A' <= c && c <= 'Z')
+                       c -= 'A';
+               else if ('a' <= c && c <= 'z')
+                       c -= 'a' - 26;
+               else if ('0' <= c && c <= '9')
+                       c -= '0' - 52;
+               else if (c == '=') {
+                       /* padding is almost like (c == 0), except we do
+                        * not output NUL resulting only from it;
+                        * for now we just trust the data.
+                        */
+                       c = 0;
+               }
+               else
+                       continue; /* garbage */
+               switch (pos++) {
+               case 0:
+                       acc = (c << 2);
+                       break;
+               case 1:
+                       *ot++ = (acc | (c >> 4));
+                       acc = (c & 15) << 4;
+                       break;
+               case 2:
+                       *ot++ = (acc | (c >> 2));
+                       acc = (c & 3) << 6;
+                       break;
+               case 3:
+                       *ot++ = (acc | c);
+                       acc = pos = 0;
+                       break;
+               }
+       }
+       *ot = 0;
+       return 0;
+}
+
+/*
+ * When there is no known charset, guess.
+ *
+ * Right now we assume that if the target is UTF-8 (the default),
+ * and it already looks like UTF-8 (which includes US-ASCII as its
+ * subset, of course) then that is what it is and there is nothing
+ * to do.
+ *
+ * Otherwise, we default to assuming it is Latin1 for historical
+ * reasons.
+ */
+static const char *guess_charset(const char *line, const char *target_charset)
+{
+       if (is_encoding_utf8(target_charset)) {
+               if (is_utf8(line))
+                       return NULL;
+       }
+       return "latin1";
+}
+
+static void convert_to_utf8(char *line, unsigned linesize, const char *charset)
+{
+       char *out;
+
+       if (!charset || !*charset) {
+               charset = guess_charset(line, metainfo_charset);
+               if (!charset)
+                       return;
+       }
+
+       if (!strcmp(metainfo_charset, charset))
+               return;
+       out = reencode_string(line, metainfo_charset, charset);
+       if (!out)
+               die("cannot convert from %s to %s\n",
+                   charset, metainfo_charset);
+       strlcpy(line, out, linesize);
+       free(out);
+}
+
+static int decode_header_bq(char *it, unsigned itsize)
+{
+       char *in, *out, *ep, *cp, *sp;
+       char outbuf[1000];
+       int rfc2047 = 0;
+
+       in = it;
+       out = outbuf;
+       while ((ep = strstr(in, "=?")) != NULL) {
+               int sz, encoding;
+               char charset_q[256], piecebuf[256];
+               rfc2047 = 1;
+
+               if (in != ep) {
+                       sz = ep - in;
+                       memcpy(out, in, sz);
+                       out += sz;
+                       in += sz;
+               }
+               /* E.g.
+                * ep : "=?iso-2022-jp?B?GyR...?= foo"
+                * ep : "=?ISO-8859-1?Q?Foo=FCbar?= baz"
+                */
+               ep += 2;
+               cp = strchr(ep, '?');
+               if (!cp)
+                       return rfc2047; /* no munging */
+               for (sp = ep; sp < cp; sp++)
+                       charset_q[sp - ep] = tolower(*sp);
+               charset_q[cp - ep] = 0;
+               encoding = cp[1];
+               if (!encoding || cp[2] != '?')
+                       return rfc2047; /* no munging */
+               ep = strstr(cp + 3, "?=");
+               if (!ep)
+                       return rfc2047; /* no munging */
+               switch (tolower(encoding)) {
+               default:
+                       return rfc2047; /* no munging */
+               case 'b':
+                       sz = decode_b_segment(cp + 3, piecebuf, sizeof(piecebuf), ep);
+                       break;
+               case 'q':
+                       sz = decode_q_segment(cp + 3, piecebuf, sizeof(piecebuf), ep, 1);
+                       break;
+               }
+               if (sz < 0)
+                       return rfc2047;
+               if (metainfo_charset)
+                       convert_to_utf8(piecebuf, sizeof(piecebuf), charset_q);
+
+               sz = strlen(piecebuf);
+               if (outbuf + sizeof(outbuf) <= out + sz)
+                       return rfc2047; /* no munging */
+               strcpy(out, piecebuf);
+               out += sz;
+               in = ep + 2;
+       }
+       strcpy(out, in);
+       strlcpy(it, outbuf, itsize);
+       return rfc2047;
+}
+
+static void decode_header(char *it, unsigned itsize)
+{
+
+       if (decode_header_bq(it, itsize))
+               return;
+       /* otherwise "it" is a straight copy of the input.
+        * This can be binary guck but there is no charset specified.
+        */
+       if (metainfo_charset)
+               convert_to_utf8(it, itsize, "");
+}
+
+static void decode_transfer_encoding(char *line, unsigned linesize)
+{
+       char *ep;
+
+       switch (transfer_encoding) {
+       case TE_QP:
+               ep = line + strlen(line);
+               decode_q_segment(line, line, linesize, ep, 0);
+               break;
+       case TE_BASE64:
+               ep = line + strlen(line);
+               decode_b_segment(line, line, linesize, ep);
+               break;
+       case TE_DONTCARE:
+               break;
+       }
+}
+
+static int handle_filter(char *line, unsigned linesize);
+
+static int find_boundary(void)
+{
+       while(fgets(line, sizeof(line), fin) != NULL) {
+               if (is_multipart_boundary(line))
+                       return 1;
+       }
+       return 0;
+}
+
+static int handle_boundary(void)
+{
+       char newline[]="\n";
+again:
+       if (!memcmp(line+content_top->boundary_len, "--", 2)) {
+               /* we hit an end boundary */
+               /* pop the current boundary off the stack */
+               free(content_top->boundary);
+
+               /* technically won't happen as is_multipart_boundary()
+                  will fail first.  But just in case..
+                */
+               if (content_top-- < content) {
+                       fprintf(stderr, "Detected mismatched boundaries, "
+                                       "can't recover\n");
+                       exit(1);
+               }
+               handle_filter(newline, sizeof(newline));
+
+               /* skip to the next boundary */
+               if (!find_boundary())
+                       return 0;
+               goto again;
+       }
+
+       /* set some defaults */
+       transfer_encoding = TE_DONTCARE;
+       charset[0] = 0;
+       message_type = TYPE_TEXT;
+
+       /* slurp in this section's info */
+       while (read_one_header_line(line, sizeof(line), fin))
+               check_header(line, sizeof(line), p_hdr_data, 0);
+
+       /* eat the blank line after section info */
+       return (fgets(line, sizeof(line), fin) != NULL);
+}
+
+static inline int patchbreak(const char *line)
+{
+       /* Beginning of a "diff -" header? */
+       if (!memcmp("diff -", line, 6))
+               return 1;
+
+       /* CVS "Index: " line? */
+       if (!memcmp("Index: ", line, 7))
+               return 1;
+
+       /*
+        * "--- <filename>" starts patches without headers
+        * "---<sp>*" is a manual separator
+        */
+       if (!memcmp("---", line, 3)) {
+               line += 3;
+               /* space followed by a filename? */
+               if (line[0] == ' ' && !isspace(line[1]))
+                       return 1;
+               /* Just whitespace? */
+               for (;;) {
+                       unsigned char c = *line++;
+                       if (c == '\n')
+                               return 1;
+                       if (!isspace(c))
+                               break;
+               }
+               return 0;
+       }
+       return 0;
+}
+
+
+static int handle_commit_msg(char *line, unsigned linesize)
+{
+       static int still_looking = 1;
+       char *endline = line + linesize;
+
+       if (!cmitmsg)
+               return 0;
+
+       if (still_looking) {
+               char *cp = line;
+               if (isspace(*line)) {
+                       for (cp = line + 1; *cp; cp++) {
+                               if (!isspace(*cp))
+                                       break;
+                       }
+                       if (!*cp)
+                               return 0;
+               }
+               if ((still_looking = check_header(cp, endline - cp, s_hdr_data, 0)) != 0)
+                       return 0;
+       }
+
+       /* normalize the log message to UTF-8. */
+       if (metainfo_charset)
+               convert_to_utf8(line, endline - line, charset);
+
+       if (patchbreak(line)) {
+               fclose(cmitmsg);
+               cmitmsg = NULL;
+               return 1;
+       }
+
+       fputs(line, cmitmsg);
+       return 0;
+}
+
+static int handle_patch(char *line)
+{
+       fputs(line, patchfile);
+       patch_lines++;
+       return 0;
+}
+
+static int handle_filter(char *line, unsigned linesize)
+{
+       static int filter = 0;
+
+       /* filter tells us which part we left off on
+        * a non-zero return indicates we hit a filter point
+        */
+       switch (filter) {
+       case 0:
+               if (!handle_commit_msg(line, linesize))
+                       break;
+               filter++;
+       case 1:
+               if (!handle_patch(line))
+                       break;
+               filter++;
+       default:
+               return 1;
+       }
+
+       return 0;
+}
+
+static void handle_body(void)
+{
+       int rc = 0;
+       static char newline[2000];
+       static char *np = newline;
+
+       /* Skip up to the first boundary */
+       if (content_top->boundary) {
+               if (!find_boundary())
+                       return;
+       }
+
+       do {
+               /* process any boundary lines */
+               if (content_top->boundary && is_multipart_boundary(line)) {
+                       /* flush any leftover */
+                       if ((transfer_encoding == TE_BASE64)  &&
+                           (np != newline)) {
+                               handle_filter(newline, sizeof(newline));
+                       }
+                       if (!handle_boundary())
+                               return;
+               }
+
+               /* Unwrap transfer encoding */
+               decode_transfer_encoding(line, sizeof(line));
+
+               switch (transfer_encoding) {
+               case TE_BASE64:
+               {
+                       char *op = line;
+
+                       /* binary data most likely doesn't have newlines */
+                       if (message_type != TYPE_TEXT) {
+                               rc = handle_filter(line, sizeof(newline));
+                               break;
+                       }
+
+                       /* this is a decoded line that may contain
+                        * multiple new lines.  Pass only one chunk
+                        * at a time to handle_filter()
+                        */
+
+                       do {
+                               while (*op != '\n' && *op != 0)
+                                       *np++ = *op++;
+                               *np = *op;
+                               if (*np != 0) {
+                                       /* should be sitting on a new line */
+                                       *(++np) = 0;
+                                       op++;
+                                       rc = handle_filter(newline, sizeof(newline));
+                                       np = newline;
+                               }
+                       } while (*op != 0);
+                       /* the partial chunk is saved in newline and
+                        * will be appended by the next iteration of fgets
+                        */
+                       break;
+               }
+               default:
+                       rc = handle_filter(line, sizeof(newline));
+               }
+               if (rc)
+                       /* nothing left to filter */
+                       break;
+       } while (fgets(line, sizeof(line), fin));
+
+       return;
+}
+
+static void output_header_lines(FILE *fout, const char *hdr, char *data)
+{
+       while (1) {
+               char *ep = strchr(data, '\n');
+               int len;
+               if (!ep)
+                       len = strlen(data);
+               else
+                       len = ep - data;
+               fprintf(fout, "%s: %.*s\n", hdr, len, data);
+               if (!ep)
+                       break;
+               data = ep + 1;
+       }
+}
+
+static void handle_info(void)
+{
+       char *sub;
+       char *hdr;
+       int i;
+
+       for (i = 0; header[i]; i++) {
+
+               /* only print inbody headers if we output a patch file */
+               if (patch_lines && s_hdr_data[i])
+                       hdr = s_hdr_data[i];
+               else if (p_hdr_data[i])
+                       hdr = p_hdr_data[i];
+               else
+                       continue;
+
+               if (!memcmp(header[i], "Subject", 7)) {
+                       if (keep_subject)
+                               sub = hdr;
+                       else {
+                               sub = cleanup_subject(hdr);
+                               cleanup_space(sub);
+                       }
+                       output_header_lines(fout, "Subject", sub);
+               } else if (!memcmp(header[i], "From", 4)) {
+                       handle_from(hdr);
+                       fprintf(fout, "Author: %s\n", name);
+                       fprintf(fout, "Email: %s\n", email);
+               } else {
+                       cleanup_space(hdr);
+                       fprintf(fout, "%s: %s\n", header[i], hdr);
+               }
+       }
+       fprintf(fout, "\n");
+}
+
+static int mailinfo(FILE *in, FILE *out, int ks, const char *encoding,
+                   const char *msg, const char *patch)
+{
+       int peek;
+       keep_subject = ks;
+       metainfo_charset = encoding;
+       fin = in;
+       fout = out;
+
+       cmitmsg = fopen(msg, "w");
+       if (!cmitmsg) {
+               perror(msg);
+               return -1;
+       }
+       patchfile = fopen(patch, "w");
+       if (!patchfile) {
+               perror(patch);
+               fclose(cmitmsg);
+               return -1;
+       }
+
+       p_hdr_data = xcalloc(MAX_HDR_PARSED, sizeof(char *));
+       s_hdr_data = xcalloc(MAX_HDR_PARSED, sizeof(char *));
+
+       do {
+               peek = fgetc(in);
+       } while (isspace(peek));
+       ungetc(peek, in);
+
+       /* process the email header */
+       while (read_one_header_line(line, sizeof(line), fin))
+               check_header(line, sizeof(line), p_hdr_data, 1);
+
+       handle_body();
+       handle_info();
+
+       return 0;
+}
+
+static const char mailinfo_usage[] =
+       "git-mailinfo [-k] [-u | --encoding=<encoding>] msg patch <mail >info";
+
+int cmd_mailinfo(int argc, const char **argv, const char *prefix)
+{
+       const char *def_charset;
+
+       /* NEEDSWORK: might want to do the optional .git/ directory
+        * discovery
+        */
+       git_config(git_default_config);
+
+       def_charset = (git_commit_encoding ? git_commit_encoding : "utf-8");
+       metainfo_charset = def_charset;
+
+       while (1 < argc && argv[1][0] == '-') {
+               if (!strcmp(argv[1], "-k"))
+                       keep_subject = 1;
+               else if (!strcmp(argv[1], "-u"))
+                       metainfo_charset = def_charset;
+               else if (!strcmp(argv[1], "-n"))
+                       metainfo_charset = NULL;
+               else if (!prefixcmp(argv[1], "--encoding="))
+                       metainfo_charset = argv[1] + 11;
+               else
+                       usage(mailinfo_usage);
+               argc--; argv++;
+       }
+
+       if (argc != 3)
+               usage(mailinfo_usage);
+
+       return !!mailinfo(stdin, stdout, keep_subject, metainfo_charset, argv[1], argv[2]);
+}
diff --git a/builtin-mailsplit.c b/builtin-mailsplit.c
new file mode 100644 (file)
index 0000000..46b27cd
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ * Totally braindamaged mbox splitter program.
+ *
+ * It just splits a mbox into a list of files: "0001" "0002" ..
+ * so you can process them further from there.
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "path-list.h"
+
+static const char git_mailsplit_usage[] =
+"git-mailsplit [-d<prec>] [-f<n>] [-b] -o<directory> <mbox>|<Maildir>...";
+
+static int is_from_line(const char *line, int len)
+{
+       const char *colon;
+
+       if (len < 20 || memcmp("From ", line, 5))
+               return 0;
+
+       colon = line + len - 2;
+       line += 5;
+       for (;;) {
+               if (colon < line)
+                       return 0;
+               if (*--colon == ':')
+                       break;
+       }
+
+       if (!isdigit(colon[-4]) ||
+           !isdigit(colon[-2]) ||
+           !isdigit(colon[-1]) ||
+           !isdigit(colon[ 1]) ||
+           !isdigit(colon[ 2]))
+               return 0;
+
+       /* year */
+       if (strtol(colon+3, NULL, 10) <= 90)
+               return 0;
+
+       /* Ok, close enough */
+       return 1;
+}
+
+/* Could be as small as 64, enough to hold a Unix "From " line. */
+static char buf[4096];
+
+/* Called with the first line (potentially partial)
+ * already in buf[] -- normally that should begin with
+ * the Unix "From " line.  Write it into the specified
+ * file.
+ */
+static int split_one(FILE *mbox, const char *name, int allow_bare)
+{
+       FILE *output = NULL;
+       int len = strlen(buf);
+       int fd;
+       int status = 0;
+       int is_bare = !is_from_line(buf, len);
+
+       if (is_bare && !allow_bare)
+               goto corrupt;
+
+       fd = open(name, O_WRONLY | O_CREAT | O_EXCL, 0666);
+       if (fd < 0)
+               die("cannot open output file %s", name);
+       output = fdopen(fd, "w");
+
+       /* Copy it out, while searching for a line that begins with
+        * "From " and having something that looks like a date format.
+        */
+       for (;;) {
+               int is_partial = (buf[len-1] != '\n');
+
+               if (fputs(buf, output) == EOF)
+                       die("cannot write output");
+
+               if (fgets(buf, sizeof(buf), mbox) == NULL) {
+                       if (feof(mbox)) {
+                               status = 1;
+                               break;
+                       }
+                       die("cannot read mbox");
+               }
+               len = strlen(buf);
+               if (!is_partial && !is_bare && is_from_line(buf, len))
+                       break; /* done with one message */
+       }
+       fclose(output);
+       return status;
+
+ corrupt:
+       if (output)
+               fclose(output);
+       unlink(name);
+       fprintf(stderr, "corrupt mailbox\n");
+       exit(1);
+}
+
+static int populate_maildir_list(struct path_list *list, const char *path)
+{
+       DIR *dir;
+       struct dirent *dent;
+       char name[PATH_MAX];
+       char *subs[] = { "cur", "new", NULL };
+       char **sub;
+
+       for (sub = subs; *sub; ++sub) {
+               snprintf(name, sizeof(name), "%s/%s", path, *sub);
+               if ((dir = opendir(name)) == NULL) {
+                       if (errno == ENOENT)
+                               continue;
+                       error("cannot opendir %s (%s)", name, strerror(errno));
+                       return -1;
+               }
+
+               while ((dent = readdir(dir)) != NULL) {
+                       if (dent->d_name[0] == '.')
+                               continue;
+                       snprintf(name, sizeof(name), "%s/%s", *sub, dent->d_name);
+                       path_list_insert(name, list);
+               }
+
+               closedir(dir);
+       }
+
+       return 0;
+}
+
+static int split_maildir(const char *maildir, const char *dir,
+       int nr_prec, int skip)
+{
+       char file[PATH_MAX];
+       char name[PATH_MAX];
+       int ret = -1;
+       int i;
+       struct path_list list = {NULL, 0, 0, 1};
+
+       if (populate_maildir_list(&list, maildir) < 0)
+               goto out;
+
+       for (i = 0; i < list.nr; i++) {
+               FILE *f;
+               snprintf(file, sizeof(file), "%s/%s", maildir, list.items[i].path);
+               f = fopen(file, "r");
+               if (!f) {
+                       error("cannot open mail %s (%s)", file, strerror(errno));
+                       goto out;
+               }
+
+               if (fgets(buf, sizeof(buf), f) == NULL) {
+                       error("cannot read mail %s (%s)", file, strerror(errno));
+                       goto out;
+               }
+
+               sprintf(name, "%s/%0*d", dir, nr_prec, ++skip);
+               split_one(f, name, 1);
+
+               fclose(f);
+       }
+
+       ret = skip;
+out:
+       path_list_clear(&list, 1);
+       return ret;
+}
+
+static int split_mbox(const char *file, const char *dir, int allow_bare,
+                     int nr_prec, int skip)
+{
+       char name[PATH_MAX];
+       int ret = -1;
+       int peek;
+
+       FILE *f = !strcmp(file, "-") ? stdin : fopen(file, "r");
+       int file_done = 0;
+
+       if (!f) {
+               error("cannot open mbox %s", file);
+               goto out;
+       }
+
+       do {
+               peek = fgetc(f);
+       } while (isspace(peek));
+       ungetc(peek, f);
+
+       if (fgets(buf, sizeof(buf), f) == NULL) {
+               /* empty stdin is OK */
+               if (f != stdin) {
+                       error("cannot read mbox %s", file);
+                       goto out;
+               }
+               file_done = 1;
+       }
+
+       while (!file_done) {
+               sprintf(name, "%s/%0*d", dir, nr_prec, ++skip);
+               file_done = split_one(f, name, allow_bare);
+       }
+
+       if (f != stdin)
+               fclose(f);
+
+       ret = skip;
+out:
+       return ret;
+}
+
+int cmd_mailsplit(int argc, const char **argv, const char *prefix)
+{
+       int nr = 0, nr_prec = 4, num = 0;
+       int allow_bare = 0;
+       const char *dir = NULL;
+       const char **argp;
+       static const char *stdin_only[] = { "-", NULL };
+
+       for (argp = argv+1; *argp; argp++) {
+               const char *arg = *argp;
+
+               if (arg[0] != '-')
+                       break;
+               /* do flags here */
+               if ( arg[1] == 'd' ) {
+                       nr_prec = strtol(arg+2, NULL, 10);
+                       if (nr_prec < 3 || 10 <= nr_prec)
+                               usage(git_mailsplit_usage);
+                       continue;
+               } else if ( arg[1] == 'f' ) {
+                       nr = strtol(arg+2, NULL, 10);
+               } else if ( arg[1] == 'b' && !arg[2] ) {
+                       allow_bare = 1;
+               } else if ( arg[1] == 'o' && arg[2] ) {
+                       dir = arg+2;
+               } else if ( arg[1] == '-' && !arg[2] ) {
+                       argp++; /* -- marks end of options */
+                       break;
+               } else {
+                       die("unknown option: %s", arg);
+               }
+       }
+
+       if ( !dir ) {
+               /* Backwards compatibility: if no -o specified, accept
+                  <mbox> <dir> or just <dir> */
+               switch (argc - (argp-argv)) {
+               case 1:
+                       dir = argp[0];
+                       argp = stdin_only;
+                       break;
+               case 2:
+                       stdin_only[0] = argp[0];
+                       dir = argp[1];
+                       argp = stdin_only;
+                       break;
+               default:
+                       usage(git_mailsplit_usage);
+               }
+       } else {
+               /* New usage: if no more argument, parse stdin */
+               if ( !*argp )
+                       argp = stdin_only;
+       }
+
+       while (*argp) {
+               const char *arg = *argp++;
+               struct stat argstat;
+               int ret = 0;
+
+               if (arg[0] == '-' && arg[1] == 0) {
+                       ret = split_mbox(arg, dir, allow_bare, nr_prec, nr);
+                       if (ret < 0) {
+                               error("cannot split patches from stdin");
+                               return 1;
+                       }
+                       num += (ret - nr);
+                       nr = ret;
+                       continue;
+               }
+
+               if (stat(arg, &argstat) == -1) {
+                       error("cannot stat %s (%s)", arg, strerror(errno));
+                       return 1;
+               }
+
+               if (S_ISDIR(argstat.st_mode))
+                       ret = split_maildir(arg, dir, nr_prec, nr);
+               else
+                       ret = split_mbox(arg, dir, allow_bare, nr_prec, nr);
+
+               if (ret < 0) {
+                       error("cannot split patches from %s", arg);
+                       return 1;
+               }
+               num += (ret - nr);
+               nr = ret;
+       }
+
+       printf("%d\n", num);
+
+       return 0;
+}
diff --git a/builtin-merge-base.c b/builtin-merge-base.c
new file mode 100644 (file)
index 0000000..0108e22
--- /dev/null
@@ -0,0 +1,52 @@
+#include "builtin.h"
+#include "cache.h"
+#include "commit.h"
+
+static int show_merge_base(struct commit *rev1, struct commit *rev2, int show_all)
+{
+       struct commit_list *result = get_merge_bases(rev1, rev2, 0);
+
+       if (!result)
+               return 1;
+
+       while (result) {
+               printf("%s\n", sha1_to_hex(result->item->object.sha1));
+               if (!show_all)
+                       return 0;
+               result = result->next;
+       }
+
+       return 0;
+}
+
+static const char merge_base_usage[] =
+"git-merge-base [--all] <commit-id> <commit-id>";
+
+int cmd_merge_base(int argc, const char **argv, const char *prefix)
+{
+       struct commit *rev1, *rev2;
+       unsigned char rev1key[20], rev2key[20];
+       int show_all = 0;
+
+       git_config(git_default_config);
+
+       while (1 < argc && argv[1][0] == '-') {
+               const char *arg = argv[1];
+               if (!strcmp(arg, "-a") || !strcmp(arg, "--all"))
+                       show_all = 1;
+               else
+                       usage(merge_base_usage);
+               argc--; argv++;
+       }
+       if (argc != 3)
+               usage(merge_base_usage);
+       if (get_sha1(argv[1], rev1key))
+               die("Not a valid object name %s", argv[1]);
+       if (get_sha1(argv[2], rev2key))
+               die("Not a valid object name %s", argv[2]);
+       rev1 = lookup_commit_reference(rev1key);
+       rev2 = lookup_commit_reference(rev2key);
+       if (!rev1 || !rev2)
+               return 1;
+       return show_merge_base(rev1, rev2, show_all);
+}
diff --git a/builtin-merge-file.c b/builtin-merge-file.c
new file mode 100644 (file)
index 0000000..58deb62
--- /dev/null
@@ -0,0 +1,68 @@
+#include "builtin.h"
+#include "cache.h"
+#include "xdiff/xdiff.h"
+#include "xdiff-interface.h"
+
+static const char merge_file_usage[] =
+"git merge-file [-p | --stdout] [-q | --quiet] [-L name1 [-L orig [-L name2]]] file1 orig_file file2";
+
+int cmd_merge_file(int argc, const char **argv, const char *prefix)
+{
+       const char *names[3];
+       mmfile_t mmfs[3];
+       mmbuffer_t result = {NULL, 0};
+       xpparam_t xpp = {XDF_NEED_MINIMAL};
+       int ret = 0, i = 0, to_stdout = 0;
+
+       while (argc > 4) {
+               if (!strcmp(argv[1], "-L") && i < 3) {
+                       names[i++] = argv[2];
+                       argc--;
+                       argv++;
+               } else if (!strcmp(argv[1], "-p") ||
+                               !strcmp(argv[1], "--stdout"))
+                       to_stdout = 1;
+               else if (!strcmp(argv[1], "-q") ||
+                               !strcmp(argv[1], "--quiet"))
+                       freopen("/dev/null", "w", stderr);
+               else
+                       usage(merge_file_usage);
+               argc--;
+               argv++;
+       }
+
+       if (argc != 4)
+               usage(merge_file_usage);
+
+       for (; i < 3; i++)
+               names[i] = argv[i + 1];
+
+       for (i = 0; i < 3; i++) {
+               if (read_mmfile(mmfs + i, argv[i + 1]))
+                       return -1;
+               if (buffer_is_binary(mmfs[i].ptr, mmfs[i].size))
+                       return error("Cannot merge binary files: %s\n",
+                                       argv[i + 1]);
+       }
+
+       ret = xdl_merge(mmfs + 1, mmfs + 0, names[0], mmfs + 2, names[2],
+                       &xpp, XDL_MERGE_ZEALOUS, &result);
+
+       for (i = 0; i < 3; i++)
+               free(mmfs[i].ptr);
+
+       if (ret >= 0) {
+               const char *filename = argv[1];
+               FILE *f = to_stdout ? stdout : fopen(filename, "wb");
+
+               if (!f)
+                       ret = error("Could not open %s for writing", filename);
+               else if (fwrite(result.ptr, result.size, 1, f) != 1)
+                       ret = error("Could not write to %s", filename);
+               else if (fclose(f))
+                       ret = error("Could not close %s", filename);
+               free(result.ptr);
+       }
+
+       return ret;
+}
diff --git a/builtin-merge-ours.c b/builtin-merge-ours.c
new file mode 100644 (file)
index 0000000..8f5bbaf
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Implementation of git-merge-ours.sh as builtin
+ *
+ * Copyright (c) 2007 Thomas Harning Jr
+ * Original:
+ * Original Copyright (c) 2005 Junio C Hamano
+ *
+ * Pretend we resolved the heads, but declare our tree trumps everybody else.
+ */
+#include "git-compat-util.h"
+#include "builtin.h"
+
+static const char *diff_index_args[] = {
+       "diff-index", "--quiet", "--cached", "HEAD", "--", NULL
+};
+#define NARGS (ARRAY_SIZE(diff_index_args) - 1)
+
+int cmd_merge_ours(int argc, const char **argv, const char *prefix)
+{
+       /*
+        * We need to exit with 2 if the index does not match our HEAD tree,
+        * because the current index is what we will be committing as the
+        * merge result.
+        */
+       if (cmd_diff_index(NARGS, diff_index_args, prefix))
+               exit(2);
+       exit(0);
+}
diff --git a/builtin-mv.c b/builtin-mv.c
new file mode 100644 (file)
index 0000000..a3f9ad1
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * "git mv" builtin command
+ *
+ * Copyright (C) 2006 Johannes Schindelin
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "dir.h"
+#include "cache-tree.h"
+#include "path-list.h"
+#include "parse-options.h"
+
+static const char * const builtin_mv_usage[] = {
+       "git-mv [options] <source>... <destination>",
+       NULL
+};
+
+static const char **copy_pathspec(const char *prefix, const char **pathspec,
+                                 int count, int base_name)
+{
+       int i;
+       const char **result = xmalloc((count + 1) * sizeof(const char *));
+       memcpy(result, pathspec, count * sizeof(const char *));
+       result[count] = NULL;
+       for (i = 0; i < count; i++) {
+               int length = strlen(result[i]);
+               if (length > 0 && result[i][length - 1] == '/') {
+                       result[i] = xmemdupz(result[i], length - 1);
+               }
+               if (base_name) {
+                       const char *last_slash = strrchr(result[i], '/');
+                       if (last_slash)
+                               result[i] = last_slash + 1;
+               }
+       }
+       return get_pathspec(prefix, result);
+}
+
+static void show_list(const char *label, struct path_list *list)
+{
+       if (list->nr > 0) {
+               int i;
+               printf("%s", label);
+               for (i = 0; i < list->nr; i++)
+                       printf("%s%s", i > 0 ? ", " : "", list->items[i].path);
+               putchar('\n');
+       }
+}
+
+static const char *add_slash(const char *path)
+{
+       int len = strlen(path);
+       if (path[len - 1] != '/') {
+               char *with_slash = xmalloc(len + 2);
+               memcpy(with_slash, path, len);
+               with_slash[len++] = '/';
+               with_slash[len] = 0;
+               return with_slash;
+       }
+       return path;
+}
+
+static struct lock_file lock_file;
+
+int cmd_mv(int argc, const char **argv, const char *prefix)
+{
+       int i, newfd;
+       int verbose = 0, show_only = 0, force = 0, ignore_errors = 0;
+       struct option builtin_mv_options[] = {
+               OPT__DRY_RUN(&show_only),
+               OPT_BOOLEAN('f', NULL, &force, "force move/rename even if target exists"),
+               OPT_BOOLEAN('k', NULL, &ignore_errors, "skip move/rename errors"),
+               OPT_END(),
+       };
+       const char **source, **destination, **dest_path;
+       enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes;
+       struct stat st;
+       struct path_list overwritten = {NULL, 0, 0, 0};
+       struct path_list src_for_dst = {NULL, 0, 0, 0};
+       struct path_list added = {NULL, 0, 0, 0};
+       struct path_list deleted = {NULL, 0, 0, 0};
+       struct path_list changed = {NULL, 0, 0, 0};
+
+       git_config(git_default_config);
+
+       newfd = hold_locked_index(&lock_file, 1);
+       if (read_cache() < 0)
+               die("index file corrupt");
+
+       argc = parse_options(argc, argv, builtin_mv_options, builtin_mv_usage, 0);
+       if (--argc < 1)
+               usage_with_options(builtin_mv_usage, builtin_mv_options);
+
+       source = copy_pathspec(prefix, argv, argc, 0);
+       modes = xcalloc(argc, sizeof(enum update_mode));
+       dest_path = copy_pathspec(prefix, argv + argc, 1, 0);
+
+       if (dest_path[0][0] == '\0')
+               /* special case: "." was normalized to "" */
+               destination = copy_pathspec(dest_path[0], argv, argc, 1);
+       else if (!lstat(dest_path[0], &st) &&
+                       S_ISDIR(st.st_mode)) {
+               dest_path[0] = add_slash(dest_path[0]);
+               destination = copy_pathspec(dest_path[0], argv, argc, 1);
+       } else {
+               if (argc != 1)
+                       usage_with_options(builtin_mv_usage, builtin_mv_options);
+               destination = dest_path;
+       }
+
+       /* Checking */
+       for (i = 0; i < argc; i++) {
+               const char *src = source[i], *dst = destination[i];
+               int length, src_is_dir;
+               const char *bad = NULL;
+
+               if (show_only)
+                       printf("Checking rename of '%s' to '%s'\n", src, dst);
+
+               length = strlen(src);
+               if (lstat(src, &st) < 0)
+                       bad = "bad source";
+               else if (!strncmp(src, dst, length) &&
+                               (dst[length] == 0 || dst[length] == '/')) {
+                       bad = "can not move directory into itself";
+               } else if ((src_is_dir = S_ISDIR(st.st_mode))
+                               && lstat(dst, &st) == 0)
+                       bad = "cannot move directory over file";
+               else if (src_is_dir) {
+                       const char *src_w_slash = add_slash(src);
+                       int len_w_slash = length + 1;
+                       int first, last;
+
+                       modes[i] = WORKING_DIRECTORY;
+
+                       first = cache_name_pos(src_w_slash, len_w_slash);
+                       if (first >= 0)
+                               die ("Huh? %.*s is in index?",
+                                               len_w_slash, src_w_slash);
+
+                       first = -1 - first;
+                       for (last = first; last < active_nr; last++) {
+                               const char *path = active_cache[last]->name;
+                               if (strncmp(path, src_w_slash, len_w_slash))
+                                       break;
+                       }
+                       free((char *)src_w_slash);
+
+                       if (last - first < 1)
+                               bad = "source directory is empty";
+                       else {
+                               int j, dst_len;
+
+                               if (last - first > 0) {
+                                       source = xrealloc(source,
+                                                       (argc + last - first)
+                                                       * sizeof(char *));
+                                       destination = xrealloc(destination,
+                                                       (argc + last - first)
+                                                       * sizeof(char *));
+                                       modes = xrealloc(modes,
+                                                       (argc + last - first)
+                                                       * sizeof(enum update_mode));
+                               }
+
+                               dst = add_slash(dst);
+                               dst_len = strlen(dst) - 1;
+
+                               for (j = 0; j < last - first; j++) {
+                                       const char *path =
+                                               active_cache[first + j]->name;
+                                       source[argc + j] = path;
+                                       destination[argc + j] =
+                                               prefix_path(dst, dst_len,
+                                                       path + length);
+                                       modes[argc + j] = INDEX;
+                               }
+                               argc += last - first;
+                       }
+               } else if (lstat(dst, &st) == 0) {
+                       bad = "destination exists";
+                       if (force) {
+                               /*
+                                * only files can overwrite each other:
+                                * check both source and destination
+                                */
+                               if (S_ISREG(st.st_mode)) {
+                                       fprintf(stderr, "Warning: %s;"
+                                                       " will overwrite!\n",
+                                                       bad);
+                                       bad = NULL;
+                                       path_list_insert(dst, &overwritten);
+                               } else
+                                       bad = "Cannot overwrite";
+                       }
+               } else if (cache_name_pos(src, length) < 0)
+                       bad = "not under version control";
+               else if (path_list_has_path(&src_for_dst, dst))
+                       bad = "multiple sources for the same target";
+               else
+                       path_list_insert(dst, &src_for_dst);
+
+               if (bad) {
+                       if (ignore_errors) {
+                               if (--argc > 0) {
+                                       memmove(source + i, source + i + 1,
+                                               (argc - i) * sizeof(char *));
+                                       memmove(destination + i,
+                                               destination + i + 1,
+                                               (argc - i) * sizeof(char *));
+                               }
+                       } else
+                               die ("%s, source=%s, destination=%s",
+                                    bad, src, dst);
+               }
+       }
+
+       for (i = 0; i < argc; i++) {
+               const char *src = source[i], *dst = destination[i];
+               enum update_mode mode = modes[i];
+               if (show_only || verbose)
+                       printf("Renaming %s to %s\n", src, dst);
+               if (!show_only && mode != INDEX &&
+                               rename(src, dst) < 0 && !ignore_errors)
+                       die ("renaming %s failed: %s", src, strerror(errno));
+
+               if (mode == WORKING_DIRECTORY)
+                       continue;
+
+               if (cache_name_pos(src, strlen(src)) >= 0) {
+                       path_list_insert(src, &deleted);
+
+                       /* destination can be a directory with 1 file inside */
+                       if (path_list_has_path(&overwritten, dst))
+                               path_list_insert(dst, &changed);
+                       else
+                               path_list_insert(dst, &added);
+               } else
+                       path_list_insert(dst, &added);
+       }
+
+       if (show_only) {
+               show_list("Changed  : ", &changed);
+               show_list("Adding   : ", &added);
+               show_list("Deleting : ", &deleted);
+       } else {
+               for (i = 0; i < changed.nr; i++) {
+                       const char *path = changed.items[i].path;
+                       int j = cache_name_pos(path, strlen(path));
+                       struct cache_entry *ce = active_cache[j];
+
+                       if (j < 0)
+                               die ("Huh? Cache entry for %s unknown?", path);
+                       refresh_cache_entry(ce, 0);
+               }
+
+               for (i = 0; i < added.nr; i++) {
+                       const char *path = added.items[i].path;
+                       add_file_to_cache(path, verbose);
+               }
+
+               for (i = 0; i < deleted.nr; i++)
+                       remove_file_from_cache(deleted.items[i].path);
+
+               if (active_cache_changed) {
+                       if (write_cache(newfd, active_cache, active_nr) ||
+                           close(newfd) ||
+                           commit_locked_index(&lock_file))
+                               die("Unable to write new index file");
+               }
+       }
+
+       return 0;
+}
diff --git a/builtin-name-rev.c b/builtin-name-rev.c
new file mode 100644 (file)
index 0000000..a0c89a8
--- /dev/null
@@ -0,0 +1,279 @@
+#include "builtin.h"
+#include "cache.h"
+#include "commit.h"
+#include "tag.h"
+#include "refs.h"
+#include "parse-options.h"
+
+#define CUTOFF_DATE_SLOP 86400 /* one day */
+
+typedef struct rev_name {
+       const char *tip_name;
+       int generation;
+       int distance;
+} rev_name;
+
+static long cutoff = LONG_MAX;
+
+/* How many generations are maximally preferred over _one_ merge traversal? */
+#define MERGE_TRAVERSAL_WEIGHT 65535
+
+static void name_rev(struct commit *commit,
+               const char *tip_name, int generation, int distance,
+               int deref)
+{
+       struct rev_name *name = (struct rev_name *)commit->util;
+       struct commit_list *parents;
+       int parent_number = 1;
+
+       if (!commit->object.parsed)
+               parse_commit(commit);
+
+       if (commit->date < cutoff)
+               return;
+
+       if (deref) {
+               char *new_name = xmalloc(strlen(tip_name)+3);
+               strcpy(new_name, tip_name);
+               strcat(new_name, "^0");
+               tip_name = new_name;
+
+               if (generation)
+                       die("generation: %d, but deref?", generation);
+       }
+
+       if (name == NULL) {
+               name = xmalloc(sizeof(rev_name));
+               commit->util = name;
+               goto copy_data;
+       } else if (name->distance > distance) {
+copy_data:
+               name->tip_name = tip_name;
+               name->generation = generation;
+               name->distance = distance;
+       } else
+               return;
+
+       for (parents = commit->parents;
+                       parents;
+                       parents = parents->next, parent_number++) {
+               if (parent_number > 1) {
+                       int len = strlen(tip_name);
+                       char *new_name = xmalloc(len +
+                               1 + decimal_length(generation) +  /* ~<n> */
+                               1 + 2 +                           /* ^NN */
+                               1);
+
+                       if (len > 2 && !strcmp(tip_name + len - 2, "^0"))
+                               len -= 2;
+                       if (generation > 0)
+                               sprintf(new_name, "%.*s~%d^%d", len, tip_name,
+                                               generation, parent_number);
+                       else
+                               sprintf(new_name, "%.*s^%d", len, tip_name,
+                                               parent_number);
+
+                       name_rev(parents->item, new_name, 0,
+                               distance + MERGE_TRAVERSAL_WEIGHT, 0);
+               } else {
+                       name_rev(parents->item, tip_name, generation + 1,
+                               distance + 1, 0);
+               }
+       }
+}
+
+struct name_ref_data {
+       int tags_only;
+       int name_only;
+       const char *ref_filter;
+};
+
+static int name_ref(const char *path, const unsigned char *sha1, int flags, void *cb_data)
+{
+       struct object *o = parse_object(sha1);
+       struct name_ref_data *data = cb_data;
+       int deref = 0;
+
+       if (data->tags_only && prefixcmp(path, "refs/tags/"))
+               return 0;
+
+       if (data->ref_filter && fnmatch(data->ref_filter, path, 0))
+               return 0;
+
+       while (o && o->type == OBJ_TAG) {
+               struct tag *t = (struct tag *) o;
+               if (!t->tagged)
+                       break; /* broken repository */
+               o = parse_object(t->tagged->sha1);
+               deref = 1;
+       }
+       if (o && o->type == OBJ_COMMIT) {
+               struct commit *commit = (struct commit *)o;
+
+               if (!prefixcmp(path, "refs/heads/"))
+                       path = path + 11;
+               else if (data->tags_only
+                   && data->name_only
+                   && !prefixcmp(path, "refs/tags/"))
+                       path = path + 10;
+               else if (!prefixcmp(path, "refs/"))
+                       path = path + 5;
+
+               name_rev(commit, xstrdup(path), 0, 0, deref);
+       }
+       return 0;
+}
+
+/* returns a static buffer */
+static const char* get_rev_name(struct object *o)
+{
+       static char buffer[1024];
+       struct rev_name *n;
+       struct commit *c;
+
+       if (o->type != OBJ_COMMIT)
+               return "undefined";
+       c = (struct commit *) o;
+       n = c->util;
+       if (!n)
+               return "undefined";
+
+       if (!n->generation)
+               return n->tip_name;
+       else {
+               int len = strlen(n->tip_name);
+               if (len > 2 && !strcmp(n->tip_name + len - 2, "^0"))
+                       len -= 2;
+               snprintf(buffer, sizeof(buffer), "%.*s~%d", len, n->tip_name,
+                               n->generation);
+
+               return buffer;
+       }
+}
+
+static char const * const name_rev_usage[] = {
+       "git-name-rev [options] ( --all | --stdin | <commit>... )",
+       NULL
+};
+
+int cmd_name_rev(int argc, const char **argv, const char *prefix)
+{
+       struct object_array revs = { 0, 0, NULL };
+       int all = 0, transform_stdin = 0;
+       struct name_ref_data data = { 0, 0, NULL };
+       struct option opts[] = {
+               OPT_BOOLEAN(0, "name-only", &data.name_only, "print only names (no SHA-1)"),
+               OPT_BOOLEAN(0, "tags", &data.tags_only, "only use tags to name the commits"),
+               OPT_STRING(0, "refs", &data.ref_filter, "pattern",
+                                  "only use refs matching <pattern>"),
+               OPT_GROUP(""),
+               OPT_BOOLEAN(0, "all", &all, "list all commits reachable from all refs"),
+               OPT_BOOLEAN(0, "stdin", &transform_stdin, "read from stdin"),
+               OPT_END(),
+       };
+
+       git_config(git_default_config);
+       argc = parse_options(argc, argv, opts, name_rev_usage, 0);
+       if (!!all + !!transform_stdin + !!argc > 1) {
+               error("Specify either a list, or --all, not both!");
+               usage_with_options(name_rev_usage, opts);
+       }
+       if (all || transform_stdin)
+               cutoff = 0;
+
+       for (; argc; argc--, argv++) {
+               unsigned char sha1[20];
+               struct object *o;
+               struct commit *commit;
+
+               if (get_sha1(*argv, sha1)) {
+                       fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
+                                       *argv);
+                       continue;
+               }
+
+               o = deref_tag(parse_object(sha1), *argv, 0);
+               if (!o || o->type != OBJ_COMMIT) {
+                       fprintf(stderr, "Could not get commit for %s. Skipping.\n",
+                                       *argv);
+                       continue;
+               }
+
+               commit = (struct commit *)o;
+               if (cutoff > commit->date)
+                       cutoff = commit->date;
+               add_object_array((struct object *)commit, *argv, &revs);
+       }
+
+       if (cutoff)
+               cutoff = cutoff - CUTOFF_DATE_SLOP;
+       for_each_ref(name_ref, &data);
+
+       if (transform_stdin) {
+               char buffer[2048];
+               char *p, *p_start;
+
+               while (!feof(stdin)) {
+                       int forty = 0;
+                       p = fgets(buffer, sizeof(buffer), stdin);
+                       if (!p)
+                               break;
+
+                       for (p_start = p; *p; p++) {
+#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
+                               if (!ishex(*p))
+                                       forty = 0;
+                               else if (++forty == 40 &&
+                                               !ishex(*(p+1))) {
+                                       unsigned char sha1[40];
+                                       const char *name = "undefined";
+                                       char c = *(p+1);
+
+                                       forty = 0;
+
+                                       *(p+1) = 0;
+                                       if (!get_sha1(p - 39, sha1)) {
+                                               struct object *o =
+                                                       lookup_object(sha1);
+                                               if (o)
+                                                       name = get_rev_name(o);
+                                       }
+                                       *(p+1) = c;
+
+                                       if (!strcmp(name, "undefined"))
+                                               continue;
+
+                                       fwrite(p_start, p - p_start + 1, 1,
+                                              stdout);
+                                       printf(" (%s)", name);
+                                       p_start = p + 1;
+                               }
+                       }
+
+                       /* flush */
+                       if (p_start != p)
+                               fwrite(p_start, p - p_start, 1, stdout);
+               }
+       } else if (all) {
+               int i, max;
+
+               max = get_max_object_index();
+               for (i = 0; i < max; i++) {
+                       struct object * obj = get_indexed_object(i);
+                       if (!obj)
+                               continue;
+                       if (!data.name_only)
+                               printf("%s ", sha1_to_hex(obj->sha1));
+                       printf("%s\n", get_rev_name(obj));
+               }
+       } else {
+               int i;
+               for (i = 0; i < revs.nr; i++) {
+                       if (!data.name_only)
+                               printf("%s ", revs.objects[i].name);
+                       printf("%s\n", get_rev_name(revs.objects[i].item));
+               }
+       }
+
+       return 0;
+}
diff --git a/builtin-pack-objects.c b/builtin-pack-objects.c
new file mode 100644 (file)
index 0000000..4f44658
--- /dev/null
@@ -0,0 +1,2156 @@
+#include "builtin.h"
+#include "cache.h"
+#include "attr.h"
+#include "object.h"
+#include "blob.h"
+#include "commit.h"
+#include "tag.h"
+#include "tree.h"
+#include "delta.h"
+#include "pack.h"
+#include "csum-file.h"
+#include "tree-walk.h"
+#include "diff.h"
+#include "revision.h"
+#include "list-objects.h"
+#include "progress.h"
+
+#ifdef THREADED_DELTA_SEARCH
+#include <pthread.h>
+#endif
+
+static const char pack_usage[] = "\
+git-pack-objects [{ -q | --progress | --all-progress }] \n\
+       [--max-pack-size=N] [--local] [--incremental] \n\
+       [--window=N] [--window-memory=N] [--depth=N] \n\
+       [--no-reuse-delta] [--no-reuse-object] [--delta-base-offset] \n\
+       [--threads=N] [--non-empty] [--revs [--unpacked | --all]*] [--reflog] \n\
+       [--stdout | base-name] [--keep-unreachable] [<ref-list | <object-list]";
+
+struct object_entry {
+       struct pack_idx_entry idx;
+       unsigned long size;     /* uncompressed size */
+       struct packed_git *in_pack;     /* already in pack */
+       off_t in_pack_offset;
+       struct object_entry *delta;     /* delta base object */
+       struct object_entry *delta_child; /* deltified objects who bases me */
+       struct object_entry *delta_sibling; /* other deltified objects who
+                                            * uses the same base as me
+                                            */
+       void *delta_data;       /* cached delta (uncompressed) */
+       unsigned long delta_size;       /* delta data size (uncompressed) */
+       unsigned int hash;      /* name hint hash */
+       enum object_type type;
+       enum object_type in_pack_type;  /* could be delta */
+       unsigned char in_pack_header_size;
+       unsigned char preferred_base; /* we do not pack this, but is available
+                                      * to be used as the base object to delta
+                                      * objects against.
+                                      */
+       unsigned char no_try_delta;
+};
+
+/*
+ * Objects we are going to pack are collected in objects array (dynamically
+ * expanded).  nr_objects & nr_alloc controls this array.  They are stored
+ * in the order we see -- typically rev-list --objects order that gives us
+ * nice "minimum seek" order.
+ */
+static struct object_entry *objects;
+static struct pack_idx_entry **written_list;
+static uint32_t nr_objects, nr_alloc, nr_result, nr_written;
+
+static int non_empty;
+static int no_reuse_delta, no_reuse_object, keep_unreachable;
+static int local;
+static int incremental;
+static int allow_ofs_delta;
+static const char *base_name;
+static int progress = 1;
+static int window = 10;
+static uint32_t pack_size_limit;
+static int depth = 50;
+static int delta_search_threads = 1;
+static int pack_to_stdout;
+static int num_preferred_base;
+static struct progress *progress_state;
+static int pack_compression_level = Z_DEFAULT_COMPRESSION;
+static int pack_compression_seen;
+
+static unsigned long delta_cache_size = 0;
+static unsigned long max_delta_cache_size = 0;
+static unsigned long cache_max_small_delta_size = 1000;
+
+static unsigned long window_memory_limit = 0;
+
+/*
+ * The object names in objects array are hashed with this hashtable,
+ * to help looking up the entry by object name.
+ * This hashtable is built after all the objects are seen.
+ */
+static int *object_ix;
+static int object_ix_hashsz;
+
+/*
+ * Pack index for existing packs give us easy access to the offsets into
+ * corresponding pack file where each object's data starts, but the entries
+ * do not store the size of the compressed representation (uncompressed
+ * size is easily available by examining the pack entry header).  It is
+ * also rather expensive to find the sha1 for an object given its offset.
+ *
+ * We build a hashtable of existing packs (pack_revindex), and keep reverse
+ * index here -- pack index file is sorted by object name mapping to offset;
+ * this pack_revindex[].revindex array is a list of offset/index_nr pairs
+ * ordered by offset, so if you know the offset of an object, next offset
+ * is where its packed representation ends and the index_nr can be used to
+ * get the object sha1 from the main index.
+ */
+struct revindex_entry {
+       off_t offset;
+       unsigned int nr;
+};
+struct pack_revindex {
+       struct packed_git *p;
+       struct revindex_entry *revindex;
+};
+static struct  pack_revindex *pack_revindex;
+static int pack_revindex_hashsz;
+
+/*
+ * stats
+ */
+static uint32_t written, written_delta;
+static uint32_t reused, reused_delta;
+
+static int pack_revindex_ix(struct packed_git *p)
+{
+       unsigned long ui = (unsigned long)p;
+       int i;
+
+       ui = ui ^ (ui >> 16); /* defeat structure alignment */
+       i = (int)(ui % pack_revindex_hashsz);
+       while (pack_revindex[i].p) {
+               if (pack_revindex[i].p == p)
+                       return i;
+               if (++i == pack_revindex_hashsz)
+                       i = 0;
+       }
+       return -1 - i;
+}
+
+static void prepare_pack_ix(void)
+{
+       int num;
+       struct packed_git *p;
+       for (num = 0, p = packed_git; p; p = p->next)
+               num++;
+       if (!num)
+               return;
+       pack_revindex_hashsz = num * 11;
+       pack_revindex = xcalloc(sizeof(*pack_revindex), pack_revindex_hashsz);
+       for (p = packed_git; p; p = p->next) {
+               num = pack_revindex_ix(p);
+               num = - 1 - num;
+               pack_revindex[num].p = p;
+       }
+       /* revindex elements are lazily initialized */
+}
+
+static int cmp_offset(const void *a_, const void *b_)
+{
+       const struct revindex_entry *a = a_;
+       const struct revindex_entry *b = b_;
+       return (a->offset < b->offset) ? -1 : (a->offset > b->offset) ? 1 : 0;
+}
+
+/*
+ * Ordered list of offsets of objects in the pack.
+ */
+static void prepare_pack_revindex(struct pack_revindex *rix)
+{
+       struct packed_git *p = rix->p;
+       int num_ent = p->num_objects;
+       int i;
+       const char *index = p->index_data;
+
+       rix->revindex = xmalloc(sizeof(*rix->revindex) * (num_ent + 1));
+       index += 4 * 256;
+
+       if (p->index_version > 1) {
+               const uint32_t *off_32 =
+                       (uint32_t *)(index + 8 + p->num_objects * (20 + 4));
+               const uint32_t *off_64 = off_32 + p->num_objects;
+               for (i = 0; i < num_ent; i++) {
+                       uint32_t off = ntohl(*off_32++);
+                       if (!(off & 0x80000000)) {
+                               rix->revindex[i].offset = off;
+                       } else {
+                               rix->revindex[i].offset =
+                                       ((uint64_t)ntohl(*off_64++)) << 32;
+                               rix->revindex[i].offset |=
+                                       ntohl(*off_64++);
+                       }
+                       rix->revindex[i].nr = i;
+               }
+       } else {
+               for (i = 0; i < num_ent; i++) {
+                       uint32_t hl = *((uint32_t *)(index + 24 * i));
+                       rix->revindex[i].offset = ntohl(hl);
+                       rix->revindex[i].nr = i;
+               }
+       }
+
+       /* This knows the pack format -- the 20-byte trailer
+        * follows immediately after the last object data.
+        */
+       rix->revindex[num_ent].offset = p->pack_size - 20;
+       rix->revindex[num_ent].nr = -1;
+       qsort(rix->revindex, num_ent, sizeof(*rix->revindex), cmp_offset);
+}
+
+static struct revindex_entry * find_packed_object(struct packed_git *p,
+                                                 off_t ofs)
+{
+       int num;
+       int lo, hi;
+       struct pack_revindex *rix;
+       struct revindex_entry *revindex;
+       num = pack_revindex_ix(p);
+       if (num < 0)
+               die("internal error: pack revindex uninitialized");
+       rix = &pack_revindex[num];
+       if (!rix->revindex)
+               prepare_pack_revindex(rix);
+       revindex = rix->revindex;
+       lo = 0;
+       hi = p->num_objects + 1;
+       do {
+               int mi = (lo + hi) / 2;
+               if (revindex[mi].offset == ofs) {
+                       return revindex + mi;
+               }
+               else if (ofs < revindex[mi].offset)
+                       hi = mi;
+               else
+                       lo = mi + 1;
+       } while (lo < hi);
+       die("internal error: pack revindex corrupt");
+}
+
+static const unsigned char *find_packed_object_name(struct packed_git *p,
+                                                   off_t ofs)
+{
+       struct revindex_entry *entry = find_packed_object(p, ofs);
+       return nth_packed_object_sha1(p, entry->nr);
+}
+
+static void *delta_against(void *buf, unsigned long size, struct object_entry *entry)
+{
+       unsigned long othersize, delta_size;
+       enum object_type type;
+       void *otherbuf = read_sha1_file(entry->delta->idx.sha1, &type, &othersize);
+       void *delta_buf;
+
+       if (!otherbuf)
+               die("unable to read %s", sha1_to_hex(entry->delta->idx.sha1));
+        delta_buf = diff_delta(otherbuf, othersize,
+                              buf, size, &delta_size, 0);
+        if (!delta_buf || delta_size != entry->delta_size)
+               die("delta size changed");
+        free(buf);
+        free(otherbuf);
+       return delta_buf;
+}
+
+/*
+ * The per-object header is a pretty dense thing, which is
+ *  - first byte: low four bits are "size", then three bits of "type",
+ *    and the high bit is "size continues".
+ *  - each byte afterwards: low seven bits are size continuation,
+ *    with the high bit being "size continues"
+ */
+static int encode_header(enum object_type type, unsigned long size, unsigned char *hdr)
+{
+       int n = 1;
+       unsigned char c;
+
+       if (type < OBJ_COMMIT || type > OBJ_REF_DELTA)
+               die("bad type %d", type);
+
+       c = (type << 4) | (size & 15);
+       size >>= 4;
+       while (size) {
+               *hdr++ = c | 0x80;
+               c = size & 0x7f;
+               size >>= 7;
+               n++;
+       }
+       *hdr = c;
+       return n;
+}
+
+/*
+ * we are going to reuse the existing object data as is.  make
+ * sure it is not corrupt.
+ */
+static int check_pack_inflate(struct packed_git *p,
+               struct pack_window **w_curs,
+               off_t offset,
+               off_t len,
+               unsigned long expect)
+{
+       z_stream stream;
+       unsigned char fakebuf[4096], *in;
+       int st;
+
+       memset(&stream, 0, sizeof(stream));
+       inflateInit(&stream);
+       do {
+               in = use_pack(p, w_curs, offset, &stream.avail_in);
+               stream.next_in = in;
+               stream.next_out = fakebuf;
+               stream.avail_out = sizeof(fakebuf);
+               st = inflate(&stream, Z_FINISH);
+               offset += stream.next_in - in;
+       } while (st == Z_OK || st == Z_BUF_ERROR);
+       inflateEnd(&stream);
+       return (st == Z_STREAM_END &&
+               stream.total_out == expect &&
+               stream.total_in == len) ? 0 : -1;
+}
+
+static int check_pack_crc(struct packed_git *p, struct pack_window **w_curs,
+                         off_t offset, off_t len, unsigned int nr)
+{
+       const uint32_t *index_crc;
+       uint32_t data_crc = crc32(0, Z_NULL, 0);
+
+       do {
+               unsigned int avail;
+               void *data = use_pack(p, w_curs, offset, &avail);
+               if (avail > len)
+                       avail = len;
+               data_crc = crc32(data_crc, data, avail);
+               offset += avail;
+               len -= avail;
+       } while (len);
+
+       index_crc = p->index_data;
+       index_crc += 2 + 256 + p->num_objects * (20/4) + nr;
+
+       return data_crc != ntohl(*index_crc);
+}
+
+static void copy_pack_data(struct sha1file *f,
+               struct packed_git *p,
+               struct pack_window **w_curs,
+               off_t offset,
+               off_t len)
+{
+       unsigned char *in;
+       unsigned int avail;
+
+       while (len) {
+               in = use_pack(p, w_curs, offset, &avail);
+               if (avail > len)
+                       avail = (unsigned int)len;
+               sha1write(f, in, avail);
+               offset += avail;
+               len -= avail;
+       }
+}
+
+static unsigned long write_object(struct sha1file *f,
+                                 struct object_entry *entry,
+                                 off_t write_offset)
+{
+       unsigned long size;
+       enum object_type type;
+       void *buf;
+       unsigned char header[10];
+       unsigned char dheader[10];
+       unsigned hdrlen;
+       off_t datalen;
+       enum object_type obj_type;
+       int to_reuse = 0;
+       /* write limit if limited packsize and not first object */
+       unsigned long limit = pack_size_limit && nr_written ?
+                               pack_size_limit - write_offset : 0;
+                               /* no if no delta */
+       int usable_delta =      !entry->delta ? 0 :
+                               /* yes if unlimited packfile */
+                               !pack_size_limit ? 1 :
+                               /* no if base written to previous pack */
+                               entry->delta->idx.offset == (off_t)-1 ? 0 :
+                               /* otherwise double-check written to this
+                                * pack,  like we do below
+                                */
+                               entry->delta->idx.offset ? 1 : 0;
+
+       if (!pack_to_stdout)
+               crc32_begin(f);
+
+       obj_type = entry->type;
+       if (no_reuse_object)
+               to_reuse = 0;   /* explicit */
+       else if (!entry->in_pack)
+               to_reuse = 0;   /* can't reuse what we don't have */
+       else if (obj_type == OBJ_REF_DELTA || obj_type == OBJ_OFS_DELTA)
+                               /* check_object() decided it for us ... */
+               to_reuse = usable_delta;
+                               /* ... but pack split may override that */
+       else if (obj_type != entry->in_pack_type)
+               to_reuse = 0;   /* pack has delta which is unusable */
+       else if (entry->delta)
+               to_reuse = 0;   /* we want to pack afresh */
+       else
+               to_reuse = 1;   /* we have it in-pack undeltified,
+                                * and we do not need to deltify it.
+                                */
+
+       if (!to_reuse) {
+               z_stream stream;
+               unsigned long maxsize;
+               void *out;
+               if (!usable_delta) {
+                       buf = read_sha1_file(entry->idx.sha1, &obj_type, &size);
+                       if (!buf)
+                               die("unable to read %s", sha1_to_hex(entry->idx.sha1));
+               } else if (entry->delta_data) {
+                       size = entry->delta_size;
+                       buf = entry->delta_data;
+                       entry->delta_data = NULL;
+                       obj_type = (allow_ofs_delta && entry->delta->idx.offset) ?
+                               OBJ_OFS_DELTA : OBJ_REF_DELTA;
+               } else {
+                       buf = read_sha1_file(entry->idx.sha1, &type, &size);
+                       if (!buf)
+                               die("unable to read %s", sha1_to_hex(entry->idx.sha1));
+                       buf = delta_against(buf, size, entry);
+                       size = entry->delta_size;
+                       obj_type = (allow_ofs_delta && entry->delta->idx.offset) ?
+                               OBJ_OFS_DELTA : OBJ_REF_DELTA;
+               }
+               /* compress the data to store and put compressed length in datalen */
+               memset(&stream, 0, sizeof(stream));
+               deflateInit(&stream, pack_compression_level);
+               maxsize = deflateBound(&stream, size);
+               out = xmalloc(maxsize);
+               /* Compress it */
+               stream.next_in = buf;
+               stream.avail_in = size;
+               stream.next_out = out;
+               stream.avail_out = maxsize;
+               while (deflate(&stream, Z_FINISH) == Z_OK)
+                       /* nothing */;
+               deflateEnd(&stream);
+               datalen = stream.total_out;
+               deflateEnd(&stream);
+               /*
+                * The object header is a byte of 'type' followed by zero or
+                * more bytes of length.
+                */
+               hdrlen = encode_header(obj_type, size, header);
+
+               if (obj_type == OBJ_OFS_DELTA) {
+                       /*
+                        * Deltas with relative base contain an additional
+                        * encoding of the relative offset for the delta
+                        * base from this object's position in the pack.
+                        */
+                       off_t ofs = entry->idx.offset - entry->delta->idx.offset;
+                       unsigned pos = sizeof(dheader) - 1;
+                       dheader[pos] = ofs & 127;
+                       while (ofs >>= 7)
+                               dheader[--pos] = 128 | (--ofs & 127);
+                       if (limit && hdrlen + sizeof(dheader) - pos + datalen + 20 >= limit) {
+                               free(out);
+                               free(buf);
+                               return 0;
+                       }
+                       sha1write(f, header, hdrlen);
+                       sha1write(f, dheader + pos, sizeof(dheader) - pos);
+                       hdrlen += sizeof(dheader) - pos;
+               } else if (obj_type == OBJ_REF_DELTA) {
+                       /*
+                        * Deltas with a base reference contain
+                        * an additional 20 bytes for the base sha1.
+                        */
+                       if (limit && hdrlen + 20 + datalen + 20 >= limit) {
+                               free(out);
+                               free(buf);
+                               return 0;
+                       }
+                       sha1write(f, header, hdrlen);
+                       sha1write(f, entry->delta->idx.sha1, 20);
+                       hdrlen += 20;
+               } else {
+                       if (limit && hdrlen + datalen + 20 >= limit) {
+                               free(out);
+                               free(buf);
+                               return 0;
+                       }
+                       sha1write(f, header, hdrlen);
+               }
+               sha1write(f, out, datalen);
+               free(out);
+               free(buf);
+       }
+       else {
+               struct packed_git *p = entry->in_pack;
+               struct pack_window *w_curs = NULL;
+               struct revindex_entry *revidx;
+               off_t offset;
+
+               if (entry->delta) {
+                       obj_type = (allow_ofs_delta && entry->delta->idx.offset) ?
+                               OBJ_OFS_DELTA : OBJ_REF_DELTA;
+                       reused_delta++;
+               }
+               hdrlen = encode_header(obj_type, entry->size, header);
+               offset = entry->in_pack_offset;
+               revidx = find_packed_object(p, offset);
+               datalen = revidx[1].offset - offset;
+               if (!pack_to_stdout && p->index_version > 1 &&
+                   check_pack_crc(p, &w_curs, offset, datalen, revidx->nr))
+                       die("bad packed object CRC for %s", sha1_to_hex(entry->idx.sha1));
+               offset += entry->in_pack_header_size;
+               datalen -= entry->in_pack_header_size;
+               if (obj_type == OBJ_OFS_DELTA) {
+                       off_t ofs = entry->idx.offset - entry->delta->idx.offset;
+                       unsigned pos = sizeof(dheader) - 1;
+                       dheader[pos] = ofs & 127;
+                       while (ofs >>= 7)
+                               dheader[--pos] = 128 | (--ofs & 127);
+                       if (limit && hdrlen + sizeof(dheader) - pos + datalen + 20 >= limit)
+                               return 0;
+                       sha1write(f, header, hdrlen);
+                       sha1write(f, dheader + pos, sizeof(dheader) - pos);
+                       hdrlen += sizeof(dheader) - pos;
+               } else if (obj_type == OBJ_REF_DELTA) {
+                       if (limit && hdrlen + 20 + datalen + 20 >= limit)
+                               return 0;
+                       sha1write(f, header, hdrlen);
+                       sha1write(f, entry->delta->idx.sha1, 20);
+                       hdrlen += 20;
+               } else {
+                       if (limit && hdrlen + datalen + 20 >= limit)
+                               return 0;
+                       sha1write(f, header, hdrlen);
+               }
+
+               if (!pack_to_stdout && p->index_version == 1 &&
+                   check_pack_inflate(p, &w_curs, offset, datalen, entry->size))
+                       die("corrupt packed object for %s", sha1_to_hex(entry->idx.sha1));
+               copy_pack_data(f, p, &w_curs, offset, datalen);
+               unuse_pack(&w_curs);
+               reused++;
+       }
+       if (usable_delta)
+               written_delta++;
+       written++;
+       if (!pack_to_stdout)
+               entry->idx.crc32 = crc32_end(f);
+       return hdrlen + datalen;
+}
+
+static off_t write_one(struct sha1file *f,
+                              struct object_entry *e,
+                              off_t offset)
+{
+       unsigned long size;
+
+       /* offset is non zero if object is written already. */
+       if (e->idx.offset || e->preferred_base)
+               return offset;
+
+       /* if we are deltified, write out base object first. */
+       if (e->delta) {
+               offset = write_one(f, e->delta, offset);
+               if (!offset)
+                       return 0;
+       }
+
+       e->idx.offset = offset;
+       size = write_object(f, e, offset);
+       if (!size) {
+               e->idx.offset = 0;
+               return 0;
+       }
+       written_list[nr_written++] = &e->idx;
+
+       /* make sure off_t is sufficiently large not to wrap */
+       if (offset > offset + size)
+               die("pack too large for current definition of off_t");
+       return offset + size;
+}
+
+/* forward declaration for write_pack_file */
+static int adjust_perm(const char *path, mode_t mode);
+
+static void write_pack_file(void)
+{
+       uint32_t i = 0, j;
+       struct sha1file *f;
+       off_t offset, offset_one, last_obj_offset = 0;
+       struct pack_header hdr;
+       int do_progress = progress >> pack_to_stdout;
+       uint32_t nr_remaining = nr_result;
+
+       if (do_progress)
+               progress_state = start_progress("Writing objects", nr_result);
+       written_list = xmalloc(nr_objects * sizeof(*written_list));
+
+       do {
+               unsigned char sha1[20];
+               char *pack_tmp_name = NULL;
+
+               if (pack_to_stdout) {
+                       f = sha1fd_throughput(1, "<stdout>", progress_state);
+               } else {
+                       char tmpname[PATH_MAX];
+                       int fd;
+                       snprintf(tmpname, sizeof(tmpname),
+                                "%s/tmp_pack_XXXXXX", get_object_directory());
+                       fd = xmkstemp(tmpname);
+                       pack_tmp_name = xstrdup(tmpname);
+                       f = sha1fd(fd, pack_tmp_name);
+               }
+
+               hdr.hdr_signature = htonl(PACK_SIGNATURE);
+               hdr.hdr_version = htonl(PACK_VERSION);
+               hdr.hdr_entries = htonl(nr_remaining);
+               sha1write(f, &hdr, sizeof(hdr));
+               offset = sizeof(hdr);
+               nr_written = 0;
+               for (; i < nr_objects; i++) {
+                       last_obj_offset = offset;
+                       offset_one = write_one(f, objects + i, offset);
+                       if (!offset_one)
+                               break;
+                       offset = offset_one;
+                       display_progress(progress_state, written);
+               }
+
+               /*
+                * Did we write the wrong # entries in the header?
+                * If so, rewrite it like in fast-import
+                */
+               if (pack_to_stdout || nr_written == nr_remaining) {
+                       sha1close(f, sha1, 1);
+               } else {
+                       int fd = sha1close(f, NULL, 0);
+                       fixup_pack_header_footer(fd, sha1, pack_tmp_name, nr_written);
+                       close(fd);
+               }
+
+               if (!pack_to_stdout) {
+                       mode_t mode = umask(0);
+                       char *idx_tmp_name, tmpname[PATH_MAX];
+
+                       umask(mode);
+                       mode = 0444 & ~mode;
+
+                       idx_tmp_name = write_idx_file(NULL, written_list,
+                                                     nr_written, sha1);
+                       snprintf(tmpname, sizeof(tmpname), "%s-%s.pack",
+                                base_name, sha1_to_hex(sha1));
+                       if (adjust_perm(pack_tmp_name, mode))
+                               die("unable to make temporary pack file readable: %s",
+                                   strerror(errno));
+                       if (rename(pack_tmp_name, tmpname))
+                               die("unable to rename temporary pack file: %s",
+                                   strerror(errno));
+                       snprintf(tmpname, sizeof(tmpname), "%s-%s.idx",
+                                base_name, sha1_to_hex(sha1));
+                       if (adjust_perm(idx_tmp_name, mode))
+                               die("unable to make temporary index file readable: %s",
+                                   strerror(errno));
+                       if (rename(idx_tmp_name, tmpname))
+                               die("unable to rename temporary index file: %s",
+                                   strerror(errno));
+                       free(idx_tmp_name);
+                       free(pack_tmp_name);
+                       puts(sha1_to_hex(sha1));
+               }
+
+               /* mark written objects as written to previous pack */
+               for (j = 0; j < nr_written; j++) {
+                       written_list[j]->offset = (off_t)-1;
+               }
+               nr_remaining -= nr_written;
+       } while (nr_remaining && i < nr_objects);
+
+       free(written_list);
+       stop_progress(&progress_state);
+       if (written != nr_result)
+               die("wrote %u objects while expecting %u", written, nr_result);
+       /*
+        * We have scanned through [0 ... i).  Since we have written
+        * the correct number of objects,  the remaining [i ... nr_objects)
+        * items must be either already written (due to out-of-order delta base)
+        * or a preferred base.  Count those which are neither and complain if any.
+        */
+       for (j = 0; i < nr_objects; i++) {
+               struct object_entry *e = objects + i;
+               j += !e->idx.offset && !e->preferred_base;
+       }
+       if (j)
+               die("wrote %u objects as expected but %u unwritten", written, j);
+}
+
+static int locate_object_entry_hash(const unsigned char *sha1)
+{
+       int i;
+       unsigned int ui;
+       memcpy(&ui, sha1, sizeof(unsigned int));
+       i = ui % object_ix_hashsz;
+       while (0 < object_ix[i]) {
+               if (!hashcmp(sha1, objects[object_ix[i] - 1].idx.sha1))
+                       return i;
+               if (++i == object_ix_hashsz)
+                       i = 0;
+       }
+       return -1 - i;
+}
+
+static struct object_entry *locate_object_entry(const unsigned char *sha1)
+{
+       int i;
+
+       if (!object_ix_hashsz)
+               return NULL;
+
+       i = locate_object_entry_hash(sha1);
+       if (0 <= i)
+               return &objects[object_ix[i]-1];
+       return NULL;
+}
+
+static void rehash_objects(void)
+{
+       uint32_t i;
+       struct object_entry *oe;
+
+       object_ix_hashsz = nr_objects * 3;
+       if (object_ix_hashsz < 1024)
+               object_ix_hashsz = 1024;
+       object_ix = xrealloc(object_ix, sizeof(int) * object_ix_hashsz);
+       memset(object_ix, 0, sizeof(int) * object_ix_hashsz);
+       for (i = 0, oe = objects; i < nr_objects; i++, oe++) {
+               int ix = locate_object_entry_hash(oe->idx.sha1);
+               if (0 <= ix)
+                       continue;
+               ix = -1 - ix;
+               object_ix[ix] = i + 1;
+       }
+}
+
+static unsigned name_hash(const char *name)
+{
+       unsigned char c;
+       unsigned hash = 0;
+
+       if (!name)
+               return 0;
+
+       /*
+        * This effectively just creates a sortable number from the
+        * last sixteen non-whitespace characters. Last characters
+        * count "most", so things that end in ".c" sort together.
+        */
+       while ((c = *name++) != 0) {
+               if (isspace(c))
+                       continue;
+               hash = (hash >> 2) + (c << 24);
+       }
+       return hash;
+}
+
+static void setup_delta_attr_check(struct git_attr_check *check)
+{
+       static struct git_attr *attr_delta;
+
+       if (!attr_delta)
+               attr_delta = git_attr("delta", 5);
+
+       check[0].attr = attr_delta;
+}
+
+static int no_try_delta(const char *path)
+{
+       struct git_attr_check check[1];
+
+       setup_delta_attr_check(check);
+       if (git_checkattr(path, ARRAY_SIZE(check), check))
+               return 0;
+       if (ATTR_FALSE(check->value))
+               return 1;
+       return 0;
+}
+
+static int add_object_entry(const unsigned char *sha1, enum object_type type,
+                           const char *name, int exclude)
+{
+       struct object_entry *entry;
+       struct packed_git *p, *found_pack = NULL;
+       off_t found_offset = 0;
+       int ix;
+       unsigned hash = name_hash(name);
+
+       ix = nr_objects ? locate_object_entry_hash(sha1) : -1;
+       if (ix >= 0) {
+               if (exclude) {
+                       entry = objects + object_ix[ix] - 1;
+                       if (!entry->preferred_base)
+                               nr_result--;
+                       entry->preferred_base = 1;
+               }
+               return 0;
+       }
+
+       for (p = packed_git; p; p = p->next) {
+               off_t offset = find_pack_entry_one(sha1, p);
+               if (offset) {
+                       if (!found_pack) {
+                               found_offset = offset;
+                               found_pack = p;
+                       }
+                       if (exclude)
+                               break;
+                       if (incremental)
+                               return 0;
+                       if (local && !p->pack_local)
+                               return 0;
+               }
+       }
+
+       if (nr_objects >= nr_alloc) {
+               nr_alloc = (nr_alloc  + 1024) * 3 / 2;
+               objects = xrealloc(objects, nr_alloc * sizeof(*entry));
+       }
+
+       entry = objects + nr_objects++;
+       memset(entry, 0, sizeof(*entry));
+       hashcpy(entry->idx.sha1, sha1);
+       entry->hash = hash;
+       if (type)
+               entry->type = type;
+       if (exclude)
+               entry->preferred_base = 1;
+       else
+               nr_result++;
+       if (found_pack) {
+               entry->in_pack = found_pack;
+               entry->in_pack_offset = found_offset;
+       }
+
+       if (object_ix_hashsz * 3 <= nr_objects * 4)
+               rehash_objects();
+       else
+               object_ix[-1 - ix] = nr_objects;
+
+       display_progress(progress_state, nr_objects);
+
+       if (name && no_try_delta(name))
+               entry->no_try_delta = 1;
+
+       return 1;
+}
+
+struct pbase_tree_cache {
+       unsigned char sha1[20];
+       int ref;
+       int temporary;
+       void *tree_data;
+       unsigned long tree_size;
+};
+
+static struct pbase_tree_cache *(pbase_tree_cache[256]);
+static int pbase_tree_cache_ix(const unsigned char *sha1)
+{
+       return sha1[0] % ARRAY_SIZE(pbase_tree_cache);
+}
+static int pbase_tree_cache_ix_incr(int ix)
+{
+       return (ix+1) % ARRAY_SIZE(pbase_tree_cache);
+}
+
+static struct pbase_tree {
+       struct pbase_tree *next;
+       /* This is a phony "cache" entry; we are not
+        * going to evict it nor find it through _get()
+        * mechanism -- this is for the toplevel node that
+        * would almost always change with any commit.
+        */
+       struct pbase_tree_cache pcache;
+} *pbase_tree;
+
+static struct pbase_tree_cache *pbase_tree_get(const unsigned char *sha1)
+{
+       struct pbase_tree_cache *ent, *nent;
+       void *data;
+       unsigned long size;
+       enum object_type type;
+       int neigh;
+       int my_ix = pbase_tree_cache_ix(sha1);
+       int available_ix = -1;
+
+       /* pbase-tree-cache acts as a limited hashtable.
+        * your object will be found at your index or within a few
+        * slots after that slot if it is cached.
+        */
+       for (neigh = 0; neigh < 8; neigh++) {
+               ent = pbase_tree_cache[my_ix];
+               if (ent && !hashcmp(ent->sha1, sha1)) {
+                       ent->ref++;
+                       return ent;
+               }
+               else if (((available_ix < 0) && (!ent || !ent->ref)) ||
+                        ((0 <= available_ix) &&
+                         (!ent && pbase_tree_cache[available_ix])))
+                       available_ix = my_ix;
+               if (!ent)
+                       break;
+               my_ix = pbase_tree_cache_ix_incr(my_ix);
+       }
+
+       /* Did not find one.  Either we got a bogus request or
+        * we need to read and perhaps cache.
+        */
+       data = read_sha1_file(sha1, &type, &size);
+       if (!data)
+               return NULL;
+       if (type != OBJ_TREE) {
+               free(data);
+               return NULL;
+       }
+
+       /* We need to either cache or return a throwaway copy */
+
+       if (available_ix < 0)
+               ent = NULL;
+       else {
+               ent = pbase_tree_cache[available_ix];
+               my_ix = available_ix;
+       }
+
+       if (!ent) {
+               nent = xmalloc(sizeof(*nent));
+               nent->temporary = (available_ix < 0);
+       }
+       else {
+               /* evict and reuse */
+               free(ent->tree_data);
+               nent = ent;
+       }
+       hashcpy(nent->sha1, sha1);
+       nent->tree_data = data;
+       nent->tree_size = size;
+       nent->ref = 1;
+       if (!nent->temporary)
+               pbase_tree_cache[my_ix] = nent;
+       return nent;
+}
+
+static void pbase_tree_put(struct pbase_tree_cache *cache)
+{
+       if (!cache->temporary) {
+               cache->ref--;
+               return;
+       }
+       free(cache->tree_data);
+       free(cache);
+}
+
+static int name_cmp_len(const char *name)
+{
+       int i;
+       for (i = 0; name[i] && name[i] != '\n' && name[i] != '/'; i++)
+               ;
+       return i;
+}
+
+static void add_pbase_object(struct tree_desc *tree,
+                            const char *name,
+                            int cmplen,
+                            const char *fullname)
+{
+       struct name_entry entry;
+       int cmp;
+
+       while (tree_entry(tree,&entry)) {
+               if (S_ISGITLINK(entry.mode))
+                       continue;
+               cmp = tree_entry_len(entry.path, entry.sha1) != cmplen ? 1 :
+                     memcmp(name, entry.path, cmplen);
+               if (cmp > 0)
+                       continue;
+               if (cmp < 0)
+                       return;
+               if (name[cmplen] != '/') {
+                       add_object_entry(entry.sha1,
+                                        object_type(entry.mode),
+                                        fullname, 1);
+                       return;
+               }
+               if (S_ISDIR(entry.mode)) {
+                       struct tree_desc sub;
+                       struct pbase_tree_cache *tree;
+                       const char *down = name+cmplen+1;
+                       int downlen = name_cmp_len(down);
+
+                       tree = pbase_tree_get(entry.sha1);
+                       if (!tree)
+                               return;
+                       init_tree_desc(&sub, tree->tree_data, tree->tree_size);
+
+                       add_pbase_object(&sub, down, downlen, fullname);
+                       pbase_tree_put(tree);
+               }
+       }
+}
+
+static unsigned *done_pbase_paths;
+static int done_pbase_paths_num;
+static int done_pbase_paths_alloc;
+static int done_pbase_path_pos(unsigned hash)
+{
+       int lo = 0;
+       int hi = done_pbase_paths_num;
+       while (lo < hi) {
+               int mi = (hi + lo) / 2;
+               if (done_pbase_paths[mi] == hash)
+                       return mi;
+               if (done_pbase_paths[mi] < hash)
+                       hi = mi;
+               else
+                       lo = mi + 1;
+       }
+       return -lo-1;
+}
+
+static int check_pbase_path(unsigned hash)
+{
+       int pos = (!done_pbase_paths) ? -1 : done_pbase_path_pos(hash);
+       if (0 <= pos)
+               return 1;
+       pos = -pos - 1;
+       if (done_pbase_paths_alloc <= done_pbase_paths_num) {
+               done_pbase_paths_alloc = alloc_nr(done_pbase_paths_alloc);
+               done_pbase_paths = xrealloc(done_pbase_paths,
+                                           done_pbase_paths_alloc *
+                                           sizeof(unsigned));
+       }
+       done_pbase_paths_num++;
+       if (pos < done_pbase_paths_num)
+               memmove(done_pbase_paths + pos + 1,
+                       done_pbase_paths + pos,
+                       (done_pbase_paths_num - pos - 1) * sizeof(unsigned));
+       done_pbase_paths[pos] = hash;
+       return 0;
+}
+
+static void add_preferred_base_object(const char *name)
+{
+       struct pbase_tree *it;
+       int cmplen;
+       unsigned hash = name_hash(name);
+
+       if (!num_preferred_base || check_pbase_path(hash))
+               return;
+
+       cmplen = name_cmp_len(name);
+       for (it = pbase_tree; it; it = it->next) {
+               if (cmplen == 0) {
+                       add_object_entry(it->pcache.sha1, OBJ_TREE, NULL, 1);
+               }
+               else {
+                       struct tree_desc tree;
+                       init_tree_desc(&tree, it->pcache.tree_data, it->pcache.tree_size);
+                       add_pbase_object(&tree, name, cmplen, name);
+               }
+       }
+}
+
+static void add_preferred_base(unsigned char *sha1)
+{
+       struct pbase_tree *it;
+       void *data;
+       unsigned long size;
+       unsigned char tree_sha1[20];
+
+       if (window <= num_preferred_base++)
+               return;
+
+       data = read_object_with_reference(sha1, tree_type, &size, tree_sha1);
+       if (!data)
+               return;
+
+       for (it = pbase_tree; it; it = it->next) {
+               if (!hashcmp(it->pcache.sha1, tree_sha1)) {
+                       free(data);
+                       return;
+               }
+       }
+
+       it = xcalloc(1, sizeof(*it));
+       it->next = pbase_tree;
+       pbase_tree = it;
+
+       hashcpy(it->pcache.sha1, tree_sha1);
+       it->pcache.tree_data = data;
+       it->pcache.tree_size = size;
+}
+
+static void check_object(struct object_entry *entry)
+{
+       if (entry->in_pack) {
+               struct packed_git *p = entry->in_pack;
+               struct pack_window *w_curs = NULL;
+               const unsigned char *base_ref = NULL;
+               struct object_entry *base_entry;
+               unsigned long used, used_0;
+               unsigned int avail;
+               off_t ofs;
+               unsigned char *buf, c;
+
+               buf = use_pack(p, &w_curs, entry->in_pack_offset, &avail);
+
+               /*
+                * We want in_pack_type even if we do not reuse delta
+                * since non-delta representations could still be reused.
+                */
+               used = unpack_object_header_gently(buf, avail,
+                                                  &entry->in_pack_type,
+                                                  &entry->size);
+
+               /*
+                * Determine if this is a delta and if so whether we can
+                * reuse it or not.  Otherwise let's find out as cheaply as
+                * possible what the actual type and size for this object is.
+                */
+               switch (entry->in_pack_type) {
+               default:
+                       /* Not a delta hence we've already got all we need. */
+                       entry->type = entry->in_pack_type;
+                       entry->in_pack_header_size = used;
+                       unuse_pack(&w_curs);
+                       return;
+               case OBJ_REF_DELTA:
+                       if (!no_reuse_delta && !entry->preferred_base)
+                               base_ref = use_pack(p, &w_curs,
+                                               entry->in_pack_offset + used, NULL);
+                       entry->in_pack_header_size = used + 20;
+                       break;
+               case OBJ_OFS_DELTA:
+                       buf = use_pack(p, &w_curs,
+                                      entry->in_pack_offset + used, NULL);
+                       used_0 = 0;
+                       c = buf[used_0++];
+                       ofs = c & 127;
+                       while (c & 128) {
+                               ofs += 1;
+                               if (!ofs || MSB(ofs, 7))
+                                       die("delta base offset overflow in pack for %s",
+                                           sha1_to_hex(entry->idx.sha1));
+                               c = buf[used_0++];
+                               ofs = (ofs << 7) + (c & 127);
+                       }
+                       if (ofs >= entry->in_pack_offset)
+                               die("delta base offset out of bound for %s",
+                                   sha1_to_hex(entry->idx.sha1));
+                       ofs = entry->in_pack_offset - ofs;
+                       if (!no_reuse_delta && !entry->preferred_base)
+                               base_ref = find_packed_object_name(p, ofs);
+                       entry->in_pack_header_size = used + used_0;
+                       break;
+               }
+
+               if (base_ref && (base_entry = locate_object_entry(base_ref))) {
+                       /*
+                        * If base_ref was set above that means we wish to
+                        * reuse delta data, and we even found that base
+                        * in the list of objects we want to pack. Goodie!
+                        *
+                        * Depth value does not matter - find_deltas() will
+                        * never consider reused delta as the base object to
+                        * deltify other objects against, in order to avoid
+                        * circular deltas.
+                        */
+                       entry->type = entry->in_pack_type;
+                       entry->delta = base_entry;
+                       entry->delta_sibling = base_entry->delta_child;
+                       base_entry->delta_child = entry;
+                       unuse_pack(&w_curs);
+                       return;
+               }
+
+               if (entry->type) {
+                       /*
+                        * This must be a delta and we already know what the
+                        * final object type is.  Let's extract the actual
+                        * object size from the delta header.
+                        */
+                       entry->size = get_size_from_delta(p, &w_curs,
+                                       entry->in_pack_offset + entry->in_pack_header_size);
+                       unuse_pack(&w_curs);
+                       return;
+               }
+
+               /*
+                * No choice but to fall back to the recursive delta walk
+                * with sha1_object_info() to find about the object type
+                * at this point...
+                */
+               unuse_pack(&w_curs);
+       }
+
+       entry->type = sha1_object_info(entry->idx.sha1, &entry->size);
+       if (entry->type < 0)
+               die("unable to get type of object %s",
+                   sha1_to_hex(entry->idx.sha1));
+}
+
+static int pack_offset_sort(const void *_a, const void *_b)
+{
+       const struct object_entry *a = *(struct object_entry **)_a;
+       const struct object_entry *b = *(struct object_entry **)_b;
+
+       /* avoid filesystem trashing with loose objects */
+       if (!a->in_pack && !b->in_pack)
+               return hashcmp(a->idx.sha1, b->idx.sha1);
+
+       if (a->in_pack < b->in_pack)
+               return -1;
+       if (a->in_pack > b->in_pack)
+               return 1;
+       return a->in_pack_offset < b->in_pack_offset ? -1 :
+                       (a->in_pack_offset > b->in_pack_offset);
+}
+
+static void get_object_details(void)
+{
+       uint32_t i;
+       struct object_entry **sorted_by_offset;
+
+       sorted_by_offset = xcalloc(nr_objects, sizeof(struct object_entry *));
+       for (i = 0; i < nr_objects; i++)
+               sorted_by_offset[i] = objects + i;
+       qsort(sorted_by_offset, nr_objects, sizeof(*sorted_by_offset), pack_offset_sort);
+
+       prepare_pack_ix();
+       for (i = 0; i < nr_objects; i++)
+               check_object(sorted_by_offset[i]);
+       free(sorted_by_offset);
+}
+
+static int type_size_sort(const void *_a, const void *_b)
+{
+       const struct object_entry *a = *(struct object_entry **)_a;
+       const struct object_entry *b = *(struct object_entry **)_b;
+
+       if (a->type < b->type)
+               return -1;
+       if (a->type > b->type)
+               return 1;
+       if (a->hash < b->hash)
+               return -1;
+       if (a->hash > b->hash)
+               return 1;
+       if (a->preferred_base < b->preferred_base)
+               return -1;
+       if (a->preferred_base > b->preferred_base)
+               return 1;
+       if (a->size < b->size)
+               return -1;
+       if (a->size > b->size)
+               return 1;
+       return a > b ? -1 : (a < b);  /* newest last */
+}
+
+struct unpacked {
+       struct object_entry *entry;
+       void *data;
+       struct delta_index *index;
+       unsigned depth;
+};
+
+static int delta_cacheable(unsigned long src_size, unsigned long trg_size,
+                          unsigned long delta_size)
+{
+       if (max_delta_cache_size && delta_cache_size + delta_size > max_delta_cache_size)
+               return 0;
+
+       if (delta_size < cache_max_small_delta_size)
+               return 1;
+
+       /* cache delta, if objects are large enough compared to delta size */
+       if ((src_size >> 20) + (trg_size >> 21) > (delta_size >> 10))
+               return 1;
+
+       return 0;
+}
+
+#ifdef THREADED_DELTA_SEARCH
+
+static pthread_mutex_t read_mutex = PTHREAD_MUTEX_INITIALIZER;
+#define read_lock()            pthread_mutex_lock(&read_mutex)
+#define read_unlock()          pthread_mutex_unlock(&read_mutex)
+
+static pthread_mutex_t cache_mutex = PTHREAD_MUTEX_INITIALIZER;
+#define cache_lock()           pthread_mutex_lock(&cache_mutex)
+#define cache_unlock()         pthread_mutex_unlock(&cache_mutex)
+
+static pthread_mutex_t progress_mutex = PTHREAD_MUTEX_INITIALIZER;
+#define progress_lock()                pthread_mutex_lock(&progress_mutex)
+#define progress_unlock()      pthread_mutex_unlock(&progress_mutex)
+
+#else
+
+#define read_lock()            (void)0
+#define read_unlock()          (void)0
+#define cache_lock()           (void)0
+#define cache_unlock()         (void)0
+#define progress_lock()                (void)0
+#define progress_unlock()      (void)0
+
+#endif
+
+/*
+ * We search for deltas _backwards_ in a list sorted by type and
+ * by size, so that we see progressively smaller and smaller files.
+ * That's because we prefer deltas to be from the bigger file
+ * to the smaller - deletes are potentially cheaper, but perhaps
+ * more importantly, the bigger file is likely the more recent
+ * one.
+ */
+static int try_delta(struct unpacked *trg, struct unpacked *src,
+                    unsigned max_depth, unsigned long *mem_usage)
+{
+       struct object_entry *trg_entry = trg->entry;
+       struct object_entry *src_entry = src->entry;
+       unsigned long trg_size, src_size, delta_size, sizediff, max_size, sz;
+       unsigned ref_depth;
+       enum object_type type;
+       void *delta_buf;
+
+       /* Don't bother doing diffs between different types */
+       if (trg_entry->type != src_entry->type)
+               return -1;
+
+       /*
+        * We do not bother to try a delta that we discarded
+        * on an earlier try, but only when reusing delta data.
+        */
+       if (!no_reuse_delta && trg_entry->in_pack &&
+           trg_entry->in_pack == src_entry->in_pack &&
+           trg_entry->in_pack_type != OBJ_REF_DELTA &&
+           trg_entry->in_pack_type != OBJ_OFS_DELTA)
+               return 0;
+
+       /* Let's not bust the allowed depth. */
+       if (src->depth >= max_depth)
+               return 0;
+
+       /* Now some size filtering heuristics. */
+       trg_size = trg_entry->size;
+       if (!trg_entry->delta) {
+               max_size = trg_size/2 - 20;
+               ref_depth = 1;
+       } else {
+               max_size = trg_entry->delta_size;
+               ref_depth = trg->depth;
+       }
+       max_size = max_size * (max_depth - src->depth) /
+                                               (max_depth - ref_depth + 1);
+       if (max_size == 0)
+               return 0;
+       src_size = src_entry->size;
+       sizediff = src_size < trg_size ? trg_size - src_size : 0;
+       if (sizediff >= max_size)
+               return 0;
+       if (trg_size < src_size / 32)
+               return 0;
+
+       /* Load data if not already done */
+       if (!trg->data) {
+               read_lock();
+               trg->data = read_sha1_file(trg_entry->idx.sha1, &type, &sz);
+               read_unlock();
+               if (!trg->data)
+                       die("object %s cannot be read",
+                           sha1_to_hex(trg_entry->idx.sha1));
+               if (sz != trg_size)
+                       die("object %s inconsistent object length (%lu vs %lu)",
+                           sha1_to_hex(trg_entry->idx.sha1), sz, trg_size);
+               *mem_usage += sz;
+       }
+       if (!src->data) {
+               read_lock();
+               src->data = read_sha1_file(src_entry->idx.sha1, &type, &sz);
+               read_unlock();
+               if (!src->data)
+                       die("object %s cannot be read",
+                           sha1_to_hex(src_entry->idx.sha1));
+               if (sz != src_size)
+                       die("object %s inconsistent object length (%lu vs %lu)",
+                           sha1_to_hex(src_entry->idx.sha1), sz, src_size);
+               *mem_usage += sz;
+       }
+       if (!src->index) {
+               src->index = create_delta_index(src->data, src_size);
+               if (!src->index) {
+                       static int warned = 0;
+                       if (!warned++)
+                               warning("suboptimal pack - out of memory");
+                       return 0;
+               }
+               *mem_usage += sizeof_delta_index(src->index);
+       }
+
+       delta_buf = create_delta(src->index, trg->data, trg_size, &delta_size, max_size);
+       if (!delta_buf)
+               return 0;
+
+       if (trg_entry->delta) {
+               /* Prefer only shallower same-sized deltas. */
+               if (delta_size == trg_entry->delta_size &&
+                   src->depth + 1 >= trg->depth) {
+                       free(delta_buf);
+                       return 0;
+               }
+       }
+
+       trg_entry->delta = src_entry;
+       trg_entry->delta_size = delta_size;
+       trg->depth = src->depth + 1;
+
+       /*
+        * Handle memory allocation outside of the cache
+        * accounting lock.  Compiler will optimize the strangeness
+        * away when THREADED_DELTA_SEARCH is not defined.
+        */
+       if (trg_entry->delta_data)
+               free(trg_entry->delta_data);
+       cache_lock();
+       if (trg_entry->delta_data) {
+               delta_cache_size -= trg_entry->delta_size;
+               trg_entry->delta_data = NULL;
+       }
+       if (delta_cacheable(src_size, trg_size, delta_size)) {
+               delta_cache_size += trg_entry->delta_size;
+               cache_unlock();
+               trg_entry->delta_data = xrealloc(delta_buf, delta_size);
+       } else {
+               cache_unlock();
+               free(delta_buf);
+       }
+
+       return 1;
+}
+
+static unsigned int check_delta_limit(struct object_entry *me, unsigned int n)
+{
+       struct object_entry *child = me->delta_child;
+       unsigned int m = n;
+       while (child) {
+               unsigned int c = check_delta_limit(child, n + 1);
+               if (m < c)
+                       m = c;
+               child = child->delta_sibling;
+       }
+       return m;
+}
+
+static unsigned long free_unpacked(struct unpacked *n)
+{
+       unsigned long freed_mem = sizeof_delta_index(n->index);
+       free_delta_index(n->index);
+       n->index = NULL;
+       if (n->data) {
+               freed_mem += n->entry->size;
+               free(n->data);
+               n->data = NULL;
+       }
+       n->entry = NULL;
+       n->depth = 0;
+       return freed_mem;
+}
+
+static void find_deltas(struct object_entry **list, unsigned list_size,
+                       int window, int depth, unsigned *processed)
+{
+       uint32_t i = list_size, idx = 0, count = 0;
+       unsigned int array_size = window * sizeof(struct unpacked);
+       struct unpacked *array;
+       unsigned long mem_usage = 0;
+
+       array = xmalloc(array_size);
+       memset(array, 0, array_size);
+
+       do {
+               struct object_entry *entry = list[--i];
+               struct unpacked *n = array + idx;
+               int j, max_depth, best_base = -1;
+
+               mem_usage -= free_unpacked(n);
+               n->entry = entry;
+
+               while (window_memory_limit &&
+                      mem_usage > window_memory_limit &&
+                      count > 1) {
+                       uint32_t tail = (idx + window - count) % window;
+                       mem_usage -= free_unpacked(array + tail);
+                       count--;
+               }
+
+               /* We do not compute delta to *create* objects we are not
+                * going to pack.
+                */
+               if (entry->preferred_base)
+                       goto next;
+
+               progress_lock();
+               (*processed)++;
+               display_progress(progress_state, *processed);
+               progress_unlock();
+
+               /*
+                * If the current object is at pack edge, take the depth the
+                * objects that depend on the current object into account
+                * otherwise they would become too deep.
+                */
+               max_depth = depth;
+               if (entry->delta_child) {
+                       max_depth -= check_delta_limit(entry, 0);
+                       if (max_depth <= 0)
+                               goto next;
+               }
+
+               j = window;
+               while (--j > 0) {
+                       int ret;
+                       uint32_t other_idx = idx + j;
+                       struct unpacked *m;
+                       if (other_idx >= window)
+                               other_idx -= window;
+                       m = array + other_idx;
+                       if (!m->entry)
+                               break;
+                       ret = try_delta(n, m, max_depth, &mem_usage);
+                       if (ret < 0)
+                               break;
+                       else if (ret > 0)
+                               best_base = other_idx;
+               }
+
+               /* if we made n a delta, and if n is already at max
+                * depth, leaving it in the window is pointless.  we
+                * should evict it first.
+                */
+               if (entry->delta && depth <= n->depth)
+                       continue;
+
+               /*
+                * Move the best delta base up in the window, after the
+                * currently deltified object, to keep it longer.  It will
+                * be the first base object to be attempted next.
+                */
+               if (entry->delta) {
+                       struct unpacked swap = array[best_base];
+                       int dist = (window + idx - best_base) % window;
+                       int dst = best_base;
+                       while (dist--) {
+                               int src = (dst + 1) % window;
+                               array[dst] = array[src];
+                               dst = src;
+                       }
+                       array[dst] = swap;
+               }
+
+               next:
+               idx++;
+               if (count + 1 < window)
+                       count++;
+               if (idx >= window)
+                       idx = 0;
+       } while (i > 0);
+
+       for (i = 0; i < window; ++i) {
+               free_delta_index(array[i].index);
+               free(array[i].data);
+       }
+       free(array);
+}
+
+#ifdef THREADED_DELTA_SEARCH
+
+struct thread_params {
+       pthread_t thread;
+       struct object_entry **list;
+       unsigned list_size;
+       int window;
+       int depth;
+       unsigned *processed;
+};
+
+static pthread_mutex_t data_request  = PTHREAD_MUTEX_INITIALIZER;
+static pthread_mutex_t data_ready    = PTHREAD_MUTEX_INITIALIZER;
+static pthread_mutex_t data_provider = PTHREAD_MUTEX_INITIALIZER;
+static struct thread_params *data_requester;
+
+static void *threaded_find_deltas(void *arg)
+{
+       struct thread_params *me = arg;
+
+       for (;;) {
+               pthread_mutex_lock(&data_request);
+               data_requester = me;
+               pthread_mutex_unlock(&data_provider);
+               pthread_mutex_lock(&data_ready);
+               pthread_mutex_unlock(&data_request);
+
+               if (!me->list_size)
+                       return NULL;
+
+               find_deltas(me->list, me->list_size,
+                           me->window, me->depth, me->processed);
+       }
+}
+
+static void ll_find_deltas(struct object_entry **list, unsigned list_size,
+                          int window, int depth, unsigned *processed)
+{
+       struct thread_params *target, p[delta_search_threads];
+       int i, ret;
+       unsigned chunk_size;
+
+       if (delta_search_threads <= 1) {
+               find_deltas(list, list_size, window, depth, processed);
+               return;
+       }
+
+       pthread_mutex_lock(&data_provider);
+       pthread_mutex_lock(&data_ready);
+
+       for (i = 0; i < delta_search_threads; i++) {
+               p[i].window = window;
+               p[i].depth = depth;
+               p[i].processed = processed;
+               ret = pthread_create(&p[i].thread, NULL,
+                                    threaded_find_deltas, &p[i]);
+               if (ret)
+                       die("unable to create thread: %s", strerror(ret));
+       }
+
+       /* this should be auto-tuned somehow */
+       chunk_size = window * 1000;
+
+       do {
+               unsigned sublist_size = chunk_size;
+               if (sublist_size > list_size)
+                       sublist_size = list_size;
+
+               /* try to split chunks on "path" boundaries */
+               while (sublist_size < list_size && list[sublist_size]->hash &&
+                      list[sublist_size]->hash == list[sublist_size-1]->hash)
+                       sublist_size++;
+
+               pthread_mutex_lock(&data_provider);
+               target = data_requester;
+               target->list = list;
+               target->list_size = sublist_size;
+               pthread_mutex_unlock(&data_ready);
+
+               list += sublist_size;
+               list_size -= sublist_size;
+               if (!sublist_size) {
+                       pthread_join(target->thread, NULL);
+                       i--;
+               }
+       } while (i);
+}
+
+#else
+#define ll_find_deltas find_deltas
+#endif
+
+static void prepare_pack(int window, int depth)
+{
+       struct object_entry **delta_list;
+       uint32_t i, n, nr_deltas;
+
+       get_object_details();
+
+       if (!nr_objects || !window || !depth)
+               return;
+
+       delta_list = xmalloc(nr_objects * sizeof(*delta_list));
+       nr_deltas = n = 0;
+
+       for (i = 0; i < nr_objects; i++) {
+               struct object_entry *entry = objects + i;
+
+               if (entry->delta)
+                       /* This happens if we decided to reuse existing
+                        * delta from a pack.  "!no_reuse_delta &&" is implied.
+                        */
+                       continue;
+
+               if (entry->size < 50)
+                       continue;
+
+               if (entry->no_try_delta)
+                       continue;
+
+               if (!entry->preferred_base)
+                       nr_deltas++;
+
+               delta_list[n++] = entry;
+       }
+
+       if (nr_deltas && n > 1) {
+               unsigned nr_done = 0;
+               if (progress)
+                       progress_state = start_progress("Compressing objects",
+                                                       nr_deltas);
+               qsort(delta_list, n, sizeof(*delta_list), type_size_sort);
+               ll_find_deltas(delta_list, n, window+1, depth, &nr_done);
+               stop_progress(&progress_state);
+               if (nr_done != nr_deltas)
+                       die("inconsistency with delta count");
+       }
+       free(delta_list);
+}
+
+static int git_pack_config(const char *k, const char *v)
+{
+       if(!strcmp(k, "pack.window")) {
+               window = git_config_int(k, v);
+               return 0;
+       }
+       if (!strcmp(k, "pack.windowmemory")) {
+               window_memory_limit = git_config_ulong(k, v);
+               return 0;
+       }
+       if (!strcmp(k, "pack.depth")) {
+               depth = git_config_int(k, v);
+               return 0;
+       }
+       if (!strcmp(k, "pack.compression")) {
+               int level = git_config_int(k, v);
+               if (level == -1)
+                       level = Z_DEFAULT_COMPRESSION;
+               else if (level < 0 || level > Z_BEST_COMPRESSION)
+                       die("bad pack compression level %d", level);
+               pack_compression_level = level;
+               pack_compression_seen = 1;
+               return 0;
+       }
+       if (!strcmp(k, "pack.deltacachesize")) {
+               max_delta_cache_size = git_config_int(k, v);
+               return 0;
+       }
+       if (!strcmp(k, "pack.deltacachelimit")) {
+               cache_max_small_delta_size = git_config_int(k, v);
+               return 0;
+       }
+       if (!strcmp(k, "pack.threads")) {
+               delta_search_threads = git_config_int(k, v);
+               if (delta_search_threads < 1)
+                       die("invalid number of threads specified (%d)",
+                           delta_search_threads);
+#ifndef THREADED_DELTA_SEARCH
+               if (delta_search_threads > 1)
+                       warning("no threads support, ignoring %s", k);
+#endif
+               return 0;
+       }
+       if (!strcmp(k, "pack.indexversion")) {
+               pack_idx_default_version = git_config_int(k, v);
+               if (pack_idx_default_version > 2)
+                       die("bad pack.indexversion=%d", pack_idx_default_version);
+               return 0;
+       }
+       return git_default_config(k, v);
+}
+
+static void read_object_list_from_stdin(void)
+{
+       char line[40 + 1 + PATH_MAX + 2];
+       unsigned char sha1[20];
+
+       for (;;) {
+               if (!fgets(line, sizeof(line), stdin)) {
+                       if (feof(stdin))
+                               break;
+                       if (!ferror(stdin))
+                               die("fgets returned NULL, not EOF, not error!");
+                       if (errno != EINTR)
+                               die("fgets: %s", strerror(errno));
+                       clearerr(stdin);
+                       continue;
+               }
+               if (line[0] == '-') {
+                       if (get_sha1_hex(line+1, sha1))
+                               die("expected edge sha1, got garbage:\n %s",
+                                   line);
+                       add_preferred_base(sha1);
+                       continue;
+               }
+               if (get_sha1_hex(line, sha1))
+                       die("expected sha1, got garbage:\n %s", line);
+
+               add_preferred_base_object(line+41);
+               add_object_entry(sha1, 0, line+41, 0);
+       }
+}
+
+#define OBJECT_ADDED (1u<<20)
+
+static void show_commit(struct commit *commit)
+{
+       add_object_entry(commit->object.sha1, OBJ_COMMIT, NULL, 0);
+       commit->object.flags |= OBJECT_ADDED;
+}
+
+static void show_object(struct object_array_entry *p)
+{
+       add_preferred_base_object(p->name);
+       add_object_entry(p->item->sha1, p->item->type, p->name, 0);
+       p->item->flags |= OBJECT_ADDED;
+}
+
+static void show_edge(struct commit *commit)
+{
+       add_preferred_base(commit->object.sha1);
+}
+
+struct in_pack_object {
+       off_t offset;
+       struct object *object;
+};
+
+struct in_pack {
+       int alloc;
+       int nr;
+       struct in_pack_object *array;
+};
+
+static void mark_in_pack_object(struct object *object, struct packed_git *p, struct in_pack *in_pack)
+{
+       in_pack->array[in_pack->nr].offset = find_pack_entry_one(object->sha1, p);
+       in_pack->array[in_pack->nr].object = object;
+       in_pack->nr++;
+}
+
+/*
+ * Compare the objects in the offset order, in order to emulate the
+ * "git-rev-list --objects" output that produced the pack originally.
+ */
+static int ofscmp(const void *a_, const void *b_)
+{
+       struct in_pack_object *a = (struct in_pack_object *)a_;
+       struct in_pack_object *b = (struct in_pack_object *)b_;
+
+       if (a->offset < b->offset)
+               return -1;
+       else if (a->offset > b->offset)
+               return 1;
+       else
+               return hashcmp(a->object->sha1, b->object->sha1);
+}
+
+static void add_objects_in_unpacked_packs(struct rev_info *revs)
+{
+       struct packed_git *p;
+       struct in_pack in_pack;
+       uint32_t i;
+
+       memset(&in_pack, 0, sizeof(in_pack));
+
+       for (p = packed_git; p; p = p->next) {
+               const unsigned char *sha1;
+               struct object *o;
+
+               for (i = 0; i < revs->num_ignore_packed; i++) {
+                       if (matches_pack_name(p, revs->ignore_packed[i]))
+                               break;
+               }
+               if (revs->num_ignore_packed <= i)
+                       continue;
+               if (open_pack_index(p))
+                       die("cannot open pack index");
+
+               ALLOC_GROW(in_pack.array,
+                          in_pack.nr + p->num_objects,
+                          in_pack.alloc);
+
+               for (i = 0; i < p->num_objects; i++) {
+                       sha1 = nth_packed_object_sha1(p, i);
+                       o = lookup_unknown_object(sha1);
+                       if (!(o->flags & OBJECT_ADDED))
+                               mark_in_pack_object(o, p, &in_pack);
+                       o->flags |= OBJECT_ADDED;
+               }
+       }
+
+       if (in_pack.nr) {
+               qsort(in_pack.array, in_pack.nr, sizeof(in_pack.array[0]),
+                     ofscmp);
+               for (i = 0; i < in_pack.nr; i++) {
+                       struct object *o = in_pack.array[i].object;
+                       add_object_entry(o->sha1, o->type, "", 0);
+               }
+       }
+       free(in_pack.array);
+}
+
+static void get_object_list(int ac, const char **av)
+{
+       struct rev_info revs;
+       char line[1000];
+       int flags = 0;
+
+       init_revisions(&revs, NULL);
+       save_commit_buffer = 0;
+       track_object_refs = 0;
+       setup_revisions(ac, av, &revs, NULL);
+
+       while (fgets(line, sizeof(line), stdin) != NULL) {
+               int len = strlen(line);
+               if (line[len - 1] == '\n')
+                       line[--len] = 0;
+               if (!len)
+                       break;
+               if (*line == '-') {
+                       if (!strcmp(line, "--not")) {
+                               flags ^= UNINTERESTING;
+                               continue;
+                       }
+                       die("not a rev '%s'", line);
+               }
+               if (handle_revision_arg(line, &revs, flags, 1))
+                       die("bad revision '%s'", line);
+       }
+
+       prepare_revision_walk(&revs);
+       mark_edges_uninteresting(revs.commits, &revs, show_edge);
+       traverse_commit_list(&revs, show_commit, show_object);
+
+       if (keep_unreachable)
+               add_objects_in_unpacked_packs(&revs);
+}
+
+static int adjust_perm(const char *path, mode_t mode)
+{
+       if (chmod(path, mode))
+               return -1;
+       return adjust_shared_perm(path);
+}
+
+int cmd_pack_objects(int argc, const char **argv, const char *prefix)
+{
+       int use_internal_rev_list = 0;
+       int thin = 0;
+       uint32_t i;
+       const char **rp_av;
+       int rp_ac_alloc = 64;
+       int rp_ac;
+
+       rp_av = xcalloc(rp_ac_alloc, sizeof(*rp_av));
+
+       rp_av[0] = "pack-objects";
+       rp_av[1] = "--objects"; /* --thin will make it --objects-edge */
+       rp_ac = 2;
+
+       git_config(git_pack_config);
+       if (!pack_compression_seen && core_compression_seen)
+               pack_compression_level = core_compression_level;
+
+       progress = isatty(2);
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (*arg != '-')
+                       break;
+
+               if (!strcmp("--non-empty", arg)) {
+                       non_empty = 1;
+                       continue;
+               }
+               if (!strcmp("--local", arg)) {
+                       local = 1;
+                       continue;
+               }
+               if (!strcmp("--incremental", arg)) {
+                       incremental = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--compression=")) {
+                       char *end;
+                       int level = strtoul(arg+14, &end, 0);
+                       if (!arg[14] || *end)
+                               usage(pack_usage);
+                       if (level == -1)
+                               level = Z_DEFAULT_COMPRESSION;
+                       else if (level < 0 || level > Z_BEST_COMPRESSION)
+                               die("bad pack compression level %d", level);
+                       pack_compression_level = level;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--max-pack-size=")) {
+                       char *end;
+                       pack_size_limit = strtoul(arg+16, &end, 0) * 1024 * 1024;
+                       if (!arg[16] || *end)
+                               usage(pack_usage);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--window=")) {
+                       char *end;
+                       window = strtoul(arg+9, &end, 0);
+                       if (!arg[9] || *end)
+                               usage(pack_usage);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--window-memory=")) {
+                       if (!git_parse_ulong(arg+16, &window_memory_limit))
+                               usage(pack_usage);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--threads=")) {
+                       char *end;
+                       delta_search_threads = strtoul(arg+10, &end, 0);
+                       if (!arg[10] || *end || delta_search_threads < 1)
+                               usage(pack_usage);
+#ifndef THREADED_DELTA_SEARCH
+                       if (delta_search_threads > 1)
+                               warning("no threads support, "
+                                       "ignoring %s", arg);
+#endif
+                       continue;
+               }
+               if (!prefixcmp(arg, "--depth=")) {
+                       char *end;
+                       depth = strtoul(arg+8, &end, 0);
+                       if (!arg[8] || *end)
+                               usage(pack_usage);
+                       continue;
+               }
+               if (!strcmp("--progress", arg)) {
+                       progress = 1;
+                       continue;
+               }
+               if (!strcmp("--all-progress", arg)) {
+                       progress = 2;
+                       continue;
+               }
+               if (!strcmp("-q", arg)) {
+                       progress = 0;
+                       continue;
+               }
+               if (!strcmp("--no-reuse-delta", arg)) {
+                       no_reuse_delta = 1;
+                       continue;
+               }
+               if (!strcmp("--no-reuse-object", arg)) {
+                       no_reuse_object = no_reuse_delta = 1;
+                       continue;
+               }
+               if (!strcmp("--delta-base-offset", arg)) {
+                       allow_ofs_delta = 1;
+                       continue;
+               }
+               if (!strcmp("--stdout", arg)) {
+                       pack_to_stdout = 1;
+                       continue;
+               }
+               if (!strcmp("--revs", arg)) {
+                       use_internal_rev_list = 1;
+                       continue;
+               }
+               if (!strcmp("--keep-unreachable", arg)) {
+                       keep_unreachable = 1;
+                       continue;
+               }
+               if (!strcmp("--unpacked", arg) ||
+                   !prefixcmp(arg, "--unpacked=") ||
+                   !strcmp("--reflog", arg) ||
+                   !strcmp("--all", arg)) {
+                       use_internal_rev_list = 1;
+                       if (rp_ac >= rp_ac_alloc - 1) {
+                               rp_ac_alloc = alloc_nr(rp_ac_alloc);
+                               rp_av = xrealloc(rp_av,
+                                                rp_ac_alloc * sizeof(*rp_av));
+                       }
+                       rp_av[rp_ac++] = arg;
+                       continue;
+               }
+               if (!strcmp("--thin", arg)) {
+                       use_internal_rev_list = 1;
+                       thin = 1;
+                       rp_av[1] = "--objects-edge";
+                       continue;
+               }
+               if (!prefixcmp(arg, "--index-version=")) {
+                       char *c;
+                       pack_idx_default_version = strtoul(arg + 16, &c, 10);
+                       if (pack_idx_default_version > 2)
+                               die("bad %s", arg);
+                       if (*c == ',')
+                               pack_idx_off32_limit = strtoul(c+1, &c, 0);
+                       if (*c || pack_idx_off32_limit & 0x80000000)
+                               die("bad %s", arg);
+                       continue;
+               }
+               usage(pack_usage);
+       }
+
+       /* Traditionally "pack-objects [options] base extra" failed;
+        * we would however want to take refs parameter that would
+        * have been given to upstream rev-list ourselves, which means
+        * we somehow want to say what the base name is.  So the
+        * syntax would be:
+        *
+        * pack-objects [options] base <refs...>
+        *
+        * in other words, we would treat the first non-option as the
+        * base_name and send everything else to the internal revision
+        * walker.
+        */
+
+       if (!pack_to_stdout)
+               base_name = argv[i++];
+
+       if (pack_to_stdout != !base_name)
+               usage(pack_usage);
+
+       if (pack_to_stdout && pack_size_limit)
+               die("--max-pack-size cannot be used to build a pack for transfer.");
+
+       if (!pack_to_stdout && thin)
+               die("--thin cannot be used to build an indexable pack.");
+
+       prepare_packed_git();
+
+       if (progress)
+               progress_state = start_progress("Counting objects", 0);
+       if (!use_internal_rev_list)
+               read_object_list_from_stdin();
+       else {
+               rp_av[rp_ac] = NULL;
+               get_object_list(rp_ac, rp_av);
+       }
+       stop_progress(&progress_state);
+
+       if (non_empty && !nr_result)
+               return 0;
+       if (nr_result)
+               prepare_pack(window, depth);
+       write_pack_file();
+       if (progress)
+               fprintf(stderr, "Total %u (delta %u), reused %u (delta %u)\n",
+                       written, written_delta, reused, reused_delta);
+       return 0;
+}
diff --git a/builtin-pack-refs.c b/builtin-pack-refs.c
new file mode 100644 (file)
index 0000000..1923fb1
--- /dev/null
@@ -0,0 +1,134 @@
+#include "builtin.h"
+#include "cache.h"
+#include "refs.h"
+#include "object.h"
+#include "tag.h"
+#include "parse-options.h"
+
+struct ref_to_prune {
+       struct ref_to_prune *next;
+       unsigned char sha1[20];
+       char name[FLEX_ARRAY];
+};
+
+#define PACK_REFS_PRUNE        0x0001
+#define PACK_REFS_ALL  0x0002
+
+struct pack_refs_cb_data {
+       unsigned int flags;
+       struct ref_to_prune *ref_to_prune;
+       FILE *refs_file;
+};
+
+static int do_not_prune(int flags)
+{
+       /* If it is already packed or if it is a symref,
+        * do not prune it.
+        */
+       return (flags & (REF_ISSYMREF|REF_ISPACKED));
+}
+
+static int handle_one_ref(const char *path, const unsigned char *sha1,
+                         int flags, void *cb_data)
+{
+       struct pack_refs_cb_data *cb = cb_data;
+       int is_tag_ref;
+
+       /* Do not pack the symbolic refs */
+       if ((flags & REF_ISSYMREF))
+               return 0;
+       is_tag_ref = !prefixcmp(path, "refs/tags/");
+
+       /* ALWAYS pack refs that were already packed or are tags */
+       if (!(cb->flags & PACK_REFS_ALL) && !is_tag_ref && !(flags & REF_ISPACKED))
+               return 0;
+
+       fprintf(cb->refs_file, "%s %s\n", sha1_to_hex(sha1), path);
+       if (is_tag_ref) {
+               struct object *o = parse_object(sha1);
+               if (o->type == OBJ_TAG) {
+                       o = deref_tag(o, path, 0);
+                       if (o)
+                               fprintf(cb->refs_file, "^%s\n",
+                                       sha1_to_hex(o->sha1));
+               }
+       }
+
+       if ((cb->flags & PACK_REFS_PRUNE) && !do_not_prune(flags)) {
+               int namelen = strlen(path) + 1;
+               struct ref_to_prune *n = xcalloc(1, sizeof(*n) + namelen);
+               hashcpy(n->sha1, sha1);
+               strcpy(n->name, path);
+               n->next = cb->ref_to_prune;
+               cb->ref_to_prune = n;
+       }
+       return 0;
+}
+
+/* make sure nobody touched the ref, and unlink */
+static void prune_ref(struct ref_to_prune *r)
+{
+       struct ref_lock *lock = lock_ref_sha1(r->name + 5, r->sha1);
+
+       if (lock) {
+               unlink(git_path("%s", r->name));
+               unlock_ref(lock);
+       }
+}
+
+static void prune_refs(struct ref_to_prune *r)
+{
+       while (r) {
+               prune_ref(r);
+               r = r->next;
+       }
+}
+
+static struct lock_file packed;
+
+static int pack_refs(unsigned int flags)
+{
+       int fd;
+       struct pack_refs_cb_data cbdata;
+
+       memset(&cbdata, 0, sizeof(cbdata));
+       cbdata.flags = flags;
+
+       fd = hold_lock_file_for_update(&packed, git_path("packed-refs"), 1);
+       cbdata.refs_file = fdopen(fd, "w");
+       if (!cbdata.refs_file)
+               die("unable to create ref-pack file structure (%s)",
+                   strerror(errno));
+
+       /* perhaps other traits later as well */
+       fprintf(cbdata.refs_file, "# pack-refs with: peeled \n");
+
+       for_each_ref(handle_one_ref, &cbdata);
+       if (ferror(cbdata.refs_file))
+               die("failed to write ref-pack file");
+       if (fflush(cbdata.refs_file) || fsync(fd) || fclose(cbdata.refs_file))
+               die("failed to write ref-pack file (%s)", strerror(errno));
+       if (commit_lock_file(&packed) < 0)
+               die("unable to overwrite old ref-pack file (%s)", strerror(errno));
+       if (cbdata.flags & PACK_REFS_PRUNE)
+               prune_refs(cbdata.ref_to_prune);
+       return 0;
+}
+
+static char const * const pack_refs_usage[] = {
+       "git-pack-refs [options]",
+       NULL
+};
+
+int cmd_pack_refs(int argc, const char **argv, const char *prefix)
+{
+       unsigned int flags = PACK_REFS_PRUNE;
+       struct option opts[] = {
+               OPT_BIT(0, "all",   &flags, "pack everything", PACK_REFS_ALL),
+               OPT_BIT(0, "prune", &flags, "prune loose refs (default)", PACK_REFS_PRUNE),
+               OPT_END(),
+       };
+       if (parse_options(argc, argv, opts, pack_refs_usage, 0))
+               usage_with_options(pack_refs_usage, opts);
+       return pack_refs(flags);
+}
diff --git a/builtin-prune-packed.c b/builtin-prune-packed.c
new file mode 100644 (file)
index 0000000..23faf31
--- /dev/null
@@ -0,0 +1,91 @@
+#include "builtin.h"
+#include "cache.h"
+#include "progress.h"
+
+static const char prune_packed_usage[] =
+"git-prune-packed [-n] [-q]";
+
+#define DRY_RUN 01
+#define VERBOSE 02
+
+static struct progress *progress;
+
+static void prune_dir(int i, DIR *dir, char *pathname, int len, int opts)
+{
+       struct dirent *de;
+       char hex[40];
+
+       sprintf(hex, "%02x", i);
+       while ((de = readdir(dir)) != NULL) {
+               unsigned char sha1[20];
+               if (strlen(de->d_name) != 38)
+                       continue;
+               memcpy(hex+2, de->d_name, 38);
+               if (get_sha1_hex(hex, sha1))
+                       continue;
+               if (!has_sha1_pack(sha1, NULL))
+                       continue;
+               memcpy(pathname + len, de->d_name, 38);
+               if (opts & DRY_RUN)
+                       printf("rm -f %s\n", pathname);
+               else if (unlink(pathname) < 0)
+                       error("unable to unlink %s", pathname);
+               display_progress(progress, i + 1);
+       }
+       pathname[len] = 0;
+       rmdir(pathname);
+}
+
+void prune_packed_objects(int opts)
+{
+       int i;
+       static char pathname[PATH_MAX];
+       const char *dir = get_object_directory();
+       int len = strlen(dir);
+
+       if (opts == VERBOSE)
+               progress = start_progress_delay("Removing duplicate objects",
+                       256, 95, 2);
+
+       if (len > PATH_MAX - 42)
+               die("impossible object directory");
+       memcpy(pathname, dir, len);
+       if (len && pathname[len-1] != '/')
+               pathname[len++] = '/';
+       for (i = 0; i < 256; i++) {
+               DIR *d;
+
+               sprintf(pathname + len, "%02x/", i);
+               d = opendir(pathname);
+               if (!d)
+                       continue;
+               prune_dir(i, d, pathname, len + 3, opts);
+               closedir(d);
+       }
+       stop_progress(&progress);
+}
+
+int cmd_prune_packed(int argc, const char **argv, const char *prefix)
+{
+       int i;
+       int opts = VERBOSE;
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (*arg == '-') {
+                       if (!strcmp(arg, "-n"))
+                               opts |= DRY_RUN;
+                       else if (!strcmp(arg, "-q"))
+                               opts &= ~VERBOSE;
+                       else
+                               usage(prune_packed_usage);
+                       continue;
+               }
+               /* Handle arguments here .. */
+               usage(prune_packed_usage);
+       }
+       sync();
+       prune_packed_objects(opts);
+       return 0;
+}
diff --git a/builtin-prune.c b/builtin-prune.c
new file mode 100644 (file)
index 0000000..b5e7684
--- /dev/null
@@ -0,0 +1,119 @@
+#include "cache.h"
+#include "commit.h"
+#include "diff.h"
+#include "revision.h"
+#include "builtin.h"
+#include "reachable.h"
+
+static const char prune_usage[] = "git-prune [-n]";
+static int show_only;
+static unsigned long expire;
+
+static int prune_object(char *path, const char *filename, const unsigned char *sha1)
+{
+       const char *fullpath = mkpath("%s/%s", path, filename);
+       if (expire) {
+               struct stat st;
+               if (lstat(fullpath, &st))
+                       return error("Could not stat '%s'", fullpath);
+               if (st.st_mtime > expire)
+                       return 0;
+       }
+       if (show_only) {
+               enum object_type type = sha1_object_info(sha1, NULL);
+               printf("%s %s\n", sha1_to_hex(sha1),
+                      (type > 0) ? typename(type) : "unknown");
+       } else
+               unlink(fullpath);
+       return 0;
+}
+
+static int prune_dir(int i, char *path)
+{
+       DIR *dir = opendir(path);
+       struct dirent *de;
+
+       if (!dir)
+               return 0;
+
+       while ((de = readdir(dir)) != NULL) {
+               char name[100];
+               unsigned char sha1[20];
+               int len = strlen(de->d_name);
+
+               switch (len) {
+               case 2:
+                       if (de->d_name[1] != '.')
+                               break;
+               case 1:
+                       if (de->d_name[0] != '.')
+                               break;
+                       continue;
+               case 38:
+                       sprintf(name, "%02x", i);
+                       memcpy(name+2, de->d_name, len+1);
+                       if (get_sha1_hex(name, sha1) < 0)
+                               break;
+
+                       /*
+                        * Do we know about this object?
+                        * It must have been reachable
+                        */
+                       if (lookup_object(sha1))
+                               continue;
+
+                       prune_object(path, de->d_name, sha1);
+                       continue;
+               }
+               fprintf(stderr, "bad sha1 file: %s/%s\n", path, de->d_name);
+       }
+       if (!show_only)
+               rmdir(path);
+       closedir(dir);
+       return 0;
+}
+
+static void prune_object_dir(const char *path)
+{
+       int i;
+       for (i = 0; i < 256; i++) {
+               static char dir[4096];
+               sprintf(dir, "%s/%02x", path, i);
+               prune_dir(i, dir);
+       }
+}
+
+int cmd_prune(int argc, const char **argv, const char *prefix)
+{
+       int i;
+       struct rev_info revs;
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (!strcmp(arg, "-n")) {
+                       show_only = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--expire")) {
+                       if (++i < argc) {
+                               expire = approxidate(argv[i]);
+                               continue;
+                       }
+               }
+               else if (!prefixcmp(arg, "--expire=")) {
+                       expire = approxidate(arg + 9);
+                       continue;
+               }
+               usage(prune_usage);
+       }
+
+       save_commit_buffer = 0;
+       init_revisions(&revs, prefix);
+       mark_reachable_objects(&revs, 1);
+
+       prune_object_dir(get_object_directory());
+
+       sync();
+       prune_packed_objects(show_only);
+       return 0;
+}
diff --git a/builtin-push.c b/builtin-push.c
new file mode 100644 (file)
index 0000000..c8cb63e
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * "git push"
+ */
+#include "cache.h"
+#include "refs.h"
+#include "run-command.h"
+#include "builtin.h"
+#include "remote.h"
+#include "transport.h"
+#include "parse-options.h"
+
+static const char * const push_usage[] = {
+       "git-push [--all | --mirror] [--dry-run] [--tags] [--receive-pack=<git-receive-pack>] [--repo=all] [-f | --force] [-v] [<repository> <refspec>...]",
+       NULL,
+};
+
+static int thin, verbose;
+static const char *receivepack;
+
+static const char **refspec;
+static int refspec_nr;
+
+static void add_refspec(const char *ref)
+{
+       int nr = refspec_nr + 1;
+       refspec = xrealloc(refspec, nr * sizeof(char *));
+       refspec[nr-1] = ref;
+       refspec_nr = nr;
+}
+
+static void set_refspecs(const char **refs, int nr)
+{
+       int i;
+       for (i = 0; i < nr; i++) {
+               const char *ref = refs[i];
+               if (!strcmp("tag", ref)) {
+                       char *tag;
+                       int len;
+                       if (nr <= ++i)
+                               die("tag shorthand without <tag>");
+                       len = strlen(refs[i]) + 11;
+                       tag = xmalloc(len);
+                       strcpy(tag, "refs/tags/");
+                       strcat(tag, refs[i]);
+                       ref = tag;
+               }
+               if (!strcmp("HEAD", ref)) {
+                       unsigned char sha1_dummy[20];
+                       ref = resolve_ref(ref, sha1_dummy, 1, NULL);
+                       if (!ref)
+                               die("HEAD cannot be resolved.");
+                       if (prefixcmp(ref, "refs/heads/"))
+                               die("HEAD cannot be resolved to branch.");
+                       ref = xstrdup(ref + 11);
+               }
+               add_refspec(ref);
+       }
+}
+
+static int do_push(const char *repo, int flags)
+{
+       int i, errs;
+       struct remote *remote = remote_get(repo);
+
+       if (!remote)
+               die("bad repository '%s'", repo);
+
+       if (!refspec
+               && !(flags & TRANSPORT_PUSH_ALL)
+               && remote->push_refspec_nr) {
+               refspec = remote->push_refspec;
+               refspec_nr = remote->push_refspec_nr;
+       }
+       errs = 0;
+       for (i = 0; i < remote->url_nr; i++) {
+               struct transport *transport =
+                       transport_get(remote, remote->url[i]);
+               int err;
+               if (receivepack)
+                       transport_set_option(transport,
+                                            TRANS_OPT_RECEIVEPACK, receivepack);
+               if (thin)
+                       transport_set_option(transport, TRANS_OPT_THIN, "yes");
+
+               if (verbose)
+                       fprintf(stderr, "Pushing to %s\n", remote->url[i]);
+               err = transport_push(transport, refspec_nr, refspec, flags);
+               err |= transport_disconnect(transport);
+
+               if (!err)
+                       continue;
+
+               error("failed to push to '%s'", remote->url[i]);
+               errs++;
+       }
+       return !!errs;
+}
+
+int cmd_push(int argc, const char **argv, const char *prefix)
+{
+       int flags = 0;
+       int all = 0;
+       int mirror = 0;
+       int dry_run = 0;
+       int force = 0;
+       int tags = 0;
+       const char *repo = NULL;        /* default repository */
+
+       struct option options[] = {
+               OPT__VERBOSE(&verbose),
+               OPT_STRING( 0 , "repo", &repo, "repository", "repository"),
+               OPT_BOOLEAN( 0 , "all", &all, "push all refs"),
+               OPT_BOOLEAN( 0 , "mirror", &mirror, "mirror all refs"),
+               OPT_BOOLEAN( 0 , "tags", &tags, "push tags"),
+               OPT_BOOLEAN( 0 , "dry-run", &dry_run, "dry run"),
+               OPT_BOOLEAN('f', "force", &force, "force updates"),
+               OPT_BOOLEAN( 0 , "thin", &thin, "use thin pack"),
+               OPT_STRING( 0 , "receive-pack", &receivepack, "receive-pack", "receive pack program"),
+               OPT_STRING( 0 , "exec", &receivepack, "receive-pack", "receive pack program"),
+               OPT_END()
+       };
+
+       argc = parse_options(argc, argv, options, push_usage, 0);
+
+       if (force)
+               flags |= TRANSPORT_PUSH_FORCE;
+       if (dry_run)
+               flags |= TRANSPORT_PUSH_DRY_RUN;
+       if (verbose)
+               flags |= TRANSPORT_PUSH_VERBOSE;
+       if (tags)
+               add_refspec("refs/tags/*");
+       if (all)
+               flags |= TRANSPORT_PUSH_ALL;
+       if (mirror)
+               flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE);
+
+       if (argc > 0) {
+               repo = argv[0];
+               set_refspecs(argv + 1, argc - 1);
+       }
+       if ((flags & (TRANSPORT_PUSH_ALL|TRANSPORT_PUSH_MIRROR)) && refspec)
+               usage_with_options(push_usage, options);
+
+       if ((flags & (TRANSPORT_PUSH_ALL|TRANSPORT_PUSH_MIRROR)) ==
+                               (TRANSPORT_PUSH_ALL|TRANSPORT_PUSH_MIRROR)) {
+               error("--all and --mirror are incompatible");
+               usage_with_options(push_usage, options);
+       }
+
+       return do_push(repo, flags);
+}
diff --git a/builtin-read-tree.c b/builtin-read-tree.c
new file mode 100644 (file)
index 0000000..43cd56a
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+
+#include "cache.h"
+#include "object.h"
+#include "tree.h"
+#include "tree-walk.h"
+#include "cache-tree.h"
+#include "unpack-trees.h"
+#include "dir.h"
+#include "builtin.h"
+
+#define MAX_TREES 8
+static int nr_trees;
+static struct tree *trees[MAX_TREES];
+
+static int list_tree(unsigned char *sha1)
+{
+       struct tree *tree;
+
+       if (nr_trees >= MAX_TREES)
+               die("I cannot read more than %d trees", MAX_TREES);
+       tree = parse_tree_indirect(sha1);
+       if (!tree)
+               return -1;
+       trees[nr_trees++] = tree;
+       return 0;
+}
+
+static int read_cache_unmerged(void)
+{
+       int i;
+       struct cache_entry **dst;
+       struct cache_entry *last = NULL;
+
+       read_cache();
+       dst = active_cache;
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (ce_stage(ce)) {
+                       if (last && !strcmp(ce->name, last->name))
+                               continue;
+                       cache_tree_invalidate_path(active_cache_tree, ce->name);
+                       last = ce;
+                       ce->ce_mode = 0;
+                       ce->ce_flags &= ~htons(CE_STAGEMASK);
+               }
+               *dst++ = ce;
+       }
+       active_nr = dst - active_cache;
+       return !!last;
+}
+
+static void prime_cache_tree_rec(struct cache_tree *it, struct tree *tree)
+{
+       struct tree_desc desc;
+       struct name_entry entry;
+       int cnt;
+
+       hashcpy(it->sha1, tree->object.sha1);
+       init_tree_desc(&desc, tree->buffer, tree->size);
+       cnt = 0;
+       while (tree_entry(&desc, &entry)) {
+               if (!S_ISDIR(entry.mode))
+                       cnt++;
+               else {
+                       struct cache_tree_sub *sub;
+                       struct tree *subtree = lookup_tree(entry.sha1);
+                       if (!subtree->object.parsed)
+                               parse_tree(subtree);
+                       sub = cache_tree_sub(it, entry.path);
+                       sub->cache_tree = cache_tree();
+                       prime_cache_tree_rec(sub->cache_tree, subtree);
+                       cnt += sub->cache_tree->entry_count;
+               }
+       }
+       it->entry_count = cnt;
+}
+
+static void prime_cache_tree(void)
+{
+       if (!nr_trees)
+               return;
+       active_cache_tree = cache_tree();
+       prime_cache_tree_rec(active_cache_tree, trees[0]);
+
+}
+
+static const char read_tree_usage[] = "git-read-tree (<sha> | [[-m [--trivial] [--aggressive] | --reset | --prefix=<prefix>] [-u | -i]] [--exclude-per-directory=<gitignore>] [--index-output=<file>] <sha1> [<sha2> [<sha3>]])";
+
+static struct lock_file lock_file;
+
+int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
+{
+       int i, newfd, stage = 0;
+       unsigned char sha1[20];
+       struct tree_desc t[MAX_TREES];
+       struct unpack_trees_options opts;
+
+       memset(&opts, 0, sizeof(opts));
+       opts.head_idx = -1;
+
+       git_config(git_default_config);
+
+       newfd = hold_locked_index(&lock_file, 1);
+
+       git_config(git_default_config);
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               /* "-u" means "update", meaning that a merge will update
+                * the working tree.
+                */
+               if (!strcmp(arg, "-u")) {
+                       opts.update = 1;
+                       continue;
+               }
+
+               if (!strcmp(arg, "-v")) {
+                       opts.verbose_update = 1;
+                       continue;
+               }
+
+               /* "-i" means "index only", meaning that a merge will
+                * not even look at the working tree.
+                */
+               if (!strcmp(arg, "-i")) {
+                       opts.index_only = 1;
+                       continue;
+               }
+
+               if (!prefixcmp(arg, "--index-output=")) {
+                       set_alternate_index_output(arg + 15);
+                       continue;
+               }
+
+               /* "--prefix=<subdirectory>/" means keep the current index
+                *  entries and put the entries from the tree under the
+                * given subdirectory.
+                */
+               if (!prefixcmp(arg, "--prefix=")) {
+                       if (stage || opts.merge || opts.prefix)
+                               usage(read_tree_usage);
+                       opts.prefix = arg + 9;
+                       opts.merge = 1;
+                       stage = 1;
+                       if (read_cache_unmerged())
+                               die("you need to resolve your current index first");
+                       continue;
+               }
+
+               /* This differs from "-m" in that we'll silently ignore
+                * unmerged entries and overwrite working tree files that
+                * correspond to them.
+                */
+               if (!strcmp(arg, "--reset")) {
+                       if (stage || opts.merge || opts.prefix)
+                               usage(read_tree_usage);
+                       opts.reset = 1;
+                       opts.merge = 1;
+                       stage = 1;
+                       read_cache_unmerged();
+                       continue;
+               }
+
+               if (!strcmp(arg, "--trivial")) {
+                       opts.trivial_merges_only = 1;
+                       continue;
+               }
+
+               if (!strcmp(arg, "--aggressive")) {
+                       opts.aggressive = 1;
+                       continue;
+               }
+
+               /* "-m" stands for "merge", meaning we start in stage 1 */
+               if (!strcmp(arg, "-m")) {
+                       if (stage || opts.merge || opts.prefix)
+                               usage(read_tree_usage);
+                       if (read_cache_unmerged())
+                               die("you need to resolve your current index first");
+                       stage = 1;
+                       opts.merge = 1;
+                       continue;
+               }
+
+               if (!prefixcmp(arg, "--exclude-per-directory=")) {
+                       struct dir_struct *dir;
+
+                       if (opts.dir)
+                               die("more than one --exclude-per-directory are given.");
+
+                       dir = xcalloc(1, sizeof(*opts.dir));
+                       dir->show_ignored = 1;
+                       dir->exclude_per_dir = arg + 24;
+                       opts.dir = dir;
+                       /* We do not need to nor want to do read-directory
+                        * here; we are merely interested in reusing the
+                        * per directory ignore stack mechanism.
+                        */
+                       continue;
+               }
+
+               /* using -u and -i at the same time makes no sense */
+               if (1 < opts.index_only + opts.update)
+                       usage(read_tree_usage);
+
+               if (get_sha1(arg, sha1))
+                       die("Not a valid object name %s", arg);
+               if (list_tree(sha1) < 0)
+                       die("failed to unpack tree object %s", arg);
+               stage++;
+       }
+       if ((opts.update||opts.index_only) && !opts.merge)
+               usage(read_tree_usage);
+       if ((opts.dir && !opts.update))
+               die("--exclude-per-directory is meaningless unless -u");
+
+       if (opts.prefix) {
+               int pfxlen = strlen(opts.prefix);
+               int pos;
+               if (opts.prefix[pfxlen-1] != '/')
+                       die("prefix must end with /");
+               if (stage != 2)
+                       die("binding merge takes only one tree");
+               pos = cache_name_pos(opts.prefix, pfxlen);
+               if (0 <= pos)
+                       die("corrupt index file");
+               pos = -pos-1;
+               if (pos < active_nr &&
+                   !strncmp(active_cache[pos]->name, opts.prefix, pfxlen))
+                       die("subdirectory '%s' already exists.", opts.prefix);
+               pos = cache_name_pos(opts.prefix, pfxlen-1);
+               if (0 <= pos)
+                       die("file '%.*s' already exists.",
+                                       pfxlen-1, opts.prefix);
+               opts.pos = -1 - pos;
+       }
+
+       if (opts.merge) {
+               if (stage < 2)
+                       die("just how do you expect me to merge %d trees?", stage-1);
+               switch (stage - 1) {
+               case 1:
+                       opts.fn = opts.prefix ? bind_merge : oneway_merge;
+                       break;
+               case 2:
+                       opts.fn = twoway_merge;
+                       break;
+               case 3:
+               default:
+                       opts.fn = threeway_merge;
+                       cache_tree_free(&active_cache_tree);
+                       break;
+               }
+
+               if (stage - 1 >= 3)
+                       opts.head_idx = stage - 2;
+               else
+                       opts.head_idx = 1;
+       }
+
+       for (i = 0; i < nr_trees; i++) {
+               struct tree *tree = trees[i];
+               parse_tree(tree);
+               init_tree_desc(t+i, tree->buffer, tree->size);
+       }
+       unpack_trees(nr_trees, t, &opts);
+
+       /*
+        * When reading only one tree (either the most basic form,
+        * "-m ent" or "--reset ent" form), we can obtain a fully
+        * valid cache-tree because the index must match exactly
+        * what came from the tree.
+        */
+       if (nr_trees && !opts.prefix && (!opts.merge || (stage == 2))) {
+               cache_tree_free(&active_cache_tree);
+               prime_cache_tree();
+       }
+
+       if (write_cache(newfd, active_cache, active_nr) ||
+           close(newfd) || commit_locked_index(&lock_file))
+               die("unable to write new index file");
+       return 0;
+}
diff --git a/builtin-reflog.c b/builtin-reflog.c
new file mode 100644 (file)
index 0000000..ce093ca
--- /dev/null
@@ -0,0 +1,387 @@
+#include "cache.h"
+#include "builtin.h"
+#include "commit.h"
+#include "refs.h"
+#include "dir.h"
+#include "tree-walk.h"
+#include "diff.h"
+#include "revision.h"
+#include "reachable.h"
+
+/*
+ * reflog expire
+ */
+
+static const char reflog_expire_usage[] =
+"git-reflog (show|expire) [--verbose] [--dry-run] [--stale-fix] [--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>...";
+
+static unsigned long default_reflog_expire;
+static unsigned long default_reflog_expire_unreachable;
+
+struct cmd_reflog_expire_cb {
+       struct rev_info revs;
+       int dry_run;
+       int stalefix;
+       int verbose;
+       unsigned long expire_total;
+       unsigned long expire_unreachable;
+};
+
+struct expire_reflog_cb {
+       FILE *newlog;
+       const char *ref;
+       struct commit *ref_commit;
+       struct cmd_reflog_expire_cb *cmd;
+};
+
+#define INCOMPLETE     (1u<<10)
+#define STUDYING       (1u<<11)
+
+static int tree_is_complete(const unsigned char *sha1)
+{
+       struct tree_desc desc;
+       struct name_entry entry;
+       int complete;
+       struct tree *tree;
+
+       tree = lookup_tree(sha1);
+       if (!tree)
+               return 0;
+       if (tree->object.flags & SEEN)
+               return 1;
+       if (tree->object.flags & INCOMPLETE)
+               return 0;
+
+       if (!tree->buffer) {
+               enum object_type type;
+               unsigned long size;
+               void *data = read_sha1_file(sha1, &type, &size);
+               if (!data) {
+                       tree->object.flags |= INCOMPLETE;
+                       return 0;
+               }
+               tree->buffer = data;
+               tree->size = size;
+       }
+       init_tree_desc(&desc, tree->buffer, tree->size);
+       complete = 1;
+       while (tree_entry(&desc, &entry)) {
+               if (!has_sha1_file(entry.sha1) ||
+                   (S_ISDIR(entry.mode) && !tree_is_complete(entry.sha1))) {
+                       tree->object.flags |= INCOMPLETE;
+                       complete = 0;
+               }
+       }
+       free(tree->buffer);
+       tree->buffer = NULL;
+
+       if (complete)
+               tree->object.flags |= SEEN;
+       return complete;
+}
+
+static int commit_is_complete(struct commit *commit)
+{
+       struct object_array study;
+       struct object_array found;
+       int is_incomplete = 0;
+       int i;
+
+       /* early return */
+       if (commit->object.flags & SEEN)
+               return 1;
+       if (commit->object.flags & INCOMPLETE)
+               return 0;
+       /*
+        * Find all commits that are reachable and are not marked as
+        * SEEN.  Then make sure the trees and blobs contained are
+        * complete.  After that, mark these commits also as SEEN.
+        * If some of the objects that are needed to complete this
+        * commit are missing, mark this commit as INCOMPLETE.
+        */
+       memset(&study, 0, sizeof(study));
+       memset(&found, 0, sizeof(found));
+       add_object_array(&commit->object, NULL, &study);
+       add_object_array(&commit->object, NULL, &found);
+       commit->object.flags |= STUDYING;
+       while (study.nr) {
+               struct commit *c;
+               struct commit_list *parent;
+
+               c = (struct commit *)study.objects[--study.nr].item;
+               if (!c->object.parsed && !parse_object(c->object.sha1))
+                       c->object.flags |= INCOMPLETE;
+
+               if (c->object.flags & INCOMPLETE) {
+                       is_incomplete = 1;
+                       break;
+               }
+               else if (c->object.flags & SEEN)
+                       continue;
+               for (parent = c->parents; parent; parent = parent->next) {
+                       struct commit *p = parent->item;
+                       if (p->object.flags & STUDYING)
+                               continue;
+                       p->object.flags |= STUDYING;
+                       add_object_array(&p->object, NULL, &study);
+                       add_object_array(&p->object, NULL, &found);
+               }
+       }
+       if (!is_incomplete) {
+               /*
+                * make sure all commits in "found" array have all the
+                * necessary objects.
+                */
+               for (i = 0; i < found.nr; i++) {
+                       struct commit *c =
+                               (struct commit *)found.objects[i].item;
+                       if (!tree_is_complete(c->tree->object.sha1)) {
+                               is_incomplete = 1;
+                               c->object.flags |= INCOMPLETE;
+                       }
+               }
+               if (!is_incomplete) {
+                       /* mark all found commits as complete, iow SEEN */
+                       for (i = 0; i < found.nr; i++)
+                               found.objects[i].item->flags |= SEEN;
+               }
+       }
+       /* clear flags from the objects we traversed */
+       for (i = 0; i < found.nr; i++)
+               found.objects[i].item->flags &= ~STUDYING;
+       if (is_incomplete)
+               commit->object.flags |= INCOMPLETE;
+       else {
+               /*
+                * If we come here, we have (1) traversed the ancestry chain
+                * from the "commit" until we reach SEEN commits (which are
+                * known to be complete), and (2) made sure that the commits
+                * encountered during the above traversal refer to trees that
+                * are complete.  Which means that we know *all* the commits
+                * we have seen during this process are complete.
+                */
+               for (i = 0; i < found.nr; i++)
+                       found.objects[i].item->flags |= SEEN;
+       }
+       /* free object arrays */
+       free(study.objects);
+       free(found.objects);
+       return !is_incomplete;
+}
+
+static int keep_entry(struct commit **it, unsigned char *sha1)
+{
+       struct commit *commit;
+
+       if (is_null_sha1(sha1))
+               return 1;
+       commit = lookup_commit_reference_gently(sha1, 1);
+       if (!commit)
+               return 0;
+
+       /*
+        * Make sure everything in this commit exists.
+        *
+        * We have walked all the objects reachable from the refs
+        * and cache earlier.  The commits reachable by this commit
+        * must meet SEEN commits -- and then we should mark them as
+        * SEEN as well.
+        */
+       if (!commit_is_complete(commit))
+               return 0;
+       *it = commit;
+       return 1;
+}
+
+static int expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
+               const char *email, unsigned long timestamp, int tz,
+               const char *message, void *cb_data)
+{
+       struct expire_reflog_cb *cb = cb_data;
+       struct commit *old, *new;
+
+       if (timestamp < cb->cmd->expire_total)
+               goto prune;
+
+       old = new = NULL;
+       if (cb->cmd->stalefix &&
+           (!keep_entry(&old, osha1) || !keep_entry(&new, nsha1)))
+               goto prune;
+
+       if (timestamp < cb->cmd->expire_unreachable) {
+               if (!cb->ref_commit)
+                       goto prune;
+               if (!old && !is_null_sha1(osha1))
+                       old = lookup_commit_reference_gently(osha1, 1);
+               if (!new && !is_null_sha1(nsha1))
+                       new = lookup_commit_reference_gently(nsha1, 1);
+               if ((old && !in_merge_bases(old, &cb->ref_commit, 1)) ||
+                   (new && !in_merge_bases(new, &cb->ref_commit, 1)))
+                       goto prune;
+       }
+
+       if (cb->newlog) {
+               char sign = (tz < 0) ? '-' : '+';
+               int zone = (tz < 0) ? (-tz) : tz;
+               fprintf(cb->newlog, "%s %s %s %lu %c%04d\t%s",
+                       sha1_to_hex(osha1), sha1_to_hex(nsha1),
+                       email, timestamp, sign, zone,
+                       message);
+       }
+       if (cb->cmd->verbose)
+               printf("keep %s", message);
+       return 0;
+ prune:
+       if (!cb->newlog || cb->cmd->verbose)
+               printf("%sprune %s", cb->newlog ? "" : "would ", message);
+       return 0;
+}
+
+static int expire_reflog(const char *ref, const unsigned char *sha1, int unused, void *cb_data)
+{
+       struct cmd_reflog_expire_cb *cmd = cb_data;
+       struct expire_reflog_cb cb;
+       struct ref_lock *lock;
+       char *log_file, *newlog_path = NULL;
+       int status = 0;
+
+       memset(&cb, 0, sizeof(cb));
+       /* we take the lock for the ref itself to prevent it from
+        * getting updated.
+        */
+       lock = lock_any_ref_for_update(ref, sha1, 0);
+       if (!lock)
+               return error("cannot lock ref '%s'", ref);
+       log_file = xstrdup(git_path("logs/%s", ref));
+       if (!file_exists(log_file))
+               goto finish;
+       if (!cmd->dry_run) {
+               newlog_path = xstrdup(git_path("logs/%s.lock", ref));
+               cb.newlog = fopen(newlog_path, "w");
+       }
+
+       cb.ref_commit = lookup_commit_reference_gently(sha1, 1);
+       cb.ref = ref;
+       cb.cmd = cmd;
+       for_each_reflog_ent(ref, expire_reflog_ent, &cb);
+ finish:
+       if (cb.newlog) {
+               if (fclose(cb.newlog))
+                       status |= error("%s: %s", strerror(errno),
+                                       newlog_path);
+               if (rename(newlog_path, log_file)) {
+                       status |= error("cannot rename %s to %s",
+                                       newlog_path, log_file);
+                       unlink(newlog_path);
+               }
+       }
+       free(newlog_path);
+       free(log_file);
+       unlock_ref(lock);
+       return status;
+}
+
+static int reflog_expire_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "gc.reflogexpire"))
+               default_reflog_expire = approxidate(value);
+       else if (!strcmp(var, "gc.reflogexpireunreachable"))
+               default_reflog_expire_unreachable = approxidate(value);
+       else
+               return git_default_config(var, value);
+       return 0;
+}
+
+static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
+{
+       struct cmd_reflog_expire_cb cb;
+       unsigned long now = time(NULL);
+       int i, status, do_all;
+
+       git_config(reflog_expire_config);
+
+       save_commit_buffer = 0;
+       do_all = status = 0;
+       memset(&cb, 0, sizeof(cb));
+
+       if (!default_reflog_expire_unreachable)
+               default_reflog_expire_unreachable = now - 30 * 24 * 3600;
+       if (!default_reflog_expire)
+               default_reflog_expire = now - 90 * 24 * 3600;
+       cb.expire_total = default_reflog_expire;
+       cb.expire_unreachable = default_reflog_expire_unreachable;
+
+       /*
+        * We can trust the commits and objects reachable from refs
+        * even in older repository.  We cannot trust what's reachable
+        * from reflog if the repository was pruned with older git.
+        */
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (!strcmp(arg, "--dry-run") || !strcmp(arg, "-n"))
+                       cb.dry_run = 1;
+               else if (!prefixcmp(arg, "--expire="))
+                       cb.expire_total = approxidate(arg + 9);
+               else if (!prefixcmp(arg, "--expire-unreachable="))
+                       cb.expire_unreachable = approxidate(arg + 21);
+               else if (!strcmp(arg, "--stale-fix"))
+                       cb.stalefix = 1;
+               else if (!strcmp(arg, "--all"))
+                       do_all = 1;
+               else if (!strcmp(arg, "--verbose"))
+                       cb.verbose = 1;
+               else if (!strcmp(arg, "--")) {
+                       i++;
+                       break;
+               }
+               else if (arg[0] == '-')
+                       usage(reflog_expire_usage);
+               else
+                       break;
+       }
+       if (cb.stalefix) {
+               init_revisions(&cb.revs, prefix);
+               if (cb.verbose)
+                       printf("Marking reachable objects...");
+               mark_reachable_objects(&cb.revs, 0);
+               if (cb.verbose)
+                       putchar('\n');
+       }
+
+       if (do_all)
+               status |= for_each_reflog(expire_reflog, &cb);
+       while (i < argc) {
+               const char *ref = argv[i++];
+               unsigned char sha1[20];
+               if (!resolve_ref(ref, sha1, 1, NULL)) {
+                       status |= error("%s points nowhere!", ref);
+                       continue;
+               }
+               status |= expire_reflog(ref, sha1, 0, &cb);
+       }
+       return status;
+}
+
+/*
+ * main "reflog"
+ */
+
+static const char reflog_usage[] =
+"git-reflog (expire | ...)";
+
+int cmd_reflog(int argc, const char **argv, const char *prefix)
+{
+       /* With no command, we default to showing it. */
+       if (argc < 2 || *argv[1] == '-')
+               return cmd_log_reflog(argc, argv, prefix);
+
+       if (!strcmp(argv[1], "show"))
+               return cmd_log_reflog(argc - 1, argv + 1, prefix);
+
+       if (!strcmp(argv[1], "expire"))
+               return cmd_reflog_expire(argc - 1, argv + 1, prefix);
+
+       /* Not a recognized reflog command..*/
+       usage(reflog_usage);
+}
diff --git a/builtin-rerere.c b/builtin-rerere.c
new file mode 100644 (file)
index 0000000..7449323
--- /dev/null
@@ -0,0 +1,454 @@
+#include "builtin.h"
+#include "cache.h"
+#include "path-list.h"
+#include "xdiff/xdiff.h"
+#include "xdiff-interface.h"
+
+#include <time.h>
+
+static const char git_rerere_usage[] =
+"git-rerere [clear | status | diff | gc]";
+
+/* these values are days */
+static int cutoff_noresolve = 15;
+static int cutoff_resolve = 60;
+
+/* if rerere_enabled == -1, fall back to detection of .git/rr-cache */
+static int rerere_enabled = -1;
+
+static char *merge_rr_path;
+
+static const char *rr_path(const char *name, const char *file)
+{
+       return git_path("rr-cache/%s/%s", name, file);
+}
+
+static void read_rr(struct path_list *rr)
+{
+       unsigned char sha1[20];
+       char buf[PATH_MAX];
+       FILE *in = fopen(merge_rr_path, "r");
+       if (!in)
+               return;
+       while (fread(buf, 40, 1, in) == 1) {
+               int i;
+               char *name;
+               if (get_sha1_hex(buf, sha1))
+                       die("corrupt MERGE_RR");
+               buf[40] = '\0';
+               name = xstrdup(buf);
+               if (fgetc(in) != '\t')
+                       die("corrupt MERGE_RR");
+               for (i = 0; i < sizeof(buf) && (buf[i] = fgetc(in)); i++)
+                       ; /* do nothing */
+               if (i == sizeof(buf))
+                       die("filename too long");
+               path_list_insert(buf, rr)->util = xstrdup(name);
+       }
+       fclose(in);
+}
+
+static struct lock_file write_lock;
+
+static int write_rr(struct path_list *rr, int out_fd)
+{
+       int i;
+       for (i = 0; i < rr->nr; i++) {
+               const char *path = rr->items[i].path;
+               int length = strlen(path) + 1;
+               if (write_in_full(out_fd, rr->items[i].util, 40) != 40 ||
+                   write_in_full(out_fd, "\t", 1) != 1 ||
+                   write_in_full(out_fd, path, length) != length)
+                       die("unable to write rerere record");
+       }
+       if (close(out_fd) != 0)
+               die("unable to write rerere record");
+       return commit_lock_file(&write_lock);
+}
+
+static int handle_file(const char *path,
+        unsigned char *sha1, const char *output)
+{
+       SHA_CTX ctx;
+       char buf[1024];
+       int hunk = 0, hunk_no = 0;
+       struct strbuf one, two;
+       FILE *f = fopen(path, "r");
+       FILE *out = NULL;
+
+       if (!f)
+               return error("Could not open %s", path);
+
+       if (output) {
+               out = fopen(output, "w");
+               if (!out) {
+                       fclose(f);
+                       return error("Could not write %s", output);
+               }
+       }
+
+       if (sha1)
+               SHA1_Init(&ctx);
+
+       strbuf_init(&one, 0);
+       strbuf_init(&two,  0);
+       while (fgets(buf, sizeof(buf), f)) {
+               if (!prefixcmp(buf, "<<<<<<< "))
+                       hunk = 1;
+               else if (!prefixcmp(buf, "======="))
+                       hunk = 2;
+               else if (!prefixcmp(buf, ">>>>>>> ")) {
+                       int cmp = strbuf_cmp(&one, &two);
+
+                       hunk_no++;
+                       hunk = 0;
+                       if (cmp > 0) {
+                               strbuf_swap(&one, &two);
+                       }
+                       if (out) {
+                               fputs("<<<<<<<\n", out);
+                               fwrite(one.buf, one.len, 1, out);
+                               fputs("=======\n", out);
+                               fwrite(two.buf, two.len, 1, out);
+                               fputs(">>>>>>>\n", out);
+                       }
+                       if (sha1) {
+                               SHA1_Update(&ctx, one.buf ? one.buf : "",
+                                           one.len + 1);
+                               SHA1_Update(&ctx, two.buf ? two.buf : "",
+                                           two.len + 1);
+                       }
+                       strbuf_reset(&one);
+                       strbuf_reset(&two);
+               } else if (hunk == 1)
+                       strbuf_addstr(&one, buf);
+               else if (hunk == 2)
+                       strbuf_addstr(&two, buf);
+               else if (out)
+                       fputs(buf, out);
+       }
+       strbuf_release(&one);
+       strbuf_release(&two);
+
+       fclose(f);
+       if (out)
+               fclose(out);
+       if (sha1)
+               SHA1_Final(sha1, &ctx);
+       return hunk_no;
+}
+
+static int find_conflict(struct path_list *conflict)
+{
+       int i;
+       if (read_cache() < 0)
+               return error("Could not read index");
+       for (i = 0; i+1 < active_nr; i++) {
+               struct cache_entry *e2 = active_cache[i];
+               struct cache_entry *e3 = active_cache[i+1];
+               if (ce_stage(e2) == 2 &&
+                   ce_stage(e3) == 3 &&
+                   ce_same_name(e2, e3) &&
+                   S_ISREG(ntohl(e2->ce_mode)) &&
+                   S_ISREG(ntohl(e3->ce_mode))) {
+                       path_list_insert((const char *)e2->name, conflict);
+                       i++; /* skip over both #2 and #3 */
+               }
+       }
+       return 0;
+}
+
+static int merge(const char *name, const char *path)
+{
+       int ret;
+       mmfile_t cur, base, other;
+       mmbuffer_t result = {NULL, 0};
+       xpparam_t xpp = {XDF_NEED_MINIMAL};
+
+       if (handle_file(path, NULL, rr_path(name, "thisimage")) < 0)
+               return 1;
+
+       if (read_mmfile(&cur, rr_path(name, "thisimage")) ||
+                       read_mmfile(&base, rr_path(name, "preimage")) ||
+                       read_mmfile(&other, rr_path(name, "postimage")))
+               return 1;
+       ret = xdl_merge(&base, &cur, "", &other, "",
+                       &xpp, XDL_MERGE_ZEALOUS, &result);
+       if (!ret) {
+               FILE *f = fopen(path, "w");
+               if (!f)
+                       return error("Could not write to %s", path);
+               fwrite(result.ptr, result.size, 1, f);
+               fclose(f);
+       }
+
+       free(cur.ptr);
+       free(base.ptr);
+       free(other.ptr);
+       free(result.ptr);
+
+       return ret;
+}
+
+static void unlink_rr_item(const char *name)
+{
+       unlink(rr_path(name, "thisimage"));
+       unlink(rr_path(name, "preimage"));
+       unlink(rr_path(name, "postimage"));
+       rmdir(git_path("rr-cache/%s", name));
+}
+
+static void garbage_collect(struct path_list *rr)
+{
+       struct path_list to_remove = { NULL, 0, 0, 1 };
+       char buf[1024];
+       DIR *dir;
+       struct dirent *e;
+       int len, i, cutoff;
+       time_t now = time(NULL), then;
+
+       strlcpy(buf, git_path("rr-cache"), sizeof(buf));
+       len = strlen(buf);
+       dir = opendir(buf);
+       strcpy(buf + len++, "/");
+       while ((e = readdir(dir))) {
+               const char *name = e->d_name;
+               struct stat st;
+               if (name[0] == '.' && (name[1] == '\0' ||
+                                       (name[1] == '.' && name[2] == '\0')))
+                       continue;
+               i = snprintf(buf + len, sizeof(buf) - len, "%s", name);
+               strlcpy(buf + len + i, "/preimage", sizeof(buf) - len - i);
+               if (stat(buf, &st))
+                       continue;
+               then = st.st_mtime;
+               strlcpy(buf + len + i, "/postimage", sizeof(buf) - len - i);
+               cutoff = stat(buf, &st) ? cutoff_noresolve : cutoff_resolve;
+               if (then < now - cutoff * 86400) {
+                       buf[len + i] = '\0';
+                       path_list_insert(xstrdup(name), &to_remove);
+               }
+       }
+       for (i = 0; i < to_remove.nr; i++)
+               unlink_rr_item(to_remove.items[i].path);
+       path_list_clear(&to_remove, 0);
+}
+
+static int outf(void *dummy, mmbuffer_t *ptr, int nbuf)
+{
+       int i;
+       for (i = 0; i < nbuf; i++)
+               if (write_in_full(1, ptr[i].ptr, ptr[i].size) != ptr[i].size)
+                       return -1;
+       return 0;
+}
+
+static int diff_two(const char *file1, const char *label1,
+               const char *file2, const char *label2)
+{
+       xpparam_t xpp;
+       xdemitconf_t xecfg;
+       xdemitcb_t ecb;
+       mmfile_t minus, plus;
+
+       if (read_mmfile(&minus, file1) || read_mmfile(&plus, file2))
+               return 1;
+
+       printf("--- a/%s\n+++ b/%s\n", label1, label2);
+       fflush(stdout);
+       xpp.flags = XDF_NEED_MINIMAL;
+       memset(&xecfg, 0, sizeof(xecfg));
+       xecfg.ctxlen = 3;
+       ecb.outf = outf;
+       xdl_diff(&minus, &plus, &xpp, &xecfg, &ecb);
+
+       free(minus.ptr);
+       free(plus.ptr);
+       return 0;
+}
+
+static int copy_file(const char *src, const char *dest)
+{
+       FILE *in, *out;
+       char buffer[32768];
+       int count;
+
+       if (!(in = fopen(src, "r")))
+               return error("Could not open %s", src);
+       if (!(out = fopen(dest, "w")))
+               return error("Could not open %s", dest);
+       while ((count = fread(buffer, 1, sizeof(buffer), in)))
+               fwrite(buffer, 1, count, out);
+       fclose(in);
+       fclose(out);
+       return 0;
+}
+
+static int do_plain_rerere(struct path_list *rr, int fd)
+{
+       struct path_list conflict = { NULL, 0, 0, 1 };
+       int i;
+
+       find_conflict(&conflict);
+
+       /*
+        * MERGE_RR records paths with conflicts immediately after merge
+        * failed.  Some of the conflicted paths might have been hand resolved
+        * in the working tree since then, but the initial run would catch all
+        * and register their preimages.
+        */
+
+       for (i = 0; i < conflict.nr; i++) {
+               const char *path = conflict.items[i].path;
+               if (!path_list_has_path(rr, path)) {
+                       unsigned char sha1[20];
+                       char *hex;
+                       int ret;
+                       ret = handle_file(path, sha1, NULL);
+                       if (ret < 1)
+                               continue;
+                       hex = xstrdup(sha1_to_hex(sha1));
+                       path_list_insert(path, rr)->util = hex;
+                       if (mkdir(git_path("rr-cache/%s", hex), 0755))
+                               continue;;
+                       handle_file(path, NULL, rr_path(hex, "preimage"));
+                       fprintf(stderr, "Recorded preimage for '%s'\n", path);
+               }
+       }
+
+       /*
+        * Now some of the paths that had conflicts earlier might have been
+        * hand resolved.  Others may be similar to a conflict already that
+        * was resolved before.
+        */
+
+       for (i = 0; i < rr->nr; i++) {
+               struct stat st;
+               int ret;
+               const char *path = rr->items[i].path;
+               const char *name = (const char *)rr->items[i].util;
+
+               if (!stat(rr_path(name, "preimage"), &st) &&
+                               !stat(rr_path(name, "postimage"), &st)) {
+                       if (!merge(name, path)) {
+                               fprintf(stderr, "Resolved '%s' using "
+                                               "previous resolution.\n", path);
+                               goto tail_optimization;
+                       }
+               }
+
+               /* Let's see if we have resolved it. */
+               ret = handle_file(path, NULL, NULL);
+               if (ret)
+                       continue;
+
+               fprintf(stderr, "Recorded resolution for '%s'.\n", path);
+               copy_file(path, rr_path(name, "postimage"));
+tail_optimization:
+               if (i < rr->nr - 1)
+                       memmove(rr->items + i,
+                               rr->items + i + 1,
+                               sizeof(rr->items[0]) * (rr->nr - i - 1));
+               rr->nr--;
+               i--;
+       }
+
+       return write_rr(rr, fd);
+}
+
+static int git_rerere_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "gc.rerereresolved"))
+               cutoff_resolve = git_config_int(var, value);
+       else if (!strcmp(var, "gc.rerereunresolved"))
+               cutoff_noresolve = git_config_int(var, value);
+       else if (!strcmp(var, "rerere.enabled"))
+               rerere_enabled = git_config_bool(var, value);
+       else
+               return git_default_config(var, value);
+       return 0;
+}
+
+static int is_rerere_enabled(void)
+{
+       struct stat st;
+       const char *rr_cache;
+       int rr_cache_exists;
+
+       if (!rerere_enabled)
+               return 0;
+
+       rr_cache = git_path("rr-cache");
+       rr_cache_exists = !stat(rr_cache, &st) && S_ISDIR(st.st_mode);
+       if (rerere_enabled < 0)
+               return rr_cache_exists;
+
+       if (!rr_cache_exists &&
+           (mkdir(rr_cache, 0777) || adjust_shared_perm(rr_cache)))
+               die("Could not create directory %s", rr_cache);
+       return 1;
+}
+
+static int setup_rerere(struct path_list *merge_rr)
+{
+       int fd;
+
+       git_config(git_rerere_config);
+       if (!is_rerere_enabled())
+               return -1;
+
+       merge_rr_path = xstrdup(git_path("rr-cache/MERGE_RR"));
+       fd = hold_lock_file_for_update(&write_lock, merge_rr_path, 1);
+       read_rr(merge_rr);
+       return fd;
+}
+
+int rerere(void)
+{
+       struct path_list merge_rr = { NULL, 0, 0, 1 };
+       int fd;
+
+       fd = setup_rerere(&merge_rr);
+       if (fd < 0)
+               return 0;
+       return do_plain_rerere(&merge_rr, fd);
+}
+
+int cmd_rerere(int argc, const char **argv, const char *prefix)
+{
+       struct path_list merge_rr = { NULL, 0, 0, 1 };
+       int i, fd;
+
+       fd = setup_rerere(&merge_rr);
+       if (fd < 0)
+               return 0;
+
+       if (argc < 2)
+               return do_plain_rerere(&merge_rr, fd);
+       else if (!strcmp(argv[1], "clear")) {
+               for (i = 0; i < merge_rr.nr; i++) {
+                       struct stat st;
+                       const char *name = (const char *)merge_rr.items[i].util;
+                       if (!stat(git_path("rr-cache/%s", name), &st) &&
+                                       S_ISDIR(st.st_mode) &&
+                                       stat(rr_path(name, "postimage"), &st))
+                               unlink_rr_item(name);
+               }
+               unlink(merge_rr_path);
+       } else if (!strcmp(argv[1], "gc"))
+               garbage_collect(&merge_rr);
+       else if (!strcmp(argv[1], "status"))
+               for (i = 0; i < merge_rr.nr; i++)
+                       printf("%s\n", merge_rr.items[i].path);
+       else if (!strcmp(argv[1], "diff"))
+               for (i = 0; i < merge_rr.nr; i++) {
+                       const char *path = merge_rr.items[i].path;
+                       const char *name = (const char *)merge_rr.items[i].util;
+                       diff_two(rr_path(name, "preimage"), path, path, path);
+               }
+       else
+               usage(git_rerere_usage);
+
+       path_list_clear(&merge_rr, 1);
+       return 0;
+}
diff --git a/builtin-reset.c b/builtin-reset.c
new file mode 100644 (file)
index 0000000..4c61025
--- /dev/null
@@ -0,0 +1,290 @@
+/*
+ * "git reset" builtin command
+ *
+ * Copyright (c) 2007 Carlos Rica
+ *
+ * Based on git-reset.sh, which is
+ *
+ * Copyright (c) 2005, 2006 Linus Torvalds and Junio C Hamano
+ */
+#include "cache.h"
+#include "tag.h"
+#include "object.h"
+#include "commit.h"
+#include "run-command.h"
+#include "refs.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "tree.h"
+
+static const char builtin_reset_usage[] =
+"git-reset [--mixed | --soft | --hard] [-q] [<commit-ish>] [ [--] <paths>...]";
+
+static char *args_to_str(const char **argv)
+{
+       char *buf = NULL;
+       unsigned long len, space = 0, nr = 0;
+
+       for (; *argv; argv++) {
+               len = strlen(*argv);
+               ALLOC_GROW(buf, nr + 1 + len, space);
+               if (nr)
+                       buf[nr++] = ' ';
+               memcpy(buf + nr, *argv, len);
+               nr += len;
+       }
+       ALLOC_GROW(buf, nr + 1, space);
+       buf[nr] = '\0';
+
+       return buf;
+}
+
+static inline int is_merge(void)
+{
+       return !access(git_path("MERGE_HEAD"), F_OK);
+}
+
+static int unmerged_files(void)
+{
+       int i;
+       read_cache();
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (ce_stage(ce))
+                       return 1;
+       }
+       return 0;
+}
+
+static int reset_index_file(const unsigned char *sha1, int is_hard_reset)
+{
+       int i = 0;
+       const char *args[6];
+
+       args[i++] = "read-tree";
+       args[i++] = "-v";
+       args[i++] = "--reset";
+       if (is_hard_reset)
+               args[i++] = "-u";
+       args[i++] = sha1_to_hex(sha1);
+       args[i] = NULL;
+
+       return run_command_v_opt(args, RUN_GIT_CMD);
+}
+
+static void print_new_head_line(struct commit *commit)
+{
+       const char *hex, *dots = "...", *body;
+
+       hex = find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV);
+       if (!hex) {
+               hex = sha1_to_hex(commit->object.sha1);
+               dots = "";
+       }
+       printf("HEAD is now at %s%s", hex, dots);
+       body = strstr(commit->buffer, "\n\n");
+       if (body) {
+               const char *eol;
+               size_t len;
+               body += 2;
+               eol = strchr(body, '\n');
+               len = eol ? eol - body : strlen(body);
+               printf(" %.*s\n", (int) len, body);
+       }
+       else
+               printf("\n");
+}
+
+static int update_index_refresh(int fd, struct lock_file *index_lock)
+{
+       int result;
+
+       if (!index_lock) {
+               index_lock = xcalloc(1, sizeof(struct lock_file));
+               fd = hold_locked_index(index_lock, 1);
+       }
+
+       if (read_cache() < 0)
+               return error("Could not read index");
+       result = refresh_cache(0) ? 1 : 0;
+       if (write_cache(fd, active_cache, active_nr) ||
+                       close(fd) ||
+                       commit_locked_index(index_lock))
+               return error ("Could not refresh index");
+       return result;
+}
+
+static void update_index_from_diff(struct diff_queue_struct *q,
+               struct diff_options *opt, void *data)
+{
+       int i;
+       int *discard_flag = data;
+
+       /* do_diff_cache() mangled the index */
+       discard_cache();
+       *discard_flag = 1;
+       read_cache();
+
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filespec *one = q->queue[i]->one;
+               if (one->mode) {
+                       struct cache_entry *ce;
+                       ce = make_cache_entry(one->mode, one->sha1, one->path,
+                               0, 0);
+                       add_cache_entry(ce, ADD_CACHE_OK_TO_ADD |
+                               ADD_CACHE_OK_TO_REPLACE);
+               } else
+                       remove_file_from_cache(one->path);
+       }
+}
+
+static int read_from_tree(const char *prefix, const char **argv,
+               unsigned char *tree_sha1)
+{
+       struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
+       int index_fd, index_was_discarded = 0;
+       struct diff_options opt;
+
+       memset(&opt, 0, sizeof(opt));
+       diff_tree_setup_paths(get_pathspec(prefix, (const char **)argv), &opt);
+       opt.output_format = DIFF_FORMAT_CALLBACK;
+       opt.format_callback = update_index_from_diff;
+       opt.format_callback_data = &index_was_discarded;
+
+       index_fd = hold_locked_index(lock, 1);
+       index_was_discarded = 0;
+       read_cache();
+       if (do_diff_cache(tree_sha1, &opt))
+               return 1;
+       diffcore_std(&opt);
+       diff_flush(&opt);
+
+       if (!index_was_discarded)
+               /* The index is still clobbered from do_diff_cache() */
+               discard_cache();
+       return update_index_refresh(index_fd, lock);
+}
+
+static void prepend_reflog_action(const char *action, char *buf, size_t size)
+{
+       const char *sep = ": ";
+       const char *rla = getenv("GIT_REFLOG_ACTION");
+       if (!rla)
+               rla = sep = "";
+       if (snprintf(buf, size, "%s%s%s", rla, sep, action) >= size)
+               warning("Reflog action message too long: %.*s...", 50, buf);
+}
+
+enum reset_type { MIXED, SOFT, HARD, NONE };
+static const char *reset_type_names[] = { "mixed", "soft", "hard", NULL };
+
+int cmd_reset(int argc, const char **argv, const char *prefix)
+{
+       int i = 1, reset_type = NONE, update_ref_status = 0, quiet = 0;
+       const char *rev = "HEAD";
+       unsigned char sha1[20], *orig = NULL, sha1_orig[20],
+                               *old_orig = NULL, sha1_old_orig[20];
+       struct commit *commit;
+       char *reflog_action, msg[1024];
+
+       git_config(git_default_config);
+
+       reflog_action = args_to_str(argv);
+       setenv("GIT_REFLOG_ACTION", reflog_action, 0);
+
+       while (i < argc) {
+               if (!strcmp(argv[i], "--mixed")) {
+                       reset_type = MIXED;
+                       i++;
+               }
+               else if (!strcmp(argv[i], "--soft")) {
+                       reset_type = SOFT;
+                       i++;
+               }
+               else if (!strcmp(argv[i], "--hard")) {
+                       reset_type = HARD;
+                       i++;
+               }
+               else if (!strcmp(argv[i], "-q")) {
+                       quiet = 1;
+                       i++;
+               }
+               else
+                       break;
+       }
+
+       if (i < argc && argv[i][0] != '-')
+               rev = argv[i++];
+
+       if (get_sha1(rev, sha1))
+               die("Failed to resolve '%s' as a valid ref.", rev);
+
+       commit = lookup_commit_reference(sha1);
+       if (!commit)
+               die("Could not parse object '%s'.", rev);
+       hashcpy(sha1, commit->object.sha1);
+
+       if (i < argc && !strcmp(argv[i], "--"))
+               i++;
+       else if (i < argc && argv[i][0] == '-')
+               usage(builtin_reset_usage);
+
+       /* git reset tree [--] paths... can be used to
+        * load chosen paths from the tree into the index without
+        * affecting the working tree nor HEAD. */
+       if (i < argc) {
+               if (reset_type == MIXED)
+                       warning("--mixed option is deprecated with paths.");
+               else if (reset_type != NONE)
+                       die("Cannot do %s reset with paths.",
+                                       reset_type_names[reset_type]);
+               return read_from_tree(prefix, argv + i, sha1);
+       }
+       if (reset_type == NONE)
+               reset_type = MIXED; /* by default */
+
+       /* Soft reset does not touch the index file nor the working tree
+        * at all, but requires them in a good order.  Other resets reset
+        * the index file to the tree object we are switching to. */
+       if (reset_type == SOFT) {
+               if (is_merge() || unmerged_files())
+                       die("Cannot do a soft reset in the middle of a merge.");
+       }
+       else if (reset_index_file(sha1, (reset_type == HARD)))
+               die("Could not reset index file to revision '%s'.", rev);
+
+       /* Any resets update HEAD to the head being switched to,
+        * saving the previous head in ORIG_HEAD before. */
+       if (!get_sha1("ORIG_HEAD", sha1_old_orig))
+               old_orig = sha1_old_orig;
+       if (!get_sha1("HEAD", sha1_orig)) {
+               orig = sha1_orig;
+               prepend_reflog_action("updating ORIG_HEAD", msg, sizeof(msg));
+               update_ref(msg, "ORIG_HEAD", orig, old_orig, 0, MSG_ON_ERR);
+       }
+       else if (old_orig)
+               delete_ref("ORIG_HEAD", old_orig);
+       prepend_reflog_action("updating HEAD", msg, sizeof(msg));
+       update_ref_status = update_ref(msg, "HEAD", sha1, orig, 0, MSG_ON_ERR);
+
+       switch (reset_type) {
+       case HARD:
+               if (!update_ref_status && !quiet)
+                       print_new_head_line(commit);
+               break;
+       case SOFT: /* Nothing else to do. */
+               break;
+       case MIXED: /* Report what has not been updated. */
+               update_index_refresh(0, NULL);
+               break;
+       }
+
+       unlink(git_path("MERGE_HEAD"));
+       unlink(git_path("rr-cache/MERGE_RR"));
+       unlink(git_path("MERGE_MSG"));
+       unlink(git_path("SQUASH_MSG"));
+
+       free(reflog_action);
+
+       return update_ref_status;
+}
diff --git a/builtin-rev-list.c b/builtin-rev-list.c
new file mode 100644 (file)
index 0000000..1cb5f67
--- /dev/null
@@ -0,0 +1,664 @@
+#include "cache.h"
+#include "refs.h"
+#include "tag.h"
+#include "commit.h"
+#include "tree.h"
+#include "blob.h"
+#include "tree-walk.h"
+#include "diff.h"
+#include "revision.h"
+#include "list-objects.h"
+#include "builtin.h"
+#include "log-tree.h"
+
+/* bits #0-15 in revision.h */
+
+#define COUNTED                (1u<<16)
+
+static const char rev_list_usage[] =
+"git-rev-list [OPTION] <commit-id>... [ -- paths... ]\n"
+"  limiting output:\n"
+"    --max-count=nr\n"
+"    --max-age=epoch\n"
+"    --min-age=epoch\n"
+"    --sparse\n"
+"    --no-merges\n"
+"    --remove-empty\n"
+"    --all\n"
+"    --stdin\n"
+"    --quiet\n"
+"  ordering output:\n"
+"    --topo-order\n"
+"    --date-order\n"
+"  formatting output:\n"
+"    --parents\n"
+"    --objects | --objects-edge\n"
+"    --unpacked\n"
+"    --header | --pretty\n"
+"    --abbrev=nr | --no-abbrev\n"
+"    --abbrev-commit\n"
+"    --left-right\n"
+"  special purpose:\n"
+"    --bisect\n"
+"    --bisect-vars\n"
+"    --bisect-all"
+;
+
+static struct rev_info revs;
+
+static int bisect_list;
+static int show_timestamp;
+static int hdr_termination;
+static const char *header_prefix;
+
+static void finish_commit(struct commit *commit);
+static void show_commit(struct commit *commit)
+{
+       if (show_timestamp)
+               printf("%lu ", commit->date);
+       if (header_prefix)
+               fputs(header_prefix, stdout);
+       if (commit->object.flags & BOUNDARY)
+               putchar('-');
+       else if (revs.left_right) {
+               if (commit->object.flags & SYMMETRIC_LEFT)
+                       putchar('<');
+               else
+                       putchar('>');
+       }
+       if (revs.abbrev_commit && revs.abbrev)
+               fputs(find_unique_abbrev(commit->object.sha1, revs.abbrev),
+                     stdout);
+       else
+               fputs(sha1_to_hex(commit->object.sha1), stdout);
+       if (revs.parents) {
+               struct commit_list *parents = commit->parents;
+               while (parents) {
+                       printf(" %s", sha1_to_hex(parents->item->object.sha1));
+                       parents = parents->next;
+               }
+       }
+       show_decorations(commit);
+       if (revs.commit_format == CMIT_FMT_ONELINE)
+               putchar(' ');
+       else
+               putchar('\n');
+
+       if (revs.verbose_header) {
+               struct strbuf buf;
+               strbuf_init(&buf, 0);
+               pretty_print_commit(revs.commit_format, commit,
+                                   &buf, revs.abbrev, NULL, NULL,
+                                   revs.date_mode, 0);
+               if (buf.len)
+                       printf("%s%c", buf.buf, hdr_termination);
+               strbuf_release(&buf);
+       }
+       maybe_flush_or_die(stdout, "stdout");
+       finish_commit(commit);
+}
+
+static void finish_commit(struct commit *commit)
+{
+       if (commit->parents) {
+               free_commit_list(commit->parents);
+               commit->parents = NULL;
+       }
+       free(commit->buffer);
+       commit->buffer = NULL;
+}
+
+static void finish_object(struct object_array_entry *p)
+{
+       if (p->item->type == OBJ_BLOB && !has_sha1_file(p->item->sha1))
+               die("missing blob object '%s'", sha1_to_hex(p->item->sha1));
+}
+
+static void show_object(struct object_array_entry *p)
+{
+       /* An object with name "foo\n0000000..." can be used to
+        * confuse downstream git-pack-objects very badly.
+        */
+       const char *ep = strchr(p->name, '\n');
+
+       finish_object(p);
+       if (ep) {
+               printf("%s %.*s\n", sha1_to_hex(p->item->sha1),
+                      (int) (ep - p->name),
+                      p->name);
+       }
+       else
+               printf("%s %s\n", sha1_to_hex(p->item->sha1), p->name);
+}
+
+static void show_edge(struct commit *commit)
+{
+       printf("-%s\n", sha1_to_hex(commit->object.sha1));
+}
+
+/*
+ * This is a truly stupid algorithm, but it's only
+ * used for bisection, and we just don't care enough.
+ *
+ * We care just barely enough to avoid recursing for
+ * non-merge entries.
+ */
+static int count_distance(struct commit_list *entry)
+{
+       int nr = 0;
+
+       while (entry) {
+               struct commit *commit = entry->item;
+               struct commit_list *p;
+
+               if (commit->object.flags & (UNINTERESTING | COUNTED))
+                       break;
+               if (!(commit->object.flags & TREESAME))
+                       nr++;
+               commit->object.flags |= COUNTED;
+               p = commit->parents;
+               entry = p;
+               if (p) {
+                       p = p->next;
+                       while (p) {
+                               nr += count_distance(p);
+                               p = p->next;
+                       }
+               }
+       }
+
+       return nr;
+}
+
+static void clear_distance(struct commit_list *list)
+{
+       while (list) {
+               struct commit *commit = list->item;
+               commit->object.flags &= ~COUNTED;
+               list = list->next;
+       }
+}
+
+#define DEBUG_BISECT 0
+
+static inline int weight(struct commit_list *elem)
+{
+       return *((int*)(elem->item->util));
+}
+
+static inline void weight_set(struct commit_list *elem, int weight)
+{
+       *((int*)(elem->item->util)) = weight;
+}
+
+static int count_interesting_parents(struct commit *commit)
+{
+       struct commit_list *p;
+       int count;
+
+       for (count = 0, p = commit->parents; p; p = p->next) {
+               if (p->item->object.flags & UNINTERESTING)
+                       continue;
+               count++;
+       }
+       return count;
+}
+
+static inline int halfway(struct commit_list *p, int nr)
+{
+       /*
+        * Don't short-cut something we are not going to return!
+        */
+       if (p->item->object.flags & TREESAME)
+               return 0;
+       if (DEBUG_BISECT)
+               return 0;
+       /*
+        * 2 and 3 are halfway of 5.
+        * 3 is halfway of 6 but 2 and 4 are not.
+        */
+       switch (2 * weight(p) - nr) {
+       case -1: case 0: case 1:
+               return 1;
+       default:
+               return 0;
+       }
+}
+
+#if !DEBUG_BISECT
+#define show_list(a,b,c,d) do { ; } while (0)
+#else
+static void show_list(const char *debug, int counted, int nr,
+                     struct commit_list *list)
+{
+       struct commit_list *p;
+
+       fprintf(stderr, "%s (%d/%d)\n", debug, counted, nr);
+
+       for (p = list; p; p = p->next) {
+               struct commit_list *pp;
+               struct commit *commit = p->item;
+               unsigned flags = commit->object.flags;
+               enum object_type type;
+               unsigned long size;
+               char *buf = read_sha1_file(commit->object.sha1, &type, &size);
+               char *ep, *sp;
+
+               fprintf(stderr, "%c%c%c ",
+                       (flags & TREESAME) ? ' ' : 'T',
+                       (flags & UNINTERESTING) ? 'U' : ' ',
+                       (flags & COUNTED) ? 'C' : ' ');
+               if (commit->util)
+                       fprintf(stderr, "%3d", weight(p));
+               else
+                       fprintf(stderr, "---");
+               fprintf(stderr, " %.*s", 8, sha1_to_hex(commit->object.sha1));
+               for (pp = commit->parents; pp; pp = pp->next)
+                       fprintf(stderr, " %.*s", 8,
+                               sha1_to_hex(pp->item->object.sha1));
+
+               sp = strstr(buf, "\n\n");
+               if (sp) {
+                       sp += 2;
+                       for (ep = sp; *ep && *ep != '\n'; ep++)
+                               ;
+                       fprintf(stderr, " %.*s", (int)(ep - sp), sp);
+               }
+               fprintf(stderr, "\n");
+       }
+}
+#endif /* DEBUG_BISECT */
+
+static struct commit_list *best_bisection(struct commit_list *list, int nr)
+{
+       struct commit_list *p, *best;
+       int best_distance = -1;
+
+       best = list;
+       for (p = list; p; p = p->next) {
+               int distance;
+               unsigned flags = p->item->object.flags;
+
+               if (flags & TREESAME)
+                       continue;
+               distance = weight(p);
+               if (nr - distance < distance)
+                       distance = nr - distance;
+               if (distance > best_distance) {
+                       best = p;
+                       best_distance = distance;
+               }
+       }
+
+       return best;
+}
+
+struct commit_dist {
+       struct commit *commit;
+       int distance;
+};
+
+static int compare_commit_dist(const void *a_, const void *b_)
+{
+       struct commit_dist *a, *b;
+
+       a = (struct commit_dist *)a_;
+       b = (struct commit_dist *)b_;
+       if (a->distance != b->distance)
+               return b->distance - a->distance; /* desc sort */
+       return hashcmp(a->commit->object.sha1, b->commit->object.sha1);
+}
+
+static struct commit_list *best_bisection_sorted(struct commit_list *list, int nr)
+{
+       struct commit_list *p;
+       struct commit_dist *array = xcalloc(nr, sizeof(*array));
+       int cnt, i;
+
+       for (p = list, cnt = 0; p; p = p->next) {
+               int distance;
+               unsigned flags = p->item->object.flags;
+
+               if (flags & TREESAME)
+                       continue;
+               distance = weight(p);
+               if (nr - distance < distance)
+                       distance = nr - distance;
+               array[cnt].commit = p->item;
+               array[cnt].distance = distance;
+               cnt++;
+       }
+       qsort(array, cnt, sizeof(*array), compare_commit_dist);
+       for (p = list, i = 0; i < cnt; i++) {
+               struct name_decoration *r = xmalloc(sizeof(*r) + 100);
+               struct object *obj = &(array[i].commit->object);
+
+               sprintf(r->name, "dist=%d", array[i].distance);
+               r->next = add_decoration(&name_decoration, obj, r);
+               p->item = array[i].commit;
+               p = p->next;
+       }
+       if (p)
+               p->next = NULL;
+       free(array);
+       return list;
+}
+
+/*
+ * zero or positive weight is the number of interesting commits it can
+ * reach, including itself.  Especially, weight = 0 means it does not
+ * reach any tree-changing commits (e.g. just above uninteresting one
+ * but traversal is with pathspec).
+ *
+ * weight = -1 means it has one parent and its distance is yet to
+ * be computed.
+ *
+ * weight = -2 means it has more than one parent and its distance is
+ * unknown.  After running count_distance() first, they will get zero
+ * or positive distance.
+ */
+static struct commit_list *do_find_bisection(struct commit_list *list,
+                                            int nr, int *weights,
+                                            int find_all)
+{
+       int n, counted;
+       struct commit_list *p;
+
+       counted = 0;
+
+       for (n = 0, p = list; p; p = p->next) {
+               struct commit *commit = p->item;
+               unsigned flags = commit->object.flags;
+
+               p->item->util = &weights[n++];
+               switch (count_interesting_parents(commit)) {
+               case 0:
+                       if (!(flags & TREESAME)) {
+                               weight_set(p, 1);
+                               counted++;
+                               show_list("bisection 2 count one",
+                                         counted, nr, list);
+                       }
+                       /*
+                        * otherwise, it is known not to reach any
+                        * tree-changing commit and gets weight 0.
+                        */
+                       break;
+               case 1:
+                       weight_set(p, -1);
+                       break;
+               default:
+                       weight_set(p, -2);
+                       break;
+               }
+       }
+
+       show_list("bisection 2 initialize", counted, nr, list);
+
+       /*
+        * If you have only one parent in the resulting set
+        * then you can reach one commit more than that parent
+        * can reach.  So we do not have to run the expensive
+        * count_distance() for single strand of pearls.
+        *
+        * However, if you have more than one parents, you cannot
+        * just add their distance and one for yourself, since
+        * they usually reach the same ancestor and you would
+        * end up counting them twice that way.
+        *
+        * So we will first count distance of merges the usual
+        * way, and then fill the blanks using cheaper algorithm.
+        */
+       for (p = list; p; p = p->next) {
+               if (p->item->object.flags & UNINTERESTING)
+                       continue;
+               if (weight(p) != -2)
+                       continue;
+               weight_set(p, count_distance(p));
+               clear_distance(list);
+
+               /* Does it happen to be at exactly half-way? */
+               if (!find_all && halfway(p, nr))
+                       return p;
+               counted++;
+       }
+
+       show_list("bisection 2 count_distance", counted, nr, list);
+
+       while (counted < nr) {
+               for (p = list; p; p = p->next) {
+                       struct commit_list *q;
+                       unsigned flags = p->item->object.flags;
+
+                       if (0 <= weight(p))
+                               continue;
+                       for (q = p->item->parents; q; q = q->next) {
+                               if (q->item->object.flags & UNINTERESTING)
+                                       continue;
+                               if (0 <= weight(q))
+                                       break;
+                       }
+                       if (!q)
+                               continue;
+
+                       /*
+                        * weight for p is unknown but q is known.
+                        * add one for p itself if p is to be counted,
+                        * otherwise inherit it from q directly.
+                        */
+                       if (!(flags & TREESAME)) {
+                               weight_set(p, weight(q)+1);
+                               counted++;
+                               show_list("bisection 2 count one",
+                                         counted, nr, list);
+                       }
+                       else
+                               weight_set(p, weight(q));
+
+                       /* Does it happen to be at exactly half-way? */
+                       if (!find_all && halfway(p, nr))
+                               return p;
+               }
+       }
+
+       show_list("bisection 2 counted all", counted, nr, list);
+
+       if (!find_all)
+               return best_bisection(list, nr);
+       else
+               return best_bisection_sorted(list, nr);
+}
+
+static struct commit_list *find_bisection(struct commit_list *list,
+                                         int *reaches, int *all,
+                                         int find_all)
+{
+       int nr, on_list;
+       struct commit_list *p, *best, *next, *last;
+       int *weights;
+
+       show_list("bisection 2 entry", 0, 0, list);
+
+       /*
+        * Count the number of total and tree-changing items on the
+        * list, while reversing the list.
+        */
+       for (nr = on_list = 0, last = NULL, p = list;
+            p;
+            p = next) {
+               unsigned flags = p->item->object.flags;
+
+               next = p->next;
+               if (flags & UNINTERESTING)
+                       continue;
+               p->next = last;
+               last = p;
+               if (!(flags & TREESAME))
+                       nr++;
+               on_list++;
+       }
+       list = last;
+       show_list("bisection 2 sorted", 0, nr, list);
+
+       *all = nr;
+       weights = xcalloc(on_list, sizeof(*weights));
+
+       /* Do the real work of finding bisection commit. */
+       best = do_find_bisection(list, nr, weights, find_all);
+       if (best) {
+               if (!find_all)
+                       best->next = NULL;
+               *reaches = weight(best);
+       }
+       free(weights);
+       return best;
+}
+
+static void read_revisions_from_stdin(struct rev_info *revs)
+{
+       char line[1000];
+
+       while (fgets(line, sizeof(line), stdin) != NULL) {
+               int len = strlen(line);
+               if (line[len - 1] == '\n')
+                       line[--len] = 0;
+               if (!len)
+                       break;
+               if (line[0] == '-')
+                       die("options not supported in --stdin mode");
+               if (handle_revision_arg(line, revs, 0, 1))
+                       die("bad revision '%s'", line);
+       }
+}
+
+int cmd_rev_list(int argc, const char **argv, const char *prefix)
+{
+       struct commit_list *list;
+       int i;
+       int read_from_stdin = 0;
+       int bisect_show_vars = 0;
+       int bisect_find_all = 0;
+       int quiet = 0;
+
+       git_config(git_default_config);
+       init_revisions(&revs, prefix);
+       revs.abbrev = 0;
+       revs.commit_format = CMIT_FMT_UNSPECIFIED;
+       argc = setup_revisions(argc, argv, &revs, NULL);
+
+       for (i = 1 ; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (!strcmp(arg, "--header")) {
+                       revs.verbose_header = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--timestamp")) {
+                       show_timestamp = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--bisect")) {
+                       bisect_list = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--bisect-all")) {
+                       bisect_list = 1;
+                       bisect_find_all = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--bisect-vars")) {
+                       bisect_list = 1;
+                       bisect_show_vars = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--stdin")) {
+                       if (read_from_stdin++)
+                               die("--stdin given twice?");
+                       read_revisions_from_stdin(&revs);
+                       continue;
+               }
+               if (!strcmp(arg, "--quiet")) {
+                       quiet = 1;
+                       continue;
+               }
+               usage(rev_list_usage);
+
+       }
+       if (revs.commit_format != CMIT_FMT_UNSPECIFIED) {
+               /* The command line has a --pretty  */
+               hdr_termination = '\n';
+               if (revs.commit_format == CMIT_FMT_ONELINE)
+                       header_prefix = "";
+               else
+                       header_prefix = "commit ";
+       }
+       else if (revs.verbose_header)
+               /* Only --header was specified */
+               revs.commit_format = CMIT_FMT_RAW;
+
+       list = revs.commits;
+
+       if ((!list &&
+            (!(revs.tag_objects||revs.tree_objects||revs.blob_objects) &&
+             !revs.pending.nr)) ||
+           revs.diff)
+               usage(rev_list_usage);
+
+       save_commit_buffer = revs.verbose_header || revs.grep_filter;
+       track_object_refs = 0;
+       if (bisect_list)
+               revs.limited = 1;
+
+       prepare_revision_walk(&revs);
+       if (revs.tree_objects)
+               mark_edges_uninteresting(revs.commits, &revs, show_edge);
+
+       if (bisect_list) {
+               int reaches = reaches, all = all;
+
+               revs.commits = find_bisection(revs.commits, &reaches, &all,
+                                             bisect_find_all);
+               if (bisect_show_vars) {
+                       int cnt;
+                       char hex[41];
+                       if (!revs.commits)
+                               return 1;
+                       /*
+                        * revs.commits can reach "reaches" commits among
+                        * "all" commits.  If it is good, then there are
+                        * (all-reaches) commits left to be bisected.
+                        * On the other hand, if it is bad, then the set
+                        * to bisect is "reaches".
+                        * A bisect set of size N has (N-1) commits further
+                        * to test, as we already know one bad one.
+                        */
+                       cnt = all - reaches;
+                       if (cnt < reaches)
+                               cnt = reaches;
+                       strcpy(hex, sha1_to_hex(revs.commits->item->object.sha1));
+
+                       if (bisect_find_all) {
+                               traverse_commit_list(&revs, show_commit, show_object);
+                               printf("------\n");
+                       }
+
+                       printf("bisect_rev=%s\n"
+                              "bisect_nr=%d\n"
+                              "bisect_good=%d\n"
+                              "bisect_bad=%d\n"
+                              "bisect_all=%d\n",
+                              hex,
+                              cnt - 1,
+                              all - reaches - 1,
+                              reaches - 1,
+                              all);
+                       return 0;
+               }
+       }
+
+       traverse_commit_list(&revs,
+               quiet ? finish_commit : show_commit,
+               quiet ? finish_object : show_object);
+
+       return 0;
+}
diff --git a/builtin-rev-parse.c b/builtin-rev-parse.c
new file mode 100644 (file)
index 0000000..20d1789
--- /dev/null
@@ -0,0 +1,541 @@
+/*
+ * rev-parse.c
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "cache.h"
+#include "commit.h"
+#include "refs.h"
+#include "quote.h"
+#include "builtin.h"
+#include "parse-options.h"
+
+#define DO_REVS                1
+#define DO_NOREV       2
+#define DO_FLAGS       4
+#define DO_NONFLAGS    8
+static int filter = ~0;
+
+static const char *def;
+
+#define NORMAL 0
+#define REVERSED 1
+static int show_type = NORMAL;
+static int symbolic;
+static int abbrev;
+static int output_sq;
+
+static int revs_count;
+
+/*
+ * Some arguments are relevant "revision" arguments,
+ * others are about output format or other details.
+ * This sorts it all out.
+ */
+static int is_rev_argument(const char *arg)
+{
+       static const char *rev_args[] = {
+               "--all",
+               "--bisect",
+               "--dense",
+               "--branches",
+               "--header",
+               "--max-age=",
+               "--max-count=",
+               "--min-age=",
+               "--no-merges",
+               "--objects",
+               "--objects-edge",
+               "--parents",
+               "--pretty",
+               "--remotes",
+               "--sparse",
+               "--tags",
+               "--topo-order",
+               "--date-order",
+               "--unpacked",
+               NULL
+       };
+       const char **p = rev_args;
+
+       /* accept -<digit>, like traditional "head" */
+       if ((*arg == '-') && isdigit(arg[1]))
+               return 1;
+
+       for (;;) {
+               const char *str = *p++;
+               int len;
+               if (!str)
+                       return 0;
+               len = strlen(str);
+               if (!strcmp(arg, str) ||
+                   (str[len-1] == '=' && !strncmp(arg, str, len)))
+                       return 1;
+       }
+}
+
+/* Output argument as a string, either SQ or normal */
+static void show(const char *arg)
+{
+       if (output_sq) {
+               int sq = '\'', ch;
+
+               putchar(sq);
+               while ((ch = *arg++)) {
+                       if (ch == sq)
+                               fputs("'\\'", stdout);
+                       putchar(ch);
+               }
+               putchar(sq);
+               putchar(' ');
+       }
+       else
+               puts(arg);
+}
+
+/* Output a revision, only if filter allows it */
+static void show_rev(int type, const unsigned char *sha1, const char *name)
+{
+       if (!(filter & DO_REVS))
+               return;
+       def = NULL;
+       revs_count++;
+
+       if (type != show_type)
+               putchar('^');
+       if (symbolic && name)
+               show(name);
+       else if (abbrev)
+               show(find_unique_abbrev(sha1, abbrev));
+       else
+               show(sha1_to_hex(sha1));
+}
+
+/* Output a flag, only if filter allows it. */
+static int show_flag(const char *arg)
+{
+       if (!(filter & DO_FLAGS))
+               return 0;
+       if (filter & (is_rev_argument(arg) ? DO_REVS : DO_NOREV)) {
+               show(arg);
+               return 1;
+       }
+       return 0;
+}
+
+static void show_default(void)
+{
+       const char *s = def;
+
+       if (s) {
+               unsigned char sha1[20];
+
+               def = NULL;
+               if (!get_sha1(s, sha1)) {
+                       show_rev(NORMAL, sha1, s);
+                       return;
+               }
+       }
+}
+
+static int show_reference(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       show_rev(NORMAL, sha1, refname);
+       return 0;
+}
+
+static void show_datestring(const char *flag, const char *datestr)
+{
+       static char buffer[100];
+
+       /* date handling requires both flags and revs */
+       if ((filter & (DO_FLAGS | DO_REVS)) != (DO_FLAGS | DO_REVS))
+               return;
+       snprintf(buffer, sizeof(buffer), "%s%lu", flag, approxidate(datestr));
+       show(buffer);
+}
+
+static int show_file(const char *arg)
+{
+       show_default();
+       if ((filter & (DO_NONFLAGS|DO_NOREV)) == (DO_NONFLAGS|DO_NOREV)) {
+               show(arg);
+               return 1;
+       }
+       return 0;
+}
+
+static int try_difference(const char *arg)
+{
+       char *dotdot;
+       unsigned char sha1[20];
+       unsigned char end[20];
+       const char *next;
+       const char *this;
+       int symmetric;
+
+       if (!(dotdot = strstr(arg, "..")))
+               return 0;
+       next = dotdot + 2;
+       this = arg;
+       symmetric = (*next == '.');
+
+       *dotdot = 0;
+       next += symmetric;
+
+       if (!*next)
+               next = "HEAD";
+       if (dotdot == arg)
+               this = "HEAD";
+       if (!get_sha1(this, sha1) && !get_sha1(next, end)) {
+               show_rev(NORMAL, end, next);
+               show_rev(symmetric ? NORMAL : REVERSED, sha1, this);
+               if (symmetric) {
+                       struct commit_list *exclude;
+                       struct commit *a, *b;
+                       a = lookup_commit_reference(sha1);
+                       b = lookup_commit_reference(end);
+                       exclude = get_merge_bases(a, b, 1);
+                       while (exclude) {
+                               struct commit_list *n = exclude->next;
+                               show_rev(REVERSED,
+                                        exclude->item->object.sha1,NULL);
+                               free(exclude);
+                               exclude = n;
+                       }
+               }
+               return 1;
+       }
+       *dotdot = '.';
+       return 0;
+}
+
+static int parseopt_dump(const struct option *o, const char *arg, int unset)
+{
+       struct strbuf *parsed = o->value;
+       if (unset)
+               strbuf_addf(parsed, " --no-%s", o->long_name);
+       else if (o->short_name)
+               strbuf_addf(parsed, " -%c", o->short_name);
+       else
+               strbuf_addf(parsed, " --%s", o->long_name);
+       if (arg) {
+               strbuf_addch(parsed, ' ');
+               sq_quote_buf(parsed, arg);
+       }
+       return 0;
+}
+
+static const char *skipspaces(const char *s)
+{
+       while (isspace(*s))
+               s++;
+       return s;
+}
+
+static int cmd_parseopt(int argc, const char **argv, const char *prefix)
+{
+       static int keep_dashdash = 0;
+       static char const * const parseopt_usage[] = {
+               "git-rev-parse --parseopt [options] -- [<args>...]",
+               NULL
+       };
+       static struct option parseopt_opts[] = {
+               OPT_BOOLEAN(0, "keep-dashdash", &keep_dashdash,
+                                       "keep the `--` passed as an arg"),
+               OPT_END(),
+       };
+
+       struct strbuf sb, parsed;
+       const char **usage = NULL;
+       struct option *opts = NULL;
+       int onb = 0, osz = 0, unb = 0, usz = 0;
+
+       strbuf_init(&parsed, 0);
+       strbuf_addstr(&parsed, "set --");
+       argc = parse_options(argc, argv, parseopt_opts, parseopt_usage,
+                            PARSE_OPT_KEEP_DASHDASH);
+       if (argc < 1 || strcmp(argv[0], "--"))
+               usage_with_options(parseopt_usage, parseopt_opts);
+
+       strbuf_init(&sb, 0);
+       /* get the usage up to the first line with a -- on it */
+       for (;;) {
+               if (strbuf_getline(&sb, stdin, '\n') == EOF)
+                       die("premature end of input");
+               ALLOC_GROW(usage, unb + 1, usz);
+               if (!strcmp("--", sb.buf)) {
+                       if (unb < 1)
+                               die("no usage string given before the `--' separator");
+                       usage[unb] = NULL;
+                       break;
+               }
+               usage[unb++] = strbuf_detach(&sb, NULL);
+       }
+
+       /* parse: (<short>|<short>,<long>|<long>)[=?]? SP+ <help> */
+       while (strbuf_getline(&sb, stdin, '\n') != EOF) {
+               const char *s;
+               struct option *o;
+
+               if (!sb.len)
+                       continue;
+
+               ALLOC_GROW(opts, onb + 1, osz);
+               memset(opts + onb, 0, sizeof(opts[onb]));
+
+               o = &opts[onb++];
+               s = strchr(sb.buf, ' ');
+               if (!s || *sb.buf == ' ') {
+                       o->type = OPTION_GROUP;
+                       o->help = xstrdup(skipspaces(s));
+                       continue;
+               }
+
+               o->type = OPTION_CALLBACK;
+               o->help = xstrdup(skipspaces(s));
+               o->value = &parsed;
+               o->callback = &parseopt_dump;
+               switch (s[-1]) {
+               case '=':
+                       s--;
+                       break;
+               case '?':
+                       o->flags = PARSE_OPT_OPTARG;
+                       s--;
+                       break;
+               default:
+                       o->flags = PARSE_OPT_NOARG;
+                       break;
+               }
+
+               if (s - sb.buf == 1) /* short option only */
+                       o->short_name = *sb.buf;
+               else if (sb.buf[1] != ',') /* long option only */
+                       o->long_name = xmemdupz(sb.buf, s - sb.buf);
+               else {
+                       o->short_name = *sb.buf;
+                       o->long_name = xmemdupz(sb.buf + 2, s - sb.buf - 2);
+               }
+       }
+       strbuf_release(&sb);
+
+       /* put an OPT_END() */
+       ALLOC_GROW(opts, onb + 1, osz);
+       memset(opts + onb, 0, sizeof(opts[onb]));
+       argc = parse_options(argc, argv, opts, usage,
+                            keep_dashdash ? PARSE_OPT_KEEP_DASHDASH : 0);
+
+       strbuf_addf(&parsed, " --");
+       sq_quote_argv(&parsed, argv, 0);
+       puts(parsed.buf);
+       return 0;
+}
+
+int cmd_rev_parse(int argc, const char **argv, const char *prefix)
+{
+       int i, as_is = 0, verify = 0;
+       unsigned char sha1[20];
+
+       if (argc > 1 && !strcmp("--parseopt", argv[1]))
+               return cmd_parseopt(argc - 1, argv + 1, prefix);
+
+       prefix = setup_git_directory();
+       git_config(git_default_config);
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (as_is) {
+                       if (show_file(arg) && as_is < 2)
+                               verify_filename(prefix, arg);
+                       continue;
+               }
+               if (!strcmp(arg,"-n")) {
+                       if (++i >= argc)
+                               die("-n requires an argument");
+                       if ((filter & DO_FLAGS) && (filter & DO_REVS)) {
+                               show(arg);
+                               show(argv[i]);
+                       }
+                       continue;
+               }
+               if (!prefixcmp(arg, "-n")) {
+                       if ((filter & DO_FLAGS) && (filter & DO_REVS))
+                               show(arg);
+                       continue;
+               }
+
+               if (*arg == '-') {
+                       if (!strcmp(arg, "--")) {
+                               as_is = 2;
+                               /* Pass on the "--" if we show anything but files.. */
+                               if (filter & (DO_FLAGS | DO_REVS))
+                                       show_file(arg);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--default")) {
+                               def = argv[i+1];
+                               i++;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--revs-only")) {
+                               filter &= ~DO_NOREV;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--no-revs")) {
+                               filter &= ~DO_REVS;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--flags")) {
+                               filter &= ~DO_NONFLAGS;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--no-flags")) {
+                               filter &= ~DO_FLAGS;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--verify")) {
+                               filter &= ~(DO_FLAGS|DO_NOREV);
+                               verify = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--short") ||
+                           !prefixcmp(arg, "--short=")) {
+                               filter &= ~(DO_FLAGS|DO_NOREV);
+                               verify = 1;
+                               abbrev = DEFAULT_ABBREV;
+                               if (arg[7] == '=')
+                                       abbrev = strtoul(arg + 8, NULL, 10);
+                               if (abbrev < MINIMUM_ABBREV)
+                                       abbrev = MINIMUM_ABBREV;
+                               else if (40 <= abbrev)
+                                       abbrev = 40;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--sq")) {
+                               output_sq = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--not")) {
+                               show_type ^= REVERSED;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--symbolic")) {
+                               symbolic = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--all")) {
+                               for_each_ref(show_reference, NULL);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--branches")) {
+                               for_each_branch_ref(show_reference, NULL);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--tags")) {
+                               for_each_tag_ref(show_reference, NULL);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--remotes")) {
+                               for_each_remote_ref(show_reference, NULL);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--show-prefix")) {
+                               if (prefix)
+                                       puts(prefix);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--show-cdup")) {
+                               const char *pfx = prefix;
+                               if (!is_inside_work_tree()) {
+                                       const char *work_tree =
+                                               get_git_work_tree();
+                                       if (work_tree)
+                                               printf("%s\n", work_tree);
+                                       continue;
+                               }
+                               while (pfx) {
+                                       pfx = strchr(pfx, '/');
+                                       if (pfx) {
+                                               pfx++;
+                                               printf("../");
+                                       }
+                               }
+                               putchar('\n');
+                               continue;
+                       }
+                       if (!strcmp(arg, "--git-dir")) {
+                               const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
+                               static char cwd[PATH_MAX];
+                               if (gitdir) {
+                                       puts(gitdir);
+                                       continue;
+                               }
+                               if (!prefix) {
+                                       puts(".git");
+                                       continue;
+                               }
+                               if (!getcwd(cwd, PATH_MAX))
+                                       die("unable to get current working directory");
+                               printf("%s/.git\n", cwd);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--is-inside-git-dir")) {
+                               printf("%s\n", is_inside_git_dir() ? "true"
+                                               : "false");
+                               continue;
+                       }
+                       if (!strcmp(arg, "--is-inside-work-tree")) {
+                               printf("%s\n", is_inside_work_tree() ? "true"
+                                               : "false");
+                               continue;
+                       }
+                       if (!strcmp(arg, "--is-bare-repository")) {
+                               printf("%s\n", is_bare_repository() ? "true"
+                                               : "false");
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--since=")) {
+                               show_datestring("--max-age=", arg+8);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--after=")) {
+                               show_datestring("--max-age=", arg+8);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--before=")) {
+                               show_datestring("--min-age=", arg+9);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--until=")) {
+                               show_datestring("--min-age=", arg+8);
+                               continue;
+                       }
+                       if (show_flag(arg) && verify)
+                               die("Needed a single revision");
+                       continue;
+               }
+
+               /* Not a flag argument */
+               if (try_difference(arg))
+                       continue;
+               if (!get_sha1(arg, sha1)) {
+                       show_rev(NORMAL, sha1, arg);
+                       continue;
+               }
+               if (*arg == '^' && !get_sha1(arg+1, sha1)) {
+                       show_rev(REVERSED, sha1, arg+1);
+                       continue;
+               }
+               as_is = 1;
+               if (!show_file(arg))
+                       continue;
+               if (verify)
+                       die("Needed a single revision");
+               verify_filename(prefix, arg);
+       }
+       show_default();
+       if (verify && revs_count != 1)
+               die("Needed a single revision");
+       return 0;
+}
diff --git a/builtin-revert.c b/builtin-revert.c
new file mode 100644 (file)
index 0000000..4bf8eb2
--- /dev/null
@@ -0,0 +1,419 @@
+#include "cache.h"
+#include "builtin.h"
+#include "object.h"
+#include "commit.h"
+#include "tag.h"
+#include "wt-status.h"
+#include "run-command.h"
+#include "exec_cmd.h"
+#include "utf8.h"
+#include "parse-options.h"
+
+/*
+ * This implements the builtins revert and cherry-pick.
+ *
+ * Copyright (c) 2007 Johannes E. Schindelin
+ *
+ * Based on git-revert.sh, which is
+ *
+ * Copyright (c) 2005 Linus Torvalds
+ * Copyright (c) 2005 Junio C Hamano
+ */
+
+static const char * const revert_usage[] = {
+       "git-revert [options] <commit-ish>",
+       NULL
+};
+
+static const char * const cherry_pick_usage[] = {
+       "git-cherry-pick [options] <commit-ish>",
+       NULL
+};
+
+static int edit, no_replay, no_commit, mainline;
+static enum { REVERT, CHERRY_PICK } action;
+static struct commit *commit;
+
+static const char *me;
+
+#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
+
+static void parse_args(int argc, const char **argv)
+{
+       const char * const * usage_str =
+               action == REVERT ?  revert_usage : cherry_pick_usage;
+       unsigned char sha1[20];
+       const char *arg;
+       int noop;
+       struct option options[] = {
+               OPT_BOOLEAN('n', "no-commit", &no_commit, "don't automatically commit"),
+               OPT_BOOLEAN('e', "edit", &edit, "edit the commit message"),
+               OPT_BOOLEAN('x', NULL, &no_replay, "append commit name when cherry-picking"),
+               OPT_BOOLEAN('r', NULL, &noop, "no-op (backward compatibility)"),
+               OPT_INTEGER('m', "mainline", &mainline, "parent number"),
+               OPT_END(),
+       };
+
+       if (parse_options(argc, argv, options, usage_str, 0) != 1)
+               usage_with_options(usage_str, options);
+       arg = argv[0];
+
+       if (get_sha1(arg, sha1))
+               die ("Cannot find '%s'", arg);
+       commit = (struct commit *)parse_object(sha1);
+       if (!commit)
+               die ("Could not find %s", sha1_to_hex(sha1));
+       if (commit->object.type == OBJ_TAG) {
+               commit = (struct commit *)
+                       deref_tag((struct object *)commit, arg, strlen(arg));
+       }
+       if (commit->object.type != OBJ_COMMIT)
+               die ("'%s' does not point to a commit", arg);
+}
+
+static char *get_oneline(const char *message)
+{
+       char *result;
+       const char *p = message, *abbrev, *eol;
+       int abbrev_len, oneline_len;
+
+       if (!p)
+               die ("Could not read commit message of %s",
+                               sha1_to_hex(commit->object.sha1));
+       while (*p && (*p != '\n' || p[1] != '\n'))
+               p++;
+
+       if (*p) {
+               p += 2;
+               for (eol = p + 1; *eol && *eol != '\n'; eol++)
+                       ; /* do nothing */
+       } else
+               eol = p;
+       abbrev = find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV);
+       abbrev_len = strlen(abbrev);
+       oneline_len = eol - p;
+       result = xmalloc(abbrev_len + 5 + oneline_len);
+       memcpy(result, abbrev, abbrev_len);
+       memcpy(result + abbrev_len, "... ", 4);
+       memcpy(result + abbrev_len + 4, p, oneline_len);
+       result[abbrev_len + 4 + oneline_len] = '\0';
+       return result;
+}
+
+static char *get_encoding(const char *message)
+{
+       const char *p = message, *eol;
+
+       if (!p)
+               die ("Could not read commit message of %s",
+                               sha1_to_hex(commit->object.sha1));
+       while (*p && *p != '\n') {
+               for (eol = p + 1; *eol && *eol != '\n'; eol++)
+                       ; /* do nothing */
+               if (!prefixcmp(p, "encoding ")) {
+                       char *result = xmalloc(eol - 8 - p);
+                       strlcpy(result, p + 9, eol - 8 - p);
+                       return result;
+               }
+               p = eol;
+               if (*p == '\n')
+                       p++;
+       }
+       return NULL;
+}
+
+static struct lock_file msg_file;
+static int msg_fd;
+
+static void add_to_msg(const char *string)
+{
+       int len = strlen(string);
+       if (write_in_full(msg_fd, string, len) < 0)
+               die ("Could not write to MERGE_MSG");
+}
+
+static void add_message_to_msg(const char *message)
+{
+       const char *p = message;
+       while (*p && (*p != '\n' || p[1] != '\n'))
+               p++;
+
+       if (!*p)
+               add_to_msg(sha1_to_hex(commit->object.sha1));
+
+       p += 2;
+       add_to_msg(p);
+       return;
+}
+
+static void set_author_ident_env(const char *message)
+{
+       const char *p = message;
+       if (!p)
+               die ("Could not read commit message of %s",
+                               sha1_to_hex(commit->object.sha1));
+       while (*p && *p != '\n') {
+               const char *eol;
+
+               for (eol = p; *eol && *eol != '\n'; eol++)
+                       ; /* do nothing */
+               if (!prefixcmp(p, "author ")) {
+                       char *line, *pend, *email, *timestamp;
+
+                       p += 7;
+                       line = xmemdupz(p, eol - p);
+                       email = strchr(line, '<');
+                       if (!email)
+                               die ("Could not extract author email from %s",
+                                       sha1_to_hex(commit->object.sha1));
+                       if (email == line)
+                               pend = line;
+                       else
+                               for (pend = email; pend != line + 1 &&
+                                               isspace(pend[-1]); pend--);
+                                       ; /* do nothing */
+                       *pend = '\0';
+                       email++;
+                       timestamp = strchr(email, '>');
+                       if (!timestamp)
+                               die ("Could not extract author email from %s",
+                                       sha1_to_hex(commit->object.sha1));
+                       *timestamp = '\0';
+                       for (timestamp++; *timestamp && isspace(*timestamp);
+                                       timestamp++)
+                               ; /* do nothing */
+                       setenv("GIT_AUTHOR_NAME", line, 1);
+                       setenv("GIT_AUTHOR_EMAIL", email, 1);
+                       setenv("GIT_AUTHOR_DATE", timestamp, 1);
+                       free(line);
+                       return;
+               }
+               p = eol;
+               if (*p == '\n')
+                       p++;
+       }
+       die ("No author information found in %s",
+                       sha1_to_hex(commit->object.sha1));
+}
+
+static int merge_recursive(const char *base_sha1,
+               const char *head_sha1, const char *head_name,
+               const char *next_sha1, const char *next_name)
+{
+       char buffer[256];
+       const char *argv[6];
+
+       sprintf(buffer, "GITHEAD_%s", head_sha1);
+       setenv(buffer, head_name, 1);
+       sprintf(buffer, "GITHEAD_%s", next_sha1);
+       setenv(buffer, next_name, 1);
+
+       /*
+        * This three way merge is an interesting one.  We are at
+        * $head, and would want to apply the change between $commit
+        * and $prev on top of us (when reverting), or the change between
+        * $prev and $commit on top of us (when cherry-picking or replaying).
+        */
+       argv[0] = "merge-recursive";
+       argv[1] = base_sha1;
+       argv[2] = "--";
+       argv[3] = head_sha1;
+       argv[4] = next_sha1;
+       argv[5] = NULL;
+
+       return run_command_v_opt(argv, RUN_COMMAND_NO_STDIN | RUN_GIT_CMD);
+}
+
+static char *help_msg(const unsigned char *sha1)
+{
+       static char helpbuf[1024];
+       char *msg = getenv("GIT_CHERRY_PICK_HELP");
+
+       if (msg)
+               return msg;
+
+       strcpy(helpbuf, "  After resolving the conflicts,\n"
+              "mark the corrected paths with 'git add <paths>' "
+              "or 'git rm <paths>' and commit the result.");
+
+       if (action == CHERRY_PICK) {
+               sprintf(helpbuf + strlen(helpbuf),
+                       "\nWhen commiting, use the option "
+                       "'-c %s' to retain authorship and message.",
+                       find_unique_abbrev(sha1, DEFAULT_ABBREV));
+       }
+       return helpbuf;
+}
+
+static int revert_or_cherry_pick(int argc, const char **argv)
+{
+       unsigned char head[20];
+       struct commit *base, *next, *parent;
+       int i;
+       char *oneline, *reencoded_message = NULL;
+       const char *message, *encoding;
+       const char *defmsg = xstrdup(git_path("MERGE_MSG"));
+
+       git_config(git_default_config);
+       me = action == REVERT ? "revert" : "cherry-pick";
+       setenv(GIT_REFLOG_ACTION, me, 0);
+       parse_args(argc, argv);
+
+       /* this is copied from the shell script, but it's never triggered... */
+       if (action == REVERT && !no_replay)
+               die("revert is incompatible with replay");
+
+       if (no_commit) {
+               /*
+                * We do not intend to commit immediately.  We just want to
+                * merge the differences in, so let's compute the tree
+                * that represents the "current" state for merge-recursive
+                * to work on.
+                */
+               if (write_tree(head, 0, NULL))
+                       die ("Your index file is unmerged.");
+       } else {
+               struct wt_status s;
+
+               if (get_sha1("HEAD", head))
+                       die ("You do not have a valid HEAD");
+               wt_status_prepare(&s);
+               if (s.commitable)
+                       die ("Dirty index: cannot %s", me);
+               discard_cache();
+       }
+
+       if (!commit->parents)
+               die ("Cannot %s a root commit", me);
+       if (commit->parents->next) {
+               /* Reverting or cherry-picking a merge commit */
+               int cnt;
+               struct commit_list *p;
+
+               if (!mainline)
+                       die("Commit %s is a merge but no -m option was given.",
+                           sha1_to_hex(commit->object.sha1));
+
+               for (cnt = 1, p = commit->parents;
+                    cnt != mainline && p;
+                    cnt++)
+                       p = p->next;
+               if (cnt != mainline || !p)
+                       die("Commit %s does not have parent %d",
+                           sha1_to_hex(commit->object.sha1), mainline);
+               parent = p->item;
+       } else if (0 < mainline)
+               die("Mainline was specified but commit %s is not a merge.",
+                   sha1_to_hex(commit->object.sha1));
+       else
+               parent = commit->parents->item;
+
+       if (!(message = commit->buffer))
+               die ("Cannot get commit message for %s",
+                               sha1_to_hex(commit->object.sha1));
+
+       /*
+        * "commit" is an existing commit.  We would want to apply
+        * the difference it introduces since its first parent "prev"
+        * on top of the current HEAD if we are cherry-pick.  Or the
+        * reverse of it if we are revert.
+        */
+
+       msg_fd = hold_lock_file_for_update(&msg_file, defmsg, 1);
+
+       encoding = get_encoding(message);
+       if (!encoding)
+               encoding = "utf-8";
+       if (!git_commit_encoding)
+               git_commit_encoding = "utf-8";
+       if ((reencoded_message = reencode_string(message,
+                                       git_commit_encoding, encoding)))
+               message = reencoded_message;
+
+       oneline = get_oneline(message);
+
+       if (action == REVERT) {
+               char *oneline_body = strchr(oneline, ' ');
+
+               base = commit;
+               next = parent;
+               add_to_msg("Revert \"");
+               add_to_msg(oneline_body + 1);
+               add_to_msg("\"\n\nThis reverts commit ");
+               add_to_msg(sha1_to_hex(commit->object.sha1));
+               add_to_msg(".\n");
+       } else {
+               base = parent;
+               next = commit;
+               set_author_ident_env(message);
+               add_message_to_msg(message);
+               if (no_replay) {
+                       add_to_msg("(cherry picked from commit ");
+                       add_to_msg(sha1_to_hex(commit->object.sha1));
+                       add_to_msg(")\n");
+               }
+       }
+
+       if (merge_recursive(sha1_to_hex(base->object.sha1),
+                               sha1_to_hex(head), "HEAD",
+                               sha1_to_hex(next->object.sha1), oneline) ||
+                       write_tree(head, 0, NULL)) {
+               add_to_msg("\nConflicts:\n\n");
+               read_cache();
+               for (i = 0; i < active_nr;) {
+                       struct cache_entry *ce = active_cache[i++];
+                       if (ce_stage(ce)) {
+                               add_to_msg("\t");
+                               add_to_msg(ce->name);
+                               add_to_msg("\n");
+                               while (i < active_nr && !strcmp(ce->name,
+                                               active_cache[i]->name))
+                                       i++;
+                       }
+               }
+               if (close(msg_fd) || commit_lock_file(&msg_file) < 0)
+                       die ("Error wrapping up %s", defmsg);
+               fprintf(stderr, "Automatic %s failed.%s\n",
+                       me, help_msg(commit->object.sha1));
+               exit(1);
+       }
+       if (close(msg_fd) || commit_lock_file(&msg_file) < 0)
+               die ("Error wrapping up %s", defmsg);
+       fprintf(stderr, "Finished one %s.\n", me);
+
+       /*
+        *
+        * If we are cherry-pick, and if the merge did not result in
+        * hand-editing, we will hit this commit and inherit the original
+        * author date and name.
+        * If we are revert, or if our cherry-pick results in a hand merge,
+        * we had better say that the current user is responsible for that.
+        */
+
+       if (!no_commit) {
+               if (edit)
+                       return execl_git_cmd("commit", "-n", NULL);
+               else
+                       return execl_git_cmd("commit", "-n", "-F", defmsg, NULL);
+       }
+       if (reencoded_message)
+               free(reencoded_message);
+
+       return 0;
+}
+
+int cmd_revert(int argc, const char **argv, const char *prefix)
+{
+       if (isatty(0))
+               edit = 1;
+       no_replay = 1;
+       action = REVERT;
+       return revert_or_cherry_pick(argc, argv);
+}
+
+int cmd_cherry_pick(int argc, const char **argv, const char *prefix)
+{
+       no_replay = 0;
+       action = CHERRY_PICK;
+       return revert_or_cherry_pick(argc, argv);
+}
diff --git a/builtin-rm.c b/builtin-rm.c
new file mode 100644 (file)
index 0000000..a3d25e6
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+ * "git rm" builtin command
+ *
+ * Copyright (C) Linus Torvalds 2006
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "dir.h"
+#include "cache-tree.h"
+#include "tree-walk.h"
+#include "parse-options.h"
+
+static const char * const builtin_rm_usage[] = {
+       "git-rm [options] [--] <file>...",
+       NULL
+};
+
+static struct {
+       int nr, alloc;
+       const char **name;
+} list;
+
+static void add_list(const char *name)
+{
+       if (list.nr >= list.alloc) {
+               list.alloc = alloc_nr(list.alloc);
+               list.name = xrealloc(list.name, list.alloc * sizeof(const char *));
+       }
+       list.name[list.nr++] = name;
+}
+
+static int remove_file(const char *name)
+{
+       int ret;
+       char *slash;
+
+       ret = unlink(name);
+       if (ret && errno == ENOENT)
+               /* The user has removed it from the filesystem by hand */
+               ret = errno = 0;
+
+       if (!ret && (slash = strrchr(name, '/'))) {
+               char *n = xstrdup(name);
+               do {
+                       n[slash - name] = 0;
+                       name = n;
+               } while (!rmdir(name) && (slash = strrchr(name, '/')));
+       }
+       return ret;
+}
+
+static int check_local_mod(unsigned char *head, int index_only)
+{
+       /* items in list are already sorted in the cache order,
+        * so we could do this a lot more efficiently by using
+        * tree_desc based traversal if we wanted to, but I am
+        * lazy, and who cares if removal of files is a tad
+        * slower than the theoretical maximum speed?
+        */
+       int i, no_head;
+       int errs = 0;
+
+       no_head = is_null_sha1(head);
+       for (i = 0; i < list.nr; i++) {
+               struct stat st;
+               int pos;
+               struct cache_entry *ce;
+               const char *name = list.name[i];
+               unsigned char sha1[20];
+               unsigned mode;
+               int local_changes = 0;
+               int staged_changes = 0;
+
+               pos = cache_name_pos(name, strlen(name));
+               if (pos < 0)
+                       continue; /* removing unmerged entry */
+               ce = active_cache[pos];
+
+               if (lstat(ce->name, &st) < 0) {
+                       if (errno != ENOENT)
+                               fprintf(stderr, "warning: '%s': %s",
+                                       ce->name, strerror(errno));
+                       /* It already vanished from the working tree */
+                       continue;
+               }
+               else if (S_ISDIR(st.st_mode)) {
+                       /* if a file was removed and it is now a
+                        * directory, that is the same as ENOENT as
+                        * far as git is concerned; we do not track
+                        * directories.
+                        */
+                       continue;
+               }
+               if (ce_match_stat(ce, &st, 0))
+                       local_changes = 1;
+               if (no_head
+                    || get_tree_entry(head, name, sha1, &mode)
+                    || ce->ce_mode != create_ce_mode(mode)
+                    || hashcmp(ce->sha1, sha1))
+                       staged_changes = 1;
+
+               if (local_changes && staged_changes)
+                       errs = error("'%s' has staged content different "
+                                    "from both the file and the HEAD\n"
+                                    "(use -f to force removal)", name);
+               else if (!index_only) {
+                       /* It's not dangerous to git-rm --cached a
+                        * file if the index matches the file or the
+                        * HEAD, since it means the deleted content is
+                        * still available somewhere.
+                        */
+                       if (staged_changes)
+                               errs = error("'%s' has changes staged in the index\n"
+                                            "(use --cached to keep the file, "
+                                            "or -f to force removal)", name);
+                       if (local_changes)
+                               errs = error("'%s' has local modifications\n"
+                                            "(use --cached to keep the file, "
+                                            "or -f to force removal)", name);
+               }
+       }
+       return errs;
+}
+
+static struct lock_file lock_file;
+
+static int show_only = 0, force = 0, index_only = 0, recursive = 0, quiet = 0;
+static int ignore_unmatch = 0;
+
+static struct option builtin_rm_options[] = {
+       OPT__DRY_RUN(&show_only),
+       OPT__QUIET(&quiet),
+       OPT_BOOLEAN( 0 , "cached",         &index_only, "only remove from the index"),
+       OPT_BOOLEAN('f', NULL,             &force,      "override the up-to-date check"),
+       OPT_BOOLEAN('r', NULL,             &recursive,  "allow recursive removal"),
+       OPT_BOOLEAN( 0 , "ignore-unmatch", &ignore_unmatch,
+                               "exit with a zero status even if nothing matched"),
+       OPT_END(),
+};
+
+int cmd_rm(int argc, const char **argv, const char *prefix)
+{
+       int i, newfd;
+       const char **pathspec;
+       char *seen;
+
+       git_config(git_default_config);
+
+       newfd = hold_locked_index(&lock_file, 1);
+
+       if (read_cache() < 0)
+               die("index file corrupt");
+
+       argc = parse_options(argc, argv, builtin_rm_options, builtin_rm_usage, 0);
+       if (!argc)
+               usage_with_options(builtin_rm_usage, builtin_rm_options);
+
+       if (!index_only)
+               setup_work_tree();
+
+       pathspec = get_pathspec(prefix, argv);
+       seen = NULL;
+       for (i = 0; pathspec[i] ; i++)
+               /* nothing */;
+       seen = xcalloc(i, 1);
+
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (!match_pathspec(pathspec, ce->name, ce_namelen(ce), 0, seen))
+                       continue;
+               add_list(ce->name);
+       }
+
+       if (pathspec) {
+               const char *match;
+               int seen_any = 0;
+               for (i = 0; (match = pathspec[i]) != NULL ; i++) {
+                       if (!seen[i]) {
+                               if (!ignore_unmatch) {
+                                       die("pathspec '%s' did not match any files",
+                                           match);
+                               }
+                       }
+                       else {
+                               seen_any = 1;
+                       }
+                       if (!recursive && seen[i] == MATCHED_RECURSIVELY)
+                               die("not removing '%s' recursively without -r",
+                                   *match ? match : ".");
+               }
+
+               if (! seen_any)
+                       exit(0);
+       }
+
+       /*
+        * If not forced, the file, the index and the HEAD (if exists)
+        * must match; but the file can already been removed, since
+        * this sequence is a natural "novice" way:
+        *
+        *      rm F; git rm F
+        *
+        * Further, if HEAD commit exists, "diff-index --cached" must
+        * report no changes unless forced.
+        */
+       if (!force) {
+               unsigned char sha1[20];
+               if (get_sha1("HEAD", sha1))
+                       hashclr(sha1);
+               if (check_local_mod(sha1, index_only))
+                       exit(1);
+       }
+
+       /*
+        * First remove the names from the index: we won't commit
+        * the index unless all of them succeed.
+        */
+       for (i = 0; i < list.nr; i++) {
+               const char *path = list.name[i];
+               if (!quiet)
+                       printf("rm '%s'\n", path);
+
+               if (remove_file_from_cache(path))
+                       die("git-rm: unable to remove %s", path);
+       }
+
+       if (show_only)
+               return 0;
+
+       /*
+        * Then, unless we used "--cached", remove the filenames from
+        * the workspace. If we fail to remove the first one, we
+        * abort the "git rm" (but once we've successfully removed
+        * any file at all, we'll go ahead and commit to it all:
+        * by then we've already committed ourselves and can't fail
+        * in the middle)
+        */
+       if (!index_only) {
+               int removed = 0;
+               for (i = 0; i < list.nr; i++) {
+                       const char *path = list.name[i];
+                       if (!remove_file(path)) {
+                               removed = 1;
+                               continue;
+                       }
+                       if (!removed)
+                               die("git-rm: %s: %s", path, strerror(errno));
+               }
+       }
+
+       if (active_cache_changed) {
+               if (write_cache(newfd, active_cache, active_nr) ||
+                   close(newfd) || commit_locked_index(&lock_file))
+                       die("Unable to write new index file");
+       }
+
+       return 0;
+}
diff --git a/builtin-runstatus.c b/builtin-runstatus.c
new file mode 100644 (file)
index 0000000..8d167a9
--- /dev/null
@@ -0,0 +1,38 @@
+#include "builtin.h"
+#include "cache.h"
+#include "wt-status.h"
+
+extern int wt_status_use_color;
+
+static const char runstatus_usage[] =
+"git-runstatus [--color|--nocolor] [--amend] [--verbose] [--untracked]";
+
+int cmd_runstatus(int argc, const char **argv, const char *prefix)
+{
+       struct wt_status s;
+       int i;
+
+       git_config(git_status_config);
+       wt_status_prepare(&s);
+       s.prefix = prefix;
+
+       for (i = 1; i < argc; i++) {
+               if (!strcmp(argv[i], "--color"))
+                       wt_status_use_color = 1;
+               else if (!strcmp(argv[i], "--nocolor"))
+                       wt_status_use_color = 0;
+               else if (!strcmp(argv[i], "--amend")) {
+                       s.amend = 1;
+                       s.reference = "HEAD^1";
+               }
+               else if (!strcmp(argv[i], "--verbose"))
+                       s.verbose = 1;
+               else if (!strcmp(argv[i], "--untracked"))
+                       s.untracked = 1;
+               else
+                       usage(runstatus_usage);
+       }
+
+       wt_status_print(&s);
+       return s.commitable ? 0 : 1;
+}
diff --git a/builtin-send-pack.c b/builtin-send-pack.c
new file mode 100644 (file)
index 0000000..25ae1fe
--- /dev/null
@@ -0,0 +1,652 @@
+#include "cache.h"
+#include "commit.h"
+#include "tag.h"
+#include "refs.h"
+#include "pkt-line.h"
+#include "run-command.h"
+#include "remote.h"
+#include "send-pack.h"
+
+static const char send_pack_usage[] =
+"git-send-pack [--all | --mirror] [--dry-run] [--force] [--receive-pack=<git-receive-pack>] [--verbose] [--thin] [<host>:]<directory> [<ref>...]\n"
+"  --all and explicit <ref> specification are mutually exclusive.";
+
+static struct send_pack_args args = {
+       /* .receivepack = */ "git-receive-pack",
+};
+
+/*
+ * Make a pack stream and spit it out into file descriptor fd
+ */
+static int pack_objects(int fd, struct ref *refs)
+{
+       /*
+        * The child becomes pack-objects --revs; we feed
+        * the revision parameters to it via its stdin and
+        * let its stdout go back to the other end.
+        */
+       const char *argv[] = {
+               "pack-objects",
+               "--all-progress",
+               "--revs",
+               "--stdout",
+               NULL,
+               NULL,
+       };
+       struct child_process po;
+
+       if (args.use_thin_pack)
+               argv[4] = "--thin";
+       memset(&po, 0, sizeof(po));
+       po.argv = argv;
+       po.in = -1;
+       po.out = fd;
+       po.git_cmd = 1;
+       if (start_command(&po))
+               die("git-pack-objects failed (%s)", strerror(errno));
+
+       /*
+        * We feed the pack-objects we just spawned with revision
+        * parameters by writing to the pipe.
+        */
+       while (refs) {
+               char buf[42];
+
+               if (!is_null_sha1(refs->old_sha1) &&
+                   has_sha1_file(refs->old_sha1)) {
+                       memcpy(buf + 1, sha1_to_hex(refs->old_sha1), 40);
+                       buf[0] = '^';
+                       buf[41] = '\n';
+                       if (!write_or_whine(po.in, buf, 42,
+                                               "send-pack: send refs"))
+                               break;
+               }
+               if (!is_null_sha1(refs->new_sha1)) {
+                       memcpy(buf, sha1_to_hex(refs->new_sha1), 40);
+                       buf[40] = '\n';
+                       if (!write_or_whine(po.in, buf, 41,
+                                               "send-pack: send refs"))
+                               break;
+               }
+               refs = refs->next;
+       }
+
+       if (finish_command(&po))
+               return error("pack-objects died with strange error");
+       return 0;
+}
+
+static void unmark_and_free(struct commit_list *list, unsigned int mark)
+{
+       while (list) {
+               struct commit_list *temp = list;
+               temp->item->object.flags &= ~mark;
+               list = temp->next;
+               free(temp);
+       }
+}
+
+static int ref_newer(const unsigned char *new_sha1,
+                    const unsigned char *old_sha1)
+{
+       struct object *o;
+       struct commit *old, *new;
+       struct commit_list *list, *used;
+       int found = 0;
+
+       /* Both new and old must be commit-ish and new is descendant of
+        * old.  Otherwise we require --force.
+        */
+       o = deref_tag(parse_object(old_sha1), NULL, 0);
+       if (!o || o->type != OBJ_COMMIT)
+               return 0;
+       old = (struct commit *) o;
+
+       o = deref_tag(parse_object(new_sha1), NULL, 0);
+       if (!o || o->type != OBJ_COMMIT)
+               return 0;
+       new = (struct commit *) o;
+
+       if (parse_commit(new) < 0)
+               return 0;
+
+       used = list = NULL;
+       commit_list_insert(new, &list);
+       while (list) {
+               new = pop_most_recent_commit(&list, 1);
+               commit_list_insert(new, &used);
+               if (new == old) {
+                       found = 1;
+                       break;
+               }
+       }
+       unmark_and_free(list, 1);
+       unmark_and_free(used, 1);
+       return found;
+}
+
+static struct ref *local_refs, **local_tail;
+static struct ref *remote_refs, **remote_tail;
+
+static int one_local_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct ref *ref;
+       int len = strlen(refname) + 1;
+       ref = xcalloc(1, sizeof(*ref) + len);
+       hashcpy(ref->new_sha1, sha1);
+       memcpy(ref->name, refname, len);
+       *local_tail = ref;
+       local_tail = &ref->next;
+       return 0;
+}
+
+static void get_local_heads(void)
+{
+       local_tail = &local_refs;
+       for_each_ref(one_local_ref, NULL);
+}
+
+static int receive_status(int in, struct ref *refs)
+{
+       struct ref *hint;
+       char line[1000];
+       int ret = 0;
+       int len = packet_read_line(in, line, sizeof(line));
+       if (len < 10 || memcmp(line, "unpack ", 7))
+               return error("did not receive remote status");
+       if (memcmp(line, "unpack ok\n", 10)) {
+               char *p = line + strlen(line) - 1;
+               if (*p == '\n')
+                       *p = '\0';
+               error("unpack failed: %s", line + 7);
+               ret = -1;
+       }
+       hint = NULL;
+       while (1) {
+               char *refname;
+               char *msg;
+               len = packet_read_line(in, line, sizeof(line));
+               if (!len)
+                       break;
+               if (len < 3 ||
+                   (memcmp(line, "ok ", 3) && memcmp(line, "ng ", 3))) {
+                       fprintf(stderr, "protocol error: %s\n", line);
+                       ret = -1;
+                       break;
+               }
+
+               line[strlen(line)-1] = '\0';
+               refname = line + 3;
+               msg = strchr(refname, ' ');
+               if (msg)
+                       *msg++ = '\0';
+
+               /* first try searching at our hint, falling back to all refs */
+               if (hint)
+                       hint = find_ref_by_name(hint, refname);
+               if (!hint)
+                       hint = find_ref_by_name(refs, refname);
+               if (!hint) {
+                       warning("remote reported status on unknown ref: %s",
+                                       refname);
+                       continue;
+               }
+               if (hint->status != REF_STATUS_EXPECTING_REPORT) {
+                       warning("remote reported status on unexpected ref: %s",
+                                       refname);
+                       continue;
+               }
+
+               if (line[0] == 'o' && line[1] == 'k')
+                       hint->status = REF_STATUS_OK;
+               else {
+                       hint->status = REF_STATUS_REMOTE_REJECT;
+                       ret = -1;
+               }
+               if (msg)
+                       hint->remote_status = xstrdup(msg);
+               /* start our next search from the next ref */
+               hint = hint->next;
+       }
+       return ret;
+}
+
+static void update_tracking_ref(struct remote *remote, struct ref *ref)
+{
+       struct refspec rs;
+
+       if (ref->status != REF_STATUS_OK)
+               return;
+
+       rs.src = ref->name;
+       rs.dst = NULL;
+
+       if (!remote_find_tracking(remote, &rs)) {
+               if (args.verbose)
+                       fprintf(stderr, "updating local tracking ref '%s'\n", rs.dst);
+               if (ref->deletion) {
+                       if (delete_ref(rs.dst, NULL))
+                               error("Failed to delete");
+               } else
+                       update_ref("update by push", rs.dst,
+                                       ref->new_sha1, NULL, 0, 0);
+               free(rs.dst);
+       }
+}
+
+static const char *prettify_ref(const struct ref *ref)
+{
+       const char *name = ref->name;
+       return name + (
+               !prefixcmp(name, "refs/heads/") ? 11 :
+               !prefixcmp(name, "refs/tags/") ? 10 :
+               !prefixcmp(name, "refs/remotes/") ? 13 :
+               0);
+}
+
+#define SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
+
+static void print_ref_status(char flag, const char *summary, struct ref *to, struct ref *from, const char *msg)
+{
+       fprintf(stderr, " %c %-*s ", flag, SUMMARY_WIDTH, summary);
+       if (from)
+               fprintf(stderr, "%s -> %s", prettify_ref(from), prettify_ref(to));
+       else
+               fputs(prettify_ref(to), stderr);
+       if (msg) {
+               fputs(" (", stderr);
+               fputs(msg, stderr);
+               fputc(')', stderr);
+       }
+       fputc('\n', stderr);
+}
+
+static const char *status_abbrev(unsigned char sha1[20])
+{
+       const char *abbrev;
+       abbrev = find_unique_abbrev(sha1, DEFAULT_ABBREV);
+       return abbrev ? abbrev : sha1_to_hex(sha1);
+}
+
+static void print_ok_ref_status(struct ref *ref)
+{
+       if (ref->deletion)
+               print_ref_status('-', "[deleted]", ref, NULL, NULL);
+       else if (is_null_sha1(ref->old_sha1))
+               print_ref_status('*',
+                       (!prefixcmp(ref->name, "refs/tags/") ? "[new tag]" :
+                         "[new branch]"),
+                       ref, ref->peer_ref, NULL);
+       else {
+               char quickref[84];
+               char type;
+               const char *msg;
+
+               strcpy(quickref, status_abbrev(ref->old_sha1));
+               if (ref->nonfastforward) {
+                       strcat(quickref, "...");
+                       type = '+';
+                       msg = "forced update";
+               } else {
+                       strcat(quickref, "..");
+                       type = ' ';
+                       msg = NULL;
+               }
+               strcat(quickref, status_abbrev(ref->new_sha1));
+
+               print_ref_status(type, quickref, ref, ref->peer_ref, msg);
+       }
+}
+
+static int print_one_push_status(struct ref *ref, const char *dest, int count)
+{
+       if (!count)
+               fprintf(stderr, "To %s\n", dest);
+
+       switch(ref->status) {
+       case REF_STATUS_NONE:
+               print_ref_status('X', "[no match]", ref, NULL, NULL);
+               break;
+       case REF_STATUS_REJECT_NODELETE:
+               print_ref_status('!', "[rejected]", ref, NULL,
+                               "remote does not support deleting refs");
+               break;
+       case REF_STATUS_UPTODATE:
+               print_ref_status('=', "[up to date]", ref,
+                               ref->peer_ref, NULL);
+               break;
+       case REF_STATUS_REJECT_NONFASTFORWARD:
+               print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+                               "non-fast forward");
+               break;
+       case REF_STATUS_REMOTE_REJECT:
+               print_ref_status('!', "[remote rejected]", ref,
+                               ref->deletion ? NULL : ref->peer_ref,
+                               ref->remote_status);
+               break;
+       case REF_STATUS_EXPECTING_REPORT:
+               print_ref_status('!', "[remote failure]", ref,
+                               ref->deletion ? NULL : ref->peer_ref,
+                               "remote failed to report status");
+               break;
+       case REF_STATUS_OK:
+               print_ok_ref_status(ref);
+               break;
+       }
+
+       return 1;
+}
+
+static void print_push_status(const char *dest, struct ref *refs)
+{
+       struct ref *ref;
+       int n = 0;
+
+       if (args.verbose) {
+               for (ref = refs; ref; ref = ref->next)
+                       if (ref->status == REF_STATUS_UPTODATE)
+                               n += print_one_push_status(ref, dest, n);
+       }
+
+       for (ref = refs; ref; ref = ref->next)
+               if (ref->status == REF_STATUS_OK)
+                       n += print_one_push_status(ref, dest, n);
+
+       for (ref = refs; ref; ref = ref->next) {
+               if (ref->status != REF_STATUS_NONE &&
+                   ref->status != REF_STATUS_UPTODATE &&
+                   ref->status != REF_STATUS_OK)
+                       n += print_one_push_status(ref, dest, n);
+       }
+}
+
+static int refs_pushed(struct ref *ref)
+{
+       for (; ref; ref = ref->next) {
+               switch(ref->status) {
+               case REF_STATUS_NONE:
+               case REF_STATUS_UPTODATE:
+                       break;
+               default:
+                       return 1;
+               }
+       }
+       return 0;
+}
+
+static int do_send_pack(int in, int out, struct remote *remote, const char *dest, int nr_refspec, const char **refspec)
+{
+       struct ref *ref;
+       int new_refs;
+       int ask_for_status_report = 0;
+       int allow_deleting_refs = 0;
+       int expect_status_report = 0;
+       int flags = MATCH_REFS_NONE;
+       int ret;
+
+       if (args.send_all)
+               flags |= MATCH_REFS_ALL;
+       if (args.send_mirror)
+               flags |= MATCH_REFS_MIRROR;
+
+       /* No funny business with the matcher */
+       remote_tail = get_remote_heads(in, &remote_refs, 0, NULL, REF_NORMAL);
+       get_local_heads();
+
+       /* Does the other end support the reporting? */
+       if (server_supports("report-status"))
+               ask_for_status_report = 1;
+       if (server_supports("delete-refs"))
+               allow_deleting_refs = 1;
+
+       /* match them up */
+       if (!remote_tail)
+               remote_tail = &remote_refs;
+       if (match_refs(local_refs, remote_refs, &remote_tail,
+                                              nr_refspec, refspec, flags))
+               return -1;
+
+       if (!remote_refs) {
+               fprintf(stderr, "No refs in common and none specified; doing nothing.\n"
+                       "Perhaps you should specify a branch such as 'master'.\n");
+               return 0;
+       }
+
+       /*
+        * Finally, tell the other end!
+        */
+       new_refs = 0;
+       for (ref = remote_refs; ref; ref = ref->next) {
+               const unsigned char *new_sha1;
+
+               if (!ref->peer_ref) {
+                       if (!args.send_mirror)
+                               continue;
+                       new_sha1 = null_sha1;
+               }
+               else
+                       new_sha1 = ref->peer_ref->new_sha1;
+
+
+               ref->deletion = is_null_sha1(new_sha1);
+               if (ref->deletion && !allow_deleting_refs) {
+                       ref->status = REF_STATUS_REJECT_NODELETE;
+                       continue;
+               }
+               if (!ref->deletion &&
+                   !hashcmp(ref->old_sha1, new_sha1)) {
+                       ref->status = REF_STATUS_UPTODATE;
+                       continue;
+               }
+
+               /* This part determines what can overwrite what.
+                * The rules are:
+                *
+                * (0) you can always use --force or +A:B notation to
+                *     selectively force individual ref pairs.
+                *
+                * (1) if the old thing does not exist, it is OK.
+                *
+                * (2) if you do not have the old thing, you are not allowed
+                *     to overwrite it; you would not know what you are losing
+                *     otherwise.
+                *
+                * (3) if both new and old are commit-ish, and new is a
+                *     descendant of old, it is OK.
+                *
+                * (4) regardless of all of the above, removing :B is
+                *     always allowed.
+                */
+
+               ref->nonfastforward =
+                   !ref->deletion &&
+                   !is_null_sha1(ref->old_sha1) &&
+                   (!has_sha1_file(ref->old_sha1)
+                     || !ref_newer(new_sha1, ref->old_sha1));
+
+               if (ref->nonfastforward && !ref->force && !args.force_update) {
+                       ref->status = REF_STATUS_REJECT_NONFASTFORWARD;
+                       continue;
+               }
+
+               hashcpy(ref->new_sha1, new_sha1);
+               if (!ref->deletion)
+                       new_refs++;
+
+               if (!args.dry_run) {
+                       char *old_hex = sha1_to_hex(ref->old_sha1);
+                       char *new_hex = sha1_to_hex(ref->new_sha1);
+
+                       if (ask_for_status_report) {
+                               packet_write(out, "%s %s %s%c%s",
+                                       old_hex, new_hex, ref->name, 0,
+                                       "report-status");
+                               ask_for_status_report = 0;
+                               expect_status_report = 1;
+                       }
+                       else
+                               packet_write(out, "%s %s %s",
+                                       old_hex, new_hex, ref->name);
+               }
+               ref->status = expect_status_report ?
+                       REF_STATUS_EXPECTING_REPORT :
+                       REF_STATUS_OK;
+       }
+
+       packet_flush(out);
+       if (new_refs && !args.dry_run) {
+               if (pack_objects(out, remote_refs) < 0) {
+                       close(out);
+                       return -1;
+               }
+       }
+       close(out);
+
+       if (expect_status_report)
+               ret = receive_status(in, remote_refs);
+       else
+               ret = 0;
+
+       print_push_status(dest, remote_refs);
+
+       if (!args.dry_run && remote) {
+               for (ref = remote_refs; ref; ref = ref->next)
+                       update_tracking_ref(remote, ref);
+       }
+
+       if (!refs_pushed(remote_refs))
+               fprintf(stderr, "Everything up-to-date\n");
+       if (ret < 0)
+               return ret;
+       for (ref = remote_refs; ref; ref = ref->next) {
+               switch (ref->status) {
+               case REF_STATUS_NONE:
+               case REF_STATUS_UPTODATE:
+               case REF_STATUS_OK:
+                       break;
+               default:
+                       return -1;
+               }
+       }
+       return 0;
+}
+
+static void verify_remote_names(int nr_heads, const char **heads)
+{
+       int i;
+
+       for (i = 0; i < nr_heads; i++) {
+               const char *remote = strchr(heads[i], ':');
+
+               remote = remote ? (remote + 1) : heads[i];
+               switch (check_ref_format(remote)) {
+               case 0: /* ok */
+               case -2: /* ok but a single level -- that is fine for
+                         * a match pattern.
+                         */
+               case -3: /* ok but ends with a pattern-match character */
+                       continue;
+               }
+               die("remote part of refspec is not a valid name in %s",
+                   heads[i]);
+       }
+}
+
+int cmd_send_pack(int argc, const char **argv, const char *prefix)
+{
+       int i, nr_heads = 0;
+       const char **heads = NULL;
+       const char *remote_name = NULL;
+       struct remote *remote = NULL;
+       const char *dest = NULL;
+
+       argv++;
+       for (i = 1; i < argc; i++, argv++) {
+               const char *arg = *argv;
+
+               if (*arg == '-') {
+                       if (!prefixcmp(arg, "--receive-pack=")) {
+                               args.receivepack = arg + 15;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--exec=")) {
+                               args.receivepack = arg + 7;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--remote=")) {
+                               remote_name = arg + 9;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--all")) {
+                               args.send_all = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--dry-run")) {
+                               args.dry_run = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--mirror")) {
+                               args.send_mirror = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--force")) {
+                               args.force_update = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--verbose")) {
+                               args.verbose = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--thin")) {
+                               args.use_thin_pack = 1;
+                               continue;
+                       }
+                       usage(send_pack_usage);
+               }
+               if (!dest) {
+                       dest = arg;
+                       continue;
+               }
+               heads = (const char **) argv;
+               nr_heads = argc - i;
+               break;
+       }
+       if (!dest)
+               usage(send_pack_usage);
+       /*
+        * --all and --mirror are incompatible; neither makes sense
+        * with any refspecs.
+        */
+       if ((heads && (args.send_all || args.send_mirror)) ||
+                                       (args.send_all && args.send_mirror))
+               usage(send_pack_usage);
+
+       if (remote_name) {
+               remote = remote_get(remote_name);
+               if (!remote_has_url(remote, dest)) {
+                       die("Destination %s is not a uri for %s",
+                           dest, remote_name);
+               }
+       }
+
+       return send_pack(&args, dest, remote, nr_heads, heads);
+}
+
+int send_pack(struct send_pack_args *my_args,
+             const char *dest, struct remote *remote,
+             int nr_heads, const char **heads)
+{
+       int fd[2], ret;
+       struct child_process *conn;
+
+       memcpy(&args, my_args, sizeof(args));
+
+       verify_remote_names(nr_heads, heads);
+
+       conn = git_connect(fd, dest, args.receivepack, args.verbose ? CONNECT_VERBOSE : 0);
+       ret = do_send_pack(fd[0], fd[1], remote, dest, nr_heads, heads);
+       close(fd[0]);
+       close(fd[1]);
+       ret |= finish_connect(conn);
+       return !!ret;
+}
diff --git a/builtin-shortlog.c b/builtin-shortlog.c
new file mode 100644 (file)
index 0000000..3fe7546
--- /dev/null
@@ -0,0 +1,326 @@
+#include "builtin.h"
+#include "cache.h"
+#include "commit.h"
+#include "diff.h"
+#include "path-list.h"
+#include "revision.h"
+#include "utf8.h"
+#include "mailmap.h"
+
+static const char shortlog_usage[] =
+"git-shortlog [-n] [-s] [<commit-id>... ]";
+
+static char *common_repo_prefix;
+
+static int compare_by_number(const void *a1, const void *a2)
+{
+       const struct path_list_item *i1 = a1, *i2 = a2;
+       const struct path_list *l1 = i1->util, *l2 = i2->util;
+
+       if (l1->nr < l2->nr)
+               return 1;
+       else if (l1->nr == l2->nr)
+               return 0;
+       else
+               return -1;
+}
+
+static struct path_list mailmap = {NULL, 0, 0, 0};
+
+static void insert_author_oneline(struct path_list *list,
+               const char *author, int authorlen,
+               const char *oneline, int onelinelen)
+{
+       const char *dot3 = common_repo_prefix;
+       char *buffer, *p;
+       struct path_list_item *item;
+       struct path_list *onelines;
+
+       while (authorlen > 0 && isspace(author[authorlen - 1]))
+               authorlen--;
+
+       buffer = xmemdupz(author, authorlen);
+       item = path_list_insert(buffer, list);
+       if (item->util == NULL)
+               item->util = xcalloc(1, sizeof(struct path_list));
+       else
+               free(buffer);
+
+       if (!prefixcmp(oneline, "[PATCH")) {
+               char *eob = strchr(oneline, ']');
+
+               if (eob) {
+                       while (isspace(eob[1]) && eob[1] != '\n')
+                               eob++;
+                       if (eob - oneline < onelinelen) {
+                               onelinelen -= eob - oneline;
+                               oneline = eob;
+                       }
+               }
+       }
+
+       while (onelinelen > 0 && isspace(oneline[0])) {
+               oneline++;
+               onelinelen--;
+       }
+       while (onelinelen > 0 && isspace(oneline[onelinelen - 1]))
+               onelinelen--;
+       buffer = xmemdupz(oneline, onelinelen);
+
+       if (dot3) {
+               int dot3len = strlen(dot3);
+               if (dot3len > 5) {
+                       while ((p = strstr(buffer, dot3)) != NULL) {
+                               int taillen = strlen(p) - dot3len;
+                               memcpy(p, "/.../", 5);
+                               memmove(p + 5, p + dot3len, taillen + 1);
+                       }
+               }
+       }
+
+       onelines = item->util;
+       if (onelines->nr >= onelines->alloc) {
+               onelines->alloc = alloc_nr(onelines->nr);
+               onelines->items = xrealloc(onelines->items,
+                               onelines->alloc
+                               * sizeof(struct path_list_item));
+       }
+
+       onelines->items[onelines->nr].util = NULL;
+       onelines->items[onelines->nr++].path = buffer;
+}
+
+static void read_from_stdin(struct path_list *list)
+{
+       char buffer[1024];
+
+       while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
+               char *bob;
+               if ((buffer[0] == 'A' || buffer[0] == 'a') &&
+                               !prefixcmp(buffer + 1, "uthor: ") &&
+                               (bob = strchr(buffer + 7, '<')) != NULL) {
+                       char buffer2[1024], offset = 0;
+
+                       if (map_email(&mailmap, bob + 1, buffer, sizeof(buffer)))
+                               bob = buffer + strlen(buffer);
+                       else {
+                               offset = 8;
+                               while (buffer + offset < bob &&
+                                      isspace(bob[-1]))
+                                       bob--;
+                       }
+
+                       while (fgets(buffer2, sizeof(buffer2), stdin) &&
+                                       buffer2[0] != '\n')
+                               ; /* chomp input */
+                       if (fgets(buffer2, sizeof(buffer2), stdin)) {
+                               int l2 = strlen(buffer2);
+                               int i;
+                               for (i = 0; i < l2; i++)
+                                       if (!isspace(buffer2[i]))
+                                               break;
+                               insert_author_oneline(list,
+                                               buffer + offset,
+                                               bob - buffer - offset,
+                                               buffer2 + i, l2 - i);
+                       }
+               }
+       }
+}
+
+static void get_from_rev(struct rev_info *rev, struct path_list *list)
+{
+       char scratch[1024];
+       struct commit *commit;
+
+       prepare_revision_walk(rev);
+       while ((commit = get_revision(rev)) != NULL) {
+               const char *author = NULL, *oneline, *buffer;
+               int authorlen = authorlen, onelinelen;
+
+               /* get author and oneline */
+               for (buffer = commit->buffer; buffer && *buffer != '\0' &&
+                               *buffer != '\n'; ) {
+                       const char *eol = strchr(buffer, '\n');
+
+                       if (eol == NULL)
+                               eol = buffer + strlen(buffer);
+                       else
+                               eol++;
+
+                       if (!prefixcmp(buffer, "author ")) {
+                               char *bracket = strchr(buffer, '<');
+
+                               if (bracket == NULL || bracket > eol)
+                                       die("Invalid commit buffer: %s",
+                                           sha1_to_hex(commit->object.sha1));
+
+                               if (map_email(&mailmap, bracket + 1, scratch,
+                                                       sizeof(scratch))) {
+                                       author = scratch;
+                                       authorlen = strlen(scratch);
+                               } else {
+                                       if (bracket[-1] == ' ')
+                                               bracket--;
+
+                                       author = buffer + 7;
+                                       authorlen = bracket - buffer - 7;
+                               }
+                       }
+                       buffer = eol;
+               }
+
+               if (author == NULL)
+                       die ("Missing author: %s",
+                                       sha1_to_hex(commit->object.sha1));
+
+               if (buffer == NULL || *buffer == '\0') {
+                       oneline = "<none>";
+                       onelinelen = sizeof(oneline) + 1;
+               } else {
+                       char *eol;
+
+                       oneline = buffer + 1;
+                       eol = strchr(oneline, '\n');
+                       if (eol == NULL)
+                               onelinelen = strlen(oneline);
+                       else
+                               onelinelen = eol - oneline;
+               }
+
+               insert_author_oneline(list,
+                               author, authorlen, oneline, onelinelen);
+       }
+
+}
+
+static int parse_uint(char const **arg, int comma)
+{
+       unsigned long ul;
+       int ret;
+       char *endp;
+
+       ul = strtoul(*arg, &endp, 10);
+       if (endp != *arg && *endp && *endp != comma)
+               return -1;
+       ret = (int) ul;
+       if (ret != ul)
+               return -1;
+       *arg = endp;
+       if (**arg)
+               (*arg)++;
+       return ret;
+}
+
+static const char wrap_arg_usage[] = "-w[<width>[,<indent1>[,<indent2>]]]";
+#define DEFAULT_WRAPLEN 76
+#define DEFAULT_INDENT1 6
+#define DEFAULT_INDENT2 9
+
+static void parse_wrap_args(const char *arg, int *in1, int *in2, int *wrap)
+{
+       arg += 2; /* skip -w */
+
+       *wrap = parse_uint(&arg, ',');
+       if (*wrap < 0)
+               die(wrap_arg_usage);
+       *in1 = parse_uint(&arg, ',');
+       if (*in1 < 0)
+               die(wrap_arg_usage);
+       *in2 = parse_uint(&arg, '\0');
+       if (*in2 < 0)
+               die(wrap_arg_usage);
+
+       if (!*wrap)
+               *wrap = DEFAULT_WRAPLEN;
+       if (!*in1)
+               *in1 = DEFAULT_INDENT1;
+       if (!*in2)
+               *in2 = DEFAULT_INDENT2;
+       if (*wrap &&
+           ((*in1 && *wrap <= *in1) ||
+            (*in2 && *wrap <= *in2)))
+               die(wrap_arg_usage);
+}
+
+int cmd_shortlog(int argc, const char **argv, const char *prefix)
+{
+       struct rev_info rev;
+       struct path_list list = { NULL, 0, 0, 1 };
+       int i, j, sort_by_number = 0, summary = 0;
+       int wrap_lines = 0;
+       int wrap = DEFAULT_WRAPLEN;
+       int in1 = DEFAULT_INDENT1;
+       int in2 = DEFAULT_INDENT2;
+
+       /* since -n is a shadowed rev argument, parse our args first */
+       while (argc > 1) {
+               if (!strcmp(argv[1], "-n") || !strcmp(argv[1], "--numbered"))
+                       sort_by_number = 1;
+               else if (!strcmp(argv[1], "-s") ||
+                               !strcmp(argv[1], "--summary"))
+                       summary = 1;
+               else if (!prefixcmp(argv[1], "-w")) {
+                       wrap_lines = 1;
+                       parse_wrap_args(argv[1], &in1, &in2, &wrap);
+               }
+               else if (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))
+                       usage(shortlog_usage);
+               else
+                       break;
+               argv++;
+               argc--;
+       }
+       init_revisions(&rev, prefix);
+       argc = setup_revisions(argc, argv, &rev, NULL);
+       if (argc > 1)
+               die ("unrecognized argument: %s", argv[1]);
+
+       read_mailmap(&mailmap, ".mailmap", &common_repo_prefix);
+
+       if (rev.pending.nr == 0) {
+               if (isatty(0))
+                       fprintf(stderr, "(reading log to summarize from standard input)\n");
+               read_from_stdin(&list);
+       }
+       else
+               get_from_rev(&rev, &list);
+
+       if (sort_by_number)
+               qsort(list.items, list.nr, sizeof(struct path_list_item),
+                       compare_by_number);
+
+       for (i = 0; i < list.nr; i++) {
+               struct path_list *onelines = list.items[i].util;
+
+               if (summary) {
+                       printf("%s: %d\n", list.items[i].path, onelines->nr);
+               } else {
+                       printf("%s (%d):\n", list.items[i].path, onelines->nr);
+                       for (j = onelines->nr - 1; j >= 0; j--) {
+                               const char *msg = onelines->items[j].path;
+
+                               if (wrap_lines) {
+                                       int col = print_wrapped_text(msg, in1, in2, wrap);
+                                       if (col != wrap)
+                                               putchar('\n');
+                               }
+                               else
+                                       printf("      %s\n", msg);
+                       }
+                       putchar('\n');
+               }
+
+               onelines->strdup_paths = 1;
+               path_list_clear(onelines, 1);
+               free(onelines);
+               list.items[i].util = NULL;
+       }
+
+       list.strdup_paths = 1;
+       path_list_clear(&list, 1);
+       mailmap.strdup_paths = 1;
+       path_list_clear(&mailmap, 1);
+
+       return 0;
+}
diff --git a/builtin-show-branch.c b/builtin-show-branch.c
new file mode 100644 (file)
index 0000000..6dc835d
--- /dev/null
@@ -0,0 +1,916 @@
+#include "cache.h"
+#include "commit.h"
+#include "refs.h"
+#include "builtin.h"
+
+static const char show_branch_usage[] =
+"git-show-branch [--sparse] [--current] [--all] [--remotes] [--topo-order] [--more=count | --list | --independent | --merge-base ] [--topics] [<refs>...] | --reflog[=n[,b]] <branch>";
+static const char show_branch_usage_reflog[] =
+"--reflog is incompatible with --all, --remotes, --independent or --merge-base";
+
+static int default_num;
+static int default_alloc;
+static const char **default_arg;
+
+#define UNINTERESTING  01
+
+#define REV_SHIFT       2
+#define MAX_REVS       (FLAG_BITS - REV_SHIFT) /* should not exceed bits_per_int - REV_SHIFT */
+
+#define DEFAULT_REFLOG 4
+
+static struct commit *interesting(struct commit_list *list)
+{
+       while (list) {
+               struct commit *commit = list->item;
+               list = list->next;
+               if (commit->object.flags & UNINTERESTING)
+                       continue;
+               return commit;
+       }
+       return NULL;
+}
+
+static struct commit *pop_one_commit(struct commit_list **list_p)
+{
+       struct commit *commit;
+       struct commit_list *list;
+       list = *list_p;
+       commit = list->item;
+       *list_p = list->next;
+       free(list);
+       return commit;
+}
+
+struct commit_name {
+       const char *head_name; /* which head's ancestor? */
+       int generation; /* how many parents away from head_name */
+};
+
+/* Name the commit as nth generation ancestor of head_name;
+ * we count only the first-parent relationship for naming purposes.
+ */
+static void name_commit(struct commit *commit, const char *head_name, int nth)
+{
+       struct commit_name *name;
+       if (!commit->util)
+               commit->util = xmalloc(sizeof(struct commit_name));
+       name = commit->util;
+       name->head_name = head_name;
+       name->generation = nth;
+}
+
+/* Parent is the first parent of the commit.  We may name it
+ * as (n+1)th generation ancestor of the same head_name as
+ * commit is nth generation ancestor of, if that generation
+ * number is better than the name it already has.
+ */
+static void name_parent(struct commit *commit, struct commit *parent)
+{
+       struct commit_name *commit_name = commit->util;
+       struct commit_name *parent_name = parent->util;
+       if (!commit_name)
+               return;
+       if (!parent_name ||
+           commit_name->generation + 1 < parent_name->generation)
+               name_commit(parent, commit_name->head_name,
+                           commit_name->generation + 1);
+}
+
+static int name_first_parent_chain(struct commit *c)
+{
+       int i = 0;
+       while (c) {
+               struct commit *p;
+               if (!c->util)
+                       break;
+               if (!c->parents)
+                       break;
+               p = c->parents->item;
+               if (!p->util) {
+                       name_parent(c, p);
+                       i++;
+               }
+               else
+                       break;
+               c = p;
+       }
+       return i;
+}
+
+static void name_commits(struct commit_list *list,
+                        struct commit **rev,
+                        char **ref_name,
+                        int num_rev)
+{
+       struct commit_list *cl;
+       struct commit *c;
+       int i;
+
+       /* First give names to the given heads */
+       for (cl = list; cl; cl = cl->next) {
+               c = cl->item;
+               if (c->util)
+                       continue;
+               for (i = 0; i < num_rev; i++) {
+                       if (rev[i] == c) {
+                               name_commit(c, ref_name[i], 0);
+                               break;
+                       }
+               }
+       }
+
+       /* Then commits on the first parent ancestry chain */
+       do {
+               i = 0;
+               for (cl = list; cl; cl = cl->next) {
+                       i += name_first_parent_chain(cl->item);
+               }
+       } while (i);
+
+       /* Finally, any unnamed commits */
+       do {
+               i = 0;
+               for (cl = list; cl; cl = cl->next) {
+                       struct commit_list *parents;
+                       struct commit_name *n;
+                       int nth;
+                       c = cl->item;
+                       if (!c->util)
+                               continue;
+                       n = c->util;
+                       parents = c->parents;
+                       nth = 0;
+                       while (parents) {
+                               struct commit *p = parents->item;
+                               char newname[1000], *en;
+                               parents = parents->next;
+                               nth++;
+                               if (p->util)
+                                       continue;
+                               en = newname;
+                               switch (n->generation) {
+                               case 0:
+                                       en += sprintf(en, "%s", n->head_name);
+                                       break;
+                               case 1:
+                                       en += sprintf(en, "%s^", n->head_name);
+                                       break;
+                               default:
+                                       en += sprintf(en, "%s~%d",
+                                               n->head_name, n->generation);
+                                       break;
+                               }
+                               if (nth == 1)
+                                       en += sprintf(en, "^");
+                               else
+                                       en += sprintf(en, "^%d", nth);
+                               name_commit(p, xstrdup(newname), 0);
+                               i++;
+                               name_first_parent_chain(p);
+                       }
+               }
+       } while (i);
+}
+
+static int mark_seen(struct commit *commit, struct commit_list **seen_p)
+{
+       if (!commit->object.flags) {
+               commit_list_insert(commit, seen_p);
+               return 1;
+       }
+       return 0;
+}
+
+static void join_revs(struct commit_list **list_p,
+                     struct commit_list **seen_p,
+                     int num_rev, int extra)
+{
+       int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1);
+       int all_revs = all_mask & ~((1u << REV_SHIFT) - 1);
+
+       while (*list_p) {
+               struct commit_list *parents;
+               int still_interesting = !!interesting(*list_p);
+               struct commit *commit = pop_one_commit(list_p);
+               int flags = commit->object.flags & all_mask;
+
+               if (!still_interesting && extra <= 0)
+                       break;
+
+               mark_seen(commit, seen_p);
+               if ((flags & all_revs) == all_revs)
+                       flags |= UNINTERESTING;
+               parents = commit->parents;
+
+               while (parents) {
+                       struct commit *p = parents->item;
+                       int this_flag = p->object.flags;
+                       parents = parents->next;
+                       if ((this_flag & flags) == flags)
+                               continue;
+                       if (!p->object.parsed)
+                               parse_commit(p);
+                       if (mark_seen(p, seen_p) && !still_interesting)
+                               extra--;
+                       p->object.flags |= flags;
+                       insert_by_date(p, list_p);
+               }
+       }
+
+       /*
+        * Postprocess to complete well-poisoning.
+        *
+        * At this point we have all the commits we have seen in
+        * seen_p list.  Mark anything that can be reached from
+        * uninteresting commits not interesting.
+        */
+       for (;;) {
+               int changed = 0;
+               struct commit_list *s;
+               for (s = *seen_p; s; s = s->next) {
+                       struct commit *c = s->item;
+                       struct commit_list *parents;
+
+                       if (((c->object.flags & all_revs) != all_revs) &&
+                           !(c->object.flags & UNINTERESTING))
+                               continue;
+
+                       /* The current commit is either a merge base or
+                        * already uninteresting one.  Mark its parents
+                        * as uninteresting commits _only_ if they are
+                        * already parsed.  No reason to find new ones
+                        * here.
+                        */
+                       parents = c->parents;
+                       while (parents) {
+                               struct commit *p = parents->item;
+                               parents = parents->next;
+                               if (!(p->object.flags & UNINTERESTING)) {
+                                       p->object.flags |= UNINTERESTING;
+                                       changed = 1;
+                               }
+                       }
+               }
+               if (!changed)
+                       break;
+       }
+}
+
+static void show_one_commit(struct commit *commit, int no_name)
+{
+       struct strbuf pretty;
+       const char *pretty_str = "(unavailable)";
+       struct commit_name *name = commit->util;
+
+       strbuf_init(&pretty, 0);
+       if (commit->object.parsed) {
+               pretty_print_commit(CMIT_FMT_ONELINE, commit,
+                                   &pretty, 0, NULL, NULL, 0, 0);
+               pretty_str = pretty.buf;
+       }
+       if (!prefixcmp(pretty_str, "[PATCH] "))
+               pretty_str += 8;
+
+       if (!no_name) {
+               if (name && name->head_name) {
+                       printf("[%s", name->head_name);
+                       if (name->generation) {
+                               if (name->generation == 1)
+                                       printf("^");
+                               else
+                                       printf("~%d", name->generation);
+                       }
+                       printf("] ");
+               }
+               else
+                       printf("[%s] ",
+                              find_unique_abbrev(commit->object.sha1, 7));
+       }
+       puts(pretty_str);
+       strbuf_release(&pretty);
+}
+
+static char *ref_name[MAX_REVS + 1];
+static int ref_name_cnt;
+
+static const char *find_digit_prefix(const char *s, int *v)
+{
+       const char *p;
+       int ver;
+       char ch;
+
+       for (p = s, ver = 0;
+            '0' <= (ch = *p) && ch <= '9';
+            p++)
+               ver = ver * 10 + ch - '0';
+       *v = ver;
+       return p;
+}
+
+
+static int version_cmp(const char *a, const char *b)
+{
+       while (1) {
+               int va, vb;
+
+               a = find_digit_prefix(a, &va);
+               b = find_digit_prefix(b, &vb);
+               if (va != vb)
+                       return va - vb;
+
+               while (1) {
+                       int ca = *a;
+                       int cb = *b;
+                       if ('0' <= ca && ca <= '9')
+                               ca = 0;
+                       if ('0' <= cb && cb <= '9')
+                               cb = 0;
+                       if (ca != cb)
+                               return ca - cb;
+                       if (!ca)
+                               break;
+                       a++;
+                       b++;
+               }
+               if (!*a && !*b)
+                       return 0;
+       }
+}
+
+static int compare_ref_name(const void *a_, const void *b_)
+{
+       const char * const*a = a_, * const*b = b_;
+       return version_cmp(*a, *b);
+}
+
+static void sort_ref_range(int bottom, int top)
+{
+       qsort(ref_name + bottom, top - bottom, sizeof(ref_name[0]),
+             compare_ref_name);
+}
+
+static int append_ref(const char *refname, const unsigned char *sha1,
+                     int allow_dups)
+{
+       struct commit *commit = lookup_commit_reference_gently(sha1, 1);
+       int i;
+
+       if (!commit)
+               return 0;
+
+       if (!allow_dups) {
+               /* Avoid adding the same thing twice */
+               for (i = 0; i < ref_name_cnt; i++)
+                       if (!strcmp(refname, ref_name[i]))
+                               return 0;
+       }
+       if (MAX_REVS <= ref_name_cnt) {
+               fprintf(stderr, "warning: ignoring %s; "
+                       "cannot handle more than %d refs\n",
+                       refname, MAX_REVS);
+               return 0;
+       }
+       ref_name[ref_name_cnt++] = xstrdup(refname);
+       ref_name[ref_name_cnt] = NULL;
+       return 0;
+}
+
+static int append_head_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       unsigned char tmp[20];
+       int ofs = 11;
+       if (prefixcmp(refname, "refs/heads/"))
+               return 0;
+       /* If both heads/foo and tags/foo exists, get_sha1 would
+        * get confused.
+        */
+       if (get_sha1(refname + ofs, tmp) || hashcmp(tmp, sha1))
+               ofs = 5;
+       return append_ref(refname + ofs, sha1, 0);
+}
+
+static int append_remote_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       unsigned char tmp[20];
+       int ofs = 13;
+       if (prefixcmp(refname, "refs/remotes/"))
+               return 0;
+       /* If both heads/foo and tags/foo exists, get_sha1 would
+        * get confused.
+        */
+       if (get_sha1(refname + ofs, tmp) || hashcmp(tmp, sha1))
+               ofs = 5;
+       return append_ref(refname + ofs, sha1, 0);
+}
+
+static int append_tag_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       if (prefixcmp(refname, "refs/tags/"))
+               return 0;
+       return append_ref(refname + 5, sha1, 0);
+}
+
+static const char *match_ref_pattern = NULL;
+static int match_ref_slash = 0;
+static int count_slash(const char *s)
+{
+       int cnt = 0;
+       while (*s)
+               if (*s++ == '/')
+                       cnt++;
+       return cnt;
+}
+
+static int append_matching_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       /* we want to allow pattern hold/<asterisk> to show all
+        * branches under refs/heads/hold/, and v0.99.9? to show
+        * refs/tags/v0.99.9a and friends.
+        */
+       const char *tail;
+       int slash = count_slash(refname);
+       for (tail = refname; *tail && match_ref_slash < slash; )
+               if (*tail++ == '/')
+                       slash--;
+       if (!*tail)
+               return 0;
+       if (fnmatch(match_ref_pattern, tail, 0))
+               return 0;
+       if (!prefixcmp(refname, "refs/heads/"))
+               return append_head_ref(refname, sha1, flag, cb_data);
+       if (!prefixcmp(refname, "refs/tags/"))
+               return append_tag_ref(refname, sha1, flag, cb_data);
+       return append_ref(refname, sha1, 0);
+}
+
+static void snarf_refs(int head, int remotes)
+{
+       if (head) {
+               int orig_cnt = ref_name_cnt;
+               for_each_ref(append_head_ref, NULL);
+               sort_ref_range(orig_cnt, ref_name_cnt);
+       }
+       if (remotes) {
+               int orig_cnt = ref_name_cnt;
+               for_each_ref(append_remote_ref, NULL);
+               sort_ref_range(orig_cnt, ref_name_cnt);
+       }
+}
+
+static int rev_is_head(char *head, int headlen, char *name,
+                      unsigned char *head_sha1, unsigned char *sha1)
+{
+       if ((!head[0]) ||
+           (head_sha1 && sha1 && hashcmp(head_sha1, sha1)))
+               return 0;
+       if (!prefixcmp(head, "refs/heads/"))
+               head += 11;
+       if (!prefixcmp(name, "refs/heads/"))
+               name += 11;
+       else if (!prefixcmp(name, "heads/"))
+               name += 6;
+       return !strcmp(head, name);
+}
+
+static int show_merge_base(struct commit_list *seen, int num_rev)
+{
+       int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1);
+       int all_revs = all_mask & ~((1u << REV_SHIFT) - 1);
+       int exit_status = 1;
+
+       while (seen) {
+               struct commit *commit = pop_one_commit(&seen);
+               int flags = commit->object.flags & all_mask;
+               if (!(flags & UNINTERESTING) &&
+                   ((flags & all_revs) == all_revs)) {
+                       puts(sha1_to_hex(commit->object.sha1));
+                       exit_status = 0;
+                       commit->object.flags |= UNINTERESTING;
+               }
+       }
+       return exit_status;
+}
+
+static int show_independent(struct commit **rev,
+                           int num_rev,
+                           char **ref_name,
+                           unsigned int *rev_mask)
+{
+       int i;
+
+       for (i = 0; i < num_rev; i++) {
+               struct commit *commit = rev[i];
+               unsigned int flag = rev_mask[i];
+
+               if (commit->object.flags == flag)
+                       puts(sha1_to_hex(commit->object.sha1));
+               commit->object.flags |= UNINTERESTING;
+       }
+       return 0;
+}
+
+static void append_one_rev(const char *av)
+{
+       unsigned char revkey[20];
+       if (!get_sha1(av, revkey)) {
+               append_ref(av, revkey, 0);
+               return;
+       }
+       if (strchr(av, '*') || strchr(av, '?') || strchr(av, '[')) {
+               /* glob style match */
+               int saved_matches = ref_name_cnt;
+               match_ref_pattern = av;
+               match_ref_slash = count_slash(av);
+               for_each_ref(append_matching_ref, NULL);
+               if (saved_matches == ref_name_cnt &&
+                   ref_name_cnt < MAX_REVS)
+                       error("no matching refs with %s", av);
+               if (saved_matches + 1 < ref_name_cnt)
+                       sort_ref_range(saved_matches, ref_name_cnt);
+               return;
+       }
+       die("bad sha1 reference %s", av);
+}
+
+static int git_show_branch_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "showbranch.default")) {
+               if (default_alloc <= default_num + 1) {
+                       default_alloc = default_alloc * 3 / 2 + 20;
+                       default_arg = xrealloc(default_arg, sizeof *default_arg * default_alloc);
+               }
+               default_arg[default_num++] = xstrdup(value);
+               default_arg[default_num] = NULL;
+               return 0;
+       }
+
+       return git_default_config(var, value);
+}
+
+static int omit_in_dense(struct commit *commit, struct commit **rev, int n)
+{
+       /* If the commit is tip of the named branches, do not
+        * omit it.
+        * Otherwise, if it is a merge that is reachable from only one
+        * tip, it is not that interesting.
+        */
+       int i, flag, count;
+       for (i = 0; i < n; i++)
+               if (rev[i] == commit)
+                       return 0;
+       flag = commit->object.flags;
+       for (i = count = 0; i < n; i++) {
+               if (flag & (1u << (i + REV_SHIFT)))
+                       count++;
+       }
+       if (count == 1)
+               return 1;
+       return 0;
+}
+
+static void parse_reflog_param(const char *arg, int *cnt, const char **base)
+{
+       char *ep;
+       *cnt = strtoul(arg, &ep, 10);
+       if (*ep == ',')
+               *base = ep + 1;
+       else if (*ep)
+               die("unrecognized reflog param '%s'", arg + 9);
+       else
+               *base = NULL;
+       if (*cnt <= 0)
+               *cnt = DEFAULT_REFLOG;
+}
+
+int cmd_show_branch(int ac, const char **av, const char *prefix)
+{
+       struct commit *rev[MAX_REVS], *commit;
+       char *reflog_msg[MAX_REVS];
+       struct commit_list *list = NULL, *seen = NULL;
+       unsigned int rev_mask[MAX_REVS];
+       int num_rev, i, extra = 0;
+       int all_heads = 0, all_remotes = 0;
+       int all_mask, all_revs;
+       int lifo = 1;
+       char head[128];
+       const char *head_p;
+       int head_len;
+       unsigned char head_sha1[20];
+       int merge_base = 0;
+       int independent = 0;
+       int no_name = 0;
+       int sha1_name = 0;
+       int shown_merge_point = 0;
+       int with_current_branch = 0;
+       int head_at = -1;
+       int topics = 0;
+       int dense = 1;
+       int reflog = 0;
+       const char *reflog_base = NULL;
+
+       git_config(git_show_branch_config);
+
+       /* If nothing is specified, try the default first */
+       if (ac == 1 && default_num) {
+               ac = default_num + 1;
+               av = default_arg - 1; /* ick; we would not address av[0] */
+       }
+
+       while (1 < ac && av[1][0] == '-') {
+               const char *arg = av[1];
+               if (!strcmp(arg, "--")) {
+                       ac--; av++;
+                       break;
+               }
+               else if (!strcmp(arg, "--all") || !strcmp(arg, "-a"))
+                       all_heads = all_remotes = 1;
+               else if (!strcmp(arg, "--remotes") || !strcmp(arg, "-r"))
+                       all_remotes = 1;
+               else if (!strcmp(arg, "--more"))
+                       extra = 1;
+               else if (!strcmp(arg, "--list"))
+                       extra = -1;
+               else if (!strcmp(arg, "--no-name"))
+                       no_name = 1;
+               else if (!strcmp(arg, "--current"))
+                       with_current_branch = 1;
+               else if (!strcmp(arg, "--sha1-name"))
+                       sha1_name = 1;
+               else if (!prefixcmp(arg, "--more="))
+                       extra = atoi(arg + 7);
+               else if (!strcmp(arg, "--merge-base"))
+                       merge_base = 1;
+               else if (!strcmp(arg, "--independent"))
+                       independent = 1;
+               else if (!strcmp(arg, "--topo-order"))
+                       lifo = 1;
+               else if (!strcmp(arg, "--topics"))
+                       topics = 1;
+               else if (!strcmp(arg, "--sparse"))
+                       dense = 0;
+               else if (!strcmp(arg, "--date-order"))
+                       lifo = 0;
+               else if (!strcmp(arg, "--reflog") || !strcmp(arg, "-g")) {
+                       reflog = DEFAULT_REFLOG;
+               }
+               else if (!prefixcmp(arg, "--reflog="))
+                       parse_reflog_param(arg + 9, &reflog, &reflog_base);
+               else if (!prefixcmp(arg, "-g="))
+                       parse_reflog_param(arg + 3, &reflog, &reflog_base);
+               else
+                       usage(show_branch_usage);
+               ac--; av++;
+       }
+       ac--; av++;
+
+       if (extra || reflog) {
+               /* "listing" mode is incompatible with
+                * independent nor merge-base modes.
+                */
+               if (independent || merge_base)
+                       usage(show_branch_usage);
+               if (reflog && ((0 < extra) || all_heads || all_remotes))
+                       /*
+                        * Asking for --more in reflog mode does not
+                        * make sense.  --list is Ok.
+                        *
+                        * Also --all and --remotes do not make sense either.
+                        */
+                       usage(show_branch_usage_reflog);
+       }
+
+       /* If nothing is specified, show all branches by default */
+       if (ac + all_heads + all_remotes == 0)
+               all_heads = 1;
+
+       if (reflog) {
+               unsigned char sha1[20];
+               char nth_desc[256];
+               char *ref;
+               int base = 0;
+
+               if (ac == 0) {
+                       static const char *fake_av[2];
+                       const char *refname;
+
+                       refname = resolve_ref("HEAD", sha1, 1, NULL);
+                       fake_av[0] = xstrdup(refname);
+                       fake_av[1] = NULL;
+                       av = fake_av;
+                       ac = 1;
+               }
+               if (ac != 1)
+                       die("--reflog option needs one branch name");
+
+               if (MAX_REVS < reflog)
+                       die("Only %d entries can be shown at one time.",
+                           MAX_REVS);
+               if (!dwim_ref(*av, strlen(*av), sha1, &ref))
+                       die("No such ref %s", *av);
+
+               /* Has the base been specified? */
+               if (reflog_base) {
+                       char *ep;
+                       base = strtoul(reflog_base, &ep, 10);
+                       if (*ep) {
+                               /* Ah, that is a date spec... */
+                               unsigned long at;
+                               at = approxidate(reflog_base);
+                               read_ref_at(ref, at, -1, sha1, NULL,
+                                           NULL, NULL, &base);
+                       }
+               }
+
+               for (i = 0; i < reflog; i++) {
+                       char *logmsg, *m;
+                       const char *msg;
+                       unsigned long timestamp;
+                       int tz;
+
+                       if (read_ref_at(ref, 0, base+i, sha1, &logmsg,
+                                       &timestamp, &tz, NULL)) {
+                               reflog = i;
+                               break;
+                       }
+                       msg = strchr(logmsg, '\t');
+                       if (!msg)
+                               msg = "(none)";
+                       else
+                               msg++;
+                       m = xmalloc(strlen(msg) + 200);
+                       sprintf(m, "(%s) %s",
+                               show_date(timestamp, tz, 1),
+                               msg);
+                       reflog_msg[i] = m;
+                       free(logmsg);
+                       sprintf(nth_desc, "%s@{%d}", *av, base+i);
+                       append_ref(nth_desc, sha1, 1);
+               }
+       }
+       else if (all_heads + all_remotes)
+               snarf_refs(all_heads, all_remotes);
+       else {
+               while (0 < ac) {
+                       append_one_rev(*av);
+                       ac--; av++;
+               }
+       }
+
+       head_p = resolve_ref("HEAD", head_sha1, 1, NULL);
+       if (head_p) {
+               head_len = strlen(head_p);
+               memcpy(head, head_p, head_len + 1);
+       }
+       else {
+               head_len = 0;
+               head[0] = 0;
+       }
+
+       if (with_current_branch && head_p) {
+               int has_head = 0;
+               for (i = 0; !has_head && i < ref_name_cnt; i++) {
+                       /* We are only interested in adding the branch
+                        * HEAD points at.
+                        */
+                       if (rev_is_head(head,
+                                       head_len,
+                                       ref_name[i],
+                                       head_sha1, NULL))
+                               has_head++;
+               }
+               if (!has_head) {
+                       int pfxlen = strlen("refs/heads/");
+                       append_one_rev(head + pfxlen);
+               }
+       }
+
+       if (!ref_name_cnt) {
+               fprintf(stderr, "No revs to be shown.\n");
+               exit(0);
+       }
+
+       for (num_rev = 0; ref_name[num_rev]; num_rev++) {
+               unsigned char revkey[20];
+               unsigned int flag = 1u << (num_rev + REV_SHIFT);
+
+               if (MAX_REVS <= num_rev)
+                       die("cannot handle more than %d revs.", MAX_REVS);
+               if (get_sha1(ref_name[num_rev], revkey))
+                       die("'%s' is not a valid ref.", ref_name[num_rev]);
+               commit = lookup_commit_reference(revkey);
+               if (!commit)
+                       die("cannot find commit %s (%s)",
+                           ref_name[num_rev], revkey);
+               parse_commit(commit);
+               mark_seen(commit, &seen);
+
+               /* rev#0 uses bit REV_SHIFT, rev#1 uses bit REV_SHIFT+1,
+                * and so on.  REV_SHIFT bits from bit 0 are used for
+                * internal bookkeeping.
+                */
+               commit->object.flags |= flag;
+               if (commit->object.flags == flag)
+                       insert_by_date(commit, &list);
+               rev[num_rev] = commit;
+       }
+       for (i = 0; i < num_rev; i++)
+               rev_mask[i] = rev[i]->object.flags;
+
+       if (0 <= extra)
+               join_revs(&list, &seen, num_rev, extra);
+
+       sort_by_date(&seen);
+
+       if (merge_base)
+               return show_merge_base(seen, num_rev);
+
+       if (independent)
+               return show_independent(rev, num_rev, ref_name, rev_mask);
+
+       /* Show list; --more=-1 means list-only */
+       if (1 < num_rev || extra < 0) {
+               for (i = 0; i < num_rev; i++) {
+                       int j;
+                       int is_head = rev_is_head(head,
+                                                 head_len,
+                                                 ref_name[i],
+                                                 head_sha1,
+                                                 rev[i]->object.sha1);
+                       if (extra < 0)
+                               printf("%c [%s] ",
+                                      is_head ? '*' : ' ', ref_name[i]);
+                       else {
+                               for (j = 0; j < i; j++)
+                                       putchar(' ');
+                               printf("%c [%s] ",
+                                      is_head ? '*' : '!', ref_name[i]);
+                       }
+
+                       if (!reflog) {
+                               /* header lines never need name */
+                               show_one_commit(rev[i], 1);
+                       }
+                       else
+                               puts(reflog_msg[i]);
+
+                       if (is_head)
+                               head_at = i;
+               }
+               if (0 <= extra) {
+                       for (i = 0; i < num_rev; i++)
+                               putchar('-');
+                       putchar('\n');
+               }
+       }
+       if (extra < 0)
+               exit(0);
+
+       /* Sort topologically */
+       sort_in_topological_order(&seen, lifo);
+
+       /* Give names to commits */
+       if (!sha1_name && !no_name)
+               name_commits(seen, rev, ref_name, num_rev);
+
+       all_mask = ((1u << (REV_SHIFT + num_rev)) - 1);
+       all_revs = all_mask & ~((1u << REV_SHIFT) - 1);
+
+       while (seen) {
+               struct commit *commit = pop_one_commit(&seen);
+               int this_flag = commit->object.flags;
+               int is_merge_point = ((this_flag & all_revs) == all_revs);
+
+               shown_merge_point |= is_merge_point;
+
+               if (1 < num_rev) {
+                       int is_merge = !!(commit->parents &&
+                                         commit->parents->next);
+                       if (topics &&
+                           !is_merge_point &&
+                           (this_flag & (1u << REV_SHIFT)))
+                               continue;
+                       if (dense && is_merge &&
+                           omit_in_dense(commit, rev, num_rev))
+                               continue;
+                       for (i = 0; i < num_rev; i++) {
+                               int mark;
+                               if (!(this_flag & (1u << (i + REV_SHIFT))))
+                                       mark = ' ';
+                               else if (is_merge)
+                                       mark = '-';
+                               else if (i == head_at)
+                                       mark = '*';
+                               else
+                                       mark = '+';
+                               putchar(mark);
+                       }
+                       putchar(' ');
+               }
+               show_one_commit(commit, no_name);
+
+               if (shown_merge_point && --extra < 0)
+                       break;
+       }
+       return 0;
+}
diff --git a/builtin-show-ref.c b/builtin-show-ref.c
new file mode 100644 (file)
index 0000000..65051d1
--- /dev/null
@@ -0,0 +1,253 @@
+#include "builtin.h"
+#include "cache.h"
+#include "refs.h"
+#include "object.h"
+#include "tag.h"
+#include "path-list.h"
+
+static const char show_ref_usage[] = "git show-ref [-q|--quiet] [--verify] [-h|--head] [-d|--dereference] [-s|--hash[=<length>]] [--abbrev[=<length>]] [--tags] [--heads] [--] [pattern*] < ref-list";
+
+static int deref_tags = 0, show_head = 0, tags_only = 0, heads_only = 0,
+       found_match = 0, verify = 0, quiet = 0, hash_only = 0, abbrev = 0;
+static const char **pattern;
+
+static void show_one(const char *refname, const unsigned char *sha1)
+{
+       const char *hex = find_unique_abbrev(sha1, abbrev);
+       if (hash_only)
+               printf("%s\n", hex);
+       else
+               printf("%s %s\n", hex, refname);
+}
+
+static int show_ref(const char *refname, const unsigned char *sha1, int flag, void *cbdata)
+{
+       struct object *obj;
+       const char *hex;
+       unsigned char peeled[20];
+
+       if (tags_only || heads_only) {
+               int match;
+
+               match = heads_only && !prefixcmp(refname, "refs/heads/");
+               match |= tags_only && !prefixcmp(refname, "refs/tags/");
+               if (!match)
+                       return 0;
+       }
+       if (pattern) {
+               int reflen = strlen(refname);
+               const char **p = pattern, *m;
+               while ((m = *p++) != NULL) {
+                       int len = strlen(m);
+                       if (len > reflen)
+                               continue;
+                       if (memcmp(m, refname + reflen - len, len))
+                               continue;
+                       if (len == reflen)
+                               goto match;
+                       /* "--verify" requires an exact match */
+                       if (verify)
+                               continue;
+                       if (refname[reflen - len - 1] == '/')
+                               goto match;
+               }
+               return 0;
+       }
+
+match:
+       found_match++;
+
+       /* This changes the semantics slightly that even under quiet we
+        * detect and return error if the repository is corrupt and
+        * ref points at a nonexistent object.
+        */
+       if (!has_sha1_file(sha1))
+               die("git-show-ref: bad ref %s (%s)", refname,
+                   sha1_to_hex(sha1));
+
+       if (quiet)
+               return 0;
+
+       show_one(refname, sha1);
+
+       if (!deref_tags)
+               return 0;
+
+       if ((flag & REF_ISPACKED) && !peel_ref(refname, peeled)) {
+               if (!is_null_sha1(peeled)) {
+                       hex = find_unique_abbrev(peeled, abbrev);
+                       printf("%s %s^{}\n", hex, refname);
+               }
+       }
+       else {
+               obj = parse_object(sha1);
+               if (!obj)
+                       die("git-show-ref: bad ref %s (%s)", refname,
+                           sha1_to_hex(sha1));
+               if (obj->type == OBJ_TAG) {
+                       obj = deref_tag(obj, refname, 0);
+                       hex = find_unique_abbrev(obj->sha1, abbrev);
+                       printf("%s %s^{}\n", hex, refname);
+               }
+       }
+       return 0;
+}
+
+static int add_existing(const char *refname, const unsigned char *sha1, int flag, void *cbdata)
+{
+       struct path_list *list = (struct path_list *)cbdata;
+       path_list_insert(refname, list);
+       return 0;
+}
+
+/*
+ * read "^(?:<anything>\s)?<refname>(?:\^\{\})?$" from the standard input,
+ * and
+ * (1) strip "^{}" at the end of line if any;
+ * (2) ignore if match is provided and does not head-match refname;
+ * (3) warn if refname is not a well-formed refname and skip;
+ * (4) ignore if refname is a ref that exists in the local repository;
+ * (5) otherwise output the line.
+ */
+static int exclude_existing(const char *match)
+{
+       static struct path_list existing_refs = { NULL, 0, 0, 0 };
+       char buf[1024];
+       int matchlen = match ? strlen(match) : 0;
+
+       for_each_ref(add_existing, &existing_refs);
+       while (fgets(buf, sizeof(buf), stdin)) {
+               char *ref;
+               int len = strlen(buf);
+
+               if (len > 0 && buf[len - 1] == '\n')
+                       buf[--len] = '\0';
+               if (3 <= len && !strcmp(buf + len - 3, "^{}")) {
+                       len -= 3;
+                       buf[len] = '\0';
+               }
+               for (ref = buf + len; buf < ref; ref--)
+                       if (isspace(ref[-1]))
+                               break;
+               if (match) {
+                       int reflen = buf + len - ref;
+                       if (reflen < matchlen)
+                               continue;
+                       if (strncmp(ref, match, matchlen))
+                               continue;
+               }
+               if (check_ref_format(ref)) {
+                       fprintf(stderr, "warning: ref '%s' ignored\n", ref);
+                       continue;
+               }
+               if (!path_list_has_path(&existing_refs, ref)) {
+                       printf("%s\n", buf);
+               }
+       }
+       return 0;
+}
+
+int cmd_show_ref(int argc, const char **argv, const char *prefix)
+{
+       int i;
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (*arg != '-') {
+                       pattern = argv + i;
+                       break;
+               }
+               if (!strcmp(arg, "--")) {
+                       pattern = argv + i + 1;
+                       if (!*pattern)
+                               pattern = NULL;
+                       break;
+               }
+               if (!strcmp(arg, "-q") || !strcmp(arg, "--quiet")) {
+                       quiet = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-h") || !strcmp(arg, "--head")) {
+                       show_head = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-d") || !strcmp(arg, "--dereference")) {
+                       deref_tags = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "-s") || !strcmp(arg, "--hash")) {
+                       hash_only = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--hash=") ||
+                   (!prefixcmp(arg, "--abbrev") &&
+                    (arg[8] == '=' || arg[8] == '\0'))) {
+                       if (arg[2] != 'h' && !arg[8])
+                               /* --abbrev only */
+                               abbrev = DEFAULT_ABBREV;
+                       else {
+                               /* --hash= or --abbrev= */
+                               char *end;
+                               if (arg[2] == 'h') {
+                                       hash_only = 1;
+                                       arg += 7;
+                               }
+                               else
+                                       arg += 9;
+                               abbrev = strtoul(arg, &end, 10);
+                               if (*end || abbrev > 40)
+                                       usage(show_ref_usage);
+                               if (abbrev < MINIMUM_ABBREV)
+                                       abbrev = MINIMUM_ABBREV;
+                       }
+                       continue;
+               }
+               if (!strcmp(arg, "--verify")) {
+                       verify = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--tags")) {
+                       tags_only = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--heads")) {
+                       heads_only = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--exclude-existing"))
+                       return exclude_existing(NULL);
+               if (!prefixcmp(arg, "--exclude-existing="))
+                       return exclude_existing(arg + 19);
+               usage(show_ref_usage);
+       }
+
+       if (verify) {
+               if (!pattern)
+                       die("--verify requires a reference");
+               while (*pattern) {
+                       unsigned char sha1[20];
+
+                       if (!prefixcmp(*pattern, "refs/") &&
+                           resolve_ref(*pattern, sha1, 1, NULL)) {
+                               if (!quiet)
+                                       show_one(*pattern, sha1);
+                       }
+                       else if (!quiet)
+                               die("'%s' - not a valid ref", *pattern);
+                       else
+                               return 1;
+                       pattern++;
+               }
+               return 0;
+       }
+
+       if (show_head)
+               head_ref(show_ref, NULL);
+       for_each_ref(show_ref, NULL);
+       if (!found_match) {
+               if (verify && !quiet)
+                       die("No match");
+               return 1;
+       }
+       return 0;
+}
diff --git a/builtin-stripspace.c b/builtin-stripspace.c
new file mode 100644 (file)
index 0000000..c0b2130
--- /dev/null
@@ -0,0 +1,89 @@
+#include "builtin.h"
+#include "cache.h"
+
+/*
+ * Returns the length of a line, without trailing spaces.
+ *
+ * If the line ends with newline, it will be removed too.
+ */
+static size_t cleanup(char *line, size_t len)
+{
+       while (len) {
+               unsigned char c = line[len - 1];
+               if (!isspace(c))
+                       break;
+               len--;
+       }
+
+       return len;
+}
+
+/*
+ * Remove empty lines from the beginning and end
+ * and also trailing spaces from every line.
+ *
+ * Note that the buffer will not be NUL-terminated.
+ *
+ * Turn multiple consecutive empty lines between paragraphs
+ * into just one empty line.
+ *
+ * If the input has only empty lines and spaces,
+ * no output will be produced.
+ *
+ * If last line does not have a newline at the end, one is added.
+ *
+ * Enable skip_comments to skip every line starting with "#".
+ */
+void stripspace(struct strbuf *sb, int skip_comments)
+{
+       int empties = 0;
+       size_t i, j, len, newlen;
+       char *eol;
+
+       /* We may have to add a newline. */
+       strbuf_grow(sb, 1);
+
+       for (i = j = 0; i < sb->len; i += len, j += newlen) {
+               eol = memchr(sb->buf + i, '\n', sb->len - i);
+               len = eol ? eol - (sb->buf + i) + 1 : sb->len - i;
+
+               if (skip_comments && len && sb->buf[i] == '#') {
+                       newlen = 0;
+                       continue;
+               }
+               newlen = cleanup(sb->buf + i, len);
+
+               /* Not just an empty line? */
+               if (newlen) {
+                       if (empties > 0 && j > 0)
+                               sb->buf[j++] = '\n';
+                       empties = 0;
+                       memmove(sb->buf + j, sb->buf + i, newlen);
+                       sb->buf[newlen + j++] = '\n';
+               } else {
+                       empties++;
+               }
+       }
+
+       strbuf_setlen(sb, j);
+}
+
+int cmd_stripspace(int argc, const char **argv, const char *prefix)
+{
+       struct strbuf buf;
+       int strip_comments = 0;
+
+       if (argc > 1 && (!strcmp(argv[1], "-s") ||
+                               !strcmp(argv[1], "--strip-comments")))
+               strip_comments = 1;
+
+       strbuf_init(&buf, 0);
+       if (strbuf_read(&buf, 0, 1024) < 0)
+               die("could not read the input");
+
+       stripspace(&buf, strip_comments);
+
+       write_or_die(1, buf.buf, buf.len);
+       strbuf_release(&buf);
+       return 0;
+}
diff --git a/builtin-symbolic-ref.c b/builtin-symbolic-ref.c
new file mode 100644 (file)
index 0000000..d33982b
--- /dev/null
@@ -0,0 +1,53 @@
+#include "builtin.h"
+#include "cache.h"
+#include "refs.h"
+#include "parse-options.h"
+
+static const char * const git_symbolic_ref_usage[] = {
+       "git-symbolic-ref [options] name [ref]",
+       NULL
+};
+
+static void check_symref(const char *HEAD, int quiet)
+{
+       unsigned char sha1[20];
+       int flag;
+       const char *refs_heads_master = resolve_ref(HEAD, sha1, 0, &flag);
+
+       if (!refs_heads_master)
+               die("No such ref: %s", HEAD);
+       else if (!(flag & REF_ISSYMREF)) {
+               if (!quiet)
+                       die("ref %s is not a symbolic ref", HEAD);
+               else
+                       exit(1);
+       }
+       puts(refs_heads_master);
+}
+
+int cmd_symbolic_ref(int argc, const char **argv, const char *prefix)
+{
+       int quiet = 0;
+       const char *msg = NULL;
+       struct option options[] = {
+               OPT__QUIET(&quiet),
+               OPT_STRING('m', NULL, &msg, "reason", "reason of the update"),
+               OPT_END(),
+       };
+
+       git_config(git_default_config);
+       argc = parse_options(argc, argv, options, git_symbolic_ref_usage, 0);
+       if (msg &&!*msg)
+               die("Refusing to perform update with empty message");
+       switch (argc) {
+       case 1:
+               check_symref(argv[0], quiet);
+               break;
+       case 2:
+               create_symref(argv[0], argv[1], msg);
+               break;
+       default:
+               usage_with_options(git_symbolic_ref_usage, options);
+       }
+       return 0;
+}
diff --git a/builtin-tag.c b/builtin-tag.c
new file mode 100644 (file)
index 0000000..729389b
--- /dev/null
@@ -0,0 +1,462 @@
+/*
+ * Builtin "git tag"
+ *
+ * Copyright (c) 2007 Kristian Høgsberg <krh@redhat.com>,
+ *                    Carlos Rica <jasampler@gmail.com>
+ * Based on git-tag.sh and mktag.c by Linus Torvalds.
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "refs.h"
+#include "tag.h"
+#include "run-command.h"
+#include "parse-options.h"
+
+static const char * const git_tag_usage[] = {
+       "git-tag [-a|-s|-u <key-id>] [-f] [-m <msg>|-F <file>] <tagname> [<head>]",
+       "git-tag -d <tagname>...",
+       "git-tag [-n [<num>]] -l [<pattern>]",
+       "git-tag -v <tagname>...",
+       NULL
+};
+
+static char signingkey[1000];
+
+void launch_editor(const char *path, struct strbuf *buffer, const char *const *env)
+{
+       const char *editor, *terminal;
+
+       editor = getenv("GIT_EDITOR");
+       if (!editor && editor_program)
+               editor = editor_program;
+       if (!editor)
+               editor = getenv("VISUAL");
+       if (!editor)
+               editor = getenv("EDITOR");
+
+       terminal = getenv("TERM");
+       if (!editor && (!terminal || !strcmp(terminal, "dumb"))) {
+               fprintf(stderr,
+               "Terminal is dumb but no VISUAL nor EDITOR defined.\n"
+               "Please supply the message using either -m or -F option.\n");
+               exit(1);
+       }
+
+       if (!editor)
+               editor = "vi";
+
+       if (strcmp(editor, ":")) {
+               const char *args[] = { editor, path, NULL };
+
+               if (run_command_v_opt_cd_env(args, 0, NULL, env))
+                       die("There was a problem with the editor %s.", editor);
+       }
+
+       if (strbuf_read_file(buffer, path, 0) < 0)
+               die("could not read message file '%s': %s",
+                   path, strerror(errno));
+}
+
+struct tag_filter {
+       const char *pattern;
+       int lines;
+};
+
+#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----"
+
+static int show_reference(const char *refname, const unsigned char *sha1,
+                         int flag, void *cb_data)
+{
+       struct tag_filter *filter = cb_data;
+
+       if (!fnmatch(filter->pattern, refname, 0)) {
+               int i;
+               unsigned long size;
+               enum object_type type;
+               char *buf, *sp, *eol;
+               size_t len;
+
+               if (!filter->lines) {
+                       printf("%s\n", refname);
+                       return 0;
+               }
+               printf("%-15s ", refname);
+
+               buf = read_sha1_file(sha1, &type, &size);
+               if (!buf || !size)
+                       return 0;
+
+               /* skip header */
+               sp = strstr(buf, "\n\n");
+               if (!sp) {
+                       free(buf);
+                       return 0;
+               }
+               /* only take up to "lines" lines, and strip the signature */
+               for (i = 0, sp += 2;
+                               i < filter->lines && sp < buf + size &&
+                               prefixcmp(sp, PGP_SIGNATURE "\n");
+                               i++) {
+                       if (i)
+                               printf("\n    ");
+                       eol = memchr(sp, '\n', size - (sp - buf));
+                       len = eol ? eol - sp : size - (sp - buf);
+                       fwrite(sp, len, 1, stdout);
+                       if (!eol)
+                               break;
+                       sp = eol + 1;
+               }
+               putchar('\n');
+               free(buf);
+       }
+
+       return 0;
+}
+
+static int list_tags(const char *pattern, int lines)
+{
+       struct tag_filter filter;
+
+       if (pattern == NULL)
+               pattern = "*";
+
+       filter.pattern = pattern;
+       filter.lines = lines;
+
+       for_each_tag_ref(show_reference, (void *) &filter);
+
+       return 0;
+}
+
+typedef int (*each_tag_name_fn)(const char *name, const char *ref,
+                               const unsigned char *sha1);
+
+static int for_each_tag_name(const char **argv, each_tag_name_fn fn)
+{
+       const char **p;
+       char ref[PATH_MAX];
+       int had_error = 0;
+       unsigned char sha1[20];
+
+       for (p = argv; *p; p++) {
+               if (snprintf(ref, sizeof(ref), "refs/tags/%s", *p)
+                                       >= sizeof(ref)) {
+                       error("tag name too long: %.*s...", 50, *p);
+                       had_error = 1;
+                       continue;
+               }
+               if (!resolve_ref(ref, sha1, 1, NULL)) {
+                       error("tag '%s' not found.", *p);
+                       had_error = 1;
+                       continue;
+               }
+               if (fn(*p, ref, sha1))
+                       had_error = 1;
+       }
+       return had_error;
+}
+
+static int delete_tag(const char *name, const char *ref,
+                               const unsigned char *sha1)
+{
+       if (delete_ref(ref, sha1))
+               return 1;
+       printf("Deleted tag '%s'\n", name);
+       return 0;
+}
+
+static int verify_tag(const char *name, const char *ref,
+                               const unsigned char *sha1)
+{
+       const char *argv_verify_tag[] = {"git-verify-tag",
+                                       "-v", "SHA1_HEX", NULL};
+       argv_verify_tag[2] = sha1_to_hex(sha1);
+
+       if (run_command_v_opt(argv_verify_tag, 0))
+               return error("could not verify the tag '%s'", name);
+       return 0;
+}
+
+static int do_sign(struct strbuf *buffer)
+{
+       struct child_process gpg;
+       const char *args[4];
+       char *bracket;
+       int len;
+
+       if (!*signingkey) {
+               if (strlcpy(signingkey, git_committer_info(1),
+                               sizeof(signingkey)) > sizeof(signingkey) - 1)
+                       return error("committer info too long.");
+               bracket = strchr(signingkey, '>');
+               if (bracket)
+                       bracket[1] = '\0';
+       }
+
+       /* When the username signingkey is bad, program could be terminated
+        * because gpg exits without reading and then write gets SIGPIPE. */
+       signal(SIGPIPE, SIG_IGN);
+
+       memset(&gpg, 0, sizeof(gpg));
+       gpg.argv = args;
+       gpg.in = -1;
+       gpg.out = -1;
+       args[0] = "gpg";
+       args[1] = "-bsau";
+       args[2] = signingkey;
+       args[3] = NULL;
+
+       if (start_command(&gpg))
+               return error("could not run gpg.");
+
+       if (write_in_full(gpg.in, buffer->buf, buffer->len) != buffer->len) {
+               close(gpg.in);
+               finish_command(&gpg);
+               return error("gpg did not accept the tag data");
+       }
+       close(gpg.in);
+       gpg.close_in = 0;
+       len = strbuf_read(buffer, gpg.out, 1024);
+
+       if (finish_command(&gpg) || !len || len < 0)
+               return error("gpg failed to sign the tag");
+
+       if (len < 0)
+               return error("could not read the entire signature from gpg.");
+
+       return 0;
+}
+
+static const char tag_template[] =
+       "\n"
+       "#\n"
+       "# Write a tag message\n"
+       "#\n";
+
+static int git_tag_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "user.signingkey")) {
+               if (!value)
+                       die("user.signingkey without value");
+               if (strlcpy(signingkey, value, sizeof(signingkey))
+                                               >= sizeof(signingkey))
+                       die("user.signingkey value too long");
+               return 0;
+       }
+
+       return git_default_config(var, value);
+}
+
+static void write_tag_body(int fd, const unsigned char *sha1)
+{
+       unsigned long size;
+       enum object_type type;
+       char *buf, *sp, *eob;
+       size_t len;
+
+       buf = read_sha1_file(sha1, &type, &size);
+       if (!buf)
+               return;
+       /* skip header */
+       sp = strstr(buf, "\n\n");
+
+       if (!sp || !size || type != OBJ_TAG) {
+               free(buf);
+               return;
+       }
+       sp += 2; /* skip the 2 LFs */
+       eob = strstr(sp, "\n" PGP_SIGNATURE "\n");
+       if (eob)
+               len = eob - sp;
+       else
+               len = buf + size - sp;
+       write_or_die(fd, sp, len);
+
+       free(buf);
+}
+
+static void create_tag(const unsigned char *object, const char *tag,
+                      struct strbuf *buf, int message, int sign,
+                      unsigned char *prev, unsigned char *result)
+{
+       enum object_type type;
+       char header_buf[1024];
+       int header_len;
+
+       type = sha1_object_info(object, NULL);
+       if (type <= OBJ_NONE)
+           die("bad object type.");
+
+       header_len = snprintf(header_buf, sizeof(header_buf),
+                         "object %s\n"
+                         "type %s\n"
+                         "tag %s\n"
+                         "tagger %s\n\n",
+                         sha1_to_hex(object),
+                         typename(type),
+                         tag,
+                         git_committer_info(1));
+
+       if (header_len > sizeof(header_buf) - 1)
+               die("tag header too big.");
+
+       if (!message) {
+               char *path;
+               int fd;
+
+               /* write the template message before editing: */
+               path = xstrdup(git_path("TAG_EDITMSG"));
+               fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
+               if (fd < 0)
+                       die("could not create file '%s': %s",
+                                               path, strerror(errno));
+
+               if (!is_null_sha1(prev))
+                       write_tag_body(fd, prev);
+               else
+                       write_or_die(fd, tag_template, strlen(tag_template));
+               close(fd);
+
+               launch_editor(path, buf, NULL);
+
+               unlink(path);
+               free(path);
+       }
+
+       stripspace(buf, 1);
+
+       if (!message && !buf->len)
+               die("no tag message?");
+
+       strbuf_insert(buf, 0, header_buf, header_len);
+
+       if (sign && do_sign(buf) < 0)
+               die("unable to sign the tag");
+       if (write_sha1_file(buf->buf, buf->len, tag_type, result) < 0)
+               die("unable to write tag file");
+}
+
+struct msg_arg {
+       int given;
+       struct strbuf buf;
+};
+
+static int parse_msg_arg(const struct option *opt, const char *arg, int unset)
+{
+       struct msg_arg *msg = opt->value;
+
+       if (!arg)
+               return -1;
+       if (msg->buf.len)
+               strbuf_addstr(&(msg->buf), "\n\n");
+       strbuf_addstr(&(msg->buf), arg);
+       msg->given = 1;
+       return 0;
+}
+
+int cmd_tag(int argc, const char **argv, const char *prefix)
+{
+       struct strbuf buf;
+       unsigned char object[20], prev[20];
+       char ref[PATH_MAX];
+       const char *object_ref, *tag;
+       struct ref_lock *lock;
+
+       int annotate = 0, sign = 0, force = 0, lines = 0,
+                                       delete = 0, verify = 0;
+       char *list = NULL, *msgfile = NULL, *keyid = NULL;
+       const char *no_pattern = "NO_PATTERN";
+       struct msg_arg msg = { 0, STRBUF_INIT };
+       struct option options[] = {
+               { OPTION_STRING, 'l', NULL, &list, "pattern", "list tag names",
+                       PARSE_OPT_OPTARG, NULL, (intptr_t) no_pattern },
+               { OPTION_INTEGER, 'n', NULL, &lines, NULL,
+                               "print n lines of each tag message",
+                               PARSE_OPT_OPTARG, NULL, 1 },
+               OPT_BOOLEAN('d', NULL, &delete, "delete tags"),
+               OPT_BOOLEAN('v', NULL, &verify, "verify tags"),
+
+               OPT_GROUP("Tag creation options"),
+               OPT_BOOLEAN('a', NULL, &annotate,
+                                       "annotated tag, needs a message"),
+               OPT_CALLBACK('m', NULL, &msg, "msg",
+                            "message for the tag", parse_msg_arg),
+               OPT_STRING('F', NULL, &msgfile, "file", "message in a file"),
+               OPT_BOOLEAN('s', NULL, &sign, "annotated and GPG-signed tag"),
+               OPT_STRING('u', NULL, &keyid, "key-id",
+                                       "use another key to sign the tag"),
+               OPT_BOOLEAN('f', NULL, &force, "replace the tag if exists"),
+               OPT_END()
+       };
+
+       git_config(git_tag_config);
+
+       argc = parse_options(argc, argv, options, git_tag_usage, 0);
+
+       if (sign)
+               annotate = 1;
+
+       if (list)
+               return list_tags(list == no_pattern ? NULL : list, lines);
+       if (delete)
+               return for_each_tag_name(argv, delete_tag);
+       if (verify)
+               return for_each_tag_name(argv, verify_tag);
+
+       strbuf_init(&buf, 0);
+       if (msg.given || msgfile) {
+               if (msg.given && msgfile)
+                       die("only one -F or -m option is allowed.");
+               annotate = 1;
+               if (msg.given)
+                       strbuf_addbuf(&buf, &(msg.buf));
+               else {
+                       if (!strcmp(msgfile, "-")) {
+                               if (strbuf_read(&buf, 0, 1024) < 0)
+                                       die("cannot read %s", msgfile);
+                       } else {
+                               if (strbuf_read_file(&buf, msgfile, 1024) < 0)
+                                       die("could not open or read '%s': %s",
+                                               msgfile, strerror(errno));
+                       }
+               }
+       }
+
+       if (argc == 0) {
+               if (annotate)
+                       usage_with_options(git_tag_usage, options);
+               return list_tags(NULL, lines);
+       }
+       tag = argv[0];
+
+       object_ref = argc == 2 ? argv[1] : "HEAD";
+       if (argc > 2)
+               die("too many params");
+
+       if (get_sha1(object_ref, object))
+               die("Failed to resolve '%s' as a valid ref.", object_ref);
+
+       if (snprintf(ref, sizeof(ref), "refs/tags/%s", tag) > sizeof(ref) - 1)
+               die("tag name too long: %.*s...", 50, tag);
+       if (check_ref_format(ref))
+               die("'%s' is not a valid tag name.", tag);
+
+       if (!resolve_ref(ref, prev, 1, NULL))
+               hashclr(prev);
+       else if (!force)
+               die("tag '%s' already exists", tag);
+
+       if (annotate)
+               create_tag(object, tag, &buf, msg.given || msgfile,
+                          sign, prev, object);
+
+       lock = lock_any_ref_for_update(ref, prev, 0);
+       if (!lock)
+               die("%s: cannot lock the ref", ref);
+       if (write_ref_sha1(lock, object, NULL) < 0)
+               die("%s: cannot update the ref", ref);
+
+       strbuf_release(&buf);
+       return 0;
+}
diff --git a/builtin-tar-tree.c b/builtin-tar-tree.c
new file mode 100644 (file)
index 0000000..b04719e
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2005, 2006 Rene Scharfe
+ */
+#include "cache.h"
+#include "commit.h"
+#include "tar.h"
+#include "builtin.h"
+#include "quote.h"
+
+static const char tar_tree_usage[] =
+"git-tar-tree [--remote=<repo>] <tree-ish> [basedir]\n"
+"*** Note that this command is now deprecated; use git-archive instead.";
+
+int cmd_tar_tree(int argc, const char **argv, const char *prefix)
+{
+       /*
+        * git-tar-tree is now a wrapper around git-archive --format=tar
+        *
+        * $0 --remote=<repo> arg... ==>
+        *      git-archive --format=tar --remote=<repo> arg...
+        * $0 tree-ish ==>
+        *      git-archive --format=tar tree-ish
+        * $0 tree-ish basedir ==>
+        *      git-archive --format-tar --prefix=basedir tree-ish
+        */
+       int i;
+       const char **nargv = xcalloc(sizeof(*nargv), argc + 2);
+       char *basedir_arg;
+       int nargc = 0;
+
+       nargv[nargc++] = "git-archive";
+       nargv[nargc++] = "--format=tar";
+
+       if (2 <= argc && !prefixcmp(argv[1], "--remote=")) {
+               nargv[nargc++] = argv[1];
+               argv++;
+               argc--;
+       }
+       switch (argc) {
+       default:
+               usage(tar_tree_usage);
+               break;
+       case 3:
+               /* base-path */
+               basedir_arg = xmalloc(strlen(argv[2]) + 11);
+               sprintf(basedir_arg, "--prefix=%s/", argv[2]);
+               nargv[nargc++] = basedir_arg;
+               /* fallthru */
+       case 2:
+               /* tree-ish */
+               nargv[nargc++] = argv[1];
+       }
+       nargv[nargc] = NULL;
+
+       fprintf(stderr,
+               "*** git-tar-tree is now deprecated.\n"
+               "*** Running git-archive instead.\n***");
+       for (i = 0; i < nargc; i++) {
+               fputc(' ', stderr);
+               sq_quote_print(stderr, nargv[i]);
+       }
+       fputc('\n', stderr);
+       return cmd_archive(nargc, nargv, prefix);
+}
+
+/* ustar header + extended global header content */
+#define RECORDSIZE     (512)
+#define HEADERSIZE (2 * RECORDSIZE)
+
+int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix)
+{
+       char buffer[HEADERSIZE];
+       struct ustar_header *header = (struct ustar_header *)buffer;
+       char *content = buffer + RECORDSIZE;
+       ssize_t n;
+
+       n = read_in_full(0, buffer, HEADERSIZE);
+       if (n < HEADERSIZE)
+               die("git-get-tar-commit-id: read error");
+       if (header->typeflag[0] != 'g')
+               return 1;
+       if (memcmp(content, "52 comment=", 11))
+               return 1;
+
+       n = write_in_full(1, content + 11, 41);
+       if (n < 41)
+               die("git-get-tar-commit-id: write error");
+
+       return 0;
+}
diff --git a/builtin-unpack-objects.c b/builtin-unpack-objects.c
new file mode 100644 (file)
index 0000000..1e51865
--- /dev/null
@@ -0,0 +1,402 @@
+#include "builtin.h"
+#include "cache.h"
+#include "object.h"
+#include "delta.h"
+#include "pack.h"
+#include "blob.h"
+#include "commit.h"
+#include "tag.h"
+#include "tree.h"
+#include "progress.h"
+
+static int dry_run, quiet, recover, has_errors;
+static const char unpack_usage[] = "git-unpack-objects [-n] [-q] [-r] < pack-file";
+
+/* We always read in 4kB chunks. */
+static unsigned char buffer[4096];
+static unsigned int offset, len;
+static off_t consumed_bytes;
+static SHA_CTX ctx;
+
+/*
+ * Make sure at least "min" bytes are available in the buffer, and
+ * return the pointer to the buffer.
+ */
+static void *fill(int min)
+{
+       if (min <= len)
+               return buffer + offset;
+       if (min > sizeof(buffer))
+               die("cannot fill %d bytes", min);
+       if (offset) {
+               SHA1_Update(&ctx, buffer, offset);
+               memmove(buffer, buffer + offset, len);
+               offset = 0;
+       }
+       do {
+               ssize_t ret = xread(0, buffer + len, sizeof(buffer) - len);
+               if (ret <= 0) {
+                       if (!ret)
+                               die("early EOF");
+                       die("read error on input: %s", strerror(errno));
+               }
+               len += ret;
+       } while (len < min);
+       return buffer;
+}
+
+static void use(int bytes)
+{
+       if (bytes > len)
+               die("used more bytes than were available");
+       len -= bytes;
+       offset += bytes;
+
+       /* make sure off_t is sufficiently large not to wrap */
+       if (consumed_bytes > consumed_bytes + bytes)
+               die("pack too large for current definition of off_t");
+       consumed_bytes += bytes;
+}
+
+static void *get_data(unsigned long size)
+{
+       z_stream stream;
+       void *buf = xmalloc(size);
+
+       memset(&stream, 0, sizeof(stream));
+
+       stream.next_out = buf;
+       stream.avail_out = size;
+       stream.next_in = fill(1);
+       stream.avail_in = len;
+       inflateInit(&stream);
+
+       for (;;) {
+               int ret = inflate(&stream, 0);
+               use(len - stream.avail_in);
+               if (stream.total_out == size && ret == Z_STREAM_END)
+                       break;
+               if (ret != Z_OK) {
+                       error("inflate returned %d\n", ret);
+                       free(buf);
+                       buf = NULL;
+                       if (!recover)
+                               exit(1);
+                       has_errors = 1;
+                       break;
+               }
+               stream.next_in = fill(1);
+               stream.avail_in = len;
+       }
+       inflateEnd(&stream);
+       return buf;
+}
+
+struct delta_info {
+       unsigned char base_sha1[20];
+       unsigned nr;
+       off_t base_offset;
+       unsigned long size;
+       void *delta;
+       struct delta_info *next;
+};
+
+static struct delta_info *delta_list;
+
+static void add_delta_to_list(unsigned nr, unsigned const char *base_sha1,
+                             off_t base_offset,
+                             void *delta, unsigned long size)
+{
+       struct delta_info *info = xmalloc(sizeof(*info));
+
+       hashcpy(info->base_sha1, base_sha1);
+       info->base_offset = base_offset;
+       info->size = size;
+       info->delta = delta;
+       info->nr = nr;
+       info->next = delta_list;
+       delta_list = info;
+}
+
+struct obj_info {
+       off_t offset;
+       unsigned char sha1[20];
+};
+
+static struct obj_info *obj_list;
+
+static void added_object(unsigned nr, enum object_type type,
+                        void *data, unsigned long size);
+
+static void write_object(unsigned nr, enum object_type type,
+                        void *buf, unsigned long size)
+{
+       if (write_sha1_file(buf, size, typename(type), obj_list[nr].sha1) < 0)
+               die("failed to write object");
+       added_object(nr, type, buf, size);
+}
+
+static void resolve_delta(unsigned nr, enum object_type type,
+                         void *base, unsigned long base_size,
+                         void *delta, unsigned long delta_size)
+{
+       void *result;
+       unsigned long result_size;
+
+       result = patch_delta(base, base_size,
+                            delta, delta_size,
+                            &result_size);
+       if (!result)
+               die("failed to apply delta");
+       free(delta);
+       write_object(nr, type, result, result_size);
+       free(result);
+}
+
+static void added_object(unsigned nr, enum object_type type,
+                        void *data, unsigned long size)
+{
+       struct delta_info **p = &delta_list;
+       struct delta_info *info;
+
+       while ((info = *p) != NULL) {
+               if (!hashcmp(info->base_sha1, obj_list[nr].sha1) ||
+                   info->base_offset == obj_list[nr].offset) {
+                       *p = info->next;
+                       p = &delta_list;
+                       resolve_delta(info->nr, type, data, size,
+                                     info->delta, info->size);
+                       free(info);
+                       continue;
+               }
+               p = &info->next;
+       }
+}
+
+static void unpack_non_delta_entry(enum object_type type, unsigned long size,
+                                  unsigned nr)
+{
+       void *buf = get_data(size);
+
+       if (!dry_run && buf)
+               write_object(nr, type, buf, size);
+       free(buf);
+}
+
+static void unpack_delta_entry(enum object_type type, unsigned long delta_size,
+                              unsigned nr)
+{
+       void *delta_data, *base;
+       unsigned long base_size;
+       unsigned char base_sha1[20];
+
+       if (type == OBJ_REF_DELTA) {
+               hashcpy(base_sha1, fill(20));
+               use(20);
+               delta_data = get_data(delta_size);
+               if (dry_run || !delta_data) {
+                       free(delta_data);
+                       return;
+               }
+               if (!has_sha1_file(base_sha1)) {
+                       hashcpy(obj_list[nr].sha1, null_sha1);
+                       add_delta_to_list(nr, base_sha1, 0, delta_data, delta_size);
+                       return;
+               }
+       } else {
+               unsigned base_found = 0;
+               unsigned char *pack, c;
+               off_t base_offset;
+               unsigned lo, mid, hi;
+
+               pack = fill(1);
+               c = *pack;
+               use(1);
+               base_offset = c & 127;
+               while (c & 128) {
+                       base_offset += 1;
+                       if (!base_offset || MSB(base_offset, 7))
+                               die("offset value overflow for delta base object");
+                       pack = fill(1);
+                       c = *pack;
+                       use(1);
+                       base_offset = (base_offset << 7) + (c & 127);
+               }
+               base_offset = obj_list[nr].offset - base_offset;
+
+               delta_data = get_data(delta_size);
+               if (dry_run || !delta_data) {
+                       free(delta_data);
+                       return;
+               }
+               lo = 0;
+               hi = nr;
+               while (lo < hi) {
+                       mid = (lo + hi)/2;
+                       if (base_offset < obj_list[mid].offset) {
+                               hi = mid;
+                       } else if (base_offset > obj_list[mid].offset) {
+                               lo = mid + 1;
+                       } else {
+                               hashcpy(base_sha1, obj_list[mid].sha1);
+                               base_found = !is_null_sha1(base_sha1);
+                               break;
+                       }
+               }
+               if (!base_found) {
+                       /* The delta base object is itself a delta that
+                          has not been resolved yet. */
+                       hashcpy(obj_list[nr].sha1, null_sha1);
+                       add_delta_to_list(nr, null_sha1, base_offset, delta_data, delta_size);
+                       return;
+               }
+       }
+
+       base = read_sha1_file(base_sha1, &type, &base_size);
+       if (!base) {
+               error("failed to read delta-pack base object %s",
+                     sha1_to_hex(base_sha1));
+               if (!recover)
+                       exit(1);
+               has_errors = 1;
+               return;
+       }
+       resolve_delta(nr, type, base, base_size, delta_data, delta_size);
+       free(base);
+}
+
+static void unpack_one(unsigned nr)
+{
+       unsigned shift;
+       unsigned char *pack, c;
+       unsigned long size;
+       enum object_type type;
+
+       obj_list[nr].offset = consumed_bytes;
+
+       pack = fill(1);
+       c = *pack;
+       use(1);
+       type = (c >> 4) & 7;
+       size = (c & 15);
+       shift = 4;
+       while (c & 0x80) {
+               pack = fill(1);
+               c = *pack;
+               use(1);
+               size += (c & 0x7f) << shift;
+               shift += 7;
+       }
+
+       switch (type) {
+       case OBJ_COMMIT:
+       case OBJ_TREE:
+       case OBJ_BLOB:
+       case OBJ_TAG:
+               unpack_non_delta_entry(type, size, nr);
+               return;
+       case OBJ_REF_DELTA:
+       case OBJ_OFS_DELTA:
+               unpack_delta_entry(type, size, nr);
+               return;
+       default:
+               error("bad object type %d", type);
+               has_errors = 1;
+               if (recover)
+                       return;
+               exit(1);
+       }
+}
+
+static void unpack_all(void)
+{
+       int i;
+       struct progress *progress = NULL;
+       struct pack_header *hdr = fill(sizeof(struct pack_header));
+       unsigned nr_objects = ntohl(hdr->hdr_entries);
+
+       if (ntohl(hdr->hdr_signature) != PACK_SIGNATURE)
+               die("bad pack file");
+       if (!pack_version_ok(hdr->hdr_version))
+               die("unknown pack file version %d", ntohl(hdr->hdr_version));
+       use(sizeof(struct pack_header));
+
+       if (!quiet)
+               progress = start_progress("Unpacking objects", nr_objects);
+       obj_list = xmalloc(nr_objects * sizeof(*obj_list));
+       for (i = 0; i < nr_objects; i++) {
+               unpack_one(i);
+               display_progress(progress, i + 1);
+       }
+       stop_progress(&progress);
+
+       if (delta_list)
+               die("unresolved deltas left after unpacking");
+}
+
+int cmd_unpack_objects(int argc, const char **argv, const char *prefix)
+{
+       int i;
+       unsigned char sha1[20];
+
+       git_config(git_default_config);
+
+       quiet = !isatty(2);
+
+       for (i = 1 ; i < argc; i++) {
+               const char *arg = argv[i];
+
+               if (*arg == '-') {
+                       if (!strcmp(arg, "-n")) {
+                               dry_run = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "-q")) {
+                               quiet = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "-r")) {
+                               recover = 1;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--pack_header=")) {
+                               struct pack_header *hdr;
+                               char *c;
+
+                               hdr = (struct pack_header *)buffer;
+                               hdr->hdr_signature = htonl(PACK_SIGNATURE);
+                               hdr->hdr_version = htonl(strtoul(arg + 14, &c, 10));
+                               if (*c != ',')
+                                       die("bad %s", arg);
+                               hdr->hdr_entries = htonl(strtoul(c + 1, &c, 10));
+                               if (*c)
+                                       die("bad %s", arg);
+                               len = sizeof(*hdr);
+                               continue;
+                       }
+                       usage(unpack_usage);
+               }
+
+               /* We don't take any non-flag arguments now.. Maybe some day */
+               usage(unpack_usage);
+       }
+       SHA1_Init(&ctx);
+       unpack_all();
+       SHA1_Update(&ctx, buffer, offset);
+       SHA1_Final(sha1, &ctx);
+       if (hashcmp(fill(20), sha1))
+               die("final sha1 did not match");
+       use(20);
+
+       /* Write the last part of the buffer to stdout */
+       while (len) {
+               int ret = xwrite(1, buffer + offset, len);
+               if (ret <= 0)
+                       break;
+               len -= ret;
+               offset += ret;
+       }
+
+       /* All done */
+       return has_errors;
+}
diff --git a/builtin-update-index.c b/builtin-update-index.c
new file mode 100644 (file)
index 0000000..e1a938d
--- /dev/null
@@ -0,0 +1,748 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "cache.h"
+#include "quote.h"
+#include "cache-tree.h"
+#include "tree-walk.h"
+#include "builtin.h"
+#include "refs.h"
+
+/*
+ * Default to not allowing changes to the list of files. The
+ * tool doesn't actually care, but this makes it harder to add
+ * files to the revision control by mistake by doing something
+ * like "git-update-index *" and suddenly having all the object
+ * files be revision controlled.
+ */
+static int allow_add;
+static int allow_remove;
+static int allow_replace;
+static int info_only;
+static int force_remove;
+static int verbose;
+static int mark_valid_only;
+#define MARK_VALID 1
+#define UNMARK_VALID 2
+
+static void report(const char *fmt, ...)
+{
+       va_list vp;
+
+       if (!verbose)
+               return;
+
+       va_start(vp, fmt);
+       vprintf(fmt, vp);
+       putchar('\n');
+       va_end(vp);
+}
+
+static int mark_valid(const char *path)
+{
+       int namelen = strlen(path);
+       int pos = cache_name_pos(path, namelen);
+       if (0 <= pos) {
+               switch (mark_valid_only) {
+               case MARK_VALID:
+                       active_cache[pos]->ce_flags |= htons(CE_VALID);
+                       break;
+               case UNMARK_VALID:
+                       active_cache[pos]->ce_flags &= ~htons(CE_VALID);
+                       break;
+               }
+               cache_tree_invalidate_path(active_cache_tree, path);
+               active_cache_changed = 1;
+               return 0;
+       }
+       return -1;
+}
+
+static int remove_one_path(const char *path)
+{
+       if (!allow_remove)
+               return error("%s: does not exist and --remove not passed", path);
+       if (remove_file_from_cache(path))
+               return error("%s: cannot remove from the index", path);
+       return 0;
+}
+
+/*
+ * Handle a path that couldn't be lstat'ed. It's either:
+ *  - missing file (ENOENT or ENOTDIR). That's ok if we're
+ *    supposed to be removing it and the removal actually
+ *    succeeds.
+ *  - permission error. That's never ok.
+ */
+static int process_lstat_error(const char *path, int err)
+{
+       if (err == ENOENT || err == ENOTDIR)
+               return remove_one_path(path);
+       return error("lstat(\"%s\"): %s", path, strerror(errno));
+}
+
+static int add_one_path(struct cache_entry *old, const char *path, int len, struct stat *st)
+{
+       int option, size;
+       struct cache_entry *ce;
+
+       /* Was the old index entry already up-to-date? */
+       if (old && !ce_stage(old) && !ce_match_stat(old, st, 0))
+               return 0;
+
+       size = cache_entry_size(len);
+       ce = xcalloc(1, size);
+       memcpy(ce->name, path, len);
+       ce->ce_flags = htons(len);
+       fill_stat_cache_info(ce, st);
+       ce->ce_mode = ce_mode_from_stat(old, st->st_mode);
+
+       if (index_path(ce->sha1, path, st, !info_only))
+               return -1;
+       option = allow_add ? ADD_CACHE_OK_TO_ADD : 0;
+       option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0;
+       if (add_cache_entry(ce, option))
+               return error("%s: cannot add to the index - missing --add option?", path);
+       return 0;
+}
+
+/*
+ * Handle a path that was a directory. Four cases:
+ *
+ *  - it's already a gitlink in the index, and we keep it that
+ *    way, and update it if we can (if we cannot find the HEAD,
+ *    we're going to keep it unchanged in the index!)
+ *
+ *  - it's a *file* in the index, in which case it should be
+ *    removed as a file if removal is allowed, since it doesn't
+ *    exist as such any more. If removal isn't allowed, it's
+ *    an error.
+ *
+ *    (NOTE! This is old and arguably fairly strange behaviour.
+ *    We might want to make this an error unconditionally, and
+ *    use "--force-remove" if you actually want to force removal).
+ *
+ *  - it used to exist as a subdirectory (ie multiple files with
+ *    this particular prefix) in the index, in which case it's wrong
+ *    to try to update it as a directory.
+ *
+ *  - it doesn't exist at all in the index, but it is a valid
+ *    git directory, and it should be *added* as a gitlink.
+ */
+static int process_directory(const char *path, int len, struct stat *st)
+{
+       unsigned char sha1[20];
+       int pos = cache_name_pos(path, len);
+
+       /* Exact match: file or existing gitlink */
+       if (pos >= 0) {
+               struct cache_entry *ce = active_cache[pos];
+               if (S_ISGITLINK(ntohl(ce->ce_mode))) {
+
+                       /* Do nothing to the index if there is no HEAD! */
+                       if (resolve_gitlink_ref(path, "HEAD", sha1) < 0)
+                               return 0;
+
+                       return add_one_path(ce, path, len, st);
+               }
+               /* Should this be an unconditional error? */
+               return remove_one_path(path);
+       }
+
+       /* Inexact match: is there perhaps a subdirectory match? */
+       pos = -pos-1;
+       while (pos < active_nr) {
+               struct cache_entry *ce = active_cache[pos++];
+
+               if (strncmp(ce->name, path, len))
+                       break;
+               if (ce->name[len] > '/')
+                       break;
+               if (ce->name[len] < '/')
+                       continue;
+
+               /* Subdirectory match - error out */
+               return error("%s: is a directory - add individual files instead", path);
+       }
+
+       /* No match - should we add it as a gitlink? */
+       if (!resolve_gitlink_ref(path, "HEAD", sha1))
+               return add_one_path(NULL, path, len, st);
+
+       /* Error out. */
+       return error("%s: is a directory - add files inside instead", path);
+}
+
+/*
+ * Process a regular file
+ */
+static int process_file(const char *path, int len, struct stat *st)
+{
+       int pos = cache_name_pos(path, len);
+       struct cache_entry *ce = pos < 0 ? NULL : active_cache[pos];
+
+       if (ce && S_ISGITLINK(ntohl(ce->ce_mode)))
+               return error("%s is already a gitlink, not replacing", path);
+
+       return add_one_path(ce, path, len, st);
+}
+
+static int process_path(const char *path)
+{
+       int len;
+       struct stat st;
+
+       /*
+        * First things first: get the stat information, to decide
+        * what to do about the pathname!
+        */
+       if (lstat(path, &st) < 0)
+               return process_lstat_error(path, errno);
+
+       len = strlen(path);
+       if (S_ISDIR(st.st_mode))
+               return process_directory(path, len, &st);
+
+       return process_file(path, len, &st);
+}
+
+static int add_cacheinfo(unsigned int mode, const unsigned char *sha1,
+                        const char *path, int stage)
+{
+       int size, len, option;
+       struct cache_entry *ce;
+
+       if (!verify_path(path))
+               return -1;
+
+       len = strlen(path);
+       size = cache_entry_size(len);
+       ce = xcalloc(1, size);
+
+       hashcpy(ce->sha1, sha1);
+       memcpy(ce->name, path, len);
+       ce->ce_flags = create_ce_flags(len, stage);
+       ce->ce_mode = create_ce_mode(mode);
+       if (assume_unchanged)
+               ce->ce_flags |= htons(CE_VALID);
+       option = allow_add ? ADD_CACHE_OK_TO_ADD : 0;
+       option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0;
+       if (add_cache_entry(ce, option))
+               return error("%s: cannot add to the index - missing --add option?",
+                            path);
+       report("add '%s'", path);
+       return 0;
+}
+
+static void chmod_path(int flip, const char *path)
+{
+       int pos;
+       struct cache_entry *ce;
+       unsigned int mode;
+
+       pos = cache_name_pos(path, strlen(path));
+       if (pos < 0)
+               goto fail;
+       ce = active_cache[pos];
+       mode = ntohl(ce->ce_mode);
+       if (!S_ISREG(mode))
+               goto fail;
+       switch (flip) {
+       case '+':
+               ce->ce_mode |= htonl(0111); break;
+       case '-':
+               ce->ce_mode &= htonl(~0111); break;
+       default:
+               goto fail;
+       }
+       cache_tree_invalidate_path(active_cache_tree, path);
+       active_cache_changed = 1;
+       report("chmod %cx '%s'", flip, path);
+       return;
+ fail:
+       die("git-update-index: cannot chmod %cx '%s'", flip, path);
+}
+
+static void update_one(const char *path, const char *prefix, int prefix_length)
+{
+       const char *p = prefix_path(prefix, prefix_length, path);
+       if (!verify_path(p)) {
+               fprintf(stderr, "Ignoring path %s\n", path);
+               goto free_return;
+       }
+       if (mark_valid_only) {
+               if (mark_valid(p))
+                       die("Unable to mark file %s", path);
+               goto free_return;
+       }
+
+       if (force_remove) {
+               if (remove_file_from_cache(p))
+                       die("git-update-index: unable to remove %s", path);
+               report("remove '%s'", path);
+               goto free_return;
+       }
+       if (process_path(p))
+               die("Unable to process path %s", path);
+       report("add '%s'", path);
+ free_return:
+       if (p < path || p > path + strlen(path))
+               free((char*)p);
+}
+
+static void read_index_info(int line_termination)
+{
+       struct strbuf buf;
+       struct strbuf uq;
+
+       strbuf_init(&buf, 0);
+       strbuf_init(&uq, 0);
+       while (strbuf_getline(&buf, stdin, line_termination) != EOF) {
+               char *ptr, *tab;
+               char *path_name;
+               unsigned char sha1[20];
+               unsigned int mode;
+               unsigned long ul;
+               int stage;
+
+               /* This reads lines formatted in one of three formats:
+                *
+                * (1) mode         SP sha1          TAB path
+                * The first format is what "git-apply --index-info"
+                * reports, and used to reconstruct a partial tree
+                * that is used for phony merge base tree when falling
+                * back on 3-way merge.
+                *
+                * (2) mode SP type SP sha1          TAB path
+                * The second format is to stuff git-ls-tree output
+                * into the index file.
+                *
+                * (3) mode         SP sha1 SP stage TAB path
+                * This format is to put higher order stages into the
+                * index file and matches git-ls-files --stage output.
+                */
+               errno = 0;
+               ul = strtoul(buf.buf, &ptr, 8);
+               if (ptr == buf.buf || *ptr != ' '
+                   || errno || (unsigned int) ul != ul)
+                       goto bad_line;
+               mode = ul;
+
+               tab = strchr(ptr, '\t');
+               if (!tab || tab - ptr < 41)
+                       goto bad_line;
+
+               if (tab[-2] == ' ' && '0' <= tab[-1] && tab[-1] <= '3') {
+                       stage = tab[-1] - '0';
+                       ptr = tab + 1; /* point at the head of path */
+                       tab = tab - 2; /* point at tail of sha1 */
+               }
+               else {
+                       stage = 0;
+                       ptr = tab + 1; /* point at the head of path */
+               }
+
+               if (get_sha1_hex(tab - 40, sha1) || tab[-41] != ' ')
+                       goto bad_line;
+
+               path_name = ptr;
+               if (line_termination && path_name[0] == '"') {
+                       strbuf_reset(&uq);
+                       if (unquote_c_style(&uq, path_name, NULL)) {
+                               die("git-update-index: bad quoting of path name");
+                       }
+                       path_name = uq.buf;
+               }
+
+               if (!verify_path(path_name)) {
+                       fprintf(stderr, "Ignoring path %s\n", path_name);
+                       continue;
+               }
+
+               if (!mode) {
+                       /* mode == 0 means there is no such path -- remove */
+                       if (remove_file_from_cache(path_name))
+                               die("git-update-index: unable to remove %s",
+                                   ptr);
+               }
+               else {
+                       /* mode ' ' sha1 '\t' name
+                        * ptr[-1] points at tab,
+                        * ptr[-41] is at the beginning of sha1
+                        */
+                       ptr[-42] = ptr[-1] = 0;
+                       if (add_cacheinfo(mode, sha1, path_name, stage))
+                               die("git-update-index: unable to update %s",
+                                   path_name);
+               }
+               continue;
+
+       bad_line:
+               die("malformed index info %s", buf.buf);
+       }
+       strbuf_release(&buf);
+       strbuf_release(&uq);
+}
+
+static const char update_index_usage[] =
+"git-update-index [-q] [--add] [--replace] [--remove] [--unmerged] [--refresh] [--really-refresh] [--cacheinfo] [--chmod=(+|-)x] [--assume-unchanged] [--info-only] [--force-remove] [--stdin] [--index-info] [--unresolve] [--again | -g] [--ignore-missing] [-z] [--verbose] [--] <file>...";
+
+static unsigned char head_sha1[20];
+static unsigned char merge_head_sha1[20];
+
+static struct cache_entry *read_one_ent(const char *which,
+                                       unsigned char *ent, const char *path,
+                                       int namelen, int stage)
+{
+       unsigned mode;
+       unsigned char sha1[20];
+       int size;
+       struct cache_entry *ce;
+
+       if (get_tree_entry(ent, path, sha1, &mode)) {
+               if (which)
+                       error("%s: not in %s branch.", path, which);
+               return NULL;
+       }
+       if (mode == S_IFDIR) {
+               if (which)
+                       error("%s: not a blob in %s branch.", path, which);
+               return NULL;
+       }
+       size = cache_entry_size(namelen);
+       ce = xcalloc(1, size);
+
+       hashcpy(ce->sha1, sha1);
+       memcpy(ce->name, path, namelen);
+       ce->ce_flags = create_ce_flags(namelen, stage);
+       ce->ce_mode = create_ce_mode(mode);
+       return ce;
+}
+
+static int unresolve_one(const char *path)
+{
+       int namelen = strlen(path);
+       int pos;
+       int ret = 0;
+       struct cache_entry *ce_2 = NULL, *ce_3 = NULL;
+
+       /* See if there is such entry in the index. */
+       pos = cache_name_pos(path, namelen);
+       if (pos < 0) {
+               /* If there isn't, either it is unmerged, or
+                * resolved as "removed" by mistake.  We do not
+                * want to do anything in the former case.
+                */
+               pos = -pos-1;
+               if (pos < active_nr) {
+                       struct cache_entry *ce = active_cache[pos];
+                       if (ce_namelen(ce) == namelen &&
+                           !memcmp(ce->name, path, namelen)) {
+                               fprintf(stderr,
+                                       "%s: skipping still unmerged path.\n",
+                                       path);
+                               goto free_return;
+                       }
+               }
+       }
+
+       /* Grab blobs from given path from HEAD and MERGE_HEAD,
+        * stuff HEAD version in stage #2,
+        * stuff MERGE_HEAD version in stage #3.
+        */
+       ce_2 = read_one_ent("our", head_sha1, path, namelen, 2);
+       ce_3 = read_one_ent("their", merge_head_sha1, path, namelen, 3);
+
+       if (!ce_2 || !ce_3) {
+               ret = -1;
+               goto free_return;
+       }
+       if (!hashcmp(ce_2->sha1, ce_3->sha1) &&
+           ce_2->ce_mode == ce_3->ce_mode) {
+               fprintf(stderr, "%s: identical in both, skipping.\n",
+                       path);
+               goto free_return;
+       }
+
+       remove_file_from_cache(path);
+       if (add_cache_entry(ce_2, ADD_CACHE_OK_TO_ADD)) {
+               error("%s: cannot add our version to the index.", path);
+               ret = -1;
+               goto free_return;
+       }
+       if (!add_cache_entry(ce_3, ADD_CACHE_OK_TO_ADD))
+               return 0;
+       error("%s: cannot add their version to the index.", path);
+       ret = -1;
+ free_return:
+       free(ce_2);
+       free(ce_3);
+       return ret;
+}
+
+static void read_head_pointers(void)
+{
+       if (read_ref("HEAD", head_sha1))
+               die("No HEAD -- no initial commit yet?\n");
+       if (read_ref("MERGE_HEAD", merge_head_sha1)) {
+               fprintf(stderr, "Not in the middle of a merge.\n");
+               exit(0);
+       }
+}
+
+static int do_unresolve(int ac, const char **av,
+                       const char *prefix, int prefix_length)
+{
+       int i;
+       int err = 0;
+
+       /* Read HEAD and MERGE_HEAD; if MERGE_HEAD does not exist, we
+        * are not doing a merge, so exit with success status.
+        */
+       read_head_pointers();
+
+       for (i = 1; i < ac; i++) {
+               const char *arg = av[i];
+               const char *p = prefix_path(prefix, prefix_length, arg);
+               err |= unresolve_one(p);
+               if (p < arg || p > arg + strlen(arg))
+                       free((char*)p);
+       }
+       return err;
+}
+
+static int do_reupdate(int ac, const char **av,
+                      const char *prefix, int prefix_length)
+{
+       /* Read HEAD and run update-index on paths that are
+        * merged and already different between index and HEAD.
+        */
+       int pos;
+       int has_head = 1;
+       const char **pathspec = get_pathspec(prefix, av + 1);
+
+       if (read_ref("HEAD", head_sha1))
+               /* If there is no HEAD, that means it is an initial
+                * commit.  Update everything in the index.
+                */
+               has_head = 0;
+ redo:
+       for (pos = 0; pos < active_nr; pos++) {
+               struct cache_entry *ce = active_cache[pos];
+               struct cache_entry *old = NULL;
+               int save_nr;
+
+               if (ce_stage(ce) || !ce_path_match(ce, pathspec))
+                       continue;
+               if (has_head)
+                       old = read_one_ent(NULL, head_sha1,
+                                          ce->name, ce_namelen(ce), 0);
+               if (old && ce->ce_mode == old->ce_mode &&
+                   !hashcmp(ce->sha1, old->sha1)) {
+                       free(old);
+                       continue; /* unchanged */
+               }
+               /* Be careful.  The working tree may not have the
+                * path anymore, in which case, under 'allow_remove',
+                * or worse yet 'allow_replace', active_nr may decrease.
+                */
+               save_nr = active_nr;
+               update_one(ce->name + prefix_length, prefix, prefix_length);
+               if (save_nr != active_nr)
+                       goto redo;
+       }
+       return 0;
+}
+
+int cmd_update_index(int argc, const char **argv, const char *prefix)
+{
+       int i, newfd, entries, has_errors = 0, line_termination = '\n';
+       int allow_options = 1;
+       int read_from_stdin = 0;
+       int prefix_length = prefix ? strlen(prefix) : 0;
+       char set_executable_bit = 0;
+       unsigned int refresh_flags = 0;
+       int lock_error = 0;
+       struct lock_file *lock_file;
+
+       git_config(git_default_config);
+
+       /* We can't free this memory, it becomes part of a linked list parsed atexit() */
+       lock_file = xcalloc(1, sizeof(struct lock_file));
+
+       newfd = hold_locked_index(lock_file, 0);
+       if (newfd < 0)
+               lock_error = errno;
+
+       entries = read_cache();
+       if (entries < 0)
+               die("cache corrupted");
+
+       for (i = 1 ; i < argc; i++) {
+               const char *path = argv[i];
+               const char *p;
+
+               if (allow_options && *path == '-') {
+                       if (!strcmp(path, "--")) {
+                               allow_options = 0;
+                               continue;
+                       }
+                       if (!strcmp(path, "-q")) {
+                               refresh_flags |= REFRESH_QUIET;
+                               continue;
+                       }
+                       if (!strcmp(path, "--add")) {
+                               allow_add = 1;
+                               continue;
+                       }
+                       if (!strcmp(path, "--replace")) {
+                               allow_replace = 1;
+                               continue;
+                       }
+                       if (!strcmp(path, "--remove")) {
+                               allow_remove = 1;
+                               continue;
+                       }
+                       if (!strcmp(path, "--unmerged")) {
+                               refresh_flags |= REFRESH_UNMERGED;
+                               continue;
+                       }
+                       if (!strcmp(path, "--refresh")) {
+                               has_errors |= refresh_cache(refresh_flags);
+                               continue;
+                       }
+                       if (!strcmp(path, "--really-refresh")) {
+                               has_errors |= refresh_cache(REFRESH_REALLY | refresh_flags);
+                               continue;
+                       }
+                       if (!strcmp(path, "--cacheinfo")) {
+                               unsigned char sha1[20];
+                               unsigned int mode;
+
+                               if (i+3 >= argc)
+                                       die("git-update-index: --cacheinfo <mode> <sha1> <path>");
+
+                               if (strtoul_ui(argv[i+1], 8, &mode) ||
+                                   get_sha1_hex(argv[i+2], sha1) ||
+                                   add_cacheinfo(mode, sha1, argv[i+3], 0))
+                                       die("git-update-index: --cacheinfo"
+                                           " cannot add %s", argv[i+3]);
+                               i += 3;
+                               continue;
+                       }
+                       if (!strcmp(path, "--chmod=-x") ||
+                           !strcmp(path, "--chmod=+x")) {
+                               if (argc <= i+1)
+                                       die("git-update-index: %s <path>", path);
+                               set_executable_bit = path[8];
+                               continue;
+                       }
+                       if (!strcmp(path, "--assume-unchanged")) {
+                               mark_valid_only = MARK_VALID;
+                               continue;
+                       }
+                       if (!strcmp(path, "--no-assume-unchanged")) {
+                               mark_valid_only = UNMARK_VALID;
+                               continue;
+                       }
+                       if (!strcmp(path, "--info-only")) {
+                               info_only = 1;
+                               continue;
+                       }
+                       if (!strcmp(path, "--force-remove")) {
+                               force_remove = 1;
+                               continue;
+                       }
+                       if (!strcmp(path, "-z")) {
+                               line_termination = 0;
+                               continue;
+                       }
+                       if (!strcmp(path, "--stdin")) {
+                               if (i != argc - 1)
+                                       die("--stdin must be at the end");
+                               read_from_stdin = 1;
+                               break;
+                       }
+                       if (!strcmp(path, "--index-info")) {
+                               if (i != argc - 1)
+                                       die("--index-info must be at the end");
+                               allow_add = allow_replace = allow_remove = 1;
+                               read_index_info(line_termination);
+                               break;
+                       }
+                       if (!strcmp(path, "--unresolve")) {
+                               has_errors = do_unresolve(argc - i, argv + i,
+                                                         prefix, prefix_length);
+                               if (has_errors)
+                                       active_cache_changed = 0;
+                               goto finish;
+                       }
+                       if (!strcmp(path, "--again") || !strcmp(path, "-g")) {
+                               has_errors = do_reupdate(argc - i, argv + i,
+                                                        prefix, prefix_length);
+                               if (has_errors)
+                                       active_cache_changed = 0;
+                               goto finish;
+                       }
+                       if (!strcmp(path, "--ignore-missing")) {
+                               refresh_flags |= REFRESH_IGNORE_MISSING;
+                               continue;
+                       }
+                       if (!strcmp(path, "--verbose")) {
+                               verbose = 1;
+                               continue;
+                       }
+                       if (!strcmp(path, "-h") || !strcmp(path, "--help"))
+                               usage(update_index_usage);
+                       die("unknown option %s", path);
+               }
+               p = prefix_path(prefix, prefix_length, path);
+               update_one(p, NULL, 0);
+               if (set_executable_bit)
+                       chmod_path(set_executable_bit, p);
+               if (p < path || p > path + strlen(path))
+                       free((char*)p);
+       }
+       if (read_from_stdin) {
+               struct strbuf buf, nbuf;
+
+               strbuf_init(&buf, 0);
+               strbuf_init(&nbuf, 0);
+               while (strbuf_getline(&buf, stdin, line_termination) != EOF) {
+                       const char *p;
+                       if (line_termination && buf.buf[0] == '"') {
+                               strbuf_reset(&nbuf);
+                               if (unquote_c_style(&nbuf, buf.buf, NULL))
+                                       die("line is badly quoted");
+                               strbuf_swap(&buf, &nbuf);
+                       }
+                       p = prefix_path(prefix, prefix_length, buf.buf);
+                       update_one(p, NULL, 0);
+                       if (set_executable_bit)
+                               chmod_path(set_executable_bit, p);
+                       if (p < buf.buf || p > buf.buf + buf.len)
+                               free((char *)p);
+               }
+               strbuf_release(&nbuf);
+               strbuf_release(&buf);
+       }
+
+ finish:
+       if (active_cache_changed) {
+               if (newfd < 0) {
+                       if (refresh_flags & REFRESH_QUIET)
+                               exit(128);
+                       die("unable to create '%s.lock': %s",
+                           get_index_file(), strerror(lock_error));
+               }
+               if (write_cache(newfd, active_cache, active_nr) ||
+                   close(newfd) || commit_locked_index(lock_file))
+                       die("Unable to write new index file");
+       }
+
+       rollback_lock_file(lock_file);
+
+       return has_errors ? 1 : 0;
+}
diff --git a/builtin-update-ref.c b/builtin-update-ref.c
new file mode 100644 (file)
index 0000000..e90737c
--- /dev/null
@@ -0,0 +1,51 @@
+#include "cache.h"
+#include "refs.h"
+#include "builtin.h"
+#include "parse-options.h"
+
+static const char * const git_update_ref_usage[] = {
+       "git-update-ref [options] -d <refname> <oldval>",
+       "git-update-ref [options]    <refname> <newval> [<oldval>]",
+       NULL
+};
+
+int cmd_update_ref(int argc, const char **argv, const char *prefix)
+{
+       const char *refname, *value, *oldval, *msg=NULL;
+       unsigned char sha1[20], oldsha1[20];
+       int delete = 0, no_deref = 0;
+       struct option options[] = {
+               OPT_STRING( 'm', NULL, &msg, "reason", "reason of the update"),
+               OPT_BOOLEAN('d', NULL, &delete, "deletes the reference"),
+               OPT_BOOLEAN( 0 , "no-deref", &no_deref,
+                                       "update <refname> not the one it points to"),
+               OPT_END(),
+       };
+
+       git_config(git_default_config);
+       argc = parse_options(argc, argv, options, git_update_ref_usage, 0);
+       if (msg && !*msg)
+               die("Refusing to perform update with empty message.");
+
+       if (argc < 2 || argc > 3)
+               usage_with_options(git_update_ref_usage, options);
+       refname = argv[0];
+       value   = argv[1];
+       oldval  = argv[2];
+
+       if (get_sha1(value, sha1))
+               die("%s: not a valid SHA1", value);
+
+       if (delete) {
+               if (oldval)
+                       usage_with_options(git_update_ref_usage, options);
+               return delete_ref(refname, sha1);
+       }
+
+       hashclr(oldsha1);
+       if (oldval && *oldval && get_sha1(oldval, oldsha1))
+               die("%s: not a valid old SHA1", oldval);
+
+       return update_ref(msg, refname, sha1, oldval ? oldsha1 : NULL,
+                         no_deref ? REF_NODEREF : 0, DIE_ON_ERR);
+}
diff --git a/builtin-upload-archive.c b/builtin-upload-archive.c
new file mode 100644 (file)
index 0000000..48ae09e
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2006 Franck Bui-Huu
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "archive.h"
+#include "pkt-line.h"
+#include "sideband.h"
+
+static const char upload_archive_usage[] =
+       "git-upload-archive <repo>";
+
+static const char deadchild[] =
+"git-upload-archive: archiver died with error";
+
+static const char lostchild[] =
+"git-upload-archive: archiver process was lost";
+
+
+static int run_upload_archive(int argc, const char **argv, const char *prefix)
+{
+       struct archiver ar;
+       const char *sent_argv[MAX_ARGS];
+       const char *arg_cmd = "argument ";
+       char *p, buf[4096];
+       int treeish_idx;
+       int sent_argc;
+       int len;
+
+       if (argc != 2)
+               usage(upload_archive_usage);
+
+       if (strlen(argv[1]) > sizeof(buf))
+               die("insanely long repository name");
+
+       strcpy(buf, argv[1]); /* enter-repo smudges its argument */
+
+       if (!enter_repo(buf, 0))
+               die("not a git archive");
+
+       /* put received options in sent_argv[] */
+       sent_argc = 1;
+       sent_argv[0] = "git-upload-archive";
+       for (p = buf;;) {
+               /* This will die if not enough free space in buf */
+               len = packet_read_line(0, p, (buf + sizeof buf) - p);
+               if (len == 0)
+                       break;  /* got a flush */
+               if (sent_argc > MAX_ARGS - 2)
+                       die("Too many options (>29)");
+
+               if (p[len-1] == '\n') {
+                       p[--len] = 0;
+               }
+               if (len < strlen(arg_cmd) ||
+                   strncmp(arg_cmd, p, strlen(arg_cmd)))
+                       die("'argument' token or flush expected");
+
+               len -= strlen(arg_cmd);
+               memmove(p, p + strlen(arg_cmd), len);
+               sent_argv[sent_argc++] = p;
+               p += len;
+               *p++ = 0;
+       }
+       sent_argv[sent_argc] = NULL;
+
+       /* parse all options sent by the client */
+       treeish_idx = parse_archive_args(sent_argc, sent_argv, &ar);
+
+       parse_treeish_arg(sent_argv + treeish_idx, &ar.args, prefix);
+       parse_pathspec_arg(sent_argv + treeish_idx + 1, &ar.args);
+
+       return ar.write_archive(&ar.args);
+}
+
+static void error_clnt(const char *fmt, ...)
+{
+       char buf[1024];
+       va_list params;
+       int len;
+
+       va_start(params, fmt);
+       len = vsprintf(buf, fmt, params);
+       va_end(params);
+       send_sideband(1, 3, buf, len, LARGE_PACKET_MAX);
+       die("sent error to the client: %s", buf);
+}
+
+static void process_input(int child_fd, int band)
+{
+       char buf[16384];
+       ssize_t sz = read(child_fd, buf, sizeof(buf));
+       if (sz < 0) {
+               if (errno != EAGAIN && errno != EINTR)
+                       error_clnt("read error: %s\n", strerror(errno));
+               return;
+       }
+       send_sideband(1, band, buf, sz, LARGE_PACKET_MAX);
+}
+
+int cmd_upload_archive(int argc, const char **argv, const char *prefix)
+{
+       pid_t writer;
+       int fd1[2], fd2[2];
+       /*
+        * Set up sideband subprocess.
+        *
+        * We (parent) monitor and read from child, sending its fd#1 and fd#2
+        * multiplexed out to our fd#1.  If the child dies, we tell the other
+        * end over channel #3.
+        */
+       if (pipe(fd1) < 0 || pipe(fd2) < 0) {
+               int err = errno;
+               packet_write(1, "NACK pipe failed on the remote side\n");
+               die("upload-archive: %s", strerror(err));
+       }
+       writer = fork();
+       if (writer < 0) {
+               int err = errno;
+               packet_write(1, "NACK fork failed on the remote side\n");
+               die("upload-archive: %s", strerror(err));
+       }
+       if (!writer) {
+               /* child - connect fd#1 and fd#2 to the pipe */
+               dup2(fd1[1], 1);
+               dup2(fd2[1], 2);
+               close(fd1[1]); close(fd2[1]);
+               close(fd1[0]); close(fd2[0]); /* we do not read from pipe */
+
+               exit(run_upload_archive(argc, argv, prefix));
+       }
+
+       /* parent - read from child, multiplex and send out to fd#1 */
+       close(fd1[1]); close(fd2[1]); /* we do not write to pipe */
+       packet_write(1, "ACK\n");
+       packet_flush(1);
+
+       while (1) {
+               struct pollfd pfd[2];
+               int status;
+
+               pfd[0].fd = fd1[0];
+               pfd[0].events = POLLIN;
+               pfd[1].fd = fd2[0];
+               pfd[1].events = POLLIN;
+               if (poll(pfd, 2, -1) < 0) {
+                       if (errno != EINTR) {
+                               error("poll failed resuming: %s",
+                                     strerror(errno));
+                               sleep(1);
+                       }
+                       continue;
+               }
+               if (pfd[0].revents & POLLIN)
+                       /* Data stream ready */
+                       process_input(pfd[0].fd, 1);
+               if (pfd[1].revents & POLLIN)
+                       /* Status stream ready */
+                       process_input(pfd[1].fd, 2);
+               /* Always finish to read data when available */
+               if ((pfd[0].revents | pfd[1].revents) & POLLIN)
+                       continue;
+
+               if (waitpid(writer, &status, 0) < 0)
+                       error_clnt("%s", lostchild);
+               else if (!WIFEXITED(status) || WEXITSTATUS(status) > 0)
+                       error_clnt("%s", deadchild);
+               packet_flush(1);
+               break;
+       }
+       return 0;
+}
diff --git a/builtin-verify-pack.c b/builtin-verify-pack.c
new file mode 100644 (file)
index 0000000..4e31c27
--- /dev/null
@@ -0,0 +1,80 @@
+#include "builtin.h"
+#include "cache.h"
+#include "pack.h"
+
+static int verify_one_pack(const char *path, int verbose)
+{
+       char arg[PATH_MAX];
+       int len;
+       struct packed_git *pack;
+       int err;
+
+       len = strlcpy(arg, path, PATH_MAX);
+       if (len >= PATH_MAX)
+               return error("name too long: %s", path);
+
+       /*
+        * In addition to "foo.idx" we accept "foo.pack" and "foo";
+        * normalize these forms to "foo.idx" for add_packed_git().
+        */
+       if (has_extension(arg, ".pack")) {
+               strcpy(arg + len - 5, ".idx");
+               len--;
+       } else if (!has_extension(arg, ".idx")) {
+               if (len + 4 >= PATH_MAX)
+                       return error("name too long: %s.idx", arg);
+               strcpy(arg + len, ".idx");
+               len += 4;
+       }
+
+       /*
+        * add_packed_git() uses our buffer (containing "foo.idx") to
+        * build the pack filename ("foo.pack").  Make sure it fits.
+        */
+       if (len + 1 >= PATH_MAX) {
+               arg[len - 4] = '\0';
+               return error("name too long: %s.pack", arg);
+       }
+
+       pack = add_packed_git(arg, len, 1);
+       if (!pack)
+               return error("packfile %s not found.", arg);
+
+       err = verify_pack(pack, verbose);
+       free(pack);
+
+       return err;
+}
+
+static const char verify_pack_usage[] = "git-verify-pack [-v] <pack>...";
+
+int cmd_verify_pack(int argc, const char **argv, const char *prefix)
+{
+       int err = 0;
+       int verbose = 0;
+       int no_more_options = 0;
+       int nothing_done = 1;
+
+       git_config(git_default_config);
+       while (1 < argc) {
+               if (!no_more_options && argv[1][0] == '-') {
+                       if (!strcmp("-v", argv[1]))
+                               verbose = 1;
+                       else if (!strcmp("--", argv[1]))
+                               no_more_options = 1;
+                       else
+                               usage(verify_pack_usage);
+               }
+               else {
+                       if (verify_one_pack(argv[1], verbose))
+                               err = 1;
+                       nothing_done = 0;
+               }
+               argc--; argv++;
+       }
+
+       if (nothing_done)
+               usage(verify_pack_usage);
+
+       return err;
+}
diff --git a/builtin-verify-tag.c b/builtin-verify-tag.c
new file mode 100644 (file)
index 0000000..cc4c55d
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Builtin "git verify-tag"
+ *
+ * Copyright (c) 2007 Carlos Rica <jasampler@gmail.com>
+ *
+ * Based on git-verify-tag.sh
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "tag.h"
+#include "run-command.h"
+#include <signal.h>
+
+static const char builtin_verify_tag_usage[] =
+               "git-verify-tag [-v|--verbose] <tag>...";
+
+#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----"
+
+static int run_gpg_verify(const char *buf, unsigned long size, int verbose)
+{
+       struct child_process gpg;
+       const char *args_gpg[] = {"gpg", "--verify", "FILE", "-", NULL};
+       char path[PATH_MAX], *eol;
+       size_t len;
+       int fd, ret;
+
+       fd = git_mkstemp(path, PATH_MAX, ".git_vtag_tmpXXXXXX");
+       if (fd < 0)
+               return error("could not create temporary file '%s': %s",
+                                               path, strerror(errno));
+       if (write_in_full(fd, buf, size) < 0)
+               return error("failed writing temporary file '%s': %s",
+                                               path, strerror(errno));
+       close(fd);
+
+       /* find the length without signature */
+       len = 0;
+       while (len < size && prefixcmp(buf + len, PGP_SIGNATURE)) {
+               eol = memchr(buf + len, '\n', size - len);
+               len += eol ? eol - (buf + len) + 1 : size - len;
+       }
+       if (verbose)
+               write_in_full(1, buf, len);
+
+       memset(&gpg, 0, sizeof(gpg));
+       gpg.argv = args_gpg;
+       gpg.in = -1;
+       gpg.out = 1;
+       args_gpg[2] = path;
+       if (start_command(&gpg))
+               return error("could not run gpg.");
+
+       write_in_full(gpg.in, buf, len);
+       close(gpg.in);
+       gpg.close_in = 0;
+       ret = finish_command(&gpg);
+
+       unlink(path);
+
+       return ret;
+}
+
+static int verify_tag(const char *name, int verbose)
+{
+       enum object_type type;
+       unsigned char sha1[20];
+       char *buf;
+       unsigned long size;
+       int ret;
+
+       if (get_sha1(name, sha1))
+               return error("tag '%s' not found.", name);
+
+       type = sha1_object_info(sha1, NULL);
+       if (type != OBJ_TAG)
+               return error("%s: cannot verify a non-tag object of type %s.",
+                               name, typename(type));
+
+       buf = read_sha1_file(sha1, &type, &size);
+       if (!buf)
+               return error("%s: unable to read file.", name);
+
+       ret = run_gpg_verify(buf, size, verbose);
+
+       free(buf);
+       return ret;
+}
+
+int cmd_verify_tag(int argc, const char **argv, const char *prefix)
+{
+       int i = 1, verbose = 0, had_error = 0;
+
+       git_config(git_default_config);
+
+       if (argc == 1)
+               usage(builtin_verify_tag_usage);
+
+       if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose")) {
+               verbose = 1;
+               i++;
+       }
+
+       /* sometimes the program was terminated because this signal
+        * was received in the process of writing the gpg input: */
+       signal(SIGPIPE, SIG_IGN);
+       while (i < argc)
+               if (verify_tag(argv[i++], verbose))
+                       had_error = 1;
+       return had_error;
+}
diff --git a/builtin-write-tree.c b/builtin-write-tree.c
new file mode 100644 (file)
index 0000000..b89d02e
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "builtin.h"
+#include "cache.h"
+#include "tree.h"
+#include "cache-tree.h"
+
+static const char write_tree_usage[] =
+"git-write-tree [--missing-ok] [--prefix=<prefix>/]";
+
+int write_tree(unsigned char *sha1, int missing_ok, const char *prefix)
+{
+       int entries, was_valid, newfd;
+
+       /* We can't free this memory, it becomes part of a linked list parsed atexit() */
+       struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file));
+
+       newfd = hold_locked_index(lock_file, 1);
+
+       entries = read_cache();
+       if (entries < 0)
+               die("git-write-tree: error reading cache");
+
+       if (!active_cache_tree)
+               active_cache_tree = cache_tree();
+
+       was_valid = cache_tree_fully_valid(active_cache_tree);
+
+       if (!was_valid) {
+               if (cache_tree_update(active_cache_tree,
+                                     active_cache, active_nr,
+                                     missing_ok, 0) < 0)
+                       die("git-write-tree: error building trees");
+               if (0 <= newfd) {
+                       if (!write_cache(newfd, active_cache, active_nr)
+                                       && !close(newfd)) {
+                               commit_lock_file(lock_file);
+                               newfd = -1;
+                       }
+               }
+               /* Not being able to write is fine -- we are only interested
+                * in updating the cache-tree part, and if the next caller
+                * ends up using the old index with unupdated cache-tree part
+                * it misses the work we did here, but that is just a
+                * performance penalty and not a big deal.
+                */
+       }
+
+       if (prefix) {
+               struct cache_tree *subtree =
+                       cache_tree_find(active_cache_tree, prefix);
+               if (!subtree)
+                       die("git-write-tree: prefix %s not found", prefix);
+               hashcpy(sha1, subtree->sha1);
+       }
+       else
+               hashcpy(sha1, active_cache_tree->sha1);
+
+       if (0 <= newfd)
+               close(newfd);
+       rollback_lock_file(lock_file);
+
+       return 0;
+}
+
+int cmd_write_tree(int argc, const char **argv, const char *unused_prefix)
+{
+       int missing_ok = 0, ret;
+       const char *prefix = NULL;
+       unsigned char sha1[20];
+
+       git_config(git_default_config);
+       while (1 < argc) {
+               const char *arg = argv[1];
+               if (!strcmp(arg, "--missing-ok"))
+                       missing_ok = 1;
+               else if (!prefixcmp(arg, "--prefix="))
+                       prefix = arg + 9;
+               else
+                       usage(write_tree_usage);
+               argc--; argv++;
+       }
+
+       if (argc > 2)
+               die("too many options");
+
+       ret = write_tree(sha1, missing_ok, prefix);
+       printf("%s\n", sha1_to_hex(sha1));
+
+       return ret;
+}
diff --git a/builtin.h b/builtin.h
new file mode 100644 (file)
index 0000000..cb675c4
--- /dev/null
+++ b/builtin.h
@@ -0,0 +1,98 @@
+#ifndef BUILTIN_H
+#define BUILTIN_H
+
+#include "git-compat-util.h"
+
+extern const char git_version_string[];
+extern const char git_usage_string[];
+
+extern void list_common_cmds_help(void);
+extern void help_unknown_cmd(const char *cmd);
+extern int write_tree(unsigned char *sha1, int missing_ok, const char *prefix);
+extern void prune_packed_objects(int);
+
+extern int cmd_add(int argc, const char **argv, const char *prefix);
+extern int cmd_annotate(int argc, const char **argv, const char *prefix);
+extern int cmd_apply(int argc, const char **argv, const char *prefix);
+extern int cmd_archive(int argc, const char **argv, const char *prefix);
+extern int cmd_blame(int argc, const char **argv, const char *prefix);
+extern int cmd_branch(int argc, const char **argv, const char *prefix);
+extern int cmd_bundle(int argc, const char **argv, const char *prefix);
+extern int cmd_cat_file(int argc, const char **argv, const char *prefix);
+extern int cmd_checkout_index(int argc, const char **argv, const char *prefix);
+extern int cmd_check_attr(int argc, const char **argv, const char *prefix);
+extern int cmd_check_ref_format(int argc, const char **argv, const char *prefix);
+extern int cmd_cherry(int argc, const char **argv, const char *prefix);
+extern int cmd_cherry_pick(int argc, const char **argv, const char *prefix);
+extern int cmd_clean(int argc, const char **argv, const char *prefix);
+extern int cmd_commit(int argc, const char **argv, const char *prefix);
+extern int cmd_commit_tree(int argc, const char **argv, const char *prefix);
+extern int cmd_count_objects(int argc, const char **argv, const char *prefix);
+extern int cmd_describe(int argc, const char **argv, const char *prefix);
+extern int cmd_diff_files(int argc, const char **argv, const char *prefix);
+extern int cmd_diff_index(int argc, const char **argv, const char *prefix);
+extern int cmd_diff(int argc, const char **argv, const char *prefix);
+extern int cmd_diff_tree(int argc, const char **argv, const char *prefix);
+extern int cmd_fast_export(int argc, const char **argv, const char *prefix);
+extern int cmd_fetch(int argc, const char **argv, const char *prefix);
+extern int cmd_fetch_pack(int argc, const char **argv, const char *prefix);
+extern int cmd_fetch__tool(int argc, const char **argv, const char *prefix);
+extern int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix);
+extern int cmd_for_each_ref(int argc, const char **argv, const char *prefix);
+extern int cmd_format_patch(int argc, const char **argv, const char *prefix);
+extern int cmd_fsck(int argc, const char **argv, const char *prefix);
+extern int cmd_gc(int argc, const char **argv, const char *prefix);
+extern int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);
+extern int cmd_grep(int argc, const char **argv, const char *prefix);
+extern int cmd_help(int argc, const char **argv, const char *prefix);
+extern int cmd_http_fetch(int argc, const char **argv, const char *prefix);
+extern int cmd_init_db(int argc, const char **argv, const char *prefix);
+extern int cmd_log(int argc, const char **argv, const char *prefix);
+extern int cmd_log_reflog(int argc, const char **argv, const char *prefix);
+extern int cmd_ls_files(int argc, const char **argv, const char *prefix);
+extern int cmd_ls_tree(int argc, const char **argv, const char *prefix);
+extern int cmd_ls_remote(int argc, const char **argv, const char *prefix);
+extern int cmd_mailinfo(int argc, const char **argv, const char *prefix);
+extern int cmd_mailsplit(int argc, const char **argv, const char *prefix);
+extern int cmd_merge_base(int argc, const char **argv, const char *prefix);
+extern int cmd_merge_ours(int argc, const char **argv, const char *prefix);
+extern int cmd_merge_file(int argc, const char **argv, const char *prefix);
+extern int cmd_mv(int argc, const char **argv, const char *prefix);
+extern int cmd_name_rev(int argc, const char **argv, const char *prefix);
+extern int cmd_pack_objects(int argc, const char **argv, const char *prefix);
+extern int cmd_pickaxe(int argc, const char **argv, const char *prefix);
+extern int cmd_prune(int argc, const char **argv, const char *prefix);
+extern int cmd_prune_packed(int argc, const char **argv, const char *prefix);
+extern int cmd_push(int argc, const char **argv, const char *prefix);
+extern int cmd_read_tree(int argc, const char **argv, const char *prefix);
+extern int cmd_reflog(int argc, const char **argv, const char *prefix);
+extern int cmd_config(int argc, const char **argv, const char *prefix);
+extern int cmd_rerere(int argc, const char **argv, const char *prefix);
+extern int cmd_reset(int argc, const char **argv, const char *prefix);
+extern int cmd_rev_list(int argc, const char **argv, const char *prefix);
+extern int cmd_rev_parse(int argc, const char **argv, const char *prefix);
+extern int cmd_revert(int argc, const char **argv, const char *prefix);
+extern int cmd_rm(int argc, const char **argv, const char *prefix);
+extern int cmd_send_pack(int argc, const char **argv, const char *prefix);
+extern int cmd_shortlog(int argc, const char **argv, const char *prefix);
+extern int cmd_show(int argc, const char **argv, const char *prefix);
+extern int cmd_show_branch(int argc, const char **argv, const char *prefix);
+extern int cmd_status(int argc, const char **argv, const char *prefix);
+extern int cmd_stripspace(int argc, const char **argv, const char *prefix);
+extern int cmd_symbolic_ref(int argc, const char **argv, const char *prefix);
+extern int cmd_tag(int argc, const char **argv, const char *prefix);
+extern int cmd_tar_tree(int argc, const char **argv, const char *prefix);
+extern int cmd_unpack_objects(int argc, const char **argv, const char *prefix);
+extern int cmd_update_index(int argc, const char **argv, const char *prefix);
+extern int cmd_update_ref(int argc, const char **argv, const char *prefix);
+extern int cmd_upload_archive(int argc, const char **argv, const char *prefix);
+extern int cmd_upload_tar(int argc, const char **argv, const char *prefix);
+extern int cmd_verify_tag(int argc, const char **argv, const char *prefix);
+extern int cmd_version(int argc, const char **argv, const char *prefix);
+extern int cmd_whatchanged(int argc, const char **argv, const char *prefix);
+extern int cmd_write_tree(int argc, const char **argv, const char *prefix);
+extern int cmd_verify_pack(int argc, const char **argv, const char *prefix);
+extern int cmd_show_ref(int argc, const char **argv, const char *prefix);
+extern int cmd_pack_refs(int argc, const char **argv, const char *prefix);
+
+#endif
diff --git a/bundle.c b/bundle.c
new file mode 100644 (file)
index 0000000..9b9b916
--- /dev/null
+++ b/bundle.c
@@ -0,0 +1,351 @@
+#include "cache.h"
+#include "bundle.h"
+#include "object.h"
+#include "commit.h"
+#include "diff.h"
+#include "revision.h"
+#include "list-objects.h"
+#include "run-command.h"
+#include "refs.h"
+
+static const char bundle_signature[] = "# v2 git bundle\n";
+
+static void add_to_ref_list(const unsigned char *sha1, const char *name,
+               struct ref_list *list)
+{
+       if (list->nr + 1 >= list->alloc) {
+               list->alloc = alloc_nr(list->nr + 1);
+               list->list = xrealloc(list->list,
+                               list->alloc * sizeof(list->list[0]));
+       }
+       memcpy(list->list[list->nr].sha1, sha1, 20);
+       list->list[list->nr].name = xstrdup(name);
+       list->nr++;
+}
+
+/* returns an fd */
+int read_bundle_header(const char *path, struct bundle_header *header)
+{
+       char buffer[1024];
+       int fd;
+       long fpos;
+       FILE *ffd = fopen(path, "rb");
+
+       if (!ffd)
+               return error("could not open '%s'", path);
+       if (!fgets(buffer, sizeof(buffer), ffd) ||
+                       strcmp(buffer, bundle_signature)) {
+               fclose(ffd);
+               return error("'%s' does not look like a v2 bundle file", path);
+       }
+       while (fgets(buffer, sizeof(buffer), ffd)
+                       && buffer[0] != '\n') {
+               int is_prereq = buffer[0] == '-';
+               int offset = is_prereq ? 1 : 0;
+               int len = strlen(buffer);
+               unsigned char sha1[20];
+               struct ref_list *list = is_prereq ? &header->prerequisites
+                       : &header->references;
+               char delim;
+
+               if (buffer[len - 1] == '\n')
+                       buffer[len - 1] = '\0';
+               if (get_sha1_hex(buffer + offset, sha1)) {
+                       warning("unrecognized header: %s", buffer);
+                       continue;
+               }
+               delim = buffer[40 + offset];
+               if (!isspace(delim) && (delim != '\0' || !is_prereq))
+                       die ("invalid header: %s", buffer);
+               add_to_ref_list(sha1, isspace(delim) ?
+                               buffer + 41 + offset : "", list);
+       }
+       fpos = ftell(ffd);
+       fclose(ffd);
+       fd = open(path, O_RDONLY);
+       if (fd < 0)
+               return error("could not open '%s'", path);
+       lseek(fd, fpos, SEEK_SET);
+       return fd;
+}
+
+static int list_refs(struct ref_list *r, int argc, const char **argv)
+{
+       int i;
+
+       for (i = 0; i < r->nr; i++) {
+               if (argc > 1) {
+                       int j;
+                       for (j = 1; j < argc; j++)
+                               if (!strcmp(r->list[i].name, argv[j]))
+                                       break;
+                       if (j == argc)
+                               continue;
+               }
+               printf("%s %s\n", sha1_to_hex(r->list[i].sha1),
+                               r->list[i].name);
+       }
+       return 0;
+}
+
+#define PREREQ_MARK (1u<<16)
+
+int verify_bundle(struct bundle_header *header, int verbose)
+{
+       /*
+        * Do fast check, then if any prereqs are missing then go line by line
+        * to be verbose about the errors
+        */
+       struct ref_list *p = &header->prerequisites;
+       struct rev_info revs;
+       const char *argv[] = {NULL, "--all"};
+       struct object_array refs;
+       struct commit *commit;
+       int i, ret = 0, req_nr;
+       const char *message = "Repository lacks these prerequisite commits:";
+
+       init_revisions(&revs, NULL);
+       for (i = 0; i < p->nr; i++) {
+               struct ref_list_entry *e = p->list + i;
+               struct object *o = parse_object(e->sha1);
+               if (o) {
+                       o->flags |= PREREQ_MARK;
+                       add_pending_object(&revs, o, e->name);
+                       continue;
+               }
+               if (++ret == 1)
+                       error(message);
+               error("%s %s", sha1_to_hex(e->sha1), e->name);
+       }
+       if (revs.pending.nr != p->nr)
+               return ret;
+       req_nr = revs.pending.nr;
+       setup_revisions(2, argv, &revs, NULL);
+
+       memset(&refs, 0, sizeof(struct object_array));
+       for (i = 0; i < revs.pending.nr; i++) {
+               struct object_array_entry *e = revs.pending.objects + i;
+               add_object_array(e->item, e->name, &refs);
+       }
+
+       prepare_revision_walk(&revs);
+
+       i = req_nr;
+       while (i && (commit = get_revision(&revs)))
+               if (commit->object.flags & PREREQ_MARK)
+                       i--;
+
+       for (i = 0; i < req_nr; i++)
+               if (!(refs.objects[i].item->flags & SHOWN)) {
+                       if (++ret == 1)
+                               error(message);
+                       error("%s %s", sha1_to_hex(refs.objects[i].item->sha1),
+                               refs.objects[i].name);
+               }
+
+       for (i = 0; i < refs.nr; i++)
+               clear_commit_marks((struct commit *)refs.objects[i].item, -1);
+
+       if (verbose) {
+               struct ref_list *r;
+
+               r = &header->references;
+               printf("The bundle contains %d ref%s\n",
+                      r->nr, (1 < r->nr) ? "s" : "");
+               list_refs(r, 0, NULL);
+               r = &header->prerequisites;
+               printf("The bundle requires these %d ref%s\n",
+                      r->nr, (1 < r->nr) ? "s" : "");
+               list_refs(r, 0, NULL);
+       }
+       return ret;
+}
+
+int list_bundle_refs(struct bundle_header *header, int argc, const char **argv)
+{
+       return list_refs(&header->references, argc, argv);
+}
+
+int create_bundle(struct bundle_header *header, const char *path,
+               int argc, const char **argv)
+{
+       static struct lock_file lock;
+       int bundle_fd = -1;
+       int bundle_to_stdout;
+       const char **argv_boundary = xmalloc((argc + 4) * sizeof(const char *));
+       const char **argv_pack = xmalloc(5 * sizeof(const char *));
+       int i, ref_count = 0;
+       char buffer[1024];
+       struct rev_info revs;
+       struct child_process rls;
+       FILE *rls_fout;
+
+       bundle_to_stdout = !strcmp(path, "-");
+       if (bundle_to_stdout)
+               bundle_fd = 1;
+       else
+               bundle_fd = hold_lock_file_for_update(&lock, path, 1);
+
+       /* write signature */
+       write_or_die(bundle_fd, bundle_signature, strlen(bundle_signature));
+
+       /* init revs to list objects for pack-objects later */
+       save_commit_buffer = 0;
+       init_revisions(&revs, NULL);
+
+       /* write prerequisites */
+       memcpy(argv_boundary + 3, argv + 1, argc * sizeof(const char *));
+       argv_boundary[0] = "rev-list";
+       argv_boundary[1] = "--boundary";
+       argv_boundary[2] = "--pretty=oneline";
+       argv_boundary[argc + 2] = NULL;
+       memset(&rls, 0, sizeof(rls));
+       rls.argv = argv_boundary;
+       rls.out = -1;
+       rls.git_cmd = 1;
+       if (start_command(&rls))
+               return -1;
+       rls_fout = fdopen(rls.out, "r");
+       while (fgets(buffer, sizeof(buffer), rls_fout)) {
+               unsigned char sha1[20];
+               if (buffer[0] == '-') {
+                       write_or_die(bundle_fd, buffer, strlen(buffer));
+                       if (!get_sha1_hex(buffer + 1, sha1)) {
+                               struct object *object = parse_object(sha1);
+                               object->flags |= UNINTERESTING;
+                               add_pending_object(&revs, object, buffer);
+                       }
+               } else if (!get_sha1_hex(buffer, sha1)) {
+                       struct object *object = parse_object(sha1);
+                       object->flags |= SHOWN;
+               }
+       }
+       fclose(rls_fout);
+       if (finish_command(&rls))
+               return error("rev-list died");
+
+       /* write references */
+       argc = setup_revisions(argc, argv, &revs, NULL);
+       if (argc > 1)
+               return error("unrecognized argument: %s'", argv[1]);
+
+       for (i = 0; i < revs.pending.nr; i++) {
+               struct object_array_entry *e = revs.pending.objects + i;
+               unsigned char sha1[20];
+               char *ref;
+               const char *display_ref;
+               int flag;
+
+               if (e->item->flags & UNINTERESTING)
+                       continue;
+               if (dwim_ref(e->name, strlen(e->name), sha1, &ref) != 1)
+                       continue;
+               if (!resolve_ref(e->name, sha1, 1, &flag))
+                       flag = 0;
+               display_ref = (flag & REF_ISSYMREF) ? e->name : ref;
+
+               /*
+                * Make sure the refs we wrote out is correct; --max-count and
+                * other limiting options could have prevented all the tips
+                * from getting output.
+                *
+                * Non commit objects such as tags and blobs do not have
+                * this issue as they are not affected by those extra
+                * constraints.
+                */
+               if (!(e->item->flags & SHOWN) && e->item->type == OBJ_COMMIT) {
+                       warning("ref '%s' is excluded by the rev-list options",
+                               e->name);
+                       free(ref);
+                       continue;
+               }
+               /*
+                * If you run "git bundle create bndl v1.0..v2.0", the
+                * name of the positive ref is "v2.0" but that is the
+                * commit that is referenced by the tag, and not the tag
+                * itself.
+                */
+               if (hashcmp(sha1, e->item->sha1)) {
+                       /*
+                        * Is this the positive end of a range expressed
+                        * in terms of a tag (e.g. v2.0 from the range
+                        * "v1.0..v2.0")?
+                        */
+                       struct commit *one = lookup_commit_reference(sha1);
+                       struct object *obj;
+
+                       if (e->item == &(one->object)) {
+                               /*
+                                * Need to include e->name as an
+                                * independent ref to the pack-objects
+                                * input, so that the tag is included
+                                * in the output; otherwise we would
+                                * end up triggering "empty bundle"
+                                * error.
+                                */
+                               obj = parse_object(sha1);
+                               obj->flags |= SHOWN;
+                               add_pending_object(&revs, obj, e->name);
+                       }
+                       free(ref);
+                       continue;
+               }
+
+               ref_count++;
+               write_or_die(bundle_fd, sha1_to_hex(e->item->sha1), 40);
+               write_or_die(bundle_fd, " ", 1);
+               write_or_die(bundle_fd, display_ref, strlen(display_ref));
+               write_or_die(bundle_fd, "\n", 1);
+               free(ref);
+       }
+       if (!ref_count)
+               die ("Refusing to create empty bundle.");
+
+       /* end header */
+       write_or_die(bundle_fd, "\n", 1);
+
+       /* write pack */
+       argv_pack[0] = "pack-objects";
+       argv_pack[1] = "--all-progress";
+       argv_pack[2] = "--stdout";
+       argv_pack[3] = "--thin";
+       argv_pack[4] = NULL;
+       memset(&rls, 0, sizeof(rls));
+       rls.argv = argv_pack;
+       rls.in = -1;
+       rls.out = bundle_fd;
+       rls.git_cmd = 1;
+       if (start_command(&rls))
+               return error("Could not spawn pack-objects");
+       for (i = 0; i < revs.pending.nr; i++) {
+               struct object *object = revs.pending.objects[i].item;
+               if (object->flags & UNINTERESTING)
+                       write(rls.in, "^", 1);
+               write(rls.in, sha1_to_hex(object->sha1), 40);
+               write(rls.in, "\n", 1);
+       }
+       if (finish_command(&rls))
+               return error ("pack-objects died");
+       close(bundle_fd);
+       if (!bundle_to_stdout)
+               commit_lock_file(&lock);
+       return 0;
+}
+
+int unbundle(struct bundle_header *header, int bundle_fd)
+{
+       const char *argv_index_pack[] = {"index-pack",
+               "--fix-thin", "--stdin", NULL};
+       struct child_process ip;
+
+       if (verify_bundle(header, 0))
+               return -1;
+       memset(&ip, 0, sizeof(ip));
+       ip.argv = argv_index_pack;
+       ip.in = bundle_fd;
+       ip.no_stdout = 1;
+       ip.git_cmd = 1;
+       if (run_command(&ip))
+               return error("index-pack died");
+       return 0;
+}
diff --git a/bundle.h b/bundle.h
new file mode 100644 (file)
index 0000000..e2aedd6
--- /dev/null
+++ b/bundle.h
@@ -0,0 +1,25 @@
+#ifndef BUNDLE_H
+#define BUNDLE_H
+
+struct ref_list {
+       unsigned int nr, alloc;
+       struct ref_list_entry {
+               unsigned char sha1[20];
+               char *name;
+       } *list;
+};
+
+struct bundle_header {
+       struct ref_list prerequisites;
+       struct ref_list references;
+};
+
+int read_bundle_header(const char *path, struct bundle_header *header);
+int create_bundle(struct bundle_header *header, const char *path,
+               int argc, const char **argv);
+int verify_bundle(struct bundle_header *header, int verbose);
+int unbundle(struct bundle_header *header, int bundle_fd);
+int list_bundle_refs(struct bundle_header *header,
+               int argc, const char **argv);
+
+#endif
diff --git a/cache-tree.c b/cache-tree.c
new file mode 100644 (file)
index 0000000..50b3526
--- /dev/null
@@ -0,0 +1,531 @@
+#include "cache.h"
+#include "tree.h"
+#include "cache-tree.h"
+
+#ifndef DEBUG
+#define DEBUG 0
+#endif
+
+struct cache_tree *cache_tree(void)
+{
+       struct cache_tree *it = xcalloc(1, sizeof(struct cache_tree));
+       it->entry_count = -1;
+       return it;
+}
+
+void cache_tree_free(struct cache_tree **it_p)
+{
+       int i;
+       struct cache_tree *it = *it_p;
+
+       if (!it)
+               return;
+       for (i = 0; i < it->subtree_nr; i++)
+               if (it->down[i])
+                       cache_tree_free(&it->down[i]->cache_tree);
+       free(it->down);
+       free(it);
+       *it_p = NULL;
+}
+
+static int subtree_name_cmp(const char *one, int onelen,
+                           const char *two, int twolen)
+{
+       if (onelen < twolen)
+               return -1;
+       if (twolen < onelen)
+               return 1;
+       return memcmp(one, two, onelen);
+}
+
+static int subtree_pos(struct cache_tree *it, const char *path, int pathlen)
+{
+       struct cache_tree_sub **down = it->down;
+       int lo, hi;
+       lo = 0;
+       hi = it->subtree_nr;
+       while (lo < hi) {
+               int mi = (lo + hi) / 2;
+               struct cache_tree_sub *mdl = down[mi];
+               int cmp = subtree_name_cmp(path, pathlen,
+                                          mdl->name, mdl->namelen);
+               if (!cmp)
+                       return mi;
+               if (cmp < 0)
+                       hi = mi;
+               else
+                       lo = mi + 1;
+       }
+       return -lo-1;
+}
+
+static struct cache_tree_sub *find_subtree(struct cache_tree *it,
+                                          const char *path,
+                                          int pathlen,
+                                          int create)
+{
+       struct cache_tree_sub *down;
+       int pos = subtree_pos(it, path, pathlen);
+       if (0 <= pos)
+               return it->down[pos];
+       if (!create)
+               return NULL;
+
+       pos = -pos-1;
+       if (it->subtree_alloc <= it->subtree_nr) {
+               it->subtree_alloc = alloc_nr(it->subtree_alloc);
+               it->down = xrealloc(it->down, it->subtree_alloc *
+                                   sizeof(*it->down));
+       }
+       it->subtree_nr++;
+
+       down = xmalloc(sizeof(*down) + pathlen + 1);
+       down->cache_tree = NULL;
+       down->namelen = pathlen;
+       memcpy(down->name, path, pathlen);
+       down->name[pathlen] = 0;
+
+       if (pos < it->subtree_nr)
+               memmove(it->down + pos + 1,
+                       it->down + pos,
+                       sizeof(down) * (it->subtree_nr - pos - 1));
+       it->down[pos] = down;
+       return down;
+}
+
+struct cache_tree_sub *cache_tree_sub(struct cache_tree *it, const char *path)
+{
+       int pathlen = strlen(path);
+       return find_subtree(it, path, pathlen, 1);
+}
+
+void cache_tree_invalidate_path(struct cache_tree *it, const char *path)
+{
+       /* a/b/c
+        * ==> invalidate self
+        * ==> find "a", have it invalidate "b/c"
+        * a
+        * ==> invalidate self
+        * ==> if "a" exists as a subtree, remove it.
+        */
+       const char *slash;
+       int namelen;
+       struct cache_tree_sub *down;
+
+#if DEBUG
+       fprintf(stderr, "cache-tree invalidate <%s>\n", path);
+#endif
+
+       if (!it)
+               return;
+       slash = strchr(path, '/');
+       it->entry_count = -1;
+       if (!slash) {
+               int pos;
+               namelen = strlen(path);
+               pos = subtree_pos(it, path, namelen);
+               if (0 <= pos) {
+                       cache_tree_free(&it->down[pos]->cache_tree);
+                       free(it->down[pos]);
+                       /* 0 1 2 3 4 5
+                        *       ^     ^subtree_nr = 6
+                        *       pos
+                        * move 4 and 5 up one place (2 entries)
+                        * 2 = 6 - 3 - 1 = subtree_nr - pos - 1
+                        */
+                       memmove(it->down+pos, it->down+pos+1,
+                               sizeof(struct cache_tree_sub *) *
+                               (it->subtree_nr - pos - 1));
+                       it->subtree_nr--;
+               }
+               return;
+       }
+       namelen = slash - path;
+       down = find_subtree(it, path, namelen, 0);
+       if (down)
+               cache_tree_invalidate_path(down->cache_tree, slash + 1);
+}
+
+static int verify_cache(struct cache_entry **cache,
+                       int entries)
+{
+       int i, funny;
+
+       /* Verify that the tree is merged */
+       funny = 0;
+       for (i = 0; i < entries; i++) {
+               struct cache_entry *ce = cache[i];
+               if (ce_stage(ce)) {
+                       if (10 < ++funny) {
+                               fprintf(stderr, "...\n");
+                               break;
+                       }
+                       fprintf(stderr, "%s: unmerged (%s)\n",
+                               ce->name, sha1_to_hex(ce->sha1));
+               }
+       }
+       if (funny)
+               return -1;
+
+       /* Also verify that the cache does not have path and path/file
+        * at the same time.  At this point we know the cache has only
+        * stage 0 entries.
+        */
+       funny = 0;
+       for (i = 0; i < entries - 1; i++) {
+               /* path/file always comes after path because of the way
+                * the cache is sorted.  Also path can appear only once,
+                * which means conflicting one would immediately follow.
+                */
+               const char *this_name = cache[i]->name;
+               const char *next_name = cache[i+1]->name;
+               int this_len = strlen(this_name);
+               if (this_len < strlen(next_name) &&
+                   strncmp(this_name, next_name, this_len) == 0 &&
+                   next_name[this_len] == '/') {
+                       if (10 < ++funny) {
+                               fprintf(stderr, "...\n");
+                               break;
+                       }
+                       fprintf(stderr, "You have both %s and %s\n",
+                               this_name, next_name);
+               }
+       }
+       if (funny)
+               return -1;
+       return 0;
+}
+
+static void discard_unused_subtrees(struct cache_tree *it)
+{
+       struct cache_tree_sub **down = it->down;
+       int nr = it->subtree_nr;
+       int dst, src;
+       for (dst = src = 0; src < nr; src++) {
+               struct cache_tree_sub *s = down[src];
+               if (s->used)
+                       down[dst++] = s;
+               else {
+                       cache_tree_free(&s->cache_tree);
+                       free(s);
+                       it->subtree_nr--;
+               }
+       }
+}
+
+int cache_tree_fully_valid(struct cache_tree *it)
+{
+       int i;
+       if (!it)
+               return 0;
+       if (it->entry_count < 0 || !has_sha1_file(it->sha1))
+               return 0;
+       for (i = 0; i < it->subtree_nr; i++) {
+               if (!cache_tree_fully_valid(it->down[i]->cache_tree))
+                       return 0;
+       }
+       return 1;
+}
+
+static int update_one(struct cache_tree *it,
+                     struct cache_entry **cache,
+                     int entries,
+                     const char *base,
+                     int baselen,
+                     int missing_ok,
+                     int dryrun)
+{
+       struct strbuf buffer;
+       int i;
+
+       if (0 <= it->entry_count && has_sha1_file(it->sha1))
+               return it->entry_count;
+
+       /*
+        * We first scan for subtrees and update them; we start by
+        * marking existing subtrees -- the ones that are unmarked
+        * should not be in the result.
+        */
+       for (i = 0; i < it->subtree_nr; i++)
+               it->down[i]->used = 0;
+
+       /*
+        * Find the subtrees and update them.
+        */
+       for (i = 0; i < entries; i++) {
+               struct cache_entry *ce = cache[i];
+               struct cache_tree_sub *sub;
+               const char *path, *slash;
+               int pathlen, sublen, subcnt;
+
+               path = ce->name;
+               pathlen = ce_namelen(ce);
+               if (pathlen <= baselen || memcmp(base, path, baselen))
+                       break; /* at the end of this level */
+
+               slash = strchr(path + baselen, '/');
+               if (!slash)
+                       continue;
+               /*
+                * a/bbb/c (base = a/, slash = /c)
+                * ==>
+                * path+baselen = bbb/c, sublen = 3
+                */
+               sublen = slash - (path + baselen);
+               sub = find_subtree(it, path + baselen, sublen, 1);
+               if (!sub->cache_tree)
+                       sub->cache_tree = cache_tree();
+               subcnt = update_one(sub->cache_tree,
+                                   cache + i, entries - i,
+                                   path,
+                                   baselen + sublen + 1,
+                                   missing_ok,
+                                   dryrun);
+               if (subcnt < 0)
+                       return subcnt;
+               i += subcnt - 1;
+               sub->used = 1;
+       }
+
+       discard_unused_subtrees(it);
+
+       /*
+        * Then write out the tree object for this level.
+        */
+       strbuf_init(&buffer, 8192);
+
+       for (i = 0; i < entries; i++) {
+               struct cache_entry *ce = cache[i];
+               struct cache_tree_sub *sub;
+               const char *path, *slash;
+               int pathlen, entlen;
+               const unsigned char *sha1;
+               unsigned mode;
+
+               path = ce->name;
+               pathlen = ce_namelen(ce);
+               if (pathlen <= baselen || memcmp(base, path, baselen))
+                       break; /* at the end of this level */
+
+               slash = strchr(path + baselen, '/');
+               if (slash) {
+                       entlen = slash - (path + baselen);
+                       sub = find_subtree(it, path + baselen, entlen, 0);
+                       if (!sub)
+                               die("cache-tree.c: '%.*s' in '%s' not found",
+                                   entlen, path + baselen, path);
+                       i += sub->cache_tree->entry_count - 1;
+                       sha1 = sub->cache_tree->sha1;
+                       mode = S_IFDIR;
+               }
+               else {
+                       sha1 = ce->sha1;
+                       mode = ntohl(ce->ce_mode);
+                       entlen = pathlen - baselen;
+               }
+               if (mode != S_IFGITLINK && !missing_ok && !has_sha1_file(sha1))
+                       return error("invalid object %s", sha1_to_hex(sha1));
+
+               if (!ce->ce_mode)
+                       continue; /* entry being removed */
+
+               strbuf_grow(&buffer, entlen + 100);
+               strbuf_addf(&buffer, "%o %.*s%c", mode, entlen, path + baselen, '\0');
+               strbuf_add(&buffer, sha1, 20);
+
+#if DEBUG
+               fprintf(stderr, "cache-tree update-one %o %.*s\n",
+                       mode, entlen, path + baselen);
+#endif
+       }
+
+       if (dryrun)
+               hash_sha1_file(buffer.buf, buffer.len, tree_type, it->sha1);
+       else
+               write_sha1_file(buffer.buf, buffer.len, tree_type, it->sha1);
+       strbuf_release(&buffer);
+       it->entry_count = i;
+#if DEBUG
+       fprintf(stderr, "cache-tree update-one (%d ent, %d subtree) %s\n",
+               it->entry_count, it->subtree_nr,
+               sha1_to_hex(it->sha1));
+#endif
+       return i;
+}
+
+int cache_tree_update(struct cache_tree *it,
+                     struct cache_entry **cache,
+                     int entries,
+                     int missing_ok,
+                     int dryrun)
+{
+       int i;
+       i = verify_cache(cache, entries);
+       if (i)
+               return i;
+       i = update_one(it, cache, entries, "", 0, missing_ok, dryrun);
+       if (i < 0)
+               return i;
+       return 0;
+}
+
+static void write_one(struct strbuf *buffer, struct cache_tree *it,
+                      const char *path, int pathlen)
+{
+       int i;
+
+       /* One "cache-tree" entry consists of the following:
+        * path (NUL terminated)
+        * entry_count, subtree_nr ("%d %d\n")
+        * tree-sha1 (missing if invalid)
+        * subtree_nr "cache-tree" entries for subtrees.
+        */
+       strbuf_grow(buffer, pathlen + 100);
+       strbuf_add(buffer, path, pathlen);
+       strbuf_addf(buffer, "%c%d %d\n", 0, it->entry_count, it->subtree_nr);
+
+#if DEBUG
+       if (0 <= it->entry_count)
+               fprintf(stderr, "cache-tree <%.*s> (%d ent, %d subtree) %s\n",
+                       pathlen, path, it->entry_count, it->subtree_nr,
+                       sha1_to_hex(it->sha1));
+       else
+               fprintf(stderr, "cache-tree <%.*s> (%d subtree) invalid\n",
+                       pathlen, path, it->subtree_nr);
+#endif
+
+       if (0 <= it->entry_count) {
+               strbuf_add(buffer, it->sha1, 20);
+       }
+       for (i = 0; i < it->subtree_nr; i++) {
+               struct cache_tree_sub *down = it->down[i];
+               if (i) {
+                       struct cache_tree_sub *prev = it->down[i-1];
+                       if (subtree_name_cmp(down->name, down->namelen,
+                                            prev->name, prev->namelen) <= 0)
+                               die("fatal - unsorted cache subtree");
+               }
+               write_one(buffer, down->cache_tree, down->name, down->namelen);
+       }
+}
+
+void cache_tree_write(struct strbuf *sb, struct cache_tree *root)
+{
+       write_one(sb, root, "", 0);
+}
+
+static struct cache_tree *read_one(const char **buffer, unsigned long *size_p)
+{
+       const char *buf = *buffer;
+       unsigned long size = *size_p;
+       const char *cp;
+       char *ep;
+       struct cache_tree *it;
+       int i, subtree_nr;
+
+       it = NULL;
+       /* skip name, but make sure name exists */
+       while (size && *buf) {
+               size--;
+               buf++;
+       }
+       if (!size)
+               goto free_return;
+       buf++; size--;
+       it = cache_tree();
+
+       cp = buf;
+       it->entry_count = strtol(cp, &ep, 10);
+       if (cp == ep)
+               goto free_return;
+       cp = ep;
+       subtree_nr = strtol(cp, &ep, 10);
+       if (cp == ep)
+               goto free_return;
+       while (size && *buf && *buf != '\n') {
+               size--;
+               buf++;
+       }
+       if (!size)
+               goto free_return;
+       buf++; size--;
+       if (0 <= it->entry_count) {
+               if (size < 20)
+                       goto free_return;
+               hashcpy(it->sha1, (const unsigned char*)buf);
+               buf += 20;
+               size -= 20;
+       }
+
+#if DEBUG
+       if (0 <= it->entry_count)
+               fprintf(stderr, "cache-tree <%s> (%d ent, %d subtree) %s\n",
+                       *buffer, it->entry_count, subtree_nr,
+                       sha1_to_hex(it->sha1));
+       else
+               fprintf(stderr, "cache-tree <%s> (%d subtrees) invalid\n",
+                       *buffer, subtree_nr);
+#endif
+
+       /*
+        * Just a heuristic -- we do not add directories that often but
+        * we do not want to have to extend it immediately when we do,
+        * hence +2.
+        */
+       it->subtree_alloc = subtree_nr + 2;
+       it->down = xcalloc(it->subtree_alloc, sizeof(struct cache_tree_sub *));
+       for (i = 0; i < subtree_nr; i++) {
+               /* read each subtree */
+               struct cache_tree *sub;
+               struct cache_tree_sub *subtree;
+               const char *name = buf;
+
+               sub = read_one(&buf, &size);
+               if (!sub)
+                       goto free_return;
+               subtree = cache_tree_sub(it, name);
+               subtree->cache_tree = sub;
+       }
+       if (subtree_nr != it->subtree_nr)
+               die("cache-tree: internal error");
+       *buffer = buf;
+       *size_p = size;
+       return it;
+
+ free_return:
+       cache_tree_free(&it);
+       return NULL;
+}
+
+struct cache_tree *cache_tree_read(const char *buffer, unsigned long size)
+{
+       if (buffer[0])
+               return NULL; /* not the whole tree */
+       return read_one(&buffer, &size);
+}
+
+struct cache_tree *cache_tree_find(struct cache_tree *it, const char *path)
+{
+       while (*path) {
+               const char *slash;
+               struct cache_tree_sub *sub;
+
+               slash = strchr(path, '/');
+               if (!slash)
+                       slash = path + strlen(path);
+               /* between path and slash is the name of the
+                * subtree to look for.
+                */
+               sub = find_subtree(it, path, slash - path, 0);
+               if (!sub)
+                       return NULL;
+               it = sub->cache_tree;
+               if (slash)
+                       while (*slash && *slash == '/')
+                               slash++;
+               if (!slash || !*slash)
+                       return it; /* prefix ended with slashes */
+               path = slash;
+       }
+       return it;
+}
diff --git a/cache-tree.h b/cache-tree.h
new file mode 100644 (file)
index 0000000..8243228
--- /dev/null
@@ -0,0 +1,33 @@
+#ifndef CACHE_TREE_H
+#define CACHE_TREE_H
+
+struct cache_tree;
+struct cache_tree_sub {
+       struct cache_tree *cache_tree;
+       int namelen;
+       int used;
+       char name[FLEX_ARRAY];
+};
+
+struct cache_tree {
+       int entry_count; /* negative means "invalid" */
+       unsigned char sha1[20];
+       int subtree_nr;
+       int subtree_alloc;
+       struct cache_tree_sub **down;
+};
+
+struct cache_tree *cache_tree(void);
+void cache_tree_free(struct cache_tree **);
+void cache_tree_invalidate_path(struct cache_tree *, const char *);
+struct cache_tree_sub *cache_tree_sub(struct cache_tree *, const char *);
+
+void cache_tree_write(struct strbuf *, struct cache_tree *root);
+struct cache_tree *cache_tree_read(const char *buffer, unsigned long size);
+
+int cache_tree_fully_valid(struct cache_tree *);
+int cache_tree_update(struct cache_tree *, struct cache_entry **, int, int, int);
+
+struct cache_tree *cache_tree_find(struct cache_tree *, const char *);
+
+#endif
diff --git a/cache.h b/cache.h
new file mode 100644 (file)
index 0000000..406befb
--- /dev/null
+++ b/cache.h
@@ -0,0 +1,649 @@
+#ifndef CACHE_H
+#define CACHE_H
+
+#include "git-compat-util.h"
+#include "strbuf.h"
+
+#include SHA1_HEADER
+#include <zlib.h>
+
+#if defined(NO_DEFLATE_BOUND) || ZLIB_VERNUM < 0x1200
+#define deflateBound(c,s)  ((s) + (((s) + 7) >> 3) + (((s) + 63) >> 6) + 11)
+#endif
+
+#if defined(DT_UNKNOWN) && !defined(NO_D_TYPE_IN_DIRENT)
+#define DTYPE(de)      ((de)->d_type)
+#else
+#undef DT_UNKNOWN
+#undef DT_DIR
+#undef DT_REG
+#undef DT_LNK
+#define DT_UNKNOWN     0
+#define DT_DIR         1
+#define DT_REG         2
+#define DT_LNK         3
+#define DTYPE(de)      DT_UNKNOWN
+#endif
+
+/* unknown mode (impossible combination S_IFIFO|S_IFCHR) */
+#define S_IFINVALID     0030000
+
+/*
+ * A "directory link" is a link to another git directory.
+ *
+ * The value 0160000 is not normally a valid mode, and
+ * also just happens to be S_IFDIR + S_IFLNK
+ *
+ * NOTE! We *really* shouldn't depend on the S_IFxxx macros
+ * always having the same values everywhere. We should use
+ * our internal git values for these things, and then we can
+ * translate that to the OS-specific value. It just so
+ * happens that everybody shares the same bit representation
+ * in the UNIX world (and apparently wider too..)
+ */
+#define S_IFGITLINK    0160000
+#define S_ISGITLINK(m) (((m) & S_IFMT) == S_IFGITLINK)
+
+/*
+ * Intensive research over the course of many years has shown that
+ * port 9418 is totally unused by anything else. Or
+ *
+ *     Your search - "port 9418" - did not match any documents.
+ *
+ * as www.google.com puts it.
+ *
+ * This port has been properly assigned for git use by IANA:
+ * git (Assigned-9418) [I06-050728-0001].
+ *
+ *     git  9418/tcp   git pack transfer service
+ *     git  9418/udp   git pack transfer service
+ *
+ * with Linus Torvalds <torvalds@osdl.org> as the point of
+ * contact. September 2005.
+ *
+ * See http://www.iana.org/assignments/port-numbers
+ */
+#define DEFAULT_GIT_PORT 9418
+
+/*
+ * Basic data structures for the directory cache
+ */
+
+#define CACHE_SIGNATURE 0x44495243     /* "DIRC" */
+struct cache_header {
+       unsigned int hdr_signature;
+       unsigned int hdr_version;
+       unsigned int hdr_entries;
+};
+
+/*
+ * The "cache_time" is just the low 32 bits of the
+ * time. It doesn't matter if it overflows - we only
+ * check it for equality in the 32 bits we save.
+ */
+struct cache_time {
+       unsigned int sec;
+       unsigned int nsec;
+};
+
+/*
+ * dev/ino/uid/gid/size are also just tracked to the low 32 bits
+ * Again - this is just a (very strong in practice) heuristic that
+ * the inode hasn't changed.
+ *
+ * We save the fields in big-endian order to allow using the
+ * index file over NFS transparently.
+ */
+struct cache_entry {
+       struct cache_time ce_ctime;
+       struct cache_time ce_mtime;
+       unsigned int ce_dev;
+       unsigned int ce_ino;
+       unsigned int ce_mode;
+       unsigned int ce_uid;
+       unsigned int ce_gid;
+       unsigned int ce_size;
+       unsigned char sha1[20];
+       unsigned short ce_flags;
+       char name[FLEX_ARRAY]; /* more */
+};
+
+#define CE_NAMEMASK  (0x0fff)
+#define CE_STAGEMASK (0x3000)
+#define CE_UPDATE    (0x4000)
+#define CE_VALID     (0x8000)
+#define CE_STAGESHIFT 12
+
+#define create_ce_flags(len, stage) htons((len) | ((stage) << CE_STAGESHIFT))
+#define ce_namelen(ce) (CE_NAMEMASK & ntohs((ce)->ce_flags))
+#define ce_size(ce) cache_entry_size(ce_namelen(ce))
+#define ce_stage(ce) ((CE_STAGEMASK & ntohs((ce)->ce_flags)) >> CE_STAGESHIFT)
+
+#define ce_permissions(mode) (((mode) & 0100) ? 0755 : 0644)
+static inline unsigned int create_ce_mode(unsigned int mode)
+{
+       if (S_ISLNK(mode))
+               return htonl(S_IFLNK);
+       if (S_ISDIR(mode) || S_ISGITLINK(mode))
+               return htonl(S_IFGITLINK);
+       return htonl(S_IFREG | ce_permissions(mode));
+}
+static inline unsigned int ce_mode_from_stat(struct cache_entry *ce, unsigned int mode)
+{
+       extern int trust_executable_bit, has_symlinks;
+       if (!has_symlinks && S_ISREG(mode) &&
+           ce && S_ISLNK(ntohl(ce->ce_mode)))
+               return ce->ce_mode;
+       if (!trust_executable_bit && S_ISREG(mode)) {
+               if (ce && S_ISREG(ntohl(ce->ce_mode)))
+                       return ce->ce_mode;
+               return create_ce_mode(0666);
+       }
+       return create_ce_mode(mode);
+}
+#define canon_mode(mode) \
+       (S_ISREG(mode) ? (S_IFREG | ce_permissions(mode)) : \
+       S_ISLNK(mode) ? S_IFLNK : S_ISDIR(mode) ? S_IFDIR : S_IFGITLINK)
+
+#define cache_entry_size(len) ((offsetof(struct cache_entry,name) + (len) + 8) & ~7)
+
+struct index_state {
+       struct cache_entry **cache;
+       unsigned int cache_nr, cache_alloc, cache_changed;
+       struct cache_tree *cache_tree;
+       time_t timestamp;
+       void *mmap;
+       size_t mmap_size;
+};
+
+extern struct index_state the_index;
+
+#ifndef NO_THE_INDEX_COMPATIBILITY_MACROS
+#define active_cache (the_index.cache)
+#define active_nr (the_index.cache_nr)
+#define active_alloc (the_index.cache_alloc)
+#define active_cache_changed (the_index.cache_changed)
+#define active_cache_tree (the_index.cache_tree)
+
+#define read_cache() read_index(&the_index)
+#define read_cache_from(path) read_index_from(&the_index, (path))
+#define write_cache(newfd, cache, entries) write_index(&the_index, (newfd))
+#define discard_cache() discard_index(&the_index)
+#define cache_name_pos(name, namelen) index_name_pos(&the_index,(name),(namelen))
+#define add_cache_entry(ce, option) add_index_entry(&the_index, (ce), (option))
+#define remove_cache_entry_at(pos) remove_index_entry_at(&the_index, (pos))
+#define remove_file_from_cache(path) remove_file_from_index(&the_index, (path))
+#define add_file_to_cache(path, verbose) add_file_to_index(&the_index, (path), (verbose))
+#define refresh_cache(flags) refresh_index(&the_index, (flags), NULL, NULL)
+#define ce_match_stat(ce, st, options) ie_match_stat(&the_index, (ce), (st), (options))
+#define ce_modified(ce, st, options) ie_modified(&the_index, (ce), (st), (options))
+#endif
+
+enum object_type {
+       OBJ_BAD = -1,
+       OBJ_NONE = 0,
+       OBJ_COMMIT = 1,
+       OBJ_TREE = 2,
+       OBJ_BLOB = 3,
+       OBJ_TAG = 4,
+       /* 5 for future expansion */
+       OBJ_OFS_DELTA = 6,
+       OBJ_REF_DELTA = 7,
+       OBJ_MAX,
+};
+
+static inline enum object_type object_type(unsigned int mode)
+{
+       return S_ISDIR(mode) ? OBJ_TREE :
+               S_ISGITLINK(mode) ? OBJ_COMMIT :
+               OBJ_BLOB;
+}
+
+#define GIT_DIR_ENVIRONMENT "GIT_DIR"
+#define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE"
+#define DEFAULT_GIT_DIR_ENVIRONMENT ".git"
+#define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY"
+#define INDEX_ENVIRONMENT "GIT_INDEX_FILE"
+#define GRAFT_ENVIRONMENT "GIT_GRAFT_FILE"
+#define TEMPLATE_DIR_ENVIRONMENT "GIT_TEMPLATE_DIR"
+#define CONFIG_ENVIRONMENT "GIT_CONFIG"
+#define CONFIG_LOCAL_ENVIRONMENT "GIT_CONFIG_LOCAL"
+#define EXEC_PATH_ENVIRONMENT "GIT_EXEC_PATH"
+#define GITATTRIBUTES_FILE ".gitattributes"
+#define INFOATTRIBUTES_FILE "info/attributes"
+#define ATTRIBUTE_MACRO_PREFIX "[attr]"
+
+extern int is_bare_repository_cfg;
+extern int is_bare_repository(void);
+extern int is_inside_git_dir(void);
+extern char *git_work_tree_cfg;
+extern int is_inside_work_tree(void);
+extern const char *get_git_dir(void);
+extern char *get_object_directory(void);
+extern char *get_refs_directory(void);
+extern char *get_index_file(void);
+extern char *get_graft_file(void);
+extern int set_git_dir(const char *path);
+extern const char *get_git_work_tree(void);
+
+#define ALTERNATE_DB_ENVIRONMENT "GIT_ALTERNATE_OBJECT_DIRECTORIES"
+
+extern const char **get_pathspec(const char *prefix, const char **pathspec);
+extern void setup_work_tree(void);
+extern const char *setup_git_directory_gently(int *);
+extern const char *setup_git_directory(void);
+extern const char *prefix_path(const char *prefix, int len, const char *path);
+extern const char *prefix_filename(const char *prefix, int len, const char *path);
+extern void verify_filename(const char *prefix, const char *name);
+extern void verify_non_filename(const char *prefix, const char *name);
+
+#define alloc_nr(x) (((x)+16)*3/2)
+
+/*
+ * Realloc the buffer pointed at by variable 'x' so that it can hold
+ * at least 'nr' entries; the number of entries currently allocated
+ * is 'alloc', using the standard growing factor alloc_nr() macro.
+ *
+ * DO NOT USE any expression with side-effect for 'x' or 'alloc'.
+ */
+#define ALLOC_GROW(x, nr, alloc) \
+       do { \
+               if ((nr) > alloc) { \
+                       if (alloc_nr(alloc) < (nr)) \
+                               alloc = (nr); \
+                       else \
+                               alloc = alloc_nr(alloc); \
+                       x = xrealloc((x), alloc * sizeof(*(x))); \
+               } \
+       } while(0)
+
+/* Initialize and use the cache information */
+extern int read_index(struct index_state *);
+extern int read_index_from(struct index_state *, const char *path);
+extern int write_index(struct index_state *, int newfd);
+extern int discard_index(struct index_state *);
+extern int verify_path(const char *path);
+extern int index_name_pos(struct index_state *, const char *name, int namelen);
+#define ADD_CACHE_OK_TO_ADD 1          /* Ok to add */
+#define ADD_CACHE_OK_TO_REPLACE 2      /* Ok to replace file/directory */
+#define ADD_CACHE_SKIP_DFCHECK 4       /* Ok to skip DF conflict checks */
+#define ADD_CACHE_JUST_APPEND 8                /* Append only; tree.c::read_tree() */
+extern int add_index_entry(struct index_state *, struct cache_entry *ce, int option);
+extern struct cache_entry *refresh_cache_entry(struct cache_entry *ce, int really);
+extern int remove_index_entry_at(struct index_state *, int pos);
+extern int remove_file_from_index(struct index_state *, const char *path);
+extern int add_file_to_index(struct index_state *, const char *path, int verbose);
+extern struct cache_entry *make_cache_entry(unsigned int mode, const unsigned char *sha1, const char *path, int stage, int refresh);
+extern int ce_same_name(struct cache_entry *a, struct cache_entry *b);
+
+/* do stat comparison even if CE_VALID is true */
+#define CE_MATCH_IGNORE_VALID          01
+/* do not check the contents but report dirty on racily-clean entries */
+#define CE_MATCH_RACY_IS_DIRTY 02
+extern int ie_match_stat(struct index_state *, struct cache_entry *, struct stat *, unsigned int);
+extern int ie_modified(struct index_state *, struct cache_entry *, struct stat *, unsigned int);
+
+extern int ce_path_match(const struct cache_entry *ce, const char **pathspec);
+extern int index_fd(unsigned char *sha1, int fd, struct stat *st, int write_object, enum object_type type, const char *path);
+extern int index_pipe(unsigned char *sha1, int fd, const char *type, int write_object);
+extern int index_path(unsigned char *sha1, const char *path, struct stat *st, int write_object);
+extern void fill_stat_cache_info(struct cache_entry *ce, struct stat *st);
+
+#define REFRESH_REALLY         0x0001  /* ignore_valid */
+#define REFRESH_UNMERGED       0x0002  /* allow unmerged */
+#define REFRESH_QUIET          0x0004  /* be quiet about it */
+#define REFRESH_IGNORE_MISSING 0x0008  /* ignore non-existent */
+extern int refresh_index(struct index_state *, unsigned int flags, const char **pathspec, char *seen);
+
+struct lock_file {
+       struct lock_file *next;
+       int fd;
+       pid_t owner;
+       char on_list;
+       char filename[PATH_MAX];
+};
+extern int hold_lock_file_for_update(struct lock_file *, const char *path, int);
+extern int commit_lock_file(struct lock_file *);
+
+extern int hold_locked_index(struct lock_file *, int);
+extern int commit_locked_index(struct lock_file *);
+extern void set_alternate_index_output(const char *);
+
+extern void rollback_lock_file(struct lock_file *);
+extern int delete_ref(const char *, const unsigned char *sha1);
+
+/* Environment bits from configuration mechanism */
+extern int trust_executable_bit;
+extern int quote_path_fully;
+extern int has_symlinks;
+extern int assume_unchanged;
+extern int prefer_symlink_refs;
+extern int log_all_ref_updates;
+extern int warn_ambiguous_refs;
+extern int shared_repository;
+extern const char *apply_default_whitespace;
+extern int zlib_compression_level;
+extern int core_compression_level;
+extern int core_compression_seen;
+extern size_t packed_git_window_size;
+extern size_t packed_git_limit;
+extern size_t delta_base_cache_limit;
+extern int auto_crlf;
+
+#define GIT_REPO_VERSION 0
+extern int repository_format_version;
+extern int check_repository_format(void);
+
+#define MTIME_CHANGED  0x0001
+#define CTIME_CHANGED  0x0002
+#define OWNER_CHANGED  0x0004
+#define MODE_CHANGED    0x0008
+#define INODE_CHANGED   0x0010
+#define DATA_CHANGED    0x0020
+#define TYPE_CHANGED    0x0040
+
+/* Return a statically allocated filename matching the sha1 signature */
+extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern char *sha1_file_name(const unsigned char *sha1);
+extern char *sha1_pack_name(const unsigned char *sha1);
+extern char *sha1_pack_index_name(const unsigned char *sha1);
+extern const char *find_unique_abbrev(const unsigned char *sha1, int);
+extern const unsigned char null_sha1[20];
+static inline int is_null_sha1(const unsigned char *sha1)
+{
+       return !memcmp(sha1, null_sha1, 20);
+}
+static inline int hashcmp(const unsigned char *sha1, const unsigned char *sha2)
+{
+       return memcmp(sha1, sha2, 20);
+}
+static inline void hashcpy(unsigned char *sha_dst, const unsigned char *sha_src)
+{
+       memcpy(sha_dst, sha_src, 20);
+}
+static inline void hashclr(unsigned char *hash)
+{
+       memset(hash, 0, 20);
+}
+
+int git_mkstemp(char *path, size_t n, const char *template);
+
+enum sharedrepo {
+       PERM_UMASK = 0,
+       PERM_GROUP,
+       PERM_EVERYBODY
+};
+int git_config_perm(const char *var, const char *value);
+int adjust_shared_perm(const char *path);
+int safe_create_leading_directories(char *path);
+char *enter_repo(char *path, int strict);
+static inline int is_absolute_path(const char *path)
+{
+       return path[0] == '/';
+}
+const char *make_absolute_path(const char *path);
+
+/* Read and unpack a sha1 file into memory, write memory to a sha1 file */
+extern int sha1_object_info(const unsigned char *, unsigned long *);
+extern void * read_sha1_file(const unsigned char *sha1, enum object_type *type, unsigned long *size);
+extern int hash_sha1_file(const void *buf, unsigned long len, const char *type, unsigned char *sha1);
+extern int write_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *return_sha1);
+extern int pretend_sha1_file(void *, unsigned long, enum object_type, unsigned char *);
+
+extern int check_sha1_signature(const unsigned char *sha1, void *buf, unsigned long size, const char *type);
+
+extern int write_sha1_from_fd(const unsigned char *sha1, int fd, char *buffer,
+                             size_t bufsize, size_t *bufposn);
+extern int write_sha1_to_fd(int fd, const unsigned char *sha1);
+extern int move_temp_to_file(const char *tmpfile, const char *filename);
+
+extern int has_sha1_pack(const unsigned char *sha1, const char **ignore);
+extern int has_sha1_file(const unsigned char *sha1);
+
+extern int has_pack_file(const unsigned char *sha1);
+extern int has_pack_index(const unsigned char *sha1);
+
+extern const signed char hexval_table[256];
+static inline unsigned int hexval(unsigned char c)
+{
+       return hexval_table[c];
+}
+
+/* Convert to/from hex/sha1 representation */
+#define MINIMUM_ABBREV 4
+#define DEFAULT_ABBREV 7
+
+extern int get_sha1(const char *str, unsigned char *sha1);
+extern int get_sha1_with_mode(const char *str, unsigned char *sha1, unsigned *mode);
+extern int get_sha1_hex(const char *hex, unsigned char *sha1);
+extern char *sha1_to_hex(const unsigned char *sha1);   /* static buffer result! */
+extern int read_ref(const char *filename, unsigned char *sha1);
+extern const char *resolve_ref(const char *path, unsigned char *sha1, int, int *);
+extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref);
+extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
+
+extern int refname_match(const char *abbrev_name, const char *full_name, const char **rules);
+extern const char *ref_rev_parse_rules[];
+extern const char *ref_fetch_rules[];
+
+extern int create_symref(const char *ref, const char *refs_heads_master, const char *logmsg);
+extern int validate_headref(const char *ref);
+
+extern int base_name_compare(const char *name1, int len1, int mode1, const char *name2, int len2, int mode2);
+extern int cache_name_compare(const char *name1, int len1, const char *name2, int len2);
+
+extern void *read_object_with_reference(const unsigned char *sha1,
+                                       const char *required_type,
+                                       unsigned long *size,
+                                       unsigned char *sha1_ret);
+
+enum date_mode {
+       DATE_NORMAL = 0,
+       DATE_RELATIVE,
+       DATE_SHORT,
+       DATE_LOCAL,
+       DATE_ISO8601,
+       DATE_RFC2822
+};
+
+const char *show_date(unsigned long time, int timezone, enum date_mode mode);
+int parse_date(const char *date, char *buf, int bufsize);
+void datestamp(char *buf, int bufsize);
+unsigned long approxidate(const char *);
+enum date_mode parse_date_format(const char *format);
+
+extern const char *git_author_info(int);
+extern const char *git_committer_info(int);
+extern const char *fmt_ident(const char *name, const char *email, const char *date_str, int);
+extern const char *fmt_name(const char *name, const char *email);
+
+struct checkout {
+       const char *base_dir;
+       int base_dir_len;
+       unsigned force:1,
+                quiet:1,
+                not_new:1,
+                refresh_cache:1;
+};
+
+extern int checkout_entry(struct cache_entry *ce, const struct checkout *state, char *topath);
+extern int has_symlink_leading_path(const char *name, char *last_symlink);
+
+extern struct alternate_object_database {
+       struct alternate_object_database *next;
+       char *name;
+       char base[FLEX_ARRAY]; /* more */
+} *alt_odb_list;
+extern void prepare_alt_odb(void);
+
+struct pack_window {
+       struct pack_window *next;
+       unsigned char *base;
+       off_t offset;
+       size_t len;
+       unsigned int last_used;
+       unsigned int inuse_cnt;
+};
+
+extern struct packed_git {
+       struct packed_git *next;
+       struct pack_window *windows;
+       off_t pack_size;
+       const void *index_data;
+       size_t index_size;
+       uint32_t num_objects;
+       int index_version;
+       time_t mtime;
+       int pack_fd;
+       int pack_local;
+       unsigned char sha1[20];
+       /* something like ".git/objects/pack/xxxxx.pack" */
+       char pack_name[FLEX_ARRAY]; /* more */
+} *packed_git;
+
+struct pack_entry {
+       off_t offset;
+       unsigned char sha1[20];
+       struct packed_git *p;
+};
+
+struct ref {
+       struct ref *next;
+       unsigned char old_sha1[20];
+       unsigned char new_sha1[20];
+       unsigned int force:1,
+               merge:1,
+               nonfastforward:1,
+               deletion:1;
+       enum {
+               REF_STATUS_NONE = 0,
+               REF_STATUS_OK,
+               REF_STATUS_REJECT_NONFASTFORWARD,
+               REF_STATUS_REJECT_NODELETE,
+               REF_STATUS_UPTODATE,
+               REF_STATUS_REMOTE_REJECT,
+               REF_STATUS_EXPECTING_REPORT,
+       } status;
+       char *remote_status;
+       struct ref *peer_ref; /* when renaming */
+       char name[FLEX_ARRAY]; /* more */
+};
+
+#define REF_NORMAL     (1u << 0)
+#define REF_HEADS      (1u << 1)
+#define REF_TAGS       (1u << 2)
+
+extern struct ref *find_ref_by_name(struct ref *list, const char *name);
+
+#define CONNECT_VERBOSE       (1u << 0)
+extern struct child_process *git_connect(int fd[2], const char *url, const char *prog, int flags);
+extern int finish_connect(struct child_process *conn);
+extern int path_match(const char *path, int nr, char **match);
+extern int get_ack(int fd, unsigned char *result_sha1);
+extern struct ref **get_remote_heads(int in, struct ref **list, int nr_match, char **match, unsigned int flags);
+extern int server_supports(const char *feature);
+
+extern struct packed_git *parse_pack_index(unsigned char *sha1);
+extern struct packed_git *parse_pack_index_file(const unsigned char *sha1,
+                                               const char *idx_path);
+
+extern void prepare_packed_git(void);
+extern void reprepare_packed_git(void);
+extern void install_packed_git(struct packed_git *pack);
+
+extern struct packed_git *find_sha1_pack(const unsigned char *sha1,
+                                        struct packed_git *packs);
+
+extern void pack_report(void);
+extern int open_pack_index(struct packed_git *);
+extern unsigned char* use_pack(struct packed_git *, struct pack_window **, off_t, unsigned int *);
+extern void unuse_pack(struct pack_window **);
+extern struct packed_git *add_packed_git(const char *, int, int);
+extern const unsigned char *nth_packed_object_sha1(struct packed_git *, uint32_t);
+extern off_t find_pack_entry_one(const unsigned char *, struct packed_git *);
+extern void *unpack_entry(struct packed_git *, off_t, enum object_type *, unsigned long *);
+extern unsigned long unpack_object_header_gently(const unsigned char *buf, unsigned long len, enum object_type *type, unsigned long *sizep);
+extern unsigned long get_size_from_delta(struct packed_git *, struct pack_window **, off_t);
+extern const char *packed_object_info_detail(struct packed_git *, off_t, unsigned long *, unsigned long *, unsigned int *, unsigned char *);
+extern int matches_pack_name(struct packed_git *p, const char *name);
+
+/* Dumb servers support */
+extern int update_server_info(int);
+
+typedef int (*config_fn_t)(const char *, const char *);
+extern int git_default_config(const char *, const char *);
+extern int git_config_from_file(config_fn_t fn, const char *);
+extern int git_config(config_fn_t fn);
+extern int git_parse_long(const char *, long *);
+extern int git_parse_ulong(const char *, unsigned long *);
+extern int git_config_int(const char *, const char *);
+extern unsigned long git_config_ulong(const char *, const char *);
+extern int git_config_bool(const char *, const char *);
+extern int git_config_set(const char *, const char *);
+extern int git_config_set_multivar(const char *, const char *, const char *, int);
+extern int git_config_rename_section(const char *, const char *);
+extern const char *git_etc_gitconfig(void);
+extern int check_repository_format_version(const char *var, const char *value);
+
+#define MAX_GITNAME (1000)
+extern char git_default_email[MAX_GITNAME];
+extern char git_default_name[MAX_GITNAME];
+
+extern const char *git_commit_encoding;
+extern const char *git_log_output_encoding;
+
+/* IO helper functions */
+extern void maybe_flush_or_die(FILE *, const char *);
+extern int copy_fd(int ifd, int ofd);
+extern int read_in_full(int fd, void *buf, size_t count);
+extern int write_in_full(int fd, const void *buf, size_t count);
+extern void write_or_die(int fd, const void *buf, size_t count);
+extern int write_or_whine(int fd, const void *buf, size_t count, const char *msg);
+extern int write_or_whine_pipe(int fd, const void *buf, size_t count, const char *msg);
+
+/* pager.c */
+extern void setup_pager(void);
+extern char *pager_program;
+extern int pager_in_use;
+extern int pager_use_color;
+
+extern char *editor_program;
+extern char *excludes_file;
+
+/* base85 */
+int decode_85(char *dst, const char *line, int linelen);
+void encode_85(char *buf, const unsigned char *data, int bytes);
+
+/* alloc.c */
+extern void *alloc_blob_node(void);
+extern void *alloc_tree_node(void);
+extern void *alloc_commit_node(void);
+extern void *alloc_tag_node(void);
+extern void *alloc_object_node(void);
+extern void alloc_report(void);
+
+/* trace.c */
+extern void trace_printf(const char *format, ...);
+extern void trace_argv_printf(const char **argv, const char *format, ...);
+
+/* convert.c */
+/* returns 1 if *dst was used */
+extern int convert_to_git(const char *path, const char *src, size_t len, struct strbuf *dst);
+extern int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst);
+
+/* add */
+void add_files_to_cache(int verbose, const char *prefix, const char **pathspec);
+
+/* diff.c */
+extern int diff_auto_refresh_index;
+
+/* match-trees.c */
+void shift_tree(const unsigned char *, const unsigned char *, unsigned char *, int);
+
+/* ls-files */
+int pathspec_match(const char **spec, char *matched, const char *filename, int skiplen);
+int report_path_error(const char *ps_matched, const char **pathspec, int prefix_offset);
+void overlay_tree_on_cache(const char *tree_name, const char *prefix);
+
+#endif /* CACHE_H */
diff --git a/check-builtins.sh b/check-builtins.sh
new file mode 100755 (executable)
index 0000000..d6fe6cf
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+{
+       cat <<\EOF
+sayIt:
+       $(foreach b,$(BUILT_INS),echo XXX $b YYY;)
+EOF
+       cat Makefile
+} |
+make -f - sayIt 2>/dev/null |
+sed -n -e 's/.*XXX \(.*\) YYY.*/\1/p' |
+sort |
+{
+    bad=0
+    while read builtin
+    do
+       base=`expr "$builtin" : 'git-\(.*\)'`
+       x=`sed -ne 's/.*{ "'$base'", \(cmd_[^, ]*\).*/'$base'   \1/p' git.c`
+       if test -z "$x"
+       then
+               echo "$base is builtin but not listed in git.c command list"
+               bad=1
+       fi
+       for sfx in sh perl py
+       do
+               if test -f "$builtin.$sfx"
+               then
+                       echo "$base is builtin but $builtin.$sfx still exists"
+                       bad=1
+               fi
+       done
+    done
+    exit $bad
+}
diff --git a/check-racy.c b/check-racy.c
new file mode 100644 (file)
index 0000000..00d92a1
--- /dev/null
@@ -0,0 +1,28 @@
+#include "cache.h"
+
+int main(int ac, char **av)
+{
+       int i;
+       int dirty, clean, racy;
+
+       dirty = clean = racy = 0;
+       read_cache();
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               struct stat st;
+
+               if (lstat(ce->name, &st)) {
+                       error("lstat(%s): %s", ce->name, strerror(errno));
+                       continue;
+               }
+
+               if (ce_match_stat(ce, &st, 0))
+                       dirty++;
+               else if (ce_match_stat(ce, &st, CE_MATCH_RACY_IS_DIRTY))
+                       racy++;
+               else
+                       clean++;
+       }
+       printf("dirty %d, clean %d, racy %d\n", dirty, clean, racy);
+       return 0;
+}
diff --git a/color.c b/color.c
new file mode 100644 (file)
index 0000000..7bd424a
--- /dev/null
+++ b/color.c
@@ -0,0 +1,181 @@
+#include "cache.h"
+#include "color.h"
+
+#define COLOR_RESET "\033[m"
+
+static int parse_color(const char *name, int len)
+{
+       static const char * const color_names[] = {
+               "normal", "black", "red", "green", "yellow",
+               "blue", "magenta", "cyan", "white"
+       };
+       char *end;
+       int i;
+       for (i = 0; i < ARRAY_SIZE(color_names); i++) {
+               const char *str = color_names[i];
+               if (!strncasecmp(name, str, len) && !str[len])
+                       return i - 1;
+       }
+       i = strtol(name, &end, 10);
+       if (*name && !*end && i >= -1 && i <= 255)
+               return i;
+       return -2;
+}
+
+static int parse_attr(const char *name, int len)
+{
+       static const int attr_values[] = { 1, 2, 4, 5, 7 };
+       static const char * const attr_names[] = {
+               "bold", "dim", "ul", "blink", "reverse"
+       };
+       int i;
+       for (i = 0; i < ARRAY_SIZE(attr_names); i++) {
+               const char *str = attr_names[i];
+               if (!strncasecmp(name, str, len) && !str[len])
+                       return attr_values[i];
+       }
+       return -1;
+}
+
+void color_parse(const char *value, const char *var, char *dst)
+{
+       const char *ptr = value;
+       int attr = -1;
+       int fg = -2;
+       int bg = -2;
+
+       if (!strcasecmp(value, "reset")) {
+               strcpy(dst, "\033[m");
+               return;
+       }
+
+       /* [fg [bg]] [attr] */
+       while (*ptr) {
+               const char *word = ptr;
+               int val, len = 0;
+
+               while (word[len] && !isspace(word[len]))
+                       len++;
+
+               ptr = word + len;
+               while (*ptr && isspace(*ptr))
+                       ptr++;
+
+               val = parse_color(word, len);
+               if (val >= -1) {
+                       if (fg == -2) {
+                               fg = val;
+                               continue;
+                       }
+                       if (bg == -2) {
+                               bg = val;
+                               continue;
+                       }
+                       goto bad;
+               }
+               val = parse_attr(word, len);
+               if (val < 0 || attr != -1)
+                       goto bad;
+               attr = val;
+       }
+
+       if (attr >= 0 || fg >= 0 || bg >= 0) {
+               int sep = 0;
+
+               *dst++ = '\033';
+               *dst++ = '[';
+               if (attr >= 0) {
+                       *dst++ = '0' + attr;
+                       sep++;
+               }
+               if (fg >= 0) {
+                       if (sep++)
+                               *dst++ = ';';
+                       if (fg < 8) {
+                               *dst++ = '3';
+                               *dst++ = '0' + fg;
+                       } else {
+                               dst += sprintf(dst, "38;5;%d", fg);
+                       }
+               }
+               if (bg >= 0) {
+                       if (sep++)
+                               *dst++ = ';';
+                       if (bg < 8) {
+                               *dst++ = '4';
+                               *dst++ = '0' + bg;
+                       } else {
+                               dst += sprintf(dst, "48;5;%d", bg);
+                       }
+               }
+               *dst++ = 'm';
+       }
+       *dst = 0;
+       return;
+bad:
+       die("bad config value '%s' for variable '%s'", value, var);
+}
+
+int git_config_colorbool(const char *var, const char *value, int stdout_is_tty)
+{
+       if (value) {
+               if (!strcasecmp(value, "never"))
+                       return 0;
+               if (!strcasecmp(value, "always"))
+                       return 1;
+               if (!strcasecmp(value, "auto"))
+                       goto auto_color;
+       }
+
+       /* Missing or explicit false to turn off colorization */
+       if (!git_config_bool(var, value))
+               return 0;
+
+       /* any normal truth value defaults to 'auto' */
+ auto_color:
+       if (stdout_is_tty < 0)
+               stdout_is_tty = isatty(1);
+       if (stdout_is_tty || (pager_in_use && pager_use_color)) {
+               char *term = getenv("TERM");
+               if (term && strcmp(term, "dumb"))
+                       return 1;
+       }
+       return 0;
+}
+
+static int color_vfprintf(FILE *fp, const char *color, const char *fmt,
+               va_list args, const char *trail)
+{
+       int r = 0;
+
+       if (*color)
+               r += fprintf(fp, "%s", color);
+       r += vfprintf(fp, fmt, args);
+       if (*color)
+               r += fprintf(fp, "%s", COLOR_RESET);
+       if (trail)
+               r += fprintf(fp, "%s", trail);
+       return r;
+}
+
+
+
+int color_fprintf(FILE *fp, const char *color, const char *fmt, ...)
+{
+       va_list args;
+       int r;
+       va_start(args, fmt);
+       r = color_vfprintf(fp, color, fmt, args, NULL);
+       va_end(args);
+       return r;
+}
+
+int color_fprintf_ln(FILE *fp, const char *color, const char *fmt, ...)
+{
+       va_list args;
+       int r;
+       va_start(args, fmt);
+       r = color_vfprintf(fp, color, fmt, args, "\n");
+       va_end(args);
+       return r;
+}
diff --git a/color.h b/color.h
new file mode 100644 (file)
index 0000000..ff63513
--- /dev/null
+++ b/color.h
@@ -0,0 +1,12 @@
+#ifndef COLOR_H
+#define COLOR_H
+
+/* "\033[1;38;5;2xx;48;5;2xxm\0" is 23 bytes */
+#define COLOR_MAXLEN 24
+
+int git_config_colorbool(const char *var, const char *value, int stdout_is_tty);
+void color_parse(const char *var, const char *value, char *dst);
+int color_fprintf(FILE *fp, const char *color, const char *fmt, ...);
+int color_fprintf_ln(FILE *fp, const char *color, const char *fmt, ...);
+
+#endif /* COLOR_H */
diff --git a/combine-diff.c b/combine-diff.c
new file mode 100644 (file)
index 0000000..5a658dc
--- /dev/null
@@ -0,0 +1,1018 @@
+#include "cache.h"
+#include "commit.h"
+#include "blob.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "quote.h"
+#include "xdiff-interface.h"
+#include "log-tree.h"
+
+static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, int n, int num_parent)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       struct combine_diff_path *p;
+       int i;
+
+       if (!n) {
+               struct combine_diff_path *list = NULL, **tail = &list;
+               for (i = 0; i < q->nr; i++) {
+                       int len;
+                       const char *path;
+                       if (diff_unmodified_pair(q->queue[i]))
+                               continue;
+                       path = q->queue[i]->two->path;
+                       len = strlen(path);
+                       p = xmalloc(combine_diff_path_size(num_parent, len));
+                       p->path = (char*) &(p->parent[num_parent]);
+                       memcpy(p->path, path, len);
+                       p->path[len] = 0;
+                       p->len = len;
+                       p->next = NULL;
+                       memset(p->parent, 0,
+                              sizeof(p->parent[0]) * num_parent);
+
+                       hashcpy(p->sha1, q->queue[i]->two->sha1);
+                       p->mode = q->queue[i]->two->mode;
+                       hashcpy(p->parent[n].sha1, q->queue[i]->one->sha1);
+                       p->parent[n].mode = q->queue[i]->one->mode;
+                       p->parent[n].status = q->queue[i]->status;
+                       *tail = p;
+                       tail = &p->next;
+               }
+               return list;
+       }
+
+       for (p = curr; p; p = p->next) {
+               int found = 0;
+               if (!p->len)
+                       continue;
+               for (i = 0; i < q->nr; i++) {
+                       const char *path;
+                       int len;
+
+                       if (diff_unmodified_pair(q->queue[i]))
+                               continue;
+                       path = q->queue[i]->two->path;
+                       len = strlen(path);
+                       if (len == p->len && !memcmp(path, p->path, len)) {
+                               found = 1;
+                               hashcpy(p->parent[n].sha1, q->queue[i]->one->sha1);
+                               p->parent[n].mode = q->queue[i]->one->mode;
+                               p->parent[n].status = q->queue[i]->status;
+                               break;
+                       }
+               }
+               if (!found)
+                       p->len = 0;
+       }
+       return curr;
+}
+
+/* Lines lost from parent */
+struct lline {
+       struct lline *next;
+       int len;
+       unsigned long parent_map;
+       char line[FLEX_ARRAY];
+};
+
+/* Lines surviving in the merge result */
+struct sline {
+       struct lline *lost_head, **lost_tail;
+       char *bol;
+       int len;
+       /* bit 0 up to (N-1) are on if the parent has this line (i.e.
+        * we did not change it).
+        * bit N is used for "interesting" lines, including context.
+        */
+       unsigned long flag;
+       unsigned long *p_lno;
+};
+
+static char *grab_blob(const unsigned char *sha1, unsigned long *size)
+{
+       char *blob;
+       enum object_type type;
+       if (is_null_sha1(sha1)) {
+               /* deleted blob */
+               *size = 0;
+               return xcalloc(1, 1);
+       }
+       blob = read_sha1_file(sha1, &type, size);
+       if (type != OBJ_BLOB)
+               die("object '%s' is not a blob!", sha1_to_hex(sha1));
+       return blob;
+}
+
+static void append_lost(struct sline *sline, int n, const char *line, int len)
+{
+       struct lline *lline;
+       unsigned long this_mask = (1UL<<n);
+       if (line[len-1] == '\n')
+               len--;
+
+       /* Check to see if we can squash things */
+       if (sline->lost_head) {
+               struct lline *last_one = NULL;
+               /* We cannot squash it with earlier one */
+               for (lline = sline->lost_head;
+                    lline;
+                    lline = lline->next)
+                       if (lline->parent_map & this_mask)
+                               last_one = lline;
+               lline = last_one ? last_one->next : sline->lost_head;
+               while (lline) {
+                       if (lline->len == len &&
+                           !memcmp(lline->line, line, len)) {
+                               lline->parent_map |= this_mask;
+                               return;
+                       }
+                       lline = lline->next;
+               }
+       }
+
+       lline = xmalloc(sizeof(*lline) + len + 1);
+       lline->len = len;
+       lline->next = NULL;
+       lline->parent_map = this_mask;
+       memcpy(lline->line, line, len);
+       lline->line[len] = 0;
+       *sline->lost_tail = lline;
+       sline->lost_tail = &lline->next;
+}
+
+struct combine_diff_state {
+       struct xdiff_emit_state xm;
+
+       unsigned int lno;
+       int ob, on, nb, nn;
+       unsigned long nmask;
+       int num_parent;
+       int n;
+       struct sline *sline;
+       struct sline *lost_bucket;
+};
+
+static void consume_line(void *state_, char *line, unsigned long len)
+{
+       struct combine_diff_state *state = state_;
+       if (5 < len && !memcmp("@@ -", line, 4)) {
+               if (parse_hunk_header(line, len,
+                                     &state->ob, &state->on,
+                                     &state->nb, &state->nn))
+                       return;
+               state->lno = state->nb;
+               if (!state->nb)
+                       /* @@ -1,2 +0,0 @@ to remove the
+                        * first two lines...
+                        */
+                       state->nb = 1;
+               if (state->nn == 0)
+                       /* @@ -X,Y +N,0 @@ removed Y lines
+                        * that would have come *after* line N
+                        * in the result.  Our lost buckets hang
+                        * to the line after the removed lines,
+                        */
+                       state->lost_bucket = &state->sline[state->nb];
+               else
+                       state->lost_bucket = &state->sline[state->nb-1];
+               if (!state->sline[state->nb-1].p_lno)
+                       state->sline[state->nb-1].p_lno =
+                               xcalloc(state->num_parent,
+                                       sizeof(unsigned long));
+               state->sline[state->nb-1].p_lno[state->n] = state->ob;
+               return;
+       }
+       if (!state->lost_bucket)
+               return; /* not in any hunk yet */
+       switch (line[0]) {
+       case '-':
+               append_lost(state->lost_bucket, state->n, line+1, len-1);
+               break;
+       case '+':
+               state->sline[state->lno-1].flag |= state->nmask;
+               state->lno++;
+               break;
+       }
+}
+
+static void combine_diff(const unsigned char *parent, mmfile_t *result_file,
+                        struct sline *sline, unsigned int cnt, int n,
+                        int num_parent)
+{
+       unsigned int p_lno, lno;
+       unsigned long nmask = (1UL << n);
+       xpparam_t xpp;
+       xdemitconf_t xecfg;
+       mmfile_t parent_file;
+       xdemitcb_t ecb;
+       struct combine_diff_state state;
+       unsigned long sz;
+
+       if (!cnt)
+               return; /* result deleted */
+
+       parent_file.ptr = grab_blob(parent, &sz);
+       parent_file.size = sz;
+       xpp.flags = XDF_NEED_MINIMAL;
+       memset(&xecfg, 0, sizeof(xecfg));
+       ecb.outf = xdiff_outf;
+       ecb.priv = &state;
+       memset(&state, 0, sizeof(state));
+       state.xm.consume = consume_line;
+       state.nmask = nmask;
+       state.sline = sline;
+       state.lno = 1;
+       state.num_parent = num_parent;
+       state.n = n;
+
+       xdl_diff(&parent_file, result_file, &xpp, &xecfg, &ecb);
+       free(parent_file.ptr);
+
+       /* Assign line numbers for this parent.
+        *
+        * sline[lno].p_lno[n] records the first line number
+        * (counting from 1) for parent N if the final hunk display
+        * started by showing sline[lno] (possibly showing the lost
+        * lines attached to it first).
+        */
+       for (lno = 0,  p_lno = 1; lno <= cnt; lno++) {
+               struct lline *ll;
+               sline[lno].p_lno[n] = p_lno;
+
+               /* How many lines would this sline advance the p_lno? */
+               ll = sline[lno].lost_head;
+               while (ll) {
+                       if (ll->parent_map & nmask)
+                               p_lno++; /* '-' means parent had it */
+                       ll = ll->next;
+               }
+               if (lno < cnt && !(sline[lno].flag & nmask))
+                       p_lno++; /* no '+' means parent had it */
+       }
+       sline[lno].p_lno[n] = p_lno; /* trailer */
+}
+
+static unsigned long context = 3;
+static char combine_marker = '@';
+
+static int interesting(struct sline *sline, unsigned long all_mask)
+{
+       /* If some parents lost lines here, or if we have added to
+        * some parent, it is interesting.
+        */
+       return ((sline->flag & all_mask) || sline->lost_head);
+}
+
+static unsigned long adjust_hunk_tail(struct sline *sline,
+                                     unsigned long all_mask,
+                                     unsigned long hunk_begin,
+                                     unsigned long i)
+{
+       /* i points at the first uninteresting line.  If the last line
+        * of the hunk was interesting only because it has some
+        * deletion, then it is not all that interesting for the
+        * purpose of giving trailing context lines.  This is because
+        * we output '-' line and then unmodified sline[i-1] itself in
+        * that case which gives us one extra context line.
+        */
+       if ((hunk_begin + 1 <= i) && !(sline[i-1].flag & all_mask))
+               i--;
+       return i;
+}
+
+static unsigned long find_next(struct sline *sline,
+                              unsigned long mark,
+                              unsigned long i,
+                              unsigned long cnt,
+                              int look_for_uninteresting)
+{
+       /* We have examined up to i-1 and are about to look at i.
+        * Find next interesting or uninteresting line.  Here,
+        * "interesting" does not mean interesting(), but marked by
+        * the give_context() function below (i.e. it includes context
+        * lines that are not interesting to interesting() function
+        * that are surrounded by interesting() ones.
+        */
+       while (i <= cnt)
+               if (look_for_uninteresting
+                   ? !(sline[i].flag & mark)
+                   : (sline[i].flag & mark))
+                       return i;
+               else
+                       i++;
+       return i;
+}
+
+static int give_context(struct sline *sline, unsigned long cnt, int num_parent)
+{
+       unsigned long all_mask = (1UL<<num_parent) - 1;
+       unsigned long mark = (1UL<<num_parent);
+       unsigned long i;
+
+       /* Two groups of interesting lines may have a short gap of
+        * uninteresting lines.  Connect such groups to give them a
+        * bit of context.
+        *
+        * We first start from what the interesting() function says,
+        * and mark them with "mark", and paint context lines with the
+        * mark.  So interesting() would still say false for such context
+        * lines but they are treated as "interesting" in the end.
+        */
+       i = find_next(sline, mark, 0, cnt, 0);
+       if (cnt < i)
+               return 0;
+
+       while (i <= cnt) {
+               unsigned long j = (context < i) ? (i - context) : 0;
+               unsigned long k;
+
+               /* Paint a few lines before the first interesting line. */
+               while (j < i)
+                       sline[j++].flag |= mark;
+
+       again:
+               /* we know up to i is to be included.  where does the
+                * next uninteresting one start?
+                */
+               j = find_next(sline, mark, i, cnt, 1);
+               if (cnt < j)
+                       break; /* the rest are all interesting */
+
+               /* lookahead context lines */
+               k = find_next(sline, mark, j, cnt, 0);
+               j = adjust_hunk_tail(sline, all_mask, i, j);
+
+               if (k < j + context) {
+                       /* k is interesting and [j,k) are not, but
+                        * paint them interesting because the gap is small.
+                        */
+                       while (j < k)
+                               sline[j++].flag |= mark;
+                       i = k;
+                       goto again;
+               }
+
+               /* j is the first uninteresting line and there is
+                * no overlap beyond it within context lines.  Paint
+                * the trailing edge a bit.
+                */
+               i = k;
+               k = (j + context < cnt+1) ? j + context : cnt+1;
+               while (j < k)
+                       sline[j++].flag |= mark;
+       }
+       return 1;
+}
+
+static int make_hunks(struct sline *sline, unsigned long cnt,
+                      int num_parent, int dense)
+{
+       unsigned long all_mask = (1UL<<num_parent) - 1;
+       unsigned long mark = (1UL<<num_parent);
+       unsigned long i;
+       int has_interesting = 0;
+
+       for (i = 0; i <= cnt; i++) {
+               if (interesting(&sline[i], all_mask))
+                       sline[i].flag |= mark;
+               else
+                       sline[i].flag &= ~mark;
+       }
+       if (!dense)
+               return give_context(sline, cnt, num_parent);
+
+       /* Look at each hunk, and if we have changes from only one
+        * parent, or the changes are the same from all but one
+        * parent, mark that uninteresting.
+        */
+       i = 0;
+       while (i <= cnt) {
+               unsigned long j, hunk_begin, hunk_end;
+               unsigned long same_diff;
+               while (i <= cnt && !(sline[i].flag & mark))
+                       i++;
+               if (cnt < i)
+                       break; /* No more interesting hunks */
+               hunk_begin = i;
+               for (j = i + 1; j <= cnt; j++) {
+                       if (!(sline[j].flag & mark)) {
+                               /* Look beyond the end to see if there
+                                * is an interesting line after this
+                                * hunk within context span.
+                                */
+                               unsigned long la; /* lookahead */
+                               int contin = 0;
+                               la = adjust_hunk_tail(sline, all_mask,
+                                                    hunk_begin, j);
+                               la = (la + context < cnt + 1) ?
+                                       (la + context) : cnt + 1;
+                               while (j <= --la) {
+                                       if (sline[la].flag & mark) {
+                                               contin = 1;
+                                               break;
+                                       }
+                               }
+                               if (!contin)
+                                       break;
+                               j = la;
+                       }
+               }
+               hunk_end = j;
+
+               /* [i..hunk_end) are interesting.  Now is it really
+                * interesting?  We check if there are only two versions
+                * and the result matches one of them.  That is, we look
+                * at:
+                *   (+) line, which records lines added to which parents;
+                *       this line appears in the result.
+                *   (-) line, which records from what parents the line
+                *       was removed; this line does not appear in the result.
+                * then check the set of parents the result has difference
+                * from, from all lines.  If there are lines that has
+                * different set of parents that the result has differences
+                * from, that means we have more than two versions.
+                *
+                * Even when we have only two versions, if the result does
+                * not match any of the parents, the it should be considered
+                * interesting.  In such a case, we would have all '+' line.
+                * After passing the above "two versions" test, that would
+                * appear as "the same set of parents" to be "all parents".
+                */
+               same_diff = 0;
+               has_interesting = 0;
+               for (j = i; j < hunk_end && !has_interesting; j++) {
+                       unsigned long this_diff = sline[j].flag & all_mask;
+                       struct lline *ll = sline[j].lost_head;
+                       if (this_diff) {
+                               /* This has some changes.  Is it the
+                                * same as others?
+                                */
+                               if (!same_diff)
+                                       same_diff = this_diff;
+                               else if (same_diff != this_diff) {
+                                       has_interesting = 1;
+                                       break;
+                               }
+                       }
+                       while (ll && !has_interesting) {
+                               /* Lost this line from these parents;
+                                * who are they?  Are they the same?
+                                */
+                               this_diff = ll->parent_map;
+                               if (!same_diff)
+                                       same_diff = this_diff;
+                               else if (same_diff != this_diff) {
+                                       has_interesting = 1;
+                               }
+                               ll = ll->next;
+                       }
+               }
+
+               if (!has_interesting && same_diff != all_mask) {
+                       /* This hunk is not that interesting after all */
+                       for (j = hunk_begin; j < hunk_end; j++)
+                               sline[j].flag &= ~mark;
+               }
+               i = hunk_end;
+       }
+
+       has_interesting = give_context(sline, cnt, num_parent);
+       return has_interesting;
+}
+
+static void show_parent_lno(struct sline *sline, unsigned long l0, unsigned long l1, int n, unsigned long null_context)
+{
+       l0 = sline[l0].p_lno[n];
+       l1 = sline[l1].p_lno[n];
+       printf(" -%lu,%lu", l0, l1-l0-null_context);
+}
+
+static int hunk_comment_line(const char *bol)
+{
+       int ch;
+
+       if (!bol)
+               return 0;
+       ch = *bol & 0xff;
+       return (isalpha(ch) || ch == '_' || ch == '$');
+}
+
+static void dump_sline(struct sline *sline, unsigned long cnt, int num_parent,
+                      int use_color)
+{
+       unsigned long mark = (1UL<<num_parent);
+       int i;
+       unsigned long lno = 0;
+       const char *c_frag = diff_get_color(use_color, DIFF_FRAGINFO);
+       const char *c_new = diff_get_color(use_color, DIFF_FILE_NEW);
+       const char *c_old = diff_get_color(use_color, DIFF_FILE_OLD);
+       const char *c_plain = diff_get_color(use_color, DIFF_PLAIN);
+       const char *c_reset = diff_get_color(use_color, DIFF_RESET);
+
+       if (!cnt)
+               return; /* result deleted */
+
+       while (1) {
+               struct sline *sl = &sline[lno];
+               unsigned long hunk_end;
+               unsigned long rlines;
+               const char *hunk_comment = NULL;
+               unsigned long null_context = 0;
+
+               while (lno <= cnt && !(sline[lno].flag & mark)) {
+                       if (hunk_comment_line(sline[lno].bol))
+                               hunk_comment = sline[lno].bol;
+                       lno++;
+               }
+               if (cnt < lno)
+                       break;
+               else {
+                       for (hunk_end = lno + 1; hunk_end <= cnt; hunk_end++)
+                               if (!(sline[hunk_end].flag & mark))
+                                       break;
+               }
+               rlines = hunk_end - lno;
+               if (cnt < hunk_end)
+                       rlines--; /* pointing at the last delete hunk */
+
+               if (!context) {
+                       /*
+                        * Even when running with --unified=0, all
+                        * lines in the hunk needs to be processed in
+                        * the loop below in order to show the
+                        * deletion recorded in lost_head.  However,
+                        * we do not want to show the resulting line
+                        * with all blank context markers in such a
+                        * case.  Compensate.
+                        */
+                       unsigned long j;
+                       for (j = lno; j < hunk_end; j++)
+                               if (!(sline[j].flag & (mark-1)))
+                                       null_context++;
+                       rlines -= null_context;
+               }
+
+               fputs(c_frag, stdout);
+               for (i = 0; i <= num_parent; i++) putchar(combine_marker);
+               for (i = 0; i < num_parent; i++)
+                       show_parent_lno(sline, lno, hunk_end, i, null_context);
+               printf(" +%lu,%lu ", lno+1, rlines);
+               for (i = 0; i <= num_parent; i++) putchar(combine_marker);
+
+               if (hunk_comment) {
+                       int comment_end = 0;
+                       for (i = 0; i < 40; i++) {
+                               int ch = hunk_comment[i] & 0xff;
+                               if (!ch || ch == '\n')
+                                       break;
+                               if (!isspace(ch))
+                                   comment_end = i;
+                       }
+                       if (comment_end)
+                               putchar(' ');
+                       for (i = 0; i < comment_end; i++)
+                               putchar(hunk_comment[i]);
+               }
+
+               printf("%s\n", c_reset);
+               while (lno < hunk_end) {
+                       struct lline *ll;
+                       int j;
+                       unsigned long p_mask;
+                       sl = &sline[lno++];
+                       ll = sl->lost_head;
+                       while (ll) {
+                               fputs(c_old, stdout);
+                               for (j = 0; j < num_parent; j++) {
+                                       if (ll->parent_map & (1UL<<j))
+                                               putchar('-');
+                                       else
+                                               putchar(' ');
+                               }
+                               printf("%s%s\n", ll->line, c_reset);
+                               ll = ll->next;
+                       }
+                       if (cnt < lno)
+                               break;
+                       p_mask = 1;
+                       if (!(sl->flag & (mark-1))) {
+                               /*
+                                * This sline was here to hang the
+                                * lost lines in front of it.
+                                */
+                               if (!context)
+                                       continue;
+                               fputs(c_plain, stdout);
+                       }
+                       else
+                               fputs(c_new, stdout);
+                       for (j = 0; j < num_parent; j++) {
+                               if (p_mask & sl->flag)
+                                       putchar('+');
+                               else
+                                       putchar(' ');
+                               p_mask <<= 1;
+                       }
+                       printf("%.*s%s\n", sl->len, sl->bol, c_reset);
+               }
+       }
+}
+
+static void reuse_combine_diff(struct sline *sline, unsigned long cnt,
+                              int i, int j)
+{
+       /* We have already examined parent j and we know parent i
+        * and parent j are the same, so reuse the combined result
+        * of parent j for parent i.
+        */
+       unsigned long lno, imask, jmask;
+       imask = (1UL<<i);
+       jmask = (1UL<<j);
+
+       for (lno = 0; lno <= cnt; lno++) {
+               struct lline *ll = sline->lost_head;
+               sline->p_lno[i] = sline->p_lno[j];
+               while (ll) {
+                       if (ll->parent_map & jmask)
+                               ll->parent_map |= imask;
+                       ll = ll->next;
+               }
+               if (sline->flag & jmask)
+                       sline->flag |= imask;
+               sline++;
+       }
+       /* the overall size of the file (sline[cnt]) */
+       sline->p_lno[i] = sline->p_lno[j];
+}
+
+static void dump_quoted_path(const char *prefix, const char *path,
+                            const char *c_meta, const char *c_reset)
+{
+       printf("%s%s", c_meta, prefix);
+       quote_c_style(path, NULL, stdout, 0);
+       printf("%s\n", c_reset);
+}
+
+static void show_patch_diff(struct combine_diff_path *elem, int num_parent,
+                           int dense, struct rev_info *rev)
+{
+       struct diff_options *opt = &rev->diffopt;
+       unsigned long result_size, cnt, lno;
+       char *result, *cp;
+       struct sline *sline; /* survived lines */
+       int mode_differs = 0;
+       int i, show_hunks;
+       int working_tree_file = is_null_sha1(elem->sha1);
+       int abbrev = DIFF_OPT_TST(opt, FULL_INDEX) ? 40 : DEFAULT_ABBREV;
+       mmfile_t result_file;
+
+       context = opt->context;
+       /* Read the result of merge first */
+       if (!working_tree_file)
+               result = grab_blob(elem->sha1, &result_size);
+       else {
+               /* Used by diff-tree to read from the working tree */
+               struct stat st;
+               int fd = -1;
+
+               if (lstat(elem->path, &st) < 0)
+                       goto deleted_file;
+
+               if (S_ISLNK(st.st_mode)) {
+                       size_t len = xsize_t(st.st_size);
+                       result_size = len;
+                       result = xmalloc(len + 1);
+                       if (result_size != readlink(elem->path, result, len)) {
+                               error("readlink(%s): %s", elem->path,
+                                     strerror(errno));
+                               return;
+                       }
+                       result[len] = 0;
+                       elem->mode = canon_mode(st.st_mode);
+               }
+               else if (0 <= (fd = open(elem->path, O_RDONLY)) &&
+                        !fstat(fd, &st)) {
+                       size_t len = xsize_t(st.st_size);
+                       size_t sz = 0;
+                       int is_file, i;
+
+                       elem->mode = canon_mode(st.st_mode);
+                       /* if symlinks don't work, assume symlink if all parents
+                        * are symlinks
+                        */
+                       is_file = has_symlinks;
+                       for (i = 0; !is_file && i < num_parent; i++)
+                               is_file = !S_ISLNK(elem->parent[i].mode);
+                       if (!is_file)
+                               elem->mode = canon_mode(S_IFLNK);
+
+                       result_size = len;
+                       result = xmalloc(len + 1);
+                       while (sz < len) {
+                               ssize_t done = xread(fd, result+sz, len-sz);
+                               if (done == 0)
+                                       break;
+                               if (done < 0)
+                                       die("read error '%s'", elem->path);
+                               sz += done;
+                       }
+                       result[len] = 0;
+               }
+               else {
+               deleted_file:
+                       result_size = 0;
+                       elem->mode = 0;
+                       result = xcalloc(1, 1);
+               }
+
+               if (0 <= fd)
+                       close(fd);
+       }
+
+       for (cnt = 0, cp = result; cp < result + result_size; cp++) {
+               if (*cp == '\n')
+                       cnt++;
+       }
+       if (result_size && result[result_size-1] != '\n')
+               cnt++; /* incomplete line */
+
+       sline = xcalloc(cnt+2, sizeof(*sline));
+       sline[0].bol = result;
+       for (lno = 0; lno <= cnt + 1; lno++) {
+               sline[lno].lost_tail = &sline[lno].lost_head;
+               sline[lno].flag = 0;
+       }
+       for (lno = 0, cp = result; cp < result + result_size; cp++) {
+               if (*cp == '\n') {
+                       sline[lno].len = cp - sline[lno].bol;
+                       lno++;
+                       if (lno < cnt)
+                               sline[lno].bol = cp + 1;
+               }
+       }
+       if (result_size && result[result_size-1] != '\n')
+               sline[cnt-1].len = result_size - (sline[cnt-1].bol - result);
+
+       result_file.ptr = result;
+       result_file.size = result_size;
+
+       /* Even p_lno[cnt+1] is valid -- that is for the end line number
+        * for deletion hunk at the end.
+        */
+       sline[0].p_lno = xcalloc((cnt+2) * num_parent, sizeof(unsigned long));
+       for (lno = 0; lno <= cnt; lno++)
+               sline[lno+1].p_lno = sline[lno].p_lno + num_parent;
+
+       for (i = 0; i < num_parent; i++) {
+               int j;
+               for (j = 0; j < i; j++) {
+                       if (!hashcmp(elem->parent[i].sha1,
+                                    elem->parent[j].sha1)) {
+                               reuse_combine_diff(sline, cnt, i, j);
+                               break;
+                       }
+               }
+               if (i <= j)
+                       combine_diff(elem->parent[i].sha1, &result_file, sline,
+                                    cnt, i, num_parent);
+               if (elem->parent[i].mode != elem->mode)
+                       mode_differs = 1;
+       }
+
+       show_hunks = make_hunks(sline, cnt, num_parent, dense);
+
+       if (show_hunks || mode_differs || working_tree_file) {
+               const char *abb;
+               int use_color = DIFF_OPT_TST(opt, COLOR_DIFF);
+               const char *c_meta = diff_get_color(use_color, DIFF_METAINFO);
+               const char *c_reset = diff_get_color(use_color, DIFF_RESET);
+               int added = 0;
+               int deleted = 0;
+
+               if (rev->loginfo && !rev->no_commit_id)
+                       show_log(rev, opt->msg_sep);
+               dump_quoted_path(dense ? "diff --cc " : "diff --combined ",
+                                elem->path, c_meta, c_reset);
+               printf("%sindex ", c_meta);
+               for (i = 0; i < num_parent; i++) {
+                       abb = find_unique_abbrev(elem->parent[i].sha1,
+                                                abbrev);
+                       printf("%s%s", i ? "," : "", abb);
+               }
+               abb = find_unique_abbrev(elem->sha1, abbrev);
+               printf("..%s%s\n", abb, c_reset);
+
+               if (mode_differs) {
+                       deleted = !elem->mode;
+
+                       /* We say it was added if nobody had it */
+                       added = !deleted;
+                       for (i = 0; added && i < num_parent; i++)
+                               if (elem->parent[i].status !=
+                                   DIFF_STATUS_ADDED)
+                                       added = 0;
+                       if (added)
+                               printf("%snew file mode %06o",
+                                      c_meta, elem->mode);
+                       else {
+                               if (deleted)
+                                       printf("%sdeleted file ", c_meta);
+                               printf("mode ");
+                               for (i = 0; i < num_parent; i++) {
+                                       printf("%s%06o", i ? "," : "",
+                                              elem->parent[i].mode);
+                               }
+                               if (elem->mode)
+                                       printf("..%06o", elem->mode);
+                       }
+                       printf("%s\n", c_reset);
+               }
+               if (added)
+                       dump_quoted_path("--- /dev/", "null", c_meta, c_reset);
+               else
+                       dump_quoted_path("--- a/", elem->path, c_meta, c_reset);
+               if (deleted)
+                       dump_quoted_path("+++ /dev/", "null", c_meta, c_reset);
+               else
+                       dump_quoted_path("+++ b/", elem->path, c_meta, c_reset);
+               dump_sline(sline, cnt, num_parent, DIFF_OPT_TST(opt, COLOR_DIFF));
+       }
+       free(result);
+
+       for (lno = 0; lno < cnt; lno++) {
+               if (sline[lno].lost_head) {
+                       struct lline *ll = sline[lno].lost_head;
+                       while (ll) {
+                               struct lline *tmp = ll;
+                               ll = ll->next;
+                               free(tmp);
+                       }
+               }
+       }
+       free(sline[0].p_lno);
+       free(sline);
+}
+
+#define COLONS "::::::::::::::::::::::::::::::::"
+
+static void show_raw_diff(struct combine_diff_path *p, int num_parent, struct rev_info *rev)
+{
+       struct diff_options *opt = &rev->diffopt;
+       int i, offset;
+       const char *prefix;
+       int line_termination, inter_name_termination;
+
+       line_termination = opt->line_termination;
+       inter_name_termination = '\t';
+       if (!line_termination)
+               inter_name_termination = 0;
+
+       if (rev->loginfo && !rev->no_commit_id)
+               show_log(rev, opt->msg_sep);
+
+       if (opt->output_format & DIFF_FORMAT_RAW) {
+               offset = strlen(COLONS) - num_parent;
+               if (offset < 0)
+                       offset = 0;
+               prefix = COLONS + offset;
+
+               /* Show the modes */
+               for (i = 0; i < num_parent; i++) {
+                       printf("%s%06o", prefix, p->parent[i].mode);
+                       prefix = " ";
+               }
+               printf("%s%06o", prefix, p->mode);
+
+               /* Show sha1's */
+               for (i = 0; i < num_parent; i++)
+                       printf(" %s", diff_unique_abbrev(p->parent[i].sha1,
+                                                        opt->abbrev));
+               printf(" %s ", diff_unique_abbrev(p->sha1, opt->abbrev));
+       }
+
+       if (opt->output_format & (DIFF_FORMAT_RAW | DIFF_FORMAT_NAME_STATUS)) {
+               for (i = 0; i < num_parent; i++)
+                       putchar(p->parent[i].status);
+               putchar(inter_name_termination);
+       }
+
+       write_name_quoted(p->path, stdout, line_termination);
+}
+
+void show_combined_diff(struct combine_diff_path *p,
+                      int num_parent,
+                      int dense,
+                      struct rev_info *rev)
+{
+       struct diff_options *opt = &rev->diffopt;
+       if (!p->len)
+               return;
+       if (opt->output_format & (DIFF_FORMAT_RAW |
+                                 DIFF_FORMAT_NAME |
+                                 DIFF_FORMAT_NAME_STATUS))
+               show_raw_diff(p, num_parent, rev);
+       else if (opt->output_format & DIFF_FORMAT_PATCH)
+               show_patch_diff(p, num_parent, dense, rev);
+}
+
+void diff_tree_combined(const unsigned char *sha1,
+                       const unsigned char parent[][20],
+                       int num_parent,
+                       int dense,
+                       struct rev_info *rev)
+{
+       struct diff_options *opt = &rev->diffopt;
+       struct diff_options diffopts;
+       struct combine_diff_path *p, *paths = NULL;
+       int i, num_paths, needsep, show_log_first;
+
+       diffopts = *opt;
+       diffopts.output_format = DIFF_FORMAT_NO_OUTPUT;
+       DIFF_OPT_SET(&diffopts, RECURSIVE);
+       DIFF_OPT_CLR(&diffopts, ALLOW_EXTERNAL);
+
+       show_log_first = !!rev->loginfo && !rev->no_commit_id;
+       needsep = 0;
+       /* find set of paths that everybody touches */
+       for (i = 0; i < num_parent; i++) {
+               /* show stat against the first parent even
+                * when doing combined diff.
+                */
+               int stat_opt = (opt->output_format &
+                               (DIFF_FORMAT_NUMSTAT|DIFF_FORMAT_DIFFSTAT));
+               if (i == 0 && stat_opt)
+                       diffopts.output_format = stat_opt;
+               else
+                       diffopts.output_format = DIFF_FORMAT_NO_OUTPUT;
+               diff_tree_sha1(parent[i], sha1, "", &diffopts);
+               diffcore_std(&diffopts);
+               paths = intersect_paths(paths, i, num_parent);
+
+               if (show_log_first && i == 0) {
+                       show_log(rev, opt->msg_sep);
+                       if (rev->verbose_header && opt->output_format)
+                               putchar(opt->line_termination);
+               }
+               diff_flush(&diffopts);
+       }
+
+       /* find out surviving paths */
+       for (num_paths = 0, p = paths; p; p = p->next) {
+               if (p->len)
+                       num_paths++;
+       }
+       if (num_paths) {
+               if (opt->output_format & (DIFF_FORMAT_RAW |
+                                         DIFF_FORMAT_NAME |
+                                         DIFF_FORMAT_NAME_STATUS)) {
+                       for (p = paths; p; p = p->next) {
+                               if (p->len)
+                                       show_raw_diff(p, num_parent, rev);
+                       }
+                       needsep = 1;
+               }
+               else if (opt->output_format &
+                        (DIFF_FORMAT_NUMSTAT|DIFF_FORMAT_DIFFSTAT))
+                       needsep = 1;
+               if (opt->output_format & DIFF_FORMAT_PATCH) {
+                       if (needsep)
+                               putchar(opt->line_termination);
+                       for (p = paths; p; p = p->next) {
+                               if (p->len)
+                                       show_patch_diff(p, num_parent, dense,
+                                                       rev);
+                       }
+               }
+       }
+
+       /* Clean things up */
+       while (paths) {
+               struct combine_diff_path *tmp = paths;
+               paths = paths->next;
+               free(tmp);
+       }
+}
+
+void diff_tree_combined_merge(const unsigned char *sha1,
+                            int dense, struct rev_info *rev)
+{
+       int num_parent;
+       const unsigned char (*parent)[20];
+       struct commit *commit = lookup_commit(sha1);
+       struct commit_list *parents;
+
+       /* count parents */
+       for (parents = commit->parents, num_parent = 0;
+            parents;
+            parents = parents->next, num_parent++)
+               ; /* nothing */
+
+       parent = xmalloc(num_parent * sizeof(*parent));
+       for (parents = commit->parents, num_parent = 0;
+            parents;
+            parents = parents->next, num_parent++)
+               hashcpy((unsigned char*)(parent + num_parent),
+                       parents->item->object.sha1);
+       diff_tree_combined(sha1, parent, num_parent, dense, rev);
+}
diff --git a/command-list.txt b/command-list.txt
new file mode 100644 (file)
index 0000000..28342da
--- /dev/null
@@ -0,0 +1,130 @@
+# List of known git commands.
+# command name                         category [deprecated] [common]
+git-add                                 mainporcelain common
+git-am                                  mainporcelain
+git-annotate                            ancillaryinterrogators
+git-apply                               plumbingmanipulators
+git-archimport                          foreignscminterface
+git-archive                             mainporcelain
+git-bisect                              mainporcelain common
+git-blame                               ancillaryinterrogators
+git-branch                              mainporcelain common
+git-bundle                              mainporcelain
+git-cat-file                            plumbinginterrogators
+git-check-attr                          purehelpers
+git-checkout                            mainporcelain common
+git-checkout-index                      plumbingmanipulators
+git-check-ref-format                    purehelpers
+git-cherry                              ancillaryinterrogators
+git-cherry-pick                         mainporcelain
+git-citool                              mainporcelain
+git-clean                               mainporcelain
+git-clone                               mainporcelain common
+git-commit                              mainporcelain common
+git-commit-tree                         plumbingmanipulators
+git-config                              ancillarymanipulators
+git-count-objects                       ancillaryinterrogators
+git-cvsexportcommit                     foreignscminterface
+git-cvsimport                           foreignscminterface
+git-cvsserver                           foreignscminterface
+git-daemon                              synchingrepositories
+git-describe                            mainporcelain
+git-diff                                mainporcelain common
+git-diff-files                          plumbinginterrogators
+git-diff-index                          plumbinginterrogators
+git-diff-tree                           plumbinginterrogators
+git-fast-export                                ancillarymanipulators
+git-fast-import                                ancillarymanipulators
+git-fetch                               mainporcelain common
+git-fetch-pack                          synchingrepositories
+git-filter-branch                       ancillarymanipulators
+git-fmt-merge-msg                       purehelpers
+git-for-each-ref                        plumbinginterrogators
+git-format-patch                        mainporcelain
+git-fsck                               ancillaryinterrogators
+git-gc                                  mainporcelain
+git-get-tar-commit-id                   ancillaryinterrogators
+git-grep                                mainporcelain common
+git-gui                                 mainporcelain
+git-hash-object                         plumbingmanipulators
+git-help                               ancillaryinterrogators
+git-http-fetch                          synchelpers
+git-http-push                           synchelpers
+git-imap-send                           foreignscminterface
+git-index-pack                          plumbingmanipulators
+git-init                                mainporcelain common
+git-instaweb                            ancillaryinterrogators
+gitk                                    mainporcelain
+git-log                                 mainporcelain common
+git-lost-found                          ancillarymanipulators  deprecated
+git-ls-files                            plumbinginterrogators
+git-ls-remote                           plumbinginterrogators
+git-ls-tree                             plumbinginterrogators
+git-mailinfo                            purehelpers
+git-mailsplit                           purehelpers
+git-merge                               mainporcelain common
+git-merge-base                          plumbinginterrogators
+git-merge-file                          plumbingmanipulators
+git-merge-index                         plumbingmanipulators
+git-merge-one-file                      purehelpers
+git-mergetool                           ancillarymanipulators
+git-merge-tree                          ancillaryinterrogators
+git-mktag                               plumbingmanipulators
+git-mktree                              plumbingmanipulators
+git-mv                                  mainporcelain common
+git-name-rev                            plumbinginterrogators
+git-pack-objects                        plumbingmanipulators
+git-pack-redundant                      plumbinginterrogators
+git-pack-refs                           ancillarymanipulators
+git-parse-remote                        synchelpers
+git-patch-id                            purehelpers
+git-peek-remote                         purehelpers    deprecated
+git-prune                               ancillarymanipulators
+git-prune-packed                        plumbingmanipulators
+git-pull                                mainporcelain common
+git-push                                mainporcelain common
+git-quiltimport                         foreignscminterface
+git-read-tree                           plumbingmanipulators
+git-rebase                              mainporcelain common
+git-receive-pack                        synchelpers
+git-reflog                              ancillarymanipulators
+git-relink                              ancillarymanipulators
+git-remote                              ancillarymanipulators
+git-repack                              ancillarymanipulators
+git-request-pull                        foreignscminterface
+git-rerere                              ancillaryinterrogators
+git-reset                               mainporcelain common
+git-revert                              mainporcelain
+git-rev-list                            plumbinginterrogators
+git-rev-parse                           ancillaryinterrogators
+git-rm                                  mainporcelain common
+git-runstatus                           ancillaryinterrogators
+git-send-email                          foreignscminterface
+git-send-pack                           synchingrepositories
+git-shell                               synchelpers
+git-shortlog                            mainporcelain
+git-show                                mainporcelain common
+git-show-branch                         ancillaryinterrogators
+git-show-index                          plumbinginterrogators
+git-show-ref                            plumbinginterrogators
+git-sh-setup                            purehelpers
+git-stash                               mainporcelain
+git-status                              mainporcelain common
+git-stripspace                          purehelpers
+git-submodule                           mainporcelain
+git-svn                                 foreignscminterface
+git-symbolic-ref                        plumbingmanipulators
+git-tag                                 mainporcelain common
+git-tar-tree                            plumbinginterrogators  deprecated
+git-unpack-file                         plumbinginterrogators
+git-unpack-objects                      plumbingmanipulators
+git-update-index                        plumbingmanipulators
+git-update-ref                          plumbingmanipulators
+git-update-server-info                  synchingrepositories
+git-upload-archive                      synchelpers
+git-upload-pack                         synchelpers
+git-var                                 plumbinginterrogators
+git-verify-pack                         plumbinginterrogators
+git-verify-tag                          ancillaryinterrogators
+git-whatchanged                         ancillaryinterrogators
+git-write-tree                          plumbingmanipulators
diff --git a/commit.c b/commit.c
new file mode 100644 (file)
index 0000000..f074811
--- /dev/null
+++ b/commit.c
@@ -0,0 +1,672 @@
+#include "cache.h"
+#include "tag.h"
+#include "commit.h"
+#include "pkt-line.h"
+#include "utf8.h"
+#include "diff.h"
+#include "revision.h"
+
+int save_commit_buffer = 1;
+
+const char *commit_type = "commit";
+
+static struct commit *check_commit(struct object *obj,
+                                  const unsigned char *sha1,
+                                  int quiet)
+{
+       if (obj->type != OBJ_COMMIT) {
+               if (!quiet)
+                       error("Object %s is a %s, not a commit",
+                             sha1_to_hex(sha1), typename(obj->type));
+               return NULL;
+       }
+       return (struct commit *) obj;
+}
+
+struct commit *lookup_commit_reference_gently(const unsigned char *sha1,
+                                             int quiet)
+{
+       struct object *obj = deref_tag(parse_object(sha1), NULL, 0);
+
+       if (!obj)
+               return NULL;
+       return check_commit(obj, sha1, quiet);
+}
+
+struct commit *lookup_commit_reference(const unsigned char *sha1)
+{
+       return lookup_commit_reference_gently(sha1, 0);
+}
+
+struct commit *lookup_commit(const unsigned char *sha1)
+{
+       struct object *obj = lookup_object(sha1);
+       if (!obj)
+               return create_object(sha1, OBJ_COMMIT, alloc_commit_node());
+       if (!obj->type)
+               obj->type = OBJ_COMMIT;
+       return check_commit(obj, sha1, 0);
+}
+
+static unsigned long parse_commit_date(const char *buf)
+{
+       unsigned long date;
+
+       if (memcmp(buf, "author", 6))
+               return 0;
+       while (*buf++ != '\n')
+               /* nada */;
+       if (memcmp(buf, "committer", 9))
+               return 0;
+       while (*buf++ != '>')
+               /* nada */;
+       date = strtoul(buf, NULL, 10);
+       if (date == ULONG_MAX)
+               date = 0;
+       return date;
+}
+
+static struct commit_graft **commit_graft;
+static int commit_graft_alloc, commit_graft_nr;
+
+static int commit_graft_pos(const unsigned char *sha1)
+{
+       int lo, hi;
+       lo = 0;
+       hi = commit_graft_nr;
+       while (lo < hi) {
+               int mi = (lo + hi) / 2;
+               struct commit_graft *graft = commit_graft[mi];
+               int cmp = hashcmp(sha1, graft->sha1);
+               if (!cmp)
+                       return mi;
+               if (cmp < 0)
+                       hi = mi;
+               else
+                       lo = mi + 1;
+       }
+       return -lo - 1;
+}
+
+int register_commit_graft(struct commit_graft *graft, int ignore_dups)
+{
+       int pos = commit_graft_pos(graft->sha1);
+
+       if (0 <= pos) {
+               if (ignore_dups)
+                       free(graft);
+               else {
+                       free(commit_graft[pos]);
+                       commit_graft[pos] = graft;
+               }
+               return 1;
+       }
+       pos = -pos - 1;
+       if (commit_graft_alloc <= ++commit_graft_nr) {
+               commit_graft_alloc = alloc_nr(commit_graft_alloc);
+               commit_graft = xrealloc(commit_graft,
+                                       sizeof(*commit_graft) *
+                                       commit_graft_alloc);
+       }
+       if (pos < commit_graft_nr)
+               memmove(commit_graft + pos + 1,
+                       commit_graft + pos,
+                       (commit_graft_nr - pos - 1) *
+                       sizeof(*commit_graft));
+       commit_graft[pos] = graft;
+       return 0;
+}
+
+struct commit_graft *read_graft_line(char *buf, int len)
+{
+       /* The format is just "Commit Parent1 Parent2 ...\n" */
+       int i;
+       struct commit_graft *graft = NULL;
+
+       if (buf[len-1] == '\n')
+               buf[--len] = 0;
+       if (buf[0] == '#' || buf[0] == '\0')
+               return NULL;
+       if ((len + 1) % 41) {
+       bad_graft_data:
+               error("bad graft data: %s", buf);
+               free(graft);
+               return NULL;
+       }
+       i = (len + 1) / 41 - 1;
+       graft = xmalloc(sizeof(*graft) + 20 * i);
+       graft->nr_parent = i;
+       if (get_sha1_hex(buf, graft->sha1))
+               goto bad_graft_data;
+       for (i = 40; i < len; i += 41) {
+               if (buf[i] != ' ')
+                       goto bad_graft_data;
+               if (get_sha1_hex(buf + i + 1, graft->parent[i/41]))
+                       goto bad_graft_data;
+       }
+       return graft;
+}
+
+int read_graft_file(const char *graft_file)
+{
+       FILE *fp = fopen(graft_file, "r");
+       char buf[1024];
+       if (!fp)
+               return -1;
+       while (fgets(buf, sizeof(buf), fp)) {
+               /* The format is just "Commit Parent1 Parent2 ...\n" */
+               int len = strlen(buf);
+               struct commit_graft *graft = read_graft_line(buf, len);
+               if (!graft)
+                       continue;
+               if (register_commit_graft(graft, 1))
+                       error("duplicate graft data: %s", buf);
+       }
+       fclose(fp);
+       return 0;
+}
+
+static void prepare_commit_graft(void)
+{
+       static int commit_graft_prepared;
+       char *graft_file;
+
+       if (commit_graft_prepared)
+               return;
+       graft_file = get_graft_file();
+       read_graft_file(graft_file);
+       /* make sure shallows are read */
+       is_repository_shallow();
+       commit_graft_prepared = 1;
+}
+
+static struct commit_graft *lookup_commit_graft(const unsigned char *sha1)
+{
+       int pos;
+       prepare_commit_graft();
+       pos = commit_graft_pos(sha1);
+       if (pos < 0)
+               return NULL;
+       return commit_graft[pos];
+}
+
+int write_shallow_commits(int fd, int use_pack_protocol)
+{
+       int i, count = 0;
+       for (i = 0; i < commit_graft_nr; i++)
+               if (commit_graft[i]->nr_parent < 0) {
+                       const char *hex =
+                               sha1_to_hex(commit_graft[i]->sha1);
+                       count++;
+                       if (use_pack_protocol)
+                               packet_write(fd, "shallow %s", hex);
+                       else {
+                               if (write_in_full(fd, hex,  40) != 40)
+                                       break;
+                               if (write_in_full(fd, "\n", 1) != 1)
+                                       break;
+                       }
+               }
+       return count;
+}
+
+int unregister_shallow(const unsigned char *sha1)
+{
+       int pos = commit_graft_pos(sha1);
+       if (pos < 0)
+               return -1;
+       if (pos + 1 < commit_graft_nr)
+               memcpy(commit_graft + pos, commit_graft + pos + 1,
+                               sizeof(struct commit_graft *)
+                               * (commit_graft_nr - pos - 1));
+       commit_graft_nr--;
+       return 0;
+}
+
+int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size)
+{
+       char *tail = buffer;
+       char *bufptr = buffer;
+       unsigned char parent[20];
+       struct commit_list **pptr;
+       struct commit_graft *graft;
+       unsigned n_refs = 0;
+
+       if (item->object.parsed)
+               return 0;
+       item->object.parsed = 1;
+       tail += size;
+       if (tail <= bufptr + 5 || memcmp(bufptr, "tree ", 5))
+               return error("bogus commit object %s", sha1_to_hex(item->object.sha1));
+       if (tail <= bufptr + 45 || get_sha1_hex(bufptr + 5, parent) < 0)
+               return error("bad tree pointer in commit %s",
+                            sha1_to_hex(item->object.sha1));
+       item->tree = lookup_tree(parent);
+       if (item->tree)
+               n_refs++;
+       bufptr += 46; /* "tree " + "hex sha1" + "\n" */
+       pptr = &item->parents;
+
+       graft = lookup_commit_graft(item->object.sha1);
+       while (bufptr + 48 < tail && !memcmp(bufptr, "parent ", 7)) {
+               struct commit *new_parent;
+
+               if (tail <= bufptr + 48 ||
+                   get_sha1_hex(bufptr + 7, parent) ||
+                   bufptr[47] != '\n')
+                       return error("bad parents in commit %s", sha1_to_hex(item->object.sha1));
+               bufptr += 48;
+               if (graft)
+                       continue;
+               new_parent = lookup_commit(parent);
+               if (new_parent) {
+                       pptr = &commit_list_insert(new_parent, pptr)->next;
+                       n_refs++;
+               }
+       }
+       if (graft) {
+               int i;
+               struct commit *new_parent;
+               for (i = 0; i < graft->nr_parent; i++) {
+                       new_parent = lookup_commit(graft->parent[i]);
+                       if (!new_parent)
+                               continue;
+                       pptr = &commit_list_insert(new_parent, pptr)->next;
+                       n_refs++;
+               }
+       }
+       item->date = parse_commit_date(bufptr);
+
+       if (track_object_refs) {
+               unsigned i = 0;
+               struct commit_list *p;
+               struct object_refs *refs = alloc_object_refs(n_refs);
+               if (item->tree)
+                       refs->ref[i++] = &item->tree->object;
+               for (p = item->parents; p; p = p->next)
+                       refs->ref[i++] = &p->item->object;
+               set_object_refs(&item->object, refs);
+       }
+
+       return 0;
+}
+
+int parse_commit(struct commit *item)
+{
+       enum object_type type;
+       void *buffer;
+       unsigned long size;
+       int ret;
+
+       if (item->object.parsed)
+               return 0;
+       buffer = read_sha1_file(item->object.sha1, &type, &size);
+       if (!buffer)
+               return error("Could not read %s",
+                            sha1_to_hex(item->object.sha1));
+       if (type != OBJ_COMMIT) {
+               free(buffer);
+               return error("Object %s not a commit",
+                            sha1_to_hex(item->object.sha1));
+       }
+       ret = parse_commit_buffer(item, buffer, size);
+       if (save_commit_buffer && !ret) {
+               item->buffer = buffer;
+               return 0;
+       }
+       free(buffer);
+       return ret;
+}
+
+struct commit_list *commit_list_insert(struct commit *item, struct commit_list **list_p)
+{
+       struct commit_list *new_list = xmalloc(sizeof(struct commit_list));
+       new_list->item = item;
+       new_list->next = *list_p;
+       *list_p = new_list;
+       return new_list;
+}
+
+void free_commit_list(struct commit_list *list)
+{
+       while (list) {
+               struct commit_list *temp = list;
+               list = temp->next;
+               free(temp);
+       }
+}
+
+struct commit_list * insert_by_date(struct commit *item, struct commit_list **list)
+{
+       struct commit_list **pp = list;
+       struct commit_list *p;
+       while ((p = *pp) != NULL) {
+               if (p->item->date < item->date) {
+                       break;
+               }
+               pp = &p->next;
+       }
+       return commit_list_insert(item, pp);
+}
+
+
+void sort_by_date(struct commit_list **list)
+{
+       struct commit_list *ret = NULL;
+       while (*list) {
+               insert_by_date((*list)->item, &ret);
+               *list = (*list)->next;
+       }
+       *list = ret;
+}
+
+struct commit *pop_most_recent_commit(struct commit_list **list,
+                                     unsigned int mark)
+{
+       struct commit *ret = (*list)->item;
+       struct commit_list *parents = ret->parents;
+       struct commit_list *old = *list;
+
+       *list = (*list)->next;
+       free(old);
+
+       while (parents) {
+               struct commit *commit = parents->item;
+               parse_commit(commit);
+               if (!(commit->object.flags & mark)) {
+                       commit->object.flags |= mark;
+                       insert_by_date(commit, list);
+               }
+               parents = parents->next;
+       }
+       return ret;
+}
+
+void clear_commit_marks(struct commit *commit, unsigned int mark)
+{
+       while (commit) {
+               struct commit_list *parents;
+
+               if (!(mark & commit->object.flags))
+                       return;
+
+               commit->object.flags &= ~mark;
+
+               parents = commit->parents;
+               if (!parents)
+                       return;
+
+               while ((parents = parents->next))
+                       clear_commit_marks(parents->item, mark);
+
+               commit = commit->parents->item;
+       }
+}
+
+struct commit *pop_commit(struct commit_list **stack)
+{
+       struct commit_list *top = *stack;
+       struct commit *item = top ? top->item : NULL;
+
+       if (top) {
+               *stack = top->next;
+               free(top);
+       }
+       return item;
+}
+
+/*
+ * Performs an in-place topological sort on the list supplied.
+ */
+void sort_in_topological_order(struct commit_list ** list, int lifo)
+{
+       struct commit_list *next, *orig = *list;
+       struct commit_list *work, **insert;
+       struct commit_list **pptr;
+
+       if (!orig)
+               return;
+       *list = NULL;
+
+       /* Mark them and clear the indegree */
+       for (next = orig; next; next = next->next) {
+               struct commit *commit = next->item;
+               commit->object.flags |= TOPOSORT;
+               commit->indegree = 0;
+       }
+
+       /* update the indegree */
+       for (next = orig; next; next = next->next) {
+               struct commit_list * parents = next->item->parents;
+               while (parents) {
+                       struct commit *parent = parents->item;
+
+                       if (parent->object.flags & TOPOSORT)
+                               parent->indegree++;
+                       parents = parents->next;
+               }
+       }
+
+       /*
+        * find the tips
+        *
+        * tips are nodes not reachable from any other node in the list
+        *
+        * the tips serve as a starting set for the work queue.
+        */
+       work = NULL;
+       insert = &work;
+       for (next = orig; next; next = next->next) {
+               struct commit *commit = next->item;
+
+               if (!commit->indegree)
+                       insert = &commit_list_insert(commit, insert)->next;
+       }
+
+       /* process the list in topological order */
+       if (!lifo)
+               sort_by_date(&work);
+
+       pptr = list;
+       *list = NULL;
+       while (work) {
+               struct commit *commit;
+               struct commit_list *parents, *work_item;
+
+               work_item = work;
+               work = work_item->next;
+               work_item->next = NULL;
+
+               commit = work_item->item;
+               for (parents = commit->parents; parents ; parents = parents->next) {
+                       struct commit *parent=parents->item;
+
+                       if (!(parent->object.flags & TOPOSORT))
+                               continue;
+
+                       /*
+                        * parents are only enqueued for emission
+                        * when all their children have been emitted thereby
+                        * guaranteeing topological order.
+                        */
+                       if (!--parent->indegree) {
+                               if (!lifo)
+                                       insert_by_date(parent, &work);
+                               else
+                                       commit_list_insert(parent, &work);
+                       }
+               }
+               /*
+                * work_item is a commit all of whose children
+                * have already been emitted. we can emit it now.
+                */
+               commit->object.flags &= ~TOPOSORT;
+               *pptr = work_item;
+               pptr = &work_item->next;
+       }
+}
+
+/* merge-base stuff */
+
+/* bits #0..15 in revision.h */
+#define PARENT1                (1u<<16)
+#define PARENT2                (1u<<17)
+#define STALE          (1u<<18)
+#define RESULT         (1u<<19)
+
+static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT);
+
+static struct commit *interesting(struct commit_list *list)
+{
+       while (list) {
+               struct commit *commit = list->item;
+               list = list->next;
+               if (commit->object.flags & STALE)
+                       continue;
+               return commit;
+       }
+       return NULL;
+}
+
+static struct commit_list *merge_bases(struct commit *one, struct commit *two)
+{
+       struct commit_list *list = NULL;
+       struct commit_list *result = NULL;
+
+       if (one == two)
+               /* We do not mark this even with RESULT so we do not
+                * have to clean it up.
+                */
+               return commit_list_insert(one, &result);
+
+       parse_commit(one);
+       parse_commit(two);
+
+       one->object.flags |= PARENT1;
+       two->object.flags |= PARENT2;
+       insert_by_date(one, &list);
+       insert_by_date(two, &list);
+
+       while (interesting(list)) {
+               struct commit *commit;
+               struct commit_list *parents;
+               struct commit_list *n;
+               int flags;
+
+               commit = list->item;
+               n = list->next;
+               free(list);
+               list = n;
+
+               flags = commit->object.flags & (PARENT1 | PARENT2 | STALE);
+               if (flags == (PARENT1 | PARENT2)) {
+                       if (!(commit->object.flags & RESULT)) {
+                               commit->object.flags |= RESULT;
+                               insert_by_date(commit, &result);
+                       }
+                       /* Mark parents of a found merge stale */
+                       flags |= STALE;
+               }
+               parents = commit->parents;
+               while (parents) {
+                       struct commit *p = parents->item;
+                       parents = parents->next;
+                       if ((p->object.flags & flags) == flags)
+                               continue;
+                       parse_commit(p);
+                       p->object.flags |= flags;
+                       insert_by_date(p, &list);
+               }
+       }
+
+       /* Clean up the result to remove stale ones */
+       free_commit_list(list);
+       list = result; result = NULL;
+       while (list) {
+               struct commit_list *n = list->next;
+               if (!(list->item->object.flags & STALE))
+                       insert_by_date(list->item, &result);
+               free(list);
+               list = n;
+       }
+       return result;
+}
+
+struct commit_list *get_merge_bases(struct commit *one,
+                                       struct commit *two, int cleanup)
+{
+       struct commit_list *list;
+       struct commit **rslt;
+       struct commit_list *result;
+       int cnt, i, j;
+
+       result = merge_bases(one, two);
+       if (one == two)
+               return result;
+       if (!result || !result->next) {
+               if (cleanup) {
+                       clear_commit_marks(one, all_flags);
+                       clear_commit_marks(two, all_flags);
+               }
+               return result;
+       }
+
+       /* There are more than one */
+       cnt = 0;
+       list = result;
+       while (list) {
+               list = list->next;
+               cnt++;
+       }
+       rslt = xcalloc(cnt, sizeof(*rslt));
+       for (list = result, i = 0; list; list = list->next)
+               rslt[i++] = list->item;
+       free_commit_list(result);
+
+       clear_commit_marks(one, all_flags);
+       clear_commit_marks(two, all_flags);
+       for (i = 0; i < cnt - 1; i++) {
+               for (j = i+1; j < cnt; j++) {
+                       if (!rslt[i] || !rslt[j])
+                               continue;
+                       result = merge_bases(rslt[i], rslt[j]);
+                       clear_commit_marks(rslt[i], all_flags);
+                       clear_commit_marks(rslt[j], all_flags);
+                       for (list = result; list; list = list->next) {
+                               if (rslt[i] == list->item)
+                                       rslt[i] = NULL;
+                               if (rslt[j] == list->item)
+                                       rslt[j] = NULL;
+                       }
+               }
+       }
+
+       /* Surviving ones in rslt[] are the independent results */
+       result = NULL;
+       for (i = 0; i < cnt; i++) {
+               if (rslt[i])
+                       insert_by_date(rslt[i], &result);
+       }
+       free(rslt);
+       return result;
+}
+
+int in_merge_bases(struct commit *commit, struct commit **reference, int num)
+{
+       struct commit_list *bases, *b;
+       int ret = 0;
+
+       if (num == 1)
+               bases = get_merge_bases(commit, *reference, 1);
+       else
+               die("not yet");
+       for (b = bases; b; b = b->next) {
+               if (!hashcmp(commit->object.sha1, b->item->object.sha1)) {
+                       ret = 1;
+                       break;
+               }
+       }
+
+       free_commit_list(bases);
+       return ret;
+}
diff --git a/commit.h b/commit.h
new file mode 100644 (file)
index 0000000..10e2b5d
--- /dev/null
+++ b/commit.h
@@ -0,0 +1,124 @@
+#ifndef COMMIT_H
+#define COMMIT_H
+
+#include "object.h"
+#include "tree.h"
+#include "strbuf.h"
+#include "decorate.h"
+
+struct commit_list {
+       struct commit *item;
+       struct commit_list *next;
+};
+
+struct commit {
+       struct object object;
+       void *util;
+       unsigned int indegree;
+       unsigned long date;
+       struct commit_list *parents;
+       struct tree *tree;
+       char *buffer;
+};
+
+extern int save_commit_buffer;
+extern const char *commit_type;
+
+/* While we can decorate any object with a name, it's only used for commits.. */
+extern struct decoration name_decoration;
+struct name_decoration {
+       struct name_decoration *next;
+       char name[1];
+};
+
+struct commit *lookup_commit(const unsigned char *sha1);
+struct commit *lookup_commit_reference(const unsigned char *sha1);
+struct commit *lookup_commit_reference_gently(const unsigned char *sha1,
+                                             int quiet);
+
+int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size);
+
+int parse_commit(struct commit *item);
+
+struct commit_list * commit_list_insert(struct commit *item, struct commit_list **list_p);
+struct commit_list * insert_by_date(struct commit *item, struct commit_list **list);
+
+void free_commit_list(struct commit_list *list);
+
+void sort_by_date(struct commit_list **list);
+
+/* Commit formats */
+enum cmit_fmt {
+       CMIT_FMT_RAW,
+       CMIT_FMT_MEDIUM,
+       CMIT_FMT_DEFAULT = CMIT_FMT_MEDIUM,
+       CMIT_FMT_SHORT,
+       CMIT_FMT_FULL,
+       CMIT_FMT_FULLER,
+       CMIT_FMT_ONELINE,
+       CMIT_FMT_EMAIL,
+       CMIT_FMT_USERFORMAT,
+
+       CMIT_FMT_UNSPECIFIED,
+};
+
+extern int non_ascii(int);
+extern enum cmit_fmt get_commit_format(const char *arg);
+extern void format_commit_message(const struct commit *commit,
+                                  const void *format, struct strbuf *sb);
+extern void pretty_print_commit(enum cmit_fmt fmt, const struct commit*,
+                                struct strbuf *,
+                                int abbrev, const char *subject,
+                                const char *after_subject, enum date_mode,
+                               int non_ascii_present);
+
+/** Removes the first commit from a list sorted by date, and adds all
+ * of its parents.
+ **/
+struct commit *pop_most_recent_commit(struct commit_list **list,
+                                     unsigned int mark);
+
+struct commit *pop_commit(struct commit_list **stack);
+
+void clear_commit_marks(struct commit *commit, unsigned int mark);
+
+/*
+ * Performs an in-place topological sort of list supplied.
+ *
+ *   invariant of resulting list is:
+ *      a reachable from b => ord(b) < ord(a)
+ *   in addition, when lifo == 0, commits on parallel tracks are
+ *   sorted in the dates order.
+ */
+void sort_in_topological_order(struct commit_list ** list, int lifo);
+
+struct commit_graft {
+       unsigned char sha1[20];
+       int nr_parent; /* < 0 if shallow commit */
+       unsigned char parent[FLEX_ARRAY][20]; /* more */
+};
+
+struct commit_graft *read_graft_line(char *buf, int len);
+int register_commit_graft(struct commit_graft *, int);
+int read_graft_file(const char *graft_file);
+
+extern struct commit_list *get_merge_bases(struct commit *rev1, struct commit *rev2, int cleanup);
+
+extern int register_shallow(const unsigned char *sha1);
+extern int unregister_shallow(const unsigned char *sha1);
+extern int write_shallow_commits(int fd, int use_pack_protocol);
+extern int is_repository_shallow(void);
+extern struct commit_list *get_shallow_commits(struct object_array *heads,
+               int depth, int shallow_flag, int not_shallow_flag);
+
+int in_merge_bases(struct commit *, struct commit **, int);
+
+extern int interactive_add(int argc, const char **argv, const char *prefix);
+extern int rerere(void);
+
+static inline int single_parent(struct commit *commit)
+{
+       return commit->parents && !commit->parents->next;
+}
+
+#endif /* COMMIT_H */
diff --git a/compat/hstrerror.c b/compat/hstrerror.c
new file mode 100644 (file)
index 0000000..069c555
--- /dev/null
@@ -0,0 +1,21 @@
+#include <string.h>
+#include <stdio.h>
+#include <netdb.h>
+
+const char *githstrerror(int err)
+{
+       static char buffer[48];
+       switch (err)
+       {
+       case HOST_NOT_FOUND:
+               return "Authoritative answer: host not found";
+       case NO_DATA:
+               return "Valid name, no data record of requested type";
+       case NO_RECOVERY:
+               return "Non recoverable errors, FORMERR, REFUSED, NOTIMP";
+       case TRY_AGAIN:
+               return "Non-authoritative \"host not found\", or SERVERFAIL";
+       }
+       sprintf(buffer, "Name resolution error %d", err);
+       return buffer;
+}
diff --git a/compat/inet_ntop.c b/compat/inet_ntop.c
new file mode 100644 (file)
index 0000000..f444982
--- /dev/null
@@ -0,0 +1,199 @@
+/*
+ * Copyright (c) 1996-1999 by Internet Software Consortium.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS
+ * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
+ * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+ * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+ * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+ * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+ * SOFTWARE.
+ */
+
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <string.h>
+
+#ifndef NS_INADDRSZ
+#define NS_INADDRSZ    4
+#endif
+#ifndef NS_IN6ADDRSZ
+#define NS_IN6ADDRSZ   16
+#endif
+#ifndef NS_INT16SZ
+#define NS_INT16SZ     2
+#endif
+
+/*
+ * WARNING: Don't even consider trying to compile this on a system where
+ * sizeof(int) < 4.  sizeof(int) > 4 is fine; all the world's not a VAX.
+ */
+
+/* const char *
+ * inet_ntop4(src, dst, size)
+ *     format an IPv4 address
+ * return:
+ *     `dst' (as a const)
+ * notes:
+ *     (1) uses no statics
+ *     (2) takes a u_char* not an in_addr as input
+ * author:
+ *     Paul Vixie, 1996.
+ */
+static const char *
+inet_ntop4(src, dst, size)
+       const u_char *src;
+       char *dst;
+       size_t size;
+{
+       static const char fmt[] = "%u.%u.%u.%u";
+       char tmp[sizeof "255.255.255.255"];
+       int nprinted;
+
+       nprinted = snprintf(tmp, sizeof(tmp), fmt, src[0], src[1], src[2], src[3]);
+       if (nprinted < 0)
+               return (NULL);  /* we assume "errno" was set by "snprintf()" */
+       if ((size_t)nprinted > size) {
+               errno = ENOSPC;
+               return (NULL);
+       }
+       strcpy(dst, tmp);
+       return (dst);
+}
+
+#ifndef NO_IPV6
+/* const char *
+ * inet_ntop6(src, dst, size)
+ *     convert IPv6 binary address into presentation (printable) format
+ * author:
+ *     Paul Vixie, 1996.
+ */
+static const char *
+inet_ntop6(src, dst, size)
+       const u_char *src;
+       char *dst;
+       size_t size;
+{
+       /*
+        * Note that int32_t and int16_t need only be "at least" large enough
+        * to contain a value of the specified size.  On some systems, like
+        * Crays, there is no such thing as an integer variable with 16 bits.
+        * Keep this in mind if you think this function should have been coded
+        * to use pointer overlays.  All the world's not a VAX.
+        */
+       char tmp[sizeof "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255"], *tp;
+       struct { int base, len; } best, cur;
+       unsigned int words[NS_IN6ADDRSZ / NS_INT16SZ];
+       int i;
+
+       /*
+        * Preprocess:
+        *      Copy the input (bytewise) array into a wordwise array.
+        *      Find the longest run of 0x00's in src[] for :: shorthanding.
+        */
+       memset(words, '\0', sizeof words);
+       for (i = 0; i < NS_IN6ADDRSZ; i++)
+               words[i / 2] |= (src[i] << ((1 - (i % 2)) << 3));
+       best.base = -1;
+       cur.base = -1;
+       for (i = 0; i < (NS_IN6ADDRSZ / NS_INT16SZ); i++) {
+               if (words[i] == 0) {
+                       if (cur.base == -1)
+                               cur.base = i, cur.len = 1;
+                       else
+                               cur.len++;
+               } else {
+                       if (cur.base != -1) {
+                               if (best.base == -1 || cur.len > best.len)
+                                       best = cur;
+                               cur.base = -1;
+                       }
+               }
+       }
+       if (cur.base != -1) {
+               if (best.base == -1 || cur.len > best.len)
+                       best = cur;
+       }
+       if (best.base != -1 && best.len < 2)
+               best.base = -1;
+
+       /*
+        * Format the result.
+        */
+       tp = tmp;
+       for (i = 0; i < (NS_IN6ADDRSZ / NS_INT16SZ); i++) {
+               /* Are we inside the best run of 0x00's? */
+               if (best.base != -1 && i >= best.base &&
+                   i < (best.base + best.len)) {
+                       if (i == best.base)
+                               *tp++ = ':';
+                       continue;
+               }
+               /* Are we following an initial run of 0x00s or any real hex? */
+               if (i != 0)
+                       *tp++ = ':';
+               /* Is this address an encapsulated IPv4? */
+               if (i == 6 && best.base == 0 &&
+                   (best.len == 6 || (best.len == 5 && words[5] == 0xffff))) {
+                       if (!inet_ntop4(src+12, tp, sizeof tmp - (tp - tmp)))
+                               return (NULL);
+                       tp += strlen(tp);
+                       break;
+               }
+               tp += snprintf(tp, sizeof tmp - (tp - tmp), "%x", words[i]);
+       }
+       /* Was it a trailing run of 0x00's? */
+       if (best.base != -1 && (best.base + best.len) ==
+           (NS_IN6ADDRSZ / NS_INT16SZ))
+               *tp++ = ':';
+       *tp++ = '\0';
+
+       /*
+        * Check for overflow, copy, and we're done.
+        */
+       if ((size_t)(tp - tmp) > size) {
+               errno = ENOSPC;
+               return (NULL);
+       }
+       strcpy(dst, tmp);
+       return (dst);
+}
+#endif
+
+/* char *
+ * inet_ntop(af, src, dst, size)
+ *     convert a network format address to presentation format.
+ * return:
+ *     pointer to presentation format address (`dst'), or NULL (see errno).
+ * author:
+ *     Paul Vixie, 1996.
+ */
+const char *
+inet_ntop(af, src, dst, size)
+       int af;
+       const void *src;
+       char *dst;
+       size_t size;
+{
+       switch (af) {
+       case AF_INET:
+               return (inet_ntop4(src, dst, size));
+#ifndef NO_IPV6
+       case AF_INET6:
+               return (inet_ntop6(src, dst, size));
+#endif
+       default:
+               errno = EAFNOSUPPORT;
+               return (NULL);
+       }
+       /* NOTREACHED */
+}
diff --git a/compat/inet_pton.c b/compat/inet_pton.c
new file mode 100644 (file)
index 0000000..4078fc0
--- /dev/null
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 1996-2001  Internet Software Consortium.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM
+ * DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+ * INTERNET SOFTWARE CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+ * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+ * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <string.h>
+
+#ifndef NS_INT16SZ
+#define NS_INT16SZ       2
+#endif
+
+#ifndef NS_INADDRSZ
+#define NS_INADDRSZ      4
+#endif
+
+#ifndef NS_IN6ADDRSZ
+#define NS_IN6ADDRSZ    16
+#endif
+
+/*
+ * WARNING: Don't even consider trying to compile this on a system where
+ * sizeof(int) < 4.  sizeof(int) > 4 is fine; all the world's not a VAX.
+ */
+
+static int inet_pton4(const char *src, unsigned char *dst);
+static int inet_pton6(const char *src, unsigned char *dst);
+
+/* int
+ * inet_pton4(src, dst)
+ *      like inet_aton() but without all the hexadecimal and shorthand.
+ * return:
+ *      1 if `src' is a valid dotted quad, else 0.
+ * notice:
+ *      does not touch `dst' unless it's returning 1.
+ * author:
+ *      Paul Vixie, 1996.
+ */
+static int
+inet_pton4(const char *src, unsigned char *dst)
+{
+        static const char digits[] = "0123456789";
+        int saw_digit, octets, ch;
+        unsigned char tmp[NS_INADDRSZ], *tp;
+
+        saw_digit = 0;
+        octets = 0;
+        *(tp = tmp) = 0;
+        while ((ch = *src++) != '\0') {
+                const char *pch;
+
+                if ((pch = strchr(digits, ch)) != NULL) {
+                        unsigned int new = *tp * 10 + (pch - digits);
+
+                        if (new > 255)
+                                return (0);
+                        *tp = new;
+                        if (! saw_digit) {
+                                if (++octets > 4)
+                                        return (0);
+                                saw_digit = 1;
+                        }
+                } else if (ch == '.' && saw_digit) {
+                        if (octets == 4)
+                                return (0);
+                        *++tp = 0;
+                        saw_digit = 0;
+                } else
+                        return (0);
+        }
+        if (octets < 4)
+                return (0);
+        memcpy(dst, tmp, NS_INADDRSZ);
+        return (1);
+}
+
+/* int
+ * inet_pton6(src, dst)
+ *      convert presentation level address to network order binary form.
+ * return:
+ *      1 if `src' is a valid [RFC1884 2.2] address, else 0.
+ * notice:
+ *      (1) does not touch `dst' unless it's returning 1.
+ *      (2) :: in a full address is silently ignored.
+ * credit:
+ *      inspired by Mark Andrews.
+ * author:
+ *      Paul Vixie, 1996.
+ */
+
+#ifndef NO_IPV6
+static int
+inet_pton6(const char *src, unsigned char *dst)
+{
+        static const char xdigits_l[] = "0123456789abcdef",
+                          xdigits_u[] = "0123456789ABCDEF";
+        unsigned char tmp[NS_IN6ADDRSZ], *tp, *endp, *colonp;
+        const char *xdigits, *curtok;
+        int ch, saw_xdigit;
+        unsigned int val;
+
+        memset((tp = tmp), '\0', NS_IN6ADDRSZ);
+        endp = tp + NS_IN6ADDRSZ;
+        colonp = NULL;
+        /* Leading :: requires some special handling. */
+        if (*src == ':')
+                if (*++src != ':')
+                        return (0);
+        curtok = src;
+        saw_xdigit = 0;
+        val = 0;
+        while ((ch = *src++) != '\0') {
+                const char *pch;
+
+                if ((pch = strchr((xdigits = xdigits_l), ch)) == NULL)
+                        pch = strchr((xdigits = xdigits_u), ch);
+                if (pch != NULL) {
+                        val <<= 4;
+                        val |= (pch - xdigits);
+                        if (val > 0xffff)
+                                return (0);
+                        saw_xdigit = 1;
+                        continue;
+                }
+                if (ch == ':') {
+                        curtok = src;
+                        if (!saw_xdigit) {
+                                if (colonp)
+                                        return (0);
+                                colonp = tp;
+                                continue;
+                        }
+                        if (tp + NS_INT16SZ > endp)
+                                return (0);
+                        *tp++ = (unsigned char) (val >> 8) & 0xff;
+                        *tp++ = (unsigned char) val & 0xff;
+                        saw_xdigit = 0;
+                        val = 0;
+                        continue;
+                }
+                if (ch == '.' && ((tp + NS_INADDRSZ) <= endp) &&
+                    inet_pton4(curtok, tp) > 0) {
+                        tp += NS_INADDRSZ;
+                        saw_xdigit = 0;
+                        break;  /* '\0' was seen by inet_pton4(). */
+                }
+                return (0);
+        }
+        if (saw_xdigit) {
+                if (tp + NS_INT16SZ > endp)
+                        return (0);
+                *tp++ = (unsigned char) (val >> 8) & 0xff;
+                *tp++ = (unsigned char) val & 0xff;
+        }
+        if (colonp != NULL) {
+                /*
+                 * Since some memmove()'s erroneously fail to handle
+                 * overlapping regions, we'll do the shift by hand.
+                 */
+                const int n = tp - colonp;
+                int i;
+
+                for (i = 1; i <= n; i++) {
+                        endp[- i] = colonp[n - i];
+                        colonp[n - i] = 0;
+                }
+                tp = endp;
+        }
+        if (tp != endp)
+                return (0);
+        memcpy(dst, tmp, NS_IN6ADDRSZ);
+        return (1);
+}
+#endif
+
+/* int
+ * isc_net_pton(af, src, dst)
+ *      convert from presentation format (which usually means ASCII printable)
+ *      to network format (which is usually some kind of binary format).
+ * return:
+ *      1 if the address was valid for the specified address family
+ *      0 if the address wasn't valid (`dst' is untouched in this case)
+ *      -1 if some other error occurred (`dst' is untouched in this case, too)
+ * author:
+ *      Paul Vixie, 1996.
+ */
+int
+inet_pton(int af, const char *src, void *dst)
+{
+        switch (af) {
+        case AF_INET:
+                return (inet_pton4(src, dst));
+#ifndef NO_IPV6
+        case AF_INET6:
+                return (inet_pton6(src, dst));
+#endif
+        default:
+                errno = EAFNOSUPPORT;
+                return (-1);
+        }
+        /* NOTREACHED */
+}
diff --git a/compat/memmem.c b/compat/memmem.c
new file mode 100644 (file)
index 0000000..cd0d877
--- /dev/null
@@ -0,0 +1,29 @@
+#include "../git-compat-util.h"
+
+void *gitmemmem(const void *haystack, size_t haystack_len,
+                const void *needle, size_t needle_len)
+{
+       const char *begin = haystack;
+       const char *last_possible = begin + haystack_len - needle_len;
+
+       /*
+        * The first occurrence of the empty string is deemed to occur at
+        * the beginning of the string.
+        */
+       if (needle_len == 0)
+               return (void *)begin;
+
+       /*
+        * Sanity check, otherwise the loop might search through the whole
+        * memory.
+        */
+       if (haystack_len < needle_len)
+               return NULL;
+
+       for (; begin <= last_possible; begin++) {
+               if (!memcmp(begin, needle, needle_len))
+                       return (void *)begin;
+       }
+
+       return NULL;
+}
diff --git a/compat/mkdtemp.c b/compat/mkdtemp.c
new file mode 100644 (file)
index 0000000..34d4b49
--- /dev/null
@@ -0,0 +1,8 @@
+#include "../git-compat-util.h"
+
+char *gitmkdtemp(char *template)
+{
+       if (!mktemp(template) || mkdir(template, 0700))
+               return NULL;
+       return template;
+}
diff --git a/compat/mmap.c b/compat/mmap.c
new file mode 100644 (file)
index 0000000..c9d46d1
--- /dev/null
@@ -0,0 +1,42 @@
+#include "../git-compat-util.h"
+
+void *git_mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
+{
+       size_t n = 0;
+
+       if (start != NULL || !(flags & MAP_PRIVATE))
+               die("Invalid usage of mmap when built with NO_MMAP");
+
+       start = xmalloc(length);
+       if (start == NULL) {
+               errno = ENOMEM;
+               return MAP_FAILED;
+       }
+
+       while (n < length) {
+               ssize_t count = pread(fd, (char *)start + n, length - n, offset + n);
+
+               if (count == 0) {
+                       memset((char *)start+n, 0, length-n);
+                       break;
+               }
+
+               if (count < 0) {
+                       if (errno == EAGAIN || errno == EINTR)
+                               continue;
+                       free(start);
+                       errno = EACCES;
+                       return MAP_FAILED;
+               }
+
+               n += count;
+       }
+
+       return start;
+}
+
+int git_munmap(void *start, size_t length)
+{
+       free(start);
+       return 0;
+}
diff --git a/compat/pread.c b/compat/pread.c
new file mode 100644 (file)
index 0000000..978cac4
--- /dev/null
@@ -0,0 +1,18 @@
+#include "../git-compat-util.h"
+
+ssize_t git_pread(int fd, void *buf, size_t count, off_t offset)
+{
+        off_t current_offset;
+        ssize_t rc;
+
+        current_offset = lseek(fd, 0, SEEK_CUR);
+
+        if (lseek(fd, offset, SEEK_SET) < 0)
+                return -1;
+
+        rc = read_in_full(fd, buf, count);
+
+        if (current_offset != lseek(fd, current_offset, SEEK_SET))
+                return -1;
+        return rc;
+}
diff --git a/compat/setenv.c b/compat/setenv.c
new file mode 100644 (file)
index 0000000..3a22ea7
--- /dev/null
@@ -0,0 +1,34 @@
+#include "../git-compat-util.h"
+
+int gitsetenv(const char *name, const char *value, int replace)
+{
+       int out;
+       size_t namelen, valuelen;
+       char *envstr;
+
+       if (!name || !value) return -1;
+       if (!replace) {
+               char *oldval = NULL;
+               oldval = getenv(name);
+               if (oldval) return 0;
+       }
+
+       namelen = strlen(name);
+       valuelen = strlen(value);
+       envstr = malloc((namelen + valuelen + 2));
+       if (!envstr) return -1;
+
+       memcpy(envstr, name, namelen);
+       envstr[namelen] = '=';
+       memcpy(envstr + namelen + 1, value, valuelen);
+       envstr[namelen + valuelen + 1] = 0;
+
+       out = putenv(envstr);
+       /* putenv(3) makes the argument string part of the environment,
+        * and changing that string modifies the environment --- which
+        * means we do not own that storage anymore.  Do not free
+        * envstr.
+        */
+
+       return out;
+}
diff --git a/compat/strcasestr.c b/compat/strcasestr.c
new file mode 100644 (file)
index 0000000..26896de
--- /dev/null
@@ -0,0 +1,22 @@
+#include "../git-compat-util.h"
+
+char *gitstrcasestr(const char *haystack, const char *needle)
+{
+       int nlen = strlen(needle);
+       int hlen = strlen(haystack) - nlen + 1;
+       int i;
+
+       for (i = 0; i < hlen; i++) {
+               int j;
+               for (j = 0; j < nlen; j++) {
+                       unsigned char c1 = haystack[i+j];
+                       unsigned char c2 = needle[j];
+                       if (toupper(c1) != toupper(c2))
+                               goto next;
+               }
+               return (char *) haystack + i;
+       next:
+               ;
+       }
+       return NULL;
+}
diff --git a/compat/strlcpy.c b/compat/strlcpy.c
new file mode 100644 (file)
index 0000000..4024c36
--- /dev/null
@@ -0,0 +1,13 @@
+#include "../git-compat-util.h"
+
+size_t gitstrlcpy(char *dest, const char *src, size_t size)
+{
+       size_t ret = strlen(src);
+
+       if (size) {
+               size_t len = (ret >= size) ? size - 1 : ret;
+               memcpy(dest, src, len);
+               dest[len] = '\0';
+       }
+       return ret;
+}
diff --git a/compat/strtoumax.c b/compat/strtoumax.c
new file mode 100644 (file)
index 0000000..5541353
--- /dev/null
@@ -0,0 +1,10 @@
+#include "../git-compat-util.h"
+
+uintmax_t gitstrtoumax (const char *nptr, char **endptr, int base)
+{
+#if defined(NO_STRTOULL)
+       return strtoul(nptr, endptr, base);
+#else
+       return strtoull(nptr, endptr, base);
+#endif
+}
diff --git a/compat/unsetenv.c b/compat/unsetenv.c
new file mode 100644 (file)
index 0000000..eb29f5e
--- /dev/null
@@ -0,0 +1,25 @@
+#include "../git-compat-util.h"
+
+void gitunsetenv (const char *name)
+{
+     extern char **environ;
+     int src, dst;
+     size_t nmln;
+
+     nmln = strlen(name);
+
+     for (src = dst = 0; environ[src]; ++src) {
+         size_t enln;
+         enln = strlen(environ[src]);
+         if (enln > nmln) {
+               /* might match, and can test for '=' safely */
+              if (0 == strncmp (environ[src], name, nmln)
+                  && '=' == environ[src][nmln])
+                   /* matches, so skip */
+                   continue;
+         }
+         environ[dst] = environ[src];
+         ++dst;
+     }
+     environ[dst] = NULL;
+}
diff --git a/config.c b/config.c
new file mode 100644 (file)
index 0000000..ed96213
--- /dev/null
+++ b/config.c
@@ -0,0 +1,1069 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ * Copyright (C) Johannes Schindelin, 2005
+ *
+ */
+#include "cache.h"
+#include "exec_cmd.h"
+
+#define MAXNAME (256)
+
+static FILE *config_file;
+static const char *config_file_name;
+static int config_linenr;
+static int zlib_compression_seen;
+
+static int get_next_char(void)
+{
+       int c;
+       FILE *f;
+
+       c = '\n';
+       if ((f = config_file) != NULL) {
+               c = fgetc(f);
+               if (c == '\r') {
+                       /* DOS like systems */
+                       c = fgetc(f);
+                       if (c != '\n') {
+                               ungetc(c, f);
+                               c = '\r';
+                       }
+               }
+               if (c == '\n')
+                       config_linenr++;
+               if (c == EOF) {
+                       config_file = NULL;
+                       c = '\n';
+               }
+       }
+       return c;
+}
+
+static char *parse_value(void)
+{
+       static char value[1024];
+       int quote = 0, comment = 0, len = 0, space = 0;
+
+       for (;;) {
+               int c = get_next_char();
+               if (len >= sizeof(value))
+                       return NULL;
+               if (c == '\n') {
+                       if (quote)
+                               return NULL;
+                       value[len] = 0;
+                       return value;
+               }
+               if (comment)
+                       continue;
+               if (isspace(c) && !quote) {
+                       space = 1;
+                       continue;
+               }
+               if (!quote) {
+                       if (c == ';' || c == '#') {
+                               comment = 1;
+                               continue;
+                       }
+               }
+               if (space) {
+                       if (len)
+                               value[len++] = ' ';
+                       space = 0;
+               }
+               if (c == '\\') {
+                       c = get_next_char();
+                       switch (c) {
+                       case '\n':
+                               continue;
+                       case 't':
+                               c = '\t';
+                               break;
+                       case 'b':
+                               c = '\b';
+                               break;
+                       case 'n':
+                               c = '\n';
+                               break;
+                       /* Some characters escape as themselves */
+                       case '\\': case '"':
+                               break;
+                       /* Reject unknown escape sequences */
+                       default:
+                               return NULL;
+                       }
+                       value[len++] = c;
+                       continue;
+               }
+               if (c == '"') {
+                       quote = 1-quote;
+                       continue;
+               }
+               value[len++] = c;
+       }
+}
+
+static inline int iskeychar(int c)
+{
+       return isalnum(c) || c == '-';
+}
+
+static int get_value(config_fn_t fn, char *name, unsigned int len)
+{
+       int c;
+       char *value;
+
+       /* Get the full name */
+       for (;;) {
+               c = get_next_char();
+               if (c == EOF)
+                       break;
+               if (!iskeychar(c))
+                       break;
+               name[len++] = tolower(c);
+               if (len >= MAXNAME)
+                       return -1;
+       }
+       name[len] = 0;
+       while (c == ' ' || c == '\t')
+               c = get_next_char();
+
+       value = NULL;
+       if (c != '\n') {
+               if (c != '=')
+                       return -1;
+               value = parse_value();
+               if (!value)
+                       return -1;
+       }
+       return fn(name, value);
+}
+
+static int get_extended_base_var(char *name, int baselen, int c)
+{
+       do {
+               if (c == '\n')
+                       return -1;
+               c = get_next_char();
+       } while (isspace(c));
+
+       /* We require the format to be '[base "extension"]' */
+       if (c != '"')
+               return -1;
+       name[baselen++] = '.';
+
+       for (;;) {
+               int c = get_next_char();
+               if (c == '\n')
+                       return -1;
+               if (c == '"')
+                       break;
+               if (c == '\\') {
+                       c = get_next_char();
+                       if (c == '\n')
+                               return -1;
+               }
+               name[baselen++] = c;
+               if (baselen > MAXNAME / 2)
+                       return -1;
+       }
+
+       /* Final ']' */
+       if (get_next_char() != ']')
+               return -1;
+       return baselen;
+}
+
+static int get_base_var(char *name)
+{
+       int baselen = 0;
+
+       for (;;) {
+               int c = get_next_char();
+               if (c == EOF)
+                       return -1;
+               if (c == ']')
+                       return baselen;
+               if (isspace(c))
+                       return get_extended_base_var(name, baselen, c);
+               if (!iskeychar(c) && c != '.')
+                       return -1;
+               if (baselen > MAXNAME / 2)
+                       return -1;
+               name[baselen++] = tolower(c);
+       }
+}
+
+static int git_parse_file(config_fn_t fn)
+{
+       int comment = 0;
+       int baselen = 0;
+       static char var[MAXNAME];
+
+       for (;;) {
+               int c = get_next_char();
+               if (c == '\n') {
+                       /* EOF? */
+                       if (!config_file)
+                               return 0;
+                       comment = 0;
+                       continue;
+               }
+               if (comment || isspace(c))
+                       continue;
+               if (c == '#' || c == ';') {
+                       comment = 1;
+                       continue;
+               }
+               if (c == '[') {
+                       baselen = get_base_var(var);
+                       if (baselen <= 0)
+                               break;
+                       var[baselen++] = '.';
+                       var[baselen] = 0;
+                       continue;
+               }
+               if (!isalpha(c))
+                       break;
+               var[baselen] = tolower(c);
+               if (get_value(fn, var, baselen+1) < 0)
+                       break;
+       }
+       die("bad config file line %d in %s", config_linenr, config_file_name);
+}
+
+static unsigned long get_unit_factor(const char *end)
+{
+       if (!*end)
+               return 1;
+       else if (!strcasecmp(end, "k"))
+               return 1024;
+       else if (!strcasecmp(end, "m"))
+               return 1024 * 1024;
+       else if (!strcasecmp(end, "g"))
+               return 1024 * 1024 * 1024;
+       die("unknown unit: '%s'", end);
+}
+
+int git_parse_long(const char *value, long *ret)
+{
+       if (value && *value) {
+               char *end;
+               long val = strtol(value, &end, 0);
+               *ret = val * get_unit_factor(end);
+               return 1;
+       }
+       return 0;
+}
+
+int git_parse_ulong(const char *value, unsigned long *ret)
+{
+       if (value && *value) {
+               char *end;
+               unsigned long val = strtoul(value, &end, 0);
+               *ret = val * get_unit_factor(end);
+               return 1;
+       }
+       return 0;
+}
+
+int git_config_int(const char *name, const char *value)
+{
+       long ret;
+       if (!git_parse_long(value, &ret))
+               die("bad config value for '%s' in %s", name, config_file_name);
+       return ret;
+}
+
+unsigned long git_config_ulong(const char *name, const char *value)
+{
+       unsigned long ret;
+       if (!git_parse_ulong(value, &ret))
+               die("bad config value for '%s' in %s", name, config_file_name);
+       return ret;
+}
+
+int git_config_bool(const char *name, const char *value)
+{
+       if (!value)
+               return 1;
+       if (!*value)
+               return 0;
+       if (!strcasecmp(value, "true") || !strcasecmp(value, "yes"))
+               return 1;
+       if (!strcasecmp(value, "false") || !strcasecmp(value, "no"))
+               return 0;
+       return git_config_int(name, value) != 0;
+}
+
+int git_default_config(const char *var, const char *value)
+{
+       /* This needs a better name */
+       if (!strcmp(var, "core.filemode")) {
+               trust_executable_bit = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.quotepath")) {
+               quote_path_fully = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.symlinks")) {
+               has_symlinks = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.bare")) {
+               is_bare_repository_cfg = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.ignorestat")) {
+               assume_unchanged = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.prefersymlinkrefs")) {
+               prefer_symlink_refs = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.logallrefupdates")) {
+               log_all_ref_updates = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.warnambiguousrefs")) {
+               warn_ambiguous_refs = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.loosecompression")) {
+               int level = git_config_int(var, value);
+               if (level == -1)
+                       level = Z_DEFAULT_COMPRESSION;
+               else if (level < 0 || level > Z_BEST_COMPRESSION)
+                       die("bad zlib compression level %d", level);
+               zlib_compression_level = level;
+               zlib_compression_seen = 1;
+               return 0;
+       }
+
+       if (!strcmp(var, "core.compression")) {
+               int level = git_config_int(var, value);
+               if (level == -1)
+                       level = Z_DEFAULT_COMPRESSION;
+               else if (level < 0 || level > Z_BEST_COMPRESSION)
+                       die("bad zlib compression level %d", level);
+               core_compression_level = level;
+               core_compression_seen = 1;
+               if (!zlib_compression_seen)
+                       zlib_compression_level = level;
+               return 0;
+       }
+
+       if (!strcmp(var, "core.packedgitwindowsize")) {
+               int pgsz_x2 = getpagesize() * 2;
+               packed_git_window_size = git_config_int(var, value);
+
+               /* This value must be multiple of (pagesize * 2) */
+               packed_git_window_size /= pgsz_x2;
+               if (packed_git_window_size < 1)
+                       packed_git_window_size = 1;
+               packed_git_window_size *= pgsz_x2;
+               return 0;
+       }
+
+       if (!strcmp(var, "core.packedgitlimit")) {
+               packed_git_limit = git_config_int(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.deltabasecachelimit")) {
+               delta_base_cache_limit = git_config_int(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.autocrlf")) {
+               if (value && !strcasecmp(value, "input")) {
+                       auto_crlf = -1;
+                       return 0;
+               }
+               auto_crlf = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (!strcmp(var, "user.name")) {
+               strlcpy(git_default_name, value, sizeof(git_default_name));
+               return 0;
+       }
+
+       if (!strcmp(var, "user.email")) {
+               strlcpy(git_default_email, value, sizeof(git_default_email));
+               return 0;
+       }
+
+       if (!strcmp(var, "i18n.commitencoding")) {
+               git_commit_encoding = xstrdup(value);
+               return 0;
+       }
+
+       if (!strcmp(var, "i18n.logoutputencoding")) {
+               git_log_output_encoding = xstrdup(value);
+               return 0;
+       }
+
+
+       if (!strcmp(var, "pager.color") || !strcmp(var, "color.pager")) {
+               pager_use_color = git_config_bool(var,value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.pager")) {
+               pager_program = xstrdup(value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.editor")) {
+               editor_program = xstrdup(value);
+               return 0;
+       }
+
+       if (!strcmp(var, "core.excludesfile")) {
+               if (!value)
+                       die("core.excludesfile without value");
+               excludes_file = xstrdup(value);
+               return 0;
+       }
+
+       /* Add other config variables here and to Documentation/config.txt. */
+       return 0;
+}
+
+int git_config_from_file(config_fn_t fn, const char *filename)
+{
+       int ret;
+       FILE *f = fopen(filename, "r");
+
+       ret = -1;
+       if (f) {
+               config_file = f;
+               config_file_name = filename;
+               config_linenr = 1;
+               ret = git_parse_file(fn);
+               fclose(f);
+               config_file_name = NULL;
+       }
+       return ret;
+}
+
+const char *git_etc_gitconfig(void)
+{
+       static const char *system_wide;
+       if (!system_wide) {
+               system_wide = ETC_GITCONFIG;
+               if (!is_absolute_path(system_wide)) {
+                       /* interpret path relative to exec-dir */
+                       const char *exec_path = git_exec_path();
+                       system_wide = prefix_path(exec_path, strlen(exec_path),
+                                               system_wide);
+               }
+       }
+       return system_wide;
+}
+
+int git_config(config_fn_t fn)
+{
+       int ret = 0;
+       char *repo_config = NULL;
+       const char *home = NULL, *filename;
+
+       /* $GIT_CONFIG makes git read _only_ the given config file,
+        * $GIT_CONFIG_LOCAL will make it process it in addition to the
+        * global config file, the same way it would the per-repository
+        * config file otherwise. */
+       filename = getenv(CONFIG_ENVIRONMENT);
+       if (!filename) {
+               if (!access(git_etc_gitconfig(), R_OK))
+                       ret += git_config_from_file(fn, git_etc_gitconfig());
+               home = getenv("HOME");
+               filename = getenv(CONFIG_LOCAL_ENVIRONMENT);
+               if (!filename)
+                       filename = repo_config = xstrdup(git_path("config"));
+       }
+
+       if (home) {
+               char *user_config = xstrdup(mkpath("%s/.gitconfig", home));
+               if (!access(user_config, R_OK))
+                       ret = git_config_from_file(fn, user_config);
+               free(user_config);
+       }
+
+       ret += git_config_from_file(fn, filename);
+       free(repo_config);
+       return ret;
+}
+
+/*
+ * Find all the stuff for git_config_set() below.
+ */
+
+#define MAX_MATCHES 512
+
+static struct {
+       int baselen;
+       char* key;
+       int do_not_match;
+       regex_t* value_regex;
+       int multi_replace;
+       size_t offset[MAX_MATCHES];
+       enum { START, SECTION_SEEN, SECTION_END_SEEN, KEY_SEEN } state;
+       int seen;
+} store;
+
+static int matches(const char* key, const char* value)
+{
+       return !strcmp(key, store.key) &&
+               (store.value_regex == NULL ||
+                (store.do_not_match ^
+                 !regexec(store.value_regex, value, 0, NULL, 0)));
+}
+
+static int store_aux(const char* key, const char* value)
+{
+       const char *ep;
+       size_t section_len;
+
+       switch (store.state) {
+       case KEY_SEEN:
+               if (matches(key, value)) {
+                       if (store.seen == 1 && store.multi_replace == 0) {
+                               fprintf(stderr,
+                                       "Warning: %s has multiple values\n",
+                                       key);
+                       } else if (store.seen >= MAX_MATCHES) {
+                               fprintf(stderr, "Too many matches\n");
+                               return 1;
+                       }
+
+                       store.offset[store.seen] = ftell(config_file);
+                       store.seen++;
+               }
+               break;
+       case SECTION_SEEN:
+               /*
+                * What we are looking for is in store.key (both
+                * section and var), and its section part is baselen
+                * long.  We found key (again, both section and var).
+                * We would want to know if this key is in the same
+                * section as what we are looking for.  We already
+                * know we are in the same section as what should
+                * hold store.key.
+                */
+               ep = strrchr(key, '.');
+               section_len = ep - key;
+
+               if ((section_len != store.baselen) ||
+                   memcmp(key, store.key, section_len+1)) {
+                       store.state = SECTION_END_SEEN;
+                       break;
+               }
+
+               /*
+                * Do not increment matches: this is no match, but we
+                * just made sure we are in the desired section.
+                */
+               store.offset[store.seen] = ftell(config_file);
+               /* fallthru */
+       case SECTION_END_SEEN:
+       case START:
+               if (matches(key, value)) {
+                       store.offset[store.seen] = ftell(config_file);
+                       store.state = KEY_SEEN;
+                       store.seen++;
+               } else {
+                       if (strrchr(key, '.') - key == store.baselen &&
+                             !strncmp(key, store.key, store.baselen)) {
+                                       store.state = SECTION_SEEN;
+                                       store.offset[store.seen] = ftell(config_file);
+                       }
+               }
+       }
+       return 0;
+}
+
+static int write_error(void)
+{
+       fprintf(stderr, "Failed to write new configuration file\n");
+
+       /* Same error code as "failed to rename". */
+       return 4;
+}
+
+static int store_write_section(int fd, const char* key)
+{
+       const char *dot = strchr(key, '.');
+       int len1 = store.baselen, len2 = -1;
+
+       dot = strchr(key, '.');
+       if (dot) {
+               int dotlen = dot - key;
+               if (dotlen < len1) {
+                       len2 = len1 - dotlen - 1;
+                       len1 = dotlen;
+               }
+       }
+
+       if (write_in_full(fd, "[", 1) != 1 ||
+           write_in_full(fd, key, len1) != len1)
+               return 0;
+       if (len2 >= 0) {
+               if (write_in_full(fd, " \"", 2) != 2)
+                       return 0;
+               while (--len2 >= 0) {
+                       unsigned char c = *++dot;
+                       if (c == '"')
+                               if (write_in_full(fd, "\\", 1) != 1)
+                                       return 0;
+                       if (write_in_full(fd, &c, 1) != 1)
+                               return 0;
+               }
+               if (write_in_full(fd, "\"", 1) != 1)
+                       return 0;
+       }
+       if (write_in_full(fd, "]\n", 2) != 2)
+               return 0;
+
+       return 1;
+}
+
+static int store_write_pair(int fd, const char* key, const char* value)
+{
+       int i;
+       int length = strlen(key+store.baselen+1);
+       int quote = 0;
+
+       /* Check to see if the value needs to be quoted. */
+       if (value[0] == ' ')
+               quote = 1;
+       for (i = 0; value[i]; i++)
+               if (value[i] == ';' || value[i] == '#')
+                       quote = 1;
+       if (value[i-1] == ' ')
+               quote = 1;
+
+       if (write_in_full(fd, "\t", 1) != 1 ||
+           write_in_full(fd, key+store.baselen+1, length) != length ||
+           write_in_full(fd, " = ", 3) != 3)
+               return 0;
+       if (quote && write_in_full(fd, "\"", 1) != 1)
+               return 0;
+       for (i = 0; value[i]; i++)
+               switch (value[i]) {
+               case '\n':
+                       if (write_in_full(fd, "\\n", 2) != 2)
+                               return 0;
+                       break;
+               case '\t':
+                       if (write_in_full(fd, "\\t", 2) != 2)
+                               return 0;
+                       break;
+               case '"':
+               case '\\':
+                       if (write_in_full(fd, "\\", 1) != 1)
+                               return 0;
+               default:
+                       if (write_in_full(fd, value+i, 1) != 1)
+                               return 0;
+                       break;
+               }
+       if (quote && write_in_full(fd, "\"", 1) != 1)
+               return 0;
+       if (write_in_full(fd, "\n", 1) != 1)
+               return 0;
+       return 1;
+}
+
+static ssize_t find_beginning_of_line(const char* contents, size_t size,
+       size_t offset_, int* found_bracket)
+{
+       size_t equal_offset = size, bracket_offset = size;
+       ssize_t offset;
+
+       for (offset = offset_-2; offset > 0
+                       && contents[offset] != '\n'; offset--)
+               switch (contents[offset]) {
+                       case '=': equal_offset = offset; break;
+                       case ']': bracket_offset = offset; break;
+               }
+       if (bracket_offset < equal_offset) {
+               *found_bracket = 1;
+               offset = bracket_offset+1;
+       } else
+               offset++;
+
+       return offset;
+}
+
+int git_config_set(const char* key, const char* value)
+{
+       return git_config_set_multivar(key, value, NULL, 0);
+}
+
+/*
+ * If value==NULL, unset in (remove from) config,
+ * if value_regex!=NULL, disregard key/value pairs where value does not match.
+ * if multi_replace==0, nothing, or only one matching key/value is replaced,
+ *     else all matching key/values (regardless how many) are removed,
+ *     before the new pair is written.
+ *
+ * Returns 0 on success.
+ *
+ * This function does this:
+ *
+ * - it locks the config file by creating ".git/config.lock"
+ *
+ * - it then parses the config using store_aux() as validator to find
+ *   the position on the key/value pair to replace. If it is to be unset,
+ *   it must be found exactly once.
+ *
+ * - the config file is mmap()ed and the part before the match (if any) is
+ *   written to the lock file, then the changed part and the rest.
+ *
+ * - the config file is removed and the lock file rename()d to it.
+ *
+ */
+int git_config_set_multivar(const char* key, const char* value,
+       const char* value_regex, int multi_replace)
+{
+       int i, dot;
+       int fd = -1, in_fd;
+       int ret;
+       char* config_filename;
+       struct lock_file *lock = NULL;
+       const char* last_dot = strrchr(key, '.');
+
+       config_filename = getenv(CONFIG_ENVIRONMENT);
+       if (!config_filename) {
+               config_filename = getenv(CONFIG_LOCAL_ENVIRONMENT);
+               if (!config_filename)
+                       config_filename  = git_path("config");
+       }
+       config_filename = xstrdup(config_filename);
+
+       /*
+        * Since "key" actually contains the section name and the real
+        * key name separated by a dot, we have to know where the dot is.
+        */
+
+       if (last_dot == NULL) {
+               fprintf(stderr, "key does not contain a section: %s\n", key);
+               ret = 2;
+               goto out_free;
+       }
+       store.baselen = last_dot - key;
+
+       store.multi_replace = multi_replace;
+
+       /*
+        * Validate the key and while at it, lower case it for matching.
+        */
+       store.key = xmalloc(strlen(key) + 1);
+       dot = 0;
+       for (i = 0; key[i]; i++) {
+               unsigned char c = key[i];
+               if (c == '.')
+                       dot = 1;
+               /* Leave the extended basename untouched.. */
+               if (!dot || i > store.baselen) {
+                       if (!iskeychar(c) || (i == store.baselen+1 && !isalpha(c))) {
+                               fprintf(stderr, "invalid key: %s\n", key);
+                               free(store.key);
+                               ret = 1;
+                               goto out_free;
+                       }
+                       c = tolower(c);
+               } else if (c == '\n') {
+                       fprintf(stderr, "invalid key (newline): %s\n", key);
+                       free(store.key);
+                       ret = 1;
+                       goto out_free;
+               }
+               store.key[i] = c;
+       }
+       store.key[i] = 0;
+
+       /*
+        * The lock serves a purpose in addition to locking: the new
+        * contents of .git/config will be written into it.
+        */
+       lock = xcalloc(sizeof(struct lock_file), 1);
+       fd = hold_lock_file_for_update(lock, config_filename, 0);
+       if (fd < 0) {
+               fprintf(stderr, "could not lock config file\n");
+               free(store.key);
+               ret = -1;
+               goto out_free;
+       }
+
+       /*
+        * If .git/config does not exist yet, write a minimal version.
+        */
+       in_fd = open(config_filename, O_RDONLY);
+       if ( in_fd < 0 ) {
+               free(store.key);
+
+               if ( ENOENT != errno ) {
+                       error("opening %s: %s", config_filename,
+                             strerror(errno));
+                       ret = 3; /* same as "invalid config file" */
+                       goto out_free;
+               }
+               /* if nothing to unset, error out */
+               if (value == NULL) {
+                       ret = 5;
+                       goto out_free;
+               }
+
+               store.key = (char*)key;
+               if (!store_write_section(fd, key) ||
+                   !store_write_pair(fd, key, value))
+                       goto write_err_out;
+       } else {
+               struct stat st;
+               char* contents;
+               size_t contents_sz, copy_begin, copy_end;
+               int i, new_line = 0;
+
+               if (value_regex == NULL)
+                       store.value_regex = NULL;
+               else {
+                       if (value_regex[0] == '!') {
+                               store.do_not_match = 1;
+                               value_regex++;
+                       } else
+                               store.do_not_match = 0;
+
+                       store.value_regex = (regex_t*)xmalloc(sizeof(regex_t));
+                       if (regcomp(store.value_regex, value_regex,
+                                       REG_EXTENDED)) {
+                               fprintf(stderr, "Invalid pattern: %s\n",
+                                       value_regex);
+                               free(store.value_regex);
+                               ret = 6;
+                               goto out_free;
+                       }
+               }
+
+               store.offset[0] = 0;
+               store.state = START;
+               store.seen = 0;
+
+               /*
+                * After this, store.offset will contain the *end* offset
+                * of the last match, or remain at 0 if no match was found.
+                * As a side effect, we make sure to transform only a valid
+                * existing config file.
+                */
+               if (git_config_from_file(store_aux, config_filename)) {
+                       fprintf(stderr, "invalid config file\n");
+                       free(store.key);
+                       if (store.value_regex != NULL) {
+                               regfree(store.value_regex);
+                               free(store.value_regex);
+                       }
+                       ret = 3;
+                       goto out_free;
+               }
+
+               free(store.key);
+               if (store.value_regex != NULL) {
+                       regfree(store.value_regex);
+                       free(store.value_regex);
+               }
+
+               /* if nothing to unset, or too many matches, error out */
+               if ((store.seen == 0 && value == NULL) ||
+                               (store.seen > 1 && multi_replace == 0)) {
+                       ret = 5;
+                       goto out_free;
+               }
+
+               fstat(in_fd, &st);
+               contents_sz = xsize_t(st.st_size);
+               contents = xmmap(NULL, contents_sz, PROT_READ,
+                       MAP_PRIVATE, in_fd, 0);
+               close(in_fd);
+
+               if (store.seen == 0)
+                       store.seen = 1;
+
+               for (i = 0, copy_begin = 0; i < store.seen; i++) {
+                       if (store.offset[i] == 0) {
+                               store.offset[i] = copy_end = contents_sz;
+                       } else if (store.state != KEY_SEEN) {
+                               copy_end = store.offset[i];
+                       } else
+                               copy_end = find_beginning_of_line(
+                                       contents, contents_sz,
+                                       store.offset[i]-2, &new_line);
+
+                       /* write the first part of the config */
+                       if (copy_end > copy_begin) {
+                               if (write_in_full(fd, contents + copy_begin,
+                                                 copy_end - copy_begin) <
+                                   copy_end - copy_begin)
+                                       goto write_err_out;
+                               if (new_line &&
+                                   write_in_full(fd, "\n", 1) != 1)
+                                       goto write_err_out;
+                       }
+                       copy_begin = store.offset[i];
+               }
+
+               /* write the pair (value == NULL means unset) */
+               if (value != NULL) {
+                       if (store.state == START) {
+                               if (!store_write_section(fd, key))
+                                       goto write_err_out;
+                       }
+                       if (!store_write_pair(fd, key, value))
+                               goto write_err_out;
+               }
+
+               /* write the rest of the config */
+               if (copy_begin < contents_sz)
+                       if (write_in_full(fd, contents + copy_begin,
+                                         contents_sz - copy_begin) <
+                           contents_sz - copy_begin)
+                               goto write_err_out;
+
+               munmap(contents, contents_sz);
+       }
+
+       if (close(fd) || commit_lock_file(lock) < 0) {
+               fprintf(stderr, "Cannot commit config file!\n");
+               ret = 4;
+               goto out_free;
+       }
+
+       /* fd is closed, so don't try to close it below. */
+       fd = -1;
+       /*
+        * lock is committed, so don't try to roll it back below.
+        * NOTE: Since lockfile.c keeps a linked list of all created
+        * lock_file structures, it isn't safe to free(lock).  It's
+        * better to just leave it hanging around.
+        */
+       lock = NULL;
+       ret = 0;
+
+out_free:
+       if (0 <= fd)
+               close(fd);
+       if (lock)
+               rollback_lock_file(lock);
+       free(config_filename);
+       return ret;
+
+write_err_out:
+       ret = write_error();
+       goto out_free;
+
+}
+
+static int section_name_match (const char *buf, const char *name)
+{
+       int i = 0, j = 0, dot = 0;
+       for (; buf[i] && buf[i] != ']'; i++) {
+               if (!dot && isspace(buf[i])) {
+                       dot = 1;
+                       if (name[j++] != '.')
+                               break;
+                       for (i++; isspace(buf[i]); i++)
+                               ; /* do nothing */
+                       if (buf[i] != '"')
+                               break;
+                       continue;
+               }
+               if (buf[i] == '\\' && dot)
+                       i++;
+               else if (buf[i] == '"' && dot) {
+                       for (i++; isspace(buf[i]); i++)
+                               ; /* do_nothing */
+                       break;
+               }
+               if (buf[i] != name[j++])
+                       break;
+       }
+       return (buf[i] == ']' && name[j] == 0);
+}
+
+/* if new_name == NULL, the section is removed instead */
+int git_config_rename_section(const char *old_name, const char *new_name)
+{
+       int ret = 0, remove = 0;
+       char *config_filename;
+       struct lock_file *lock = xcalloc(sizeof(struct lock_file), 1);
+       int out_fd;
+       char buf[1024];
+
+       config_filename = getenv(CONFIG_ENVIRONMENT);
+       if (!config_filename) {
+               config_filename = getenv(CONFIG_LOCAL_ENVIRONMENT);
+               if (!config_filename)
+                       config_filename  = git_path("config");
+       }
+       config_filename = xstrdup(config_filename);
+       out_fd = hold_lock_file_for_update(lock, config_filename, 0);
+       if (out_fd < 0) {
+               ret = error("Could not lock config file!");
+               goto out;
+       }
+
+       if (!(config_file = fopen(config_filename, "rb"))) {
+               /* no config file means nothing to rename, no error */
+               goto unlock_and_out;
+       }
+
+       while (fgets(buf, sizeof(buf), config_file)) {
+               int i;
+               int length;
+               for (i = 0; buf[i] && isspace(buf[i]); i++)
+                       ; /* do nothing */
+               if (buf[i] == '[') {
+                       /* it's a section */
+                       if (section_name_match (&buf[i+1], old_name)) {
+                               ret++;
+                               if (new_name == NULL) {
+                                       remove = 1;
+                                       continue;
+                               }
+                               store.baselen = strlen(new_name);
+                               if (!store_write_section(out_fd, new_name)) {
+                                       ret = write_error();
+                                       goto out;
+                               }
+                               continue;
+                       }
+                       remove = 0;
+               }
+               if (remove)
+                       continue;
+               length = strlen(buf);
+               if (write_in_full(out_fd, buf, length) != length) {
+                       ret = write_error();
+                       goto out;
+               }
+       }
+       fclose(config_file);
+ unlock_and_out:
+       if (close(out_fd) || commit_lock_file(lock) < 0)
+                       ret = error("Cannot commit config file!");
+ out:
+       free(config_filename);
+       return ret;
+}
diff --git a/config.mak.in b/config.mak.in
new file mode 100644 (file)
index 0000000..7d5df9b
--- /dev/null
@@ -0,0 +1,45 @@
+# git Makefile configuration, included in main Makefile
+# @configure_input@
+
+CC = @CC@
+CFLAGS = @CFLAGS@
+AR = @AR@
+TAR = @TAR@
+#INSTALL = @INSTALL@           # needs install-sh or install.sh in sources
+TCLTK_PATH = @TCLTK_PATH@
+
+prefix = @prefix@
+exec_prefix = @exec_prefix@
+bindir = @bindir@
+#gitexecdir = @libexecdir@/git-core/
+datarootdir = @datarootdir@
+template_dir = @datadir@/git-core/templates/
+
+mandir=@mandir@
+
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+export exec_prefix mandir
+export srcdir VPATH
+
+NEEDS_SSL_WITH_CRYPTO=@NEEDS_SSL_WITH_CRYPTO@
+NO_OPENSSL=@NO_OPENSSL@
+NO_CURL=@NO_CURL@
+NO_EXPAT=@NO_EXPAT@
+NEEDS_LIBICONV=@NEEDS_LIBICONV@
+NEEDS_SOCKET=@NEEDS_SOCKET@
+NO_D_INO_IN_DIRENT=@NO_D_INO_IN_DIRENT@
+NO_D_TYPE_IN_DIRENT=@NO_D_TYPE_IN_DIRENT@
+NO_SOCKADDR_STORAGE=@NO_SOCKADDR_STORAGE@
+NO_IPV6=@NO_IPV6@
+NO_C99_FORMAT=@NO_C99_FORMAT@
+NO_STRCASESTR=@NO_STRCASESTR@
+NO_MEMMEM=@NO_MEMMEM@
+NO_STRLCPY=@NO_STRLCPY@
+NO_STRTOUMAX=@NO_STRTOUMAX@
+NO_SETENV=@NO_SETENV@
+NO_MKDTEMP=@NO_MKDTEMP@
+NO_ICONV=@NO_ICONV@
+OLD_ICONV=@OLD_ICONV@
+NO_DEFLATE_BOUND=@NO_DEFLATE_BOUND@
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..dd4b4eb
--- /dev/null
@@ -0,0 +1,449 @@
+#                                               -*- Autoconf -*-
+# Process this file with autoconf to produce a configure script.
+
+AC_PREREQ(2.59)
+AC_INIT([git], [@@GIT_VERSION@@], [git@vger.kernel.org])
+
+AC_CONFIG_SRCDIR([git.c])
+
+config_file=config.mak.autogen
+config_append=config.mak.append
+config_in=config.mak.in
+
+echo "# ${config_append}.  Generated by configure." > "${config_append}"
+
+
+## Definitions of macros
+# GIT_CONF_APPEND_LINE(LINE)
+# --------------------------
+# Append LINE to file ${config_append}
+AC_DEFUN([GIT_CONF_APPEND_LINE],
+[echo "$1" >> "${config_append}"])# GIT_CONF_APPEND_LINE
+#
+# GIT_ARG_SET_PATH(PROGRAM)
+# -------------------------
+# Provide --with-PROGRAM=PATH option to set PATH to PROGRAM
+AC_DEFUN([GIT_ARG_SET_PATH],
+[AC_ARG_WITH([$1],
+ [AS_HELP_STRING([--with-$1=PATH],
+                 [provide PATH to $1])],
+ [GIT_CONF_APPEND_PATH($1)],[])
+])# GIT_ARG_SET_PATH
+#
+# GIT_CONF_APPEND_PATH(PROGRAM)
+# ------------------------------
+# Parse --with-PROGRAM=PATH option to set PROGRAM_PATH=PATH
+# Used by GIT_ARG_SET_PATH(PROGRAM)
+AC_DEFUN([GIT_CONF_APPEND_PATH],
+[PROGRAM=m4_toupper($1); \
+if test "$withval" = "no"; then \
+       AC_MSG_ERROR([You cannot use git without $1]); \
+else \
+       if test "$withval" = "yes"; then \
+               AC_MSG_WARN([You should provide path for --with-$1=PATH]); \
+       else \
+               GIT_CONF_APPEND_LINE(${PROGRAM}_PATH=$withval); \
+       fi; \
+fi; \
+]) # GIT_CONF_APPEND_PATH
+#
+# GIT_PARSE_WITH(PACKAGE)
+# -----------------------
+# For use in AC_ARG_WITH action-if-found, for packages default ON.
+# * Set NO_PACKAGE=YesPlease for --without-PACKAGE
+# * Set PACKAGEDIR=PATH for --with-PACKAGE=PATH
+# * Unset NO_PACKAGE for --with-PACKAGE without ARG
+AC_DEFUN([GIT_PARSE_WITH],
+[PACKAGE=m4_toupper($1); \
+if test "$withval" = "no"; then \
+       m4_toupper(NO_$1)=YesPlease; \
+elif test "$withval" = "yes"; then \
+       m4_toupper(NO_$1)=; \
+else \
+       m4_toupper(NO_$1)=; \
+       GIT_CONF_APPEND_LINE(${PACKAGE}DIR=$withval); \
+fi \
+])# GIT_PARSE_WITH
+
+
+## Site configuration related to programs (before tests)
+## --with-PACKAGE[=ARG] and --without-PACKAGE
+#
+# Set lib to alternative name of lib directory (e.g. lib64)
+AC_ARG_WITH([lib],
+ [AS_HELP_STRING([--with-lib=ARG],
+                 [ARG specifies alternative name for lib directory])],
+ [if test "$withval" = "no" || test "$withval" = "yes"; then \
+       AC_MSG_WARN([You should provide name for --with-lib=ARG]); \
+else \
+       GIT_CONF_APPEND_LINE(lib=$withval); \
+fi; \
+],[])
+#
+# Define SHELL_PATH to provide path to shell.
+GIT_ARG_SET_PATH(shell)
+#
+# Define PERL_PATH to provide path to Perl.
+GIT_ARG_SET_PATH(perl)
+#
+# Define ZLIB_PATH to provide path to zlib.
+GIT_ARG_SET_PATH(zlib)
+#
+# Declare the with-tcltk/without-tcltk options.
+AC_ARG_WITH(tcltk,
+AS_HELP_STRING([--with-tcltk],[use Tcl/Tk GUI (default is YES)])
+AS_HELP_STRING([],[ARG is the full path to the Tcl/Tk interpreter.])
+AS_HELP_STRING([],[Bare --with-tcltk will make the GUI part only if])
+AS_HELP_STRING([],[Tcl/Tk interpreter will be found in a system.]),\
+GIT_PARSE_WITH(tcltk))
+#
+
+
+## Checks for programs.
+AC_MSG_NOTICE([CHECKS for programs])
+#
+AC_PROG_CC([cc gcc])
+#AC_PROG_INSTALL               # needs install-sh or install.sh in sources
+AC_CHECK_TOOLS(AR, [gar ar], :)
+AC_CHECK_PROGS(TAR, [gtar tar])
+# TCLTK_PATH will be set to some value if we want Tcl/Tk
+# or will be empty otherwise.
+if test -z "$NO_TCLTK"; then
+  if test "$with_tcltk" = ""; then
+  # No Tcl/Tk switches given. Do not check for Tcl/Tk, use bare 'wish'.
+    TCLTK_PATH=wish
+    AC_SUBST(TCLTK_PATH)
+  elif test "$with_tcltk" = "yes"; then
+  # Tcl/Tk check requested.
+    AC_CHECK_PROGS(TCLTK_PATH, [wish], )
+  else
+    AC_MSG_RESULT([Using Tcl/Tk interpreter $with_tcltk])
+    TCLTK_PATH="$with_tcltk"
+    AC_SUBST(TCLTK_PATH)
+  fi
+fi
+
+## Checks for libraries.
+AC_MSG_NOTICE([CHECKS for libraries])
+#
+# Define NO_OPENSSL environment variable if you do not have OpenSSL.
+# Define NEEDS_SSL_WITH_CRYPTO if you need -lcrypto with -lssl (Darwin).
+AC_CHECK_LIB([crypto], [SHA1_Init],
+[NEEDS_SSL_WITH_CRYPTO=],
+[AC_CHECK_LIB([ssl], [SHA1_Init],
+ [NEEDS_SSL_WITH_CRYPTO=YesPlease
+  NEEDS_SSL_WITH_CRYPTO=],
+ [NO_OPENSSL=YesPlease])])
+AC_SUBST(NEEDS_SSL_WITH_CRYPTO)
+AC_SUBST(NO_OPENSSL)
+#
+# Define NO_CURL if you do not have curl installed.  git-http-pull and
+# git-http-push are not built, and you cannot use http:// and https://
+# transports.
+AC_CHECK_LIB([curl], [curl_global_init],
+[NO_CURL=],
+[NO_CURL=YesPlease])
+AC_SUBST(NO_CURL)
+#
+# Define NO_EXPAT if you do not have expat installed.  git-http-push is
+# not built, and you cannot push using http:// and https:// transports.
+AC_CHECK_LIB([expat], [XML_ParserCreate],
+[NO_EXPAT=],
+[NO_EXPAT=YesPlease])
+AC_SUBST(NO_EXPAT)
+#
+# Define NEEDS_LIBICONV if linking with libc is not enough (Darwin and
+# some Solaris installations).
+# Define NO_ICONV if neither libc nor libiconv support iconv.
+AC_DEFUN([ICONVTEST_SRC], [
+#include <iconv.h>
+
+int main(void)
+{
+       iconv_open("", "");
+       return 0;
+}
+])
+AC_MSG_CHECKING([for iconv in -lc])
+AC_LINK_IFELSE(ICONVTEST_SRC,
+       [AC_MSG_RESULT([yes])
+       NEEDS_LIBICONV=],
+       [AC_MSG_RESULT([no])
+       old_LIBS="$LIBS"
+       LIBS="$LIBS -liconv"
+       AC_MSG_CHECKING([for iconv in -liconv])
+       AC_LINK_IFELSE(ICONVTEST_SRC,
+               [AC_MSG_RESULT([yes])
+               NEEDS_LIBICONV=YesPlease],
+               [AC_MSG_RESULT([no])
+               NO_ICONV=YesPlease])
+       LIBS="$old_LIBS"])
+AC_SUBST(NEEDS_LIBICONV)
+AC_SUBST(NO_ICONV)
+test -n "$NEEDS_LIBICONV" && LIBS="$LIBS -liconv"
+#
+# Define NO_DEFLATE_BOUND if deflateBound is missing from zlib.
+AC_DEFUN([ZLIBTEST_SRC], [
+#include <zlib.h>
+
+int main(void)
+{
+       deflateBound(0, 0);
+       return 0;
+}
+])
+AC_MSG_CHECKING([for deflateBound in -lz])
+old_LIBS="$LIBS"
+LIBS="$LIBS -lz"
+AC_LINK_IFELSE(ZLIBTEST_SRC,
+       [AC_MSG_RESULT([yes])],
+       [AC_MSG_RESULT([no])
+       NO_DEFLATE_BOUND=yes])
+LIBS="$old_LIBS"
+AC_SUBST(NO_DEFLATE_BOUND)
+#
+# Define NEEDS_SOCKET if linking with libc is not enough (SunOS,
+# Patrick Mauritz).
+AC_CHECK_LIB([c], [socket],
+[NEEDS_SOCKET=],
+[NEEDS_SOCKET=YesPlease])
+AC_SUBST(NEEDS_SOCKET)
+test -n "$NEEDS_SOCKET" && LIBS="$LIBS -lsocket"
+
+
+## Checks for header files.
+AC_MSG_NOTICE([CHECKS for header files])
+#
+# Define OLD_ICONV if your library has an old iconv(), where the second
+# (input buffer pointer) parameter is declared with type (const char **).
+AC_DEFUN([OLDICONVTEST_SRC], [[
+#include <iconv.h>
+
+extern size_t iconv(iconv_t cd,
+                   char **inbuf, size_t *inbytesleft,
+                   char **outbuf, size_t *outbytesleft);
+
+int main(void)
+{
+       return 0;
+}
+]])
+AC_MSG_CHECKING([for old iconv()])
+AC_COMPILE_IFELSE(OLDICONVTEST_SRC,
+       [AC_MSG_RESULT([no])],
+       [AC_MSG_RESULT([yes])
+       OLD_ICONV=UnfortunatelyYes])
+AC_SUBST(OLD_ICONV)
+
+
+## Checks for typedefs, structures, and compiler characteristics.
+AC_MSG_NOTICE([CHECKS for typedefs, structures, and compiler characteristics])
+#
+# Define NO_D_INO_IN_DIRENT if you don't have d_ino in your struct dirent.
+AC_CHECK_MEMBER(struct dirent.d_ino,
+[NO_D_INO_IN_DIRENT=],
+[NO_D_INO_IN_DIRENT=YesPlease],
+[#include <dirent.h>])
+AC_SUBST(NO_D_INO_IN_DIRENT)
+#
+# Define NO_D_TYPE_IN_DIRENT if your platform defines DT_UNKNOWN but lacks
+# d_type in struct dirent (latest Cygwin -- will be fixed soonish).
+AC_CHECK_MEMBER(struct dirent.d_type,
+[NO_D_TYPE_IN_DIRENT=],
+[NO_D_TYPE_IN_DIRENT=YesPlease],
+[#include <dirent.h>])
+AC_SUBST(NO_D_TYPE_IN_DIRENT)
+#
+# Define NO_SOCKADDR_STORAGE if your platform does not have struct
+# sockaddr_storage.
+AC_CHECK_TYPE(struct sockaddr_storage,
+[NO_SOCKADDR_STORAGE=],
+[NO_SOCKADDR_STORAGE=YesPlease],[
+#include <sys/types.h>
+#include <sys/socket.h>
+])
+AC_SUBST(NO_SOCKADDR_STORAGE)
+#
+# Define NO_IPV6 if you lack IPv6 support and getaddrinfo().
+AC_CHECK_TYPE([struct addrinfo],[
+ AC_CHECK_FUNC([getaddrinfo],
+  [NO_IPV6=],
+  [NO_IPV6=YesPlease])
+],[NO_IPV6=YesPlease],[
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+])
+AC_SUBST(NO_IPV6)
+#
+# Define NO_C99_FORMAT if your formatted IO functions (printf/scanf et.al.)
+# do not support the 'size specifiers' introduced by C99, namely ll, hh,
+# j, z, t. (representing long long int, char, intmax_t, size_t, ptrdiff_t).
+# some C compilers supported these specifiers prior to C99 as an extension.
+AC_CACHE_CHECK([whether formatted IO functions support C99 size specifiers],
+ [ac_cv_c_c99_format],
+[# Actually git uses only %z (%zu) in alloc.c, and %t (%td) in mktag.c
+AC_RUN_IFELSE(
+       [AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT],
+               [[char buf[64];
+               if (sprintf(buf, "%lld%hhd%jd%zd%td", (long long int)1, (char)2, (intmax_t)3, (size_t)4, (ptrdiff_t)5) != 5)
+                 return 1;
+               else if (strcmp(buf, "12345"))
+                 return 2;]])],
+       [ac_cv_c_c99_format=yes],
+       [ac_cv_c_c99_format=no])
+])
+if test $ac_cv_c_c99_format = no; then
+       NO_C99_FORMAT=YesPlease
+else
+       NO_C99_FORMAT=
+fi
+AC_SUBST(NO_C99_FORMAT)
+
+
+## Checks for library functions.
+## (in default C library and libraries checked by AC_CHECK_LIB)
+AC_MSG_NOTICE([CHECKS for library functions])
+#
+# Define NO_STRCASESTR if you don't have strcasestr.
+AC_CHECK_FUNC(strcasestr,
+[NO_STRCASESTR=],
+[NO_STRCASESTR=YesPlease])
+AC_SUBST(NO_STRCASESTR)
+#
+# Define NO_MEMMEM if you don't have memmem.
+AC_CHECK_FUNC(memmem,
+[NO_MEMMEM=],
+[NO_MEMMEM=YesPlease])
+AC_SUBST(NO_MEMMEM)
+#
+# Define NO_STRLCPY if you don't have strlcpy.
+AC_CHECK_FUNC(strlcpy,
+[NO_STRLCPY=],
+[NO_STRLCPY=YesPlease])
+AC_SUBST(NO_STRLCPY)
+#
+# Define NO_STRTOUMAX if you don't have strtoumax in the C library.
+AC_CHECK_FUNC(strtoumax,
+[NO_STRTOUMAX=],
+[NO_STRTOUMAX=YesPlease])
+AC_SUBST(NO_STRTOUMAX)
+#
+# Define NO_SETENV if you don't have setenv in the C library.
+AC_CHECK_FUNC(setenv,
+[NO_SETENV=],
+[NO_SETENV=YesPlease])
+AC_SUBST(NO_SETENV)
+#
+# Define NO_MKDTEMP if you don't have mkdtemp in the C library.
+AC_CHECK_FUNC(mkdtemp,
+[NO_MKDTEMP=],
+[NO_MKDTEMP=YesPlease])
+AC_SUBST(NO_MKDTEMP)
+#
+# Define NO_MMAP if you want to avoid mmap.
+#
+# Define NO_ICONV if your libc does not properly support iconv.
+
+
+## Other checks.
+# Define USE_PIC if you need the main git objects to be built with -fPIC
+# in order to build and link perl/Git.so.  x86-64 seems to need this.
+#
+# Define NO_SYMLINK_HEAD if you never want .git/HEAD to be a symbolic link.
+# Enable it on Windows.  By default, symrefs are still used.
+
+## Site configuration (override autodetection)
+## --with-PACKAGE[=ARG] and --without-PACKAGE
+AC_MSG_NOTICE([CHECKS for site configuration])
+#
+# Define NO_SVN_TESTS if you want to skip time-consuming SVN interoperability
+# tests.  These tests take up a significant amount of the total test time
+# but are not needed unless you plan to talk to SVN repos.
+#
+# Define MOZILLA_SHA1 environment variable when running make to make use of
+# a bundled SHA1 routine coming from Mozilla. It is GPL'd and should be fast
+# on non-x86 architectures (e.g. PowerPC), while the OpenSSL version (default
+# choice) has very fast version optimized for i586.
+#
+# Define PPC_SHA1 environment variable when running make to make use of
+# a bundled SHA1 routine optimized for PowerPC.
+#
+# Define ARM_SHA1 environment variable when running make to make use of
+# a bundled SHA1 routine optimized for ARM.
+#
+# Define NO_OPENSSL environment variable if you do not have OpenSSL.
+# This also implies MOZILLA_SHA1.
+#
+# Define OPENSSLDIR=/foo/bar if your openssl header and library files are in
+# /foo/bar/include and /foo/bar/lib directories.
+AC_ARG_WITH(openssl,
+AS_HELP_STRING([--with-openssl],[use OpenSSL library (default is YES)])
+AS_HELP_STRING([],              [ARG can be prefix for openssl library and headers]),\
+GIT_PARSE_WITH(openssl))
+#
+# Define NO_CURL if you do not have curl installed.  git-http-pull and
+# git-http-push are not built, and you cannot use http:// and https://
+# transports.
+#
+# Define CURLDIR=/foo/bar if your curl header and library files are in
+# /foo/bar/include and /foo/bar/lib directories.
+AC_ARG_WITH(curl,
+AS_HELP_STRING([--with-curl],[support http(s):// transports (default is YES)])
+AS_HELP_STRING([],           [ARG can be also prefix for curl library and headers]),
+GIT_PARSE_WITH(curl))
+#
+# Define NO_EXPAT if you do not have expat installed.  git-http-push is
+# not built, and you cannot push using http:// and https:// transports.
+#
+# Define EXPATDIR=/foo/bar if your expat header and library files are in
+# /foo/bar/include and /foo/bar/lib directories.
+AC_ARG_WITH(expat,
+AS_HELP_STRING([--with-expat],
+[support git-push using http:// and https:// transports via WebDAV (default is YES)])
+AS_HELP_STRING([],            [ARG can be also prefix for expat library and headers]),
+GIT_PARSE_WITH(expat))
+#
+# Define NO_FINK if you are building on Darwin/Mac OS X, have Fink
+# installed in /sw, but don't want GIT to link against any libraries
+# installed there.  If defined you may specify your own (or Fink's)
+# include directories and library directories by defining CFLAGS
+# and LDFLAGS appropriately.
+#
+# Define NO_DARWIN_PORTS if you are building on Darwin/Mac OS X,
+# have DarwinPorts installed in /opt/local, but don't want GIT to
+# link against any libraries installed there.  If defined you may
+# specify your own (or DarwinPort's) include directories and
+# library directories by defining CFLAGS and LDFLAGS appropriately.
+#
+# Define NO_MMAP if you want to avoid mmap.
+#
+# Define NO_ICONV if your libc does not properly support iconv.
+AC_ARG_WITH(iconv,
+AS_HELP_STRING([--without-iconv],
+[if your architecture doesn't properly support iconv])
+AS_HELP_STRING([--with-iconv=PATH],
+[PATH is prefix for libiconv library and headers])
+AS_HELP_STRING([],
+[used only if you need linking with libiconv]),
+GIT_PARSE_WITH(iconv))
+
+## --enable-FEATURE[=ARG] and --disable-FEATURE
+#
+# Define USE_NSEC below if you want git to care about sub-second file mtimes
+# and ctimes. Note that you need recent glibc (at least 2.2.4) for this, and
+# it will BREAK YOUR LOCAL DIFFS! show-diff and anything using it will likely
+# randomly break unless your underlying filesystem supports those sub-second
+# times (my ext3 doesn't).
+#
+# Define USE_STDEV below if you want git to care about the underlying device
+# change being considered an inode change from the update-index perspective.
+
+
+## Output files
+AC_CONFIG_FILES(["${config_file}":"${config_in}":"${config_append}"])
+AC_OUTPUT
+
+
+## Cleanup
+rm -f "${config_append}"
diff --git a/connect.c b/connect.c
new file mode 100644 (file)
index 0000000..3aefd4a
--- /dev/null
+++ b/connect.c
@@ -0,0 +1,645 @@
+#include "git-compat-util.h"
+#include "cache.h"
+#include "pkt-line.h"
+#include "quote.h"
+#include "refs.h"
+#include "run-command.h"
+#include "remote.h"
+
+static char *server_capabilities;
+
+static int check_ref(const char *name, int len, unsigned int flags)
+{
+       if (!flags)
+               return 1;
+
+       if (len < 5 || memcmp(name, "refs/", 5))
+               return 0;
+
+       /* Skip the "refs/" part */
+       name += 5;
+       len -= 5;
+
+       /* REF_NORMAL means that we don't want the magic fake tag refs */
+       if ((flags & REF_NORMAL) && check_ref_format(name) < 0)
+               return 0;
+
+       /* REF_HEADS means that we want regular branch heads */
+       if ((flags & REF_HEADS) && !memcmp(name, "heads/", 6))
+               return 1;
+
+       /* REF_TAGS means that we want tags */
+       if ((flags & REF_TAGS) && !memcmp(name, "tags/", 5))
+               return 1;
+
+       /* All type bits clear means that we are ok with anything */
+       return !(flags & ~REF_NORMAL);
+}
+
+int check_ref_type(const struct ref *ref, int flags)
+{
+       return check_ref(ref->name, strlen(ref->name), flags);
+}
+
+/*
+ * Read all the refs from the other end
+ */
+struct ref **get_remote_heads(int in, struct ref **list,
+                             int nr_match, char **match,
+                             unsigned int flags)
+{
+       *list = NULL;
+       for (;;) {
+               struct ref *ref;
+               unsigned char old_sha1[20];
+               static char buffer[1000];
+               char *name;
+               int len, name_len;
+
+               len = packet_read_line(in, buffer, sizeof(buffer));
+               if (!len)
+                       break;
+               if (buffer[len-1] == '\n')
+                       buffer[--len] = 0;
+
+               if (len < 42 || get_sha1_hex(buffer, old_sha1) || buffer[40] != ' ')
+                       die("protocol error: expected sha/ref, got '%s'", buffer);
+               name = buffer + 41;
+
+               name_len = strlen(name);
+               if (len != name_len + 41) {
+                       if (server_capabilities)
+                               free(server_capabilities);
+                       server_capabilities = xstrdup(name + name_len + 1);
+               }
+
+               if (!check_ref(name, name_len, flags))
+                       continue;
+               if (nr_match && !path_match(name, nr_match, match))
+                       continue;
+               ref = alloc_ref(name_len + 1);
+               hashcpy(ref->old_sha1, old_sha1);
+               memcpy(ref->name, buffer + 41, name_len + 1);
+               *list = ref;
+               list = &ref->next;
+       }
+       return list;
+}
+
+int server_supports(const char *feature)
+{
+       return server_capabilities &&
+               strstr(server_capabilities, feature) != NULL;
+}
+
+int get_ack(int fd, unsigned char *result_sha1)
+{
+       static char line[1000];
+       int len = packet_read_line(fd, line, sizeof(line));
+
+       if (!len)
+               die("git-fetch-pack: expected ACK/NAK, got EOF");
+       if (line[len-1] == '\n')
+               line[--len] = 0;
+       if (!strcmp(line, "NAK"))
+               return 0;
+       if (!prefixcmp(line, "ACK ")) {
+               if (!get_sha1_hex(line+4, result_sha1)) {
+                       if (strstr(line+45, "continue"))
+                               return 2;
+                       return 1;
+               }
+       }
+       die("git-fetch_pack: expected ACK/NAK, got '%s'", line);
+}
+
+int path_match(const char *path, int nr, char **match)
+{
+       int i;
+       int pathlen = strlen(path);
+
+       for (i = 0; i < nr; i++) {
+               char *s = match[i];
+               int len = strlen(s);
+
+               if (!len || len > pathlen)
+                       continue;
+               if (memcmp(path + pathlen - len, s, len))
+                       continue;
+               if (pathlen > len && path[pathlen - len - 1] != '/')
+                       continue;
+               *s = 0;
+               return (i + 1);
+       }
+       return 0;
+}
+
+enum protocol {
+       PROTO_LOCAL = 1,
+       PROTO_SSH,
+       PROTO_GIT,
+};
+
+static enum protocol get_protocol(const char *name)
+{
+       if (!strcmp(name, "ssh"))
+               return PROTO_SSH;
+       if (!strcmp(name, "git"))
+               return PROTO_GIT;
+       if (!strcmp(name, "git+ssh"))
+               return PROTO_SSH;
+       if (!strcmp(name, "ssh+git"))
+               return PROTO_SSH;
+       if (!strcmp(name, "file"))
+               return PROTO_LOCAL;
+       die("I don't handle protocol '%s'", name);
+}
+
+#define STR_(s)        # s
+#define STR(s) STR_(s)
+
+#ifndef NO_IPV6
+
+static const char *ai_name(const struct addrinfo *ai)
+{
+       static char addr[INET_ADDRSTRLEN];
+       if ( AF_INET == ai->ai_family ) {
+               struct sockaddr_in *in;
+               in = (struct sockaddr_in *)ai->ai_addr;
+               inet_ntop(ai->ai_family, &in->sin_addr, addr, sizeof(addr));
+       } else if ( AF_INET6 == ai->ai_family ) {
+               struct sockaddr_in6 *in;
+               in = (struct sockaddr_in6 *)ai->ai_addr;
+               inet_ntop(ai->ai_family, &in->sin6_addr, addr, sizeof(addr));
+       } else {
+               strcpy(addr, "(unknown)");
+       }
+       return addr;
+}
+
+/*
+ * Returns a connected socket() fd, or else die()s.
+ */
+static int git_tcp_connect_sock(char *host, int flags)
+{
+       int sockfd = -1, saved_errno = 0;
+       char *colon, *end;
+       const char *port = STR(DEFAULT_GIT_PORT);
+       struct addrinfo hints, *ai0, *ai;
+       int gai;
+       int cnt = 0;
+
+       if (host[0] == '[') {
+               end = strchr(host + 1, ']');
+               if (end) {
+                       *end = 0;
+                       end++;
+                       host++;
+               } else
+                       end = host;
+       } else
+               end = host;
+       colon = strchr(end, ':');
+
+       if (colon) {
+               *colon = 0;
+               port = colon + 1;
+               if (!*port)
+                       port = "<none>";
+       }
+
+       memset(&hints, 0, sizeof(hints));
+       hints.ai_socktype = SOCK_STREAM;
+       hints.ai_protocol = IPPROTO_TCP;
+
+       if (flags & CONNECT_VERBOSE)
+               fprintf(stderr, "Looking up %s ... ", host);
+
+       gai = getaddrinfo(host, port, &hints, &ai);
+       if (gai)
+               die("Unable to look up %s (port %s) (%s)", host, port, gai_strerror(gai));
+
+       if (flags & CONNECT_VERBOSE)
+               fprintf(stderr, "done.\nConnecting to %s (port %s) ... ", host, port);
+
+       for (ai0 = ai; ai; ai = ai->ai_next) {
+               sockfd = socket(ai->ai_family,
+                               ai->ai_socktype, ai->ai_protocol);
+               if (sockfd < 0) {
+                       saved_errno = errno;
+                       continue;
+               }
+               if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
+                       saved_errno = errno;
+                       fprintf(stderr, "%s[%d: %s]: errno=%s\n",
+                               host,
+                               cnt,
+                               ai_name(ai),
+                               strerror(saved_errno));
+                       close(sockfd);
+                       sockfd = -1;
+                       continue;
+               }
+               if (flags & CONNECT_VERBOSE)
+                       fprintf(stderr, "%s ", ai_name(ai));
+               break;
+       }
+
+       freeaddrinfo(ai0);
+
+       if (sockfd < 0)
+               die("unable to connect a socket (%s)", strerror(saved_errno));
+
+       if (flags & CONNECT_VERBOSE)
+               fprintf(stderr, "done.\n");
+
+       return sockfd;
+}
+
+#else /* NO_IPV6 */
+
+/*
+ * Returns a connected socket() fd, or else die()s.
+ */
+static int git_tcp_connect_sock(char *host, int flags)
+{
+       int sockfd = -1, saved_errno = 0;
+       char *colon, *end;
+       char *port = STR(DEFAULT_GIT_PORT), *ep;
+       struct hostent *he;
+       struct sockaddr_in sa;
+       char **ap;
+       unsigned int nport;
+       int cnt;
+
+       if (host[0] == '[') {
+               end = strchr(host + 1, ']');
+               if (end) {
+                       *end = 0;
+                       end++;
+                       host++;
+               } else
+                       end = host;
+       } else
+               end = host;
+       colon = strchr(end, ':');
+
+       if (colon) {
+               *colon = 0;
+               port = colon + 1;
+       }
+
+       if (flags & CONNECT_VERBOSE)
+               fprintf(stderr, "Looking up %s ... ", host);
+
+       he = gethostbyname(host);
+       if (!he)
+               die("Unable to look up %s (%s)", host, hstrerror(h_errno));
+       nport = strtoul(port, &ep, 10);
+       if ( ep == port || *ep ) {
+               /* Not numeric */
+               struct servent *se = getservbyname(port,"tcp");
+               if ( !se )
+                       die("Unknown port %s\n", port);
+               nport = se->s_port;
+       }
+
+       if (flags & CONNECT_VERBOSE)
+               fprintf(stderr, "done.\nConnecting to %s (port %s) ... ", host, port);
+
+       for (cnt = 0, ap = he->h_addr_list; *ap; ap++, cnt++) {
+               sockfd = socket(he->h_addrtype, SOCK_STREAM, 0);
+               if (sockfd < 0) {
+                       saved_errno = errno;
+                       continue;
+               }
+
+               memset(&sa, 0, sizeof sa);
+               sa.sin_family = he->h_addrtype;
+               sa.sin_port = htons(nport);
+               memcpy(&sa.sin_addr, *ap, he->h_length);
+
+               if (connect(sockfd, (struct sockaddr *)&sa, sizeof sa) < 0) {
+                       saved_errno = errno;
+                       fprintf(stderr, "%s[%d: %s]: errno=%s\n",
+                               host,
+                               cnt,
+                               inet_ntoa(*(struct in_addr *)&sa.sin_addr),
+                               strerror(saved_errno));
+                       close(sockfd);
+                       sockfd = -1;
+                       continue;
+               }
+               if (flags & CONNECT_VERBOSE)
+                       fprintf(stderr, "%s ",
+                               inet_ntoa(*(struct in_addr *)&sa.sin_addr));
+               break;
+       }
+
+       if (sockfd < 0)
+               die("unable to connect a socket (%s)", strerror(saved_errno));
+
+       if (flags & CONNECT_VERBOSE)
+               fprintf(stderr, "done.\n");
+
+       return sockfd;
+}
+
+#endif /* NO_IPV6 */
+
+
+static void git_tcp_connect(int fd[2], char *host, int flags)
+{
+       int sockfd = git_tcp_connect_sock(host, flags);
+
+       fd[0] = sockfd;
+       fd[1] = dup(sockfd);
+}
+
+
+static char *git_proxy_command;
+static const char *rhost_name;
+static int rhost_len;
+
+static int git_proxy_command_options(const char *var, const char *value)
+{
+       if (!strcmp(var, "core.gitproxy")) {
+               const char *for_pos;
+               int matchlen = -1;
+               int hostlen;
+
+               if (git_proxy_command)
+                       return 0;
+               /* [core]
+                * ;# matches www.kernel.org as well
+                * gitproxy = netcatter-1 for kernel.org
+                * gitproxy = netcatter-2 for sample.xz
+                * gitproxy = netcatter-default
+                */
+               for_pos = strstr(value, " for ");
+               if (!for_pos)
+                       /* matches everybody */
+                       matchlen = strlen(value);
+               else {
+                       hostlen = strlen(for_pos + 5);
+                       if (rhost_len < hostlen)
+                               matchlen = -1;
+                       else if (!strncmp(for_pos + 5,
+                                         rhost_name + rhost_len - hostlen,
+                                         hostlen) &&
+                                ((rhost_len == hostlen) ||
+                                 rhost_name[rhost_len - hostlen -1] == '.'))
+                               matchlen = for_pos - value;
+                       else
+                               matchlen = -1;
+               }
+               if (0 <= matchlen) {
+                       /* core.gitproxy = none for kernel.org */
+                       if (matchlen == 4 &&
+                           !memcmp(value, "none", 4))
+                               matchlen = 0;
+                       git_proxy_command = xmemdupz(value, matchlen);
+               }
+               return 0;
+       }
+
+       return git_default_config(var, value);
+}
+
+static int git_use_proxy(const char *host)
+{
+       rhost_name = host;
+       rhost_len = strlen(host);
+       git_proxy_command = getenv("GIT_PROXY_COMMAND");
+       git_config(git_proxy_command_options);
+       rhost_name = NULL;
+       return (git_proxy_command && *git_proxy_command);
+}
+
+static void git_proxy_connect(int fd[2], char *host)
+{
+       const char *port = STR(DEFAULT_GIT_PORT);
+       char *colon, *end;
+       const char *argv[4];
+       struct child_process proxy;
+
+       if (host[0] == '[') {
+               end = strchr(host + 1, ']');
+               if (end) {
+                       *end = 0;
+                       end++;
+                       host++;
+               } else
+                       end = host;
+       } else
+               end = host;
+       colon = strchr(end, ':');
+
+       if (colon) {
+               *colon = 0;
+               port = colon + 1;
+       }
+
+       argv[0] = git_proxy_command;
+       argv[1] = host;
+       argv[2] = port;
+       argv[3] = NULL;
+       memset(&proxy, 0, sizeof(proxy));
+       proxy.argv = argv;
+       proxy.in = -1;
+       proxy.out = -1;
+       if (start_command(&proxy))
+               die("cannot start proxy %s", argv[0]);
+       fd[0] = proxy.out; /* read from proxy stdout */
+       fd[1] = proxy.in;  /* write to proxy stdin */
+}
+
+#define MAX_CMD_LEN 1024
+
+char *get_port(char *host)
+{
+       char *end;
+       char *p = strchr(host, ':');
+
+       if (p) {
+               strtol(p+1, &end, 10);
+               if (*end == '\0') {
+                       *p = '\0';
+                       return p+1;
+               }
+       }
+
+       return NULL;
+}
+
+/*
+ * This returns NULL if the transport protocol does not need fork(2), or a
+ * struct child_process object if it does.  Once done, finish the connection
+ * with finish_connect() with the value returned from this function
+ * (it is safe to call finish_connect() with NULL to support the former
+ * case).
+ *
+ * If it returns, the connect is successful; it just dies on errors.
+ */
+struct child_process *git_connect(int fd[2], const char *url_orig,
+                                 const char *prog, int flags)
+{
+       char *url = xstrdup(url_orig);
+       char *host, *path = url;
+       char *end;
+       int c;
+       struct child_process *conn;
+       enum protocol protocol = PROTO_LOCAL;
+       int free_path = 0;
+       char *port = NULL;
+       const char **arg;
+       struct strbuf cmd;
+
+       /* Without this we cannot rely on waitpid() to tell
+        * what happened to our children.
+        */
+       signal(SIGCHLD, SIG_DFL);
+
+       host = strstr(url, "://");
+       if(host) {
+               *host = '\0';
+               protocol = get_protocol(url);
+               host += 3;
+               c = '/';
+       } else {
+               host = url;
+               c = ':';
+       }
+
+       if (host[0] == '[') {
+               end = strchr(host + 1, ']');
+               if (end) {
+                       *end = 0;
+                       end++;
+                       host++;
+               } else
+                       end = host;
+       } else
+               end = host;
+
+       path = strchr(end, c);
+       if (path) {
+               if (c == ':') {
+                       protocol = PROTO_SSH;
+                       *path++ = '\0';
+               }
+       } else
+               path = end;
+
+       if (!path || !*path)
+               die("No path specified. See 'man git-pull' for valid url syntax");
+
+       /*
+        * null-terminate hostname and point path to ~ for URL's like this:
+        *    ssh://host.xz/~user/repo
+        */
+       if (protocol != PROTO_LOCAL && host != url) {
+               char *ptr = path;
+               if (path[1] == '~')
+                       path++;
+               else {
+                       path = xstrdup(ptr);
+                       free_path = 1;
+               }
+
+               *ptr = '\0';
+       }
+
+       /*
+        * Add support for ssh port: ssh://host.xy:<port>/...
+        */
+       if (protocol == PROTO_SSH && host != url)
+               port = get_port(host);
+
+       if (protocol == PROTO_GIT) {
+               /* These underlying connection commands die() if they
+                * cannot connect.
+                */
+               char *target_host = xstrdup(host);
+               if (git_use_proxy(host))
+                       git_proxy_connect(fd, host);
+               else
+                       git_tcp_connect(fd, host, flags);
+               /*
+                * Separate original protocol components prog and path
+                * from extended components with a NUL byte.
+                */
+               packet_write(fd[1],
+                            "%s %s%chost=%s%c",
+                            prog, path, 0,
+                            target_host, 0);
+               free(target_host);
+               free(url);
+               if (free_path)
+                       free(path);
+               return NULL;
+       }
+
+       conn = xcalloc(1, sizeof(*conn));
+
+       strbuf_init(&cmd, MAX_CMD_LEN);
+       strbuf_addstr(&cmd, prog);
+       strbuf_addch(&cmd, ' ');
+       sq_quote_buf(&cmd, path);
+       if (cmd.len >= MAX_CMD_LEN)
+               die("command line too long");
+
+       conn->in = conn->out = -1;
+       conn->argv = arg = xcalloc(6, sizeof(*arg));
+       if (protocol == PROTO_SSH) {
+               const char *ssh = getenv("GIT_SSH");
+               if (!ssh) ssh = "ssh";
+
+               *arg++ = ssh;
+               if (port) {
+                       *arg++ = "-p";
+                       *arg++ = port;
+               }
+               *arg++ = host;
+       }
+       else {
+               /* remove these from the environment */
+               const char *env[] = {
+                       ALTERNATE_DB_ENVIRONMENT,
+                       DB_ENVIRONMENT,
+                       GIT_DIR_ENVIRONMENT,
+                       GIT_WORK_TREE_ENVIRONMENT,
+                       GRAFT_ENVIRONMENT,
+                       INDEX_ENVIRONMENT,
+                       NULL
+               };
+               conn->env = env;
+               *arg++ = "sh";
+               *arg++ = "-c";
+       }
+       *arg++ = cmd.buf;
+       *arg = NULL;
+
+       if (start_command(conn))
+               die("unable to fork");
+
+       fd[0] = conn->out; /* read from child's stdout */
+       fd[1] = conn->in;  /* write to child's stdin */
+       strbuf_release(&cmd);
+       free(url);
+       if (free_path)
+               free(path);
+       return conn;
+}
+
+int finish_connect(struct child_process *conn)
+{
+       int code;
+       if (!conn)
+               return 0;
+
+       code = finish_command(conn);
+       free(conn->argv);
+       free(conn);
+       return code;
+}
diff --git a/contrib/README b/contrib/README
new file mode 100644 (file)
index 0000000..05f291c
--- /dev/null
@@ -0,0 +1,43 @@
+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 (file)
index 0000000..fada5ce
--- /dev/null
@@ -0,0 +1,9 @@
+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 (executable)
index 0000000..1dec001
--- /dev/null
@@ -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 (executable)
index 0000000..58e0e53
--- /dev/null
@@ -0,0 +1,1266 @@
+#
+# bash completion support for core Git.
+#
+# Copyright (C) 2006,2007 Shawn O. Pearce <spearce@spearce.org>
+# Conceptually based on gitcompletion (http://gitweb.hawaga.org.uk/).
+# Distributed under the GNU General Public License, version 2.0.
+#
+# 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
+#    *) common --long-options
+#
+# 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.
+#
+# To submit patches:
+#
+#    *) Read Documentation/SubmittingPatches
+#    *) Send all patches to the current maintainer:
+#
+#       "Shawn O. Pearce" <spearce@spearce.org>
+#
+#    *) Always CC the Git mailing list:
+#
+#       git@vger.kernel.org
+#
+
+__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_tags ()
+{
+       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/tags ); do
+                       echo "${i#refs/tags/}"
+               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/tags/*) is_hash=y; echo "${i#refs/tags/}" ;;
+               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-attr)       : plumbing;;
+               check-ref-format) : plumbing;;
+               commit-tree)      : plumbing;;
+               cvsexportcommit)  : export;;
+               cvsimport)        : import;;
+               cvsserver)        : daemon;;
+               daemon)           : daemon;;
+               diff-files)       : plumbing;;
+               diff-index)       : plumbing;;
+               diff-tree)        : plumbing;;
+               fast-import)      : import;;
+               fsck-objects)     : plumbing;;
+               fetch--tool)      : plumbing;;
+               fetch-pack)       : plumbing;;
+               fmt-merge-msg)    : plumbing;;
+               for-each-ref)     : 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;;
+               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 --refresh"
+               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_bundle ()
+{
+       local mycword="$COMP_CWORD"
+       case "${COMP_WORDS[0]}" in
+       git)
+               local cmd="${COMP_WORDS[2]}"
+               mycword="$((mycword-1))"
+               ;;
+       git-bundle*)
+               local cmd="${COMP_WORDS[1]}"
+               ;;
+       esac
+       case "$mycword" in
+       1)
+               __gitcomp "create list-heads verify unbundle"
+               ;;
+       2)
+               # looking for a file
+               ;;
+       *)
+               case "$cmd" in
+                       create)
+                               __git_complete_revlist
+                       ;;
+               esac
+               ;;
+       esac
+}
+
+_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_describe ()
+{
+       __gitcomp "$(__git_refs)"
+}
+
+_git_diff ()
+{
+       local cur="${COMP_WORDS[COMP_CWORD]}"
+       case "$cur" in
+       --*)
+               __gitcomp "--cached --stat --numstat --shortstat --summary
+                       --patch-with-stat --name-only --name-status --color
+                       --no-color --color-words --no-renames --check
+                       --full-index --binary --abbrev --diff-filter
+                       --find-copies-harder --pickaxe-all --pickaxe-regex
+                       --text --ignore-space-at-eol --ignore-space-change
+                       --ignore-all-space --exit-code --quiet --ext-diff
+                       --no-ext-diff"
+               return
+               ;;
+       esac
+       __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
+                       --numbered-files
+                       --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 --aggressive"
+               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
+               ;;
+       --date=*)
+               __gitcomp "
+                       relative iso8601 rfc2822 short local default
+               " "" "${cur##--date=}"
+               return
+               ;;
+       --*)
+               __gitcomp "
+                       --max-count= --max-age= --since= --after=
+                       --min-age= --before= --until=
+                       --root --topo-order --date-order --reverse
+                       --no-merges --follow
+                       --abbrev-commit --abbrev=
+                       --relative-date --date=
+                       --author= --committer= --grep=
+                       --all-match
+                       --pretty= --name-status --name-only --raw
+                       --not --all
+                       --left-right --cherry-pick
+                       "
+               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_refs)" + "${cur#+}"
+                       ;;
+               *)
+                       __gitcomp "$(__git_refs)"
+                       ;;
+               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 --system --file=
+                       --list --replace-all
+                       --get --get-all --get-regexp
+                       --add --unset --unset-all
+                       --remove-section --rename-section
+                       "
+               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 skipDefaultUpdate
+                       receivepack uploadpack tagopt
+                       " "$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.loosecompression
+               core.repositoryFormatVersion
+               core.sharedRepository
+               core.warnAmbiguousRefs
+               core.compression
+               core.legacyHeaders
+               core.packedGitWindowSize
+               core.packedGitLimit
+               clean.requireForce
+               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
+               format.subjectprefix
+               gitcvs.enabled
+               gitcvs.logfile
+               gitcvs.allbinary
+               gitcvs.dbname gitcvs.dbdriver gitcvs.dbuser gitcvs.dvpass
+               gc.packrefs
+               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.tool
+               merge.summary
+               merge.verbosity
+               pack.window
+               pack.depth
+               pack.windowMemory
+               pack.compression
+               pack.deltaCacheSize
+               pack.deltaCacheLimit
+               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|update) command="$i"; break ;;
+               esac
+               c=$((++c))
+       done
+
+       if [ $c -eq $COMP_CWORD -a -z "$command" ]; then
+               __gitcomp "add show prune update"
+               return
+       fi
+
+       case "$command" in
+       show|prune)
+               __gitcomp "$(__git_remotes)"
+               ;;
+       update)
+               local i c='' IFS=$'\n'
+               for i in $(git --git-dir="$(__gitdir)" config --list); do
+                       case "$i" in
+                       remotes.*)
+                               i="${i#remotes.}"
+                               c="$c ${i/=*/}"
+                               ;;
+                       esac
+               done
+               __gitcomp "$c"
+               ;;
+       *)
+               COMPREPLY=()
+               ;;
+       esac
+}
+
+_git_reset ()
+{
+       local cur="${COMP_WORDS[COMP_CWORD]}"
+       case "$cur" in
+       --*)
+               __gitcomp "--mixed --hard --soft"
+               return
+               ;;
+       esac
+       __gitcomp "$(__git_refs)"
+}
+
+_git_shortlog ()
+{
+       local cur="${COMP_WORDS[COMP_CWORD]}"
+       case "$cur" in
+       --*)
+               __gitcomp "
+                       --max-count= --max-age= --since= --after=
+                       --min-age= --before= --until=
+                       --no-merges
+                       --author= --committer= --grep=
+                       --all-match
+                       --not --all
+                       --numbered --summary
+                       "
+               return
+               ;;
+       esac
+       __git_complete_revlist
+}
+
+_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_stash ()
+{
+       __gitcomp 'list show apply clear'
+}
+
+_git_submodule ()
+{
+       local i c=1 command
+       while [ $c -lt $COMP_CWORD ]; do
+               i="${COMP_WORDS[c]}"
+               case "$i" in
+               add|status|init|update) command="$i"; break ;;
+               esac
+               c=$((++c))
+       done
+
+       if [ $c -eq $COMP_CWORD -a -z "$command" ]; then
+               local cur="${COMP_WORDS[COMP_CWORD]}"
+               case "$cur" in
+               --*)
+                       __gitcomp "--quiet --cached"
+                       ;;
+               *)
+                       __gitcomp "add status init update"
+                       ;;
+               esac
+               return
+       fi
+}
+
+_git_tag ()
+{
+       local i c=1 f=0
+       while [ $c -lt $COMP_CWORD ]; do
+               i="${COMP_WORDS[c]}"
+               case "$i" in
+               -d|-v)
+                       __gitcomp "$(__git_tags)"
+                       return
+                       ;;
+               -f)
+                       f=1
+                       ;;
+               esac
+               c=$((++c))
+       done
+
+       case "${COMP_WORDS[COMP_CWORD-1]}" in
+       -m|-F)
+               COMPREPLY=()
+               ;;
+       -*|tag|git-tag)
+               if [ $f = 1 ]; then
+                       __gitcomp "$(__git_tags)"
+               else
+                       COMPREPLY=()
+               fi
+               ;;
+       *)
+               __gitcomp "$(__git_refs)"
+               ;;
+       esac
+}
+
+_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 "
+                       --no-pager
+                       --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 ;;
+       bundle)      _git_bundle ;;
+       branch)      _git_branch ;;
+       checkout)    _git_checkout ;;
+       cherry)      _git_cherry ;;
+       cherry-pick) _git_cherry_pick ;;
+       commit)      _git_commit ;;
+       config)      _git_config ;;
+       describe)    _git_describe ;;
+       diff)        _git_diff ;;
+       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 ;;
+       shortlog)    _git_shortlog ;;
+       show)        _git_show ;;
+       show-branch) _git_log ;;
+       stash)       _git_stash ;;
+       submodule)   _git_submodule ;;
+       tag)         _git_tag ;;
+       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_bundle git-bundle
+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_describe git-describe
+complete -o default -o nospace -F _git_diff git-diff
+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_shortlog git-shortlog
+complete -o default -o nospace -F _git_show git-show
+complete -o default -o nospace -F _git_stash git-stash
+complete -o default -o nospace -F _git_submodule git-submodule
+complete -o default -o nospace -F _git_log git-show-branch
+complete -o default -o nospace -F _git_tag git-tag
+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_bundle git-bundle.exe
+complete -o default -o nospace -F _git_cherry git-cherry.exe
+complete -o default -o nospace -F _git_describe git-describe.exe
+complete -o default -o nospace -F _git_diff git-diff.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_shortlog git-shortlog.exe
+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_tag git-tag.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 (file)
index 0000000..4009a15
--- /dev/null
@@ -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 (file)
index 0000000..b8f5a60
--- /dev/null
@@ -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/convert-objects/convert-objects.c b/contrib/convert-objects/convert-objects.c
new file mode 100644 (file)
index 0000000..90e7900
--- /dev/null
@@ -0,0 +1,329 @@
+#include "cache.h"
+#include "blob.h"
+#include "commit.h"
+#include "tree.h"
+
+struct entry {
+       unsigned char old_sha1[20];
+       unsigned char new_sha1[20];
+       int converted;
+};
+
+#define MAXOBJECTS (1000000)
+
+static struct entry *convert[MAXOBJECTS];
+static int nr_convert;
+
+static struct entry * convert_entry(unsigned char *sha1);
+
+static struct entry *insert_new(unsigned char *sha1, int pos)
+{
+       struct entry *new = xcalloc(1, sizeof(struct entry));
+       hashcpy(new->old_sha1, sha1);
+       memmove(convert + pos + 1, convert + pos, (nr_convert - pos) * sizeof(struct entry *));
+       convert[pos] = new;
+       nr_convert++;
+       if (nr_convert == MAXOBJECTS)
+               die("you're kidding me - hit maximum object limit");
+       return new;
+}
+
+static struct entry *lookup_entry(unsigned char *sha1)
+{
+       int low = 0, high = nr_convert;
+
+       while (low < high) {
+               int next = (low + high) / 2;
+               struct entry *n = convert[next];
+               int cmp = hashcmp(sha1, n->old_sha1);
+               if (!cmp)
+                       return n;
+               if (cmp < 0) {
+                       high = next;
+                       continue;
+               }
+               low = next+1;
+       }
+       return insert_new(sha1, low);
+}
+
+static void convert_binary_sha1(void *buffer)
+{
+       struct entry *entry = convert_entry(buffer);
+       hashcpy(buffer, entry->new_sha1);
+}
+
+static void convert_ascii_sha1(void *buffer)
+{
+       unsigned char sha1[20];
+       struct entry *entry;
+
+       if (get_sha1_hex(buffer, sha1))
+               die("expected sha1, got '%s'", (char*) buffer);
+       entry = convert_entry(sha1);
+       memcpy(buffer, sha1_to_hex(entry->new_sha1), 40);
+}
+
+static unsigned int convert_mode(unsigned int mode)
+{
+       unsigned int newmode;
+
+       newmode = mode & S_IFMT;
+       if (S_ISREG(mode))
+               newmode |= (mode & 0100) ? 0755 : 0644;
+       return newmode;
+}
+
+static int write_subdirectory(void *buffer, unsigned long size, const char *base, int baselen, unsigned char *result_sha1)
+{
+       char *new = xmalloc(size);
+       unsigned long newlen = 0;
+       unsigned long used;
+
+       used = 0;
+       while (size) {
+               int len = 21 + strlen(buffer);
+               char *path = strchr(buffer, ' ');
+               unsigned char *sha1;
+               unsigned int mode;
+               char *slash, *origpath;
+
+               if (!path || strtoul_ui(buffer, 8, &mode))
+                       die("bad tree conversion");
+               mode = convert_mode(mode);
+               path++;
+               if (memcmp(path, base, baselen))
+                       break;
+               origpath = path;
+               path += baselen;
+               slash = strchr(path, '/');
+               if (!slash) {
+                       newlen += sprintf(new + newlen, "%o %s", mode, path);
+                       new[newlen++] = '\0';
+                       hashcpy((unsigned char*)new + newlen, (unsigned char *) buffer + len - 20);
+                       newlen += 20;
+
+                       used += len;
+                       size -= len;
+                       buffer = (char *) buffer + len;
+                       continue;
+               }
+
+               newlen += sprintf(new + newlen, "%o %.*s", S_IFDIR, (int)(slash - path), path);
+               new[newlen++] = 0;
+               sha1 = (unsigned char *)(new + newlen);
+               newlen += 20;
+
+               len = write_subdirectory(buffer, size, origpath, slash-origpath+1, sha1);
+
+               used += len;
+               size -= len;
+               buffer = (char *) buffer + len;
+       }
+
+       write_sha1_file(new, newlen, tree_type, result_sha1);
+       free(new);
+       return used;
+}
+
+static void convert_tree(void *buffer, unsigned long size, unsigned char *result_sha1)
+{
+       void *orig_buffer = buffer;
+       unsigned long orig_size = size;
+
+       while (size) {
+               size_t len = 1+strlen(buffer);
+
+               convert_binary_sha1((char *) buffer + len);
+
+               len += 20;
+               if (len > size)
+                       die("corrupt tree object");
+               size -= len;
+               buffer = (char *) buffer + len;
+       }
+
+       write_subdirectory(orig_buffer, orig_size, "", 0, result_sha1);
+}
+
+static unsigned long parse_oldstyle_date(const char *buf)
+{
+       char c, *p;
+       char buffer[100];
+       struct tm tm;
+       const char *formats[] = {
+               "%c",
+               "%a %b %d %T",
+               "%Z",
+               "%Y",
+               " %Y",
+               NULL
+       };
+       /* We only ever did two timezones in the bad old format .. */
+       const char *timezones[] = {
+               "PDT", "PST", "CEST", NULL
+       };
+       const char **fmt = formats;
+
+       p = buffer;
+       while (isspace(c = *buf))
+               buf++;
+       while ((c = *buf++) != '\n')
+               *p++ = c;
+       *p++ = 0;
+       buf = buffer;
+       memset(&tm, 0, sizeof(tm));
+       do {
+               const char *next = strptime(buf, *fmt, &tm);
+               if (next) {
+                       if (!*next)
+                               return mktime(&tm);
+                       buf = next;
+               } else {
+                       const char **p = timezones;
+                       while (isspace(*buf))
+                               buf++;
+                       while (*p) {
+                               if (!memcmp(buf, *p, strlen(*p))) {
+                                       buf += strlen(*p);
+                                       break;
+                               }
+                               p++;
+                       }
+               }
+               fmt++;
+       } while (*buf && *fmt);
+       printf("left: %s\n", buf);
+       return mktime(&tm);
+}
+
+static int convert_date_line(char *dst, void **buf, unsigned long *sp)
+{
+       unsigned long size = *sp;
+       char *line = *buf;
+       char *next = strchr(line, '\n');
+       char *date = strchr(line, '>');
+       int len;
+
+       if (!next || !date)
+               die("missing or bad author/committer line %s", line);
+       next++; date += 2;
+
+       *buf = next;
+       *sp = size - (next - line);
+
+       len = date - line;
+       memcpy(dst, line, len);
+       dst += len;
+
+       /* Is it already in new format? */
+       if (isdigit(*date)) {
+               int datelen = next - date;
+               memcpy(dst, date, datelen);
+               return len + datelen;
+       }
+
+       /*
+        * Hacky hacky: one of the sparse old-style commits does not have
+        * any date at all, but we can fake it by using the committer date.
+        */
+       if (*date == '\n' && strchr(next, '>'))
+               date = strchr(next, '>')+2;
+
+       return len + sprintf(dst, "%lu -0700\n", parse_oldstyle_date(date));
+}
+
+static void convert_date(void *buffer, unsigned long size, unsigned char *result_sha1)
+{
+       char *new = xmalloc(size + 100);
+       unsigned long newlen = 0;
+
+       /* "tree <sha1>\n" */
+       memcpy(new + newlen, buffer, 46);
+       newlen += 46;
+       buffer = (char *) buffer + 46;
+       size -= 46;
+
+       /* "parent <sha1>\n" */
+       while (!memcmp(buffer, "parent ", 7)) {
+               memcpy(new + newlen, buffer, 48);
+               newlen += 48;
+               buffer = (char *) buffer + 48;
+               size -= 48;
+       }
+
+       /* "author xyz <xyz> date" */
+       newlen += convert_date_line(new + newlen, &buffer, &size);
+       /* "committer xyz <xyz> date" */
+       newlen += convert_date_line(new + newlen, &buffer, &size);
+
+       /* Rest */
+       memcpy(new + newlen, buffer, size);
+       newlen += size;
+
+       write_sha1_file(new, newlen, commit_type, result_sha1);
+       free(new);
+}
+
+static void convert_commit(void *buffer, unsigned long size, unsigned char *result_sha1)
+{
+       void *orig_buffer = buffer;
+       unsigned long orig_size = size;
+
+       if (memcmp(buffer, "tree ", 5))
+               die("Bad commit '%s'", (char*) buffer);
+       convert_ascii_sha1((char *) buffer + 5);
+       buffer = (char *) buffer + 46;    /* "tree " + "hex sha1" + "\n" */
+       while (!memcmp(buffer, "parent ", 7)) {
+               convert_ascii_sha1((char *) buffer + 7);
+               buffer = (char *) buffer + 48;
+       }
+       convert_date(orig_buffer, orig_size, result_sha1);
+}
+
+static struct entry * convert_entry(unsigned char *sha1)
+{
+       struct entry *entry = lookup_entry(sha1);
+       enum object_type type;
+       void *buffer, *data;
+       unsigned long size;
+
+       if (entry->converted)
+               return entry;
+       data = read_sha1_file(sha1, &type, &size);
+       if (!data)
+               die("unable to read object %s", sha1_to_hex(sha1));
+
+       buffer = xmalloc(size);
+       memcpy(buffer, data, size);
+
+       if (type == OBJ_BLOB) {
+               write_sha1_file(buffer, size, blob_type, entry->new_sha1);
+       } else if (type == OBJ_TREE)
+               convert_tree(buffer, size, entry->new_sha1);
+       else if (type == OBJ_COMMIT)
+               convert_commit(buffer, size, entry->new_sha1);
+       else
+               die("unknown object type %d in %s", type, sha1_to_hex(sha1));
+       entry->converted = 1;
+       free(buffer);
+       free(data);
+       return entry;
+}
+
+int main(int argc, char **argv)
+{
+       unsigned char sha1[20];
+       struct entry *entry;
+
+       setup_git_directory();
+
+       if (argc != 2)
+               usage("git-convert-objects <sha1>");
+       if (get_sha1(argv[1], sha1))
+               die("Not a valid object name %s", argv[1]);
+
+       entry = convert_entry(sha1);
+       printf("new sha1: %s\n", sha1_to_hex(entry->new_sha1));
+       return 0;
+}
diff --git a/contrib/convert-objects/git-convert-objects.txt b/contrib/convert-objects/git-convert-objects.txt
new file mode 100644 (file)
index 0000000..9718abf
--- /dev/null
@@ -0,0 +1,28 @@
+git-convert-objects(1)
+======================
+
+NAME
+----
+git-convert-objects - Converts old-style git repository
+
+
+SYNOPSIS
+--------
+'git-convert-objects'
+
+DESCRIPTION
+-----------
+Converts old-style git repository to the latest format
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/contrib/emacs/.gitignore b/contrib/emacs/.gitignore
new file mode 100644 (file)
index 0000000..c531d98
--- /dev/null
@@ -0,0 +1 @@
+*.elc
diff --git a/contrib/emacs/Makefile b/contrib/emacs/Makefile
new file mode 100644 (file)
index 0000000..a48540a
--- /dev/null
@@ -0,0 +1,21 @@
+## 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
+RM ?= rm -f
+
+all: $(ELC)
+
+install: all
+       $(INSTALL) -d $(DESTDIR)$(emacsdir)
+       $(INSTALL_ELC) $(ELC:.elc=.el) $(ELC) $(DESTDIR)$(emacsdir)
+
+%.elc: %.el
+       $(EMACS) -batch -f batch-byte-compile $<
+
+clean:; $(RM) $(ELC)
diff --git a/contrib/emacs/git-blame.el b/contrib/emacs/git-blame.el
new file mode 100644 (file)
index 0000000..bb671d5
--- /dev/null
@@ -0,0 +1,426 @@
+;;; 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, Emacs22 and EmacsCVS
+;;                Git 1.5 and up
+
+;; 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 or later and Git 1.5.0 and up
+;;
+;; 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:
+
+(eval-when-compile (require 'cl))                            ; to use `push', `pop'
+
+
+(defun git-blame-color-scale (&rest elements)
+  "Given a list, returns a list of triples formed with each
+elements of the list.
+
+a b => bbb bba bab baa abb aba aaa aab"
+  (let (result)
+    (dolist (a elements)
+      (dolist (b elements)
+        (dolist (c elements)
+          (setq result (cons (format "#%s%s%s" a b c) result)))))
+    result))
+
+;; (git-blame-color-scale "0c" "04" "24" "1c" "2c" "34" "14" "3c") =>
+;; ("#3c3c3c" "#3c3c14" "#3c3c34" "#3c3c2c" "#3c3c1c" "#3c3c24"
+;; "#3c3c04" "#3c3c0c" "#3c143c" "#3c1414" "#3c1434" "#3c142c" ...)
+
+(defmacro git-blame-random-pop (l)
+  "Select a random element from L and returns it. Also remove
+selected element from l."
+  ;; only works on lists with unique elements
+  `(let ((e (elt ,l (random (length ,l)))))
+     (setq ,l (remove e ,l))
+     e))
+
+(defvar git-blame-dark-colors
+  (git-blame-color-scale "0c" "04" "24" "1c" "2c" "34" "14" "3c")
+  "*List of colors (format #RGB) to use in a dark environment.
+
+To check out the list, evaluate (list-colors-display git-blame-dark-colors).")
+
+(defvar git-blame-light-colors
+  (git-blame-color-scale "c4" "d4" "cc" "dc" "f4" "e4" "fc" "ec")
+  "*List of colors (format #RGB) to use in a light environment.
+
+To check out the list, evaluate (list-colors-display git-blame-light-colors).")
+
+(defvar git-blame-colors '()
+  "Colors used by git-blame. The list is built once when activating git-blame
+minor mode.")
+
+(defvar git-blame-ancient-color "dark green"
+  "*Color to be used for ancient commit.")
+
+(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)
+
+;; FIXME: docstrings
+(defvar git-blame-file nil)
+(defvar git-blame-current nil)
+
+(defvar git-blame-mode nil)
+(make-variable-buffer-local 'git-blame-mode)
+
+(defvar git-blame-mode-line-string " blame"
+  "String to display on the mode line when git-blame is active.")
+
+(or (assq 'git-blame-mode minor-mode-alist)
+    (setq minor-mode-alist
+         (cons '(git-blame-mode git-blame-mode-line-string) minor-mode-alist)))
+
+;;;###autoload
+(defun git-blame-mode (&optional arg)
+  "Toggle minor mode for displaying Git blame
+
+With prefix ARG, turn the mode on if ARG is positive."
+  (interactive "P")
+  (cond
+   ((null arg)
+    (if git-blame-mode (git-blame-mode-off) (git-blame-mode-on)))
+   ((> (prefix-numeric-value arg) 0) (git-blame-mode-on))
+   (t (git-blame-mode-off))))
+
+(defun git-blame-mode-on ()
+  "Turn on git-blame mode.
+
+See also function `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)
+  (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))
+  (setq git-blame-mode t)
+  (git-blame-run))
+
+(defun git-blame-mode-off ()
+  "Turn off git-blame mode.
+
+See also function `git-blame-mode'."
+  (git-blame-cleanup)
+  (if git-blame-idle-timer (cancel-timer git-blame-idle-timer))
+  (setq git-blame-mode nil))
+
+;;;###autoload
+(defun git-reblame ()
+  "Recalculate all blame information in the current buffer"
+  (interactive)
+  (unless git-blame-mode
+    (error "Git-blame is not active"))
+
+  (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)
+       ;; Assign a random color to each new commit info
+       ;; Take care not to select the same color multiple times
+       (let ((color (if git-blame-colors
+                        (git-blame-random-pop git-blame-colors)
+                      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 (file)
index 0000000..e147da0
--- /dev/null
@@ -0,0 +1,1376 @@
+;;; 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
+;;  - 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)
+
+(defcustom git-show-uptodate nil
+  "Whether to display up-to-date files."
+  :group 'git
+  :type 'boolean)
+
+(defcustom git-show-ignored nil
+  "Whether to display ignored files."
+  :group 'git
+  :type 'boolean)
+
+(defcustom git-show-unknown t
+  "Whether to display unknown files."
+  :group 'git
+  :type 'boolean)
+
+
+(defface git-status-face
+  '((((class color) (background light)) (:foreground "purple"))
+    (((class color) (background dark)) (:foreground "salmon")))
+  "Git mode face used to highlight added and modified files."
+  :group 'git)
+
+(defface git-unmerged-face
+  '((((class color) (background light)) (:foreground "red" :bold t))
+    (((class color) (background dark)) (: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))
+    (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
+  "Git mode face used to highlight unknown files."
+  :group 'git)
+
+(defface git-uptodate-face
+  '((((class color) (background light)) (:foreground "grey60"))
+    (((class color) (background dark)) (:foreground "grey40")))
+  "Git mode face used to highlight up-to-date files."
+  :group 'git)
+
+(defface git-ignored-face
+  '((((class color) (background light)) (:foreground "grey60"))
+    (((class color) (background dark)) (:foreground "grey40")))
+  "Git mode face used to highlight ignored files."
+  :group 'git)
+
+(defface git-mark-face
+  '((((class color) (background light)) (:foreground "red" :bold t))
+    (((class color) (background dark)) (:foreground "tomato" :bold t)))
+  "Git mode face used for the file marks."
+  :group 'git)
+
+(defface git-header-face
+  '((((class color) (background light)) (:foreground "blue"))
+    (((class color) (background dark)) (:foreground "blue")))
+  "Git mode face used for commit headers."
+  :group 'git)
+
+(defface git-separator-face
+  '((((class color) (background light)) (:foreground "brown"))
+    (((class color) (background dark)) (:foreground "brown")))
+  "Git mode face used for commit separator."
+  :group 'git)
+
+(defface git-permission-face
+  '((((class color) (background light)) (:foreground "green" :bold t))
+    (((class color) (background dark)) (: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-region (buffer start end env &rest args)
+  "Run a git command with specified buffer region as input."
+  (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))))
+
+(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-success-message (text files)
+  "Print a success message after having handled FILES."
+  (let ((n (length files)))
+    (if (equal n 1)
+        (message "%s %s" text (car files))
+      (message "%s %d files" text n))))
+
+(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-call-process-env nil nil "update-index" "--add" "--" (file-relative-name ignore-name)))
+  (git-update-status-files (list (file-relative-name ignore-name)) 'unknown)))
+
+; 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 newval &optional oldval reason)
+  "Update a reference by calling git-update-ref."
+  (let ((args (and oldval (list oldval))))
+    (push newval args)
+    (push ref args)
+    (when reason
+     (push reason args)
+     (push "-m" args))
+    (eq 0 (apply #'git-call-process-env nil nil "update-ref" args))))
+
+(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))
+        (subject "commit (initial): ")
+        author-date log-start log-end args coding-system-for-write)
+    (when head
+      (setq subject "commit: ")
+      (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))
+                (setq subject "commit (merge): ")
+                (push "-p" args)
+                (push (match-string 1) args))))
+        (setq log-start (point-min)))
+      (setq log-end (point-max))
+      (goto-char log-start)
+      (when (re-search-forward ".*$" nil t)
+        (setq subject (concat subject (match-string 0))))
+      (setq coding-system-for-write buffer-file-coding-system))
+    (let ((commit
+           (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))))))))
+      (and (git-update-ref "HEAD" commit head subject)
+           commit))))
+
+(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-fileinfo-state (info state)
+  "Set the state of a file info."
+  (unless (eq (git-fileinfo->state info) state)
+    (setf (git-fileinfo->state info) state
+          (git-fileinfo->old-perm info) 0
+          (git-fileinfo->new-perm info) 0
+          (git-fileinfo->rename-state info) nil
+          (git-fileinfo->orig-name info) nil
+          (git-fileinfo->needs-refresh info) t)))
+
+(defun git-status-filenames-map (status func files &rest args)
+  "Apply FUNC to the status files names in the FILES list."
+  (when files
+    (setq files (sort files #'string-lessp))
+    (let ((file (pop files))
+          (node (ewoc-nth status 0)))
+      (while (and file node)
+        (let ((info (ewoc-data node)))
+          (if (string-lessp (git-fileinfo->name info) file)
+              (setq node (ewoc-next status node))
+            (if (string-equal (git-fileinfo->name info) file)
+                (apply func info args))
+            (setq file (pop files))))))))
+
+(defun git-set-filenames-state (status files state)
+  "Set the state of a list of named files."
+  (when files
+    (git-status-filenames-map status #'git-set-fileinfo-state files state)
+    (unless state  ;; delete files whose state has been set to nil
+      (ewoc-filter status (lambda (info) (git-fileinfo->state info))))))
+
+(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-insert-info-list (status infolist)
+  "Insert a list of file infos in the status buffer, replacing existing ones if any."
+  (setq infolist (sort infolist
+                       (lambda (info1 info2)
+                         (string-lessp (git-fileinfo->name info1)
+                                       (git-fileinfo->name info2)))))
+  (let ((info (pop infolist))
+        (node (ewoc-nth status 0)))
+    (while info
+      (setf (git-fileinfo->needs-refresh info) t)
+      (cond ((not node)
+             (ewoc-enter-last status info)
+             (setq info (pop infolist)))
+            ((string-lessp (git-fileinfo->name (ewoc-data node))
+                           (git-fileinfo->name info))
+             (setq node (ewoc-next status node)))
+            ((string-equal (git-fileinfo->name (ewoc-data node))
+                           (git-fileinfo->name info))
+              ;; preserve the marked flag
+              (setf (git-fileinfo->marked info) (git-fileinfo->marked (ewoc-data node)))
+              (setf (ewoc-data node) info)
+              (setq info (pop infolist)))
+            (t
+             (ewoc-enter-before status node info)
+             (setq info (pop infolist)))))))
+
+(defun git-run-diff-index (status files)
+  "Run git-diff-index on FILES and parse the results into STATUS.
+Return the list of files that haven't been handled."
+  (let (infolist)
+    (with-temp-buffer
+      (apply #'git-call-process-env t nil "diff-index" "-z" "-M" "HEAD" "--" files)
+      (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))
+                  (push (git-create-fileinfo 'added new-name old-perm new-perm 'copy name) infolist)
+                (push (git-create-fileinfo 'deleted name 0 0 'rename new-name) infolist)
+                (push (git-create-fileinfo 'added new-name old-perm new-perm 'rename name) infolist))
+            (push (git-create-fileinfo (git-state-code state) name old-perm new-perm) infolist))
+          (setq files (delete name files))
+          (when new-name (setq files (delete new-name files))))))
+    (git-insert-info-list status infolist)
+    files))
+
+(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-run-ls-files (status files default-state &rest options)
+  "Run git-ls-files on FILES and parse the results into STATUS.
+Return the list of files that haven't been handled."
+  (let (infolist)
+    (with-temp-buffer
+      (apply #'git-call-process-env t nil "ls-files" "-z" (append options (list "--") files))
+      (goto-char (point-min))
+      (while (re-search-forward "\\([^\0]*\\)\0" nil t 1)
+        (let ((name (match-string 1)))
+          (push (git-create-fileinfo default-state name) infolist)
+          (setq files (delete name files)))))
+    (git-insert-info-list status infolist)
+    files))
+
+(defun git-run-ls-unmerged (status files)
+  "Run git-ls-files -u on FILES and parse the results into STATUS."
+  (with-temp-buffer
+    (apply #'git-call-process-env t nil "ls-files" "-z" "-u" "--" files)
+    (goto-char (point-min))
+    (let (unmerged-files)
+      (while (re-search-forward "[0-7]\\{6\\} [0-9a-f]\\{40\\} [123]\t\\([^\0]+\\)\0" nil t)
+        (push (match-string 1) unmerged-files))
+      (git-set-filenames-state status unmerged-files 'unmerged))))
+
+(defun git-get-exclude-files ()
+  "Get the list of exclude files to pass to git-ls-files."
+  (let (files
+        (config (git-config "core.excludesfile")))
+    (when (file-readable-p ".git/info/exclude")
+      (push ".git/info/exclude" files))
+    (when (and config (file-readable-p config))
+      (push config files))
+    files))
+
+(defun git-run-ls-files-with-excludes (status files default-state &rest options)
+  "Run git-ls-files on FILES with appropriate --exclude-from options."
+  (let ((exclude-files (git-get-exclude-files)))
+    (apply #'git-run-ls-files status files default-state
+           (concat "--exclude-per-directory=" git-per-dir-ignore-file)
+           (append options (mapcar (lambda (f) (concat "--exclude-from=" f)) exclude-files)))))
+
+(defun git-update-status-files (files &optional default-state)
+  "Update the status of FILES from the index."
+  (unless git-status (error "Not in git-status buffer."))
+  (unless files
+    (when git-show-uptodate (git-run-ls-files git-status nil 'uptodate "-c")))
+  (let* ((remaining-files
+          (if (git-empty-db-p) ; we need some special handling for an empty db
+              (git-run-ls-files git-status files 'added "-c")
+            (git-run-diff-index git-status files))))
+    (git-run-ls-unmerged git-status files)
+    (when (or remaining-files (and git-show-unknown (not files)))
+      (setq remaining-files (git-run-ls-files-with-excludes git-status remaining-files 'unknown "-o")))
+    (when (or remaining-files (and git-show-ignored (not files)))
+      (setq remaining-files (git-run-ls-files-with-excludes git-status remaining-files 'ignored "-o" "-i")))
+    (git-set-filenames-state git-status remaining-files default-state)
+    (git-refresh-files)
+    (git-refresh-ewoc-hf git-status)))
+
+(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 branch
+                             (if (string-match "^refs/heads/" branch)
+                                 (substring branch (match-end 0))
+                               branch)
+                           "none (detached HEAD)")
+                         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-call-process-env nil env "update-index" "--add" "--" (git-get-filenames added)))
+    (when deleted
+      (apply #'git-call-process-env nil env "update-index" "--remove" "--" (git-get-filenames deleted)))
+    (when modified
+      (apply #'git-call-process-env 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
+                    (message "Running git commit...")
+                    (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)))
+                            (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))
+                            (dolist (info files) (git-set-fileinfo-state info 'uptodate))
+                            (git-call-process-env nil nil "rerere")
+                            (git-call-process-env nil nil "gc" "--auto")
+                            (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) (unless (git-fileinfo->marked info)
+                             (setf (git-fileinfo->marked info) 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) (when (git-fileinfo->marked 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-get-filenames (git-marked-files-state 'unknown 'ignored))))
+    (unless files
+      (push (file-relative-name (read-file-name "File to add: " nil nil t)) files))
+    (apply #'git-call-process-env nil nil "update-index" "--add" "--" files)
+    (git-update-status-files files 'uptodate)
+    (git-success-message "Added" files)))
+
+(defun git-ignore-file ()
+  "Add marked file(s) to the ignore list."
+  (interactive)
+  (let ((files (git-get-filenames (git-marked-files-state 'unknown))))
+    (unless files
+      (push (file-relative-name (read-file-name "File to ignore: " nil nil t)) files))
+    (dolist (f files) (git-append-to-ignore f))
+    (git-update-status-files files 'ignored)
+    (git-success-message "Ignored" files)))
+
+(defun git-remove-file ()
+  "Remove the marked file(s)."
+  (interactive)
+  (let ((files (git-get-filenames (git-marked-files-state 'added 'modified 'unknown 'uptodate 'ignored))))
+    (unless files
+      (push (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 (name files)
+            (when (file-exists-p name) (delete-file name)))
+          (apply #'git-call-process-env nil nil "update-index" "--remove" "--" files)
+          (git-update-status-files files nil)
+          (git-success-message "Removed" files))
+      (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 (git-fileinfo->name info) added))
+          ('deleted (push (git-fileinfo->name info) modified))
+          ('unmerged (push (git-fileinfo->name info) modified))
+          ('modified (push (git-fileinfo->name info) modified))))
+      (when added
+        (apply #'git-call-process-env nil nil "update-index" "--force-remove" "--" added))
+      (when modified
+        (apply #'git-call-process-env nil nil "checkout" "HEAD" modified))
+      (git-update-status-files (append added modified) 'uptodate)
+      (git-success-message "Reverted" (git-get-filenames files)))))
+
+(defun git-resolve-file ()
+  "Resolve conflicts in marked file(s)."
+  (interactive)
+  (let ((files (git-get-filenames (git-marked-files-state 'unmerged))))
+    (when files
+      (apply #'git-call-process-env nil nil "update-index" "--" files)
+      (git-update-status-files files 'uptodate)
+      (git-success-message "Resolved" files))))
+
+(defun git-remove-handled ()
+  "Remove handled files from the status list."
+  (interactive)
+  (ewoc-filter git-status
+               (lambda (info)
+                 (case (git-fileinfo->state info)
+                   ('ignored git-show-ignored)
+                   ('uptodate git-show-uptodate)
+                   ('unknown git-show-unknown)
+                   (t t))))
+  (unless (ewoc-nth git-status 0)  ; refresh header if list is empty
+    (git-refresh-ewoc-hf git-status)))
+
+(defun git-toggle-show-uptodate ()
+  "Toogle the option for showing up-to-date files."
+  (interactive)
+  (if (setq git-show-uptodate (not git-show-uptodate))
+      (git-refresh-status)
+    (git-remove-handled)))
+
+(defun git-toggle-show-ignored ()
+  "Toogle the option for showing ignored files."
+  (interactive)
+  (if (setq git-show-ignored (not git-show-ignored))
+      (progn
+        (message "Inserting ignored files...")
+        (git-run-ls-files-with-excludes git-status nil 'ignored "-o" "-i")
+        (git-refresh-files)
+        (git-refresh-ewoc-hf git-status)
+        (message "Inserting ignored files...done"))
+    (git-remove-handled)))
+
+(defun git-toggle-show-unknown ()
+  "Toogle the option for showing unknown files."
+  (interactive)
+  (if (setq git-show-unknown (not git-show-unknown))
+      (progn
+        (message "Inserting unknown files...")
+        (git-run-ls-files-with-excludes git-status nil 'unknown "-o")
+        (git-refresh-files)
+        (git-refresh-ewoc-hf git-status)
+        (message "Inserting unknown files...done"))
+    (git-remove-handled)))
+
+(defun git-setup-diff-buffer (buffer)
+  "Setup a buffer for displaying a diff."
+  (let ((dir default-directory))
+    (with-current-buffer buffer
+      (diff-mode)
+      (goto-char (point-min))
+      (setq default-directory dir)
+      (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)
+  (let ((files (git-marked-files-state 'added 'deleted 'modified)))
+    (unless (eq 1 (length files))
+      (error "Cannot perform an interactive diff on multiple files."))
+    (let* ((filename (car (git-get-filenames files)))
+           (buff1 (find-file-noselect filename))
+           (buff2 (git-run-command-buffer (concat filename ".~HEAD~") "cat-file" "blob" (concat "HEAD:" filename))))
+      (ediff-buffers buff1 buff2))))
+
+(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 1))))
+
+(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))
+         (marked-files (git-get-filenames (ewoc-collect status (lambda (info) (git-fileinfo->marked info)))))
+         (cur-name (and pos (git-fileinfo->name (ewoc-data pos)))))
+    (unless status (error "Not in git-status buffer."))
+    (message "Refreshing git status...")
+    (git-call-process-env nil nil "update-index" "--refresh")
+    (git-clear-status status)
+    (git-update-status-files nil)
+    ; restore file marks
+    (when marked-files
+      (git-status-filenames-map status
+                                (lambda (info)
+                                        (setf (git-fileinfo->marked info) t)
+                                        (setf (git-fileinfo->needs-refresh info) t))
+                                marked-files)
+      (git-refresh-files))
+    ; move point to the current file name if any
+    (message "Refreshing git status...done")
+    (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))
+        (toggle-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"    toggle-map)
+    (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)
+    ; the toggle submap
+    (define-key toggle-map "u" 'git-toggle-show-uptodate)
+    (define-key toggle-map "i" 'git-toggle-show-ignored)
+    (define-key toggle-map "k" 'git-toggle-show-unknown)
+    (define-key toggle-map "m" 'git-toggle-all-marks)
+    (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)
+  (make-local-variable 'git-show-uptodate)
+  (make-local-variable 'git-show-ignored)
+  (make-local-variable 'git-show-unknown)
+  (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))
+        (add-hook 'after-save-hook 'git-update-saved-file))
+    (message "%s is not a git working tree." dir)))
+
+(defun git-update-saved-file ()
+  "Update the corresponding git-status buffer when a file is saved.
+Meant to be used in `after-save-hook'."
+  (let* ((file (expand-file-name buffer-file-name))
+         (dir (condition-case nil (git-get-top-dir (file-name-directory file)) (error nil)))
+         (buffer (and dir (git-find-status-buffer dir))))
+    (when buffer
+      (with-current-buffer buffer
+        (let ((filename (file-relative-name file dir)))
+          ; skip files located inside the .git directory
+          (unless (string-match "^\\.git/" filename)
+            (git-call-process-env nil nil "add" "--refresh" "--" filename)
+            (git-update-status-files (list filename) 'uptodate)))))))
+
+(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 (file)
index 0000000..b8f6be5
--- /dev/null
@@ -0,0 +1,216 @@
+;;; 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-symbolic-commit (commit)
+  "Translate COMMIT string into symbolic form.
+Returns nil if not possible."
+  (and commit
+       (with-temp-buffer
+        (and
+         (zerop
+          (call-process "git" nil '(t nil) nil "name-rev"
+                        "--name-only" "--tags"
+                        commit))
+         (goto-char (point-min))
+         (= (forward-line 2) 1)
+         (bolp)
+         (buffer-substring-no-properties (point-min) (1- (point-max)))))))
+
+(defun vc-git-previous-version (file rev)
+  "git-specific version of `vc-previous-version'."
+  (let ((default-directory (file-name-directory (expand-file-name file)))
+       (file (file-name-nondirectory file)))
+    (vc-git-symbolic-commit
+     (with-temp-buffer
+       (and
+       (zerop
+        (call-process "git" nil '(t nil) nil "rev-list"
+                      "-2" rev "--" file))
+       (goto-char (point-max))
+       (bolp)
+       (zerop (forward-line -1))
+       (not (bobp))
+       (buffer-substring-no-properties
+          (point)
+          (1- (point-max))))))))
+
+(defun vc-git-next-version (file rev)
+  "git-specific version of `vc-next-version'."
+  (let* ((default-directory (file-name-directory
+                            (expand-file-name file)))
+       (file (file-name-nondirectory file))
+       (current-rev
+        (with-temp-buffer
+          (and
+           (zerop
+            (call-process "git" nil '(t nil) nil "rev-list"
+                          "-1" rev "--" file))
+           (goto-char (point-max))
+           (bolp)
+           (zerop (forward-line -1))
+           (bobp)
+           (buffer-substring-no-properties
+            (point)
+            (1- (point-max)))))))
+    (and current-rev
+        (vc-git-symbolic-commit
+         (with-temp-buffer
+           (and
+            (zerop
+             (call-process "git" nil '(t nil) nil "rev-list"
+                           "HEAD" "--" file))
+            (goto-char (point-min))
+            (search-forward current-rev nil t)
+            (zerop (forward-line -1))
+            (buffer-substring-no-properties
+             (point)
+             (progn (forward-line 1) (1- (point))))))))))
+
+(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-clean.sh b/contrib/examples/git-clean.sh
new file mode 100755 (executable)
index 0000000..01c95e9
--- /dev/null
@@ -0,0 +1,118 @@
+#!/bin/sh
+#
+# Copyright (c) 2005-2006 Pavel Roskin
+#
+
+OPTIONS_KEEPDASHDASH=
+OPTIONS_SPEC="\
+git-clean [options] <paths>...
+
+Clean untracked files from the working directory
+
+When optional <paths>... arguments are given, the paths
+affected are further limited to those that match them.
+--
+d remove directories as well
+f override clean.requireForce and clean anyway
+n don't remove anything, just show what would be done
+q be quiet, only report errors
+x remove ignored files as well
+X remove only ignored files"
+
+SUBDIRECTORY_OK=Yes
+. git-sh-setup
+require_work_tree
+
+ignored=
+ignoredonly=
+cleandir=
+rmf="rm -f --"
+rmrf="rm -rf --"
+rm_refuse="echo Not removing"
+echo1="echo"
+
+disabled=$(git config --bool clean.requireForce)
+
+while test $# != 0
+do
+       case "$1" in
+       -d)
+               cleandir=1
+               ;;
+       -f)
+               disabled=false
+               ;;
+       -n)
+               disabled=false
+               rmf="echo Would remove"
+               rmrf="echo Would remove"
+               rm_refuse="echo Would not remove"
+               echo1=":"
+               ;;
+       -q)
+               echo1=":"
+               ;;
+       -x)
+               ignored=1
+               ;;
+       -X)
+               ignoredonly=1
+               ;;
+       --)
+               shift
+               break
+               ;;
+       *)
+               usage # should not happen
+               ;;
+       esac
+       shift
+done
+
+# requireForce used to default to false but now it defaults to true.
+# IOW, lack of explicit "clean.requireForce = false" is taken as
+# "clean.requireForce = true".
+case "$disabled" in
+"")
+       die "clean.requireForce not set and -n or -f not given; refusing to clean"
+       ;;
+"true")
+       die "clean.requireForce set and -n or -f not given; refusing to clean"
+       ;;
+esac
+
+if [ "$ignored,$ignoredonly" = "1,1" ]; then
+       die "-x and -X cannot be set together"
+fi
+
+if [ -z "$ignored" ]; then
+       excl="--exclude-per-directory=.gitignore"
+       excl_info= excludes_file=
+       if [ -f "$GIT_DIR/info/exclude" ]; then
+               excl_info="--exclude-from=$GIT_DIR/info/exclude"
+       fi
+       if cfg_excl=$(git config core.excludesfile) && test -f "$cfg_excl"
+       then
+               excludes_file="--exclude-from=$cfg_excl"
+       fi
+       if [ "$ignoredonly" ]; then
+               excl="$excl --ignored"
+       fi
+fi
+
+git ls-files --others --directory \
+       $excl ${excl_info:+"$excl_info"} ${excludes_file:+"$excludes_file"} \
+       -- "$@" |
+while read -r file; do
+       if [ -d "$file" -a ! -L "$file" ]; then
+               if [ -z "$cleandir" ]; then
+                       $rm_refuse "$file"
+                       continue
+               fi
+               $echo1 "Removing $file"
+               $rmrf "$file"
+       else
+               $echo1 "Removing $file"
+               $rmf "$file"
+       fi
+done
diff --git a/contrib/examples/git-commit.sh b/contrib/examples/git-commit.sh
new file mode 100755 (executable)
index 0000000..2c4a406
--- /dev/null
@@ -0,0 +1,639 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Linus Torvalds
+# Copyright (c) 2006 Junio C Hamano
+
+USAGE='[-a | --interactive] [-s] [-v] [--no-verify] [-m <message> | -F <logfile> | (-C|-c) <commit> | --amend] [-u] [-e] [--author <author>] [--template <file>] [[-i | -o] <path>...]'
+SUBDIRECTORY_OK=Yes
+OPTIONS_SPEC=
+. git-sh-setup
+require_work_tree
+
+git rev-parse --verify HEAD >/dev/null 2>&1 || initial_commit=t
+
+case "$0" in
+*status)
+       status_only=t
+       ;;
+*commit)
+       status_only=
+       ;;
+esac
+
+refuse_partial () {
+       echo >&2 "$1"
+       echo >&2 "You might have meant to say 'git commit -i paths...', perhaps?"
+       exit 1
+}
+
+TMP_INDEX=
+THIS_INDEX="${GIT_INDEX_FILE:-$GIT_DIR/index}"
+NEXT_INDEX="$GIT_DIR/next-index$$"
+rm -f "$NEXT_INDEX"
+save_index () {
+       cp -p "$THIS_INDEX" "$NEXT_INDEX"
+}
+
+run_status () {
+       # If TMP_INDEX is defined, that means we are doing
+       # "--only" partial commit, and that index file is used
+       # to build the tree for the commit.  Otherwise, if
+       # NEXT_INDEX exists, that is the index file used to
+       # make the commit.  Otherwise we are using as-is commit
+       # so the regular index file is what we use to compare.
+       if test '' != "$TMP_INDEX"
+       then
+               GIT_INDEX_FILE="$TMP_INDEX"
+               export GIT_INDEX_FILE
+       elif test -f "$NEXT_INDEX"
+       then
+               GIT_INDEX_FILE="$NEXT_INDEX"
+               export GIT_INDEX_FILE
+       fi
+
+       if test "$status_only" = "t" -o "$use_status_color" = "t"; then
+               color=
+       else
+               color=--nocolor
+       fi
+       git runstatus ${color} \
+               ${verbose:+--verbose} \
+               ${amend:+--amend} \
+               ${untracked_files:+--untracked}
+}
+
+trap '
+       test -z "$TMP_INDEX" || {
+               test -f "$TMP_INDEX" && rm -f "$TMP_INDEX"
+       }
+       rm -f "$NEXT_INDEX"
+' 0
+
+################################################################
+# Command line argument parsing and sanity checking
+
+all=
+also=
+allow_empty=f
+interactive=
+only=
+logfile=
+use_commit=
+amend=
+edit_flag=
+no_edit=
+log_given=
+log_message=
+verify=t
+quiet=
+verbose=
+signoff=
+force_author=
+only_include_assumed=
+untracked_files=
+templatefile="`git config commit.template`"
+while test $# != 0
+do
+       case "$1" in
+       -F|--F|-f|--f|--fi|--fil|--file)
+               case "$#" in 1) usage ;; esac
+               shift
+               no_edit=t
+               log_given=t$log_given
+               logfile="$1"
+               ;;
+       -F*|-f*)
+               no_edit=t
+               log_given=t$log_given
+               logfile="${1#-[Ff]}"
+               ;;
+       --F=*|--f=*|--fi=*|--fil=*|--file=*)
+               no_edit=t
+               log_given=t$log_given
+               logfile="${1#*=}"
+               ;;
+       -a|--a|--al|--all)
+               all=t
+               ;;
+       --allo|--allow|--allow-|--allow-e|--allow-em|--allow-emp|\
+       --allow-empt|--allow-empty)
+               allow_empty=t
+               ;;
+       --au=*|--aut=*|--auth=*|--autho=*|--author=*)
+               force_author="${1#*=}"
+               ;;
+       --au|--aut|--auth|--autho|--author)
+               case "$#" in 1) usage ;; esac
+               shift
+               force_author="$1"
+               ;;
+       -e|--e|--ed|--edi|--edit)
+               edit_flag=t
+               ;;
+       -i|--i|--in|--inc|--incl|--inclu|--includ|--include)
+               also=t
+               ;;
+       --int|--inte|--inter|--intera|--interac|--interact|--interacti|\
+       --interactiv|--interactive)
+               interactive=t
+               ;;
+       -o|--o|--on|--onl|--only)
+               only=t
+               ;;
+       -m|--m|--me|--mes|--mess|--messa|--messag|--message)
+               case "$#" in 1) usage ;; esac
+               shift
+               log_given=m$log_given
+               log_message="${log_message:+${log_message}
+
+}$1"
+               no_edit=t
+               ;;
+       -m*)
+               log_given=m$log_given
+               log_message="${log_message:+${log_message}
+
+}${1#-m}"
+               no_edit=t
+               ;;
+       --m=*|--me=*|--mes=*|--mess=*|--messa=*|--messag=*|--message=*)
+               log_given=m$log_given
+               log_message="${log_message:+${log_message}
+
+}${1#*=}"
+               no_edit=t
+               ;;
+       -n|--n|--no|--no-|--no-v|--no-ve|--no-ver|--no-veri|--no-verif|\
+       --no-verify)
+               verify=
+               ;;
+       --a|--am|--ame|--amen|--amend)
+               amend=t
+               use_commit=HEAD
+               ;;
+       -c)
+               case "$#" in 1) usage ;; esac
+               shift
+               log_given=t$log_given
+               use_commit="$1"
+               no_edit=
+               ;;
+       --ree=*|--reed=*|--reedi=*|--reedit=*|--reedit-=*|--reedit-m=*|\
+       --reedit-me=*|--reedit-mes=*|--reedit-mess=*|--reedit-messa=*|\
+       --reedit-messag=*|--reedit-message=*)
+               log_given=t$log_given
+               use_commit="${1#*=}"
+               no_edit=
+               ;;
+       --ree|--reed|--reedi|--reedit|--reedit-|--reedit-m|--reedit-me|\
+       --reedit-mes|--reedit-mess|--reedit-messa|--reedit-messag|\
+       --reedit-message)
+               case "$#" in 1) usage ;; esac
+               shift
+               log_given=t$log_given
+               use_commit="$1"
+               no_edit=
+               ;;
+       -C)
+               case "$#" in 1) usage ;; esac
+               shift
+               log_given=t$log_given
+               use_commit="$1"
+               no_edit=t
+               ;;
+       --reu=*|--reus=*|--reuse=*|--reuse-=*|--reuse-m=*|--reuse-me=*|\
+       --reuse-mes=*|--reuse-mess=*|--reuse-messa=*|--reuse-messag=*|\
+       --reuse-message=*)
+               log_given=t$log_given
+               use_commit="${1#*=}"
+               no_edit=t
+               ;;
+       --reu|--reus|--reuse|--reuse-|--reuse-m|--reuse-me|--reuse-mes|\
+       --reuse-mess|--reuse-messa|--reuse-messag|--reuse-message)
+               case "$#" in 1) usage ;; esac
+               shift
+               log_given=t$log_given
+               use_commit="$1"
+               no_edit=t
+               ;;
+       -s|--s|--si|--sig|--sign|--signo|--signof|--signoff)
+               signoff=t
+               ;;
+       -t|--t|--te|--tem|--temp|--templ|--templa|--templat|--template)
+               case "$#" in 1) usage ;; esac
+               shift
+               templatefile="$1"
+               no_edit=
+               ;;
+       -q|--q|--qu|--qui|--quie|--quiet)
+               quiet=t
+               ;;
+       -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
+               verbose=t
+               ;;
+       -u|--u|--un|--unt|--untr|--untra|--untrac|--untrack|--untracke|\
+       --untracked|--untracked-|--untracked-f|--untracked-fi|--untracked-fil|\
+       --untracked-file|--untracked-files)
+               untracked_files=t
+               ;;
+       --)
+               shift
+               break
+               ;;
+       -*)
+               usage
+               ;;
+       *)
+               break
+               ;;
+       esac
+       shift
+done
+case "$edit_flag" in t) no_edit= ;; esac
+
+################################################################
+# Sanity check options
+
+case "$amend,$initial_commit" in
+t,t)
+       die "You do not have anything to amend." ;;
+t,)
+       if [ -f "$GIT_DIR/MERGE_HEAD" ]; then
+               die "You are in the middle of a merge -- cannot amend."
+       fi ;;
+esac
+
+case "$log_given" in
+tt*)
+       die "Only one of -c/-C/-F can be used." ;;
+*tm*|*mt*)
+       die "Option -m cannot be combined with -c/-C/-F." ;;
+esac
+
+case "$#,$also,$only,$amend" in
+*,t,t,*)
+       die "Only one of --include/--only can be used." ;;
+0,t,,* | 0,,t,)
+       die "No paths with --include/--only does not make sense." ;;
+0,,t,t)
+       only_include_assumed="# Clever... amending the last one with dirty index." ;;
+0,,,*)
+       ;;
+*,,,*)
+       only_include_assumed="# Explicit paths specified without -i nor -o; assuming --only paths..."
+       also=
+       ;;
+esac
+unset only
+case "$all,$interactive,$also,$#" in
+*t,*t,*)
+       die "Cannot use -a, --interactive or -i at the same time." ;;
+t,,,[1-9]*)
+       die "Paths with -a does not make sense." ;;
+,t,,[1-9]*)
+       die "Paths with --interactive does not make sense." ;;
+,,t,0)
+       die "No paths with -i does not make sense." ;;
+esac
+
+if test ! -z "$templatefile" -a -z "$log_given"
+then
+       if test ! -f "$templatefile"
+       then
+               die "Commit template file does not exist."
+       fi
+fi
+
+################################################################
+# Prepare index to have a tree to be committed
+
+case "$all,$also" in
+t,)
+       if test ! -f "$THIS_INDEX"
+       then
+               die 'nothing to commit (use "git add file1 file2" to include for commit)'
+       fi
+       save_index &&
+       (
+               cd_to_toplevel &&
+               GIT_INDEX_FILE="$NEXT_INDEX" &&
+               export GIT_INDEX_FILE &&
+               git diff-files --name-only -z |
+               git update-index --remove -z --stdin
+       ) || exit
+       ;;
+,t)
+       save_index &&
+       git ls-files --error-unmatch -- "$@" >/dev/null || exit
+
+       git diff-files --name-only -z -- "$@"  |
+       (
+               cd_to_toplevel &&
+               GIT_INDEX_FILE="$NEXT_INDEX" &&
+               export GIT_INDEX_FILE &&
+               git update-index --remove -z --stdin
+       ) || exit
+       ;;
+,)
+       if test "$interactive" = t; then
+               git add --interactive || exit
+       fi
+       case "$#" in
+       0)
+               ;; # commit as-is
+       *)
+               if test -f "$GIT_DIR/MERGE_HEAD"
+               then
+                       refuse_partial "Cannot do a partial commit during a merge."
+               fi
+
+               TMP_INDEX="$GIT_DIR/tmp-index$$"
+               W=
+               test -z "$initial_commit" && W=--with-tree=HEAD
+               commit_only=`git ls-files --error-unmatch $W -- "$@"` || exit
+
+               # Build a temporary index and update the real index
+               # the same way.
+               if test -z "$initial_commit"
+               then
+                       GIT_INDEX_FILE="$THIS_INDEX" \
+                       git read-tree --index-output="$TMP_INDEX" -i -m HEAD
+               else
+                       rm -f "$TMP_INDEX"
+               fi || exit
+
+               printf '%s\n' "$commit_only" |
+               GIT_INDEX_FILE="$TMP_INDEX" \
+               git update-index --add --remove --stdin &&
+
+               save_index &&
+               printf '%s\n' "$commit_only" |
+               (
+                       GIT_INDEX_FILE="$NEXT_INDEX"
+                       export GIT_INDEX_FILE
+                       git update-index --add --remove --stdin
+               ) || exit
+               ;;
+       esac
+       ;;
+esac
+
+################################################################
+# If we do as-is commit, the index file will be THIS_INDEX,
+# otherwise NEXT_INDEX after we make this commit.  We leave
+# the index as is if we abort.
+
+if test -f "$NEXT_INDEX"
+then
+       USE_INDEX="$NEXT_INDEX"
+else
+       USE_INDEX="$THIS_INDEX"
+fi
+
+case "$status_only" in
+t)
+       # This will silently fail in a read-only repository, which is
+       # what we want.
+       GIT_INDEX_FILE="$USE_INDEX" git update-index -q --unmerged --refresh
+       run_status
+       exit $?
+       ;;
+'')
+       GIT_INDEX_FILE="$USE_INDEX" git update-index -q --refresh || exit
+       ;;
+esac
+
+################################################################
+# Grab commit message, write out tree and make commit.
+
+if test t = "$verify" && test -x "$GIT_DIR"/hooks/pre-commit
+then
+    GIT_INDEX_FILE="${TMP_INDEX:-${USE_INDEX}}" "$GIT_DIR"/hooks/pre-commit \
+    || exit
+fi
+
+if test "$log_message" != ''
+then
+       printf '%s\n' "$log_message"
+elif test "$logfile" != ""
+then
+       if test "$logfile" = -
+       then
+               test -t 0 &&
+               echo >&2 "(reading log message from standard input)"
+               cat
+       else
+               cat <"$logfile"
+       fi
+elif test "$use_commit" != ""
+then
+       encoding=$(git config i18n.commitencoding || echo UTF-8)
+       git show -s --pretty=raw --encoding="$encoding" "$use_commit" |
+       sed -e '1,/^$/d' -e 's/^    //'
+elif test -f "$GIT_DIR/MERGE_MSG"
+then
+       cat "$GIT_DIR/MERGE_MSG"
+elif test -f "$GIT_DIR/SQUASH_MSG"
+then
+       cat "$GIT_DIR/SQUASH_MSG"
+elif test "$templatefile" != ""
+then
+       cat "$templatefile"
+fi | git stripspace >"$GIT_DIR"/COMMIT_EDITMSG
+
+case "$signoff" in
+t)
+       sign=$(git-var GIT_COMMITTER_IDENT | sed -e '
+               s/>.*/>/
+               s/^/Signed-off-by: /
+               ')
+       blank_before_signoff=
+       tail -n 1 "$GIT_DIR"/COMMIT_EDITMSG |
+       grep 'Signed-off-by:' >/dev/null || blank_before_signoff='
+'
+       tail -n 1 "$GIT_DIR"/COMMIT_EDITMSG |
+       grep "$sign"$ >/dev/null ||
+       printf '%s%s\n' "$blank_before_signoff" "$sign" \
+               >>"$GIT_DIR"/COMMIT_EDITMSG
+       ;;
+esac
+
+if test -f "$GIT_DIR/MERGE_HEAD" && test -z "$no_edit"; then
+       echo "#"
+       echo "# It looks like you may be committing a MERGE."
+       echo "# If this is not correct, please remove the file"
+       printf '%s\n' "#        $GIT_DIR/MERGE_HEAD"
+       echo "# and try again"
+       echo "#"
+fi >>"$GIT_DIR"/COMMIT_EDITMSG
+
+# Author
+if test '' != "$use_commit"
+then
+       eval "$(get_author_ident_from_commit "$use_commit")"
+       export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
+fi
+if test '' != "$force_author"
+then
+       GIT_AUTHOR_NAME=`expr "z$force_author" : 'z\(.*[^ ]\) *<.*'` &&
+       GIT_AUTHOR_EMAIL=`expr "z$force_author" : '.*\(<.*\)'` &&
+       test '' != "$GIT_AUTHOR_NAME" &&
+       test '' != "$GIT_AUTHOR_EMAIL" ||
+       die "malformed --author parameter"
+       export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+fi
+
+PARENTS="-p HEAD"
+if test -z "$initial_commit"
+then
+       rloga='commit'
+       if [ -f "$GIT_DIR/MERGE_HEAD" ]; then
+               rloga='commit (merge)'
+               PARENTS="-p HEAD "`sed -e 's/^/-p /' "$GIT_DIR/MERGE_HEAD"`
+       elif test -n "$amend"; then
+               rloga='commit (amend)'
+               PARENTS=$(git cat-file commit HEAD |
+                       sed -n -e '/^$/q' -e 's/^parent /-p /p')
+       fi
+       current="$(git rev-parse --verify HEAD)"
+else
+       if [ -z "$(git ls-files)" ]; then
+               echo >&2 'nothing to commit (use "git add file1 file2" to include for commit)'
+               exit 1
+       fi
+       PARENTS=""
+       rloga='commit (initial)'
+       current=''
+fi
+set_reflog_action "$rloga"
+
+if test -z "$no_edit"
+then
+       {
+               echo ""
+               echo "# Please enter the commit message for your changes."
+               echo "# (Comment lines starting with '#' will not be included)"
+               test -z "$only_include_assumed" || echo "$only_include_assumed"
+               run_status
+       } >>"$GIT_DIR"/COMMIT_EDITMSG
+else
+       # we need to check if there is anything to commit
+       run_status >/dev/null
+fi
+case "$allow_empty,$?,$PARENTS" in
+t,* | ?,0,* | ?,*,-p' '?*-p' '?*)
+       # an explicit --allow-empty, or a merge commit can record the
+       # same tree as its parent.  Otherwise having commitable paths
+       # is required.
+       ;;
+*)
+       rm -f "$GIT_DIR/COMMIT_EDITMSG" "$GIT_DIR/SQUASH_MSG"
+       use_status_color=t
+       run_status
+       exit 1
+esac
+
+case "$no_edit" in
+'')
+       git-var GIT_AUTHOR_IDENT > /dev/null  || die
+       git-var GIT_COMMITTER_IDENT > /dev/null  || die
+       git_editor "$GIT_DIR/COMMIT_EDITMSG"
+       ;;
+esac
+
+case "$verify" in
+t)
+       if test -x "$GIT_DIR"/hooks/commit-msg
+       then
+               "$GIT_DIR"/hooks/commit-msg "$GIT_DIR"/COMMIT_EDITMSG || exit
+       fi
+esac
+
+if test -z "$no_edit"
+then
+    sed -e '
+        /^diff --git a\/.*/{
+           s///
+           q
+       }
+       /^#/d
+    ' "$GIT_DIR"/COMMIT_EDITMSG
+else
+    cat "$GIT_DIR"/COMMIT_EDITMSG
+fi |
+git stripspace >"$GIT_DIR"/COMMIT_MSG
+
+# Test whether the commit message has any content we didn't supply.
+have_commitmsg=
+grep -v -i '^Signed-off-by' "$GIT_DIR"/COMMIT_MSG |
+       git stripspace > "$GIT_DIR"/COMMIT_BAREMSG
+
+# Is the commit message totally empty?
+if test -s "$GIT_DIR"/COMMIT_BAREMSG
+then
+       if test "$templatefile" != ""
+       then
+               # Test whether this is just the unaltered template.
+               if cnt=`sed -e '/^#/d' < "$templatefile" |
+                       git stripspace |
+                       diff "$GIT_DIR"/COMMIT_BAREMSG - |
+                       wc -l` &&
+                  test 0 -lt $cnt
+               then
+                       have_commitmsg=t
+               fi
+       else
+               # No template, so the content in the commit message must
+               # have come from the user.
+               have_commitmsg=t
+       fi
+fi
+
+rm -f "$GIT_DIR"/COMMIT_BAREMSG
+
+if test "$have_commitmsg" = "t"
+then
+       if test -z "$TMP_INDEX"
+       then
+               tree=$(GIT_INDEX_FILE="$USE_INDEX" git write-tree)
+       else
+               tree=$(GIT_INDEX_FILE="$TMP_INDEX" git write-tree) &&
+               rm -f "$TMP_INDEX"
+       fi &&
+       commit=$(git commit-tree $tree $PARENTS <"$GIT_DIR/COMMIT_MSG") &&
+       rlogm=$(sed -e 1q "$GIT_DIR"/COMMIT_MSG) &&
+       git update-ref -m "$GIT_REFLOG_ACTION: $rlogm" HEAD $commit "$current" &&
+       rm -f -- "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/MERGE_MSG" &&
+       if test -f "$NEXT_INDEX"
+       then
+               mv "$NEXT_INDEX" "$THIS_INDEX"
+       else
+               : ;# happy
+       fi
+else
+       echo >&2 "* no commit message?  aborting commit."
+       false
+fi
+ret="$?"
+rm -f "$GIT_DIR/COMMIT_MSG" "$GIT_DIR/COMMIT_EDITMSG" "$GIT_DIR/SQUASH_MSG"
+
+cd_to_toplevel
+
+git rerere
+
+if test "$ret" = 0
+then
+       git gc --auto
+       if test -x "$GIT_DIR"/hooks/post-commit
+       then
+               "$GIT_DIR"/hooks/post-commit
+       fi
+       if test -z "$quiet"
+       then
+               commit=`git diff-tree --always --shortstat --pretty="format:%h: %s"\
+                      --summary --root HEAD --`
+               echo "Created${initial_commit:+ initial} commit $commit"
+       fi
+fi
+
+exit "$ret"
diff --git a/contrib/examples/git-fetch.sh b/contrib/examples/git-fetch.sh
new file mode 100755 (executable)
index 0000000..e44af2c
--- /dev/null
@@ -0,0 +1,377 @@
+#!/bin/sh
+#
+
+USAGE='<fetch-options> <repository> <refspec>...'
+SUBDIRECTORY_OK=Yes
+. git-sh-setup
+set_reflog_action "fetch $*"
+cd_to_toplevel ;# probably unnecessary...
+
+. git-parse-remote
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+
+LF='
+'
+IFS="$LF"
+
+no_tags=
+tags=
+append=
+force=
+verbose=
+update_head_ok=
+exec=
+keep=
+shallow_depth=
+no_progress=
+test -t 1 || no_progress=--no-progress
+quiet=
+while test $# != 0
+do
+       case "$1" in
+       -a|--a|--ap|--app|--appe|--appen|--append)
+               append=t
+               ;;
+       --upl|--uplo|--uploa|--upload|--upload-|--upload-p|\
+       --upload-pa|--upload-pac|--upload-pack)
+               shift
+               exec="--upload-pack=$1"
+               ;;
+       --upl=*|--uplo=*|--uploa=*|--upload=*|\
+       --upload-=*|--upload-p=*|--upload-pa=*|--upload-pac=*|--upload-pack=*)
+               exec=--upload-pack=$(expr "z$1" : 'z-[^=]*=\(.*\)')
+               shift
+               ;;
+       -f|--f|--fo|--for|--forc|--force)
+               force=t
+               ;;
+       -t|--t|--ta|--tag|--tags)
+               tags=t
+               ;;
+       -n|--n|--no|--no-|--no-t|--no-ta|--no-tag|--no-tags)
+               no_tags=t
+               ;;
+       -u|--u|--up|--upd|--upda|--updat|--update|--update-|--update-h|\
+       --update-he|--update-hea|--update-head|--update-head-|\
+       --update-head-o|--update-head-ok)
+               update_head_ok=t
+               ;;
+       -q|--q|--qu|--qui|--quie|--quiet)
+               quiet=--quiet
+               ;;
+       -v|--verbose)
+               verbose="$verbose"Yes
+               ;;
+       -k|--k|--ke|--kee|--keep)
+               keep='-k -k'
+               ;;
+       --depth=*)
+               shallow_depth="--depth=`expr "z$1" : 'z-[^=]*=\(.*\)'`"
+               ;;
+       --depth)
+               shift
+               shallow_depth="--depth=$1"
+               ;;
+       -*)
+               usage
+               ;;
+       *)
+               break
+               ;;
+       esac
+       shift
+done
+
+case "$#" in
+0)
+       origin=$(get_default_remote)
+       test -n "$(get_remote_url ${origin})" ||
+               die "Where do you want to fetch from today?"
+       set x $origin ; shift ;;
+esac
+
+if test -z "$exec"
+then
+       # No command line override and we have configuration for the remote.
+       exec="--upload-pack=$(get_uploadpack $1)"
+fi
+
+remote_nick="$1"
+remote=$(get_remote_url "$@")
+refs=
+rref=
+rsync_slurped_objects=
+
+if test "" = "$append"
+then
+       : >"$GIT_DIR/FETCH_HEAD"
+fi
+
+# Global that is reused later
+ls_remote_result=$(git ls-remote $exec "$remote") ||
+       die "Cannot get the repository state from $remote"
+
+append_fetch_head () {
+       flags=
+       test -n "$verbose" && flags="$flags$LF-v"
+       test -n "$force$single_force" && flags="$flags$LF-f"
+       GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION" \
+               git fetch--tool $flags append-fetch-head "$@"
+}
+
+# updating the current HEAD with git-fetch in a bare
+# repository is always fine.
+if test -z "$update_head_ok" && test $(is_bare_repository) = false
+then
+       orig_head=$(git rev-parse --verify HEAD 2>/dev/null)
+fi
+
+# Allow --notags from remote.$1.tagopt
+case "$tags$no_tags" in
+'')
+       case "$(git config --get "remote.$1.tagopt")" in
+       --no-tags)
+               no_tags=t ;;
+       esac
+esac
+
+# If --tags (and later --heads or --all) is specified, then we are
+# not talking about defaults stored in Pull: line of remotes or
+# branches file, and just fetch those and refspecs explicitly given.
+# Otherwise we do what we always did.
+
+reflist=$(get_remote_refs_for_fetch "$@")
+if test "$tags"
+then
+       taglist=`IFS='  ' &&
+                 echo "$ls_remote_result" |
+                 git show-ref --exclude-existing=refs/tags/ |
+                 while read sha1 name
+                 do
+                       echo ".${name}:${name}"
+                 done` || exit
+       if test "$#" -gt 1
+       then
+               # remote URL plus explicit refspecs; we need to merge them.
+               reflist="$reflist$LF$taglist"
+       else
+               # No explicit refspecs; fetch tags only.
+               reflist=$taglist
+       fi
+fi
+
+fetch_all_at_once () {
+
+  eval=$(echo "$1" | git fetch--tool parse-reflist "-")
+  eval "$eval"
+
+    ( : subshell because we muck with IFS
+      IFS="    $LF"
+      (
+       if test "$remote" = . ; then
+           git show-ref $rref || echo failed "$remote"
+       elif test -f "$remote" ; then
+           test -n "$shallow_depth" &&
+               die "shallow clone with bundle is not supported"
+           git bundle unbundle "$remote" $rref ||
+           echo failed "$remote"
+       else
+               if      test -d "$remote" &&
+
+                       # The remote might be our alternate.  With
+                       # this optimization we will bypass fetch-pack
+                       # altogether, which means we cannot be doing
+                       # the shallow stuff at all.
+                       test ! -f "$GIT_DIR/shallow" &&
+                       test -z "$shallow_depth" &&
+
+                       # See if all of what we are going to fetch are
+                       # connected to our repository's tips, in which
+                       # case we do not have to do any fetch.
+                       theirs=$(echo "$ls_remote_result" | \
+                               git fetch--tool -s pick-rref "$rref" "-") &&
+
+                       # This will barf when $theirs reach an object that
+                       # we do not have in our repository.  Otherwise,
+                       # we already have everything the fetch would bring in.
+                       git rev-list --objects $theirs --not --all \
+                               >/dev/null 2>/dev/null
+               then
+                       echo "$ls_remote_result" | \
+                               git fetch--tool pick-rref "$rref" "-"
+               else
+                       flags=
+                       case $verbose in
+                       YesYes*)
+                           flags="-v"
+                           ;;
+                       esac
+                       git-fetch-pack --thin $exec $keep $shallow_depth \
+                               $quiet $no_progress $flags "$remote" $rref ||
+                       echo failed "$remote"
+               fi
+       fi
+      ) |
+      (
+       flags=
+       test -n "$verbose" && flags="$flags -v"
+       test -n "$force" && flags="$flags -f"
+       GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION" \
+               git fetch--tool $flags native-store \
+                       "$remote" "$remote_nick" "$refs"
+      )
+    ) || exit
+
+}
+
+fetch_per_ref () {
+  reflist="$1"
+  refs=
+  rref=
+
+  for ref in $reflist
+  do
+      refs="$refs$LF$ref"
+
+      # These are relative path from $GIT_DIR, typically starting at refs/
+      # but may be HEAD
+      if expr "z$ref" : 'z\.' >/dev/null
+      then
+         not_for_merge=t
+         ref=$(expr "z$ref" : 'z\.\(.*\)')
+      else
+         not_for_merge=
+      fi
+      if expr "z$ref" : 'z+' >/dev/null
+      then
+         single_force=t
+         ref=$(expr "z$ref" : 'z+\(.*\)')
+      else
+         single_force=
+      fi
+      remote_name=$(expr "z$ref" : 'z\([^:]*\):')
+      local_name=$(expr "z$ref" : 'z[^:]*:\(.*\)')
+
+      rref="$rref$LF$remote_name"
+
+      # There are transports that can fetch only one head at a time...
+      case "$remote" in
+      http://* | https://* | ftp://*)
+         test -n "$shallow_depth" &&
+               die "shallow clone with http not supported"
+         proto=`expr "$remote" : '\([^:]*\):'`
+         if [ -n "$GIT_SSL_NO_VERIFY" ]; then
+             curl_extra_args="-k"
+         fi
+         if [ -n "$GIT_CURL_FTP_NO_EPSV" -o \
+               "`git config --bool http.noEPSV`" = true ]; then
+             noepsv_opt="--disable-epsv"
+         fi
+
+         # Find $remote_name from ls-remote output.
+         head=$(echo "$ls_remote_result" | \
+               git fetch--tool -s pick-rref "$remote_name" "-")
+         expr "z$head" : "z$_x40\$" >/dev/null ||
+               die "No such ref $remote_name at $remote"
+         echo >&2 "Fetching $remote_name from $remote using $proto"
+         case "$quiet" in '') v=-v ;; *) v= ;; esac
+         git-http-fetch $v -a "$head" "$remote" || exit
+         ;;
+      rsync://*)
+         test -n "$shallow_depth" &&
+               die "shallow clone with rsync not supported"
+         TMP_HEAD="$GIT_DIR/TMP_HEAD"
+         rsync -L -q "$remote/$remote_name" "$TMP_HEAD" || exit 1
+         head=$(git rev-parse --verify TMP_HEAD)
+         rm -f "$TMP_HEAD"
+         case "$quiet" in '') v=-v ;; *) v= ;; esac
+         test "$rsync_slurped_objects" || {
+             rsync -a $v --ignore-existing --exclude info \
+                 "$remote/objects/" "$GIT_OBJECT_DIRECTORY/" || exit
+
+             # Look at objects/info/alternates for rsync -- http will
+             # support it natively and git native ones will do it on
+             # the remote end.  Not having that file is not a crime.
+             rsync -q "$remote/objects/info/alternates" \
+                 "$GIT_DIR/TMP_ALT" 2>/dev/null ||
+                 rm -f "$GIT_DIR/TMP_ALT"
+             if test -f "$GIT_DIR/TMP_ALT"
+             then
+                 resolve_alternates "$remote" <"$GIT_DIR/TMP_ALT" |
+                 while read alt
+                 do
+                     case "$alt" in 'bad alternate: '*) die "$alt";; esac
+                     echo >&2 "Getting alternate: $alt"
+                     rsync -av --ignore-existing --exclude info \
+                     "$alt" "$GIT_OBJECT_DIRECTORY/" || exit
+                 done
+                 rm -f "$GIT_DIR/TMP_ALT"
+             fi
+             rsync_slurped_objects=t
+         }
+         ;;
+      esac
+
+      append_fetch_head "$head" "$remote" \
+         "$remote_name" "$remote_nick" "$local_name" "$not_for_merge" || exit
+
+  done
+
+}
+
+fetch_main () {
+       case "$remote" in
+       http://* | https://* | ftp://* | rsync://* )
+               fetch_per_ref "$@"
+               ;;
+       *)
+               fetch_all_at_once "$@"
+               ;;
+       esac
+}
+
+fetch_main "$reflist" || exit
+
+# automated tag following
+case "$no_tags$tags" in
+'')
+       case "$reflist" in
+       *:refs/*)
+               # effective only when we are following remote branch
+               # using local tracking branch.
+               taglist=$(IFS=' ' &&
+               echo "$ls_remote_result" |
+               git show-ref --exclude-existing=refs/tags/ |
+               while read sha1 name
+               do
+                       git cat-file -t "$sha1" >/dev/null 2>&1 || continue
+                       echo >&2 "Auto-following $name"
+                       echo ".${name}:${name}"
+               done)
+       esac
+       case "$taglist" in
+       '') ;;
+       ?*)
+               # do not deepen a shallow tree when following tags
+               shallow_depth=
+               fetch_main "$taglist" || exit ;;
+       esac
+esac
+
+# If the original head was empty (i.e. no "master" yet), or
+# if we were told not to worry, we do not have to check.
+case "$orig_head" in
+'')
+       ;;
+?*)
+       curr_head=$(git rev-parse --verify HEAD 2>/dev/null)
+       if test "$curr_head" != "$orig_head"
+       then
+           git update-ref \
+                       -m "$GIT_REFLOG_ACTION: Undoing incorrectly fetched HEAD." \
+                       HEAD "$orig_head"
+               die "Cannot fetch into the current branch."
+       fi
+       ;;
+esac
diff --git a/contrib/examples/git-gc.sh b/contrib/examples/git-gc.sh
new file mode 100755 (executable)
index 0000000..1597e9f
--- /dev/null
@@ -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 test $# != 0
+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-ls-remote.sh b/contrib/examples/git-ls-remote.sh
new file mode 100755 (executable)
index 0000000..fec70bb
--- /dev/null
@@ -0,0 +1,142 @@
+#!/bin/sh
+#
+
+usage () {
+    echo >&2 "usage: $0 [--heads] [--tags] [-u|--upload-pack <upload-pack>]"
+    echo >&2 "          <repository> <refs>..."
+    exit 1;
+}
+
+die () {
+    echo >&2 "$*"
+    exit 1
+}
+
+exec=
+while test $# != 0
+do
+  case "$1" in
+  -h|--h|--he|--hea|--head|--heads)
+  heads=heads; shift ;;
+  -t|--t|--ta|--tag|--tags)
+  tags=tags; shift ;;
+  -u|--u|--up|--upl|--uploa|--upload|--upload-|--upload-p|--upload-pa|\
+  --upload-pac|--upload-pack)
+       shift
+       exec="--upload-pack=$1"
+       shift;;
+  -u=*|--u=*|--up=*|--upl=*|--uplo=*|--uploa=*|--upload=*|\
+  --upload-=*|--upload-p=*|--upload-pa=*|--upload-pac=*|--upload-pack=*)
+       exec=--upload-pack=$(expr "z$1" : 'z-[^=]*=\(.*\)')
+       shift;;
+  --)
+  shift; break ;;
+  -*)
+  usage ;;
+  *)
+  break ;;
+  esac
+done
+
+case "$#" in 0) usage ;; esac
+
+case ",$heads,$tags," in
+,,,) heads=heads tags=tags other=other ;;
+esac
+
+. git-parse-remote
+peek_repo="$(get_remote_url "$@")"
+shift
+
+tmp=.ls-remote-$$
+trap "rm -fr $tmp-*" 0 1 2 3 15
+tmpdir=$tmp-d
+
+case "$peek_repo" in
+http://* | https://* | ftp://* )
+       if [ -n "$GIT_SSL_NO_VERIFY" -o \
+               "`git config --bool http.sslVerify`" = false ]; then
+               curl_extra_args="-k"
+       fi
+       if [ -n "$GIT_CURL_FTP_NO_EPSV" -o \
+               "`git config --bool http.noEPSV`" = true ]; then
+               curl_extra_args="${curl_extra_args} --disable-epsv"
+       fi
+       curl -nsf $curl_extra_args --header "Pragma: no-cache" "$peek_repo/info/refs" ||
+               echo "failed    slurping"
+       ;;
+
+rsync://* )
+       mkdir $tmpdir &&
+       rsync -rlq "$peek_repo/HEAD" $tmpdir &&
+       rsync -rq "$peek_repo/refs" $tmpdir || {
+               echo "failed    slurping"
+               exit
+       }
+       head=$(cat "$tmpdir/HEAD") &&
+       case "$head" in
+       ref:' '*)
+               head=$(expr "z$head" : 'zref: \(.*\)') &&
+               head=$(cat "$tmpdir/$head") || exit
+       esac &&
+       echo "$head     HEAD"
+       (cd $tmpdir && find refs -type f) |
+       while read path
+       do
+               tr -d '\012' <"$tmpdir/$path"
+               echo "  $path"
+       done &&
+       rm -fr $tmpdir
+       ;;
+
+* )
+       if test -f "$peek_repo" ; then
+               git bundle list-heads "$peek_repo" ||
+               echo "failed    slurping"
+       else
+               git-peek-remote $exec "$peek_repo" ||
+               echo "failed    slurping"
+       fi
+       ;;
+esac |
+sort -t '      ' -k 2 |
+while read sha1 path
+do
+       case "$sha1" in
+       failed)
+               exit 1 ;;
+       esac
+       case "$path" in
+       refs/heads/*)
+               group=heads ;;
+       refs/tags/*)
+               group=tags ;;
+       *)
+               group=other ;;
+       esac
+       case ",$heads,$tags,$other," in
+       *,$group,*)
+               ;;
+       *)
+               continue;;
+       esac
+       case "$#" in
+       0)
+               match=yes ;;
+       *)
+               match=no
+               for pat
+               do
+                       case "/$path" in
+                       */$pat )
+                               match=yes
+                               break ;;
+                       esac
+               done
+       esac
+       case "$match" in
+       no)
+               continue ;;
+       esac
+       echo "$sha1     $path"
+done
diff --git a/contrib/examples/git-merge-ours.sh b/contrib/examples/git-merge-ours.sh
new file mode 100755 (executable)
index 0000000..29dba4b
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+# Pretend we resolved the heads, but declare our tree trumps everybody else.
+#
+
+# We need to exit with 2 if the index does not match our HEAD tree,
+# because the current index is what we will be committing as the
+# merge result.
+
+git diff-index --quiet --cached HEAD -- || exit 2
+
+exit 0
diff --git a/contrib/examples/git-reset.sh b/contrib/examples/git-reset.sh
new file mode 100755 (executable)
index 0000000..bafeb52
--- /dev/null
@@ -0,0 +1,106 @@
+#!/bin/sh
+#
+# Copyright (c) 2005, 2006 Linus Torvalds and Junio C Hamano
+#
+USAGE='[--mixed | --soft | --hard]  [<commit-ish>] [ [--] <paths>...]'
+SUBDIRECTORY_OK=Yes
+. git-sh-setup
+set_reflog_action "reset $*"
+require_work_tree
+
+update= reset_type=--mixed
+unset rev
+
+while test $# != 0
+do
+       case "$1" in
+       --mixed | --soft | --hard)
+               reset_type="$1"
+               ;;
+       --)
+               break
+               ;;
+       -*)
+               usage
+               ;;
+       *)
+               rev=$(git rev-parse --verify "$1") || exit
+               shift
+               break
+               ;;
+       esac
+       shift
+done
+
+: ${rev=HEAD}
+rev=$(git rev-parse --verify $rev^0) || exit
+
+# Skip -- in "git reset HEAD -- foo" and "git reset -- foo".
+case "$1" in --) shift ;; esac
+
+# git reset --mixed tree [--] paths... can be used to
+# load chosen paths from the tree into the index without
+# affecting the working tree nor HEAD.
+if test $# != 0
+then
+       test "$reset_type" = "--mixed" ||
+               die "Cannot do partial $reset_type reset."
+
+       git diff-index --cached $rev -- "$@" |
+       sed -e 's/^:\([0-7][0-7]*\) [0-7][0-7]* \([0-9a-f][0-9a-f]*\) [0-9a-f][0-9a-f]* [A-Z]   \(.*\)$/\1 \2   \3/' |
+       git update-index --add --remove --index-info || exit
+       git update-index --refresh
+       exit
+fi
+
+cd_to_toplevel
+
+if test "$reset_type" = "--hard"
+then
+       update=-u
+fi
+
+# Soft reset does not touch the index file nor the working tree
+# at all, but requires them in a good order.  Other resets reset
+# the index file to the tree object we are switching to.
+if test "$reset_type" = "--soft"
+then
+       if test -f "$GIT_DIR/MERGE_HEAD" ||
+          test "" != "$(git ls-files --unmerged)"
+       then
+               die "Cannot do a soft reset in the middle of a merge."
+       fi
+else
+       git read-tree -v --reset $update "$rev" || exit
+fi
+
+# Any resets update HEAD to the head being switched to.
+if orig=$(git rev-parse --verify HEAD 2>/dev/null)
+then
+       echo "$orig" >"$GIT_DIR/ORIG_HEAD"
+else
+       rm -f "$GIT_DIR/ORIG_HEAD"
+fi
+git update-ref -m "$GIT_REFLOG_ACTION" HEAD "$rev"
+update_ref_status=$?
+
+case "$reset_type" in
+--hard )
+       test $update_ref_status = 0 && {
+               printf "HEAD is now at "
+               GIT_PAGER= git log --max-count=1 --pretty=oneline \
+                       --abbrev-commit HEAD
+       }
+       ;;
+--soft )
+       ;; # Nothing else to do
+--mixed )
+       # Report what has not been updated.
+       git update-index --refresh
+       ;;
+esac
+
+rm -f "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/rr-cache/MERGE_RR" \
+       "$GIT_DIR/SQUASH_MSG" "$GIT_DIR/MERGE_MSG"
+
+exit $update_ref_status
diff --git a/contrib/examples/git-resolve.sh b/contrib/examples/git-resolve.sh
new file mode 100755 (executable)
index 0000000..0ee1bd8
--- /dev/null
@@ -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/examples/git-svnimport.perl b/contrib/examples/git-svnimport.perl
new file mode 100755 (executable)
index 0000000..ea8c1b2
--- /dev/null
@@ -0,0 +1,976 @@
+#!/usr/bin/perl -w
+
+# This tool is copyright (c) 2005, Matthias Urlichs.
+# It is released under the Gnu Public License, version 2.
+#
+# The basic idea is to pull and analyze SVN changes.
+#
+# Checking out the files is done by a single long-running SVN connection.
+#
+# The head revision is on branch "origin" by default.
+# You can change that with the '-o' option.
+
+use strict;
+use warnings;
+use Getopt::Std;
+use File::Copy;
+use File::Spec;
+use File::Temp qw(tempfile);
+use File::Path qw(mkpath);
+use File::Basename qw(basename dirname);
+use Time::Local;
+use IO::Pipe;
+use POSIX qw(strftime dup2);
+use IPC::Open2;
+use SVN::Core;
+use SVN::Ra;
+
+die "Need SVN:Core 1.2.1 or better" if $SVN::Core::VERSION lt "1.2.1";
+
+$SIG{'PIPE'}="IGNORE";
+$ENV{'TZ'}="UTC";
+
+our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T,
+    $opt_b,$opt_r,$opt_I,$opt_A,$opt_s,$opt_l,$opt_d,$opt_D,$opt_S,$opt_F,
+    $opt_P,$opt_R);
+
+sub usage() {
+       print STDERR <<END;
+Usage: ${\basename $0}     # fetch/update GIT from SVN
+       [-o branch-for-HEAD] [-h] [-v] [-l max_rev] [-R repack_each_revs]
+       [-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname]
+       [-d|-D] [-i] [-u] [-r] [-I ignorefilename] [-s start_chg]
+       [-m] [-M regex] [-A author_file] [-S] [-F] [-P project_name] [SVN_URL]
+END
+       exit(1);
+}
+
+getopts("A:b:C:dDFhiI:l:mM:o:rs:t:T:SP:R:uv") or usage();
+usage if $opt_h;
+
+my $tag_name = $opt_t || "tags";
+my $trunk_name = defined $opt_T ? $opt_T : "trunk";
+my $branch_name = $opt_b || "branches";
+my $project_name = $opt_P || "";
+$project_name = "/" . $project_name if ($project_name);
+my $repack_after = $opt_R || 1000;
+my $root_pool = SVN::Pool->new_default;
+
+@ARGV == 1 or @ARGV == 2 or usage();
+
+$opt_o ||= "origin";
+$opt_s ||= 1;
+my $git_tree = $opt_C;
+$git_tree ||= ".";
+
+my $svn_url = $ARGV[0];
+my $svn_dir = $ARGV[1];
+
+our @mergerx = ();
+if ($opt_m) {
+       my $branch_esc = quotemeta ($branch_name);
+       my $trunk_esc  = quotemeta ($trunk_name);
+       @mergerx =
+       (
+               qr!\b(?:merg(?:ed?|ing))\b.*?\b((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i,
+               qr!\b(?:from|of)\W+((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i,
+               qr!\b(?:from|of)\W+(?:the )?([\w\.\-]+)[-\s]branch\b!i
+       );
+}
+if ($opt_M) {
+       unshift (@mergerx, qr/$opt_M/);
+}
+
+# Absolutize filename now, since we will have chdir'ed by the time we
+# get around to opening it.
+$opt_A = File::Spec->rel2abs($opt_A) if $opt_A;
+
+our %users = ();
+our $users_file = undef;
+sub read_users($) {
+       $users_file = File::Spec->rel2abs(@_);
+       die "Cannot open $users_file\n" unless -f $users_file;
+       open(my $authors,$users_file);
+       while(<$authors>) {
+               chomp;
+               next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
+               (my $user,my $name,my $email) = ($1,$2,$3);
+               $users{$user} = [$name,$email];
+       }
+       close($authors);
+}
+
+select(STDERR); $|=1; select(STDOUT);
+
+
+package SVNconn;
+# Basic SVN connection.
+# We're only interested in connecting and downloading, so ...
+
+use File::Spec;
+use File::Temp qw(tempfile);
+use POSIX qw(strftime dup2);
+use Fcntl qw(SEEK_SET);
+
+sub new {
+       my($what,$repo) = @_;
+       $what=ref($what) if ref($what);
+
+       my $self = {};
+       $self->{'buffer'} = "";
+       bless($self,$what);
+
+       $repo =~ s#/+$##;
+       $self->{'fullrep'} = $repo;
+       $self->conn();
+
+       return $self;
+}
+
+sub conn {
+       my $self = shift;
+       my $repo = $self->{'fullrep'};
+       my $auth = SVN::Core::auth_open ([SVN::Client::get_simple_provider,
+                         SVN::Client::get_ssl_server_trust_file_provider,
+                         SVN::Client::get_username_provider]);
+       my $s = SVN::Ra->new(url => $repo, auth => $auth, pool => $root_pool);
+       die "SVN connection to $repo: $!\n" unless defined $s;
+       $self->{'svn'} = $s;
+       $self->{'repo'} = $repo;
+       $self->{'maxrev'} = $s->get_latest_revnum();
+}
+
+sub file {
+       my($self,$path,$rev) = @_;
+
+       my ($fh, $name) = tempfile('gitsvn.XXXXXX',
+                   DIR => File::Spec->tmpdir(), UNLINK => 1);
+
+       print "... $rev $path ...\n" if $opt_v;
+       my (undef, $properties);
+       $path =~ s#^/*##;
+       my $subpool = SVN::Pool::new_default_sub;
+       eval { (undef, $properties)
+                  = $self->{'svn'}->get_file($path,$rev,$fh); };
+       if($@) {
+               return undef if $@ =~ /Attempted to get checksum/;
+               die $@;
+       }
+       my $mode;
+       if (exists $properties->{'svn:executable'}) {
+               $mode = '100755';
+       } elsif (exists $properties->{'svn:special'}) {
+               my ($special_content, $filesize);
+               $filesize = tell $fh;
+               seek $fh, 0, SEEK_SET;
+               read $fh, $special_content, $filesize;
+               if ($special_content =~ s/^link //) {
+                       $mode = '120000';
+                       seek $fh, 0, SEEK_SET;
+                       truncate $fh, 0;
+                       print $fh $special_content;
+               } else {
+                       die "unexpected svn:special file encountered";
+               }
+       } else {
+               $mode = '100644';
+       }
+       close ($fh);
+
+       return ($name, $mode);
+}
+
+sub ignore {
+       my($self,$path,$rev) = @_;
+
+       print "... $rev $path ...\n" if $opt_v;
+       $path =~ s#^/*##;
+       my $subpool = SVN::Pool::new_default_sub;
+       my (undef,undef,$properties)
+           = $self->{'svn'}->get_dir($path,$rev,undef);
+       if (exists $properties->{'svn:ignore'}) {
+               my ($fh, $name) = tempfile('gitsvn.XXXXXX',
+                                          DIR => File::Spec->tmpdir(),
+                                          UNLINK => 1);
+               print $fh $properties->{'svn:ignore'};
+               close($fh);
+               return $name;
+       } else {
+               return undef;
+       }
+}
+
+sub dir_list {
+       my($self,$path,$rev) = @_;
+       $path =~ s#^/*##;
+       my $subpool = SVN::Pool::new_default_sub;
+       my ($dirents,undef,$properties)
+           = $self->{'svn'}->get_dir($path,$rev,undef);
+       return $dirents;
+}
+
+package main;
+use URI;
+
+our $svn = $svn_url;
+$svn .= "/$svn_dir" if defined $svn_dir;
+my $svn2 = SVNconn->new($svn);
+$svn = SVNconn->new($svn);
+
+my $lwp_ua;
+if($opt_d or $opt_D) {
+       $svn_url = URI->new($svn_url)->canonical;
+       if($opt_D) {
+               $svn_dir =~ s#/*$#/#;
+       } else {
+               $svn_dir = "";
+       }
+       if ($svn_url->scheme eq "http") {
+               use LWP::UserAgent;
+               $lwp_ua = LWP::UserAgent->new(keep_alive => 1, requests_redirectable => []);
+       } else {
+               print STDERR "Warning: not HTTP; turning off direct file access\n";
+               $opt_d=0;
+       }
+}
+
+sub pdate($) {
+       my($d) = @_;
+       $d =~ m#(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)#
+               or die "Unparseable date: $d\n";
+       my $y=$1; $y-=1900 if $y>1900;
+       return timegm($6||0,$5,$4,$3,$2-1,$y);
+}
+
+sub getwd() {
+       my $pwd = `pwd`;
+       chomp $pwd;
+       return $pwd;
+}
+
+
+sub get_headref($$) {
+    my $name    = shift;
+    my $git_dir = shift;
+    my $sha;
+
+    if (open(C,"$git_dir/refs/heads/$name")) {
+       chomp($sha = <C>);
+       close(C);
+       length($sha) == 40
+           or die "Cannot get head id for $name ($sha): $!\n";
+    }
+    return $sha;
+}
+
+
+-d $git_tree
+       or mkdir($git_tree,0777)
+       or die "Could not create $git_tree: $!";
+chdir($git_tree);
+
+my $orig_branch = "";
+my $forward_master = 0;
+my %branches;
+
+my $git_dir = $ENV{"GIT_DIR"} || ".git";
+$git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#;
+$ENV{"GIT_DIR"} = $git_dir;
+my $orig_git_index;
+$orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE};
+my ($git_ih, $git_index) = tempfile('gitXXXXXX', SUFFIX => '.idx',
+                                   DIR => File::Spec->tmpdir());
+close ($git_ih);
+$ENV{GIT_INDEX_FILE} = $git_index;
+my $maxnum = 0;
+my $last_rev = "";
+my $last_branch;
+my $current_rev = $opt_s || 1;
+unless(-d $git_dir) {
+       system("git-init");
+       die "Cannot init the GIT db at $git_tree: $?\n" if $?;
+       system("git-read-tree");
+       die "Cannot init an empty tree: $?\n" if $?;
+
+       $last_branch = $opt_o;
+       $orig_branch = "";
+} else {
+       -f "$git_dir/refs/heads/$opt_o"
+               or die "Branch '$opt_o' does not exist.\n".
+                      "Either use the correct '-o branch' option,\n".
+                      "or import to a new repository.\n";
+
+       -f "$git_dir/svn2git"
+               or die "'$git_dir/svn2git' does not exist.\n".
+                      "You need that file for incremental imports.\n";
+       open(F, "git-symbolic-ref HEAD |") or
+               die "Cannot run git-symbolic-ref: $!\n";
+       chomp ($last_branch = <F>);
+       $last_branch = basename($last_branch);
+       close(F);
+       unless($last_branch) {
+               warn "Cannot read the last branch name: $! -- assuming 'master'\n";
+               $last_branch = "master";
+       }
+       $orig_branch = $last_branch;
+       $last_rev = get_headref($orig_branch, $git_dir);
+       if (-f "$git_dir/SVN2GIT_HEAD") {
+               die <<EOM;
+SVN2GIT_HEAD exists.
+Make sure your working directory corresponds to HEAD and remove SVN2GIT_HEAD.
+You may need to run
+
+    git-read-tree -m -u SVN2GIT_HEAD HEAD
+EOM
+       }
+       system('cp', "$git_dir/HEAD", "$git_dir/SVN2GIT_HEAD");
+
+       $forward_master =
+           $opt_o ne 'master' && -f "$git_dir/refs/heads/master" &&
+           system('cmp', '-s', "$git_dir/refs/heads/master",
+                               "$git_dir/refs/heads/$opt_o") == 0;
+
+       # populate index
+       system('git-read-tree', $last_rev);
+       die "read-tree failed: $?\n" if $?;
+
+       # Get the last import timestamps
+       open my $B,"<", "$git_dir/svn2git";
+       while(<$B>) {
+               chomp;
+               my($num,$branch,$ref) = split;
+               $branches{$branch}{$num} = $ref;
+               $branches{$branch}{"LAST"} = $ref;
+               $current_rev = $num+1 if $current_rev <= $num;
+       }
+       close($B);
+}
+-d $git_dir
+       or die "Could not create git subdir ($git_dir).\n";
+
+my $default_authors = "$git_dir/svn-authors";
+if ($opt_A) {
+       read_users($opt_A);
+       copy($opt_A,$default_authors) or die "Copy failed: $!";
+} else {
+       read_users($default_authors) if -f $default_authors;
+}
+
+open BRANCHES,">>", "$git_dir/svn2git";
+
+sub node_kind($$) {
+       my ($svnpath, $revision) = @_;
+       $svnpath =~ s#^/*##;
+       my $subpool = SVN::Pool::new_default_sub;
+       my $kind = $svn->{'svn'}->check_path($svnpath,$revision);
+       return $kind;
+}
+
+sub get_file($$$) {
+       my($svnpath,$rev,$path) = @_;
+
+       # now get it
+       my ($name,$mode);
+       if($opt_d) {
+               my($req,$res);
+
+               # /svn/!svn/bc/2/django/trunk/django-docs/build.py
+               my $url=$svn_url->clone();
+               $url->path($url->path."/!svn/bc/$rev/$svn_dir$svnpath");
+               print "... $path...\n" if $opt_v;
+               $req = HTTP::Request->new(GET => $url);
+               $res = $lwp_ua->request($req);
+               if ($res->is_success) {
+                       my $fh;
+                       ($fh, $name) = tempfile('gitsvn.XXXXXX',
+                       DIR => File::Spec->tmpdir(), UNLINK => 1);
+                       print $fh $res->content;
+                       close($fh) or die "Could not write $name: $!\n";
+               } else {
+                       return undef if $res->code == 301; # directory?
+                       die $res->status_line." at $url\n";
+               }
+               $mode = '0644'; # can't obtain mode via direct http request?
+       } else {
+               ($name,$mode) = $svn->file("$svnpath",$rev);
+               return undef unless defined $name;
+       }
+
+       my $pid = open(my $F, '-|');
+       die $! unless defined $pid;
+       if (!$pid) {
+           exec("git-hash-object", "-w", $name)
+               or die "Cannot create object: $!\n";
+       }
+       my $sha = <$F>;
+       chomp $sha;
+       close $F;
+       unlink $name;
+       return [$mode, $sha, $path];
+}
+
+sub get_ignore($$$$$) {
+       my($new,$old,$rev,$path,$svnpath) = @_;
+
+       return unless $opt_I;
+       my $name = $svn->ignore("$svnpath",$rev);
+       if ($path eq '/') {
+               $path = $opt_I;
+       } else {
+               $path = File::Spec->catfile($path,$opt_I);
+       }
+       if (defined $name) {
+               my $pid = open(my $F, '-|');
+               die $! unless defined $pid;
+               if (!$pid) {
+                       exec("git-hash-object", "-w", $name)
+                           or die "Cannot create object: $!\n";
+               }
+               my $sha = <$F>;
+               chomp $sha;
+               close $F;
+               unlink $name;
+               push(@$new,['0644',$sha,$path]);
+       } elsif (defined $old) {
+               push(@$old,$path);
+       }
+}
+
+sub project_path($$)
+{
+       my ($path, $project) = @_;
+
+       $path = "/".$path unless ($path =~ m#^\/#) ;
+       return $1 if ($path =~ m#^$project\/(.*)$#);
+
+       $path =~ s#\.#\\\.#g;
+       $path =~ s#\+#\\\+#g;
+       return "/" if ($project =~ m#^$path.*$#);
+
+       return undef;
+}
+
+sub split_path($$) {
+       my($rev,$path) = @_;
+       my $branch;
+
+       if($path =~ s#^/\Q$tag_name\E/([^/]+)/?##) {
+               $branch = "/$1";
+       } elsif($path =~ s#^/\Q$trunk_name\E/?##) {
+               $branch = "/";
+       } elsif($path =~ s#^/\Q$branch_name\E/([^/]+)/?##) {
+               $branch = $1;
+       } else {
+               my %no_error = (
+                       "/" => 1,
+                       "/$tag_name" => 1,
+                       "/$branch_name" => 1
+               );
+               print STDERR "$rev: Unrecognized path: $path\n" unless (defined $no_error{$path});
+               return ()
+       }
+       if ($path eq "") {
+               $path = "/";
+       } elsif ($project_name) {
+               $path = project_path($path, $project_name);
+       }
+       return ($branch,$path);
+}
+
+sub branch_rev($$) {
+
+       my ($srcbranch,$uptorev) = @_;
+
+       my $bbranches = $branches{$srcbranch};
+       my @revs = reverse sort { ($a eq 'LAST' ? 0 : $a) <=> ($b eq 'LAST' ? 0 : $b) } keys %$bbranches;
+       my $therev;
+       foreach my $arev(@revs) {
+               next if  ($arev eq 'LAST');
+               if ($arev <= $uptorev) {
+                       $therev = $arev;
+                       last;
+               }
+       }
+       return $therev;
+}
+
+sub expand_svndir($$$);
+
+sub expand_svndir($$$)
+{
+       my ($svnpath, $rev, $path) = @_;
+       my @list;
+       get_ignore(\@list, undef, $rev, $path, $svnpath);
+       my $dirents = $svn->dir_list($svnpath, $rev);
+       foreach my $p(keys %$dirents) {
+               my $kind = node_kind($svnpath.'/'.$p, $rev);
+               if ($kind eq $SVN::Node::file) {
+                       my $f = get_file($svnpath.'/'.$p, $rev, $path.'/'.$p);
+                       push(@list, $f) if $f;
+               } elsif ($kind eq $SVN::Node::dir) {
+                       push(@list,
+                            expand_svndir($svnpath.'/'.$p, $rev, $path.'/'.$p));
+               }
+       }
+       return @list;
+}
+
+sub copy_path($$$$$$$$) {
+       # Somebody copied a whole subdirectory.
+       # We need to find the index entries from the old version which the
+       # SVN log entry points to, and add them to the new place.
+
+       my($newrev,$newbranch,$path,$oldpath,$rev,$node_kind,$new,$parents) = @_;
+
+       my($srcbranch,$srcpath) = split_path($rev,$oldpath);
+       unless(defined $srcbranch && defined $srcpath) {
+               print "Path not found when copying from $oldpath @ $rev.\n".
+                       "Will try to copy from original SVN location...\n"
+                       if $opt_v;
+               push (@$new, expand_svndir($oldpath, $rev, $path));
+               return;
+       }
+       my $therev = branch_rev($srcbranch, $rev);
+       my $gitrev = $branches{$srcbranch}{$therev};
+       unless($gitrev) {
+               print STDERR "$newrev:$newbranch: could not find $oldpath \@ $rev\n";
+               return;
+       }
+       if ($srcbranch ne $newbranch) {
+               push(@$parents, $branches{$srcbranch}{'LAST'});
+       }
+       print "$newrev:$newbranch:$path: copying from $srcbranch:$srcpath @ $rev\n" if $opt_v;
+       if ($node_kind eq $SVN::Node::dir) {
+               $srcpath =~ s#/*$#/#;
+       }
+
+       my $pid = open my $f,'-|';
+       die $! unless defined $pid;
+       if (!$pid) {
+               exec("git-ls-tree","-r","-z",$gitrev,$srcpath)
+                       or die $!;
+       }
+       local $/ = "\0";
+       while(<$f>) {
+               chomp;
+               my($m,$p) = split(/\t/,$_,2);
+               my($mode,$type,$sha1) = split(/ /,$m);
+               next if $type ne "blob";
+               if ($node_kind eq $SVN::Node::dir) {
+                       $p = $path . substr($p,length($srcpath)-1);
+               } else {
+                       $p = $path;
+               }
+               push(@$new,[$mode,$sha1,$p]);
+       }
+       close($f) or
+               print STDERR "$newrev:$newbranch: could not list files in $oldpath \@ $rev\n";
+}
+
+sub commit {
+       my($branch, $changed_paths, $revision, $author, $date, $message) = @_;
+       my($committer_name,$committer_email,$dest);
+       my($author_name,$author_email);
+       my(@old,@new,@parents);
+
+       if (not defined $author or $author eq "") {
+               $committer_name = $committer_email = "unknown";
+       } elsif (defined $users_file) {
+               die "User $author is not listed in $users_file\n"
+                   unless exists $users{$author};
+               ($committer_name,$committer_email) = @{$users{$author}};
+       } elsif ($author =~ /^(.*?)\s+<(.*)>$/) {
+               ($committer_name, $committer_email) = ($1, $2);
+       } else {
+               $author =~ s/^<(.*)>$/$1/;
+               $committer_name = $committer_email = $author;
+       }
+
+       if ($opt_F && $message =~ /From:\s+(.*?)\s+<(.*)>\s*\n/) {
+               ($author_name, $author_email) = ($1, $2);
+               print "Author from From: $1 <$2>\n" if ($opt_v);;
+       } elsif ($opt_S && $message =~ /Signed-off-by:\s+(.*?)\s+<(.*)>\s*\n/) {
+               ($author_name, $author_email) = ($1, $2);
+               print "Author from Signed-off-by: $1 <$2>\n" if ($opt_v);;
+       } else {
+               $author_name = $committer_name;
+               $author_email = $committer_email;
+       }
+
+       $date = pdate($date);
+
+       my $tag;
+       my $parent;
+       if($branch eq "/") { # trunk
+               $parent = $opt_o;
+       } elsif($branch =~ m#^/(.+)#) { # tag
+               $tag = 1;
+               $parent = $1;
+       } else { # "normal" branch
+               # nothing to do
+               $parent = $branch;
+       }
+       $dest = $parent;
+
+       my $prev = $changed_paths->{"/"};
+       if($prev and $prev->[0] eq "A") {
+               delete $changed_paths->{"/"};
+               my $oldpath = $prev->[1];
+               my $rev;
+               if(defined $oldpath) {
+                       my $p;
+                       ($parent,$p) = split_path($revision,$oldpath);
+                       if(defined $parent) {
+                               if($parent eq "/") {
+                                       $parent = $opt_o;
+                               } else {
+                                       $parent =~ s#^/##; # if it's a tag
+                               }
+                       }
+               } else {
+                       $parent = undef;
+               }
+       }
+
+       my $rev;
+       if($revision > $opt_s and defined $parent) {
+               open(H,'-|',"git-rev-parse","--verify",$parent);
+               $rev = <H>;
+               close(H) or do {
+                       print STDERR "$revision: cannot find commit '$parent'!\n";
+                       return;
+               };
+               chop $rev;
+               if(length($rev) != 40) {
+                       print STDERR "$revision: cannot find commit '$parent'!\n";
+                       return;
+               }
+               $rev = $branches{($parent eq $opt_o) ? "/" : $parent}{"LAST"};
+               if($revision != $opt_s and not $rev) {
+                       print STDERR "$revision: do not know ancestor for '$parent'!\n";
+                       return;
+               }
+       } else {
+               $rev = undef;
+       }
+
+#      if($prev and $prev->[0] eq "A") {
+#              if(not $tag) {
+#                      unless(open(H,"> $git_dir/refs/heads/$branch")) {
+#                              print STDERR "$revision: Could not create branch $branch: $!\n";
+#                              $state=11;
+#                              next;
+#                      }
+#                      print H "$rev\n"
+#                              or die "Could not write branch $branch: $!";
+#                      close(H)
+#                              or die "Could not write branch $branch: $!";
+#              }
+#      }
+       if(not defined $rev) {
+               unlink($git_index);
+       } elsif ($rev ne $last_rev) {
+               print "Switching from $last_rev to $rev ($branch)\n" if $opt_v;
+               system("git-read-tree", $rev);
+               die "read-tree failed for $rev: $?\n" if $?;
+               $last_rev = $rev;
+       }
+
+       push (@parents, $rev) if defined $rev;
+
+       my $cid;
+       if($tag and not %$changed_paths) {
+               $cid = $rev;
+       } else {
+               my @paths = sort keys %$changed_paths;
+               foreach my $path(@paths) {
+                       my $action = $changed_paths->{$path};
+
+                       if ($action->[0] eq "R") {
+                               # refer to a file/tree in an earlier commit
+                               push(@old,$path); # remove any old stuff
+                       }
+                       if(($action->[0] eq "A") || ($action->[0] eq "R")) {
+                               my $node_kind = node_kind($action->[3], $revision);
+                               if ($node_kind eq $SVN::Node::file) {
+                                       my $f = get_file($action->[3],
+                                                        $revision, $path);
+                                       if ($f) {
+                                               push(@new,$f) if $f;
+                                       } else {
+                                               my $opath = $action->[3];
+                                               print STDERR "$revision: $branch: could not fetch '$opath'\n";
+                                       }
+                               } elsif ($node_kind eq $SVN::Node::dir) {
+                                       if($action->[1]) {
+                                               copy_path($revision, $branch,
+                                                         $path, $action->[1],
+                                                         $action->[2], $node_kind,
+                                                         \@new, \@parents);
+                                       } else {
+                                               get_ignore(\@new, \@old, $revision,
+                                                          $path, $action->[3]);
+                                       }
+                               }
+                       } elsif ($action->[0] eq "D") {
+                               push(@old,$path);
+                       } elsif ($action->[0] eq "M") {
+                               my $node_kind = node_kind($action->[3], $revision);
+                               if ($node_kind eq $SVN::Node::file) {
+                                       my $f = get_file($action->[3],
+                                                        $revision, $path);
+                                       push(@new,$f) if $f;
+                               } elsif ($node_kind eq $SVN::Node::dir) {
+                                       get_ignore(\@new, \@old, $revision,
+                                                  $path, $action->[3]);
+                               }
+                       } else {
+                               die "$revision: unknown action '".$action->[0]."' for $path\n";
+                       }
+               }
+
+               while(@old) {
+                       my @o1;
+                       if(@old > 55) {
+                               @o1 = splice(@old,0,50);
+                       } else {
+                               @o1 = @old;
+                               @old = ();
+                       }
+                       my $pid = open my $F, "-|";
+                       die "$!" unless defined $pid;
+                       if (!$pid) {
+                               exec("git-ls-files", "-z", @o1) or die $!;
+                       }
+                       @o1 = ();
+                       local $/ = "\0";
+                       while(<$F>) {
+                               chomp;
+                               push(@o1,$_);
+                       }
+                       close($F);
+
+                       while(@o1) {
+                               my @o2;
+                               if(@o1 > 55) {
+                                       @o2 = splice(@o1,0,50);
+                               } else {
+                                       @o2 = @o1;
+                                       @o1 = ();
+                               }
+                               system("git-update-index","--force-remove","--",@o2);
+                               die "Cannot remove files: $?\n" if $?;
+                       }
+               }
+               while(@new) {
+                       my @n2;
+                       if(@new > 12) {
+                               @n2 = splice(@new,0,10);
+                       } else {
+                               @n2 = @new;
+                               @new = ();
+                       }
+                       system("git-update-index","--add",
+                               (map { ('--cacheinfo', @$_) } @n2));
+                       die "Cannot add files: $?\n" if $?;
+               }
+
+               my $pid = open(C,"-|");
+               die "Cannot fork: $!" unless defined $pid;
+               unless($pid) {
+                       exec("git-write-tree");
+                       die "Cannot exec git-write-tree: $!\n";
+               }
+               chomp(my $tree = <C>);
+               length($tree) == 40
+                       or die "Cannot get tree id ($tree): $!\n";
+               close(C)
+                       or die "Error running git-write-tree: $?\n";
+               print "Tree ID $tree\n" if $opt_v;
+
+               my $pr = IO::Pipe->new() or die "Cannot open pipe: $!\n";
+               my $pw = IO::Pipe->new() or die "Cannot open pipe: $!\n";
+               $pid = fork();
+               die "Fork: $!\n" unless defined $pid;
+               unless($pid) {
+                       $pr->writer();
+                       $pw->reader();
+                       open(OUT,">&STDOUT");
+                       dup2($pw->fileno(),0);
+                       dup2($pr->fileno(),1);
+                       $pr->close();
+                       $pw->close();
+
+                       my @par = ();
+
+                       # loose detection of merges
+                       # based on the commit msg
+                       foreach my $rx (@mergerx) {
+                               if ($message =~ $rx) {
+                                       my $mparent = $1;
+                                       if ($mparent eq 'HEAD') { $mparent = $opt_o };
+                                       if ( -e "$git_dir/refs/heads/$mparent") {
+                                               $mparent = get_headref($mparent, $git_dir);
+                                               push (@parents, $mparent);
+                                               print OUT "Merge parent branch: $mparent\n" if $opt_v;
+                                       }
+                               }
+                       }
+                       my %seen_parents = ();
+                       my @unique_parents = grep { ! $seen_parents{$_} ++ } @parents;
+                       foreach my $bparent (@unique_parents) {
+                               push @par, '-p', $bparent;
+                               print OUT "Merge parent branch: $bparent\n" if $opt_v;
+                       }
+
+                       exec("env",
+                               "GIT_AUTHOR_NAME=$author_name",
+                               "GIT_AUTHOR_EMAIL=$author_email",
+                               "GIT_AUTHOR_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)),
+                               "GIT_COMMITTER_NAME=$committer_name",
+                               "GIT_COMMITTER_EMAIL=$committer_email",
+                               "GIT_COMMITTER_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)),
+                               "git-commit-tree", $tree,@par);
+                       die "Cannot exec git-commit-tree: $!\n";
+               }
+               $pw->writer();
+               $pr->reader();
+
+               $message =~ s/[\s\n]+\z//;
+               $message = "r$revision: $message" if $opt_r;
+
+               print $pw "$message\n"
+                       or die "Error writing to git-commit-tree: $!\n";
+               $pw->close();
+
+               print "Committed change $revision:$branch ".strftime("%Y-%m-%d %H:%M:%S",gmtime($date)).")\n" if $opt_v;
+               chomp($cid = <$pr>);
+               length($cid) == 40
+                       or die "Cannot get commit id ($cid): $!\n";
+               print "Commit ID $cid\n" if $opt_v;
+               $pr->close();
+
+               waitpid($pid,0);
+               die "Error running git-commit-tree: $?\n" if $?;
+       }
+
+       if (not defined $cid) {
+               $cid = $branches{"/"}{"LAST"};
+       }
+
+       if(not defined $dest) {
+               print "... no known parent\n" if $opt_v;
+       } elsif(not $tag) {
+               print "Writing to refs/heads/$dest\n" if $opt_v;
+               open(C,">$git_dir/refs/heads/$dest") and
+               print C ("$cid\n") and
+               close(C)
+                       or die "Cannot write branch $dest for update: $!\n";
+       }
+
+       if ($tag) {
+               $last_rev = "-" if %$changed_paths;
+               # the tag was 'complex', i.e. did not refer to a "real" revision
+
+               $dest =~ tr/_/\./ if $opt_u;
+
+               system('git-tag', '-f', $dest, $cid) == 0
+                       or die "Cannot create tag $dest: $!\n";
+
+               print "Created tag '$dest' on '$branch'\n" if $opt_v;
+       }
+       $branches{$branch}{"LAST"} = $cid;
+       $branches{$branch}{$revision} = $cid;
+       $last_rev = $cid;
+       print BRANCHES "$revision $branch $cid\n";
+       print "DONE: $revision $dest $cid\n" if $opt_v;
+}
+
+sub commit_all {
+       # Recursive use of the SVN connection does not work
+       local $svn = $svn2;
+
+       my ($changed_paths, $revision, $author, $date, $message) = @_;
+       my %p;
+       while(my($path,$action) = each %$changed_paths) {
+               $p{$path} = [ $action->action,$action->copyfrom_path, $action->copyfrom_rev, $path ];
+       }
+       $changed_paths = \%p;
+
+       my %done;
+       my @col;
+       my $pref;
+       my $branch;
+
+       while(my($path,$action) = each %$changed_paths) {
+               ($branch,$path) = split_path($revision,$path);
+               next if not defined $branch;
+               next if not defined $path;
+               $done{$branch}{$path} = $action;
+       }
+       while(($branch,$changed_paths) = each %done) {
+               commit($branch, $changed_paths, $revision, $author, $date, $message);
+       }
+}
+
+$opt_l = $svn->{'maxrev'} if not defined $opt_l or $opt_l > $svn->{'maxrev'};
+
+if ($opt_l < $current_rev) {
+    print "Up to date: no new revisions to fetch!\n" if $opt_v;
+    unlink("$git_dir/SVN2GIT_HEAD");
+    exit;
+}
+
+print "Processing from $current_rev to $opt_l ...\n" if $opt_v;
+
+my $from_rev;
+my $to_rev = $current_rev - 1;
+
+my $subpool = SVN::Pool::new_default_sub;
+while ($to_rev < $opt_l) {
+       $subpool->clear;
+       $from_rev = $to_rev + 1;
+       $to_rev = $from_rev + $repack_after;
+       $to_rev = $opt_l if $opt_l < $to_rev;
+       print "Fetching from $from_rev to $to_rev ...\n" if $opt_v;
+       $svn->{'svn'}->get_log("/",$from_rev,$to_rev,0,1,1,\&commit_all);
+       my $pid = fork();
+       die "Fork: $!\n" unless defined $pid;
+       unless($pid) {
+               exec("git-repack", "-d")
+                       or die "Cannot repack: $!\n";
+       }
+       waitpid($pid, 0);
+}
+
+
+unlink($git_index);
+
+if (defined $orig_git_index) {
+       $ENV{GIT_INDEX_FILE} = $orig_git_index;
+} else {
+       delete $ENV{GIT_INDEX_FILE};
+}
+
+# Now switch back to the branch we were in before all of this happened
+if($orig_branch) {
+       print "DONE\n" if $opt_v and (not defined $opt_l or $opt_l > 0);
+       system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master")
+               if $forward_master;
+       unless ($opt_i) {
+               system('git-read-tree', '-m', '-u', 'SVN2GIT_HEAD', 'HEAD');
+               die "read-tree failed: $?\n" if $?;
+       }
+} else {
+       $orig_branch = "master";
+       print "DONE; creating $orig_branch branch\n" if $opt_v and (not defined $opt_l or $opt_l > 0);
+       system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master")
+               unless -f "$git_dir/refs/heads/master";
+       system('git-update-ref', 'HEAD', "$orig_branch");
+       unless ($opt_i) {
+               system('git checkout');
+               die "checkout failed: $?\n" if $?;
+       }
+}
+unlink("$git_dir/SVN2GIT_HEAD");
+close(BRANCHES);
diff --git a/contrib/examples/git-svnimport.txt b/contrib/examples/git-svnimport.txt
new file mode 100644 (file)
index 0000000..71aad8b
--- /dev/null
@@ -0,0 +1,179 @@
+git-svnimport(1)
+================
+v0.1, July 2005
+
+NAME
+----
+git-svnimport - Import a SVN repository into git
+
+
+SYNOPSIS
+--------
+[verse]
+'git-svnimport' [ -o <branch-for-HEAD> ] [ -h ] [ -v ] [ -d | -D ]
+               [ -C <GIT_repository> ] [ -i ] [ -u ] [-l limit_rev]
+               [ -b branch_subdir ] [ -T trunk_subdir ] [ -t tag_subdir ]
+               [ -s start_chg ] [ -m ] [ -r ] [ -M regex ]
+               [ -I <ignorefile_name> ] [ -A <author_file> ]
+               [ -R <repack_each_revs>] [ -P <path_from_trunk> ]
+               <SVN_repository_URL> [ <path> ]
+
+
+DESCRIPTION
+-----------
+Imports a SVN repository into git. It will either create a new
+repository, or incrementally import into an existing one.
+
+SVN access is done by the SVN::Perl module.
+
+git-svnimport assumes that SVN repositories are organized into one
+"trunk" directory where the main development happens, "branches/FOO"
+directories for branches, and "/tags/FOO" directories for tags.
+Other subdirectories are ignored.
+
+git-svnimport creates a file ".git/svn2git", which is required for
+incremental SVN imports.
+
+OPTIONS
+-------
+-C <target-dir>::
+        The GIT repository to import to.  If the directory doesn't
+        exist, it will be created.  Default is the current directory.
+
+-s <start_rev>::
+        Start importing at this SVN change number. The  default is 1.
++
+When importing incrementally, you might need to edit the .git/svn2git file.
+
+-i::
+       Import-only: don't perform a checkout after importing.  This option
+       ensures the working directory and index remain untouched and will
+       not create them if they do not exist.
+
+-T <trunk_subdir>::
+       Name the SVN trunk. Default "trunk".
+
+-t <tag_subdir>::
+       Name the SVN subdirectory for tags. Default "tags".
+
+-b <branch_subdir>::
+       Name the SVN subdirectory for branches. Default "branches".
+
+-o <branch-for-HEAD>::
+       The 'trunk' branch from SVN is imported to the 'origin' branch within
+       the git repository. Use this option if you want to import into a
+       different branch.
+
+-r::
+       Prepend 'rX: ' to commit messages, where X is the imported
+       subversion revision.
+
+-u::
+       Replace underscores in tag names with periods.
+
+-I <ignorefile_name>::
+       Import the svn:ignore directory property to files with this
+       name in each directory. (The Subversion and GIT ignore
+       syntaxes are similar enough that using the Subversion patterns
+       directly with "-I .gitignore" will almost always just work.)
+
+-A <author_file>::
+       Read a file with lines on the form
++
+------
+       username = User's Full Name <email@addr.es>
+
+------
++
+and use "User's Full Name <email@addr.es>" as the GIT
+author and committer for Subversion commits made by
+"username". If encountering a commit made by a user not in the
+list, abort.
++
+For convenience, this data is saved to $GIT_DIR/svn-authors
+each time the -A option is provided, and read from that same
+file each time git-svnimport is run with an existing GIT
+repository without -A.
+
+-m::
+       Attempt to detect merges based on the commit message. This option
+       will enable default regexes that try to capture the name source
+       branch name from the commit message.
+
+-M <regex>::
+       Attempt to detect merges based on the commit message with a custom
+       regex. It can be used with -m to also see the default regexes.
+       You must escape forward slashes.
+
+-l <max_rev>::
+       Specify a maximum revision number to pull.
++
+Formerly, this option controlled how many revisions to pull,
+due to SVN memory leaks. (These have been worked around.)
+
+-R <repack_each_revs>::
+       Specify how often git repository should be repacked.
++
+The default value is 1000. git-svnimport will do import in chunks of 1000
+revisions, after each chunk git repository will be repacked. To disable
+this behavior specify some big value here which is mote than number of
+revisions to import.
+
+-P <path_from_trunk>::
+       Partial import of the SVN tree.
++
+By default, the whole tree on the SVN trunk (/trunk) is imported.
+'-P my/proj' will import starting only from '/trunk/my/proj'.
+This option is useful when you want to import one project from a
+svn repo which hosts multiple projects under the same trunk.
+
+-v::
+       Verbosity: let 'svnimport' report what it is doing.
+
+-d::
+       Use direct HTTP requests if possible. The "<path>" argument is used
+       only for retrieving the SVN logs; the path to the contents is
+       included in the SVN log.
+
+-D::
+       Use direct HTTP requests if possible. The "<path>" argument is used
+       for retrieving the logs, as well as for the contents.
++
+There's no safe way to automatically find out which of these options to
+use, so you need to try both. Usually, the one that's wrong will die
+with a 40x error pretty quickly.
+
+<SVN_repository_URL>::
+       The URL of the SVN module you want to import. For local
+       repositories, use "file:///absolute/path".
++
+If you're using the "-d" or "-D" option, this is the URL of the SVN
+repository itself; it usually ends in "/svn".
+
+<path>::
+       The path to the module you want to check out.
+
+-h::
+       Print a short usage message and exit.
+
+OUTPUT
+------
+If '-v' is specified, the script reports what it is doing.
+
+Otherwise, success is indicated the Unix way, i.e. by simply exiting with
+a zero exit status.
+
+Author
+------
+Written by Matthias Urlichs <smurf@smurf.noris.de>, with help from
+various participants of the git-list <git@vger.kernel.org>.
+
+Based on a cvs2git script by the same author.
+
+Documentation
+--------------
+Documentation by Matthias Urlichs <smurf@smurf.noris.de>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/contrib/examples/git-tag.sh b/contrib/examples/git-tag.sh
new file mode 100755 (executable)
index 0000000..ae7c531
--- /dev/null
@@ -0,0 +1,205 @@
+#!/bin/sh
+# Copyright (c) 2005 Linus Torvalds
+
+USAGE='[-n [<num>]] -l [<pattern>] | [-a | -s | -u <key-id>] [-f | -d | -v] [-m <msg>] <tagname> [<head>]'
+SUBDIRECTORY_OK='Yes'
+. git-sh-setup
+
+message_given=
+annotate=
+signed=
+force=
+message=
+username=
+list=
+verify=
+LINES=0
+while test $# != 0
+do
+    case "$1" in
+    -a)
+       annotate=1
+       shift
+       ;;
+    -s)
+       annotate=1
+       signed=1
+       shift
+       ;;
+    -f)
+       force=1
+       shift
+       ;;
+    -n)
+        case "$#,$2" in
+       1,* | *,-*)
+               LINES=1         # no argument
+               ;;
+       *)      shift
+               LINES=$(expr "$1" : '\([0-9]*\)')
+               [ -z "$LINES" ] && LINES=1 # 1 line is default when -n is used
+               ;;
+       esac
+       shift
+       ;;
+    -l)
+       list=1
+       shift
+       case $# in
+       0)      PATTERN=
+               ;;
+       *)
+               PATTERN="$1"    # select tags by shell pattern, not re
+               shift
+               ;;
+       esac
+       git rev-parse --symbolic --tags | sort |
+           while read TAG
+           do
+               case "$TAG" in
+               *$PATTERN*) ;;
+               *)          continue ;;
+               esac
+               [ "$LINES" -le 0 ] && { echo "$TAG"; continue ;}
+               OBJTYPE=$(git cat-file -t "$TAG")
+               case $OBJTYPE in
+               tag)
+                       ANNOTATION=$(git cat-file tag "$TAG" |
+                               sed -e '1,/^$/d' |
+                               sed -n -e "
+                                       /^-----BEGIN PGP SIGNATURE-----\$/q
+                                       2,\$s/^/    /
+                                       p
+                                       ${LINES}q
+                               ")
+                       printf "%-15s %s\n" "$TAG" "$ANNOTATION"
+                       ;;
+               *)      echo "$TAG"
+                       ;;
+               esac
+           done
+       ;;
+    -m)
+       annotate=1
+       shift
+       message="$1"
+       if test "$#" = "0"; then
+           die "error: option -m needs an argument"
+       else
+           message="$1"
+           message_given=1
+           shift
+       fi
+       ;;
+    -F)
+       annotate=1
+       shift
+       if test "$#" = "0"; then
+           die "error: option -F needs an argument"
+       else
+           message="$(cat "$1")"
+           message_given=1
+           shift
+       fi
+       ;;
+    -u)
+       annotate=1
+       signed=1
+       shift
+       if test "$#" = "0"; then
+           die "error: option -u needs an argument"
+       else
+           username="$1"
+           shift
+       fi
+       ;;
+    -d)
+       shift
+       had_error=0
+       for tag
+       do
+               cur=$(git show-ref --verify --hash -- "refs/tags/$tag") || {
+                       echo >&2 "Seriously, what tag are you talking about?"
+                       had_error=1
+                       continue
+               }
+               git update-ref -m 'tag: delete' -d "refs/tags/$tag" "$cur" || {
+                       had_error=1
+                       continue
+               }
+               echo "Deleted tag $tag."
+       done
+       exit $had_error
+       ;;
+    -v)
+       shift
+       tag_name="$1"
+       tag=$(git show-ref --verify --hash -- "refs/tags/$tag_name") ||
+               die "Seriously, what tag are you talking about?"
+       git-verify-tag -v "$tag"
+       exit $?
+       ;;
+    -*)
+        usage
+       ;;
+    *)
+       break
+       ;;
+    esac
+done
+
+[ -n "$list" ] && exit 0
+
+name="$1"
+[ "$name" ] || usage
+prev=0000000000000000000000000000000000000000
+if git show-ref --verify --quiet -- "refs/tags/$name"
+then
+    test -n "$force" || die "tag '$name' already exists"
+    prev=`git rev-parse "refs/tags/$name"`
+fi
+shift
+git check-ref-format "tags/$name" ||
+       die "we do not like '$name' as a tag name."
+
+object=$(git rev-parse --verify --default HEAD "$@") || exit 1
+type=$(git cat-file -t $object) || exit 1
+tagger=$(git-var GIT_COMMITTER_IDENT) || exit 1
+
+test -n "$username" ||
+       username=$(git repo-config user.signingkey) ||
+       username=$(expr "z$tagger" : 'z\(.*>\)')
+
+trap 'rm -f "$GIT_DIR"/TAG_TMP* "$GIT_DIR"/TAG_FINALMSG "$GIT_DIR"/TAG_EDITMSG' 0
+
+if [ "$annotate" ]; then
+    if [ -z "$message_given" ]; then
+        ( echo "#"
+          echo "# Write a tag message"
+          echo "#" ) > "$GIT_DIR"/TAG_EDITMSG
+        git_editor "$GIT_DIR"/TAG_EDITMSG || exit
+    else
+        printf '%s\n' "$message" >"$GIT_DIR"/TAG_EDITMSG
+    fi
+
+    grep -v '^#' <"$GIT_DIR"/TAG_EDITMSG |
+    git stripspace >"$GIT_DIR"/TAG_FINALMSG
+
+    [ -s "$GIT_DIR"/TAG_FINALMSG -o -n "$message_given" ] || {
+       echo >&2 "No tag message?"
+       exit 1
+    }
+
+    ( printf 'object %s\ntype %s\ntag %s\ntagger %s\n\n' \
+       "$object" "$type" "$name" "$tagger";
+      cat "$GIT_DIR"/TAG_FINALMSG ) >"$GIT_DIR"/TAG_TMP
+    rm -f "$GIT_DIR"/TAG_TMP.asc "$GIT_DIR"/TAG_FINALMSG
+    if [ "$signed" ]; then
+       gpg -bsa -u "$username" "$GIT_DIR"/TAG_TMP &&
+       cat "$GIT_DIR"/TAG_TMP.asc >>"$GIT_DIR"/TAG_TMP ||
+       die "failed to sign the tag with GPG."
+    fi
+    object=$(git-mktag < "$GIT_DIR"/TAG_TMP)
+fi
+
+git update-ref "refs/tags/$name" "$object" "$prev"
diff --git a/contrib/examples/git-verify-tag.sh b/contrib/examples/git-verify-tag.sh
new file mode 100755 (executable)
index 0000000..0902a5c
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+USAGE='<tag>'
+SUBDIRECTORY_OK='Yes'
+. git-sh-setup
+
+verbose=
+while test $# != 0
+do
+       case "$1" in
+       -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
+               verbose=t ;;
+       *)
+               break ;;
+       esac
+       shift
+done
+
+if [ "$#" != "1" ]
+then
+       usage
+fi
+
+type="$(git cat-file -t "$1" 2>/dev/null)" ||
+       die "$1: no such object."
+
+test "$type" = tag ||
+       die "$1: cannot verify a non-tag object of type $type."
+
+case "$verbose" in
+t)
+       git cat-file -p "$1" |
+       sed -n -e '/^-----BEGIN PGP SIGNATURE-----/q' -e p
+       ;;
+esac
+
+trap 'rm -f "$GIT_DIR/.tmp-vtag"' 0
+
+git cat-file tag "$1" >"$GIT_DIR/.tmp-vtag" || exit 1
+sed -n -e '
+       /^-----BEGIN PGP SIGNATURE-----$/q
+       p
+' <"$GIT_DIR/.tmp-vtag" |
+gpg --verify "$GIT_DIR/.tmp-vtag" - || exit 1
+rm -f "$GIT_DIR/.tmp-vtag"
diff --git a/contrib/fast-import/git-import.perl b/contrib/fast-import/git-import.perl
new file mode 100755 (executable)
index 0000000..f9fef6d
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/perl
+#
+# Performs an initial import of a directory. This is the equivalent
+# of doing 'git init; git add .; git commit'. It's a little slower,
+# but is meant to be a simple fast-import example.
+
+use strict;
+use File::Find;
+
+my $USAGE = 'Usage: git-import branch import-message';
+my $branch = shift or die "$USAGE\n";
+my $message = shift or die "$USAGE\n";
+
+chomp(my $username = `git config user.name`);
+chomp(my $email = `git config user.email`);
+die 'You need to set user name and email'
+  unless $username && $email;
+
+system('git init');
+open(my $fi, '|-', qw(git fast-import --date-format=now))
+  or die "unable to spawn fast-import: $!";
+
+print $fi <<EOF;
+commit refs/heads/$branch
+committer $username <$email> now
+data <<MSGEOF
+$message
+MSGEOF
+
+EOF
+
+find(
+  sub {
+    if($File::Find::name eq './.git') {
+      $File::Find::prune = 1;
+      return;
+    }
+    return unless -f $_;
+
+    my $fn = $File::Find::name;
+    $fn =~ s#^.\/##;
+
+    open(my $in, '<', $_)
+      or die "unable to open $fn: $!";
+    my @st = stat($in)
+      or die "unable to stat $fn: $!";
+    my $len = $st[7];
+
+    print $fi "M 644 inline $fn\n";
+    print $fi "data $len\n";
+    while($len > 0) {
+      my $r = read($in, my $buf, $len < 4096 ? $len : 4096);
+      defined($r) or die "read error from $fn: $!";
+      $r > 0 or die "premature EOF from $fn: $!";
+      print $fi $buf;
+      $len -= $r;
+    }
+    print $fi "\n";
+
+  }, '.'
+);
+
+close($fi);
+exit $?;
diff --git a/contrib/fast-import/git-import.sh b/contrib/fast-import/git-import.sh
new file mode 100755 (executable)
index 0000000..0ca7718
--- /dev/null
@@ -0,0 +1,38 @@
+#!/bin/sh
+#
+# Performs an initial import of a directory. This is the equivalent
+# of doing 'git init; git add .; git commit'. It's a lot slower,
+# but is meant to be a simple fast-import example.
+
+if [ -z "$1" -o -z "$2" ]; then
+       echo "Usage: git-import branch import-message"
+       exit 1
+fi
+
+USERNAME="$(git config user.name)"
+EMAIL="$(git config user.email)"
+
+if [ -z "$USERNAME" -o -z "$EMAIL" ]; then
+       echo "You need to set user name and email"
+       exit 1
+fi
+
+git init
+
+(
+       cat <<EOF
+commit refs/heads/$1
+committer $USERNAME <$EMAIL> now
+data <<MSGEOF
+$2
+MSGEOF
+
+EOF
+       find * -type f|while read i;do
+               echo "M 100644 inline $i"
+               echo data $(stat -c '%s' "$i")
+               cat "$i"
+               echo
+       done
+       echo
+) | git fast-import --date-format=now
diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4
new file mode 100755 (executable)
index 0000000..c80a6da
--- /dev/null
@@ -0,0 +1,1828 @@
+#!/usr/bin/env python
+#
+# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
+#
+# Author: Simon Hausmann <simon@lst.de>
+# Copyright: 2007 Simon Hausmann <simon@lst.de>
+#            2007 Trolltech ASA
+# License: MIT <http://www.opensource.org/licenses/mit-license.php>
+#
+
+import optparse, sys, os, marshal, popen2, subprocess, shelve
+import tempfile, getopt, sha, os.path, time, platform
+import re
+
+from sets import Set;
+
+verbose = False
+
+def die(msg):
+    if verbose:
+        raise Exception(msg)
+    else:
+        sys.stderr.write(msg + "\n")
+        sys.exit(1)
+
+def write_pipe(c, str):
+    if verbose:
+        sys.stderr.write('Writing pipe: %s\n' % c)
+
+    pipe = os.popen(c, 'w')
+    val = pipe.write(str)
+    if pipe.close():
+        die('Command failed: %s' % c)
+
+    return val
+
+def read_pipe(c, ignore_error=False):
+    if verbose:
+        sys.stderr.write('Reading pipe: %s\n' % c)
+
+    pipe = os.popen(c, 'rb')
+    val = pipe.read()
+    if pipe.close() and not ignore_error:
+        die('Command failed: %s' % c)
+
+    return val
+
+
+def read_pipe_lines(c):
+    if verbose:
+        sys.stderr.write('Reading pipe: %s\n' % c)
+    ## todo: check return status
+    pipe = os.popen(c, 'rb')
+    val = pipe.readlines()
+    if pipe.close():
+        die('Command failed: %s' % c)
+
+    return val
+
+def system(cmd):
+    if verbose:
+        sys.stderr.write("executing %s\n" % cmd)
+    if os.system(cmd) != 0:
+        die("command failed: %s" % cmd)
+
+def isP4Exec(kind):
+    """Determine if a Perforce 'kind' should have execute permission
+
+    'p4 help filetypes' gives a list of the types.  If it starts with 'x',
+    or x follows one of a few letters.  Otherwise, if there is an 'x' after
+    a plus sign, it is also executable"""
+    return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
+
+def setP4ExecBit(file, mode):
+    # Reopens an already open file and changes the execute bit to match
+    # the execute bit setting in the passed in mode.
+
+    p4Type = "+x"
+
+    if not isModeExec(mode):
+        p4Type = getP4OpenedType(file)
+        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
+        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
+        if p4Type[-1] == "+":
+            p4Type = p4Type[0:-1]
+
+    system("p4 reopen -t %s %s" % (p4Type, file))
+
+def getP4OpenedType(file):
+    # Returns the perforce file type for the given file.
+
+    result = read_pipe("p4 opened %s" % file)
+    match = re.match(".*\((.+)\)$", result)
+    if match:
+        return match.group(1)
+    else:
+        die("Could not determine file type for %s" % file)
+
+def diffTreePattern():
+    # This is a simple generator for the diff tree regex pattern. This could be
+    # a class variable if this and parseDiffTreeEntry were a part of a class.
+    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
+    while True:
+        yield pattern
+
+def parseDiffTreeEntry(entry):
+    """Parses a single diff tree entry into its component elements.
+
+    See git-diff-tree(1) manpage for details about the format of the diff
+    output. This method returns a dictionary with the following elements:
+
+    src_mode - The mode of the source file
+    dst_mode - The mode of the destination file
+    src_sha1 - The sha1 for the source file
+    dst_sha1 - The sha1 fr the destination file
+    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
+    status_score - The score for the status (applicable for 'C' and 'R'
+                   statuses). This is None if there is no score.
+    src - The path for the source file.
+    dst - The path for the destination file. This is only present for
+          copy or renames. If it is not present, this is None.
+
+    If the pattern is not matched, None is returned."""
+
+    match = diffTreePattern().next().match(entry)
+    if match:
+        return {
+            'src_mode': match.group(1),
+            'dst_mode': match.group(2),
+            'src_sha1': match.group(3),
+            'dst_sha1': match.group(4),
+            'status': match.group(5),
+            'status_score': match.group(6),
+            'src': match.group(7),
+            'dst': match.group(10)
+        }
+    return None
+
+def isModeExec(mode):
+    # Returns True if the given git mode represents an executable file,
+    # otherwise False.
+    return mode[-3:] == "755"
+
+def isModeExecChanged(src_mode, dst_mode):
+    return isModeExec(src_mode) != isModeExec(dst_mode)
+
+def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
+    cmd = "p4 -G %s" % cmd
+    if verbose:
+        sys.stderr.write("Opening pipe: %s\n" % cmd)
+
+    # Use a temporary file to avoid deadlocks without
+    # subprocess.communicate(), which would put another copy
+    # of stdout into memory.
+    stdin_file = None
+    if stdin is not None:
+        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
+        stdin_file.write(stdin)
+        stdin_file.flush()
+        stdin_file.seek(0)
+
+    p4 = subprocess.Popen(cmd, shell=True,
+                          stdin=stdin_file,
+                          stdout=subprocess.PIPE)
+
+    result = []
+    try:
+        while True:
+            entry = marshal.load(p4.stdout)
+            result.append(entry)
+    except EOFError:
+        pass
+    exitCode = p4.wait()
+    if exitCode != 0:
+        entry = {}
+        entry["p4ExitCode"] = exitCode
+        result.append(entry)
+
+    return result
+
+def p4Cmd(cmd):
+    list = p4CmdList(cmd)
+    result = {}
+    for entry in list:
+        result.update(entry)
+    return result;
+
+def p4Where(depotPath):
+    if not depotPath.endswith("/"):
+        depotPath += "/"
+    output = p4Cmd("where %s..." % depotPath)
+    if output["code"] == "error":
+        return ""
+    clientPath = ""
+    if "path" in output:
+        clientPath = output.get("path")
+    elif "data" in output:
+        data = output.get("data")
+        lastSpace = data.rfind(" ")
+        clientPath = data[lastSpace + 1:]
+
+    if clientPath.endswith("..."):
+        clientPath = clientPath[:-3]
+    return clientPath
+
+def currentGitBranch():
+    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
+
+def isValidGitDir(path):
+    if (os.path.exists(path + "/HEAD")
+        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
+        return True;
+    return False
+
+def parseRevision(ref):
+    return read_pipe("git rev-parse %s" % ref).strip()
+
+def extractLogMessageFromGitCommit(commit):
+    logMessage = ""
+
+    ## fixme: title is first line of commit, not 1st paragraph.
+    foundTitle = False
+    for log in read_pipe_lines("git cat-file commit %s" % commit):
+       if not foundTitle:
+           if len(log) == 1:
+               foundTitle = True
+           continue
+
+       logMessage += log
+    return logMessage
+
+def extractSettingsGitLog(log):
+    values = {}
+    for line in log.split("\n"):
+        line = line.strip()
+        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
+        if not m:
+            continue
+
+        assignments = m.group(1).split (':')
+        for a in assignments:
+            vals = a.split ('=')
+            key = vals[0].strip()
+            val = ('='.join (vals[1:])).strip()
+            if val.endswith ('\"') and val.startswith('"'):
+                val = val[1:-1]
+
+            values[key] = val
+
+    paths = values.get("depot-paths")
+    if not paths:
+        paths = values.get("depot-path")
+    if paths:
+        values['depot-paths'] = paths.split(',')
+    return values
+
+def gitBranchExists(branch):
+    proc = subprocess.Popen(["git", "rev-parse", branch],
+                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
+    return proc.wait() == 0;
+
+def gitConfig(key):
+    return read_pipe("git config %s" % key, ignore_error=True).strip()
+
+def p4BranchesInGit(branchesAreInRemotes = True):
+    branches = {}
+
+    cmdline = "git rev-parse --symbolic "
+    if branchesAreInRemotes:
+        cmdline += " --remotes"
+    else:
+        cmdline += " --branches"
+
+    for line in read_pipe_lines(cmdline):
+        line = line.strip()
+
+        ## only import to p4/
+        if not line.startswith('p4/') or line == "p4/HEAD":
+            continue
+        branch = line
+
+        # strip off p4
+        branch = re.sub ("^p4/", "", line)
+
+        branches[branch] = parseRevision(line)
+    return branches
+
+def findUpstreamBranchPoint(head = "HEAD"):
+    branches = p4BranchesInGit()
+    # map from depot-path to branch name
+    branchByDepotPath = {}
+    for branch in branches.keys():
+        tip = branches[branch]
+        log = extractLogMessageFromGitCommit(tip)
+        settings = extractSettingsGitLog(log)
+        if settings.has_key("depot-paths"):
+            paths = ",".join(settings["depot-paths"])
+            branchByDepotPath[paths] = "remotes/p4/" + branch
+
+    settings = None
+    parent = 0
+    while parent < 65535:
+        commit = head + "~%s" % parent
+        log = extractLogMessageFromGitCommit(commit)
+        settings = extractSettingsGitLog(log)
+        if settings.has_key("depot-paths"):
+            paths = ",".join(settings["depot-paths"])
+            if branchByDepotPath.has_key(paths):
+                return [branchByDepotPath[paths], settings]
+
+        parent = parent + 1
+
+    return ["", settings]
+
+def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
+    if not silent:
+        print ("Creating/updating branch(es) in %s based on origin branch(es)"
+               % localRefPrefix)
+
+    originPrefix = "origin/p4/"
+
+    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
+        line = line.strip()
+        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
+            continue
+
+        headName = line[len(originPrefix):]
+        remoteHead = localRefPrefix + headName
+        originHead = line
+
+        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
+        if (not original.has_key('depot-paths')
+            or not original.has_key('change')):
+            continue
+
+        update = False
+        if not gitBranchExists(remoteHead):
+            if verbose:
+                print "creating %s" % remoteHead
+            update = True
+        else:
+            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
+            if settings.has_key('change') > 0:
+                if settings['depot-paths'] == original['depot-paths']:
+                    originP4Change = int(original['change'])
+                    p4Change = int(settings['change'])
+                    if originP4Change > p4Change:
+                        print ("%s (%s) is newer than %s (%s). "
+                               "Updating p4 branch from origin."
+                               % (originHead, originP4Change,
+                                  remoteHead, p4Change))
+                        update = True
+                else:
+                    print ("Ignoring: %s was imported from %s while "
+                           "%s was imported from %s"
+                           % (originHead, ','.join(original['depot-paths']),
+                              remoteHead, ','.join(settings['depot-paths'])))
+
+        if update:
+            system("git update-ref %s %s" % (remoteHead, originHead))
+
+def originP4BranchesExist():
+        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
+
+def p4ChangesForPaths(depotPaths, changeRange):
+    assert depotPaths
+    output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, changeRange)
+                                                        for p in depotPaths]))
+
+    changes = []
+    for line in output:
+        changeNum = line.split(" ")[1]
+        changes.append(int(changeNum))
+
+    changes.sort()
+    return changes
+
+class Command:
+    def __init__(self):
+        self.usage = "usage: %prog [options]"
+        self.needsGit = True
+
+class P4Debug(Command):
+    def __init__(self):
+        Command.__init__(self)
+        self.options = [
+            optparse.make_option("--verbose", dest="verbose", action="store_true",
+                                 default=False),
+            ]
+        self.description = "A tool to debug the output of p4 -G."
+        self.needsGit = False
+        self.verbose = False
+
+    def run(self, args):
+        j = 0
+        for output in p4CmdList(" ".join(args)):
+            print 'Element: %d' % j
+            j += 1
+            print output
+        return True
+
+class P4RollBack(Command):
+    def __init__(self):
+        Command.__init__(self)
+        self.options = [
+            optparse.make_option("--verbose", dest="verbose", action="store_true"),
+            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
+        ]
+        self.description = "A tool to debug the multi-branch import. Don't use :)"
+        self.verbose = False
+        self.rollbackLocalBranches = False
+
+    def run(self, args):
+        if len(args) != 1:
+            return False
+        maxChange = int(args[0])
+
+        if "p4ExitCode" in p4Cmd("changes -m 1"):
+            die("Problems executing p4");
+
+        if self.rollbackLocalBranches:
+            refPrefix = "refs/heads/"
+            lines = read_pipe_lines("git rev-parse --symbolic --branches")
+        else:
+            refPrefix = "refs/remotes/"
+            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
+
+        for line in lines:
+            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
+                line = line.strip()
+                ref = refPrefix + line
+                log = extractLogMessageFromGitCommit(ref)
+                settings = extractSettingsGitLog(log)
+
+                depotPaths = settings['depot-paths']
+                change = settings['change']
+
+                changed = False
+
+                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
+                                                           for p in depotPaths]))) == 0:
+                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
+                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
+                    continue
+
+                while change and int(change) > maxChange:
+                    changed = True
+                    if self.verbose:
+                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
+                    system("git update-ref %s \"%s^\"" % (ref, ref))
+                    log = extractLogMessageFromGitCommit(ref)
+                    settings =  extractSettingsGitLog(log)
+
+
+                    depotPaths = settings['depot-paths']
+                    change = settings['change']
+
+                if changed:
+                    print "%s rewound to %s" % (ref, change)
+
+        return True
+
+class P4Submit(Command):
+    def __init__(self):
+        Command.__init__(self)
+        self.options = [
+                optparse.make_option("--continue", action="store_false", dest="firstTime"),
+                optparse.make_option("--verbose", dest="verbose", action="store_true"),
+                optparse.make_option("--origin", dest="origin"),
+                optparse.make_option("--reset", action="store_true", dest="reset"),
+                optparse.make_option("--log-substitutions", dest="substFile"),
+                optparse.make_option("--dry-run", action="store_true"),
+                optparse.make_option("--direct", dest="directSubmit", action="store_true"),
+                optparse.make_option("--trust-me-like-a-fool", dest="trustMeLikeAFool", action="store_true"),
+                optparse.make_option("-M", dest="detectRename", action="store_true"),
+        ]
+        self.description = "Submit changes from git to the perforce depot."
+        self.usage += " [name of git branch to submit into perforce depot]"
+        self.firstTime = True
+        self.reset = False
+        self.interactive = True
+        self.dryRun = False
+        self.substFile = ""
+        self.firstTime = True
+        self.origin = ""
+        self.directSubmit = False
+        self.trustMeLikeAFool = False
+        self.detectRename = False
+        self.verbose = False
+        self.isWindows = (platform.system() == "Windows")
+
+        self.logSubstitutions = {}
+        self.logSubstitutions["<enter description here>"] = "%log%"
+        self.logSubstitutions["\tDetails:"] = "\tDetails:  %log%"
+
+    def check(self):
+        if len(p4CmdList("opened ...")) > 0:
+            die("You have files opened with perforce! Close them before starting the sync.")
+
+    def start(self):
+        if len(self.config) > 0 and not self.reset:
+            die("Cannot start sync. Previous sync config found at %s\n"
+                "If you want to start submitting again from scratch "
+                "maybe you want to call git-p4 submit --reset" % self.configFile)
+
+        commits = []
+        if self.directSubmit:
+            commits.append("0")
+        else:
+            for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
+                commits.append(line.strip())
+            commits.reverse()
+
+        self.config["commits"] = commits
+
+    def prepareLogMessage(self, template, message):
+        result = ""
+
+        for line in template.split("\n"):
+            if line.startswith("#"):
+                result += line + "\n"
+                continue
+
+            substituted = False
+            for key in self.logSubstitutions.keys():
+                if line.find(key) != -1:
+                    value = self.logSubstitutions[key]
+                    value = value.replace("%log%", message)
+                    if value != "@remove@":
+                        result += line.replace(key, value) + "\n"
+                    substituted = True
+                    break
+
+            if not substituted:
+                result += line + "\n"
+
+        return result
+
+    def prepareSubmitTemplate(self):
+        # remove lines in the Files section that show changes to files outside the depot path we're committing into
+        template = ""
+        inFilesSection = False
+        for line in read_pipe_lines("p4 change -o"):
+            if inFilesSection:
+                if line.startswith("\t"):
+                    # path starts and ends with a tab
+                    path = line[1:]
+                    lastTab = path.rfind("\t")
+                    if lastTab != -1:
+                        path = path[:lastTab]
+                        if not path.startswith(self.depotPath):
+                            continue
+                else:
+                    inFilesSection = False
+            else:
+                if line.startswith("Files:"):
+                    inFilesSection = True
+
+            template += line
+
+        return template
+
+    def applyCommit(self, id):
+        if self.directSubmit:
+            print "Applying local change in working directory/index"
+            diff = self.diffStatus
+        else:
+            print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
+            diffOpts = ("", "-M")[self.detectRename]
+            diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
+        filesToAdd = set()
+        filesToDelete = set()
+        editedFiles = set()
+        filesToChangeExecBit = {}
+        for line in diff:
+            diff = parseDiffTreeEntry(line)
+            modifier = diff['status']
+            path = diff['src']
+            if modifier == "M":
+                system("p4 edit \"%s\"" % path)
+                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
+                    filesToChangeExecBit[path] = diff['dst_mode']
+                editedFiles.add(path)
+            elif modifier == "A":
+                filesToAdd.add(path)
+                filesToChangeExecBit[path] = diff['dst_mode']
+                if path in filesToDelete:
+                    filesToDelete.remove(path)
+            elif modifier == "D":
+                filesToDelete.add(path)
+                if path in filesToAdd:
+                    filesToAdd.remove(path)
+            elif modifier == "R":
+                src, dest = diff['src'], diff['dst']
+                system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
+                system("p4 edit \"%s\"" % (dest))
+                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
+                    filesToChangeExecBit[dest] = diff['dst_mode']
+                os.unlink(dest)
+                editedFiles.add(dest)
+                filesToDelete.add(src)
+            else:
+                die("unknown modifier %s for %s" % (modifier, path))
+
+        if self.directSubmit:
+            diffcmd = "cat \"%s\"" % self.diffFile
+        else:
+            diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
+        patchcmd = diffcmd + " | git apply "
+        tryPatchCmd = patchcmd + "--check -"
+        applyPatchCmd = patchcmd + "--check --apply -"
+
+        if os.system(tryPatchCmd) != 0:
+            print "Unfortunately applying the change failed!"
+            print "What do you want to do?"
+            response = "x"
+            while response != "s" and response != "a" and response != "w":
+                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
+                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
+            if response == "s":
+                print "Skipping! Good luck with the next patches..."
+                for f in editedFiles:
+                    system("p4 revert \"%s\"" % f);
+                for f in filesToAdd:
+                    system("rm %s" %f)
+                return
+            elif response == "a":
+                os.system(applyPatchCmd)
+                if len(filesToAdd) > 0:
+                    print "You may also want to call p4 add on the following files:"
+                    print " ".join(filesToAdd)
+                if len(filesToDelete):
+                    print "The following files should be scheduled for deletion with p4 delete:"
+                    print " ".join(filesToDelete)
+                die("Please resolve and submit the conflict manually and "
+                    + "continue afterwards with git-p4 submit --continue")
+            elif response == "w":
+                system(diffcmd + " > patch.txt")
+                print "Patch saved to patch.txt in %s !" % self.clientPath
+                die("Please resolve and submit the conflict manually and "
+                    "continue afterwards with git-p4 submit --continue")
+
+        system(applyPatchCmd)
+
+        for f in filesToAdd:
+            system("p4 add \"%s\"" % f)
+        for f in filesToDelete:
+            system("p4 revert \"%s\"" % f)
+            system("p4 delete \"%s\"" % f)
+
+        # Set/clear executable bits
+        for f in filesToChangeExecBit.keys():
+            mode = filesToChangeExecBit[f]
+            setP4ExecBit(f, mode)
+
+        logMessage = ""
+        if not self.directSubmit:
+            logMessage = extractLogMessageFromGitCommit(id)
+            logMessage = logMessage.replace("\n", "\n\t")
+            if self.isWindows:
+                logMessage = logMessage.replace("\n", "\r\n")
+            logMessage = logMessage.strip()
+
+        template = self.prepareSubmitTemplate()
+
+        if self.interactive:
+            submitTemplate = self.prepareLogMessage(template, logMessage)
+            diff = read_pipe("p4 diff -du ...")
+
+            for newFile in filesToAdd:
+                diff += "==== new file ====\n"
+                diff += "--- /dev/null\n"
+                diff += "+++ %s\n" % newFile
+                f = open(newFile, "r")
+                for line in f.readlines():
+                    diff += "+" + line
+                f.close()
+
+            separatorLine = "######## everything below this line is just the diff #######"
+            if platform.system() == "Windows":
+                separatorLine += "\r"
+            separatorLine += "\n"
+
+            response = "e"
+            if self.trustMeLikeAFool:
+                response = "y"
+
+            firstIteration = True
+            while response == "e":
+                if not firstIteration:
+                    response = raw_input("Do you want to submit this change? [y]es/[e]dit/[n]o/[s]kip ")
+                firstIteration = False
+                if response == "e":
+                    [handle, fileName] = tempfile.mkstemp()
+                    tmpFile = os.fdopen(handle, "w+")
+                    tmpFile.write(submitTemplate + separatorLine + diff)
+                    tmpFile.close()
+                    defaultEditor = "vi"
+                    if platform.system() == "Windows":
+                        defaultEditor = "notepad"
+                    editor = os.environ.get("EDITOR", defaultEditor);
+                    system(editor + " " + fileName)
+                    tmpFile = open(fileName, "rb")
+                    message = tmpFile.read()
+                    tmpFile.close()
+                    os.remove(fileName)
+                    submitTemplate = message[:message.index(separatorLine)]
+                    if self.isWindows:
+                        submitTemplate = submitTemplate.replace("\r\n", "\n")
+
+            if response == "y" or response == "yes":
+               if self.dryRun:
+                   print submitTemplate
+                   raw_input("Press return to continue...")
+               else:
+                   if self.directSubmit:
+                       print "Submitting to git first"
+                       os.chdir(self.oldWorkingDirectory)
+                       write_pipe("git commit -a -F -", submitTemplate)
+                       os.chdir(self.clientPath)
+
+                   write_pipe("p4 submit -i", submitTemplate)
+            elif response == "s":
+                for f in editedFiles:
+                    system("p4 revert \"%s\"" % f);
+                for f in filesToAdd:
+                    system("p4 revert \"%s\"" % f);
+                    system("rm %s" %f)
+                for f in filesToDelete:
+                    system("p4 delete \"%s\"" % f);
+                return
+            else:
+                print "Not submitting!"
+                self.interactive = False
+        else:
+            fileName = "submit.txt"
+            file = open(fileName, "w+")
+            file.write(self.prepareLogMessage(template, logMessage))
+            file.close()
+            print ("Perforce submit template written as %s. "
+                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
+                   % (fileName, fileName))
+
+    def run(self, args):
+        if len(args) == 0:
+            self.master = currentGitBranch()
+            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
+                die("Detecting current git branch failed!")
+        elif len(args) == 1:
+            self.master = args[0]
+        else:
+            return False
+
+        [upstream, settings] = findUpstreamBranchPoint()
+        self.depotPath = settings['depot-paths'][0]
+        if len(self.origin) == 0:
+            self.origin = upstream
+
+        if self.verbose:
+            print "Origin branch is " + self.origin
+
+        if len(self.depotPath) == 0:
+            print "Internal error: cannot locate perforce depot path from existing branches"
+            sys.exit(128)
+
+        self.clientPath = p4Where(self.depotPath)
+
+        if len(self.clientPath) == 0:
+            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
+            sys.exit(128)
+
+        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
+        self.oldWorkingDirectory = os.getcwd()
+
+        if self.directSubmit:
+            self.diffStatus = read_pipe_lines("git diff -r --name-status HEAD")
+            if len(self.diffStatus) == 0:
+                print "No changes in working directory to submit."
+                return True
+            patch = read_pipe("git diff -p --binary --diff-filter=ACMRTUXB HEAD")
+            self.diffFile = self.gitdir + "/p4-git-diff"
+            f = open(self.diffFile, "wb")
+            f.write(patch)
+            f.close();
+
+        os.chdir(self.clientPath)
+        print "Syncronizing p4 checkout..."
+        system("p4 sync ...")
+
+        if self.reset:
+            self.firstTime = True
+
+        if len(self.substFile) > 0:
+            for line in open(self.substFile, "r").readlines():
+                tokens = line.strip().split("=")
+                self.logSubstitutions[tokens[0]] = tokens[1]
+
+        self.check()
+        self.configFile = self.gitdir + "/p4-git-sync.cfg"
+        self.config = shelve.open(self.configFile, writeback=True)
+
+        if self.firstTime:
+            self.start()
+
+        commits = self.config.get("commits", [])
+
+        while len(commits) > 0:
+            self.firstTime = False
+            commit = commits[0]
+            commits = commits[1:]
+            self.config["commits"] = commits
+            self.applyCommit(commit)
+            if not self.interactive:
+                break
+
+        self.config.close()
+
+        if self.directSubmit:
+            os.remove(self.diffFile)
+
+        if len(commits) == 0:
+            if self.firstTime:
+                print "No changes found to apply between %s and current HEAD" % self.origin
+            else:
+                print "All changes applied!"
+                os.chdir(self.oldWorkingDirectory)
+
+                sync = P4Sync()
+                sync.run([])
+
+                response = raw_input("Do you want to rebase current HEAD from Perforce now using git-p4 rebase? [y]es/[n]o ")
+                if response == "y" or response == "yes":
+                    rebase = P4Rebase()
+                    rebase.rebase()
+            os.remove(self.configFile)
+
+        return True
+
+class P4Sync(Command):
+    def __init__(self):
+        Command.__init__(self)
+        self.options = [
+                optparse.make_option("--branch", dest="branch"),
+                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
+                optparse.make_option("--changesfile", dest="changesFile"),
+                optparse.make_option("--silent", dest="silent", action="store_true"),
+                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
+                optparse.make_option("--verbose", dest="verbose", action="store_true"),
+                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
+                                     help="Import into refs/heads/ , not refs/remotes"),
+                optparse.make_option("--max-changes", dest="maxChanges"),
+                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
+                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import")
+        ]
+        self.description = """Imports from Perforce into a git repository.\n
+    example:
+    //depot/my/project/ -- to import the current head
+    //depot/my/project/@all -- to import everything
+    //depot/my/project/@1,6 -- to import only from revision 1 to 6
+
+    (a ... is not needed in the path p4 specification, it's added implicitly)"""
+
+        self.usage += " //depot/path[@revRange]"
+        self.silent = False
+        self.createdBranches = Set()
+        self.committedChanges = Set()
+        self.branch = ""
+        self.detectBranches = False
+        self.detectLabels = False
+        self.changesFile = ""
+        self.syncWithOrigin = True
+        self.verbose = False
+        self.importIntoRemotes = True
+        self.maxChanges = ""
+        self.isWindows = (platform.system() == "Windows")
+        self.keepRepoPath = False
+        self.depotPaths = None
+        self.p4BranchesInGit = []
+
+        if gitConfig("git-p4.syncFromOrigin") == "false":
+            self.syncWithOrigin = False
+
+    def extractFilesFromCommit(self, commit):
+        files = []
+        fnum = 0
+        while commit.has_key("depotFile%s" % fnum):
+            path =  commit["depotFile%s" % fnum]
+
+            found = [p for p in self.depotPaths
+                     if path.startswith (p)]
+            if not found:
+                fnum = fnum + 1
+                continue
+
+            file = {}
+            file["path"] = path
+            file["rev"] = commit["rev%s" % fnum]
+            file["action"] = commit["action%s" % fnum]
+            file["type"] = commit["type%s" % fnum]
+            files.append(file)
+            fnum = fnum + 1
+        return files
+
+    def stripRepoPath(self, path, prefixes):
+        if self.keepRepoPath:
+            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
+
+        for p in prefixes:
+            if path.startswith(p):
+                path = path[len(p):]
+
+        return path
+
+    def splitFilesIntoBranches(self, commit):
+        branches = {}
+        fnum = 0
+        while commit.has_key("depotFile%s" % fnum):
+            path =  commit["depotFile%s" % fnum]
+            found = [p for p in self.depotPaths
+                     if path.startswith (p)]
+            if not found:
+                fnum = fnum + 1
+                continue
+
+            file = {}
+            file["path"] = path
+            file["rev"] = commit["rev%s" % fnum]
+            file["action"] = commit["action%s" % fnum]
+            file["type"] = commit["type%s" % fnum]
+            fnum = fnum + 1
+
+            relPath = self.stripRepoPath(path, self.depotPaths)
+
+            for branch in self.knownBranches.keys():
+
+                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
+                if relPath.startswith(branch + "/"):
+                    if branch not in branches:
+                        branches[branch] = []
+                    branches[branch].append(file)
+                    break
+
+        return branches
+
+    ## Should move this out, doesn't use SELF.
+    def readP4Files(self, files):
+        files = [f for f in files
+                 if f['action'] != 'delete']
+
+        if not files:
+            return
+
+        filedata = p4CmdList('-x - print',
+                             stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
+                                              for f in files]),
+                             stdin_mode='w+')
+        if "p4ExitCode" in filedata[0]:
+            die("Problems executing p4. Error: [%d]."
+                % (filedata[0]['p4ExitCode']));
+
+        j = 0;
+        contents = {}
+        while j < len(filedata):
+            stat = filedata[j]
+            j += 1
+            text = ''
+            while j < len(filedata) and filedata[j]['code'] in ('text',
+                                                                'binary'):
+                text += filedata[j]['data']
+                j += 1
+
+
+            if not stat.has_key('depotFile'):
+                sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
+                continue
+
+            contents[stat['depotFile']] = text
+
+        for f in files:
+            assert not f.has_key('data')
+            f['data'] = contents[f['path']]
+
+    def commit(self, details, files, branch, branchPrefixes, parent = ""):
+        epoch = details["time"]
+        author = details["user"]
+
+        if self.verbose:
+            print "commit into %s" % branch
+
+        # start with reading files; if that fails, we should not
+        # create a commit.
+        new_files = []
+        for f in files:
+            if [p for p in branchPrefixes if f['path'].startswith(p)]:
+                new_files.append (f)
+            else:
+                sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
+        files = new_files
+        self.readP4Files(files)
+
+
+
+
+        self.gitStream.write("commit %s\n" % branch)
+#        gitStream.write("mark :%s\n" % details["change"])
+        self.committedChanges.add(int(details["change"]))
+        committer = ""
+        if author not in self.users:
+            self.getUserMapFromPerforceServer()
+        if author in self.users:
+            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
+        else:
+            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
+
+        self.gitStream.write("committer %s\n" % committer)
+
+        self.gitStream.write("data <<EOT\n")
+        self.gitStream.write(details["desc"])
+        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
+                             % (','.join (branchPrefixes), details["change"]))
+        if len(details['options']) > 0:
+            self.gitStream.write(": options = %s" % details['options'])
+        self.gitStream.write("]\nEOT\n\n")
+
+        if len(parent) > 0:
+            if self.verbose:
+                print "parent %s" % parent
+            self.gitStream.write("from %s\n" % parent)
+
+        for file in files:
+            if file["type"] == "apple":
+                print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
+                continue
+
+            relPath = self.stripRepoPath(file['path'], branchPrefixes)
+            if file["action"] == "delete":
+                self.gitStream.write("D %s\n" % relPath)
+            else:
+                data = file['data']
+
+                mode = "644"
+                if isP4Exec(file["type"]):
+                    mode = "755"
+                elif file["type"] == "symlink":
+                    mode = "120000"
+                    # p4 print on a symlink contains "target\n", so strip it off
+                    data = data[:-1]
+
+                if self.isWindows and file["type"].endswith("text"):
+                    data = data.replace("\r\n", "\n")
+
+                self.gitStream.write("M %s inline %s\n" % (mode, relPath))
+                self.gitStream.write("data %s\n" % len(data))
+                self.gitStream.write(data)
+                self.gitStream.write("\n")
+
+        self.gitStream.write("\n")
+
+        change = int(details["change"])
+
+        if self.labels.has_key(change):
+            label = self.labels[change]
+            labelDetails = label[0]
+            labelRevisions = label[1]
+            if self.verbose:
+                print "Change %s is labelled %s" % (change, labelDetails)
+
+            files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
+                                                    for p in branchPrefixes]))
+
+            if len(files) == len(labelRevisions):
+
+                cleanedFiles = {}
+                for info in files:
+                    if info["action"] == "delete":
+                        continue
+                    cleanedFiles[info["depotFile"]] = info["rev"]
+
+                if cleanedFiles == labelRevisions:
+                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
+                    self.gitStream.write("from %s\n" % branch)
+
+                    owner = labelDetails["Owner"]
+                    tagger = ""
+                    if author in self.users:
+                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
+                    else:
+                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
+                    self.gitStream.write("tagger %s\n" % tagger)
+                    self.gitStream.write("data <<EOT\n")
+                    self.gitStream.write(labelDetails["Description"])
+                    self.gitStream.write("EOT\n\n")
+
+                else:
+                    if not self.silent:
+                        print ("Tag %s does not match with change %s: files do not match."
+                               % (labelDetails["label"], change))
+
+            else:
+                if not self.silent:
+                    print ("Tag %s does not match with change %s: file count is different."
+                           % (labelDetails["label"], change))
+
+    def getUserCacheFilename(self):
+        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
+        return home + "/.gitp4-usercache.txt"
+
+    def getUserMapFromPerforceServer(self):
+        if self.userMapFromPerforceServer:
+            return
+        self.users = {}
+
+        for output in p4CmdList("users"):
+            if not output.has_key("User"):
+                continue
+            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
+
+
+        s = ''
+        for (key, val) in self.users.items():
+            s += "%s\t%s\n" % (key, val)
+
+        open(self.getUserCacheFilename(), "wb").write(s)
+        self.userMapFromPerforceServer = True
+
+    def loadUserMapFromCache(self):
+        self.users = {}
+        self.userMapFromPerforceServer = False
+        try:
+            cache = open(self.getUserCacheFilename(), "rb")
+            lines = cache.readlines()
+            cache.close()
+            for line in lines:
+                entry = line.strip().split("\t")
+                self.users[entry[0]] = entry[1]
+        except IOError:
+            self.getUserMapFromPerforceServer()
+
+    def getLabels(self):
+        self.labels = {}
+
+        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
+        if len(l) > 0 and not self.silent:
+            print "Finding files belonging to labels in %s" % `self.depotPaths`
+
+        for output in l:
+            label = output["label"]
+            revisions = {}
+            newestChange = 0
+            if self.verbose:
+                print "Querying files for label %s" % label
+            for file in p4CmdList("files "
+                                  +  ' '.join (["%s...@%s" % (p, label)
+                                                for p in self.depotPaths])):
+                revisions[file["depotFile"]] = file["rev"]
+                change = int(file["change"])
+                if change > newestChange:
+                    newestChange = change
+
+            self.labels[newestChange] = [output, revisions]
+
+        if self.verbose:
+            print "Label changes: %s" % self.labels.keys()
+
+    def guessProjectName(self):
+        for p in self.depotPaths:
+            if p.endswith("/"):
+                p = p[:-1]
+            p = p[p.strip().rfind("/") + 1:]
+            if not p.endswith("/"):
+               p += "/"
+            return p
+
+    def getBranchMapping(self):
+        lostAndFoundBranches = set()
+
+        for info in p4CmdList("branches"):
+            details = p4Cmd("branch -o %s" % info["branch"])
+            viewIdx = 0
+            while details.has_key("View%s" % viewIdx):
+                paths = details["View%s" % viewIdx].split(" ")
+                viewIdx = viewIdx + 1
+                # require standard //depot/foo/... //depot/bar/... mapping
+                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
+                    continue
+                source = paths[0]
+                destination = paths[1]
+                ## HACK
+                if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
+                    source = source[len(self.depotPaths[0]):-4]
+                    destination = destination[len(self.depotPaths[0]):-4]
+
+                    if destination in self.knownBranches:
+                        if not self.silent:
+                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
+                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
+                        continue
+
+                    self.knownBranches[destination] = source
+
+                    lostAndFoundBranches.discard(destination)
+
+                    if source not in self.knownBranches:
+                        lostAndFoundBranches.add(source)
+
+
+        for branch in lostAndFoundBranches:
+            self.knownBranches[branch] = branch
+
+    def getBranchMappingFromGitBranches(self):
+        branches = p4BranchesInGit(self.importIntoRemotes)
+        for branch in branches.keys():
+            if branch == "master":
+                branch = "main"
+            else:
+                branch = branch[len(self.projectName):]
+            self.knownBranches[branch] = branch
+
+    def listExistingP4GitBranches(self):
+        # branches holds mapping from name to commit
+        branches = p4BranchesInGit(self.importIntoRemotes)
+        self.p4BranchesInGit = branches.keys()
+        for branch in branches.keys():
+            self.initialParents[self.refPrefix + branch] = branches[branch]
+
+    def updateOptionDict(self, d):
+        option_keys = {}
+        if self.keepRepoPath:
+            option_keys['keepRepoPath'] = 1
+
+        d["options"] = ' '.join(sorted(option_keys.keys()))
+
+    def readOptions(self, d):
+        self.keepRepoPath = (d.has_key('options')
+                             and ('keepRepoPath' in d['options']))
+
+    def gitRefForBranch(self, branch):
+        if branch == "main":
+            return self.refPrefix + "master"
+
+        if len(branch) <= 0:
+            return branch
+
+        return self.refPrefix + self.projectName + branch
+
+    def gitCommitByP4Change(self, ref, change):
+        if self.verbose:
+            print "looking in ref " + ref + " for change %s using bisect..." % change
+
+        earliestCommit = ""
+        latestCommit = parseRevision(ref)
+
+        while True:
+            if self.verbose:
+                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
+            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
+            if len(next) == 0:
+                if self.verbose:
+                    print "argh"
+                return ""
+            log = extractLogMessageFromGitCommit(next)
+            settings = extractSettingsGitLog(log)
+            currentChange = int(settings['change'])
+            if self.verbose:
+                print "current change %s" % currentChange
+
+            if currentChange == change:
+                if self.verbose:
+                    print "found %s" % next
+                return next
+
+            if currentChange < change:
+                earliestCommit = "^%s" % next
+            else:
+                latestCommit = "%s" % next
+
+        return ""
+
+    def importNewBranch(self, branch, maxChange):
+        # make fast-import flush all changes to disk and update the refs using the checkpoint
+        # command so that we can try to find the branch parent in the git history
+        self.gitStream.write("checkpoint\n\n");
+        self.gitStream.flush();
+        branchPrefix = self.depotPaths[0] + branch + "/"
+        range = "@1,%s" % maxChange
+        #print "prefix" + branchPrefix
+        changes = p4ChangesForPaths([branchPrefix], range)
+        if len(changes) <= 0:
+            return False
+        firstChange = changes[0]
+        #print "first change in branch: %s" % firstChange
+        sourceBranch = self.knownBranches[branch]
+        sourceDepotPath = self.depotPaths[0] + sourceBranch
+        sourceRef = self.gitRefForBranch(sourceBranch)
+        #print "source " + sourceBranch
+
+        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
+        #print "branch parent: %s" % branchParentChange
+        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
+        if len(gitParent) > 0:
+            self.initialParents[self.gitRefForBranch(branch)] = gitParent
+            #print "parent git commit: %s" % gitParent
+
+        self.importChanges(changes)
+        return True
+
+    def importChanges(self, changes):
+        cnt = 1
+        for change in changes:
+            description = p4Cmd("describe %s" % change)
+            self.updateOptionDict(description)
+
+            if not self.silent:
+                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
+                sys.stdout.flush()
+            cnt = cnt + 1
+
+            try:
+                if self.detectBranches:
+                    branches = self.splitFilesIntoBranches(description)
+                    for branch in branches.keys():
+                        ## HACK  --hwn
+                        branchPrefix = self.depotPaths[0] + branch + "/"
+
+                        parent = ""
+
+                        filesForCommit = branches[branch]
+
+                        if self.verbose:
+                            print "branch is %s" % branch
+
+                        self.updatedBranches.add(branch)
+
+                        if branch not in self.createdBranches:
+                            self.createdBranches.add(branch)
+                            parent = self.knownBranches[branch]
+                            if parent == branch:
+                                parent = ""
+                            else:
+                                fullBranch = self.projectName + branch
+                                if fullBranch not in self.p4BranchesInGit:
+                                    if not self.silent:
+                                        print("\n    Importing new branch %s" % fullBranch);
+                                    if self.importNewBranch(branch, change - 1):
+                                        parent = ""
+                                        self.p4BranchesInGit.append(fullBranch)
+                                    if not self.silent:
+                                        print("\n    Resuming with change %s" % change);
+
+                                if self.verbose:
+                                    print "parent determined through known branches: %s" % parent
+
+                        branch = self.gitRefForBranch(branch)
+                        parent = self.gitRefForBranch(parent)
+
+                        if self.verbose:
+                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
+
+                        if len(parent) == 0 and branch in self.initialParents:
+                            parent = self.initialParents[branch]
+                            del self.initialParents[branch]
+
+                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
+                else:
+                    files = self.extractFilesFromCommit(description)
+                    self.commit(description, files, self.branch, self.depotPaths,
+                                self.initialParent)
+                    self.initialParent = ""
+            except IOError:
+                print self.gitError.read()
+                sys.exit(1)
+
+    def importHeadRevision(self, revision):
+        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
+
+        details = { "user" : "git perforce import user", "time" : int(time.time()) }
+        details["desc"] = ("Initial import of %s from the state at revision %s"
+                           % (' '.join(self.depotPaths), revision))
+        details["change"] = revision
+        newestRevision = 0
+
+        fileCnt = 0
+        for info in p4CmdList("files "
+                              +  ' '.join(["%s...%s"
+                                           % (p, revision)
+                                           for p in self.depotPaths])):
+
+            if info['code'] == 'error':
+                sys.stderr.write("p4 returned an error: %s\n"
+                                 % info['data'])
+                sys.exit(1)
+
+
+            change = int(info["change"])
+            if change > newestRevision:
+                newestRevision = change
+
+            if info["action"] == "delete":
+                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
+                #fileCnt = fileCnt + 1
+                continue
+
+            for prop in ["depotFile", "rev", "action", "type" ]:
+                details["%s%s" % (prop, fileCnt)] = info[prop]
+
+            fileCnt = fileCnt + 1
+
+        details["change"] = newestRevision
+        self.updateOptionDict(details)
+        try:
+            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
+        except IOError:
+            print "IO error with git fast-import. Is your git version recent enough?"
+            print self.gitError.read()
+
+
+    def run(self, args):
+        self.depotPaths = []
+        self.changeRange = ""
+        self.initialParent = ""
+        self.previousDepotPaths = []
+
+        # map from branch depot path to parent branch
+        self.knownBranches = {}
+        self.initialParents = {}
+        self.hasOrigin = originP4BranchesExist()
+        if not self.syncWithOrigin:
+            self.hasOrigin = False
+
+        if self.importIntoRemotes:
+            self.refPrefix = "refs/remotes/p4/"
+        else:
+            self.refPrefix = "refs/heads/p4/"
+
+        if self.syncWithOrigin and self.hasOrigin:
+            if not self.silent:
+                print "Syncing with origin first by calling git fetch origin"
+            system("git fetch origin")
+
+        if len(self.branch) == 0:
+            self.branch = self.refPrefix + "master"
+            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
+                system("git update-ref %s refs/heads/p4" % self.branch)
+                system("git branch -D p4");
+            # create it /after/ importing, when master exists
+            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
+                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
+
+        # TODO: should always look at previous commits,
+        # merge with previous imports, if possible.
+        if args == []:
+            if self.hasOrigin:
+                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
+            self.listExistingP4GitBranches()
+
+            if len(self.p4BranchesInGit) > 1:
+                if not self.silent:
+                    print "Importing from/into multiple branches"
+                self.detectBranches = True
+
+            if self.verbose:
+                print "branches: %s" % self.p4BranchesInGit
+
+            p4Change = 0
+            for branch in self.p4BranchesInGit:
+                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
+
+                settings = extractSettingsGitLog(logMsg)
+
+                self.readOptions(settings)
+                if (settings.has_key('depot-paths')
+                    and settings.has_key ('change')):
+                    change = int(settings['change']) + 1
+                    p4Change = max(p4Change, change)
+
+                    depotPaths = sorted(settings['depot-paths'])
+                    if self.previousDepotPaths == []:
+                        self.previousDepotPaths = depotPaths
+                    else:
+                        paths = []
+                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
+                            for i in range(0, min(len(cur), len(prev))):
+                                if cur[i] <> prev[i]:
+                                    i = i - 1
+                                    break
+
+                            paths.append (cur[:i + 1])
+
+                        self.previousDepotPaths = paths
+
+            if p4Change > 0:
+                self.depotPaths = sorted(self.previousDepotPaths)
+                self.changeRange = "@%s,#head" % p4Change
+                if not self.detectBranches:
+                    self.initialParent = parseRevision(self.branch)
+                if not self.silent and not self.detectBranches:
+                    print "Performing incremental import into %s git branch" % self.branch
+
+        if not self.branch.startswith("refs/"):
+            self.branch = "refs/heads/" + self.branch
+
+        if len(args) == 0 and self.depotPaths:
+            if not self.silent:
+                print "Depot paths: %s" % ' '.join(self.depotPaths)
+        else:
+            if self.depotPaths and self.depotPaths != args:
+                print ("previous import used depot path %s and now %s was specified. "
+                       "This doesn't work!" % (' '.join (self.depotPaths),
+                                               ' '.join (args)))
+                sys.exit(1)
+
+            self.depotPaths = sorted(args)
+
+        revision = ""
+        self.users = {}
+
+        newPaths = []
+        for p in self.depotPaths:
+            if p.find("@") != -1:
+                atIdx = p.index("@")
+                self.changeRange = p[atIdx:]
+                if self.changeRange == "@all":
+                    self.changeRange = ""
+                elif ',' not in self.changeRange:
+                    revision = self.changeRange
+                    self.changeRange = ""
+                p = p[:atIdx]
+            elif p.find("#") != -1:
+                hashIdx = p.index("#")
+                revision = p[hashIdx:]
+                p = p[:hashIdx]
+            elif self.previousDepotPaths == []:
+                revision = "#head"
+
+            p = re.sub ("\.\.\.$", "", p)
+            if not p.endswith("/"):
+                p += "/"
+
+            newPaths.append(p)
+
+        self.depotPaths = newPaths
+
+
+        self.loadUserMapFromCache()
+        self.labels = {}
+        if self.detectLabels:
+            self.getLabels();
+
+        if self.detectBranches:
+            ## FIXME - what's a P4 projectName ?
+            self.projectName = self.guessProjectName()
+
+            if self.hasOrigin:
+                self.getBranchMappingFromGitBranches()
+            else:
+                self.getBranchMapping()
+            if self.verbose:
+                print "p4-git branches: %s" % self.p4BranchesInGit
+                print "initial parents: %s" % self.initialParents
+            for b in self.p4BranchesInGit:
+                if b != "master":
+
+                    ## FIXME
+                    b = b[len(self.projectName):]
+                self.createdBranches.add(b)
+
+        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
+
+        importProcess = subprocess.Popen(["git", "fast-import"],
+                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+                                         stderr=subprocess.PIPE);
+        self.gitOutput = importProcess.stdout
+        self.gitStream = importProcess.stdin
+        self.gitError = importProcess.stderr
+
+        if revision:
+            self.importHeadRevision(revision)
+        else:
+            changes = []
+
+            if len(self.changesFile) > 0:
+                output = open(self.changesFile).readlines()
+                changeSet = Set()
+                for line in output:
+                    changeSet.add(int(line))
+
+                for change in changeSet:
+                    changes.append(change)
+
+                changes.sort()
+            else:
+                if self.verbose:
+                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
+                                                              self.changeRange)
+                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
+
+                if len(self.maxChanges) > 0:
+                    changes = changes[:min(int(self.maxChanges), len(changes))]
+
+            if len(changes) == 0:
+                if not self.silent:
+                    print "No changes to import!"
+                return True
+
+            if not self.silent and not self.detectBranches:
+                print "Import destination: %s" % self.branch
+
+            self.updatedBranches = set()
+
+            self.importChanges(changes)
+
+            if not self.silent:
+                print ""
+                if len(self.updatedBranches) > 0:
+                    sys.stdout.write("Updated branches: ")
+                    for b in self.updatedBranches:
+                        sys.stdout.write("%s " % b)
+                    sys.stdout.write("\n")
+
+        self.gitStream.close()
+        if importProcess.wait() != 0:
+            die("fast-import failed: %s" % self.gitError.read())
+        self.gitOutput.close()
+        self.gitError.close()
+
+        return True
+
+class P4Rebase(Command):
+    def __init__(self):
+        Command.__init__(self)
+        self.options = [ ]
+        self.description = ("Fetches the latest revision from perforce and "
+                            + "rebases the current work (branch) against it")
+        self.verbose = False
+
+    def run(self, args):
+        sync = P4Sync()
+        sync.run([])
+
+        return self.rebase()
+
+    def rebase(self):
+        [upstream, settings] = findUpstreamBranchPoint()
+        if len(upstream) == 0:
+            die("Cannot find upstream branchpoint for rebase")
+
+        # the branchpoint may be p4/foo~3, so strip off the parent
+        upstream = re.sub("~[0-9]+$", "", upstream)
+
+        print "Rebasing the current branch onto %s" % upstream
+        oldHead = read_pipe("git rev-parse HEAD").strip()
+        system("git rebase %s" % upstream)
+        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
+        return True
+
+class P4Clone(P4Sync):
+    def __init__(self):
+        P4Sync.__init__(self)
+        self.description = "Creates a new git repository and imports from Perforce into it"
+        self.usage = "usage: %prog [options] //depot/path[@revRange]"
+        self.options.append(
+            optparse.make_option("--destination", dest="cloneDestination",
+                                 action='store', default=None,
+                                 help="where to leave result of the clone"))
+        self.cloneDestination = None
+        self.needsGit = False
+
+    def defaultDestination(self, args):
+        ## TODO: use common prefix of args?
+        depotPath = args[0]
+        depotDir = re.sub("(@[^@]*)$", "", depotPath)
+        depotDir = re.sub("(#[^#]*)$", "", depotDir)
+        depotDir = re.sub(r"\.\.\.$,", "", depotDir)
+        depotDir = re.sub(r"/$", "", depotDir)
+        return os.path.split(depotDir)[1]
+
+    def run(self, args):
+        if len(args) < 1:
+            return False
+
+        if self.keepRepoPath and not self.cloneDestination:
+            sys.stderr.write("Must specify destination for --keep-path\n")
+            sys.exit(1)
+
+        depotPaths = args
+
+        if not self.cloneDestination and len(depotPaths) > 1:
+            self.cloneDestination = depotPaths[-1]
+            depotPaths = depotPaths[:-1]
+
+        for p in depotPaths:
+            if not p.startswith("//"):
+                return False
+
+        if not self.cloneDestination:
+            self.cloneDestination = self.defaultDestination(args)
+
+        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
+        if not os.path.exists(self.cloneDestination):
+            os.makedirs(self.cloneDestination)
+        os.chdir(self.cloneDestination)
+        system("git init")
+        self.gitdir = os.getcwd() + "/.git"
+        if not P4Sync.run(self, depotPaths):
+            return False
+        if self.branch != "master":
+            if gitBranchExists("refs/remotes/p4/master"):
+                system("git branch master refs/remotes/p4/master")
+                system("git checkout -f")
+            else:
+                print "Could not detect main branch. No checkout/master branch created."
+
+        return True
+
+class P4Branches(Command):
+    def __init__(self):
+        Command.__init__(self)
+        self.options = [ ]
+        self.description = ("Shows the git branches that hold imports and their "
+                            + "corresponding perforce depot paths")
+        self.verbose = False
+
+    def run(self, args):
+        if originP4BranchesExist():
+            createOrUpdateBranchesFromOrigin()
+
+        cmdline = "git rev-parse --symbolic "
+        cmdline += " --remotes"
+
+        for line in read_pipe_lines(cmdline):
+            line = line.strip()
+
+            if not line.startswith('p4/') or line == "p4/HEAD":
+                continue
+            branch = line
+
+            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
+            settings = extractSettingsGitLog(log)
+
+            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
+        return True
+
+class HelpFormatter(optparse.IndentedHelpFormatter):
+    def __init__(self):
+        optparse.IndentedHelpFormatter.__init__(self)
+
+    def format_description(self, description):
+        if description:
+            return description + "\n"
+        else:
+            return ""
+
+def printUsage(commands):
+    print "usage: %s <command> [options]" % sys.argv[0]
+    print ""
+    print "valid commands: %s" % ", ".join(commands)
+    print ""
+    print "Try %s <command> --help for command specific help." % sys.argv[0]
+    print ""
+
+commands = {
+    "debug" : P4Debug,
+    "submit" : P4Submit,
+    "commit" : P4Submit,
+    "sync" : P4Sync,
+    "rebase" : P4Rebase,
+    "clone" : P4Clone,
+    "rollback" : P4RollBack,
+    "branches" : P4Branches
+}
+
+
+def main():
+    if len(sys.argv[1:]) == 0:
+        printUsage(commands.keys())
+        sys.exit(2)
+
+    cmd = ""
+    cmdName = sys.argv[1]
+    try:
+        klass = commands[cmdName]
+        cmd = klass()
+    except KeyError:
+        print "unknown command %s" % cmdName
+        print ""
+        printUsage(commands.keys())
+        sys.exit(2)
+
+    options = cmd.options
+    cmd.gitdir = os.environ.get("GIT_DIR", None)
+
+    args = sys.argv[2:]
+
+    if len(options) > 0:
+        options.append(optparse.make_option("--git-dir", dest="gitdir"))
+
+        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
+                                       options,
+                                       description = cmd.description,
+                                       formatter = HelpFormatter())
+
+        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
+    global verbose
+    verbose = cmd.verbose
+    if cmd.needsGit:
+        if cmd.gitdir == None:
+            cmd.gitdir = os.path.abspath(".git")
+            if not isValidGitDir(cmd.gitdir):
+                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
+                if os.path.exists(cmd.gitdir):
+                    cdup = read_pipe("git rev-parse --show-cdup").strip()
+                    if len(cdup) > 0:
+                        os.chdir(cdup);
+
+        if not isValidGitDir(cmd.gitdir):
+            if isValidGitDir(cmd.gitdir + "/.git"):
+                cmd.gitdir += "/.git"
+            else:
+                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
+
+        os.environ["GIT_DIR"] = cmd.gitdir
+
+    if not cmd.run(args):
+        parser.print_help()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/contrib/fast-import/git-p4.bat b/contrib/fast-import/git-p4.bat
new file mode 100644 (file)
index 0000000..9f97e88
--- /dev/null
@@ -0,0 +1 @@
+@python "%~d0%~p0git-p4" %*
diff --git a/contrib/fast-import/git-p4.txt b/contrib/fast-import/git-p4.txt
new file mode 100644 (file)
index 0000000..b16a838
--- /dev/null
@@ -0,0 +1,159 @@
+git-p4 - Perforce <-> Git converter using git-fast-import
+
+Usage
+=====
+
+git-p4 supports two main modes: Importing from Perforce to a Git repository is
+done using "git-p4 sync" or "git-p4 rebase". Submitting changes from Git back
+to Perforce is done using "git-p4 submit".
+
+Importing
+=========
+
+You can simply start with
+
+  git-p4 clone //depot/path/project
+
+or
+
+  git-p4 clone //depot/path/project myproject
+
+This will create an empty git repository in a subdirectory called "project" (or
+"myproject" with the second command), import the head revision from the
+specified perforce path into a git "p4" branch (remotes/p4 actually), create a
+master branch off it and check it out. If you want the entire history (not just
+the head revision) then you can simply append a "@all" to the depot path:
+
+  git-p4 clone //depot/project/main@all myproject
+
+
+
+If you want more control you can also use the git-p4 sync command directly:
+
+  mkdir repo-git
+  cd repo-git
+  git init
+  git-p4 sync //path/in/your/perforce/depot
+
+This will import the current head revision of the specified depot path into a
+"remotes/p4/master" branch of your git repository. You can use the
+--branch=mybranch option to use a different branch.
+
+If you want to import the entire history of a given depot path just use
+
+  git-p4 sync //path/in/depot@all
+
+To achieve optimal compression you may want to run 'git repack -a -d -f' after
+a big import. This may take a while.
+
+Support for Perforce integrations is still work in progress. Don't bother
+trying it unless you want to hack on it :)
+
+Incremental Imports
+===================
+
+After an initial import you can easily synchronize your git repository with
+newer changes from the Perforce depot by just calling
+
+  git-p4 sync
+
+in your git repository. By default the "remotes/p4/master" branch is updated.
+
+It is recommended to run 'git repack -a -d -f' from time to time when using
+incremental imports to optimally combine the individual git packs that each
+incremental import creates through the use of git-fast-import.
+
+
+A useful setup may be that you have a periodically updated git repository
+somewhere that contains a complete import of a Perforce project. That git
+repository can be used to clone the working repository from and one would
+import from Perforce directly after cloning using git-p4. If the connection to
+the Perforce server is slow and the working repository hasn't been synced for a
+while it may be desirable to fetch changes from the origin git repository using
+the efficient git protocol. git-p4 supports this setup by calling "git fetch origin"
+by default if there is an origin branch. You can disable this using
+
+  git config git-p4.syncFromOrigin false
+
+Updating
+========
+
+A common working pattern is to fetch the latest changes from the Perforce depot
+and merge them with local uncommitted changes. The recommended way is to use
+git's rebase mechanism to preserve linear history. git-p4 provides a convenient
+
+  git-p4 rebase
+
+command that calls git-p4 sync followed by git rebase to rebase the current
+working branch.
+
+Submitting
+==========
+
+git-p4 has support for submitting changes from a git repository back to the
+Perforce depot. This requires a Perforce checkout separate to your git
+repository. To submit all changes that are in the current git branch but not in
+the "p4" branch (or "origin" if "p4" doesn't exist) simply call
+
+    git-p4 submit
+
+in your git repository. If you want to submit changes in a specific branch that
+is not your current git branch you can also pass that as an argument:
+
+    git-p4 submit mytopicbranch
+
+You can override the reference branch with the --origin=mysourcebranch option.
+
+If a submit fails you may have to "p4 resolve" and submit manually. You can
+continue importing the remaining changes with
+
+  git-p4 submit --continue
+
+After submitting you should sync your perforce import branch ("p4" or "origin")
+from Perforce using git-p4's sync command.
+
+If you have changes in your working directory that you haven't committed into
+git yet but that you want to commit to Perforce directly ("quick fixes") then
+you do not have to go through the intermediate step of creating a git commit
+first but you can just call
+
+  git-p4 submit --direct
+
+
+Example
+=======
+
+# Clone a repository
+  git-p4 clone //depot/path/project
+# Enter the newly cloned directory
+  cd project
+# Do some work...
+  vi foo.h
+# ... and commit locally to gi
+  git commit foo.h
+# In the meantime somebody submitted changes to the Perforce depot. Rebase your latest
+# changes against the latest changes in Perforce:
+  git-p4 rebase
+# Submit your locally committed changes back to Perforce
+  git-p4 submit
+# ... and synchronize with Perforce
+  git-p4 rebase
+
+
+Implementation Details...
+=========================
+
+* Changesets from Perforce are imported using git fast-import.
+* The import does not require anything from the Perforce client view as it just uses
+  "p4 print //depot/path/file#revision" to get the actual file contents.
+* Every imported changeset has a special [git-p4...] line at the
+  end of the log message that gives information about the corresponding
+  Perforce change number and is also used by git-p4 itself to find out
+  where to continue importing when doing incremental imports.
+  Basically when syncing it extracts the perforce change number of the
+  latest commit in the "p4" branch and uses "p4 changes //depot/path/...@changenum,#head"
+  to find out which changes need to be imported.
+* git-p4 submit uses "git rev-list" to pick the commits between the "p4" branch
+  and the current branch.
+  The commits themselves are applied using git diff/format-patch ... | git apply
+
diff --git a/contrib/fast-import/import-tars.perl b/contrib/fast-import/import-tars.perl
new file mode 100755 (executable)
index 0000000..23aeb25
--- /dev/null
@@ -0,0 +1,131 @@
+#!/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 length($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 $typeflag == 5; # directory
+
+               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;
+               if ($prefix) {
+                       $path = "$prefix/$name";
+               } else {
+                       $path = "$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 (executable)
index 0000000..4c99dfb
--- /dev/null
@@ -0,0 +1,1305 @@
+#! /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."
+__copyright__ = "Copyright (C) 2007 Aneesh Kumar K.V <aneesh.kumar@gmail.com"
+__author__    = "Aneesh Kumar K.V <aneesh.kumar@gmail.com>"
+
+
+import sys
+import os
+import gtk
+import pygtk
+import pango
+import re
+import time
+import gobject
+import cairo
+import math
+import string
+import fcntl
+
+have_gtksourceview2 = False
+have_gtksourceview = False
+try:
+    import gtksourceview2
+    have_gtksourceview2 = True
+except ImportError:
+    try:
+        import gtksourceview
+        have_gtksourceview = True
+    except ImportError:
+        print "Running without gtksourceview2 or gtksourceview module"
+
+re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
+
+def list_to_string(args, skip):
+       count = len(args)
+       i = skip
+       str_arg=" "
+       while (i < count ):
+               str_arg = str_arg + args[i]
+               str_arg = str_arg + " "
+               i = i+1
+
+       return str_arg
+
+def show_date(epoch, tz):
+       secs = float(epoch)
+       tzsecs = float(tz[1:3]) * 3600
+       tzsecs += float(tz[3:5]) * 60
+       if (tz[0] == "+"):
+               secs += tzsecs
+       else:
+               secs -= tzsecs
+
+       return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
+
+def get_source_buffer_and_view():
+       if have_gtksourceview2:
+               buffer = gtksourceview2.Buffer()
+               slm = gtksourceview2.LanguageManager()
+               gsl = slm.get_language("diff")
+               buffer.set_highlight_syntax(True)
+               buffer.set_language(gsl)
+               view = gtksourceview2.View(buffer)
+       elif have_gtksourceview:
+               buffer = gtksourceview.SourceBuffer()
+               slm = gtksourceview.SourceLanguagesManager()
+               gsl = slm.get_language_from_mime_type("text/x-patch")
+               buffer.set_highlight(True)
+               buffer.set_language(gsl)
+               view = gtksourceview.SourceView(buffer)
+       else:
+               buffer = gtk.TextBuffer()
+               view = gtk.TextView(buffer)
+       return (buffer, view)
+
+
+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(object):
+       """ This represent a commit object obtained after parsing the git-rev-list
+       output """
+
+       __slots__ = ['children_sha1', 'message', 'author', 'date', 'committer',
+                                'commit_date', 'commit_sha1', 'parent_sha1']
+
+       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 AnnotateWindow(object):
+       """Annotate window.
+       This object represents and manages a single window containing the
+       annotate information of the file
+       """
+
+       def __init__(self):
+               self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+               self.window.set_border_width(0)
+               self.window.set_title("Git repository browser annotation window")
+               self.prev_read = ""
+
+               # 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)
+
+       def add_file_data(self, filename, commit_sha1, line_num):
+               fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
+               i = 1;
+               for line in fp.readlines():
+                       line = string.rstrip(line)
+                       self.model.append(None, ["HEAD", filename, line, i])
+                       i = i+1
+               fp.close()
+
+               # now set the cursor position
+               self.treeview.set_cursor(line_num-1)
+               self.treeview.grab_focus()
+
+       def _treeview_cursor_cb(self, *args):
+               """Callback for when the treeview cursor changes."""
+               (path, col) = self.treeview.get_cursor()
+               commit_sha1 = self.model[path][0]
+               commit_msg = ""
+               fp = os.popen("git cat-file commit " + commit_sha1)
+               for line in fp.readlines():
+                       commit_msg =  commit_msg + line
+               fp.close()
+
+               self.commit_buffer.set_text(commit_msg)
+
+       def _treeview_row_activated(self, *args):
+               """Callback for when the treeview row gets selected."""
+               (path, col) = self.treeview.get_cursor()
+               commit_sha1 = self.model[path][0]
+               filename    = self.model[path][1]
+               line_num    = self.model[path][3]
+
+               window = AnnotateWindow();
+               fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
+               commit_sha1 = string.strip(fp.readline())
+               fp.close()
+               window.annotate(filename, commit_sha1, line_num)
+
+       def data_ready(self, source, condition):
+               while (1):
+                       try :
+                               # A simple readline doesn't work
+                               # a readline bug ??
+                               buffer = source.read(100)
+
+                       except:
+                               # resource temporary not available
+                               return True
+
+                       if (len(buffer) == 0):
+                               gobject.source_remove(self.io_watch_tag)
+                               source.close()
+                               return False
+
+                       if (self.prev_read != ""):
+                               buffer = self.prev_read + buffer
+                               self.prev_read = ""
+
+                       if (buffer[len(buffer) -1] != '\n'):
+                               try:
+                                       newline_index = buffer.rindex("\n")
+                               except ValueError:
+                                       newline_index = 0
+
+                               self.prev_read = buffer[newline_index:(len(buffer))]
+                               buffer = buffer[0:newline_index]
+
+                       for buff in buffer.split("\n"):
+                               annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
+                               m = annotate_line.match(buff)
+                               if not m:
+                                       annotate_line = re.compile('^(filename) (.+)$')
+                                       m = annotate_line.match(buff)
+                                       if not m:
+                                               continue
+                                       filename = m.group(2)
+                               else:
+                                       self.commit_sha1 = m.group(1)
+                                       self.source_line = int(m.group(2))
+                                       self.result_line = int(m.group(3))
+                                       self.count          = int(m.group(4))
+                                       #set the details only when we have the file name
+                                       continue
+
+                               while (self.count > 0):
+                                       # set at result_line + count-1 the sha1 as commit_sha1
+                                       self.count = self.count - 1
+                                       iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
+                                       self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
+
+
+       def annotate(self, filename, commit_sha1, line_num):
+               # verify the commit_sha1 specified has this filename
+
+               fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
+               line = string.strip(fp.readline())
+               if line == '':
+                       # pop up the message the file is not there as a part of the commit
+                       fp.close()
+                       dialog = gtk.MessageDialog(parent=None, flags=0,
+                                       type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
+                                       message_format=None)
+                       dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
+                       dialog.run()
+                       dialog.destroy()
+                       return
+
+               fp.close()
+
+               vpan = gtk.VPaned();
+               self.window.add(vpan);
+               vpan.show()
+
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               vpan.pack1(scrollwin, True, True);
+               scrollwin.show()
+
+               self.model = gtk.TreeStore(str, str, str, int)
+               self.treeview = gtk.TreeView(self.model)
+               self.treeview.set_rules_hint(True)
+               self.treeview.set_search_column(0)
+               self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
+               self.treeview.connect("row-activated", self._treeview_row_activated)
+               scrollwin.add(self.treeview)
+               self.treeview.show()
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 10)
+               cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+               column = gtk.TreeViewColumn("Commit")
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 0)
+               self.treeview.append_column(column)
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 20)
+               cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+               column = gtk.TreeViewColumn("File Name")
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 1)
+               self.treeview.append_column(column)
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 20)
+               cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+               column = gtk.TreeViewColumn("Data")
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 2)
+               self.treeview.append_column(column)
+
+               # The commit message window
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               vpan.pack2(scrollwin, True, True);
+               scrollwin.show()
+
+               commit_text = gtk.TextView()
+               self.commit_buffer = gtk.TextBuffer()
+               commit_text.set_buffer(self.commit_buffer)
+               scrollwin.add(commit_text)
+               commit_text.show()
+
+               self.window.show()
+
+               self.add_file_data(filename, commit_sha1, line_num)
+
+               fp = os.popen("git blame --incremental -C -C -- " + filename + " " + commit_sha1)
+               flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
+               fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
+               self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
+
+
+class DiffWindow(object):
+       """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()
+
+               hpan = gtk.HPaned()
+
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               hpan.pack1(scrollwin, True, True)
+               scrollwin.show()
+
+               (self.buffer, sourceview) = get_source_buffer_and_view()
+
+               sourceview.set_editable(False)
+               sourceview.modify_font(pango.FontDescription("Monospace"))
+               scrollwin.add(sourceview)
+               sourceview.show()
+
+               # The file hierarchy: a scrollable treeview
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               scrollwin.set_size_request(20, -1)
+               hpan.pack2(scrollwin, True, True)
+               scrollwin.show()
+
+               self.model = gtk.TreeStore(str, str, str)
+               self.treeview = gtk.TreeView(self.model)
+               self.treeview.set_search_column(1)
+               self.treeview.connect("cursor-changed", self._treeview_clicked)
+               scrollwin.add(self.treeview)
+               self.treeview.show()
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 20)
+               column = gtk.TreeViewColumn("Select to annotate")
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 0)
+               self.treeview.append_column(column)
+
+               vbox.pack_start(hpan, expand=True, fill=True)
+               hpan.show()
+
+       def _treeview_clicked(self, *args):
+               """Callback for when the treeview cursor changes."""
+               (path, col) = self.treeview.get_cursor()
+               specific_file = self.model[path][1]
+               commit_sha1 =  self.model[path][2]
+               if specific_file ==  None :
+                       return
+               elif specific_file ==  "" :
+                       specific_file =  None
+
+               window = AnnotateWindow();
+               window.annotate(specific_file, commit_sha1, 1)
+
+
+       def commit_files(self, commit_sha1, parent_sha1):
+               self.model.clear()
+               add  = self.model.append(None, [ "Added", None, None])
+               dele = self.model.append(None, [ "Deleted", None, None])
+               mod  = self.model.append(None, [ "Modified", None, None])
+               diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
+               fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
+               while 1:
+                       line = string.strip(fp.readline())
+                       if line == '':
+                               break
+                       m = diff_tree.match(line)
+                       if not m:
+                               continue
+
+                       attr = m.group(5)
+                       filename = m.group(6)
+                       if attr == "A":
+                               self.model.append(add,  [filename, filename, commit_sha1])
+                       elif attr == "D":
+                               self.model.append(dele, [filename, filename, commit_sha1])
+                       elif attr == "M":
+                               self.model.append(mod,  [filename, filename, commit_sha1])
+               fp.close()
+
+               self.treeview.expand_all()
+
+       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.commit_files(commit_sha1, parent_sha1)
+               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(object):
+       """ This is the main class
+       """
+       version = "0.9"
+
+       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@gmail.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()
+
+               (self.message_buffer, sourceview) = get_source_buffer_and_view()
+
+               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 (file)
index 0000000..77c29de
--- /dev/null
@@ -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 (executable)
index 0000000..9befb92
--- /dev/null
@@ -0,0 +1,241 @@
+#! /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 = {}
+# Number of new changesets converted from hg
+hgnewcsets = 0
+
+#------------------------------------------------------------------------------
+
+def usage():
+
+        print """\
+%s: [OPTIONS] <hgprj>
+
+options:
+    -s, --gitstate=FILE: name of the state to be saved/read
+                         for incrementals
+    -n, --nrepack=INT:   number of changesets that will trigger
+                         a repack (default=0, -1 to deactivate)
+
+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 = ''
+opt_nrepack = 0
+
+try:
+    opts, args = getopt.getopt(sys.argv[1:], 's:t:n:', ['gitstate=', 'tempdir=', 'nrepack='])
+    for o, a in opts:
+        if o in ('-s', '--gitstate'):
+            state = a
+            state = os.path.abspath(state)
+        if o in ('-n', '--nrepack'):
+            opt_nrepack = int(a)
+    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
+    hgnewcsets += 1
+
+    # 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 --allow-empty -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
+
+if hgnewcsets >= opt_nrepack and opt_nrepack != -1:
+    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 (file)
index 0000000..91f8fe6
--- /dev/null
@@ -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 (file)
index 0000000..7511ea0
--- /dev/null
@@ -0,0 +1,643 @@
+#!/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.
+# hooks.envelopesender
+#   If set then the -f option is passed to sendmail to allow the envelope
+#   sender address to be set
+# hooks.emailprefix
+#   All emails have their subjects prefixed with this prefix, or "[SCM]"
+#   if emailprefix is unset, to aid filtering
+#
+# Notes
+# -----
+# 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
+               case "$refname_type" in
+                       "annotated tag")
+                               config_name="hooks.announcelist"
+                               ;;
+                       *)
+                               config_name="hooks.mailinglist"
+                               ;;
+               esac
+               echo >&2 "*** $config_name is not set so no email will be sent"
+               echo >&2 "*** for $refname update $oldrev->$newrev"
+               exit 0
+       fi
+
+       # Email parameters
+       # 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
+       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
+       fast_forward=""
+       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 [ "$fast_forward" ]; then
+               echo "      from  $oldrev ($oldrev_type)"
+       else
+               #  1. Existing revisions were removed.  In this case newrev
+               #     is a subset of oldrev - this is the reverse of a
+               #     fast-forward, a rewind
+               #  2. New revisions were added on top of an old revision,
+               #     this is a rewind and addition.
+
+               # (1) certainly happened, (2) possibly.  When (2) hasn't
+               # happened, we set a flag to indicate that no log printout
+               # is required.
+
+               echo ""
+
+               # Find the common ancestor of the old and new revisions and
+               # compare it with newrev
+               baserev=$(git merge-base $oldrev $newrev)
+               rewind_only=""
+               if [ "$baserev" = "$newrev" ]; then
+                       echo "This update discarded existing revisions and left the branch pointing at"
+                       echo "a previous point in the repository history."
+                       echo ""
+                       echo " * -- * -- N ($newrev)"
+                       echo "            \\"
+                       echo "             O -- O -- O ($oldrev)"
+                       echo ""
+                       echo "The removed revisions are not necessarilly gone - if another reference"
+                       echo "still refers to them they will stay in the repository."
+                       rewind_only=1
+               else
+                       echo "This update added new revisions after undoing existing revisions.  That is"
+                       echo "to 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 repository"
+                       echo "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
+       fi
+
+       echo ""
+       if [ -z "$rewind_only" ]; then
+               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
+       else
+               echo "No new revisions were added by this update."
+       fi
+
+       # 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
+}
+
+send_mail()
+{
+       if [ -n "$envelopesender" ]; then
+               /usr/sbin/sendmail -t -f "$envelopesender"
+       else
+               /usr/sbin/sendmail -t
+       fi
+}
+
+# ---------------------------- main()
+
+# --- Constants
+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 -ne '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)
+emailprefix=$(git-repo-config hooks.emailprefix || echo '[SCM] ')
+
+# --- 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
+       while read oldrev newrev refname
+       do
+               generate_email $oldrev $newrev $refname | send_mail
+       done
+fi
diff --git a/contrib/hooks/setgitperms.perl b/contrib/hooks/setgitperms.perl
new file mode 100644 (file)
index 0000000..dab7c8e
--- /dev/null
@@ -0,0 +1,214 @@
+#!/usr/bin/perl
+#
+# Copyright (c) 2006 Josh England
+#
+# This script can be used to save/restore full permissions and ownership data
+# within a git working tree.
+#
+# To save permissions/ownership data, place this script in your .git/hooks
+# directory and enable a `pre-commit` hook with the following lines:
+#      #!/bin/sh
+#     SUBDIRECTORY_OK=1 . git-sh-setup
+#     $GIT_DIR/hooks/setgitperms.perl -r
+#
+# To restore permissions/ownership data, place this script in your .git/hooks
+# directory and enable a `post-merge` and `post-checkout` hook with the
+# following lines:
+#      #!/bin/sh
+#     SUBDIRECTORY_OK=1 . git-sh-setup
+#     $GIT_DIR/hooks/setgitperms.perl -w
+#
+use strict;
+use Getopt::Long;
+use File::Find;
+use File::Basename;
+
+my $usage =
+"Usage: setgitperms.perl [OPTION]... <--read|--write>
+This program uses a file `.gitmeta` to store/restore permissions and uid/gid
+info for all files/dirs tracked by git in the repository.
+
+---------------------------------Read Mode-------------------------------------
+-r,  --read         Reads perms/etc from working dir into a .gitmeta file
+-s,  --stdout       Output to stdout instead of .gitmeta
+-d,  --diff         Show unified diff of perms file (XOR with --stdout)
+
+---------------------------------Write Mode------------------------------------
+-w,  --write        Modify perms/etc in working dir to match the .gitmeta file
+-v,  --verbose      Be verbose
+
+\n";
+
+my ($stdout, $showdiff, $verbose, $read_mode, $write_mode);
+
+if ((@ARGV < 0) || !GetOptions(
+                              "stdout",         \$stdout,
+                              "diff",           \$showdiff,
+                              "read",           \$read_mode,
+                              "write",          \$write_mode,
+                              "verbose",        \$verbose,
+                             )) { die $usage; }
+die $usage unless ($read_mode xor $write_mode);
+
+my $topdir = `git-rev-parse --show-cdup` or die "\n"; chomp $topdir;
+my $gitdir = $topdir . '.git';
+my $gitmeta = $topdir . '.gitmeta';
+
+if ($write_mode) {
+    # Update the working dir permissions/ownership based on data from .gitmeta
+    open (IN, "<$gitmeta") or die "Could not open $gitmeta for reading: $!\n";
+    while (defined ($_ = <IN>)) {
+       chomp;
+       if (/^(.*)  mode=(\S+)\s+uid=(\d+)\s+gid=(\d+)/) {
+           # Compare recorded perms to actual perms in the working dir
+           my ($path, $mode, $uid, $gid) = ($1, $2, $3, $4);
+           my $fullpath = $topdir . $path;
+           my (undef,undef,$wmode,undef,$wuid,$wgid) = lstat($fullpath);
+           $wmode = sprintf "%04o", $wmode & 07777;
+           if ($mode ne $wmode) {
+               $verbose && print "Updating permissions on $path: old=$wmode, new=$mode\n";
+               chmod oct($mode), $fullpath;
+           }
+           if ($uid != $wuid || $gid != $wgid) {
+               if ($verbose) {
+                   # Print out user/group names instead of uid/gid
+                   my $pwname  = getpwuid($uid);
+                   my $grpname  = getgrgid($gid);
+                   my $wpwname  = getpwuid($wuid);
+                   my $wgrpname  = getgrgid($wgid);
+                   $pwname = $uid if !defined $pwname;
+                   $grpname = $gid if !defined $grpname;
+                   $wpwname = $wuid if !defined $wpwname;
+                   $wgrpname = $wgid if !defined $wgrpname;
+
+                   print "Updating uid/gid on $path: old=$wpwname/$wgrpname, new=$pwname/$grpname\n";
+               }
+               chown $uid, $gid, $fullpath;
+           }
+       }
+       else {
+           warn "Invalid input format in $gitmeta:\n\t$_\n";
+       }
+    }
+    close IN;
+}
+elsif ($read_mode) {
+    # Handle merge conflicts in the .gitperms file
+    if (-e "$gitdir/MERGE_MSG") {
+       if (`grep ====== $gitmeta`) {
+           # Conflict not resolved -- abort the commit
+           print "PERMISSIONS/OWNERSHIP CONFLICT\n";
+           print "    Resolve the conflict in the $gitmeta file and then run\n";
+           print "    `.git/hooks/setgitperms.perl --write` to reconcile.\n";
+           exit 1;
+       }
+       elsif (`grep $gitmeta $gitdir/MERGE_MSG`) {
+           # A conflict in .gitmeta has been manually resolved. Verify that
+           # the working dir perms matches the current .gitmeta perms for
+           # each file/dir that conflicted.
+           # This is here because a `setgitperms.perl --write` was not
+           # performed due to a merge conflict, so permissions/ownership
+           # may not be consistent with the manually merged .gitmeta file.
+           my @conflict_diff = `git show \$(cat $gitdir/MERGE_HEAD)`;
+           my @conflict_files;
+           my $metadiff = 0;
+
+           # Build a list of files that conflicted from the .gitmeta diff
+           foreach my $line (@conflict_diff) {
+               if ($line =~ m|^diff --git a/$gitmeta b/$gitmeta|) {
+                   $metadiff = 1;
+               }
+               elsif ($line =~ /^diff --git/) {
+                   $metadiff = 0;
+               }
+               elsif ($metadiff && $line =~ /^\+(.*)  mode=/) {
+                   push @conflict_files, $1;
+               }
+           }
+
+           # Verify that each conflict file now has permissions consistent
+           # with the .gitmeta file
+           foreach my $file (@conflict_files) {
+               my $absfile = $topdir . $file;
+               my $gm_entry = `grep "^$file  mode=" $gitmeta`;
+               if ($gm_entry =~ /mode=(\d+)  uid=(\d+)  gid=(\d+)/) {
+                   my ($gm_mode, $gm_uid, $gm_gid) = ($1, $2, $3);
+                   my (undef,undef,$mode,undef,$uid,$gid) = lstat("$absfile");
+                   $mode = sprintf("%04o", $mode & 07777);
+                   if (($gm_mode ne $mode) || ($gm_uid != $uid)
+                       || ($gm_gid != $gid)) {
+                       print "PERMISSIONS/OWNERSHIP CONFLICT\n";
+                       print "    Mismatch found for file: $file\n";
+                       print "    Run `.git/hooks/setgitperms.perl --write` to reconcile.\n";
+                       exit 1;
+                   }
+               }
+               else {
+                   print "Warning! Permissions/ownership no longer being tracked for file: $file\n";
+               }
+           }
+       }
+    }
+
+    # No merge conflicts -- write out perms/ownership data to .gitmeta file
+    unless ($stdout) {
+       open (OUT, ">$gitmeta.tmp") or die "Could not open $gitmeta.tmp for writing: $!\n";
+    }
+
+    my @files = `git-ls-files`;
+    my %dirs;
+
+    foreach my $path (@files) {
+       chomp $path;
+       # We have to manually add stats for parent directories
+       my $parent = dirname($path);
+       while (!exists $dirs{$parent}) {
+           $dirs{$parent} = 1;
+           next if $parent eq '.';
+           printstats($parent);
+           $parent = dirname($parent);
+       }
+       # Now the git-tracked file
+       printstats($path);
+    }
+
+    # diff the temporary metadata file to see if anything has changed
+    # If no metadata has changed, don't overwrite the real file
+    # This is just so `git commit -a` doesn't try to commit a bogus update
+    unless ($stdout) {
+       if (! -e $gitmeta) {
+           rename "$gitmeta.tmp", $gitmeta;
+       }
+       else {
+           my $diff = `diff -U 0 $gitmeta $gitmeta.tmp`;
+           if ($diff ne '') {
+               rename "$gitmeta.tmp", $gitmeta;
+           }
+           else {
+               unlink "$gitmeta.tmp";
+           }
+           if ($showdiff) {
+               print $diff;
+           }
+       }
+       close OUT;
+    }
+    # Make sure the .gitmeta file is tracked
+    system("git add $gitmeta");
+}
+
+
+sub printstats {
+    my $path = $_[0];
+    $path =~ s/@/\@/g;
+    my (undef,undef,$mode,undef,$uid,$gid) = lstat($path);
+    $path =~ s/%/\%/g;
+    if ($stdout) {
+       print $path;
+       printf "  mode=%04o  uid=$uid  gid=$gid\n", $mode & 07777;
+    }
+    else {
+       print OUT $path;
+       printf OUT "  mode=%04o  uid=$uid  gid=$gid\n", $mode & 07777;
+    }
+}
diff --git a/contrib/hooks/update-paranoid b/contrib/hooks/update-paranoid
new file mode 100644 (file)
index 0000000..068fa37
--- /dev/null
@@ -0,0 +1,421 @@
+#!/usr/bin/perl
+
+use strict;
+use File::Spec;
+
+$ENV{PATH}     = '/opt/git/bin';
+my $acl_git    = '/vcs/acls.git';
+my $acl_branch = 'refs/heads/master';
+my $debug      = 0;
+
+=doc
+Invoked as: update refname old-sha1 new-sha1
+
+This script is run by git-receive-pack once for each ref that the
+client is trying to modify.  If we exit with a non-zero exit value
+then the update for that particular ref is denied, but updates for
+other refs in the same run of receive-pack may still be allowed.
+
+We are run after the objects have been uploaded, but before the
+ref is actually modified.  We take advantage of that fact when we
+look for "new" commits and tags (the new objects won't show up in
+`rev-list --all`).
+
+This script loads and parses the content of the config file
+"users/$this_user.acl" from the $acl_branch commit of $acl_git ODB.
+The acl file is a git-config style file, but uses a slightly more
+restricted syntax as the Perl parser contained within this script
+is not nearly as permissive as git-config.
+
+Example:
+
+  [user]
+    committer = John Doe <john.doe@example.com>
+    committer = John R. Doe <john.doe@example.com>
+
+  [repository "acls"]
+    allow = heads/master
+    allow = CDUR for heads/jd/
+    allow = C    for ^tags/v\\d+$
+
+For all new commit or tag objects the committer (or tagger) line
+within the object must exactly match one of the user.committer
+values listed in the acl file ("HEAD:users/$this_user.acl").
+
+For a branch to be modified an allow line within the matching
+repository section must be matched for both the refname and the
+opcode.
+
+Repository sections are matched on the basename of the repository
+(after removing the .git suffix).
+
+The opcode abbrevations are:
+
+  C: create new ref
+  D: delete existing ref
+  U: fast-forward existing ref (no commit loss)
+  R: rewind/rebase existing ref (commit loss)
+
+if no opcodes are listed before the "for" keyword then "U" (for
+fast-forward update only) is assumed as this is the most common
+usage.
+
+Refnames are matched by always assuming a prefix of "refs/".
+This hook forbids pushing or deleting anything not under "refs/".
+
+Refnames that start with ^ are Perl regular expressions, and the ^
+is kept as part of the regexp.  \\ is needed to get just one \, so
+\\d expands to \d in Perl.  The 3rd allow line above is an example.
+
+Refnames that don't start with ^ but that end with / are prefix
+matches (2nd allow line above); all other refnames are strict
+equality matches (1st allow line).
+
+Anything pushed to "heads/" (ok, really "refs/heads/") must be
+a commit.  Tags are not permitted here.
+
+Anything pushed to "tags/" (err, really "refs/tags/") must be an
+annotated tag.  Commits, blobs, trees, etc. are not permitted here.
+Annotated tag signatures aren't checked, nor are they required.
+
+The special subrepository of 'info/new-commit-check' can
+be created and used to allow users to push new commits and
+tags from another local repository to this one, even if they
+aren't the committer/tagger of those objects.  In a nut shell
+the info/new-commit-check directory is a Git repository whose
+objects/info/alternates file lists this repository and all other
+possible sources, and whose refs subdirectory contains symlinks
+to this repository's refs subdirectory, and to all other possible
+sources refs subdirectories.  Yes, this means that you cannot
+use packed-refs in those repositories as they won't be resolved
+correctly.
+
+=cut
+
+my $git_dir = $ENV{GIT_DIR};
+my $new_commit_check = "$git_dir/info/new-commit-check";
+my $ref = $ARGV[0];
+my $old = $ARGV[1];
+my $new = $ARGV[2];
+my $new_type;
+my ($this_user) = getpwuid $<; # REAL_USER_ID
+my $repository_name;
+my %user_committer;
+my @allow_rules;
+my @path_rules;
+my %diff_cache;
+
+sub deny ($) {
+       print STDERR "-Deny-    $_[0]\n" if $debug;
+       print STDERR "\ndenied: $_[0]\n\n";
+       exit 1;
+}
+
+sub grant ($) {
+       print STDERR "-Grant-   $_[0]\n" if $debug;
+       exit 0;
+}
+
+sub info ($) {
+       print STDERR "-Info-    $_[0]\n" if $debug;
+}
+
+sub git_value (@) {
+       open(T,'-|','git',@_); local $_ = <T>; chop; close T; $_;
+}
+
+sub match_string ($$) {
+       my ($acl_n, $ref) = @_;
+          ($acl_n eq $ref)
+       || ($acl_n =~ m,/$, && substr($ref,0,length $acl_n) eq $acl_n)
+       || ($acl_n =~ m,^\^, && $ref =~ m:$acl_n:);
+}
+
+sub parse_config ($$$$) {
+       my $data = shift;
+       local $ENV{GIT_DIR} = shift;
+       my $br = shift;
+       my $fn = shift;
+       info "Loading $br:$fn";
+       open(I,'-|','git','cat-file','blob',"$br:$fn");
+       my $section = '';
+       while (<I>) {
+               chomp;
+               if (/^\s*$/ || /^\s*#/) {
+               } elsif (/^\[([a-z]+)\]$/i) {
+                       $section = lc $1;
+               } elsif (/^\[([a-z]+)\s+"(.*)"\]$/i) {
+                       $section = join('.',lc $1,$2);
+               } elsif (/^\s*([a-z][a-z0-9]+)\s*=\s*(.*?)\s*$/i) {
+                       push @{$data->{join('.',$section,lc $1)}}, $2;
+               } else {
+                       deny "bad config file line $. in $br:$fn";
+               }
+       }
+       close I;
+}
+
+sub all_new_committers () {
+       local $ENV{GIT_DIR} = $git_dir;
+       $ENV{GIT_DIR} = $new_commit_check if -d $new_commit_check;
+
+       info "Getting committers of new commits.";
+       my %used;
+       open(T,'-|','git','rev-list','--pretty=raw',$new,'--not','--all');
+       while (<T>) {
+               next unless s/^committer //;
+               chop;
+               s/>.*$/>/;
+               info "Found $_." unless $used{$_}++;
+       }
+       close T;
+       info "No new commits." unless %used;
+       keys %used;
+}
+
+sub all_new_taggers () {
+       my %exists;
+       open(T,'-|','git','for-each-ref','--format=%(objectname)','refs/tags');
+       while (<T>) {
+               chop;
+               $exists{$_} = 1;
+       }
+       close T;
+
+       info "Getting taggers of new tags.";
+       my %used;
+       my $obj = $new;
+       my $obj_type = $new_type;
+       while ($obj_type eq 'tag') {
+               last if $exists{$obj};
+               $obj_type = '';
+               open(T,'-|','git','cat-file','tag',$obj);
+               while (<T>) {
+                       chop;
+                       if (/^object ([a-z0-9]{40})$/) {
+                               $obj = $1;
+                       } elsif (/^type (.+)$/) {
+                               $obj_type = $1;
+                       } elsif (s/^tagger //) {
+                               s/>.*$/>/;
+                               info "Found $_." unless $used{$_}++;
+                               last;
+                       }
+               }
+               close T;
+       }
+       info "No new tags." unless %used;
+       keys %used;
+}
+
+sub check_committers (@) {
+       my @bad;
+       foreach (@_) { push @bad, $_ unless $user_committer{$_}; }
+       if (@bad) {
+               print STDERR "\n";
+               print STDERR "You are not $_.\n" foreach (sort @bad);
+               deny "You cannot push changes not committed by you.";
+       }
+}
+
+sub load_diff ($) {
+       my $base = shift;
+       my $d = $diff_cache{$base};
+       unless ($d) {
+               local $/ = "\0";
+               my %this_diff;
+               if ($base =~ /^0{40}$/) {
+                       open(T,'-|','git','ls-tree',
+                               '-r','--name-only','-z',
+                               $new) or return undef;
+                       while (<T>) {
+                               chop;
+                               $this_diff{$_} = 'A';
+                       }
+                       close T or return undef;
+               } else {
+                       open(T,'-|','git','diff-tree',
+                               '-r','--name-status','-z',
+                               $base,$new) or return undef;
+                       while (<T>) {
+                               my $op = $_;
+                               chop $op;
+
+                               my $path = <T>;
+                               chop $path;
+
+                               $this_diff{$path} = $op;
+                       }
+                       close T or return undef;
+               }
+               $d = \%this_diff;
+               $diff_cache{$base} = $d;
+       }
+       return $d;
+}
+
+deny "No GIT_DIR inherited from caller" unless $git_dir;
+deny "Need a ref name" unless $ref;
+deny "Refusing funny ref $ref" unless $ref =~ s,^refs/,,;
+deny "Bad old value $old" unless $old =~ /^[a-z0-9]{40}$/;
+deny "Bad new value $new" unless $new =~ /^[a-z0-9]{40}$/;
+deny "Cannot determine who you are." unless $this_user;
+
+$repository_name = File::Spec->rel2abs($git_dir);
+$repository_name =~ m,/([^/]+)(?:\.git|/\.git)$,;
+$repository_name = $1;
+info "Updating in '$repository_name'.";
+
+my $op;
+if    ($old =~ /^0{40}$/) { $op = 'C'; }
+elsif ($new =~ /^0{40}$/) { $op = 'D'; }
+else                      { $op = 'R'; }
+
+# This is really an update (fast-forward) if the
+# merge base of $old and $new is $old.
+#
+$op = 'U' if ($op eq 'R'
+       && $ref =~ m,^heads/,
+       && $old eq git_value('merge-base',$old,$new));
+
+# Load the user's ACL file. Expand groups (user.memberof) one level.
+{
+       my %data = ('user.committer' => []);
+       parse_config(\%data,$acl_git,$acl_branch,"external/$repository_name.acl");
+
+       %data = (
+               'user.committer' => $data{'user.committer'},
+               'user.memberof' => [],
+       );
+       parse_config(\%data,$acl_git,$acl_branch,"users/$this_user.acl");
+
+       %user_committer = map {$_ => $_} @{$data{'user.committer'}};
+       my $rule_key = "repository.$repository_name.allow";
+       my $rules = $data{$rule_key} || [];
+
+       foreach my $group (@{$data{'user.memberof'}}) {
+               my %g;
+               parse_config(\%g,$acl_git,$acl_branch,"groups/$group.acl");
+               my $group_rules = $g{$rule_key};
+               push @$rules, @$group_rules if $group_rules;
+       }
+
+RULE:
+       foreach (@$rules) {
+               while (/\${user\.([a-z][a-zA-Z0-9]+)}/) {
+                       my $k = lc $1;
+                       my $v = $data{"user.$k"};
+                       next RULE unless defined $v;
+                       next RULE if @$v != 1;
+                       next RULE unless defined $v->[0];
+                       s/\${user\.$k}/$v->[0]/g;
+               }
+
+               if (/^([AMD ]+)\s+of\s+([^\s]+)\s+for\s+([^\s]+)\s+diff\s+([^\s]+)$/) {
+                       my ($ops, $pth, $ref, $bst) = ($1, $2, $3, $4);
+                       $ops =~ s/ //g;
+                       $pth =~ s/\\\\/\\/g;
+                       $ref =~ s/\\\\/\\/g;
+                       push @path_rules, [$ops, $pth, $ref, $bst];
+               } elsif (/^([AMD ]+)\s+of\s+([^\s]+)\s+for\s+([^\s]+)$/) {
+                       my ($ops, $pth, $ref) = ($1, $2, $3);
+                       $ops =~ s/ //g;
+                       $pth =~ s/\\\\/\\/g;
+                       $ref =~ s/\\\\/\\/g;
+                       push @path_rules, [$ops, $pth, $ref, $old];
+               } elsif (/^([CDRU ]+)\s+for\s+([^\s]+)$/) {
+                       my $ops = $1;
+                       my $ref = $2;
+                       $ops =~ s/ //g;
+                       $ref =~ s/\\\\/\\/g;
+                       push @allow_rules, [$ops, $ref];
+               } elsif (/^for\s+([^\s]+)$/) {
+                       # Mentioned, but nothing granted?
+               } elsif (/^[^\s]+$/) {
+                       s/\\\\/\\/g;
+                       push @allow_rules, ['U', $_];
+               }
+       }
+}
+
+if ($op ne 'D') {
+       $new_type = git_value('cat-file','-t',$new);
+
+       if ($ref =~ m,^heads/,) {
+               deny "$ref must be a commit." unless $new_type eq 'commit';
+       } elsif ($ref =~ m,^tags/,) {
+               deny "$ref must be an annotated tag." unless $new_type eq 'tag';
+       }
+
+       check_committers (all_new_committers);
+       check_committers (all_new_taggers) if $new_type eq 'tag';
+}
+
+info "$this_user wants $op for $ref";
+foreach my $acl_entry (@allow_rules) {
+       my ($acl_ops, $acl_n) = @$acl_entry;
+       next unless $acl_ops =~ /^[CDRU]+$/; # Uhh.... shouldn't happen.
+       next unless $acl_n;
+       next unless $op =~ /^[$acl_ops]$/;
+       next unless match_string $acl_n, $ref;
+
+       # Don't test path rules on branch deletes.
+       #
+       grant "Allowed by: $acl_ops for $acl_n" if $op eq 'D';
+
+       # Aggregate matching path rules; allow if there aren't
+       # any matching this ref.
+       #
+       my %pr;
+       foreach my $p_entry (@path_rules) {
+               my ($p_ops, $p_n, $p_ref, $p_bst) = @$p_entry;
+               next unless $p_ref;
+               push @{$pr{$p_bst}}, $p_entry if match_string $p_ref, $ref;
+       }
+       grant "Allowed by: $acl_ops for $acl_n" unless %pr;
+
+       # Allow only if all changes against a single base are
+       # allowed by file path rules.
+       #
+       my @bad;
+       foreach my $p_bst (keys %pr) {
+               my $diff_ref = load_diff $p_bst;
+               deny "Cannot difference trees." unless ref $diff_ref;
+
+               my %fd = %$diff_ref;
+               foreach my $p_entry (@{$pr{$p_bst}}) {
+                       my ($p_ops, $p_n, $p_ref, $p_bst) = @$p_entry;
+                       next unless $p_ops =~ /^[AMD]+$/;
+                       next unless $p_n;
+
+                       foreach my $f_n (keys %fd) {
+                               my $f_op = $fd{$f_n};
+                               next unless $f_op;
+                               next unless $f_op =~ /^[$p_ops]$/;
+                               delete $fd{$f_n} if match_string $p_n, $f_n;
+                       }
+                       last unless %fd;
+               }
+
+               if (%fd) {
+                       push @bad, [$p_bst, \%fd];
+               } else {
+                       # All changes relative to $p_bst were allowed.
+                       #
+                       grant "Allowed by: $acl_ops for $acl_n diff $p_bst";
+               }
+       }
+
+       foreach my $bad_ref (@bad) {
+               my ($p_bst, $fd) = @$bad_ref;
+               print STDERR "\n";
+               print STDERR "Not allowed to make the following changes:\n";
+               print STDERR "(base: $p_bst)\n";
+               foreach my $f_n (sort keys %$fd) {
+                       print STDERR "  $fd->{$f_n} $f_n\n";
+               }
+       }
+       deny "You are not permitted to $op $ref";
+}
+close A;
+deny "You are not permitted to $op $ref";
diff --git a/contrib/p4import/README b/contrib/p4import/README
new file mode 100644 (file)
index 0000000..b9892b6
--- /dev/null
@@ -0,0 +1 @@
+Please see contrib/fast-import/git-p4 for a better Perforce importer.
diff --git a/contrib/p4import/git-p4import.py b/contrib/p4import/git-p4import.py
new file mode 100644 (file)
index 0000000..0f3d97b
--- /dev/null
@@ -0,0 +1,360 @@
+#!/usr/bin/python
+#
+# This tool is copyright (c) 2006, Sean Estabrooks.
+# It is released under the Gnu Public License, version 2.
+#
+# Import Perforce branches into Git repositories.
+# Checking out the files is done by calling the standard p4
+# client which you must have properly configured yourself
+#
+
+import marshal
+import os
+import sys
+import time
+import getopt
+
+from signal import signal, \
+   SIGPIPE, SIGINT, SIG_DFL, \
+   default_int_handler
+
+signal(SIGPIPE, SIG_DFL)
+s = signal(SIGINT, SIG_DFL)
+if s != default_int_handler:
+   signal(SIGINT, s)
+
+def die(msg, *args):
+    for a in args:
+        msg = "%s %s" % (msg, a)
+    print "git-p4import fatal error:", msg
+    sys.exit(1)
+
+def usage():
+    print "USAGE: git-p4import [-q|-v]  [--authors=<file>]  [-t <timezone>]  [//p4repo/path <branch>]"
+    sys.exit(1)
+
+verbosity = 1
+logfile = "/dev/null"
+ignore_warnings = False
+stitch = 0
+tagall = True
+
+def report(level, msg, *args):
+    global verbosity
+    global logfile
+    for a in args:
+        msg = "%s %s" % (msg, a)
+    fd = open(logfile, "a")
+    fd.writelines(msg)
+    fd.close()
+    if level <= verbosity:
+        print msg
+
+class p4_command:
+    def __init__(self, _repopath):
+        try:
+            global logfile
+            self.userlist = {}
+            if _repopath[-1] == '/':
+                self.repopath = _repopath[:-1]
+            else:
+                self.repopath = _repopath
+            if self.repopath[-4:] != "/...":
+                self.repopath= "%s/..." % self.repopath
+            f=os.popen('p4 -V 2>>%s'%logfile, 'rb')
+            a = f.readlines()
+            if f.close():
+                raise
+        except:
+                die("Could not find the \"p4\" command")
+
+    def p4(self, cmd, *args):
+        global logfile
+        cmd = "%s %s" % (cmd, ' '.join(args))
+        report(2, "P4:", cmd)
+        f=os.popen('p4 -G %s 2>>%s' % (cmd,logfile), 'rb')
+        list = []
+        while 1:
+           try:
+                list.append(marshal.load(f))
+           except EOFError:
+                break
+        self.ret = f.close()
+        return list
+
+    def sync(self, id, force=False, trick=False, test=False):
+        if force:
+            ret = self.p4("sync -f %s@%s"%(self.repopath, id))[0]
+        elif trick:
+            ret = self.p4("sync -k %s@%s"%(self.repopath, id))[0]
+        elif test:
+            ret = self.p4("sync -n %s@%s"%(self.repopath, id))[0]
+        else:
+            ret = self.p4("sync    %s@%s"%(self.repopath, id))[0]
+        if ret['code'] == "error":
+             data = ret['data'].upper()
+             if data.find('VIEW') > 0:
+                 die("Perforce reports %s is not in client view"% self.repopath)
+             elif data.find('UP-TO-DATE') < 0:
+                 die("Could not sync files from perforce", self.repopath)
+
+    def changes(self, since=0):
+        try:
+            list = []
+            for rec in self.p4("changes %s@%s,#head" % (self.repopath, since+1)):
+                list.append(rec['change'])
+            list.reverse()
+            return list
+        except:
+            return []
+
+    def authors(self, filename):
+        f=open(filename)
+        for l in f.readlines():
+            self.userlist[l[:l.find('=')].rstrip()] = \
+                    (l[l.find('=')+1:l.find('<')].rstrip(),l[l.find('<')+1:l.find('>')])
+        f.close()
+        for f,e in self.userlist.items():
+                report(2, f, ":", e[0], "  <", e[1], ">")
+
+    def _get_user(self, id):
+        if not self.userlist.has_key(id):
+            try:
+                user = self.p4("users", id)[0]
+                self.userlist[id] = (user['FullName'], user['Email'])
+            except:
+                self.userlist[id] = (id, "")
+        return self.userlist[id]
+
+    def _format_date(self, ticks):
+        symbol='+'
+        name = time.tzname[0]
+        offset = time.timezone
+        if ticks[8]:
+            name = time.tzname[1]
+            offset = time.altzone
+        if offset < 0:
+            offset *= -1
+            symbol = '-'
+        localo = "%s%02d%02d %s" % (symbol, offset / 3600, offset % 3600, name)
+        tickso = time.strftime("%a %b %d %H:%M:%S %Y", ticks)
+        return "%s %s" % (tickso, localo)
+
+    def where(self):
+        try:
+            return self.p4("where %s" % self.repopath)[-1]['path']
+        except:
+            return ""
+
+    def describe(self, num):
+        desc = self.p4("describe -s", num)[0]
+        self.msg = desc['desc']
+        self.author, self.email = self._get_user(desc['user'])
+        self.date = self._format_date(time.localtime(long(desc['time'])))
+        return self
+
+class git_command:
+    def __init__(self):
+        try:
+            self.version = self.git("--version")[0][12:].rstrip()
+        except:
+            die("Could not find the \"git\" command")
+        try:
+            self.gitdir = self.get_single("rev-parse --git-dir")
+            report(2, "gdir:", self.gitdir)
+        except:
+            die("Not a git repository... did you forget to \"git init\" ?")
+        try:
+            self.cdup = self.get_single("rev-parse --show-cdup")
+            if self.cdup != "":
+                os.chdir(self.cdup)
+            self.topdir = os.getcwd()
+            report(2, "topdir:", self.topdir)
+        except:
+            die("Could not find top git directory")
+
+    def git(self, cmd):
+        global logfile
+        report(2, "GIT:", cmd)
+        f=os.popen('git %s 2>>%s' % (cmd,logfile), 'rb')
+        r=f.readlines()
+        self.ret = f.close()
+        return r
+
+    def get_single(self, cmd):
+        return self.git(cmd)[0].rstrip()
+
+    def current_branch(self):
+        try:
+            testit = self.git("rev-parse --verify HEAD")[0]
+            return self.git("symbolic-ref HEAD")[0][11:].rstrip()
+        except:
+            return None
+
+    def get_config(self, variable):
+        try:
+            return self.git("config --get %s" % variable)[0].rstrip()
+        except:
+            return None
+
+    def set_config(self, variable, value):
+        try:
+            self.git("config %s %s"%(variable, value) )
+        except:
+            die("Could not set %s to " % variable, value)
+
+    def make_tag(self, name, head):
+        self.git("tag -f %s %s"%(name,head))
+
+    def top_change(self, branch):
+        try:
+            a=self.get_single("name-rev --tags refs/heads/%s" % branch)
+            loc = a.find(' tags/') + 6
+            if a[loc:loc+3] != "p4/":
+                raise
+            return int(a[loc+3:][:-2])
+        except:
+            return 0
+
+    def update_index(self):
+        self.git("ls-files -m -d -o -z | git update-index --add --remove -z --stdin")
+
+    def checkout(self, branch):
+        self.git("checkout %s" % branch)
+
+    def repoint_head(self, branch):
+        self.git("symbolic-ref HEAD refs/heads/%s" % branch)
+
+    def remove_files(self):
+        self.git("ls-files | xargs rm")
+
+    def clean_directories(self):
+        self.git("clean -d")
+
+    def fresh_branch(self, branch):
+        report(1, "Creating new branch", branch)
+        self.git("ls-files | xargs rm")
+        os.remove(".git/index")
+        self.repoint_head(branch)
+        self.git("clean -d")
+
+    def basedir(self):
+        return self.topdir
+
+    def commit(self, author, email, date, msg, id):
+        self.update_index()
+        fd=open(".msg", "w")
+        fd.writelines(msg)
+        fd.close()
+        try:
+                current = self.get_single("rev-parse --verify HEAD")
+                head = "-p HEAD"
+        except:
+                current = ""
+                head = ""
+        tree = self.get_single("write-tree")
+        for r,l in [('DATE',date),('NAME',author),('EMAIL',email)]:
+            os.environ['GIT_AUTHOR_%s'%r] = l
+            os.environ['GIT_COMMITTER_%s'%r] = l
+        commit = self.get_single("commit-tree %s %s < .msg" % (tree,head))
+        os.remove(".msg")
+        self.make_tag("p4/%s"%id, commit)
+        self.git("update-ref HEAD %s %s" % (commit, current) )
+
+try:
+    opts, args = getopt.getopt(sys.argv[1:], "qhvt:",
+            ["authors=","help","stitch=","timezone=","log=","ignore","notags"])
+except getopt.GetoptError:
+    usage()
+
+for o, a in opts:
+    if o == "-q":
+        verbosity = 0
+    if o == "-v":
+        verbosity += 1
+    if o in ("--log"):
+        logfile = a
+    if o in ("--notags"):
+        tagall = False
+    if o in ("-h", "--help"):
+        usage()
+    if o in ("--ignore"):
+        ignore_warnings = True
+
+git = git_command()
+branch=git.current_branch()
+
+for o, a in opts:
+    if o in ("-t", "--timezone"):
+        git.set_config("perforce.timezone", a)
+    if o in ("--stitch"):
+        git.set_config("perforce.%s.path" % branch, a)
+        stitch = 1
+
+if len(args) == 2:
+    branch = args[1]
+    git.checkout(branch)
+    if branch == git.current_branch():
+        die("Branch %s already exists!" % branch)
+    report(1, "Setting perforce to ", args[0])
+    git.set_config("perforce.%s.path" % branch, args[0])
+elif len(args) != 0:
+    die("You must specify the perforce //depot/path and git branch")
+
+p4path = git.get_config("perforce.%s.path" % branch)
+if p4path == None:
+    die("Do not know Perforce //depot/path for git branch", branch)
+
+p4 = p4_command(p4path)
+
+for o, a in opts:
+    if o in ("-a", "--authors"):
+        p4.authors(a)
+
+localdir = git.basedir()
+if p4.where()[:len(localdir)] != localdir:
+    report(1, "**WARNING** Appears p4 client is misconfigured")
+    report(1, "   for sync from %s to %s" % (p4.repopath, localdir))
+    if ignore_warnings != True:
+        die("Reconfigure or use \"--ignore\" on command line")
+
+if stitch == 0:
+    top = git.top_change(branch)
+else:
+    top = 0
+changes = p4.changes(top)
+count = len(changes)
+if count == 0:
+    report(1, "Already up to date...")
+    sys.exit(0)
+
+ptz = git.get_config("perforce.timezone")
+if ptz:
+    report(1, "Setting timezone to", ptz)
+    os.environ['TZ'] = ptz
+    time.tzset()
+
+if stitch == 1:
+    git.remove_files()
+    git.clean_directories()
+    p4.sync(changes[0], force=True)
+elif top == 0 and branch != git.current_branch():
+    p4.sync(changes[0], test=True)
+    report(1, "Creating new initial commit");
+    git.fresh_branch(branch)
+    p4.sync(changes[0], force=True)
+else:
+    p4.sync(changes[0], trick=True)
+
+report(1, "processing %s changes from p4 (%s) to git (%s)" % (count, p4.repopath, branch))
+for id in changes:
+    report(1, "Importing changeset", id)
+    change = p4.describe(id)
+    p4.sync(id)
+    if tagall :
+            git.commit(change.author, change.email, change.date, change.msg, id)
+    else:
+            git.commit(change.author, change.email, change.date, change.msg, "import")
+    if stitch == 1:
+        git.clean_directories()
+        stitch = 0
diff --git a/contrib/p4import/git-p4import.txt b/contrib/p4import/git-p4import.txt
new file mode 100644 (file)
index 0000000..9967587
--- /dev/null
@@ -0,0 +1,167 @@
+git-p4import(1)
+===============
+
+NAME
+----
+git-p4import - Import a Perforce repository into git
+
+
+SYNOPSIS
+--------
+[verse]
+`git-p4import` [-q|-v] [--notags] [--authors <file>] [-t <timezone>]
+               <//p4repo/path> <branch>
+`git-p4import` --stitch <//p4repo/path>
+`git-p4import`
+
+
+DESCRIPTION
+-----------
+Import a Perforce repository into an existing git repository.  When
+a <//p4repo/path> and <branch> are specified a new branch with the
+given name will be created and the initial import will begin.
+
+Once the initial import is complete you can do an incremental import
+of new commits from the Perforce repository.  You do this by checking
+out the appropriate git branch and then running `git-p4import` without
+any options.
+
+The standard p4 client is used to communicate with the Perforce
+repository; it must be configured correctly in order for `git-p4import`
+to operate (see below).
+
+
+OPTIONS
+-------
+-q::
+       Do not display any progress information.
+
+-v::
+        Give extra progress information.
+
+\--authors::
+       Specify an authors file containing a mapping of Perforce user
+       ids to full names and email addresses (see Notes below).
+
+\--notags::
+       Do not create a tag for each imported commit.
+
+\--stitch::
+       Import the contents of the given perforce branch into the
+       currently checked out git branch.
+
+\--log::
+       Store debugging information in the specified file.
+
+-t::
+       Specify that the remote repository is in the specified timezone.
+       Timezone must be in the format "US/Pacific" or "Europe/London"
+       etc.  You only need to specify this once, it will be saved in
+       the git config file for the repository.
+
+<//p4repo/path>::
+       The Perforce path that will be imported into the specified branch.
+
+<branch>::
+       The new branch that will be created to hold the Perforce imports.
+
+
+P4 Client
+---------
+You must make the `p4` client command available in your $PATH and
+configure it to communicate with the target Perforce repository.
+Typically this means you must set the "$P4PORT" and "$P4CLIENT"
+environment variables.
+
+You must also configure a `p4` client "view" which maps the Perforce
+branch into the top level of your git repository, for example:
+
+------------
+Client: myhost
+
+Root:   /home/sean/import
+
+Options:   noallwrite clobber nocompress unlocked modtime rmdir
+
+View:
+        //public/jam/... //myhost/jam/...
+------------
+
+With the above `p4` client setup, you could import the "jam"
+perforce branch into a branch named "jammy", like so:
+
+------------
+$ mkdir -p /home/sean/import/jam
+$ cd /home/sean/import/jam
+$ git init
+$ git p4import //public/jam jammy
+------------
+
+
+Multiple Branches
+-----------------
+Note that by creating multiple "views" you can use `git-p4import`
+to import additional branches into the same git repository.
+However, the `p4` client has a limitation in that it silently
+ignores all but the last "view" that maps into the same local
+directory.  So the following will *not* work:
+
+------------
+View:
+        //public/jam/... //myhost/jam/...
+        //public/other/... //myhost/jam/...
+        //public/guest/... //myhost/jam/...
+------------
+
+If you want more than one Perforce branch to be imported into the
+same directory you must employ a workaround.  A simple option is
+to adjust your `p4` client before each import to only include a
+single view.
+
+Another option is to create multiple symlinks locally which all
+point to the same directory in your git repository and then use
+one per "view" instead of listing the actual directory.
+
+
+Tags
+----
+A git tag of the form p4/xx is created for every change imported from
+the Perforce repository where xx is the Perforce changeset number.
+Therefore after the import you can use git to access any commit by its
+Perforce number, e.g. git show p4/327.
+
+The tag associated with the HEAD commit is also how `git-p4import`
+determines if there are new changes to incrementally import from the
+Perforce repository.
+
+If you import from a repository with many thousands of changes
+you will have an equal number of p4/xxxx git tags.  Git tags can
+be expensive in terms of disk space and repository operations.
+If you don't need to perform further incremental imports, you
+may delete the tags.
+
+
+Notes
+-----
+You can interrupt the import (e.g. ctrl-c) at any time and restart it
+without worry.
+
+Author information is automatically determined by querying the
+Perforce "users" table using the id associated with each change.
+However, if you want to manually supply these mappings you can do
+so with the "--authors" option.  It accepts a file containing a list
+of mappings with each line containing one mapping in the format:
+
+------------
+    perforce_id = Full Name <email@address.com>
+------------
+
+
+Author
+------
+Written by Sean Estabrooks <seanlkml@sympatico.ca>
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/contrib/patches/docbook-xsl-manpages-charmap.patch b/contrib/patches/docbook-xsl-manpages-charmap.patch
new file mode 100644 (file)
index 0000000..f2b08b4
--- /dev/null
@@ -0,0 +1,21 @@
+From: Ismail Dönmez <ismail@pardus.org.tr>
+
+Trying to build the documentation with docbook-xsl 1.73 may result in
+the following error.  This patch fixes it.
+
+$ xmlto -m callouts.xsl man git-add.xml
+runtime error: file
+file:///usr/share/sgml/docbook/xsl-stylesheets-1.73.0/manpages/other.xsl line
+129 element call-template
+The called template 'read-character-map' was not found.
+
+--- docbook-xsl-1.73.0/manpages/docbook.xsl.manpages-charmap   2007-07-23 16:24:23.000000000 +0100
++++ docbook-xsl-1.73.0/manpages/docbook.xsl    2007-07-23 16:25:16.000000000 +0100
+@@ -37,6 +37,7 @@
+   <xsl:include href="lists.xsl"/>
+   <xsl:include href="endnotes.xsl"/>
+   <xsl:include href="table.xsl"/>
++  <xsl:include href="../common/charmap.xsl"/>
+
+   <!-- * we rename the following just to avoid using params with "man" -->
+   <!-- * prefixes in the table.xsl stylesheet (because that stylesheet -->
diff --git a/contrib/remotes2config.sh b/contrib/remotes2config.sh
new file mode 100755 (executable)
index 0000000..1cda19f
--- /dev/null
@@ -0,0 +1,33 @@
+#!/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/stats/git-common-hash b/contrib/stats/git-common-hash
new file mode 100755 (executable)
index 0000000..e27fd08
--- /dev/null
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+# This script displays the distribution of longest common hash prefixes.
+# This can be used to determine the minimum prefix length to use
+# for object names to be unique.
+
+git rev-list --objects --all | sort | perl -lne '
+  substr($_, 40) = "";
+  # uncomment next line for a distribution of bits instead of hex chars
+  # $_ = unpack("B*",pack("H*",$_));
+  if (defined $p) {
+    ($p ^ $_) =~ /^(\0*)/;
+    $common = length $1;
+    if (defined $pcommon) {
+      $count[$pcommon > $common ? $pcommon : $common]++;
+    } else {
+      $count[$common]++; # first item
+    }
+  }
+  $p = $_;
+  $pcommon = $common;
+  END {
+    $count[$common]++; # last item
+    print "$_: $count[$_]" for 0..$#count;
+  }
+'
diff --git a/contrib/stats/mailmap.pl b/contrib/stats/mailmap.pl
new file mode 100755 (executable)
index 0000000..4b852e2
--- /dev/null
@@ -0,0 +1,38 @@
+#!/usr/bin/perl -w
+my %mailmap = ();
+open I, "<", ".mailmap";
+while (<I>) {
+       chomp;
+       next if /^#/;
+       if (my ($author, $mail) = /^(.*?)\s+<(.+)>$/) {
+               $mailmap{$mail} = $author;
+       }
+}
+close I;
+
+my %mail2author = ();
+open I, "git log --pretty='format:%ae  %an' |";
+while (<I>) {
+       chomp;
+       my ($mail, $author) = split(/\t/, $_);
+       next if exists $mailmap{$mail};
+       $mail2author{$mail} ||= {};
+       $mail2author{$mail}{$author} ||= 0;
+       $mail2author{$mail}{$author}++;
+}
+close I;
+
+while (my ($mail, $authorcount) = each %mail2author) {
+       # %$authorcount is ($author => $count);
+       # sort and show the names from the most frequent ones.
+       my @names = (map { $_->[0] }
+               sort { $b->[1] <=> $a->[1] }
+               map { [$_, $authorcount->{$_}] }
+               keys %$authorcount);
+       if (1 < @names) {
+               for (@names) {
+                       print "$_ <$mail>\n";
+               }
+       }
+}
+
diff --git a/contrib/stats/packinfo.pl b/contrib/stats/packinfo.pl
new file mode 100755 (executable)
index 0000000..aab501e
--- /dev/null
@@ -0,0 +1,212 @@
+#!/usr/bin/perl
+#
+# This tool will print vaguely pretty information about a pack.  It
+# expects the output of "git-verify-pack -v" as input on stdin.
+#
+# $ git-verify-pack -v | packinfo.pl
+#
+# This prints some full-pack statistics; currently "all sizes", "all
+# path sizes", "tree sizes", "tree path sizes", and "depths".
+#
+# * "all sizes" stats are across every object size in the file;
+#   full sizes for base objects, and delta size for deltas.
+# * "all path sizes" stats are across all object's "path sizes".
+#   A path size is the sum of the size of the delta chain, including the
+#   base object.  In other words, it's how many bytes need be read to
+#   reassemble the file from deltas.
+# * "tree sizes" are object sizes grouped into delta trees.
+# * "tree path sizes" are path sizes grouped into delta trees.
+# * "depths" should be obvious.
+#
+# When run as:
+#
+# $ git-verify-pack -v | packinfo.pl -tree
+#
+# the trees of objects are output along with the stats.  This looks
+# like:
+#
+#   0 commit 031321c6...      803      803
+#
+#   0   blob 03156f21...     1767     1767
+#   1    blob f52a9d7f...       10     1777
+#   2     blob a8cc5739...       51     1828
+#   3      blob 660e90b1...       15     1843
+#   4       blob 0cb8e3bb...       33     1876
+#   2     blob e48607f0...      311     2088
+#      size: count 6 total 2187 min 10 max 1767 mean 364.50 median 51 std_dev 635.85
+# path size: count 6 total 11179 min 1767 max 2088 mean 1863.17 median 1843 std_dev 107.26
+#
+# The first number after the sha1 is the object size, the second
+# number is the path size.  The statistics are across all objects in
+# the previous delta tree.  Obviously they are omitted for trees of
+# one object.
+#
+# When run as:
+#
+# $ git-verify-pack -v | packinfo.pl -tree -filenames
+#
+# it adds filenames to the tree.  Getting this information is slow:
+#
+#   0   blob 03156f21...     1767     1767 Documentation/git-lost-found.txt @ tags/v1.2.0~142
+#   1    blob f52a9d7f...       10     1777 Documentation/git-lost-found.txt @ tags/v1.5.0-rc1~74
+#   2     blob a8cc5739...       51     1828 Documentation/git-lost+found.txt @ tags/v0.99.9h^0
+#   3      blob 660e90b1...       15     1843 Documentation/git-lost+found.txt @ master~3222^2~2
+#   4       blob 0cb8e3bb...       33     1876 Documentation/git-lost+found.txt @ master~3222^2~3
+#   2     blob e48607f0...      311     2088 Documentation/git-lost-found.txt @ tags/v1.5.2-rc3~4
+#      size: count 6 total 2187 min 10 max 1767 mean 364.50 median 51 std_dev 635.85
+# path size: count 6 total 11179 min 1767 max 2088 mean 1863.17 median 1843 std_dev 107.26
+#
+# When run as:
+#
+# $ git-verify-pack -v | packinfo.pl -dump
+#
+# it prints out "sha1 size pathsize depth" for each sha1 in lexical
+# order.
+#
+# 000079a2eaef17b7eae70e1f0f635557ea67b644 30 472 7
+# 00013cafe6980411aa6fdd940784917b5ff50f0a 44 1542 4
+# 000182eacf99cde27d5916aa415921924b82972c 499 499 0
+# ...
+#
+# This is handy for comparing two packs.  Adding "-filenames" will add
+# filenames, as per "-tree -filenames" above.
+
+use strict;
+use Getopt::Long;
+
+my $filenames = 0;
+my $tree = 0;
+my $dump = 0;
+GetOptions("tree" => \$tree,
+           "filenames" => \$filenames,
+           "dump" => \$dump);
+
+my %parents;
+my %children;
+my %sizes;
+my @roots;
+my %paths;
+my %types;
+my @commits;
+my %names;
+my %depths;
+my @depths;
+
+while (<STDIN>) {
+    my ($sha1, $type, $size, $offset, $depth, $parent) = split(/\s+/, $_);
+    next unless ($sha1 =~ /^[0-9a-f]{40}$/);
+    $depths{$sha1} = $depth || 0;
+    push(@depths, $depth || 0);
+    push(@commits, $sha1) if ($type eq 'commit');
+    push(@roots, $sha1) unless $parent;
+    $parents{$sha1} = $parent;
+    $types{$sha1} = $type;
+    push(@{$children{$parent}}, $sha1);
+    $sizes{$sha1} = $size;
+}
+
+if ($filenames && ($tree || $dump)) {
+    open(NAMES, "git-name-rev --all|");
+    while (<NAMES>) {
+        if (/^(\S+)\s+(.*)$/) {
+            my ($sha1, $name) = ($1, $2);
+            $names{$sha1} = $name;
+        }
+    }
+    close NAMES;
+
+    for my $commit (@commits) {
+        my $name = $names{$commit};
+        open(TREE, "git-ls-tree -t -r $commit|");
+        print STDERR "Plumbing tree $name\n";
+        while (<TREE>) {
+            if (/^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/) {
+                my ($mode, $type, $sha1, $path) = ($1, $2, $3, $4);
+                $paths{$sha1} = "$path @ $name";
+            }
+        }
+        close TREE;
+    }
+}
+
+sub stats {
+    my @data = sort {$a <=> $b} @_;
+    my $min = $data[0];
+    my $max = $data[$#data];
+    my $total = 0;
+    my $count = scalar @data;
+    for my $datum (@data) {
+        $total += $datum;
+    }
+    my $mean = $total / $count;
+    my $median = $data[int(@data / 2)];
+    my $diff_sum = 0;
+    for my $datum (@data) {
+        $diff_sum += ($datum - $mean)**2;
+    }
+    my $std_dev = sqrt($diff_sum / $count);
+    return ($count, $total, $min, $max, $mean, $median, $std_dev);
+}
+
+sub print_stats {
+    my $name = shift;
+    my ($count, $total, $min, $max, $mean, $median, $std_dev) = stats(@_);
+    printf("%s: count %s total %s min %s max %s mean %.2f median %s std_dev %.2f\n",
+           $name, $count, $total, $min, $max, $mean, $median, $std_dev);
+}
+
+my @sizes;
+my @path_sizes;
+my @all_sizes;
+my @all_path_sizes;
+my %path_sizes;
+
+sub dig {
+    my ($sha1, $depth, $path_size) = @_;
+    $path_size += $sizes{$sha1};
+    push(@sizes, $sizes{$sha1});
+    push(@all_sizes, $sizes{$sha1});
+    push(@path_sizes, $path_size);
+    push(@all_path_sizes, $path_size);
+    $path_sizes{$sha1} = $path_size;
+    if ($tree) {
+        printf("%3d%s %6s %s %8d %8d %s\n",
+               $depth, (" " x $depth), $types{$sha1},
+               $sha1, $sizes{$sha1}, $path_size, $paths{$sha1});
+    }
+    for my $child (@{$children{$sha1}}) {
+        dig($child, $depth + 1, $path_size);
+    }
+}
+
+my @tree_sizes;
+my @tree_path_sizes;
+
+for my $root (@roots) {
+    undef @sizes;
+    undef @path_sizes;
+    dig($root, 0, 0);
+    my ($aa, $sz_total) = stats(@sizes);
+    my ($bb, $psz_total) = stats(@path_sizes);
+    push(@tree_sizes, $sz_total);
+    push(@tree_path_sizes, $psz_total);
+    if ($tree) {
+        if (@sizes > 1) {
+            print_stats("     size", @sizes);
+            print_stats("path size", @path_sizes);
+        }
+        print "\n";
+    }
+}
+
+if ($dump) {
+    for my $sha1 (sort keys %sizes) {
+        print "$sha1 $sizes{$sha1} $path_sizes{$sha1} $depths{$sha1} $paths{$sha1}\n";
+    }
+} else {
+    print_stats("      all sizes", @all_sizes);
+    print_stats(" all path sizes", @all_path_sizes);
+    print_stats("     tree sizes", @tree_sizes);
+    print_stats("tree path sizes", @tree_path_sizes);
+    print_stats("         depths", @depths);
+}
diff --git a/contrib/vim/README b/contrib/vim/README
new file mode 100644 (file)
index 0000000..9e7881f
--- /dev/null
@@ -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 (file)
index 0000000..332121b
--- /dev/null
@@ -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 (executable)
index 0000000..2838546
--- /dev/null
@@ -0,0 +1,82 @@
+#!/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 ...
+git_dir=$(cd "$orig_git" 2>/dev/null &&
+  git rev-parse --git-dir 2>/dev/null) ||
+  die "\"$orig_git\" is not a git repository!"
+
+case "$git_dir" in
+.git)
+       git_dir="$orig_git/.git"
+       ;;
+.)
+       git_dir=$orig_git
+       ;;
+esac
+
+# don't link to a configured bare repository
+isbare=$(git --git-dir="$git_dir" config --bool --get core.bare)
+if test ztrue = z$isbare
+then
+       die "\"$git_dir\" has core.bare set to true," \
+               " remove from \"$git_dir/config\" to use $0"
+fi
+
+# don't link to a workdir
+if test -L "$git_dir/config"
+then
+       die "\"$orig_git\" is a working directory only, please specify" \
+               "a complete repository."
+fi
+
+# don't recreate a workdir over an existing repository
+if test -e "$new_workdir"
+then
+       die "destination directory '$new_workdir' already exists."
+fi
+
+# make sure the the links use full paths
+git_dir=$(cd "$git_dir"; 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 "$git_dir/$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 "$git_dir/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
diff --git a/convert.c b/convert.c
new file mode 100644 (file)
index 0000000..4df7559
--- /dev/null
+++ b/convert.c
@@ -0,0 +1,592 @@
+#include "cache.h"
+#include "attr.h"
+#include "run-command.h"
+
+/*
+ * convert.c - convert a file when checking it out and checking it in.
+ *
+ * This should use the pathname to decide on whether it wants to do some
+ * more interesting conversions (automatic gzip/unzip, general format
+ * conversions etc etc), but by default it just does automatic CRLF<->LF
+ * translation when the "auto_crlf" option is set.
+ */
+
+#define CRLF_GUESS     (-1)
+#define CRLF_BINARY    0
+#define CRLF_TEXT      1
+#define CRLF_INPUT     2
+
+struct text_stat {
+       /* CR, LF and CRLF counts */
+       unsigned cr, lf, crlf;
+
+       /* These are just approximations! */
+       unsigned printable, nonprintable;
+};
+
+static void gather_stats(const char *buf, unsigned long size, struct text_stat *stats)
+{
+       unsigned long i;
+
+       memset(stats, 0, sizeof(*stats));
+
+       for (i = 0; i < size; i++) {
+               unsigned char c = buf[i];
+               if (c == '\r') {
+                       stats->cr++;
+                       if (i+1 < size && buf[i+1] == '\n')
+                               stats->crlf++;
+                       continue;
+               }
+               if (c == '\n') {
+                       stats->lf++;
+                       continue;
+               }
+               if (c == 127)
+                       /* DEL */
+                       stats->nonprintable++;
+               else if (c < 32) {
+                       switch (c) {
+                               /* BS, HT, ESC and FF */
+                       case '\b': case '\t': case '\033': case '\014':
+                               stats->printable++;
+                               break;
+                       default:
+                               stats->nonprintable++;
+                       }
+               }
+               else
+                       stats->printable++;
+       }
+}
+
+/*
+ * The same heuristics as diff.c::mmfile_is_binary()
+ */
+static int is_binary(unsigned long size, struct text_stat *stats)
+{
+
+       if ((stats->printable >> 7) < stats->nonprintable)
+               return 1;
+       /*
+        * Other heuristics? Average line length might be relevant,
+        * as might LF vs CR vs CRLF counts..
+        *
+        * NOTE! It might be normal to have a low ratio of CRLF to LF
+        * (somebody starts with a LF-only file and edits it with an editor
+        * that adds CRLF only to lines that are added..). But do  we
+        * want to support CR-only? Probably not.
+        */
+       return 0;
+}
+
+static int crlf_to_git(const char *path, const char *src, size_t len,
+                       struct strbuf *buf, int action)
+{
+       struct text_stat stats;
+       char *dst;
+
+       if ((action == CRLF_BINARY) || !auto_crlf || !len)
+               return 0;
+
+       gather_stats(src, len, &stats);
+       /* No CR? Nothing to convert, regardless. */
+       if (!stats.cr)
+               return 0;
+
+       if (action == CRLF_GUESS) {
+               /*
+                * We're currently not going to even try to convert stuff
+                * that has bare CR characters. Does anybody do that crazy
+                * stuff?
+                */
+               if (stats.cr != stats.crlf)
+                       return 0;
+
+               /*
+                * And add some heuristics for binary vs text, of course...
+                */
+               if (is_binary(len, &stats))
+                       return 0;
+       }
+
+       /* only grow if not in place */
+       if (strbuf_avail(buf) + buf->len < len)
+               strbuf_grow(buf, len - buf->len);
+       dst = buf->buf;
+       if (action == CRLF_GUESS) {
+               /*
+                * If we guessed, we already know we rejected a file with
+                * lone CR, and we can strip a CR without looking at what
+                * follow it.
+                */
+               do {
+                       unsigned char c = *src++;
+                       if (c != '\r')
+                               *dst++ = c;
+               } while (--len);
+       } else {
+               do {
+                       unsigned char c = *src++;
+                       if (! (c == '\r' && (1 < len && *src == '\n')))
+                               *dst++ = c;
+               } while (--len);
+       }
+       strbuf_setlen(buf, dst - buf->buf);
+       return 1;
+}
+
+static int crlf_to_worktree(const char *path, const char *src, size_t len,
+                            struct strbuf *buf, int action)
+{
+       char *to_free = NULL;
+       struct text_stat stats;
+
+       if ((action == CRLF_BINARY) || (action == CRLF_INPUT) ||
+           auto_crlf <= 0)
+               return 0;
+
+       if (!len)
+               return 0;
+
+       gather_stats(src, len, &stats);
+
+       /* No LF? Nothing to convert, regardless. */
+       if (!stats.lf)
+               return 0;
+
+       /* Was it already in CRLF format? */
+       if (stats.lf == stats.crlf)
+               return 0;
+
+       if (action == CRLF_GUESS) {
+               /* If we have any bare CR characters, we're not going to touch it */
+               if (stats.cr != stats.crlf)
+                       return 0;
+
+               if (is_binary(len, &stats))
+                       return 0;
+       }
+
+       /* are we "faking" in place editing ? */
+       if (src == buf->buf)
+               to_free = strbuf_detach(buf, NULL);
+
+       strbuf_grow(buf, len + stats.lf - stats.crlf);
+       for (;;) {
+               const char *nl = memchr(src, '\n', len);
+               if (!nl)
+                       break;
+               if (nl > src && nl[-1] == '\r') {
+                       strbuf_add(buf, src, nl + 1 - src);
+               } else {
+                       strbuf_add(buf, src, nl - src);
+                       strbuf_addstr(buf, "\r\n");
+               }
+               len -= nl + 1 - src;
+               src  = nl + 1;
+       }
+       strbuf_add(buf, src, len);
+
+       free(to_free);
+       return 1;
+}
+
+struct filter_params {
+       const char *src;
+       unsigned long size;
+       const char *cmd;
+};
+
+static int filter_buffer(int fd, void *data)
+{
+       /*
+        * Spawn cmd and feed the buffer contents through its stdin.
+        */
+       struct child_process child_process;
+       struct filter_params *params = (struct filter_params *)data;
+       int write_err, status;
+       const char *argv[] = { "sh", "-c", params->cmd, NULL };
+
+       memset(&child_process, 0, sizeof(child_process));
+       child_process.argv = argv;
+       child_process.in = -1;
+       child_process.out = fd;
+
+       if (start_command(&child_process))
+               return error("cannot fork to run external filter %s", params->cmd);
+
+       write_err = (write_in_full(child_process.in, params->src, params->size) < 0);
+       if (close(child_process.in))
+               write_err = 1;
+       if (write_err)
+               error("cannot feed the input to external filter %s", params->cmd);
+
+       status = finish_command(&child_process);
+       if (status)
+               error("external filter %s failed %d", params->cmd, -status);
+       return (write_err || status);
+}
+
+static int apply_filter(const char *path, const char *src, size_t len,
+                        struct strbuf *dst, const char *cmd)
+{
+       /*
+        * Create a pipeline to have the command filter the buffer's
+        * contents.
+        *
+        * (child --> cmd) --> us
+        */
+       int ret = 1;
+       struct strbuf nbuf;
+       struct async async;
+       struct filter_params params;
+
+       if (!cmd)
+               return 0;
+
+       memset(&async, 0, sizeof(async));
+       async.proc = filter_buffer;
+       async.data = &params;
+       params.src = src;
+       params.size = len;
+       params.cmd = cmd;
+
+       fflush(NULL);
+       if (start_async(&async))
+               return 0;       /* error was already reported */
+
+       strbuf_init(&nbuf, 0);
+       if (strbuf_read(&nbuf, async.out, len) < 0) {
+               error("read from external filter %s failed", cmd);
+               ret = 0;
+       }
+       if (close(async.out)) {
+               error("read from external filter %s failed", cmd);
+               ret = 0;
+       }
+       if (finish_async(&async)) {
+               error("external filter %s failed", cmd);
+               ret = 0;
+       }
+
+       if (ret) {
+               strbuf_swap(dst, &nbuf);
+       }
+       strbuf_release(&nbuf);
+       return ret;
+}
+
+static struct convert_driver {
+       const char *name;
+       struct convert_driver *next;
+       char *smudge;
+       char *clean;
+} *user_convert, **user_convert_tail;
+
+static int read_convert_config(const char *var, const char *value)
+{
+       const char *ep, *name;
+       int namelen;
+       struct convert_driver *drv;
+
+       /*
+        * External conversion drivers are configured using
+        * "filter.<name>.variable".
+        */
+       if (prefixcmp(var, "filter.") || (ep = strrchr(var, '.')) == var + 6)
+               return 0;
+       name = var + 7;
+       namelen = ep - name;
+       for (drv = user_convert; drv; drv = drv->next)
+               if (!strncmp(drv->name, name, namelen) && !drv->name[namelen])
+                       break;
+       if (!drv) {
+               drv = xcalloc(1, sizeof(struct convert_driver));
+               drv->name = xmemdupz(name, namelen);
+               *user_convert_tail = drv;
+               user_convert_tail = &(drv->next);
+       }
+
+       ep++;
+
+       /*
+        * filter.<name>.smudge and filter.<name>.clean specifies
+        * the command line:
+        *
+        *      command-line
+        *
+        * The command-line will not be interpolated in any way.
+        */
+
+       if (!strcmp("smudge", ep)) {
+               if (!value)
+                       return error("%s: lacks value", var);
+               drv->smudge = strdup(value);
+               return 0;
+       }
+
+       if (!strcmp("clean", ep)) {
+               if (!value)
+                       return error("%s: lacks value", var);
+               drv->clean = strdup(value);
+               return 0;
+       }
+       return 0;
+}
+
+static void setup_convert_check(struct git_attr_check *check)
+{
+       static struct git_attr *attr_crlf;
+       static struct git_attr *attr_ident;
+       static struct git_attr *attr_filter;
+
+       if (!attr_crlf) {
+               attr_crlf = git_attr("crlf", 4);
+               attr_ident = git_attr("ident", 5);
+               attr_filter = git_attr("filter", 6);
+               user_convert_tail = &user_convert;
+               git_config(read_convert_config);
+       }
+       check[0].attr = attr_crlf;
+       check[1].attr = attr_ident;
+       check[2].attr = attr_filter;
+}
+
+static int count_ident(const char *cp, unsigned long size)
+{
+       /*
+        * "$Id: 0000000000000000000000000000000000000000 $" <=> "$Id$"
+        */
+       int cnt = 0;
+       char ch;
+
+       while (size) {
+               ch = *cp++;
+               size--;
+               if (ch != '$')
+                       continue;
+               if (size < 3)
+                       break;
+               if (memcmp("Id", cp, 2))
+                       continue;
+               ch = cp[2];
+               cp += 3;
+               size -= 3;
+               if (ch == '$')
+                       cnt++; /* $Id$ */
+               if (ch != ':')
+                       continue;
+
+               /*
+                * "$Id: ... "; scan up to the closing dollar sign and discard.
+                */
+               while (size) {
+                       ch = *cp++;
+                       size--;
+                       if (ch == '$') {
+                               cnt++;
+                               break;
+                       }
+               }
+       }
+       return cnt;
+}
+
+static int ident_to_git(const char *path, const char *src, size_t len,
+                        struct strbuf *buf, int ident)
+{
+       char *dst, *dollar;
+
+       if (!ident || !count_ident(src, len))
+               return 0;
+
+       /* only grow if not in place */
+       if (strbuf_avail(buf) + buf->len < len)
+               strbuf_grow(buf, len - buf->len);
+       dst = buf->buf;
+       for (;;) {
+               dollar = memchr(src, '$', len);
+               if (!dollar)
+                       break;
+               memcpy(dst, src, dollar + 1 - src);
+               dst += dollar + 1 - src;
+               len -= dollar + 1 - src;
+               src  = dollar + 1;
+
+               if (len > 3 && !memcmp(src, "Id:", 3)) {
+                       dollar = memchr(src + 3, '$', len - 3);
+                       if (!dollar)
+                               break;
+                       memcpy(dst, "Id$", 3);
+                       dst += 3;
+                       len -= dollar + 1 - src;
+                       src  = dollar + 1;
+               }
+       }
+       memcpy(dst, src, len);
+       strbuf_setlen(buf, dst + len - buf->buf);
+       return 1;
+}
+
+static int ident_to_worktree(const char *path, const char *src, size_t len,
+                             struct strbuf *buf, int ident)
+{
+       unsigned char sha1[20];
+       char *to_free = NULL, *dollar;
+       int cnt;
+
+       if (!ident)
+               return 0;
+
+       cnt = count_ident(src, len);
+       if (!cnt)
+               return 0;
+
+       /* are we "faking" in place editing ? */
+       if (src == buf->buf)
+               to_free = strbuf_detach(buf, NULL);
+       hash_sha1_file(src, len, "blob", sha1);
+
+       strbuf_grow(buf, len + cnt * 43);
+       for (;;) {
+               /* step 1: run to the next '$' */
+               dollar = memchr(src, '$', len);
+               if (!dollar)
+                       break;
+               strbuf_add(buf, src, dollar + 1 - src);
+               len -= dollar + 1 - src;
+               src  = dollar + 1;
+
+               /* step 2: does it looks like a bit like Id:xxx$ or Id$ ? */
+               if (len < 3 || memcmp("Id", src, 2))
+                       continue;
+
+               /* step 3: skip over Id$ or Id:xxxxx$ */
+               if (src[2] == '$') {
+                       src += 3;
+                       len -= 3;
+               } else if (src[2] == ':') {
+                       /*
+                        * It's possible that an expanded Id has crept its way into the
+                        * repository, we cope with that by stripping the expansion out
+                        */
+                       dollar = memchr(src + 3, '$', len - 3);
+                       if (!dollar) {
+                               /* incomplete keyword, no more '$', so just quit the loop */
+                               break;
+                       }
+
+                       len -= dollar + 1 - src;
+                       src  = dollar + 1;
+               } else {
+                       /* it wasn't a "Id$" or "Id:xxxx$" */
+                       continue;
+               }
+
+               /* step 4: substitute */
+               strbuf_addstr(buf, "Id: ");
+               strbuf_add(buf, sha1_to_hex(sha1), 40);
+               strbuf_addstr(buf, " $");
+       }
+       strbuf_add(buf, src, len);
+
+       free(to_free);
+       return 1;
+}
+
+static int git_path_check_crlf(const char *path, struct git_attr_check *check)
+{
+       const char *value = check->value;
+
+       if (ATTR_TRUE(value))
+               return CRLF_TEXT;
+       else if (ATTR_FALSE(value))
+               return CRLF_BINARY;
+       else if (ATTR_UNSET(value))
+               ;
+       else if (!strcmp(value, "input"))
+               return CRLF_INPUT;
+       return CRLF_GUESS;
+}
+
+static struct convert_driver *git_path_check_convert(const char *path,
+                                            struct git_attr_check *check)
+{
+       const char *value = check->value;
+       struct convert_driver *drv;
+
+       if (ATTR_TRUE(value) || ATTR_FALSE(value) || ATTR_UNSET(value))
+               return NULL;
+       for (drv = user_convert; drv; drv = drv->next)
+               if (!strcmp(value, drv->name))
+                       return drv;
+       return NULL;
+}
+
+static int git_path_check_ident(const char *path, struct git_attr_check *check)
+{
+       const char *value = check->value;
+
+       return !!ATTR_TRUE(value);
+}
+
+int convert_to_git(const char *path, const char *src, size_t len, struct strbuf *dst)
+{
+       struct git_attr_check check[3];
+       int crlf = CRLF_GUESS;
+       int ident = 0, ret = 0;
+       char *filter = NULL;
+
+       setup_convert_check(check);
+       if (!git_checkattr(path, ARRAY_SIZE(check), check)) {
+               struct convert_driver *drv;
+               crlf = git_path_check_crlf(path, check + 0);
+               ident = git_path_check_ident(path, check + 1);
+               drv = git_path_check_convert(path, check + 2);
+               if (drv && drv->clean)
+                       filter = drv->clean;
+       }
+
+       ret |= apply_filter(path, src, len, dst, filter);
+       if (ret) {
+               src = dst->buf;
+               len = dst->len;
+       }
+       ret |= crlf_to_git(path, src, len, dst, crlf);
+       if (ret) {
+               src = dst->buf;
+               len = dst->len;
+       }
+       return ret | ident_to_git(path, src, len, dst, ident);
+}
+
+int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst)
+{
+       struct git_attr_check check[3];
+       int crlf = CRLF_GUESS;
+       int ident = 0, ret = 0;
+       char *filter = NULL;
+
+       setup_convert_check(check);
+       if (!git_checkattr(path, ARRAY_SIZE(check), check)) {
+               struct convert_driver *drv;
+               crlf = git_path_check_crlf(path, check + 0);
+               ident = git_path_check_ident(path, check + 1);
+               drv = git_path_check_convert(path, check + 2);
+               if (drv && drv->smudge)
+                       filter = drv->smudge;
+       }
+
+       ret |= ident_to_worktree(path, src, len, dst, ident);
+       if (ret) {
+               src = dst->buf;
+               len = dst->len;
+       }
+       ret |= crlf_to_worktree(path, src, len, dst, crlf);
+       if (ret) {
+               src = dst->buf;
+               len = dst->len;
+       }
+       return ret | apply_filter(path, src, len, dst, filter);
+}
diff --git a/copy.c b/copy.c
new file mode 100644 (file)
index 0000000..c225d1b
--- /dev/null
+++ b/copy.c
@@ -0,0 +1,36 @@
+#include "cache.h"
+
+int copy_fd(int ifd, int ofd)
+{
+       while (1) {
+               char buffer[8192];
+               char *buf = buffer;
+               ssize_t len = xread(ifd, buffer, sizeof(buffer));
+               if (!len)
+                       break;
+               if (len < 0) {
+                       int read_error;
+                       read_error = errno;
+                       close(ifd);
+                       return error("copy-fd: read returned %s",
+                                    strerror(read_error));
+               }
+               while (len) {
+                       int written = xwrite(ofd, buf, len);
+                       if (written > 0) {
+                               buf += written;
+                               len -= written;
+                       }
+                       else if (!written) {
+                               close(ifd);
+                               return error("copy-fd: write returned 0");
+                       } else {
+                               close(ifd);
+                               return error("copy-fd: write returned %s",
+                                            strerror(errno));
+                       }
+               }
+       }
+       close(ifd);
+       return 0;
+}
diff --git a/csum-file.c b/csum-file.c
new file mode 100644 (file)
index 0000000..9728a99
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * csum-file.c
+ *
+ * Copyright (C) 2005 Linus Torvalds
+ *
+ * Simple file write infrastructure for writing SHA1-summed
+ * files. Useful when you write a file that you want to be
+ * able to verify hasn't been messed with afterwards.
+ */
+#include "cache.h"
+#include "progress.h"
+#include "csum-file.h"
+
+static void sha1flush(struct sha1file *f, unsigned int count)
+{
+       void *buf = f->buffer;
+
+       for (;;) {
+               int ret = xwrite(f->fd, buf, count);
+               if (ret > 0) {
+                       f->total += ret;
+                       display_throughput(f->tp, f->total);
+                       buf = (char *) buf + ret;
+                       count -= ret;
+                       if (count)
+                               continue;
+                       return;
+               }
+               if (!ret)
+                       die("sha1 file '%s' write error. Out of diskspace", f->name);
+               die("sha1 file '%s' write error (%s)", f->name, strerror(errno));
+       }
+}
+
+int sha1close(struct sha1file *f, unsigned char *result, int final)
+{
+       int fd;
+       unsigned offset = f->offset;
+       if (offset) {
+               SHA1_Update(&f->ctx, f->buffer, offset);
+               sha1flush(f, offset);
+               f->offset = 0;
+       }
+       if (final) {
+               /* write checksum and close fd */
+               SHA1_Final(f->buffer, &f->ctx);
+               if (result)
+                       hashcpy(result, f->buffer);
+               sha1flush(f, 20);
+               if (close(f->fd))
+                       die("%s: sha1 file error on close (%s)",
+                           f->name, strerror(errno));
+               fd = 0;
+       } else
+               fd = f->fd;
+       free(f);
+       return fd;
+}
+
+int sha1write(struct sha1file *f, void *buf, unsigned int count)
+{
+       if (f->do_crc)
+               f->crc32 = crc32(f->crc32, buf, count);
+       while (count) {
+               unsigned offset = f->offset;
+               unsigned left = sizeof(f->buffer) - offset;
+               unsigned nr = count > left ? left : count;
+
+               memcpy(f->buffer + offset, buf, nr);
+               count -= nr;
+               offset += nr;
+               buf = (char *) buf + nr;
+               left -= nr;
+               if (!left) {
+                       SHA1_Update(&f->ctx, f->buffer, offset);
+                       sha1flush(f, offset);
+                       offset = 0;
+               }
+               f->offset = offset;
+       }
+       return 0;
+}
+
+struct sha1file *sha1fd(int fd, const char *name)
+{
+       return sha1fd_throughput(fd, name, NULL);
+}
+
+struct sha1file *sha1fd_throughput(int fd, const char *name, struct progress *tp)
+{
+       struct sha1file *f = xmalloc(sizeof(*f));
+       f->fd = fd;
+       f->offset = 0;
+       f->total = 0;
+       f->tp = tp;
+       f->name = name;
+       f->do_crc = 0;
+       SHA1_Init(&f->ctx);
+       return f;
+}
+
+void crc32_begin(struct sha1file *f)
+{
+       f->crc32 = crc32(0, Z_NULL, 0);
+       f->do_crc = 1;
+}
+
+uint32_t crc32_end(struct sha1file *f)
+{
+       f->do_crc = 0;
+       return f->crc32;
+}
diff --git a/csum-file.h b/csum-file.h
new file mode 100644 (file)
index 0000000..1af7656
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef CSUM_FILE_H
+#define CSUM_FILE_H
+
+struct progress;
+
+/* A SHA1-protected file */
+struct sha1file {
+       int fd;
+       unsigned int offset;
+       SHA_CTX ctx;
+       off_t total;
+       struct progress *tp;
+       const char *name;
+       int do_crc;
+       uint32_t crc32;
+       unsigned char buffer[8192];
+};
+
+extern struct sha1file *sha1fd(int fd, const char *name);
+extern struct sha1file *sha1fd_throughput(int fd, const char *name, struct progress *tp);
+extern int sha1close(struct sha1file *, unsigned char *, int);
+extern int sha1write(struct sha1file *, void *, unsigned int);
+extern void crc32_begin(struct sha1file *);
+extern uint32_t crc32_end(struct sha1file *);
+
+#endif
diff --git a/ctype.c b/ctype.c
new file mode 100644 (file)
index 0000000..ee06eb7
--- /dev/null
+++ b/ctype.c
@@ -0,0 +1,22 @@
+/*
+ * Sane locale-independent, ASCII ctype.
+ *
+ * No surprises, and works with signed and unsigned chars.
+ */
+#include "cache.h"
+
+#define SS GIT_SPACE
+#define AA GIT_ALPHA
+#define DD GIT_DIGIT
+
+unsigned char sane_ctype[256] = {
+        0,  0,  0,  0,  0,  0,  0,  0,  0, SS, SS,  0,  0, SS,  0,  0,         /* 0-15 */
+        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,         /* 16-15 */
+       SS,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,         /* 32-15 */
+       DD, DD, DD, DD, DD, DD, DD, DD, DD, DD,  0,  0,  0,  0,  0,  0,         /* 48-15 */
+        0, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA,         /* 64-15 */
+       AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA,  0,  0,  0,  0,  0,         /* 80-15 */
+        0, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA,         /* 96-15 */
+       AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA,  0,  0,  0,  0,  0,         /* 112-15 */
+       /* Nothing in the 128.. range */
+};
diff --git a/daemon.c b/daemon.c
new file mode 100644 (file)
index 0000000..41a60af
--- /dev/null
+++ b/daemon.c
@@ -0,0 +1,1209 @@
+#include "cache.h"
+#include "pkt-line.h"
+#include "exec_cmd.h"
+#include "interpolate.h"
+
+#include <syslog.h>
+
+#ifndef HOST_NAME_MAX
+#define HOST_NAME_MAX 256
+#endif
+
+#ifndef NI_MAXSERV
+#define NI_MAXSERV 32
+#endif
+
+static int log_syslog;
+static int verbose;
+static int reuseaddr;
+
+static const char daemon_usage[] =
+"git-daemon [--verbose] [--syslog] [--export-all]\n"
+"           [--timeout=n] [--init-timeout=n] [--strict-paths]\n"
+"           [--base-path=path] [--base-path-relaxed]\n"
+"           [--user-path | --user-path=path]\n"
+"           [--interpolated-path=path]\n"
+"           [--reuseaddr] [--detach] [--pid-file=file]\n"
+"           [--[enable|disable|allow-override|forbid-override]=service]\n"
+"           [--inetd | [--listen=host_or_ipaddr] [--port=n]\n"
+"                      [--user=user [--group=group]]\n"
+"           [directory...]";
+
+/* List of acceptable pathname prefixes */
+static char **ok_paths;
+static int strict_paths;
+
+/* If this is set, git-daemon-export-ok is not required */
+static int export_all_trees;
+
+/* Take all paths relative to this one if non-NULL */
+static char *base_path;
+static char *interpolated_path;
+static int base_path_relaxed;
+
+/* Flag indicating client sent extra args. */
+static int saw_extended_args;
+
+/* If defined, ~user notation is allowed and the string is inserted
+ * after ~user/.  E.g. a request to git://host/~alice/frotz would
+ * go to /home/alice/pub_git/frotz with --user-path=pub_git.
+ */
+static const char *user_path;
+
+/* Timeout, and initial timeout */
+static unsigned int timeout;
+static unsigned int init_timeout;
+
+/*
+ * Static table for now.  Ugh.
+ * Feel free to make dynamic as needed.
+ */
+#define INTERP_SLOT_HOST       (0)
+#define INTERP_SLOT_CANON_HOST (1)
+#define INTERP_SLOT_IP         (2)
+#define INTERP_SLOT_PORT       (3)
+#define INTERP_SLOT_DIR                (4)
+#define INTERP_SLOT_PERCENT    (5)
+
+static struct interp interp_table[] = {
+       { "%H", 0},
+       { "%CH", 0},
+       { "%IP", 0},
+       { "%P", 0},
+       { "%D", 0},
+       { "%%", 0},
+};
+
+
+static void logreport(int priority, const char *err, va_list params)
+{
+       /* We should do a single write so that it is atomic and output
+        * of several processes do not get intermingled. */
+       char buf[1024];
+       int buflen;
+       int maxlen, msglen;
+
+       /* sizeof(buf) should be big enough for "[pid] \n" */
+       buflen = snprintf(buf, sizeof(buf), "[%ld] ", (long) getpid());
+
+       maxlen = sizeof(buf) - buflen - 1; /* -1 for our own LF */
+       msglen = vsnprintf(buf + buflen, maxlen, err, params);
+
+       if (log_syslog) {
+               syslog(priority, "%s", buf);
+               return;
+       }
+
+       /* maxlen counted our own LF but also counts space given to
+        * vsnprintf for the terminating NUL.  We want to make sure that
+        * we have space for our own LF and NUL after the "meat" of the
+        * message, so truncate it at maxlen - 1.
+        */
+       if (msglen > maxlen - 1)
+               msglen = maxlen - 1;
+       else if (msglen < 0)
+               msglen = 0; /* Protect against weird return values. */
+       buflen += msglen;
+
+       buf[buflen++] = '\n';
+       buf[buflen] = '\0';
+
+       write_in_full(2, buf, buflen);
+}
+
+static void logerror(const char *err, ...)
+{
+       va_list params;
+       va_start(params, err);
+       logreport(LOG_ERR, err, params);
+       va_end(params);
+}
+
+static void loginfo(const char *err, ...)
+{
+       va_list params;
+       if (!verbose)
+               return;
+       va_start(params, err);
+       logreport(LOG_INFO, err, params);
+       va_end(params);
+}
+
+static void NORETURN daemon_die(const char *err, va_list params)
+{
+       logreport(LOG_ERR, err, params);
+       exit(1);
+}
+
+static int avoid_alias(char *p)
+{
+       int sl, ndot;
+
+       /*
+        * This resurrects the belts and suspenders paranoia check by HPA
+        * done in <435560F7.4080006@zytor.com> thread, now enter_repo()
+        * does not do getcwd() based path canonicalizations.
+        *
+        * sl becomes true immediately after seeing '/' and continues to
+        * be true as long as dots continue after that without intervening
+        * non-dot character.
+        */
+       if (!p || (*p != '/' && *p != '~'))
+               return -1;
+       sl = 1; ndot = 0;
+       p++;
+
+       while (1) {
+               char ch = *p++;
+               if (sl) {
+                       if (ch == '.')
+                               ndot++;
+                       else if (ch == '/') {
+                               if (ndot < 3)
+                                       /* reject //, /./ and /../ */
+                                       return -1;
+                               ndot = 0;
+                       }
+                       else if (ch == 0) {
+                               if (0 < ndot && ndot < 3)
+                                       /* reject /.$ and /..$ */
+                                       return -1;
+                               return 0;
+                       }
+                       else
+                               sl = ndot = 0;
+               }
+               else if (ch == 0)
+                       return 0;
+               else if (ch == '/') {
+                       sl = 1;
+                       ndot = 0;
+               }
+       }
+}
+
+static char *path_ok(struct interp *itable)
+{
+       static char rpath[PATH_MAX];
+       static char interp_path[PATH_MAX];
+       int retried_path = 0;
+       char *path;
+       char *dir;
+
+       dir = itable[INTERP_SLOT_DIR].value;
+
+       if (avoid_alias(dir)) {
+               logerror("'%s': aliased", dir);
+               return NULL;
+       }
+
+       if (*dir == '~') {
+               if (!user_path) {
+                       logerror("'%s': User-path not allowed", dir);
+                       return NULL;
+               }
+               if (*user_path) {
+                       /* Got either "~alice" or "~alice/foo";
+                        * rewrite them to "~alice/%s" or
+                        * "~alice/%s/foo".
+                        */
+                       int namlen, restlen = strlen(dir);
+                       char *slash = strchr(dir, '/');
+                       if (!slash)
+                               slash = dir + restlen;
+                       namlen = slash - dir;
+                       restlen -= namlen;
+                       loginfo("userpath <%s>, request <%s>, namlen %d, restlen %d, slash <%s>", user_path, dir, namlen, restlen, slash);
+                       snprintf(rpath, PATH_MAX, "%.*s/%s%.*s",
+                                namlen, dir, user_path, restlen, slash);
+                       dir = rpath;
+               }
+       }
+       else if (interpolated_path && saw_extended_args) {
+               if (*dir != '/') {
+                       /* Allow only absolute */
+                       logerror("'%s': Non-absolute path denied (interpolated-path active)", dir);
+                       return NULL;
+               }
+
+               interpolate(interp_path, PATH_MAX, interpolated_path,
+                           interp_table, ARRAY_SIZE(interp_table));
+               loginfo("Interpolated dir '%s'", interp_path);
+
+               dir = interp_path;
+       }
+       else if (base_path) {
+               if (*dir != '/') {
+                       /* Allow only absolute */
+                       logerror("'%s': Non-absolute path denied (base-path active)", dir);
+                       return NULL;
+               }
+               snprintf(rpath, PATH_MAX, "%s%s", base_path, dir);
+               dir = rpath;
+       }
+
+       do {
+               path = enter_repo(dir, strict_paths);
+               if (path)
+                       break;
+
+               /*
+                * if we fail and base_path_relaxed is enabled, try without
+                * prefixing the base path
+                */
+               if (base_path && base_path_relaxed && !retried_path) {
+                       dir = itable[INTERP_SLOT_DIR].value;
+                       retried_path = 1;
+                       continue;
+               }
+               break;
+       } while (1);
+
+       if (!path) {
+               logerror("'%s': unable to chdir or not a git archive", dir);
+               return NULL;
+       }
+
+       if ( ok_paths && *ok_paths ) {
+               char **pp;
+               int pathlen = strlen(path);
+
+               /* The validation is done on the paths after enter_repo
+                * appends optional {.git,.git/.git} and friends, but
+                * it does not use getcwd().  So if your /pub is
+                * a symlink to /mnt/pub, you can whitelist /pub and
+                * do not have to say /mnt/pub.
+                * Do not say /pub/.
+                */
+               for ( pp = ok_paths ; *pp ; pp++ ) {
+                       int len = strlen(*pp);
+                       if (len <= pathlen &&
+                           !memcmp(*pp, path, len) &&
+                           (path[len] == '\0' ||
+                            (!strict_paths && path[len] == '/')))
+                               return path;
+               }
+       }
+       else {
+               /* be backwards compatible */
+               if (!strict_paths)
+                       return path;
+       }
+
+       logerror("'%s': not in whitelist", path);
+       return NULL;            /* Fallthrough. Deny by default */
+}
+
+typedef int (*daemon_service_fn)(void);
+struct daemon_service {
+       const char *name;
+       const char *config_name;
+       daemon_service_fn fn;
+       int enabled;
+       int overridable;
+};
+
+static struct daemon_service *service_looking_at;
+static int service_enabled;
+
+static int git_daemon_config(const char *var, const char *value)
+{
+       if (!prefixcmp(var, "daemon.") &&
+           !strcmp(var + 7, service_looking_at->config_name)) {
+               service_enabled = git_config_bool(var, value);
+               return 0;
+       }
+
+       /* we are not interested in parsing any other configuration here */
+       return 0;
+}
+
+static int run_service(struct interp *itable, struct daemon_service *service)
+{
+       const char *path;
+       int enabled = service->enabled;
+
+       loginfo("Request %s for '%s'",
+               service->name,
+               itable[INTERP_SLOT_DIR].value);
+
+       if (!enabled && !service->overridable) {
+               logerror("'%s': service not enabled.", service->name);
+               errno = EACCES;
+               return -1;
+       }
+
+       if (!(path = path_ok(itable)))
+               return -1;
+
+       /*
+        * Security on the cheap.
+        *
+        * We want a readable HEAD, usable "objects" directory, and
+        * a "git-daemon-export-ok" flag that says that the other side
+        * is ok with us doing this.
+        *
+        * path_ok() uses enter_repo() and does whitelist checking.
+        * We only need to make sure the repository is exported.
+        */
+
+       if (!export_all_trees && access("git-daemon-export-ok", F_OK)) {
+               logerror("'%s': repository not exported.", path);
+               errno = EACCES;
+               return -1;
+       }
+
+       if (service->overridable) {
+               service_looking_at = service;
+               service_enabled = -1;
+               git_config(git_daemon_config);
+               if (0 <= service_enabled)
+                       enabled = service_enabled;
+       }
+       if (!enabled) {
+               logerror("'%s': service not enabled for '%s'",
+                        service->name, path);
+               errno = EACCES;
+               return -1;
+       }
+
+       /*
+        * We'll ignore SIGTERM from now on, we have a
+        * good client.
+        */
+       signal(SIGTERM, SIG_IGN);
+
+       return service->fn();
+}
+
+static int upload_pack(void)
+{
+       /* Timeout as string */
+       char timeout_buf[64];
+
+       snprintf(timeout_buf, sizeof timeout_buf, "--timeout=%u", timeout);
+
+       /* git-upload-pack only ever reads stuff, so this is safe */
+       execl_git_cmd("upload-pack", "--strict", timeout_buf, ".", NULL);
+       return -1;
+}
+
+static int upload_archive(void)
+{
+       execl_git_cmd("upload-archive", ".", NULL);
+       return -1;
+}
+
+static int receive_pack(void)
+{
+       execl_git_cmd("receive-pack", ".", NULL);
+       return -1;
+}
+
+static struct daemon_service daemon_service[] = {
+       { "upload-archive", "uploadarch", upload_archive, 0, 1 },
+       { "upload-pack", "uploadpack", upload_pack, 1, 1 },
+       { "receive-pack", "receivepack", receive_pack, 0, 1 },
+};
+
+static void enable_service(const char *name, int ena)
+{
+       int i;
+       for (i = 0; i < ARRAY_SIZE(daemon_service); i++) {
+               if (!strcmp(daemon_service[i].name, name)) {
+                       daemon_service[i].enabled = ena;
+                       return;
+               }
+       }
+       die("No such service %s", name);
+}
+
+static void make_service_overridable(const char *name, int ena)
+{
+       int i;
+       for (i = 0; i < ARRAY_SIZE(daemon_service); i++) {
+               if (!strcmp(daemon_service[i].name, name)) {
+                       daemon_service[i].overridable = ena;
+                       return;
+               }
+       }
+       die("No such service %s", name);
+}
+
+/*
+ * Separate the "extra args" information as supplied by the client connection.
+ * Any resulting data is squirreled away in the given interpolation table.
+ */
+static void parse_extra_args(struct interp *table, char *extra_args, int buflen)
+{
+       char *val;
+       int vallen;
+       char *end = extra_args + buflen;
+
+       while (extra_args < end && *extra_args) {
+               saw_extended_args = 1;
+               if (strncasecmp("host=", extra_args, 5) == 0) {
+                       val = extra_args + 5;
+                       vallen = strlen(val) + 1;
+                       if (*val) {
+                               /* Split <host>:<port> at colon. */
+                               char *host = val;
+                               char *port = strrchr(host, ':');
+                               if (port) {
+                                       *port = 0;
+                                       port++;
+                                       interp_set_entry(table, INTERP_SLOT_PORT, port);
+                               }
+                               interp_set_entry(table, INTERP_SLOT_HOST, host);
+                       }
+
+                       /* On to the next one */
+                       extra_args = val + vallen;
+               }
+       }
+}
+
+static void fill_in_extra_table_entries(struct interp *itable)
+{
+       char *hp;
+
+       /*
+        * Replace literal host with lowercase-ized hostname.
+        */
+       hp = interp_table[INTERP_SLOT_HOST].value;
+       if (!hp)
+               return;
+       for ( ; *hp; hp++)
+               *hp = tolower(*hp);
+
+       /*
+        * Locate canonical hostname and its IP address.
+        */
+#ifndef NO_IPV6
+       {
+               struct addrinfo hints;
+               struct addrinfo *ai, *ai0;
+               int gai;
+               static char addrbuf[HOST_NAME_MAX + 1];
+
+               memset(&hints, 0, sizeof(hints));
+               hints.ai_flags = AI_CANONNAME;
+
+               gai = getaddrinfo(interp_table[INTERP_SLOT_HOST].value, 0, &hints, &ai0);
+               if (!gai) {
+                       for (ai = ai0; ai; ai = ai->ai_next) {
+                               struct sockaddr_in *sin_addr = (void *)ai->ai_addr;
+
+                               inet_ntop(AF_INET, &sin_addr->sin_addr,
+                                         addrbuf, sizeof(addrbuf));
+                               interp_set_entry(interp_table,
+                                                INTERP_SLOT_CANON_HOST, ai->ai_canonname);
+                               interp_set_entry(interp_table,
+                                                INTERP_SLOT_IP, addrbuf);
+                               break;
+                       }
+                       freeaddrinfo(ai0);
+               }
+       }
+#else
+       {
+               struct hostent *hent;
+               struct sockaddr_in sa;
+               char **ap;
+               static char addrbuf[HOST_NAME_MAX + 1];
+
+               hent = gethostbyname(interp_table[INTERP_SLOT_HOST].value);
+
+               ap = hent->h_addr_list;
+               memset(&sa, 0, sizeof sa);
+               sa.sin_family = hent->h_addrtype;
+               sa.sin_port = htons(0);
+               memcpy(&sa.sin_addr, *ap, hent->h_length);
+
+               inet_ntop(hent->h_addrtype, &sa.sin_addr,
+                         addrbuf, sizeof(addrbuf));
+
+               interp_set_entry(interp_table, INTERP_SLOT_CANON_HOST, hent->h_name);
+               interp_set_entry(interp_table, INTERP_SLOT_IP, addrbuf);
+       }
+#endif
+}
+
+
+static int execute(struct sockaddr *addr)
+{
+       static char line[1000];
+       int pktlen, len, i;
+
+       if (addr) {
+               char addrbuf[256] = "";
+               int port = -1;
+
+               if (addr->sa_family == AF_INET) {
+                       struct sockaddr_in *sin_addr = (void *) addr;
+                       inet_ntop(addr->sa_family, &sin_addr->sin_addr, addrbuf, sizeof(addrbuf));
+                       port = ntohs(sin_addr->sin_port);
+#ifndef NO_IPV6
+               } else if (addr && addr->sa_family == AF_INET6) {
+                       struct sockaddr_in6 *sin6_addr = (void *) addr;
+
+                       char *buf = addrbuf;
+                       *buf++ = '['; *buf = '\0'; /* stpcpy() is cool */
+                       inet_ntop(AF_INET6, &sin6_addr->sin6_addr, buf, sizeof(addrbuf) - 1);
+                       strcat(buf, "]");
+
+                       port = ntohs(sin6_addr->sin6_port);
+#endif
+               }
+               loginfo("Connection from %s:%d", addrbuf, port);
+       }
+
+       alarm(init_timeout ? init_timeout : timeout);
+       pktlen = packet_read_line(0, line, sizeof(line));
+       alarm(0);
+
+       len = strlen(line);
+       if (pktlen != len)
+               loginfo("Extended attributes (%d bytes) exist <%.*s>",
+                       (int) pktlen - len,
+                       (int) pktlen - len, line + len + 1);
+       if (len && line[len-1] == '\n') {
+               line[--len] = 0;
+               pktlen--;
+       }
+
+       /*
+        * Initialize the path interpolation table for this connection.
+        */
+       interp_clear_table(interp_table, ARRAY_SIZE(interp_table));
+       interp_set_entry(interp_table, INTERP_SLOT_PERCENT, "%");
+
+       if (len != pktlen) {
+           parse_extra_args(interp_table, line + len + 1, pktlen - len - 1);
+           fill_in_extra_table_entries(interp_table);
+       }
+
+       for (i = 0; i < ARRAY_SIZE(daemon_service); i++) {
+               struct daemon_service *s = &(daemon_service[i]);
+               int namelen = strlen(s->name);
+               if (!prefixcmp(line, "git-") &&
+                   !strncmp(s->name, line + 4, namelen) &&
+                   line[namelen + 4] == ' ') {
+                       /*
+                        * Note: The directory here is probably context sensitive,
+                        * and might depend on the actual service being performed.
+                        */
+                       interp_set_entry(interp_table,
+                                        INTERP_SLOT_DIR, line + namelen + 5);
+                       return run_service(interp_table, s);
+               }
+       }
+
+       logerror("Protocol error: '%s'", line);
+       return -1;
+}
+
+
+/*
+ * We count spawned/reaped separately, just to avoid any
+ * races when updating them from signals. The SIGCHLD handler
+ * will only update children_reaped, and the fork logic will
+ * only update children_spawned.
+ *
+ * MAX_CHILDREN should be a power-of-two to make the modulus
+ * operation cheap. It should also be at least twice
+ * the maximum number of connections we will ever allow.
+ */
+#define MAX_CHILDREN 128
+
+static int max_connections = 25;
+
+/* These are updated by the signal handler */
+static volatile unsigned int children_reaped;
+static pid_t dead_child[MAX_CHILDREN];
+
+/* These are updated by the main loop */
+static unsigned int children_spawned;
+static unsigned int children_deleted;
+
+static struct child {
+       pid_t pid;
+       int addrlen;
+       struct sockaddr_storage address;
+} live_child[MAX_CHILDREN];
+
+static void add_child(int idx, pid_t pid, struct sockaddr *addr, int addrlen)
+{
+       live_child[idx].pid = pid;
+       live_child[idx].addrlen = addrlen;
+       memcpy(&live_child[idx].address, addr, addrlen);
+}
+
+/*
+ * Walk from "deleted" to "spawned", and remove child "pid".
+ *
+ * We move everything up by one, since the new "deleted" will
+ * be one higher.
+ */
+static void remove_child(pid_t pid, unsigned deleted, unsigned spawned)
+{
+       struct child n;
+
+       deleted %= MAX_CHILDREN;
+       spawned %= MAX_CHILDREN;
+       if (live_child[deleted].pid == pid) {
+               live_child[deleted].pid = -1;
+               return;
+       }
+       n = live_child[deleted];
+       for (;;) {
+               struct child m;
+               deleted = (deleted + 1) % MAX_CHILDREN;
+               if (deleted == spawned)
+                       die("could not find dead child %d\n", pid);
+               m = live_child[deleted];
+               live_child[deleted] = n;
+               if (m.pid == pid)
+                       return;
+               n = m;
+       }
+}
+
+/*
+ * This gets called if the number of connections grows
+ * past "max_connections".
+ *
+ * We _should_ start off by searching for connections
+ * from the same IP, and if there is some address wth
+ * multiple connections, we should kill that first.
+ *
+ * As it is, we just "randomly" kill 25% of the connections,
+ * and our pseudo-random generator sucks too. I have no
+ * shame.
+ *
+ * Really, this is just a place-holder for a _real_ algorithm.
+ */
+static void kill_some_children(int signo, unsigned start, unsigned stop)
+{
+       start %= MAX_CHILDREN;
+       stop %= MAX_CHILDREN;
+       while (start != stop) {
+               if (!(start & 3))
+                       kill(live_child[start].pid, signo);
+               start = (start + 1) % MAX_CHILDREN;
+       }
+}
+
+static void check_max_connections(void)
+{
+       for (;;) {
+               int active;
+               unsigned spawned, reaped, deleted;
+
+               spawned = children_spawned;
+               reaped = children_reaped;
+               deleted = children_deleted;
+
+               while (deleted < reaped) {
+                       pid_t pid = dead_child[deleted % MAX_CHILDREN];
+                       remove_child(pid, deleted, spawned);
+                       deleted++;
+               }
+               children_deleted = deleted;
+
+               active = spawned - deleted;
+               if (active <= max_connections)
+                       break;
+
+               /* Kill some unstarted connections with SIGTERM */
+               kill_some_children(SIGTERM, deleted, spawned);
+               if (active <= max_connections << 1)
+                       break;
+
+               /* If the SIGTERM thing isn't helping use SIGKILL */
+               kill_some_children(SIGKILL, deleted, spawned);
+               sleep(1);
+       }
+}
+
+static void handle(int incoming, struct sockaddr *addr, int addrlen)
+{
+       pid_t pid = fork();
+
+       if (pid) {
+               unsigned idx;
+
+               close(incoming);
+               if (pid < 0)
+                       return;
+
+               idx = children_spawned % MAX_CHILDREN;
+               children_spawned++;
+               add_child(idx, pid, addr, addrlen);
+
+               check_max_connections();
+               return;
+       }
+
+       dup2(incoming, 0);
+       dup2(incoming, 1);
+       close(incoming);
+
+       exit(execute(addr));
+}
+
+static void child_handler(int signo)
+{
+       for (;;) {
+               int status;
+               pid_t pid = waitpid(-1, &status, WNOHANG);
+
+               if (pid > 0) {
+                       unsigned reaped = children_reaped;
+                       dead_child[reaped % MAX_CHILDREN] = pid;
+                       children_reaped = reaped + 1;
+                       /* XXX: Custom logging, since we don't wanna getpid() */
+                       if (verbose) {
+                               const char *dead = "";
+                               if (!WIFEXITED(status) || WEXITSTATUS(status) > 0)
+                                       dead = " (with error)";
+                               if (log_syslog)
+                                       syslog(LOG_INFO, "[%d] Disconnected%s", pid, dead);
+                               else
+                                       fprintf(stderr, "[%d] Disconnected%s\n", pid, dead);
+                       }
+                       continue;
+               }
+               break;
+       }
+}
+
+static int set_reuse_addr(int sockfd)
+{
+       int on = 1;
+
+       if (!reuseaddr)
+               return 0;
+       return setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
+                         &on, sizeof(on));
+}
+
+#ifndef NO_IPV6
+
+static int socksetup(char *listen_addr, int listen_port, int **socklist_p)
+{
+       int socknum = 0, *socklist = NULL;
+       int maxfd = -1;
+       char pbuf[NI_MAXSERV];
+       struct addrinfo hints, *ai0, *ai;
+       int gai;
+       long flags;
+
+       sprintf(pbuf, "%d", listen_port);
+       memset(&hints, 0, sizeof(hints));
+       hints.ai_family = AF_UNSPEC;
+       hints.ai_socktype = SOCK_STREAM;
+       hints.ai_protocol = IPPROTO_TCP;
+       hints.ai_flags = AI_PASSIVE;
+
+       gai = getaddrinfo(listen_addr, pbuf, &hints, &ai0);
+       if (gai)
+               die("getaddrinfo() failed: %s\n", gai_strerror(gai));
+
+       for (ai = ai0; ai; ai = ai->ai_next) {
+               int sockfd;
+
+               sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+               if (sockfd < 0)
+                       continue;
+               if (sockfd >= FD_SETSIZE) {
+                       error("too large socket descriptor.");
+                       close(sockfd);
+                       continue;
+               }
+
+#ifdef IPV6_V6ONLY
+               if (ai->ai_family == AF_INET6) {
+                       int on = 1;
+                       setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY,
+                                  &on, sizeof(on));
+                       /* Note: error is not fatal */
+               }
+#endif
+
+               if (set_reuse_addr(sockfd)) {
+                       close(sockfd);
+                       continue;
+               }
+
+               if (bind(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
+                       close(sockfd);
+                       continue;       /* not fatal */
+               }
+               if (listen(sockfd, 5) < 0) {
+                       close(sockfd);
+                       continue;       /* not fatal */
+               }
+
+               flags = fcntl(sockfd, F_GETFD, 0);
+               if (flags >= 0)
+                       fcntl(sockfd, F_SETFD, flags | FD_CLOEXEC);
+
+               socklist = xrealloc(socklist, sizeof(int) * (socknum + 1));
+               socklist[socknum++] = sockfd;
+
+               if (maxfd < sockfd)
+                       maxfd = sockfd;
+       }
+
+       freeaddrinfo(ai0);
+
+       *socklist_p = socklist;
+       return socknum;
+}
+
+#else /* NO_IPV6 */
+
+static int socksetup(char *listen_addr, int listen_port, int **socklist_p)
+{
+       struct sockaddr_in sin;
+       int sockfd;
+       long flags;
+
+       memset(&sin, 0, sizeof sin);
+       sin.sin_family = AF_INET;
+       sin.sin_port = htons(listen_port);
+
+       if (listen_addr) {
+               /* Well, host better be an IP address here. */
+               if (inet_pton(AF_INET, listen_addr, &sin.sin_addr.s_addr) <= 0)
+                       return 0;
+       } else {
+               sin.sin_addr.s_addr = htonl(INADDR_ANY);
+       }
+
+       sockfd = socket(AF_INET, SOCK_STREAM, 0);
+       if (sockfd < 0)
+               return 0;
+
+       if (set_reuse_addr(sockfd)) {
+               close(sockfd);
+               return 0;
+       }
+
+       if ( bind(sockfd, (struct sockaddr *)&sin, sizeof sin) < 0 ) {
+               close(sockfd);
+               return 0;
+       }
+
+       if (listen(sockfd, 5) < 0) {
+               close(sockfd);
+               return 0;
+       }
+
+       flags = fcntl(sockfd, F_GETFD, 0);
+       if (flags >= 0)
+               fcntl(sockfd, F_SETFD, flags | FD_CLOEXEC);
+
+       *socklist_p = xmalloc(sizeof(int));
+       **socklist_p = sockfd;
+       return 1;
+}
+
+#endif
+
+static int service_loop(int socknum, int *socklist)
+{
+       struct pollfd *pfd;
+       int i;
+
+       pfd = xcalloc(socknum, sizeof(struct pollfd));
+
+       for (i = 0; i < socknum; i++) {
+               pfd[i].fd = socklist[i];
+               pfd[i].events = POLLIN;
+       }
+
+       signal(SIGCHLD, child_handler);
+
+       for (;;) {
+               int i;
+
+               if (poll(pfd, socknum, -1) < 0) {
+                       if (errno != EINTR) {
+                               error("poll failed, resuming: %s",
+                                     strerror(errno));
+                               sleep(1);
+                       }
+                       continue;
+               }
+
+               for (i = 0; i < socknum; i++) {
+                       if (pfd[i].revents & POLLIN) {
+                               struct sockaddr_storage ss;
+                               unsigned int sslen = sizeof(ss);
+                               int incoming = accept(pfd[i].fd, (struct sockaddr *)&ss, &sslen);
+                               if (incoming < 0) {
+                                       switch (errno) {
+                                       case EAGAIN:
+                                       case EINTR:
+                                       case ECONNABORTED:
+                                               continue;
+                                       default:
+                                               die("accept returned %s", strerror(errno));
+                                       }
+                               }
+                               handle(incoming, (struct sockaddr *)&ss, sslen);
+                       }
+               }
+       }
+}
+
+/* if any standard file descriptor is missing open it to /dev/null */
+static void sanitize_stdfds(void)
+{
+       int fd = open("/dev/null", O_RDWR, 0);
+       while (fd != -1 && fd < 2)
+               fd = dup(fd);
+       if (fd == -1)
+               die("open /dev/null or dup failed: %s", strerror(errno));
+       if (fd > 2)
+               close(fd);
+}
+
+static void daemonize(void)
+{
+       switch (fork()) {
+               case 0:
+                       break;
+               case -1:
+                       die("fork failed: %s", strerror(errno));
+               default:
+                       exit(0);
+       }
+       if (setsid() == -1)
+               die("setsid failed: %s", strerror(errno));
+       close(0);
+       close(1);
+       close(2);
+       sanitize_stdfds();
+}
+
+static void store_pid(const char *path)
+{
+       FILE *f = fopen(path, "w");
+       if (!f)
+               die("cannot open pid file %s: %s", path, strerror(errno));
+       if (fprintf(f, "%d\n", getpid()) < 0 || fclose(f) != 0)
+               die("failed to write pid file %s: %s", path, strerror(errno));
+}
+
+static int serve(char *listen_addr, int listen_port, struct passwd *pass, gid_t gid)
+{
+       int socknum, *socklist;
+
+       socknum = socksetup(listen_addr, listen_port, &socklist);
+       if (socknum == 0)
+               die("unable to allocate any listen sockets on host %s port %u",
+                   listen_addr, listen_port);
+
+       if (pass && gid &&
+           (initgroups(pass->pw_name, gid) || setgid (gid) ||
+            setuid(pass->pw_uid)))
+               die("cannot drop privileges");
+
+       return service_loop(socknum, socklist);
+}
+
+int main(int argc, char **argv)
+{
+       int listen_port = 0;
+       char *listen_addr = NULL;
+       int inetd_mode = 0;
+       const char *pid_file = NULL, *user_name = NULL, *group_name = NULL;
+       int detach = 0;
+       struct passwd *pass = NULL;
+       struct group *group;
+       gid_t gid = 0;
+       int i;
+
+       /* Without this we cannot rely on waitpid() to tell
+        * what happened to our children.
+        */
+       signal(SIGCHLD, SIG_DFL);
+
+       for (i = 1; i < argc; i++) {
+               char *arg = argv[i];
+
+               if (!prefixcmp(arg, "--listen=")) {
+                   char *p = arg + 9;
+                   char *ph = listen_addr = xmalloc(strlen(arg + 9) + 1);
+                   while (*p)
+                       *ph++ = tolower(*p++);
+                   *ph = 0;
+                   continue;
+               }
+               if (!prefixcmp(arg, "--port=")) {
+                       char *end;
+                       unsigned long n;
+                       n = strtoul(arg+7, &end, 0);
+                       if (arg[7] && !*end) {
+                               listen_port = n;
+                               continue;
+                       }
+               }
+               if (!strcmp(arg, "--inetd")) {
+                       inetd_mode = 1;
+                       log_syslog = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--verbose")) {
+                       verbose = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--syslog")) {
+                       log_syslog = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--export-all")) {
+                       export_all_trees = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--timeout=")) {
+                       timeout = atoi(arg+10);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--init-timeout=")) {
+                       init_timeout = atoi(arg+15);
+                       continue;
+               }
+               if (!strcmp(arg, "--strict-paths")) {
+                       strict_paths = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--base-path=")) {
+                       base_path = arg+12;
+                       continue;
+               }
+               if (!strcmp(arg, "--base-path-relaxed")) {
+                       base_path_relaxed = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--interpolated-path=")) {
+                       interpolated_path = arg+20;
+                       continue;
+               }
+               if (!strcmp(arg, "--reuseaddr")) {
+                       reuseaddr = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--user-path")) {
+                       user_path = "";
+                       continue;
+               }
+               if (!prefixcmp(arg, "--user-path=")) {
+                       user_path = arg + 12;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--pid-file=")) {
+                       pid_file = arg + 11;
+                       continue;
+               }
+               if (!strcmp(arg, "--detach")) {
+                       detach = 1;
+                       log_syslog = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--user=")) {
+                       user_name = arg + 7;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--group=")) {
+                       group_name = arg + 8;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--enable=")) {
+                       enable_service(arg + 9, 1);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--disable=")) {
+                       enable_service(arg + 10, 0);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--allow-override=")) {
+                       make_service_overridable(arg + 17, 1);
+                       continue;
+               }
+               if (!prefixcmp(arg, "--forbid-override=")) {
+                       make_service_overridable(arg + 18, 0);
+                       continue;
+               }
+               if (!strcmp(arg, "--")) {
+                       ok_paths = &argv[i+1];
+                       break;
+               } else if (arg[0] != '-') {
+                       ok_paths = &argv[i];
+                       break;
+               }
+
+               usage(daemon_usage);
+       }
+
+       if (inetd_mode && (group_name || user_name))
+               die("--user and --group are incompatible with --inetd");
+
+       if (inetd_mode && (listen_port || listen_addr))
+               die("--listen= and --port= are incompatible with --inetd");
+       else if (listen_port == 0)
+               listen_port = DEFAULT_GIT_PORT;
+
+       if (group_name && !user_name)
+               die("--group supplied without --user");
+
+       if (user_name) {
+               pass = getpwnam(user_name);
+               if (!pass)
+                       die("user not found - %s", user_name);
+
+               if (!group_name)
+                       gid = pass->pw_gid;
+               else {
+                       group = getgrnam(group_name);
+                       if (!group)
+                               die("group not found - %s", group_name);
+
+                       gid = group->gr_gid;
+               }
+       }
+
+       if (log_syslog) {
+               openlog("git-daemon", 0, LOG_DAEMON);
+               set_die_routine(daemon_die);
+       }
+
+       if (strict_paths && (!ok_paths || !*ok_paths))
+               die("option --strict-paths requires a whitelist");
+
+       if (inetd_mode) {
+               struct sockaddr_storage ss;
+               struct sockaddr *peer = (struct sockaddr *)&ss;
+               socklen_t slen = sizeof(ss);
+
+               freopen("/dev/null", "w", stderr);
+
+               if (getpeername(0, peer, &slen))
+                       peer = NULL;
+
+               return execute(peer);
+       }
+
+       if (detach)
+               daemonize();
+       else
+               sanitize_stdfds();
+
+       if (pid_file)
+               store_pid(pid_file);
+
+       return serve(listen_addr, listen_port, pass, gid);
+}
diff --git a/date.c b/date.c
new file mode 100644 (file)
index 0000000..8f70500
--- /dev/null
+++ b/date.c
@@ -0,0 +1,859 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+
+#include "cache.h"
+
+static time_t my_mktime(struct tm *tm)
+{
+       static const int mdays[] = {
+           0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
+       };
+       int year = tm->tm_year - 70;
+       int month = tm->tm_mon;
+       int day = tm->tm_mday;
+
+       if (year < 0 || year > 129) /* algo only works for 1970-2099 */
+               return -1;
+       if (month < 0 || month > 11) /* array bounds */
+               return -1;
+       if (month < 2 || (year + 2) % 4)
+               day--;
+       return (year * 365 + (year + 1) / 4 + mdays[month] + day) * 24*60*60UL +
+               tm->tm_hour * 60*60 + tm->tm_min * 60 + tm->tm_sec;
+}
+
+static const char *month_names[] = {
+       "January", "February", "March", "April", "May", "June",
+       "July", "August", "September", "October", "November", "December"
+};
+
+static const char *weekday_names[] = {
+       "Sundays", "Mondays", "Tuesdays", "Wednesdays", "Thursdays", "Fridays", "Saturdays"
+};
+
+static time_t gm_time_t(unsigned long time, int tz)
+{
+       int minutes;
+
+       minutes = tz < 0 ? -tz : tz;
+       minutes = (minutes / 100)*60 + (minutes % 100);
+       minutes = tz < 0 ? -minutes : minutes;
+       return time + minutes * 60;
+}
+
+/*
+ * The "tz" thing is passed in as this strange "decimal parse of tz"
+ * thing, which means that tz -0100 is passed in as the integer -100,
+ * even though it means "sixty minutes off"
+ */
+static struct tm *time_to_tm(unsigned long time, int tz)
+{
+       time_t t = gm_time_t(time, tz);
+       return gmtime(&t);
+}
+
+/*
+ * What value of "tz" was in effect back then at "time" in the
+ * local timezone?
+ */
+static int local_tzoffset(unsigned long time)
+{
+       time_t t, t_local;
+       struct tm tm;
+       int offset, eastwest;
+
+       t = time;
+       localtime_r(&t, &tm);
+       t_local = my_mktime(&tm);
+
+       if (t_local < t) {
+               eastwest = -1;
+               offset = t - t_local;
+       } else {
+               eastwest = 1;
+               offset = t_local - t;
+       }
+       offset /= 60; /* in minutes */
+       offset = (offset % 60) + ((offset / 60) * 100);
+       return offset * eastwest;
+}
+
+const char *show_date(unsigned long time, int tz, enum date_mode mode)
+{
+       struct tm *tm;
+       static char timebuf[200];
+
+       if (mode == DATE_RELATIVE) {
+               unsigned long diff;
+               struct timeval now;
+               gettimeofday(&now, NULL);
+               if (now.tv_sec < time)
+                       return "in the future";
+               diff = now.tv_sec - time;
+               if (diff < 90) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu seconds ago", diff);
+                       return timebuf;
+               }
+               /* Turn it into minutes */
+               diff = (diff + 30) / 60;
+               if (diff < 90) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu minutes ago", diff);
+                       return timebuf;
+               }
+               /* Turn it into hours */
+               diff = (diff + 30) / 60;
+               if (diff < 36) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu hours ago", diff);
+                       return timebuf;
+               }
+               /* We deal with number of days from here on */
+               diff = (diff + 12) / 24;
+               if (diff < 14) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu days ago", diff);
+                       return timebuf;
+               }
+               /* Say weeks for the past 10 weeks or so */
+               if (diff < 70) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu weeks ago", (diff + 3) / 7);
+                       return timebuf;
+               }
+               /* Say months for the past 12 months or so */
+               if (diff < 360) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu months ago", (diff + 15) / 30);
+                       return timebuf;
+               }
+               /* Else fall back on absolute format.. */
+       }
+
+       if (mode == DATE_LOCAL)
+               tz = local_tzoffset(time);
+
+       tm = time_to_tm(time, tz);
+       if (!tm)
+               return NULL;
+       if (mode == DATE_SHORT)
+               sprintf(timebuf, "%04d-%02d-%02d", tm->tm_year + 1900,
+                               tm->tm_mon + 1, tm->tm_mday);
+       else if (mode == DATE_ISO8601)
+               sprintf(timebuf, "%04d-%02d-%02d %02d:%02d:%02d %+05d",
+                               tm->tm_year + 1900,
+                               tm->tm_mon + 1,
+                               tm->tm_mday,
+                               tm->tm_hour, tm->tm_min, tm->tm_sec,
+                               tz);
+       else if (mode == DATE_RFC2822)
+               sprintf(timebuf, "%.3s, %d %.3s %d %02d:%02d:%02d %+05d",
+                       weekday_names[tm->tm_wday], tm->tm_mday,
+                       month_names[tm->tm_mon], tm->tm_year + 1900,
+                       tm->tm_hour, tm->tm_min, tm->tm_sec, tz);
+       else
+               sprintf(timebuf, "%.3s %.3s %d %02d:%02d:%02d %d%c%+05d",
+                               weekday_names[tm->tm_wday],
+                               month_names[tm->tm_mon],
+                               tm->tm_mday,
+                               tm->tm_hour, tm->tm_min, tm->tm_sec,
+                               tm->tm_year + 1900,
+                               (mode == DATE_LOCAL) ? 0 : ' ',
+                               tz);
+       return timebuf;
+}
+
+/*
+ * Check these. And note how it doesn't do the summer-time conversion.
+ *
+ * In my world, it's always summer, and things are probably a bit off
+ * in other ways too.
+ */
+static const struct {
+       const char *name;
+       int offset;
+       int dst;
+} timezone_names[] = {
+       { "IDLW", -12, 0, },    /* International Date Line West */
+       { "NT",   -11, 0, },    /* Nome */
+       { "CAT",  -10, 0, },    /* Central Alaska */
+       { "HST",  -10, 0, },    /* Hawaii Standard */
+       { "HDT",  -10, 1, },    /* Hawaii Daylight */
+       { "YST",   -9, 0, },    /* Yukon Standard */
+       { "YDT",   -9, 1, },    /* Yukon Daylight */
+       { "PST",   -8, 0, },    /* Pacific Standard */
+       { "PDT",   -8, 1, },    /* Pacific Daylight */
+       { "MST",   -7, 0, },    /* Mountain Standard */
+       { "MDT",   -7, 1, },    /* Mountain Daylight */
+       { "CST",   -6, 0, },    /* Central Standard */
+       { "CDT",   -6, 1, },    /* Central Daylight */
+       { "EST",   -5, 0, },    /* Eastern Standard */
+       { "EDT",   -5, 1, },    /* Eastern Daylight */
+       { "AST",   -3, 0, },    /* Atlantic Standard */
+       { "ADT",   -3, 1, },    /* Atlantic Daylight */
+       { "WAT",   -1, 0, },    /* West Africa */
+
+       { "GMT",    0, 0, },    /* Greenwich Mean */
+       { "UTC",    0, 0, },    /* Universal (Coordinated) */
+
+       { "WET",    0, 0, },    /* Western European */
+       { "BST",    0, 1, },    /* British Summer */
+       { "CET",   +1, 0, },    /* Central European */
+       { "MET",   +1, 0, },    /* Middle European */
+       { "MEWT",  +1, 0, },    /* Middle European Winter */
+       { "MEST",  +1, 1, },    /* Middle European Summer */
+       { "CEST",  +1, 1, },    /* Central European Summer */
+       { "MESZ",  +1, 1, },    /* Middle European Summer */
+       { "FWT",   +1, 0, },    /* French Winter */
+       { "FST",   +1, 1, },    /* French Summer */
+       { "EET",   +2, 0, },    /* Eastern Europe, USSR Zone 1 */
+       { "EEST",  +2, 1, },    /* Eastern European Daylight */
+       { "WAST",  +7, 0, },    /* West Australian Standard */
+       { "WADT",  +7, 1, },    /* West Australian Daylight */
+       { "CCT",   +8, 0, },    /* China Coast, USSR Zone 7 */
+       { "JST",   +9, 0, },    /* Japan Standard, USSR Zone 8 */
+       { "EAST", +10, 0, },    /* Eastern Australian Standard */
+       { "EADT", +10, 1, },    /* Eastern Australian Daylight */
+       { "GST",  +10, 0, },    /* Guam Standard, USSR Zone 9 */
+       { "NZT",  +11, 0, },    /* New Zealand */
+       { "NZST", +11, 0, },    /* New Zealand Standard */
+       { "NZDT", +11, 1, },    /* New Zealand Daylight */
+       { "IDLE", +12, 0, },    /* International Date Line East */
+};
+
+static int match_string(const char *date, const char *str)
+{
+       int i = 0;
+
+       for (i = 0; *date; date++, str++, i++) {
+               if (*date == *str)
+                       continue;
+               if (toupper(*date) == toupper(*str))
+                       continue;
+               if (!isalnum(*date))
+                       break;
+               return 0;
+       }
+       return i;
+}
+
+static int skip_alpha(const char *date)
+{
+       int i = 0;
+       do {
+               i++;
+       } while (isalpha(date[i]));
+       return i;
+}
+
+/*
+* Parse month, weekday, or timezone name
+*/
+static int match_alpha(const char *date, struct tm *tm, int *offset)
+{
+       int i;
+
+       for (i = 0; i < 12; i++) {
+               int match = match_string(date, month_names[i]);
+               if (match >= 3) {
+                       tm->tm_mon = i;
+                       return match;
+               }
+       }
+
+       for (i = 0; i < 7; i++) {
+               int match = match_string(date, weekday_names[i]);
+               if (match >= 3) {
+                       tm->tm_wday = i;
+                       return match;
+               }
+       }
+
+       for (i = 0; i < ARRAY_SIZE(timezone_names); i++) {
+               int match = match_string(date, timezone_names[i].name);
+               if (match >= 3) {
+                       int off = timezone_names[i].offset;
+
+                       /* This is bogus, but we like summer */
+                       off += timezone_names[i].dst;
+
+                       /* Only use the tz name offset if we don't have anything better */
+                       if (*offset == -1)
+                               *offset = 60*off;
+
+                       return match;
+               }
+       }
+
+       if (match_string(date, "PM") == 2) {
+               tm->tm_hour = (tm->tm_hour % 12) + 12;
+               return 2;
+       }
+
+       if (match_string(date, "AM") == 2) {
+               tm->tm_hour = (tm->tm_hour % 12) + 0;
+               return 2;
+       }
+
+       /* BAD CRAP */
+       return skip_alpha(date);
+}
+
+static int is_date(int year, int month, int day, struct tm *now_tm, time_t now, struct tm *tm)
+{
+       if (month > 0 && month < 13 && day > 0 && day < 32) {
+               struct tm check = *tm;
+               struct tm *r = (now_tm ? &check : tm);
+               time_t specified;
+
+               r->tm_mon = month - 1;
+               r->tm_mday = day;
+               if (year == -1) {
+                       if (!now_tm)
+                               return 1;
+                       r->tm_year = now_tm->tm_year;
+               }
+               else if (year >= 1970 && year < 2100)
+                       r->tm_year = year - 1900;
+               else if (year > 70 && year < 100)
+                       r->tm_year = year;
+               else if (year < 38)
+                       r->tm_year = year + 100;
+               else
+                       return 0;
+               if (!now_tm)
+                       return 1;
+
+               specified = my_mktime(r);
+
+               /* Be it commit time or author time, it does not make
+                * sense to specify timestamp way into the future.  Make
+                * sure it is not later than ten days from now...
+                */
+               if (now + 10*24*3600 < specified)
+                       return 0;
+               tm->tm_mon = r->tm_mon;
+               tm->tm_mday = r->tm_mday;
+               if (year != -1)
+                       tm->tm_year = r->tm_year;
+               return 1;
+       }
+       return 0;
+}
+
+static int match_multi_number(unsigned long num, char c, const char *date, char *end, struct tm *tm)
+{
+       time_t now;
+       struct tm now_tm;
+       struct tm *refuse_future;
+       long num2, num3;
+
+       num2 = strtol(end+1, &end, 10);
+       num3 = -1;
+       if (*end == c && isdigit(end[1]))
+               num3 = strtol(end+1, &end, 10);
+
+       /* Time? Date? */
+       switch (c) {
+       case ':':
+               if (num3 < 0)
+                       num3 = 0;
+               if (num < 25 && num2 >= 0 && num2 < 60 && num3 >= 0 && num3 <= 60) {
+                       tm->tm_hour = num;
+                       tm->tm_min = num2;
+                       tm->tm_sec = num3;
+                       break;
+               }
+               return 0;
+
+       case '-':
+       case '/':
+       case '.':
+               now = time(NULL);
+               refuse_future = NULL;
+               if (gmtime_r(&now, &now_tm))
+                       refuse_future = &now_tm;
+
+               if (num > 70) {
+                       /* yyyy-mm-dd? */
+                       if (is_date(num, num2, num3, refuse_future, now, tm))
+                               break;
+                       /* yyyy-dd-mm? */
+                       if (is_date(num, num3, num2, refuse_future, now, tm))
+                               break;
+               }
+               /* Our eastern European friends say dd.mm.yy[yy]
+                * is the norm there, so giving precedence to
+                * mm/dd/yy[yy] form only when separator is not '.'
+                */
+               if (c != '.' &&
+                   is_date(num3, num, num2, refuse_future, now, tm))
+                       break;
+               /* European dd.mm.yy[yy] or funny US dd/mm/yy[yy] */
+               if (is_date(num3, num2, num, refuse_future, now, tm))
+                       break;
+               /* Funny European mm.dd.yy */
+               if (c == '.' &&
+                   is_date(num3, num, num2, refuse_future, now, tm))
+                       break;
+               return 0;
+       }
+       return end - date;
+}
+
+/*
+ * We've seen a digit. Time? Year? Date?
+ */
+static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt)
+{
+       int n;
+       char *end;
+       unsigned long num;
+
+       num = strtoul(date, &end, 10);
+
+       /*
+        * Seconds since 1970? We trigger on that for any numbers with
+        * more than 8 digits. This is because we don't want to rule out
+        * numbers like 20070606 as a YYYYMMDD date.
+        */
+       if (num >= 100000000) {
+               time_t time = num;
+               if (gmtime_r(&time, tm)) {
+                       *tm_gmt = 1;
+                       return end - date;
+               }
+       }
+
+       /*
+        * Check for special formats: num[-.:/]num[same]num
+        */
+       switch (*end) {
+       case ':':
+       case '.':
+       case '/':
+       case '-':
+               if (isdigit(end[1])) {
+                       int match = match_multi_number(num, *end, date, end, tm);
+                       if (match)
+                               return match;
+               }
+       }
+
+       /*
+        * None of the special formats? Try to guess what
+        * the number meant. We use the number of digits
+        * to make a more educated guess..
+        */
+       n = 0;
+       do {
+               n++;
+       } while (isdigit(date[n]));
+
+       /* Four-digit year or a timezone? */
+       if (n == 4) {
+               if (num <= 1400 && *offset == -1) {
+                       unsigned int minutes = num % 100;
+                       unsigned int hours = num / 100;
+                       *offset = hours*60 + minutes;
+               } else if (num > 1900 && num < 2100)
+                       tm->tm_year = num - 1900;
+               return n;
+       }
+
+       /*
+        * NOTE! We will give precedence to day-of-month over month or
+        * year numbers in the 1-12 range. So 05 is always "mday 5",
+        * unless we already have a mday..
+        *
+        * IOW, 01 Apr 05 parses as "April 1st, 2005".
+        */
+       if (num > 0 && num < 32 && tm->tm_mday < 0) {
+               tm->tm_mday = num;
+               return n;
+       }
+
+       /* Two-digit year? */
+       if (n == 2 && tm->tm_year < 0) {
+               if (num < 10 && tm->tm_mday >= 0) {
+                       tm->tm_year = num + 100;
+                       return n;
+               }
+               if (num >= 70) {
+                       tm->tm_year = num;
+                       return n;
+               }
+       }
+
+       if (num > 0 && num < 32) {
+               tm->tm_mday = num;
+       } else if (num > 1900) {
+               tm->tm_year = num - 1900;
+       } else if (num > 70) {
+               tm->tm_year = num;
+       } else if (num > 0 && num < 13) {
+               tm->tm_mon = num-1;
+       }
+
+       return n;
+}
+
+static int match_tz(const char *date, int *offp)
+{
+       char *end;
+       int offset = strtoul(date+1, &end, 10);
+       int min, hour;
+       int n = end - date - 1;
+
+       min = offset % 100;
+       hour = offset / 100;
+
+       /*
+        * Don't accept any random crap.. At least 3 digits, and
+        * a valid minute. We might want to check that the minutes
+        * are divisible by 30 or something too.
+        */
+       if (min < 60 && n > 2) {
+               offset = hour*60+min;
+               if (*date == '-')
+                       offset = -offset;
+
+               *offp = offset;
+       }
+       return end - date;
+}
+
+static int date_string(unsigned long date, int offset, char *buf, int len)
+{
+       int sign = '+';
+
+       if (offset < 0) {
+               offset = -offset;
+               sign = '-';
+       }
+       return snprintf(buf, len, "%lu %c%02d%02d", date, sign, offset / 60, offset % 60);
+}
+
+/* Gr. strptime is crap for this; it doesn't have a way to require RFC2822
+   (i.e. English) day/month names, and it doesn't work correctly with %z. */
+int parse_date(const char *date, char *result, int maxlen)
+{
+       struct tm tm;
+       int offset, tm_gmt;
+       time_t then;
+
+       memset(&tm, 0, sizeof(tm));
+       tm.tm_year = -1;
+       tm.tm_mon = -1;
+       tm.tm_mday = -1;
+       tm.tm_isdst = -1;
+       offset = -1;
+       tm_gmt = 0;
+
+       for (;;) {
+               int match = 0;
+               unsigned char c = *date;
+
+               /* Stop at end of string or newline */
+               if (!c || c == '\n')
+                       break;
+
+               if (isalpha(c))
+                       match = match_alpha(date, &tm, &offset);
+               else if (isdigit(c))
+                       match = match_digit(date, &tm, &offset, &tm_gmt);
+               else if ((c == '-' || c == '+') && isdigit(date[1]))
+                       match = match_tz(date, &offset);
+
+               if (!match) {
+                       /* BAD CRAP */
+                       match = 1;
+               }
+
+               date += match;
+       }
+
+       /* mktime uses local timezone */
+       then = my_mktime(&tm);
+       if (offset == -1)
+               offset = (then - mktime(&tm)) / 60;
+
+       if (then == -1)
+               return -1;
+
+       if (!tm_gmt)
+               then -= offset * 60;
+       return date_string(then, offset, result, maxlen);
+}
+
+enum date_mode parse_date_format(const char *format)
+{
+       if (!strcmp(format, "relative"))
+               return DATE_RELATIVE;
+       else if (!strcmp(format, "iso8601") ||
+                !strcmp(format, "iso"))
+               return DATE_ISO8601;
+       else if (!strcmp(format, "rfc2822") ||
+                !strcmp(format, "rfc"))
+               return DATE_RFC2822;
+       else if (!strcmp(format, "short"))
+               return DATE_SHORT;
+       else if (!strcmp(format, "local"))
+               return DATE_LOCAL;
+       else if (!strcmp(format, "default"))
+               return DATE_NORMAL;
+       else
+               die("unknown date format %s", format);
+}
+
+void datestamp(char *buf, int bufsize)
+{
+       time_t now;
+       int offset;
+
+       time(&now);
+
+       offset = my_mktime(localtime(&now)) - now;
+       offset /= 60;
+
+       date_string(now, offset, buf, bufsize);
+}
+
+static void update_tm(struct tm *tm, unsigned long sec)
+{
+       time_t n = mktime(tm) - sec;
+       localtime_r(&n, tm);
+}
+
+static void date_yesterday(struct tm *tm, int *num)
+{
+       update_tm(tm, 24*60*60);
+}
+
+static void date_time(struct tm *tm, int hour)
+{
+       if (tm->tm_hour < hour)
+               date_yesterday(tm, NULL);
+       tm->tm_hour = hour;
+       tm->tm_min = 0;
+       tm->tm_sec = 0;
+}
+
+static void date_midnight(struct tm *tm, int *num)
+{
+       date_time(tm, 0);
+}
+
+static void date_noon(struct tm *tm, int *num)
+{
+       date_time(tm, 12);
+}
+
+static void date_tea(struct tm *tm, int *num)
+{
+       date_time(tm, 17);
+}
+
+static void date_pm(struct tm *tm, int *num)
+{
+       int hour, n = *num;
+       *num = 0;
+
+       hour = tm->tm_hour;
+       if (n) {
+               hour = n;
+               tm->tm_min = 0;
+               tm->tm_sec = 0;
+       }
+       tm->tm_hour = (hour % 12) + 12;
+}
+
+static void date_am(struct tm *tm, int *num)
+{
+       int hour, n = *num;
+       *num = 0;
+
+       hour = tm->tm_hour;
+       if (n) {
+               hour = n;
+               tm->tm_min = 0;
+               tm->tm_sec = 0;
+       }
+       tm->tm_hour = (hour % 12);
+}
+
+static void date_never(struct tm *tm, int *num)
+{
+       tm->tm_mon = tm->tm_wday = tm->tm_yday
+               = tm->tm_hour = tm->tm_min = tm->tm_sec = 0;
+       tm->tm_year = 70;
+       tm->tm_mday = 1;
+}
+
+static const struct special {
+       const char *name;
+       void (*fn)(struct tm *, int *);
+} special[] = {
+       { "yesterday", date_yesterday },
+       { "noon", date_noon },
+       { "midnight", date_midnight },
+       { "tea", date_tea },
+       { "PM", date_pm },
+       { "AM", date_am },
+       { "never", date_never },
+       { NULL }
+};
+
+static const char *number_name[] = {
+       "zero", "one", "two", "three", "four",
+       "five", "six", "seven", "eight", "nine", "ten",
+};
+
+static const struct typelen {
+       const char *type;
+       int length;
+} typelen[] = {
+       { "seconds", 1 },
+       { "minutes", 60 },
+       { "hours", 60*60 },
+       { "days", 24*60*60 },
+       { "weeks", 7*24*60*60 },
+       { NULL }
+};
+
+static const char *approxidate_alpha(const char *date, struct tm *tm, int *num)
+{
+       const struct typelen *tl;
+       const struct special *s;
+       const char *end = date;
+       int i;
+
+       while (isalpha(*++end));
+               ;
+
+       for (i = 0; i < 12; i++) {
+               int match = match_string(date, month_names[i]);
+               if (match >= 3) {
+                       tm->tm_mon = i;
+                       return end;
+               }
+       }
+
+       for (s = special; s->name; s++) {
+               int len = strlen(s->name);
+               if (match_string(date, s->name) == len) {
+                       s->fn(tm, num);
+                       return end;
+               }
+       }
+
+       if (!*num) {
+               for (i = 1; i < 11; i++) {
+                       int len = strlen(number_name[i]);
+                       if (match_string(date, number_name[i]) == len) {
+                               *num = i;
+                               return end;
+                       }
+               }
+               if (match_string(date, "last") == 4)
+                       *num = 1;
+               return end;
+       }
+
+       tl = typelen;
+       while (tl->type) {
+               int len = strlen(tl->type);
+               if (match_string(date, tl->type) >= len-1) {
+                       update_tm(tm, tl->length * *num);
+                       *num = 0;
+                       return end;
+               }
+               tl++;
+       }
+
+       for (i = 0; i < 7; i++) {
+               int match = match_string(date, weekday_names[i]);
+               if (match >= 3) {
+                       int diff, n = *num -1;
+                       *num = 0;
+
+                       diff = tm->tm_wday - i;
+                       if (diff <= 0)
+                               n++;
+                       diff += 7*n;
+
+                       update_tm(tm, diff * 24 * 60 * 60);
+                       return end;
+               }
+       }
+
+       if (match_string(date, "months") >= 5) {
+               int n = tm->tm_mon - *num;
+               *num = 0;
+               while (n < 0) {
+                       n += 12;
+                       tm->tm_year--;
+               }
+               tm->tm_mon = n;
+               return end;
+       }
+
+       if (match_string(date, "years") >= 4) {
+               tm->tm_year -= *num;
+               *num = 0;
+               return end;
+       }
+
+       return end;
+}
+
+static const char *approxidate_digit(const char *date, struct tm *tm, int *num)
+{
+       char *end;
+       unsigned long number = strtoul(date, &end, 10);
+
+       switch (*end) {
+       case ':':
+       case '.':
+       case '/':
+       case '-':
+               if (isdigit(end[1])) {
+                       int match = match_multi_number(number, *end, date, end, tm);
+                       if (match)
+                               return date + match;
+               }
+       }
+
+       *num = number;
+       return end;
+}
+
+unsigned long approxidate(const char *date)
+{
+       int number = 0;
+       struct tm tm, now;
+       struct timeval tv;
+       char buffer[50];
+
+       if (parse_date(date, buffer, sizeof(buffer)) > 0)
+               return strtoul(buffer, NULL, 10);
+
+       gettimeofday(&tv, NULL);
+       localtime_r(&tv.tv_sec, &tm);
+       now = tm;
+       for (;;) {
+               unsigned char c = *date;
+               if (!c)
+                       break;
+               date++;
+               if (isdigit(c)) {
+                       date = approxidate_digit(date-1, &tm, &number);
+                       continue;
+               }
+               if (isalpha(c))
+                       date = approxidate_alpha(date-1, &tm, &number);
+       }
+       if (number > 0 && number < 32)
+               tm.tm_mday = number;
+       if (tm.tm_mon > now.tm_mon && tm.tm_year == now.tm_year)
+               tm.tm_year--;
+       return mktime(&tm);
+}
diff --git a/decorate.c b/decorate.c
new file mode 100644 (file)
index 0000000..23f6b00
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * decorate.c - decorate a git object with some arbitrary
+ * data.
+ */
+#include "cache.h"
+#include "object.h"
+#include "decorate.h"
+
+static unsigned int hash_obj(struct object *obj, unsigned int n)
+{
+       unsigned int hash = *(unsigned int *)obj->sha1;
+       return hash % n;
+}
+
+static void *insert_decoration(struct decoration *n, struct object *base, void *decoration)
+{
+       int size = n->size;
+       struct object_decoration *hash = n->hash;
+       int j = hash_obj(base, size);
+
+       while (hash[j].base) {
+               if (hash[j].base == base) {
+                       void *old = hash[j].decoration;
+                       hash[j].decoration = decoration;
+                       return old;
+               }
+               if (++j >= size)
+                       j = 0;
+       }
+       hash[j].base = base;
+       hash[j].decoration = decoration;
+       n->nr++;
+       return NULL;
+}
+
+static void grow_decoration(struct decoration *n)
+{
+       int i;
+       int old_size = n->size;
+       struct object_decoration *old_hash;
+
+       old_size = n->size;
+       old_hash = n->hash;
+
+       n->size = (old_size + 1000) * 3 / 2;
+       n->hash = xcalloc(n->size, sizeof(struct object_decoration));
+       n->nr = 0;
+
+       for (i = 0; i < old_size; i++) {
+               struct object *base = old_hash[i].base;
+               void *decoration = old_hash[i].decoration;
+
+               if (!base)
+                       continue;
+               insert_decoration(n, base, decoration);
+       }
+       free(old_hash);
+}
+
+/* Add a decoration pointer, return any old one */
+void *add_decoration(struct decoration *n, struct object *obj, void *decoration)
+{
+       int nr = n->nr + 1;
+
+       if (nr > n->size * 2 / 3)
+               grow_decoration(n);
+       return insert_decoration(n, obj, decoration);
+}
+
+/* Lookup a decoration pointer */
+void *lookup_decoration(struct decoration *n, struct object *obj)
+{
+       int j;
+
+       /* nothing to lookup */
+       if (!n->size)
+               return NULL;
+       j = hash_obj(obj, n->size);
+       for (;;) {
+               struct object_decoration *ref = n->hash + j;
+               if (ref->base == obj)
+                       return ref->decoration;
+               if (!ref->base)
+                       return NULL;
+               if (++j == n->size)
+                       j = 0;
+       }
+}
diff --git a/decorate.h b/decorate.h
new file mode 100644 (file)
index 0000000..1fa4ad9
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef DECORATE_H
+#define DECORATE_H
+
+struct object_decoration {
+       struct object *base;
+       void *decoration;
+};
+
+struct decoration {
+       const char *name;
+       unsigned int size, nr;
+       struct object_decoration *hash;
+};
+
+extern void *add_decoration(struct decoration *n, struct object *obj, void *decoration);
+extern void *lookup_decoration(struct decoration *n, struct object *obj);
+
+#endif
diff --git a/delta.h b/delta.h
new file mode 100644 (file)
index 0000000..40ccf5a
--- /dev/null
+++ b/delta.h
@@ -0,0 +1,105 @@
+#ifndef DELTA_H
+#define DELTA_H
+
+/* opaque object for delta index */
+struct delta_index;
+
+/*
+ * create_delta_index: compute index data from given buffer
+ *
+ * This returns a pointer to a struct delta_index that should be passed to
+ * subsequent create_delta() calls, or to free_delta_index().  A NULL pointer
+ * is returned on failure.  The given buffer must not be freed nor altered
+ * before free_delta_index() is called.  The returned pointer must be freed
+ * using free_delta_index().
+ */
+extern struct delta_index *
+create_delta_index(const void *buf, unsigned long bufsize);
+
+/*
+ * free_delta_index: free the index created by create_delta_index()
+ *
+ * Given pointer must be what create_delta_index() returned, or NULL.
+ */
+extern void free_delta_index(struct delta_index *index);
+
+/*
+ * sizeof_delta_index: returns memory usage of delta index
+ *
+ * Given pointer must be what create_delta_index() returned, or NULL.
+ */
+extern unsigned long sizeof_delta_index(struct delta_index *index);
+
+/*
+ * create_delta: create a delta from given index for the given buffer
+ *
+ * This function may be called multiple times with different buffers using
+ * the same delta_index pointer.  If max_delta_size is non-zero and the
+ * resulting delta is to be larger than max_delta_size then NULL is returned.
+ * On success, a non-NULL pointer to the buffer with the delta data is
+ * returned and *delta_size is updated with its size.  The returned buffer
+ * must be freed by the caller.
+ */
+extern void *
+create_delta(const struct delta_index *index,
+            const void *buf, unsigned long bufsize,
+            unsigned long *delta_size, unsigned long max_delta_size);
+
+/*
+ * diff_delta: create a delta from source buffer to target buffer
+ *
+ * If max_delta_size is non-zero and the resulting delta is to be larger
+ * than max_delta_size then NULL is returned.  On success, a non-NULL
+ * pointer to the buffer with the delta data is returned and *delta_size is
+ * updated with its size.  The returned buffer must be freed by the caller.
+ */
+static inline void *
+diff_delta(const void *src_buf, unsigned long src_bufsize,
+          const void *trg_buf, unsigned long trg_bufsize,
+          unsigned long *delta_size, unsigned long max_delta_size)
+{
+       struct delta_index *index = create_delta_index(src_buf, src_bufsize);
+       if (index) {
+               void *delta = create_delta(index, trg_buf, trg_bufsize,
+                                          delta_size, max_delta_size);
+               free_delta_index(index);
+               return delta;
+       }
+       return NULL;
+}
+
+/*
+ * patch_delta: recreate target buffer given source buffer and delta data
+ *
+ * On success, a non-NULL pointer to the target buffer is returned and
+ * *trg_bufsize is updated with its size.  On failure a NULL pointer is
+ * returned.  The returned buffer must be freed by the caller.
+ */
+extern void *patch_delta(const void *src_buf, unsigned long src_size,
+                        const void *delta_buf, unsigned long delta_size,
+                        unsigned long *dst_size);
+
+/* the smallest possible delta size is 4 bytes */
+#define DELTA_SIZE_MIN 4
+
+/*
+ * This must be called twice on the delta data buffer, first to get the
+ * expected source buffer size, and again to get the target buffer size.
+ */
+static inline unsigned long get_delta_hdr_size(const unsigned char **datap,
+                                              const unsigned char *top)
+{
+       const unsigned char *data = *datap;
+       unsigned char cmd;
+       unsigned long size = 0;
+       int i = 0;
+       do {
+               cmd = *data++;
+               size |= (cmd & ~0x80) << i;
+               i += 7;
+       } while (cmd & 0x80 && data < top);
+       *datap = data;
+       return size;
+}
+
+#endif
diff --git a/diff-delta.c b/diff-delta.c
new file mode 100644 (file)
index 0000000..9e440a9
--- /dev/null
@@ -0,0 +1,482 @@
+/*
+ * diff-delta.c: generate a delta between two buffers
+ *
+ * This code was greatly inspired by parts of LibXDiff from Davide Libenzi
+ * http://www.xmailserver.org/xdiff-lib.html
+ *
+ * Rewritten for GIT by Nicolas Pitre <nico@cam.org>, (C) 2005-2007
+ *
+ * This code is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#include "git-compat-util.h"
+#include "delta.h"
+
+/* maximum hash entry list for the same hash bucket */
+#define HASH_LIMIT 64
+
+#define RABIN_SHIFT 23
+#define RABIN_WINDOW 16
+
+static const unsigned int T[256] = {
+       0x00000000, 0xab59b4d1, 0x56b369a2, 0xfdeadd73, 0x063f6795, 0xad66d344,
+       0x508c0e37, 0xfbd5bae6, 0x0c7ecf2a, 0xa7277bfb, 0x5acda688, 0xf1941259,
+       0x0a41a8bf, 0xa1181c6e, 0x5cf2c11d, 0xf7ab75cc, 0x18fd9e54, 0xb3a42a85,
+       0x4e4ef7f6, 0xe5174327, 0x1ec2f9c1, 0xb59b4d10, 0x48719063, 0xe32824b2,
+       0x1483517e, 0xbfdae5af, 0x423038dc, 0xe9698c0d, 0x12bc36eb, 0xb9e5823a,
+       0x440f5f49, 0xef56eb98, 0x31fb3ca8, 0x9aa28879, 0x6748550a, 0xcc11e1db,
+       0x37c45b3d, 0x9c9defec, 0x6177329f, 0xca2e864e, 0x3d85f382, 0x96dc4753,
+       0x6b369a20, 0xc06f2ef1, 0x3bba9417, 0x90e320c6, 0x6d09fdb5, 0xc6504964,
+       0x2906a2fc, 0x825f162d, 0x7fb5cb5e, 0xd4ec7f8f, 0x2f39c569, 0x846071b8,
+       0x798aaccb, 0xd2d3181a, 0x25786dd6, 0x8e21d907, 0x73cb0474, 0xd892b0a5,
+       0x23470a43, 0x881ebe92, 0x75f463e1, 0xdeadd730, 0x63f67950, 0xc8afcd81,
+       0x354510f2, 0x9e1ca423, 0x65c91ec5, 0xce90aa14, 0x337a7767, 0x9823c3b6,
+       0x6f88b67a, 0xc4d102ab, 0x393bdfd8, 0x92626b09, 0x69b7d1ef, 0xc2ee653e,
+       0x3f04b84d, 0x945d0c9c, 0x7b0be704, 0xd05253d5, 0x2db88ea6, 0x86e13a77,
+       0x7d348091, 0xd66d3440, 0x2b87e933, 0x80de5de2, 0x7775282e, 0xdc2c9cff,
+       0x21c6418c, 0x8a9ff55d, 0x714a4fbb, 0xda13fb6a, 0x27f92619, 0x8ca092c8,
+       0x520d45f8, 0xf954f129, 0x04be2c5a, 0xafe7988b, 0x5432226d, 0xff6b96bc,
+       0x02814bcf, 0xa9d8ff1e, 0x5e738ad2, 0xf52a3e03, 0x08c0e370, 0xa39957a1,
+       0x584ced47, 0xf3155996, 0x0eff84e5, 0xa5a63034, 0x4af0dbac, 0xe1a96f7d,
+       0x1c43b20e, 0xb71a06df, 0x4ccfbc39, 0xe79608e8, 0x1a7cd59b, 0xb125614a,
+       0x468e1486, 0xedd7a057, 0x103d7d24, 0xbb64c9f5, 0x40b17313, 0xebe8c7c2,
+       0x16021ab1, 0xbd5bae60, 0x6cb54671, 0xc7ecf2a0, 0x3a062fd3, 0x915f9b02,
+       0x6a8a21e4, 0xc1d39535, 0x3c394846, 0x9760fc97, 0x60cb895b, 0xcb923d8a,
+       0x3678e0f9, 0x9d215428, 0x66f4eece, 0xcdad5a1f, 0x3047876c, 0x9b1e33bd,
+       0x7448d825, 0xdf116cf4, 0x22fbb187, 0x89a20556, 0x7277bfb0, 0xd92e0b61,
+       0x24c4d612, 0x8f9d62c3, 0x7836170f, 0xd36fa3de, 0x2e857ead, 0x85dcca7c,
+       0x7e09709a, 0xd550c44b, 0x28ba1938, 0x83e3ade9, 0x5d4e7ad9, 0xf617ce08,
+       0x0bfd137b, 0xa0a4a7aa, 0x5b711d4c, 0xf028a99d, 0x0dc274ee, 0xa69bc03f,
+       0x5130b5f3, 0xfa690122, 0x0783dc51, 0xacda6880, 0x570fd266, 0xfc5666b7,
+       0x01bcbbc4, 0xaae50f15, 0x45b3e48d, 0xeeea505c, 0x13008d2f, 0xb85939fe,
+       0x438c8318, 0xe8d537c9, 0x153feaba, 0xbe665e6b, 0x49cd2ba7, 0xe2949f76,
+       0x1f7e4205, 0xb427f6d4, 0x4ff24c32, 0xe4abf8e3, 0x19412590, 0xb2189141,
+       0x0f433f21, 0xa41a8bf0, 0x59f05683, 0xf2a9e252, 0x097c58b4, 0xa225ec65,
+       0x5fcf3116, 0xf49685c7, 0x033df00b, 0xa86444da, 0x558e99a9, 0xfed72d78,
+       0x0502979e, 0xae5b234f, 0x53b1fe3c, 0xf8e84aed, 0x17bea175, 0xbce715a4,
+       0x410dc8d7, 0xea547c06, 0x1181c6e0, 0xbad87231, 0x4732af42, 0xec6b1b93,
+       0x1bc06e5f, 0xb099da8e, 0x4d7307fd, 0xe62ab32c, 0x1dff09ca, 0xb6a6bd1b,
+       0x4b4c6068, 0xe015d4b9, 0x3eb80389, 0x95e1b758, 0x680b6a2b, 0xc352defa,
+       0x3887641c, 0x93ded0cd, 0x6e340dbe, 0xc56db96f, 0x32c6cca3, 0x999f7872,
+       0x6475a501, 0xcf2c11d0, 0x34f9ab36, 0x9fa01fe7, 0x624ac294, 0xc9137645,
+       0x26459ddd, 0x8d1c290c, 0x70f6f47f, 0xdbaf40ae, 0x207afa48, 0x8b234e99,
+       0x76c993ea, 0xdd90273b, 0x2a3b52f7, 0x8162e626, 0x7c883b55, 0xd7d18f84,
+       0x2c043562, 0x875d81b3, 0x7ab75cc0, 0xd1eee811
+};
+
+static const unsigned int U[256] = {
+       0x00000000, 0x7eb5200d, 0x5633f4cb, 0x2886d4c6, 0x073e5d47, 0x798b7d4a,
+       0x510da98c, 0x2fb88981, 0x0e7cba8e, 0x70c99a83, 0x584f4e45, 0x26fa6e48,
+       0x0942e7c9, 0x77f7c7c4, 0x5f711302, 0x21c4330f, 0x1cf9751c, 0x624c5511,
+       0x4aca81d7, 0x347fa1da, 0x1bc7285b, 0x65720856, 0x4df4dc90, 0x3341fc9d,
+       0x1285cf92, 0x6c30ef9f, 0x44b63b59, 0x3a031b54, 0x15bb92d5, 0x6b0eb2d8,
+       0x4388661e, 0x3d3d4613, 0x39f2ea38, 0x4747ca35, 0x6fc11ef3, 0x11743efe,
+       0x3eccb77f, 0x40799772, 0x68ff43b4, 0x164a63b9, 0x378e50b6, 0x493b70bb,
+       0x61bda47d, 0x1f088470, 0x30b00df1, 0x4e052dfc, 0x6683f93a, 0x1836d937,
+       0x250b9f24, 0x5bbebf29, 0x73386bef, 0x0d8d4be2, 0x2235c263, 0x5c80e26e,
+       0x740636a8, 0x0ab316a5, 0x2b7725aa, 0x55c205a7, 0x7d44d161, 0x03f1f16c,
+       0x2c4978ed, 0x52fc58e0, 0x7a7a8c26, 0x04cfac2b, 0x73e5d470, 0x0d50f47d,
+       0x25d620bb, 0x5b6300b6, 0x74db8937, 0x0a6ea93a, 0x22e87dfc, 0x5c5d5df1,
+       0x7d996efe, 0x032c4ef3, 0x2baa9a35, 0x551fba38, 0x7aa733b9, 0x041213b4,
+       0x2c94c772, 0x5221e77f, 0x6f1ca16c, 0x11a98161, 0x392f55a7, 0x479a75aa,
+       0x6822fc2b, 0x1697dc26, 0x3e1108e0, 0x40a428ed, 0x61601be2, 0x1fd53bef,
+       0x3753ef29, 0x49e6cf24, 0x665e46a5, 0x18eb66a8, 0x306db26e, 0x4ed89263,
+       0x4a173e48, 0x34a21e45, 0x1c24ca83, 0x6291ea8e, 0x4d29630f, 0x339c4302,
+       0x1b1a97c4, 0x65afb7c9, 0x446b84c6, 0x3adea4cb, 0x1258700d, 0x6ced5000,
+       0x4355d981, 0x3de0f98c, 0x15662d4a, 0x6bd30d47, 0x56ee4b54, 0x285b6b59,
+       0x00ddbf9f, 0x7e689f92, 0x51d01613, 0x2f65361e, 0x07e3e2d8, 0x7956c2d5,
+       0x5892f1da, 0x2627d1d7, 0x0ea10511, 0x7014251c, 0x5facac9d, 0x21198c90,
+       0x099f5856, 0x772a785b, 0x4c921c31, 0x32273c3c, 0x1aa1e8fa, 0x6414c8f7,
+       0x4bac4176, 0x3519617b, 0x1d9fb5bd, 0x632a95b0, 0x42eea6bf, 0x3c5b86b2,
+       0x14dd5274, 0x6a687279, 0x45d0fbf8, 0x3b65dbf5, 0x13e30f33, 0x6d562f3e,
+       0x506b692d, 0x2ede4920, 0x06589de6, 0x78edbdeb, 0x5755346a, 0x29e01467,
+       0x0166c0a1, 0x7fd3e0ac, 0x5e17d3a3, 0x20a2f3ae, 0x08242768, 0x76910765,
+       0x59298ee4, 0x279caee9, 0x0f1a7a2f, 0x71af5a22, 0x7560f609, 0x0bd5d604,
+       0x235302c2, 0x5de622cf, 0x725eab4e, 0x0ceb8b43, 0x246d5f85, 0x5ad87f88,
+       0x7b1c4c87, 0x05a96c8a, 0x2d2fb84c, 0x539a9841, 0x7c2211c0, 0x029731cd,
+       0x2a11e50b, 0x54a4c506, 0x69998315, 0x172ca318, 0x3faa77de, 0x411f57d3,
+       0x6ea7de52, 0x1012fe5f, 0x38942a99, 0x46210a94, 0x67e5399b, 0x19501996,
+       0x31d6cd50, 0x4f63ed5d, 0x60db64dc, 0x1e6e44d1, 0x36e89017, 0x485db01a,
+       0x3f77c841, 0x41c2e84c, 0x69443c8a, 0x17f11c87, 0x38499506, 0x46fcb50b,
+       0x6e7a61cd, 0x10cf41c0, 0x310b72cf, 0x4fbe52c2, 0x67388604, 0x198da609,
+       0x36352f88, 0x48800f85, 0x6006db43, 0x1eb3fb4e, 0x238ebd5d, 0x5d3b9d50,
+       0x75bd4996, 0x0b08699b, 0x24b0e01a, 0x5a05c017, 0x728314d1, 0x0c3634dc,
+       0x2df207d3, 0x534727de, 0x7bc1f318, 0x0574d315, 0x2acc5a94, 0x54797a99,
+       0x7cffae5f, 0x024a8e52, 0x06852279, 0x78300274, 0x50b6d6b2, 0x2e03f6bf,
+       0x01bb7f3e, 0x7f0e5f33, 0x57888bf5, 0x293dabf8, 0x08f998f7, 0x764cb8fa,
+       0x5eca6c3c, 0x207f4c31, 0x0fc7c5b0, 0x7172e5bd, 0x59f4317b, 0x27411176,
+       0x1a7c5765, 0x64c97768, 0x4c4fa3ae, 0x32fa83a3, 0x1d420a22, 0x63f72a2f,
+       0x4b71fee9, 0x35c4dee4, 0x1400edeb, 0x6ab5cde6, 0x42331920, 0x3c86392d,
+       0x133eb0ac, 0x6d8b90a1, 0x450d4467, 0x3bb8646a
+};
+
+struct index_entry {
+       const unsigned char *ptr;
+       unsigned int val;
+};
+
+struct unpacked_index_entry {
+       struct index_entry entry;
+       struct unpacked_index_entry *next;
+};
+
+struct delta_index {
+       unsigned long memsize;
+       const void *src_buf;
+       unsigned long src_size;
+       unsigned int hash_mask;
+       struct index_entry *hash[FLEX_ARRAY];
+};
+
+struct delta_index * create_delta_index(const void *buf, unsigned long bufsize)
+{
+       unsigned int i, hsize, hmask, entries, prev_val, *hash_count;
+       const unsigned char *data, *buffer = buf;
+       struct delta_index *index;
+       struct unpacked_index_entry *entry, **hash;
+       struct index_entry *packed_entry, **packed_hash;
+       void *mem;
+       unsigned long memsize;
+
+       if (!buf || !bufsize)
+               return NULL;
+
+       /* Determine index hash size.  Note that indexing skips the
+          first byte to allow for optimizing the Rabin's polynomial
+          initialization in create_delta(). */
+       entries = (bufsize - 1)  / RABIN_WINDOW;
+       hsize = entries / 4;
+       for (i = 4; (1u << i) < hsize && i < 31; i++);
+       hsize = 1 << i;
+       hmask = hsize - 1;
+
+       /* allocate lookup index */
+       memsize = sizeof(*hash) * hsize +
+                 sizeof(*entry) * entries;
+       mem = malloc(memsize);
+       if (!mem)
+               return NULL;
+       hash = mem;
+       mem = hash + hsize;
+       entry = mem;
+
+       memset(hash, 0, hsize * sizeof(*hash));
+
+       /* allocate an array to count hash entries */
+       hash_count = calloc(hsize, sizeof(*hash_count));
+       if (!hash_count) {
+               free(hash);
+               return NULL;
+       }
+
+       /* then populate the index */
+       prev_val = ~0;
+       for (data = buffer + entries * RABIN_WINDOW - RABIN_WINDOW;
+            data >= buffer;
+            data -= RABIN_WINDOW) {
+               unsigned int val = 0;
+               for (i = 1; i <= RABIN_WINDOW; i++)
+                       val = ((val << 8) | data[i]) ^ T[val >> RABIN_SHIFT];
+               if (val == prev_val) {
+                       /* keep the lowest of consecutive identical blocks */
+                       entry[-1].entry.ptr = data + RABIN_WINDOW;
+                       --entries;
+               } else {
+                       prev_val = val;
+                       i = val & hmask;
+                       entry->entry.ptr = data + RABIN_WINDOW;
+                       entry->entry.val = val;
+                       entry->next = hash[i];
+                       hash[i] = entry++;
+                       hash_count[i]++;
+               }
+       }
+
+       /*
+        * Determine a limit on the number of entries in the same hash
+        * bucket.  This guards us against pathological data sets causing
+        * really bad hash distribution with most entries in the same hash
+        * bucket that would bring us to O(m*n) computing costs (m and n
+        * corresponding to reference and target buffer sizes).
+        *
+        * Make sure none of the hash buckets has more entries than
+        * we're willing to test.  Otherwise we cull the entry list
+        * uniformly to still preserve a good repartition across
+        * the reference buffer.
+        */
+       for (i = 0; i < hsize; i++) {
+               int acc;
+
+               if (hash_count[i] <= HASH_LIMIT)
+                       continue;
+
+               entries -= hash_count[i] - HASH_LIMIT;
+               /* We leave exactly HASH_LIMIT entries in the bucket */
+
+               entry = hash[i];
+               acc = 0;
+               do {
+                       acc += hash_count[i] - HASH_LIMIT;
+                       if (acc > 0) {
+                               struct unpacked_index_entry *keep = entry;
+                               do {
+                                       entry = entry->next;
+                                       acc -= HASH_LIMIT;
+                               } while (acc > 0);
+                               keep->next = entry->next;
+                       }
+                       entry = entry->next;
+               } while (entry);
+
+               /* Assume that this loop is gone through exactly
+                * HASH_LIMIT times and is entered and left with
+                * acc==0.  So the first statement in the loop
+                * contributes (hash_count[i]-HASH_LIMIT)*HASH_LIMIT
+                * to the accumulator, and the inner loop consequently
+                * is run (hash_count[i]-HASH_LIMIT) times, removing
+                * one element from the list each time.  Since acc
+                * balances out to 0 at the final run, the inner loop
+                * body can't be left with entry==NULL.  So we indeed
+                * encounter entry==NULL in the outer loop only.
+                */
+       }
+       free(hash_count);
+
+       /* Now create the packed index in array form rather than
+        * linked lists */
+
+       memsize = sizeof(*index)
+               + sizeof(*packed_hash) * (hsize+1)
+               + sizeof(*packed_entry) * entries;
+
+       mem = malloc(memsize);
+
+       if (!mem) {
+               free(hash);
+               return NULL;
+       }
+
+       index = mem;
+       index->memsize = memsize;
+       index->src_buf = buf;
+       index->src_size = bufsize;
+       index->hash_mask = hmask;
+
+       mem = index + 1;
+       packed_hash = mem;
+       mem = packed_hash + (hsize+1);
+       packed_entry = mem;
+
+       /* Coalesce all entries belonging to one linked list into
+        * consecutive array entries */
+
+       for (i = 0; i < hsize; i++) {
+               packed_hash[i] = packed_entry;
+               for (entry = hash[i]; entry; entry = entry->next)
+                       *packed_entry++ = entry->entry;
+       }
+
+       /* Sentinel value to indicate the length of the last hash
+        * bucket */
+
+       packed_hash[hsize] = packed_entry;
+       assert(packed_entry - (struct index_entry *)mem == entries);
+       free(hash);
+
+       return index;
+}
+
+void free_delta_index(struct delta_index *index)
+{
+       free(index);
+}
+
+unsigned long sizeof_delta_index(struct delta_index *index)
+{
+       if (index)
+               return index->memsize;
+       else
+               return 0;
+}
+
+/*
+ * The maximum size for any opcode sequence, including the initial header
+ * plus Rabin window plus biggest copy.
+ */
+#define MAX_OP_SIZE    (5 + 5 + 1 + RABIN_WINDOW + 7)
+
+void *
+create_delta(const struct delta_index *index,
+            const void *trg_buf, unsigned long trg_size,
+            unsigned long *delta_size, unsigned long max_size)
+{
+       unsigned int i, outpos, outsize, moff, msize, val;
+       int inscnt;
+       const unsigned char *ref_data, *ref_top, *data, *top;
+       unsigned char *out;
+
+       if (!trg_buf || !trg_size)
+               return NULL;
+
+       outpos = 0;
+       outsize = 8192;
+       if (max_size && outsize >= max_size)
+               outsize = max_size + MAX_OP_SIZE + 1;
+       out = malloc(outsize);
+       if (!out)
+               return NULL;
+
+       /* store reference buffer size */
+       i = index->src_size;
+       while (i >= 0x80) {
+               out[outpos++] = i | 0x80;
+               i >>= 7;
+       }
+       out[outpos++] = i;
+
+       /* store target buffer size */
+       i = trg_size;
+       while (i >= 0x80) {
+               out[outpos++] = i | 0x80;
+               i >>= 7;
+       }
+       out[outpos++] = i;
+
+       ref_data = index->src_buf;
+       ref_top = ref_data + index->src_size;
+       data = trg_buf;
+       top = (const unsigned char *) trg_buf + trg_size;
+
+       outpos++;
+       val = 0;
+       for (i = 0; i < RABIN_WINDOW && data < top; i++, data++) {
+               out[outpos++] = *data;
+               val = ((val << 8) | *data) ^ T[val >> RABIN_SHIFT];
+       }
+       inscnt = i;
+
+       moff = 0;
+       msize = 0;
+       while (data < top) {
+               if (msize < 4096) {
+                       struct index_entry *entry;
+                       val ^= U[data[-RABIN_WINDOW]];
+                       val = ((val << 8) | *data) ^ T[val >> RABIN_SHIFT];
+                       i = val & index->hash_mask;
+                       for (entry = index->hash[i]; entry < index->hash[i+1]; entry++) {
+                               const unsigned char *ref = entry->ptr;
+                               const unsigned char *src = data;
+                               unsigned int ref_size = ref_top - ref;
+                               if (entry->val != val)
+                                       continue;
+                               if (ref_size > top - src)
+                                       ref_size = top - src;
+                               if (ref_size <= msize)
+                                       break;
+                               while (ref_size-- && *src++ == *ref)
+                                       ref++;
+                               if (msize < ref - entry->ptr) {
+                                       /* this is our best match so far */
+                                       msize = ref - entry->ptr;
+                                       moff = entry->ptr - ref_data;
+                                       if (msize >= 4096) /* good enough */
+                                               break;
+                               }
+                       }
+               }
+
+               if (msize < 4) {
+                       if (!inscnt)
+                               outpos++;
+                       out[outpos++] = *data++;
+                       inscnt++;
+                       if (inscnt == 0x7f) {
+                               out[outpos - inscnt - 1] = inscnt;
+                               inscnt = 0;
+                       }
+                       msize = 0;
+               } else {
+                       unsigned int left;
+                       unsigned char *op;
+
+                       if (inscnt) {
+                               while (moff && ref_data[moff-1] == data[-1]) {
+                                       /* we can match one byte back */
+                                       msize++;
+                                       moff--;
+                                       data--;
+                                       outpos--;
+                                       if (--inscnt)
+                                               continue;
+                                       outpos--;  /* remove count slot */
+                                       inscnt--;  /* make it -1 */
+                                       break;
+                               }
+                               out[outpos - inscnt - 1] = inscnt;
+                               inscnt = 0;
+                       }
+
+                       /* A copy op is currently limited to 64KB (pack v2) */
+                       left = (msize < 0x10000) ? 0 : (msize - 0x10000);
+                       msize -= left;
+
+                       op = out + outpos++;
+                       i = 0x80;
+
+                       if (moff & 0x000000ff)
+                               out[outpos++] = moff >> 0,  i |= 0x01;
+                       if (moff & 0x0000ff00)
+                               out[outpos++] = moff >> 8,  i |= 0x02;
+                       if (moff & 0x00ff0000)
+                               out[outpos++] = moff >> 16, i |= 0x04;
+                       if (moff & 0xff000000)
+                               out[outpos++] = moff >> 24, i |= 0x08;
+
+                       if (msize & 0x00ff)
+                               out[outpos++] = msize >> 0, i |= 0x10;
+                       if (msize & 0xff00)
+                               out[outpos++] = msize >> 8, i |= 0x20;
+
+                       *op = i;
+
+                       data += msize;
+                       moff += msize;
+                       msize = left;
+
+                       if (msize < 4096) {
+                               int j;
+                               val = 0;
+                               for (j = -RABIN_WINDOW; j < 0; j++)
+                                       val = ((val << 8) | data[j])
+                                             ^ T[val >> RABIN_SHIFT];
+                       }
+               }
+
+               if (outpos >= outsize - MAX_OP_SIZE) {
+                       void *tmp = out;
+                       outsize = outsize * 3 / 2;
+                       if (max_size && outsize >= max_size)
+                               outsize = max_size + MAX_OP_SIZE + 1;
+                       if (max_size && outpos > max_size)
+                               break;
+                       out = realloc(out, outsize);
+                       if (!out) {
+                               free(tmp);
+                               return NULL;
+                       }
+               }
+       }
+
+       if (inscnt)
+               out[outpos - inscnt - 1] = inscnt;
+
+       if (max_size && outpos > max_size) {
+               free(out);
+               return NULL;
+       }
+
+       *delta_size = outpos;
+       return out;
+}
diff --git a/diff-lib.c b/diff-lib.c
new file mode 100644 (file)
index 0000000..d85d8f3
--- /dev/null
@@ -0,0 +1,741 @@
+/*
+ * Copyright (C) 2005 Junio C Hamano
+ */
+#include "cache.h"
+#include "quote.h"
+#include "commit.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "revision.h"
+#include "cache-tree.h"
+#include "path-list.h"
+
+/*
+ * diff-files
+ */
+
+static int read_directory(const char *path, struct path_list *list)
+{
+       DIR *dir;
+       struct dirent *e;
+
+       if (!(dir = opendir(path)))
+               return error("Could not open directory %s", path);
+
+       while ((e = readdir(dir)))
+               if (strcmp(".", e->d_name) && strcmp("..", e->d_name))
+                       path_list_insert(e->d_name, list);
+
+       closedir(dir);
+       return 0;
+}
+
+static int get_mode(const char *path, int *mode)
+{
+       struct stat st;
+
+       if (!path || !strcmp(path, "/dev/null"))
+               *mode = 0;
+       else if (!strcmp(path, "-"))
+               *mode = ntohl(create_ce_mode(0666));
+       else if (stat(path, &st))
+               return error("Could not access '%s'", path);
+       else
+               *mode = st.st_mode;
+       return 0;
+}
+
+static int queue_diff(struct diff_options *o,
+               const char *name1, const char *name2)
+{
+       int mode1 = 0, mode2 = 0;
+
+       if (get_mode(name1, &mode1) || get_mode(name2, &mode2))
+               return -1;
+
+       if (mode1 && mode2 && S_ISDIR(mode1) != S_ISDIR(mode2))
+               return error("file/directory conflict: %s, %s", name1, name2);
+
+       if (S_ISDIR(mode1) || S_ISDIR(mode2)) {
+               char buffer1[PATH_MAX], buffer2[PATH_MAX];
+               struct path_list p1 = {NULL, 0, 0, 1}, p2 = {NULL, 0, 0, 1};
+               int len1 = 0, len2 = 0, i1, i2, ret = 0;
+
+               if (name1 && read_directory(name1, &p1))
+                       return -1;
+               if (name2 && read_directory(name2, &p2)) {
+                       path_list_clear(&p1, 0);
+                       return -1;
+               }
+
+               if (name1) {
+                       len1 = strlen(name1);
+                       if (len1 > 0 && name1[len1 - 1] == '/')
+                               len1--;
+                       memcpy(buffer1, name1, len1);
+                       buffer1[len1++] = '/';
+               }
+
+               if (name2) {
+                       len2 = strlen(name2);
+                       if (len2 > 0 && name2[len2 - 1] == '/')
+                               len2--;
+                       memcpy(buffer2, name2, len2);
+                       buffer2[len2++] = '/';
+               }
+
+               for (i1 = i2 = 0; !ret && (i1 < p1.nr || i2 < p2.nr); ) {
+                       const char *n1, *n2;
+                       int comp;
+
+                       if (i1 == p1.nr)
+                               comp = 1;
+                       else if (i2 == p2.nr)
+                               comp = -1;
+                       else
+                               comp = strcmp(p1.items[i1].path,
+                                       p2.items[i2].path);
+
+                       if (comp > 0)
+                               n1 = NULL;
+                       else {
+                               n1 = buffer1;
+                               strncpy(buffer1 + len1, p1.items[i1++].path,
+                                               PATH_MAX - len1);
+                       }
+
+                       if (comp < 0)
+                               n2 = NULL;
+                       else {
+                               n2 = buffer2;
+                               strncpy(buffer2 + len2, p2.items[i2++].path,
+                                               PATH_MAX - len2);
+                       }
+
+                       ret = queue_diff(o, n1, n2);
+               }
+               path_list_clear(&p1, 0);
+               path_list_clear(&p2, 0);
+
+               return ret;
+       } else {
+               struct diff_filespec *d1, *d2;
+
+               if (DIFF_OPT_TST(o, REVERSE_DIFF)) {
+                       unsigned tmp;
+                       const char *tmp_c;
+                       tmp = mode1; mode1 = mode2; mode2 = tmp;
+                       tmp_c = name1; name1 = name2; name2 = tmp_c;
+               }
+
+               if (!name1)
+                       name1 = "/dev/null";
+               if (!name2)
+                       name2 = "/dev/null";
+               d1 = alloc_filespec(name1);
+               d2 = alloc_filespec(name2);
+               fill_filespec(d1, null_sha1, mode1);
+               fill_filespec(d2, null_sha1, mode2);
+
+               diff_queue(&diff_queued_diff, d1, d2);
+               return 0;
+       }
+}
+
+/*
+ * Does the path name a blob in the working tree, or a directory
+ * in the working tree?
+ */
+static int is_in_index(const char *path)
+{
+       int len, pos;
+       struct cache_entry *ce;
+
+       len = strlen(path);
+       while (path[len-1] == '/')
+               len--;
+       if (!len)
+               return 1; /* "." */
+       pos = cache_name_pos(path, len);
+       if (0 <= pos)
+               return 1;
+       pos = -1 - pos;
+       while (pos < active_nr) {
+               ce = active_cache[pos++];
+               if (ce_namelen(ce) <= len ||
+                   strncmp(ce->name, path, len) ||
+                   (ce->name[len] > '/'))
+                       break; /* path cannot be a prefix */
+               if (ce->name[len] == '/')
+                       return 1;
+       }
+       return 0;
+}
+
+static int handle_diff_files_args(struct rev_info *revs,
+                                 int argc, const char **argv,
+                                 unsigned int *options)
+{
+       *options = 0;
+
+       /* revs->max_count == -2 means --no-index */
+       while (1 < argc && argv[1][0] == '-') {
+               if (!strcmp(argv[1], "--base"))
+                       revs->max_count = 1;
+               else if (!strcmp(argv[1], "--ours"))
+                       revs->max_count = 2;
+               else if (!strcmp(argv[1], "--theirs"))
+                       revs->max_count = 3;
+               else if (!strcmp(argv[1], "-n") ||
+                               !strcmp(argv[1], "--no-index")) {
+                       revs->max_count = -2;
+                       DIFF_OPT_SET(&revs->diffopt, EXIT_WITH_STATUS);
+                       DIFF_OPT_SET(&revs->diffopt, NO_INDEX);
+               }
+               else if (!strcmp(argv[1], "-q"))
+                       *options |= DIFF_SILENT_ON_REMOVED;
+               else
+                       return error("invalid option: %s", argv[1]);
+               argv++; argc--;
+       }
+
+       if (revs->max_count == -1 && revs->diffopt.nr_paths == 2) {
+               /*
+                * If two files are specified, and at least one is untracked,
+                * default to no-index.
+                */
+               read_cache();
+               if (!is_in_index(revs->diffopt.paths[0]) ||
+                                       !is_in_index(revs->diffopt.paths[1])) {
+                       revs->max_count = -2;
+                       DIFF_OPT_SET(&revs->diffopt, NO_INDEX);
+               }
+       }
+
+       /*
+        * Make sure there are NO revision (i.e. pending object) parameter,
+        * rev.max_count is reasonable (0 <= n <= 3),
+        * there is no other revision filtering parameters.
+        */
+       if (revs->pending.nr || revs->max_count > 3 ||
+           revs->min_age != -1 || revs->max_age != -1)
+               return error("no revision allowed with diff-files");
+
+       if (revs->max_count == -1 &&
+           (revs->diffopt.output_format & DIFF_FORMAT_PATCH))
+               revs->combine_merges = revs->dense_combined_merges = 1;
+
+       return 0;
+}
+
+static int is_outside_repo(const char *path, int nongit, const char *prefix)
+{
+       int i;
+       if (nongit || !strcmp(path, "-") || is_absolute_path(path))
+               return 1;
+       if (prefixcmp(path, "../"))
+               return 0;
+       if (!prefix)
+               return 1;
+       for (i = strlen(prefix); !prefixcmp(path, "../"); ) {
+               while (i > 0 && prefix[i - 1] != '/')
+                       i--;
+               if (--i < 0)
+                       return 1;
+               path += 3;
+       }
+       return 0;
+}
+
+int setup_diff_no_index(struct rev_info *revs,
+               int argc, const char ** argv, int nongit, const char *prefix)
+{
+       int i;
+       for (i = 1; i < argc; i++)
+               if (argv[i][0] != '-' || argv[i][1] == '\0')
+                       break;
+               else if (!strcmp(argv[i], "--")) {
+                       i++;
+                       break;
+               } else if (i < argc - 3 && !strcmp(argv[i], "--no-index")) {
+                       i = argc - 3;
+                       DIFF_OPT_SET(&revs->diffopt, EXIT_WITH_STATUS);
+                       break;
+               }
+       if (argc != i + 2 || (!is_outside_repo(argv[i + 1], nongit, prefix) &&
+                               !is_outside_repo(argv[i], nongit, prefix)))
+               return -1;
+
+       diff_setup(&revs->diffopt);
+       for (i = 1; i < argc - 2; )
+               if (!strcmp(argv[i], "--no-index"))
+                       i++;
+               else {
+                       int j = diff_opt_parse(&revs->diffopt,
+                                       argv + i, argc - i);
+                       if (!j)
+                               die("invalid diff option/value: %s", argv[i]);
+                       i += j;
+               }
+
+       if (prefix) {
+               int len = strlen(prefix);
+
+               revs->diffopt.paths = xcalloc(2, sizeof(char*));
+               for (i = 0; i < 2; i++) {
+                       const char *p = argv[argc - 2 + i];
+                       /*
+                        * stdin should be spelled as '-'; if you have
+                        * path that is '-', spell it as ./-.
+                        */
+                       p = (strcmp(p, "-")
+                            ? xstrdup(prefix_filename(prefix, len, p))
+                            : p);
+                       revs->diffopt.paths[i] = p;
+               }
+       }
+       else
+               revs->diffopt.paths = argv + argc - 2;
+       revs->diffopt.nr_paths = 2;
+       DIFF_OPT_SET(&revs->diffopt, NO_INDEX);
+       revs->max_count = -2;
+       if (diff_setup_done(&revs->diffopt) < 0)
+               die("diff_setup_done failed");
+       return 0;
+}
+
+int run_diff_files_cmd(struct rev_info *revs, int argc, const char **argv)
+{
+       unsigned int options;
+
+       if (handle_diff_files_args(revs, argc, argv, &options))
+               return -1;
+
+       if (DIFF_OPT_TST(&revs->diffopt, NO_INDEX)) {
+               if (revs->diffopt.nr_paths != 2)
+                       return error("need two files/directories with --no-index");
+               if (queue_diff(&revs->diffopt, revs->diffopt.paths[0],
+                               revs->diffopt.paths[1]))
+                       return -1;
+               diffcore_std(&revs->diffopt);
+               diff_flush(&revs->diffopt);
+               /*
+                * The return code for --no-index imitates diff(1):
+                * 0 = no changes, 1 = changes, else error
+                */
+               return revs->diffopt.found_changes;
+       }
+
+       if (read_cache() < 0) {
+               perror("read_cache");
+               return -1;
+       }
+       return run_diff_files(revs, options);
+}
+
+int run_diff_files(struct rev_info *revs, unsigned int option)
+{
+       int entries, i;
+       int diff_unmerged_stage = revs->max_count;
+       int silent_on_removed = option & DIFF_SILENT_ON_REMOVED;
+       unsigned ce_option = ((option & DIFF_RACY_IS_MODIFIED)
+                             ? CE_MATCH_RACY_IS_DIRTY : 0);
+
+       if (diff_unmerged_stage < 0)
+               diff_unmerged_stage = 2;
+       entries = active_nr;
+       for (i = 0; i < entries; i++) {
+               struct stat st;
+               unsigned int oldmode, newmode;
+               struct cache_entry *ce = active_cache[i];
+               int changed;
+
+               if (DIFF_OPT_TST(&revs->diffopt, QUIET) &&
+                       DIFF_OPT_TST(&revs->diffopt, HAS_CHANGES))
+                       break;
+
+               if (!ce_path_match(ce, revs->prune_data))
+                       continue;
+
+               if (ce_stage(ce)) {
+                       struct combine_diff_path *dpath;
+                       int num_compare_stages = 0;
+                       size_t path_len;
+
+                       path_len = ce_namelen(ce);
+
+                       dpath = xmalloc(combine_diff_path_size(5, path_len));
+                       dpath->path = (char *) &(dpath->parent[5]);
+
+                       dpath->next = NULL;
+                       dpath->len = path_len;
+                       memcpy(dpath->path, ce->name, path_len);
+                       dpath->path[path_len] = '\0';
+                       hashclr(dpath->sha1);
+                       memset(&(dpath->parent[0]), 0,
+                              sizeof(struct combine_diff_parent)*5);
+
+                       if (lstat(ce->name, &st) < 0) {
+                               if (errno != ENOENT && errno != ENOTDIR) {
+                                       perror(ce->name);
+                                       continue;
+                               }
+                               if (silent_on_removed)
+                                       continue;
+                       }
+                       else
+                               dpath->mode = ntohl(ce_mode_from_stat(ce, st.st_mode));
+
+                       while (i < entries) {
+                               struct cache_entry *nce = active_cache[i];
+                               int stage;
+
+                               if (strcmp(ce->name, nce->name))
+                                       break;
+
+                               /* Stage #2 (ours) is the first parent,
+                                * stage #3 (theirs) is the second.
+                                */
+                               stage = ce_stage(nce);
+                               if (2 <= stage) {
+                                       int mode = ntohl(nce->ce_mode);
+                                       num_compare_stages++;
+                                       hashcpy(dpath->parent[stage-2].sha1, nce->sha1);
+                                       dpath->parent[stage-2].mode = ntohl(ce_mode_from_stat(nce, mode));
+                                       dpath->parent[stage-2].status =
+                                               DIFF_STATUS_MODIFIED;
+                               }
+
+                               /* diff against the proper unmerged stage */
+                               if (stage == diff_unmerged_stage)
+                                       ce = nce;
+                               i++;
+                       }
+                       /*
+                        * Compensate for loop update
+                        */
+                       i--;
+
+                       if (revs->combine_merges && num_compare_stages == 2) {
+                               show_combined_diff(dpath, 2,
+                                                  revs->dense_combined_merges,
+                                                  revs);
+                               free(dpath);
+                               continue;
+                       }
+                       free(dpath);
+                       dpath = NULL;
+
+                       /*
+                        * Show the diff for the 'ce' if we found the one
+                        * from the desired stage.
+                        */
+                       diff_unmerge(&revs->diffopt, ce->name, 0, null_sha1);
+                       if (ce_stage(ce) != diff_unmerged_stage)
+                               continue;
+               }
+
+               if (lstat(ce->name, &st) < 0) {
+                       if (errno != ENOENT && errno != ENOTDIR) {
+                               perror(ce->name);
+                               continue;
+                       }
+                       if (silent_on_removed)
+                               continue;
+                       diff_addremove(&revs->diffopt, '-', ntohl(ce->ce_mode),
+                                      ce->sha1, ce->name, NULL);
+                       continue;
+               }
+               changed = ce_match_stat(ce, &st, ce_option);
+               if (!changed && !DIFF_OPT_TST(&revs->diffopt, FIND_COPIES_HARDER))
+                       continue;
+               oldmode = ntohl(ce->ce_mode);
+               newmode = ntohl(ce_mode_from_stat(ce, st.st_mode));
+               diff_change(&revs->diffopt, oldmode, newmode,
+                           ce->sha1, (changed ? null_sha1 : ce->sha1),
+                           ce->name, NULL);
+
+       }
+       diffcore_std(&revs->diffopt);
+       diff_flush(&revs->diffopt);
+       return 0;
+}
+
+/*
+ * diff-index
+ */
+
+/* A file entry went away or appeared */
+static void diff_index_show_file(struct rev_info *revs,
+                                const char *prefix,
+                                struct cache_entry *ce,
+                                unsigned char *sha1, unsigned int mode)
+{
+       diff_addremove(&revs->diffopt, prefix[0], ntohl(mode),
+                      sha1, ce->name, NULL);
+}
+
+static int get_stat_data(struct cache_entry *ce,
+                        unsigned char **sha1p,
+                        unsigned int *modep,
+                        int cached, int match_missing)
+{
+       unsigned char *sha1 = ce->sha1;
+       unsigned int mode = ce->ce_mode;
+
+       if (!cached) {
+               static unsigned char no_sha1[20];
+               int changed;
+               struct stat st;
+               if (lstat(ce->name, &st) < 0) {
+                       if (errno == ENOENT && match_missing) {
+                               *sha1p = sha1;
+                               *modep = mode;
+                               return 0;
+                       }
+                       return -1;
+               }
+               changed = ce_match_stat(ce, &st, 0);
+               if (changed) {
+                       mode = ce_mode_from_stat(ce, st.st_mode);
+                       sha1 = no_sha1;
+               }
+       }
+
+       *sha1p = sha1;
+       *modep = mode;
+       return 0;
+}
+
+static void show_new_file(struct rev_info *revs,
+                         struct cache_entry *new,
+                         int cached, int match_missing)
+{
+       unsigned char *sha1;
+       unsigned int mode;
+
+       /* New file in the index: it might actually be different in
+        * the working copy.
+        */
+       if (get_stat_data(new, &sha1, &mode, cached, match_missing) < 0)
+               return;
+
+       diff_index_show_file(revs, "+", new, sha1, mode);
+}
+
+static int show_modified(struct rev_info *revs,
+                        struct cache_entry *old,
+                        struct cache_entry *new,
+                        int report_missing,
+                        int cached, int match_missing)
+{
+       unsigned int mode, oldmode;
+       unsigned char *sha1;
+
+       if (get_stat_data(new, &sha1, &mode, cached, match_missing) < 0) {
+               if (report_missing)
+                       diff_index_show_file(revs, "-", old,
+                                            old->sha1, old->ce_mode);
+               return -1;
+       }
+
+       if (revs->combine_merges && !cached &&
+           (hashcmp(sha1, old->sha1) || hashcmp(old->sha1, new->sha1))) {
+               struct combine_diff_path *p;
+               int pathlen = ce_namelen(new);
+
+               p = xmalloc(combine_diff_path_size(2, pathlen));
+               p->path = (char *) &p->parent[2];
+               p->next = NULL;
+               p->len = pathlen;
+               memcpy(p->path, new->name, pathlen);
+               p->path[pathlen] = 0;
+               p->mode = ntohl(mode);
+               hashclr(p->sha1);
+               memset(p->parent, 0, 2 * sizeof(struct combine_diff_parent));
+               p->parent[0].status = DIFF_STATUS_MODIFIED;
+               p->parent[0].mode = ntohl(new->ce_mode);
+               hashcpy(p->parent[0].sha1, new->sha1);
+               p->parent[1].status = DIFF_STATUS_MODIFIED;
+               p->parent[1].mode = ntohl(old->ce_mode);
+               hashcpy(p->parent[1].sha1, old->sha1);
+               show_combined_diff(p, 2, revs->dense_combined_merges, revs);
+               free(p);
+               return 0;
+       }
+
+       oldmode = old->ce_mode;
+       if (mode == oldmode && !hashcmp(sha1, old->sha1) &&
+           !DIFF_OPT_TST(&revs->diffopt, FIND_COPIES_HARDER))
+               return 0;
+
+       mode = ntohl(mode);
+       oldmode = ntohl(oldmode);
+
+       diff_change(&revs->diffopt, oldmode, mode,
+                   old->sha1, sha1, old->name, NULL);
+       return 0;
+}
+
+static int diff_cache(struct rev_info *revs,
+                     struct cache_entry **ac, int entries,
+                     const char **pathspec,
+                     int cached, int match_missing)
+{
+       while (entries) {
+               struct cache_entry *ce = *ac;
+               int same = (entries > 1) && ce_same_name(ce, ac[1]);
+
+               if (DIFF_OPT_TST(&revs->diffopt, QUIET) &&
+                       DIFF_OPT_TST(&revs->diffopt, HAS_CHANGES))
+                       break;
+
+               if (!ce_path_match(ce, pathspec))
+                       goto skip_entry;
+
+               switch (ce_stage(ce)) {
+               case 0:
+                       /* No stage 1 entry? That means it's a new file */
+                       if (!same) {
+                               show_new_file(revs, ce, cached, match_missing);
+                               break;
+                       }
+                       /* Show difference between old and new */
+                       show_modified(revs, ac[1], ce, 1,
+                                     cached, match_missing);
+                       break;
+               case 1:
+                       /* No stage 3 (merge) entry?
+                        * That means it's been deleted.
+                        */
+                       if (!same) {
+                               diff_index_show_file(revs, "-", ce,
+                                                    ce->sha1, ce->ce_mode);
+                               break;
+                       }
+                       /* We come here with ce pointing at stage 1
+                        * (original tree) and ac[1] pointing at stage
+                        * 3 (unmerged).  show-modified with
+                        * report-missing set to false does not say the
+                        * file is deleted but reports true if work
+                        * tree does not have it, in which case we
+                        * fall through to report the unmerged state.
+                        * Otherwise, we show the differences between
+                        * the original tree and the work tree.
+                        */
+                       if (!cached &&
+                           !show_modified(revs, ce, ac[1], 0,
+                                          cached, match_missing))
+                               break;
+                       diff_unmerge(&revs->diffopt, ce->name,
+                                    ntohl(ce->ce_mode), ce->sha1);
+                       break;
+               case 3:
+                       diff_unmerge(&revs->diffopt, ce->name,
+                                    0, null_sha1);
+                       break;
+
+               default:
+                       die("impossible cache entry stage");
+               }
+
+skip_entry:
+               /*
+                * Ignore all the different stages for this file,
+                * we've handled the relevant cases now.
+                */
+               do {
+                       ac++;
+                       entries--;
+               } while (entries && ce_same_name(ce, ac[0]));
+       }
+       return 0;
+}
+
+/*
+ * This turns all merge entries into "stage 3". That guarantees that
+ * when we read in the new tree (into "stage 1"), we won't lose sight
+ * of the fact that we had unmerged entries.
+ */
+static void mark_merge_entries(void)
+{
+       int i;
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (!ce_stage(ce))
+                       continue;
+               ce->ce_flags |= htons(CE_STAGEMASK);
+       }
+}
+
+int run_diff_index(struct rev_info *revs, int cached)
+{
+       int ret;
+       struct object *ent;
+       struct tree *tree;
+       const char *tree_name;
+       int match_missing = 0;
+
+       /*
+        * Backward compatibility wart - "diff-index -m" does
+        * not mean "do not ignore merges", but totally different.
+        */
+       if (!revs->ignore_merges)
+               match_missing = 1;
+
+       mark_merge_entries();
+
+       ent = revs->pending.objects[0].item;
+       tree_name = revs->pending.objects[0].name;
+       tree = parse_tree_indirect(ent->sha1);
+       if (!tree)
+               return error("bad tree object %s", tree_name);
+       if (read_tree(tree, 1, revs->prune_data))
+               return error("unable to read tree object %s", tree_name);
+       ret = diff_cache(revs, active_cache, active_nr, revs->prune_data,
+                        cached, match_missing);
+       diffcore_std(&revs->diffopt);
+       diff_flush(&revs->diffopt);
+       return ret;
+}
+
+int do_diff_cache(const unsigned char *tree_sha1, struct diff_options *opt)
+{
+       struct tree *tree;
+       struct rev_info revs;
+       int i;
+       struct cache_entry **dst;
+       struct cache_entry *last = NULL;
+
+       /*
+        * This is used by git-blame to run diff-cache internally;
+        * it potentially needs to repeatedly run this, so we will
+        * start by removing the higher order entries the last round
+        * left behind.
+        */
+       dst = active_cache;
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (ce_stage(ce)) {
+                       if (last && !strcmp(ce->name, last->name))
+                               continue;
+                       cache_tree_invalidate_path(active_cache_tree,
+                                                  ce->name);
+                       last = ce;
+                       ce->ce_mode = 0;
+                       ce->ce_flags &= ~htons(CE_STAGEMASK);
+               }
+               *dst++ = ce;
+       }
+       active_nr = dst - active_cache;
+
+       init_revisions(&revs, NULL);
+       revs.prune_data = opt->paths;
+       tree = parse_tree_indirect(tree_sha1);
+       if (!tree)
+               die("bad tree object %s", sha1_to_hex(tree_sha1));
+       if (read_tree(tree, 1, opt->paths))
+               return error("unable to read tree %s", sha1_to_hex(tree_sha1));
+       return diff_cache(&revs, active_cache, active_nr, revs.prune_data,
+                         1, 0);
+}
diff --git a/diff.c b/diff.c
new file mode 100644 (file)
index 0000000..be6cf68
--- /dev/null
+++ b/diff.c
@@ -0,0 +1,3187 @@
+/*
+ * Copyright (C) 2005 Junio C Hamano
+ */
+#include "cache.h"
+#include "quote.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "delta.h"
+#include "xdiff-interface.h"
+#include "color.h"
+#include "attr.h"
+#include "run-command.h"
+
+#ifdef NO_FAST_WORKING_DIRECTORY
+#define FAST_WORKING_DIRECTORY 0
+#else
+#define FAST_WORKING_DIRECTORY 1
+#endif
+
+static int diff_detect_rename_default;
+static int diff_rename_limit_default = 100;
+static int diff_use_color_default;
+int diff_auto_refresh_index = 1;
+
+static char diff_colors[][COLOR_MAXLEN] = {
+       "\033[m",       /* reset */
+       "",             /* PLAIN (normal) */
+       "\033[1m",      /* METAINFO (bold) */
+       "\033[36m",     /* FRAGINFO (cyan) */
+       "\033[31m",     /* OLD (red) */
+       "\033[32m",     /* NEW (green) */
+       "\033[33m",     /* COMMIT (yellow) */
+       "\033[41m",     /* WHITESPACE (red background) */
+};
+
+static int parse_diff_color_slot(const char *var, int ofs)
+{
+       if (!strcasecmp(var+ofs, "plain"))
+               return DIFF_PLAIN;
+       if (!strcasecmp(var+ofs, "meta"))
+               return DIFF_METAINFO;
+       if (!strcasecmp(var+ofs, "frag"))
+               return DIFF_FRAGINFO;
+       if (!strcasecmp(var+ofs, "old"))
+               return DIFF_FILE_OLD;
+       if (!strcasecmp(var+ofs, "new"))
+               return DIFF_FILE_NEW;
+       if (!strcasecmp(var+ofs, "commit"))
+               return DIFF_COMMIT;
+       if (!strcasecmp(var+ofs, "whitespace"))
+               return DIFF_WHITESPACE;
+       die("bad config variable '%s'", var);
+}
+
+static struct ll_diff_driver {
+       const char *name;
+       struct ll_diff_driver *next;
+       char *cmd;
+} *user_diff, **user_diff_tail;
+
+static void read_config_if_needed(void)
+{
+       if (!user_diff_tail) {
+               user_diff_tail = &user_diff;
+               git_config(git_diff_ui_config);
+       }
+}
+
+/*
+ * Currently there is only "diff.<drivername>.command" variable;
+ * because there are "diff.color.<slot>" variables, we are parsing
+ * this in a bit convoluted way to allow low level diff driver
+ * called "color".
+ */
+static int parse_lldiff_command(const char *var, const char *ep, const char *value)
+{
+       const char *name;
+       int namelen;
+       struct ll_diff_driver *drv;
+
+       name = var + 5;
+       namelen = ep - name;
+       for (drv = user_diff; drv; drv = drv->next)
+               if (!strncmp(drv->name, name, namelen) && !drv->name[namelen])
+                       break;
+       if (!drv) {
+               drv = xcalloc(1, sizeof(struct ll_diff_driver));
+               drv->name = xmemdupz(name, namelen);
+               if (!user_diff_tail)
+                       user_diff_tail = &user_diff;
+               *user_diff_tail = drv;
+               user_diff_tail = &(drv->next);
+       }
+
+       if (!value)
+               return error("%s: lacks value", var);
+       drv->cmd = strdup(value);
+       return 0;
+}
+
+/*
+ * 'diff.<what>.funcname' attribute can be specified in the configuration
+ * to define a customized regexp to find the beginning of a function to
+ * be used for hunk header lines of "diff -p" style output.
+ */
+static struct funcname_pattern {
+       char *name;
+       char *pattern;
+       struct funcname_pattern *next;
+} *funcname_pattern_list;
+
+static int parse_funcname_pattern(const char *var, const char *ep, const char *value)
+{
+       const char *name;
+       int namelen;
+       struct funcname_pattern *pp;
+
+       name = var + 5; /* "diff." */
+       namelen = ep - name;
+
+       for (pp = funcname_pattern_list; pp; pp = pp->next)
+               if (!strncmp(pp->name, name, namelen) && !pp->name[namelen])
+                       break;
+       if (!pp) {
+               pp = xcalloc(1, sizeof(*pp));
+               pp->name = xmemdupz(name, namelen);
+               pp->next = funcname_pattern_list;
+               funcname_pattern_list = pp;
+       }
+       if (pp->pattern)
+               free(pp->pattern);
+       pp->pattern = xstrdup(value);
+       return 0;
+}
+
+/*
+ * These are to give UI layer defaults.
+ * The core-level commands such as git-diff-files should
+ * never be affected by the setting of diff.renames
+ * the user happens to have in the configuration file.
+ */
+int git_diff_ui_config(const char *var, const char *value)
+{
+       if (!strcmp(var, "diff.renamelimit")) {
+               diff_rename_limit_default = git_config_int(var, value);
+               return 0;
+       }
+       if (!strcmp(var, "diff.color") || !strcmp(var, "color.diff")) {
+               diff_use_color_default = git_config_colorbool(var, value, -1);
+               return 0;
+       }
+       if (!strcmp(var, "diff.renames")) {
+               if (!value)
+                       diff_detect_rename_default = DIFF_DETECT_RENAME;
+               else if (!strcasecmp(value, "copies") ||
+                        !strcasecmp(value, "copy"))
+                       diff_detect_rename_default = DIFF_DETECT_COPY;
+               else if (git_config_bool(var,value))
+                       diff_detect_rename_default = DIFF_DETECT_RENAME;
+               return 0;
+       }
+       if (!strcmp(var, "diff.autorefreshindex")) {
+               diff_auto_refresh_index = git_config_bool(var, value);
+               return 0;
+       }
+       if (!prefixcmp(var, "diff.")) {
+               const char *ep = strrchr(var, '.');
+
+               if (ep != var + 4) {
+                       if (!strcmp(ep, ".command"))
+                               return parse_lldiff_command(var, ep, value);
+                       if (!strcmp(ep, ".funcname"))
+                               return parse_funcname_pattern(var, ep, value);
+               }
+       }
+       if (!prefixcmp(var, "diff.color.") || !prefixcmp(var, "color.diff.")) {
+               int slot = parse_diff_color_slot(var, 11);
+               color_parse(value, var, diff_colors[slot]);
+               return 0;
+       }
+
+       return git_default_config(var, value);
+}
+
+static char *quote_two(const char *one, const char *two)
+{
+       int need_one = quote_c_style(one, NULL, NULL, 1);
+       int need_two = quote_c_style(two, NULL, NULL, 1);
+       struct strbuf res;
+
+       strbuf_init(&res, 0);
+       if (need_one + need_two) {
+               strbuf_addch(&res, '"');
+               quote_c_style(one, &res, NULL, 1);
+               quote_c_style(two, &res, NULL, 1);
+               strbuf_addch(&res, '"');
+       } else {
+               strbuf_addstr(&res, one);
+               strbuf_addstr(&res, two);
+       }
+       return strbuf_detach(&res, NULL);
+}
+
+static const char *external_diff(void)
+{
+       static const char *external_diff_cmd = NULL;
+       static int done_preparing = 0;
+
+       if (done_preparing)
+               return external_diff_cmd;
+       external_diff_cmd = getenv("GIT_EXTERNAL_DIFF");
+       done_preparing = 1;
+       return external_diff_cmd;
+}
+
+static struct diff_tempfile {
+       const char *name; /* filename external diff should read from */
+       char hex[41];
+       char mode[10];
+       char tmp_path[PATH_MAX];
+} diff_temp[2];
+
+static int count_lines(const char *data, int size)
+{
+       int count, ch, completely_empty = 1, nl_just_seen = 0;
+       count = 0;
+       while (0 < size--) {
+               ch = *data++;
+               if (ch == '\n') {
+                       count++;
+                       nl_just_seen = 1;
+                       completely_empty = 0;
+               }
+               else {
+                       nl_just_seen = 0;
+                       completely_empty = 0;
+               }
+       }
+       if (completely_empty)
+               return 0;
+       if (!nl_just_seen)
+               count++; /* no trailing newline */
+       return count;
+}
+
+static void print_line_count(int count)
+{
+       switch (count) {
+       case 0:
+               printf("0,0");
+               break;
+       case 1:
+               printf("1");
+               break;
+       default:
+               printf("1,%d", count);
+               break;
+       }
+}
+
+static void copy_file(int prefix, const char *data, int size,
+               const char *set, const char *reset)
+{
+       int ch, nl_just_seen = 1;
+       while (0 < size--) {
+               ch = *data++;
+               if (nl_just_seen) {
+                       fputs(set, stdout);
+                       putchar(prefix);
+               }
+               if (ch == '\n') {
+                       nl_just_seen = 1;
+                       fputs(reset, stdout);
+               } else
+                       nl_just_seen = 0;
+               putchar(ch);
+       }
+       if (!nl_just_seen)
+               printf("%s\n\\ No newline at end of file\n", reset);
+}
+
+static void emit_rewrite_diff(const char *name_a,
+                             const char *name_b,
+                             struct diff_filespec *one,
+                             struct diff_filespec *two,
+                             int color_diff)
+{
+       int lc_a, lc_b;
+       const char *name_a_tab, *name_b_tab;
+       const char *metainfo = diff_get_color(color_diff, DIFF_METAINFO);
+       const char *fraginfo = diff_get_color(color_diff, DIFF_FRAGINFO);
+       const char *old = diff_get_color(color_diff, DIFF_FILE_OLD);
+       const char *new = diff_get_color(color_diff, DIFF_FILE_NEW);
+       const char *reset = diff_get_color(color_diff, DIFF_RESET);
+
+       name_a += (*name_a == '/');
+       name_b += (*name_b == '/');
+       name_a_tab = strchr(name_a, ' ') ? "\t" : "";
+       name_b_tab = strchr(name_b, ' ') ? "\t" : "";
+
+       diff_populate_filespec(one, 0);
+       diff_populate_filespec(two, 0);
+       lc_a = count_lines(one->data, one->size);
+       lc_b = count_lines(two->data, two->size);
+       printf("%s--- a/%s%s%s\n%s+++ b/%s%s%s\n%s@@ -",
+              metainfo, name_a, name_a_tab, reset,
+              metainfo, name_b, name_b_tab, reset, fraginfo);
+       print_line_count(lc_a);
+       printf(" +");
+       print_line_count(lc_b);
+       printf(" @@%s\n", reset);
+       if (lc_a)
+               copy_file('-', one->data, one->size, old, reset);
+       if (lc_b)
+               copy_file('+', two->data, two->size, new, reset);
+}
+
+static int fill_mmfile(mmfile_t *mf, struct diff_filespec *one)
+{
+       if (!DIFF_FILE_VALID(one)) {
+               mf->ptr = (char *)""; /* does not matter */
+               mf->size = 0;
+               return 0;
+       }
+       else if (diff_populate_filespec(one, 0))
+               return -1;
+       mf->ptr = one->data;
+       mf->size = one->size;
+       return 0;
+}
+
+struct diff_words_buffer {
+       mmfile_t text;
+       long alloc;
+       long current; /* output pointer */
+       int suppressed_newline;
+};
+
+static void diff_words_append(char *line, unsigned long len,
+               struct diff_words_buffer *buffer)
+{
+       if (buffer->text.size + len > buffer->alloc) {
+               buffer->alloc = (buffer->text.size + len) * 3 / 2;
+               buffer->text.ptr = xrealloc(buffer->text.ptr, buffer->alloc);
+       }
+       line++;
+       len--;
+       memcpy(buffer->text.ptr + buffer->text.size, line, len);
+       buffer->text.size += len;
+}
+
+struct diff_words_data {
+       struct xdiff_emit_state xm;
+       struct diff_words_buffer minus, plus;
+};
+
+static void print_word(struct diff_words_buffer *buffer, int len, int color,
+               int suppress_newline)
+{
+       const char *ptr;
+       int eol = 0;
+
+       if (len == 0)
+               return;
+
+       ptr  = buffer->text.ptr + buffer->current;
+       buffer->current += len;
+
+       if (ptr[len - 1] == '\n') {
+               eol = 1;
+               len--;
+       }
+
+       fputs(diff_get_color(1, color), stdout);
+       fwrite(ptr, len, 1, stdout);
+       fputs(diff_get_color(1, DIFF_RESET), stdout);
+
+       if (eol) {
+               if (suppress_newline)
+                       buffer->suppressed_newline = 1;
+               else
+                       putchar('\n');
+       }
+}
+
+static void fn_out_diff_words_aux(void *priv, char *line, unsigned long len)
+{
+       struct diff_words_data *diff_words = priv;
+
+       if (diff_words->minus.suppressed_newline) {
+               if (line[0] != '+')
+                       putchar('\n');
+               diff_words->minus.suppressed_newline = 0;
+       }
+
+       len--;
+       switch (line[0]) {
+               case '-':
+                       print_word(&diff_words->minus, len, DIFF_FILE_OLD, 1);
+                       break;
+               case '+':
+                       print_word(&diff_words->plus, len, DIFF_FILE_NEW, 0);
+                       break;
+               case ' ':
+                       print_word(&diff_words->plus, len, DIFF_PLAIN, 0);
+                       diff_words->minus.current += len;
+                       break;
+       }
+}
+
+/* this executes the word diff on the accumulated buffers */
+static void diff_words_show(struct diff_words_data *diff_words)
+{
+       xpparam_t xpp;
+       xdemitconf_t xecfg;
+       xdemitcb_t ecb;
+       mmfile_t minus, plus;
+       int i;
+
+       memset(&xecfg, 0, sizeof(xecfg));
+       minus.size = diff_words->minus.text.size;
+       minus.ptr = xmalloc(minus.size);
+       memcpy(minus.ptr, diff_words->minus.text.ptr, minus.size);
+       for (i = 0; i < minus.size; i++)
+               if (isspace(minus.ptr[i]))
+                       minus.ptr[i] = '\n';
+       diff_words->minus.current = 0;
+
+       plus.size = diff_words->plus.text.size;
+       plus.ptr = xmalloc(plus.size);
+       memcpy(plus.ptr, diff_words->plus.text.ptr, plus.size);
+       for (i = 0; i < plus.size; i++)
+               if (isspace(plus.ptr[i]))
+                       plus.ptr[i] = '\n';
+       diff_words->plus.current = 0;
+
+       xpp.flags = XDF_NEED_MINIMAL;
+       xecfg.ctxlen = diff_words->minus.alloc + diff_words->plus.alloc;
+       ecb.outf = xdiff_outf;
+       ecb.priv = diff_words;
+       diff_words->xm.consume = fn_out_diff_words_aux;
+       xdl_diff(&minus, &plus, &xpp, &xecfg, &ecb);
+
+       free(minus.ptr);
+       free(plus.ptr);
+       diff_words->minus.text.size = diff_words->plus.text.size = 0;
+
+       if (diff_words->minus.suppressed_newline) {
+               putchar('\n');
+               diff_words->minus.suppressed_newline = 0;
+       }
+}
+
+struct emit_callback {
+       struct xdiff_emit_state xm;
+       int nparents, color_diff;
+       const char **label_path;
+       struct diff_words_data *diff_words;
+       int *found_changesp;
+};
+
+static void free_diff_words_data(struct emit_callback *ecbdata)
+{
+       if (ecbdata->diff_words) {
+               /* flush buffers */
+               if (ecbdata->diff_words->minus.text.size ||
+                               ecbdata->diff_words->plus.text.size)
+                       diff_words_show(ecbdata->diff_words);
+
+               if (ecbdata->diff_words->minus.text.ptr)
+                       free (ecbdata->diff_words->minus.text.ptr);
+               if (ecbdata->diff_words->plus.text.ptr)
+                       free (ecbdata->diff_words->plus.text.ptr);
+               free(ecbdata->diff_words);
+               ecbdata->diff_words = NULL;
+       }
+}
+
+const char *diff_get_color(int diff_use_color, enum color_diff ix)
+{
+       if (diff_use_color)
+               return diff_colors[ix];
+       return "";
+}
+
+static void emit_line(const char *set, const char *reset, const char *line, int len)
+{
+       if (len > 0 && line[len-1] == '\n')
+               len--;
+       fputs(set, stdout);
+       fwrite(line, len, 1, stdout);
+       puts(reset);
+}
+
+static void emit_line_with_ws(int nparents,
+               const char *set, const char *reset, const char *ws,
+               const char *line, int len)
+{
+       int col0 = nparents;
+       int last_tab_in_indent = -1;
+       int last_space_in_indent = -1;
+       int i;
+       int tail = len;
+       int need_highlight_leading_space = 0;
+       /* The line is a newly added line.  Does it have funny leading
+        * whitespaces?  In indent, SP should never precede a TAB.
+        */
+       for (i = col0; i < len; i++) {
+               if (line[i] == '\t') {
+                       last_tab_in_indent = i;
+                       if (0 <= last_space_in_indent)
+                               need_highlight_leading_space = 1;
+               }
+               else if (line[i] == ' ')
+                       last_space_in_indent = i;
+               else
+                       break;
+       }
+       fputs(set, stdout);
+       fwrite(line, col0, 1, stdout);
+       fputs(reset, stdout);
+       if (((i == len) || line[i] == '\n') && i != col0) {
+               /* The whole line was indent */
+               emit_line(ws, reset, line + col0, len - col0);
+               return;
+       }
+       i = col0;
+       if (need_highlight_leading_space) {
+               while (i < last_tab_in_indent) {
+                       if (line[i] == ' ') {
+                               fputs(ws, stdout);
+                               putchar(' ');
+                               fputs(reset, stdout);
+                       }
+                       else
+                               putchar(line[i]);
+                       i++;
+               }
+       }
+       tail = len - 1;
+       if (line[tail] == '\n' && i < tail)
+               tail--;
+       while (i < tail) {
+               if (!isspace(line[tail]))
+                       break;
+               tail--;
+       }
+       if ((i < tail && line[tail + 1] != '\n')) {
+               /* This has whitespace between tail+1..len */
+               fputs(set, stdout);
+               fwrite(line + i, tail - i + 1, 1, stdout);
+               fputs(reset, stdout);
+               emit_line(ws, reset, line + tail + 1, len - tail - 1);
+       }
+       else
+               emit_line(set, reset, line + i, len - i);
+}
+
+static void emit_add_line(const char *reset, struct emit_callback *ecbdata, const char *line, int len)
+{
+       const char *ws = diff_get_color(ecbdata->color_diff, DIFF_WHITESPACE);
+       const char *set = diff_get_color(ecbdata->color_diff, DIFF_FILE_NEW);
+
+       if (!*ws)
+               emit_line(set, reset, line, len);
+       else
+               emit_line_with_ws(ecbdata->nparents, set, reset, ws,
+                               line, len);
+}
+
+static void fn_out_consume(void *priv, char *line, unsigned long len)
+{
+       int i;
+       int color;
+       struct emit_callback *ecbdata = priv;
+       const char *set = diff_get_color(ecbdata->color_diff, DIFF_METAINFO);
+       const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET);
+
+       *(ecbdata->found_changesp) = 1;
+
+       if (ecbdata->label_path[0]) {
+               const char *name_a_tab, *name_b_tab;
+
+               name_a_tab = strchr(ecbdata->label_path[0], ' ') ? "\t" : "";
+               name_b_tab = strchr(ecbdata->label_path[1], ' ') ? "\t" : "";
+
+               printf("%s--- %s%s%s\n",
+                      set, ecbdata->label_path[0], reset, name_a_tab);
+               printf("%s+++ %s%s%s\n",
+                      set, ecbdata->label_path[1], reset, name_b_tab);
+               ecbdata->label_path[0] = ecbdata->label_path[1] = NULL;
+       }
+
+       /* This is not really necessary for now because
+        * this codepath only deals with two-way diffs.
+        */
+       for (i = 0; i < len && line[i] == '@'; i++)
+               ;
+       if (2 <= i && i < len && line[i] == ' ') {
+               ecbdata->nparents = i - 1;
+               emit_line(diff_get_color(ecbdata->color_diff, DIFF_FRAGINFO),
+                         reset, line, len);
+               return;
+       }
+
+       if (len < ecbdata->nparents) {
+               set = reset;
+               emit_line(reset, reset, line, len);
+               return;
+       }
+
+       color = DIFF_PLAIN;
+       if (ecbdata->diff_words && ecbdata->nparents != 1)
+               /* fall back to normal diff */
+               free_diff_words_data(ecbdata);
+       if (ecbdata->diff_words) {
+               if (line[0] == '-') {
+                       diff_words_append(line, len,
+                                         &ecbdata->diff_words->minus);
+                       return;
+               } else if (line[0] == '+') {
+                       diff_words_append(line, len,
+                                         &ecbdata->diff_words->plus);
+                       return;
+               }
+               if (ecbdata->diff_words->minus.text.size ||
+                   ecbdata->diff_words->plus.text.size)
+                       diff_words_show(ecbdata->diff_words);
+               line++;
+               len--;
+               emit_line(set, reset, line, len);
+               return;
+       }
+       for (i = 0; i < ecbdata->nparents && len; i++) {
+               if (line[i] == '-')
+                       color = DIFF_FILE_OLD;
+               else if (line[i] == '+')
+                       color = DIFF_FILE_NEW;
+       }
+
+       if (color != DIFF_FILE_NEW) {
+               emit_line(diff_get_color(ecbdata->color_diff, color),
+                         reset, line, len);
+               return;
+       }
+       emit_add_line(reset, ecbdata, line, len);
+}
+
+static char *pprint_rename(const char *a, const char *b)
+{
+       const char *old = a;
+       const char *new = b;
+       struct strbuf name;
+       int pfx_length, sfx_length;
+       int len_a = strlen(a);
+       int len_b = strlen(b);
+       int a_midlen, b_midlen;
+       int qlen_a = quote_c_style(a, NULL, NULL, 0);
+       int qlen_b = quote_c_style(b, NULL, NULL, 0);
+
+       strbuf_init(&name, 0);
+       if (qlen_a || qlen_b) {
+               quote_c_style(a, &name, NULL, 0);
+               strbuf_addstr(&name, " => ");
+               quote_c_style(b, &name, NULL, 0);
+               return strbuf_detach(&name, NULL);
+       }
+
+       /* Find common prefix */
+       pfx_length = 0;
+       while (*old && *new && *old == *new) {
+               if (*old == '/')
+                       pfx_length = old - a + 1;
+               old++;
+               new++;
+       }
+
+       /* Find common suffix */
+       old = a + len_a;
+       new = b + len_b;
+       sfx_length = 0;
+       while (a <= old && b <= new && *old == *new) {
+               if (*old == '/')
+                       sfx_length = len_a - (old - a);
+               old--;
+               new--;
+       }
+
+       /*
+        * pfx{mid-a => mid-b}sfx
+        * {pfx-a => pfx-b}sfx
+        * pfx{sfx-a => sfx-b}
+        * name-a => name-b
+        */
+       a_midlen = len_a - pfx_length - sfx_length;
+       b_midlen = len_b - pfx_length - sfx_length;
+       if (a_midlen < 0)
+               a_midlen = 0;
+       if (b_midlen < 0)
+               b_midlen = 0;
+
+       strbuf_grow(&name, pfx_length + a_midlen + b_midlen + sfx_length + 7);
+       if (pfx_length + sfx_length) {
+               strbuf_add(&name, a, pfx_length);
+               strbuf_addch(&name, '{');
+       }
+       strbuf_add(&name, a + pfx_length, a_midlen);
+       strbuf_addstr(&name, " => ");
+       strbuf_add(&name, b + pfx_length, b_midlen);
+       if (pfx_length + sfx_length) {
+               strbuf_addch(&name, '}');
+               strbuf_add(&name, a + len_a - sfx_length, sfx_length);
+       }
+       return strbuf_detach(&name, NULL);
+}
+
+struct diffstat_t {
+       struct xdiff_emit_state xm;
+
+       int nr;
+       int alloc;
+       struct diffstat_file {
+               char *name;
+               unsigned is_unmerged:1;
+               unsigned is_binary:1;
+               unsigned is_renamed:1;
+               unsigned int added, deleted;
+       } **files;
+};
+
+static struct diffstat_file *diffstat_add(struct diffstat_t *diffstat,
+                                         const char *name_a,
+                                         const char *name_b)
+{
+       struct diffstat_file *x;
+       x = xcalloc(sizeof (*x), 1);
+       if (diffstat->nr == diffstat->alloc) {
+               diffstat->alloc = alloc_nr(diffstat->alloc);
+               diffstat->files = xrealloc(diffstat->files,
+                               diffstat->alloc * sizeof(x));
+       }
+       diffstat->files[diffstat->nr++] = x;
+       if (name_b) {
+               x->name = pprint_rename(name_a, name_b);
+               x->is_renamed = 1;
+       }
+       else
+               x->name = xstrdup(name_a);
+       return x;
+}
+
+static void diffstat_consume(void *priv, char *line, unsigned long len)
+{
+       struct diffstat_t *diffstat = priv;
+       struct diffstat_file *x = diffstat->files[diffstat->nr - 1];
+
+       if (line[0] == '+')
+               x->added++;
+       else if (line[0] == '-')
+               x->deleted++;
+}
+
+const char mime_boundary_leader[] = "------------";
+
+static int scale_linear(int it, int width, int max_change)
+{
+       /*
+        * make sure that at least one '-' is printed if there were deletions,
+        * and likewise for '+'.
+        */
+       if (max_change < 2)
+               return it;
+       return ((it - 1) * (width - 1) + max_change - 1) / (max_change - 1);
+}
+
+static void show_name(const char *prefix, const char *name, int len,
+                     const char *reset, const char *set)
+{
+       printf(" %s%s%-*s%s |", set, prefix, len, name, reset);
+}
+
+static void show_graph(char ch, int cnt, const char *set, const char *reset)
+{
+       if (cnt <= 0)
+               return;
+       printf("%s", set);
+       while (cnt--)
+               putchar(ch);
+       printf("%s", reset);
+}
+
+static void show_stats(struct diffstat_t* data, struct diff_options *options)
+{
+       int i, len, add, del, total, adds = 0, dels = 0;
+       int max_change = 0, max_len = 0;
+       int total_files = data->nr;
+       int width, name_width;
+       const char *reset, *set, *add_c, *del_c;
+
+       if (data->nr == 0)
+               return;
+
+       width = options->stat_width ? options->stat_width : 80;
+       name_width = options->stat_name_width ? options->stat_name_width : 50;
+
+       /* Sanity: give at least 5 columns to the graph,
+        * but leave at least 10 columns for the name.
+        */
+       if (width < name_width + 15) {
+               if (name_width <= 25)
+                       width = name_width + 15;
+               else
+                       name_width = width - 15;
+       }
+
+       /* Find the longest filename and max number of changes */
+       reset = diff_get_color_opt(options, DIFF_RESET);
+       set   = diff_get_color_opt(options, DIFF_PLAIN);
+       add_c = diff_get_color_opt(options, DIFF_FILE_NEW);
+       del_c = diff_get_color_opt(options, DIFF_FILE_OLD);
+
+       for (i = 0; i < data->nr; i++) {
+               struct diffstat_file *file = data->files[i];
+               int change = file->added + file->deleted;
+
+               if (!file->is_renamed) {  /* renames are already quoted by pprint_rename */
+                       struct strbuf buf;
+                       strbuf_init(&buf, 0);
+                       if (quote_c_style(file->name, &buf, NULL, 0)) {
+                               free(file->name);
+                               file->name = strbuf_detach(&buf, NULL);
+                       } else {
+                               strbuf_release(&buf);
+                       }
+               }
+
+               len = strlen(file->name);
+               if (max_len < len)
+                       max_len = len;
+
+               if (file->is_binary || file->is_unmerged)
+                       continue;
+               if (max_change < change)
+                       max_change = change;
+       }
+
+       /* Compute the width of the graph part;
+        * 10 is for one blank at the beginning of the line plus
+        * " | count " between the name and the graph.
+        *
+        * From here on, name_width is the width of the name area,
+        * and width is the width of the graph area.
+        */
+       name_width = (name_width < max_len) ? name_width : max_len;
+       if (width < (name_width + 10) + max_change)
+               width = width - (name_width + 10);
+       else
+               width = max_change;
+
+       for (i = 0; i < data->nr; i++) {
+               const char *prefix = "";
+               char *name = data->files[i]->name;
+               int added = data->files[i]->added;
+               int deleted = data->files[i]->deleted;
+               int name_len;
+
+               /*
+                * "scale" the filename
+                */
+               len = name_width;
+               name_len = strlen(name);
+               if (name_width < name_len) {
+                       char *slash;
+                       prefix = "...";
+                       len -= 3;
+                       name += name_len - len;
+                       slash = strchr(name, '/');
+                       if (slash)
+                               name = slash;
+               }
+
+               if (data->files[i]->is_binary) {
+                       show_name(prefix, name, len, reset, set);
+                       printf("  Bin ");
+                       printf("%s%d%s", del_c, deleted, reset);
+                       printf(" -> ");
+                       printf("%s%d%s", add_c, added, reset);
+                       printf(" bytes");
+                       printf("\n");
+                       goto free_diffstat_file;
+               }
+               else if (data->files[i]->is_unmerged) {
+                       show_name(prefix, name, len, reset, set);
+                       printf("  Unmerged\n");
+                       goto free_diffstat_file;
+               }
+               else if (!data->files[i]->is_renamed &&
+                        (added + deleted == 0)) {
+                       total_files--;
+                       goto free_diffstat_file;
+               }
+
+               /*
+                * scale the add/delete
+                */
+               add = added;
+               del = deleted;
+               total = add + del;
+               adds += add;
+               dels += del;
+
+               if (width <= max_change) {
+                       add = scale_linear(add, width, max_change);
+                       del = scale_linear(del, width, max_change);
+                       total = add + del;
+               }
+               show_name(prefix, name, len, reset, set);
+               printf("%5d ", added + deleted);
+               show_graph('+', add, add_c, reset);
+               show_graph('-', del, del_c, reset);
+               putchar('\n');
+       free_diffstat_file:
+               free(data->files[i]->name);
+               free(data->files[i]);
+       }
+       free(data->files);
+       printf("%s %d files changed, %d insertions(+), %d deletions(-)%s\n",
+              set, total_files, adds, dels, reset);
+}
+
+static void show_shortstats(struct diffstat_t* data)
+{
+       int i, adds = 0, dels = 0, total_files = data->nr;
+
+       if (data->nr == 0)
+               return;
+
+       for (i = 0; i < data->nr; i++) {
+               if (!data->files[i]->is_binary &&
+                   !data->files[i]->is_unmerged) {
+                       int added = data->files[i]->added;
+                       int deleted= data->files[i]->deleted;
+                       if (!data->files[i]->is_renamed &&
+                           (added + deleted == 0)) {
+                               total_files--;
+                       } else {
+                               adds += added;
+                               dels += deleted;
+                       }
+               }
+               free(data->files[i]->name);
+               free(data->files[i]);
+       }
+       free(data->files);
+
+       printf(" %d files changed, %d insertions(+), %d deletions(-)\n",
+              total_files, adds, dels);
+}
+
+static void show_numstat(struct diffstat_t* data, struct diff_options *options)
+{
+       int i;
+
+       for (i = 0; i < data->nr; i++) {
+               struct diffstat_file *file = data->files[i];
+
+               if (file->is_binary)
+                       printf("-\t-\t");
+               else
+                       printf("%d\t%d\t", file->added, file->deleted);
+               if (!file->is_renamed) {
+                       write_name_quoted(file->name, stdout, options->line_termination);
+               } else {
+                       fputs(file->name, stdout);
+                       putchar(options->line_termination);
+               }
+       }
+}
+
+struct checkdiff_t {
+       struct xdiff_emit_state xm;
+       const char *filename;
+       int lineno, color_diff;
+};
+
+static void checkdiff_consume(void *priv, char *line, unsigned long len)
+{
+       struct checkdiff_t *data = priv;
+       const char *ws = diff_get_color(data->color_diff, DIFF_WHITESPACE);
+       const char *reset = diff_get_color(data->color_diff, DIFF_RESET);
+       const char *set = diff_get_color(data->color_diff, DIFF_FILE_NEW);
+
+       if (line[0] == '+') {
+               int i, spaces = 0, space_before_tab = 0, white_space_at_end = 0;
+
+               /* check space before tab */
+               for (i = 1; i < len && (line[i] == ' ' || line[i] == '\t'); i++)
+                       if (line[i] == ' ')
+                               spaces++;
+               if (line[i - 1] == '\t' && spaces)
+                       space_before_tab = 1;
+
+               /* check white space at line end */
+               if (line[len - 1] == '\n')
+                       len--;
+               if (isspace(line[len - 1]))
+                       white_space_at_end = 1;
+
+               if (space_before_tab || white_space_at_end) {
+                       printf("%s:%d: %s", data->filename, data->lineno, ws);
+                       if (space_before_tab) {
+                               printf("space before tab");
+                               if (white_space_at_end)
+                                       putchar(',');
+                       }
+                       if (white_space_at_end)
+                               printf("white space at end");
+                       printf(":%s ", reset);
+                       emit_line_with_ws(1, set, reset, ws, line, len);
+               }
+
+               data->lineno++;
+       } else if (line[0] == ' ')
+               data->lineno++;
+       else if (line[0] == '@') {
+               char *plus = strchr(line, '+');
+               if (plus)
+                       data->lineno = strtol(plus, NULL, 10);
+               else
+                       die("invalid diff");
+       }
+}
+
+static unsigned char *deflate_it(char *data,
+                                unsigned long size,
+                                unsigned long *result_size)
+{
+       int bound;
+       unsigned char *deflated;
+       z_stream stream;
+
+       memset(&stream, 0, sizeof(stream));
+       deflateInit(&stream, zlib_compression_level);
+       bound = deflateBound(&stream, size);
+       deflated = xmalloc(bound);
+       stream.next_out = deflated;
+       stream.avail_out = bound;
+
+       stream.next_in = (unsigned char *)data;
+       stream.avail_in = size;
+       while (deflate(&stream, Z_FINISH) == Z_OK)
+               ; /* nothing */
+       deflateEnd(&stream);
+       *result_size = stream.total_out;
+       return deflated;
+}
+
+static void emit_binary_diff_body(mmfile_t *one, mmfile_t *two)
+{
+       void *cp;
+       void *delta;
+       void *deflated;
+       void *data;
+       unsigned long orig_size;
+       unsigned long delta_size;
+       unsigned long deflate_size;
+       unsigned long data_size;
+
+       /* We could do deflated delta, or we could do just deflated two,
+        * whichever is smaller.
+        */
+       delta = NULL;
+       deflated = deflate_it(two->ptr, two->size, &deflate_size);
+       if (one->size && two->size) {
+               delta = diff_delta(one->ptr, one->size,
+                                  two->ptr, two->size,
+                                  &delta_size, deflate_size);
+               if (delta) {
+                       void *to_free = delta;
+                       orig_size = delta_size;
+                       delta = deflate_it(delta, delta_size, &delta_size);
+                       free(to_free);
+               }
+       }
+
+       if (delta && delta_size < deflate_size) {
+               printf("delta %lu\n", orig_size);
+               free(deflated);
+               data = delta;
+               data_size = delta_size;
+       }
+       else {
+               printf("literal %lu\n", two->size);
+               free(delta);
+               data = deflated;
+               data_size = deflate_size;
+       }
+
+       /* emit data encoded in base85 */
+       cp = data;
+       while (data_size) {
+               int bytes = (52 < data_size) ? 52 : data_size;
+               char line[70];
+               data_size -= bytes;
+               if (bytes <= 26)
+                       line[0] = bytes + 'A' - 1;
+               else
+                       line[0] = bytes - 26 + 'a' - 1;
+               encode_85(line + 1, cp, bytes);
+               cp = (char *) cp + bytes;
+               puts(line);
+       }
+       printf("\n");
+       free(data);
+}
+
+static void emit_binary_diff(mmfile_t *one, mmfile_t *two)
+{
+       printf("GIT binary patch\n");
+       emit_binary_diff_body(one, two);
+       emit_binary_diff_body(two, one);
+}
+
+static void setup_diff_attr_check(struct git_attr_check *check)
+{
+       static struct git_attr *attr_diff;
+
+       if (!attr_diff) {
+               attr_diff = git_attr("diff", 4);
+       }
+       check[0].attr = attr_diff;
+}
+
+static void diff_filespec_check_attr(struct diff_filespec *one)
+{
+       struct git_attr_check attr_diff_check;
+       int check_from_data = 0;
+
+       if (one->checked_attr)
+               return;
+
+       setup_diff_attr_check(&attr_diff_check);
+       one->is_binary = 0;
+       one->funcname_pattern_ident = NULL;
+
+       if (!git_checkattr(one->path, 1, &attr_diff_check)) {
+               const char *value;
+
+               /* binaryness */
+               value = attr_diff_check.value;
+               if (ATTR_TRUE(value))
+                       ;
+               else if (ATTR_FALSE(value))
+                       one->is_binary = 1;
+               else
+                       check_from_data = 1;
+
+               /* funcname pattern ident */
+               if (ATTR_TRUE(value) || ATTR_FALSE(value) || ATTR_UNSET(value))
+                       ;
+               else
+                       one->funcname_pattern_ident = value;
+       }
+
+       if (check_from_data) {
+               if (!one->data && DIFF_FILE_VALID(one))
+                       diff_populate_filespec(one, 0);
+
+               if (one->data)
+                       one->is_binary = buffer_is_binary(one->data, one->size);
+       }
+}
+
+int diff_filespec_is_binary(struct diff_filespec *one)
+{
+       diff_filespec_check_attr(one);
+       return one->is_binary;
+}
+
+static const char *funcname_pattern(const char *ident)
+{
+       struct funcname_pattern *pp;
+
+       read_config_if_needed();
+       for (pp = funcname_pattern_list; pp; pp = pp->next)
+               if (!strcmp(ident, pp->name))
+                       return pp->pattern;
+       return NULL;
+}
+
+static struct builtin_funcname_pattern {
+       const char *name;
+       const char *pattern;
+} builtin_funcname_pattern[] = {
+       { "java", "!^[  ]*\\(catch\\|do\\|for\\|if\\|instanceof\\|"
+                       "new\\|return\\|switch\\|throw\\|while\\)\n"
+                       "^[     ]*\\(\\([       ]*"
+                       "[A-Za-z_][A-Za-z_0-9]*\\)\\{2,\\}"
+                       "[      ]*([^;]*$\\)" },
+       { "tex", "^\\(\\\\\\(sub\\)*section{.*\\)$" },
+};
+
+static const char *diff_funcname_pattern(struct diff_filespec *one)
+{
+       const char *ident, *pattern;
+       int i;
+
+       diff_filespec_check_attr(one);
+       ident = one->funcname_pattern_ident;
+
+       if (!ident)
+               /*
+                * If the config file has "funcname.default" defined, that
+                * regexp is used; otherwise NULL is returned and xemit uses
+                * the built-in default.
+                */
+               return funcname_pattern("default");
+
+       /* Look up custom "funcname.$ident" regexp from config. */
+       pattern = funcname_pattern(ident);
+       if (pattern)
+               return pattern;
+
+       /*
+        * And define built-in fallback patterns here.  Note that
+        * these can be overriden by the user's config settings.
+        */
+       for (i = 0; i < ARRAY_SIZE(builtin_funcname_pattern); i++)
+               if (!strcmp(ident, builtin_funcname_pattern[i].name))
+                       return builtin_funcname_pattern[i].pattern;
+
+       return NULL;
+}
+
+static void builtin_diff(const char *name_a,
+                        const char *name_b,
+                        struct diff_filespec *one,
+                        struct diff_filespec *two,
+                        const char *xfrm_msg,
+                        struct diff_options *o,
+                        int complete_rewrite)
+{
+       mmfile_t mf1, mf2;
+       const char *lbl[2];
+       char *a_one, *b_two;
+       const char *set = diff_get_color_opt(o, DIFF_METAINFO);
+       const char *reset = diff_get_color_opt(o, DIFF_RESET);
+
+       a_one = quote_two("a/", name_a + (*name_a == '/'));
+       b_two = quote_two("b/", name_b + (*name_b == '/'));
+       lbl[0] = DIFF_FILE_VALID(one) ? a_one : "/dev/null";
+       lbl[1] = DIFF_FILE_VALID(two) ? b_two : "/dev/null";
+       printf("%sdiff --git %s %s%s\n", set, a_one, b_two, reset);
+       if (lbl[0][0] == '/') {
+               /* /dev/null */
+               printf("%snew file mode %06o%s\n", set, two->mode, reset);
+               if (xfrm_msg && xfrm_msg[0])
+                       printf("%s%s%s\n", set, xfrm_msg, reset);
+       }
+       else if (lbl[1][0] == '/') {
+               printf("%sdeleted file mode %06o%s\n", set, one->mode, reset);
+               if (xfrm_msg && xfrm_msg[0])
+                       printf("%s%s%s\n", set, xfrm_msg, reset);
+       }
+       else {
+               if (one->mode != two->mode) {
+                       printf("%sold mode %06o%s\n", set, one->mode, reset);
+                       printf("%snew mode %06o%s\n", set, two->mode, reset);
+               }
+               if (xfrm_msg && xfrm_msg[0])
+                       printf("%s%s%s\n", set, xfrm_msg, reset);
+               /*
+                * we do not run diff between different kind
+                * of objects.
+                */
+               if ((one->mode ^ two->mode) & S_IFMT)
+                       goto free_ab_and_return;
+               if (complete_rewrite) {
+                       emit_rewrite_diff(name_a, name_b, one, two,
+                                       DIFF_OPT_TST(o, COLOR_DIFF));
+                       o->found_changes = 1;
+                       goto free_ab_and_return;
+               }
+       }
+
+       if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
+               die("unable to read files to diff");
+
+       if (!DIFF_OPT_TST(o, TEXT) &&
+           (diff_filespec_is_binary(one) || diff_filespec_is_binary(two))) {
+               /* Quite common confusing case */
+               if (mf1.size == mf2.size &&
+                   !memcmp(mf1.ptr, mf2.ptr, mf1.size))
+                       goto free_ab_and_return;
+               if (DIFF_OPT_TST(o, BINARY))
+                       emit_binary_diff(&mf1, &mf2);
+               else
+                       printf("Binary files %s and %s differ\n",
+                              lbl[0], lbl[1]);
+               o->found_changes = 1;
+       }
+       else {
+               /* Crazy xdl interfaces.. */
+               const char *diffopts = getenv("GIT_DIFF_OPTS");
+               xpparam_t xpp;
+               xdemitconf_t xecfg;
+               xdemitcb_t ecb;
+               struct emit_callback ecbdata;
+               const char *funcname_pattern;
+
+               funcname_pattern = diff_funcname_pattern(one);
+               if (!funcname_pattern)
+                       funcname_pattern = diff_funcname_pattern(two);
+
+               memset(&xecfg, 0, sizeof(xecfg));
+               memset(&ecbdata, 0, sizeof(ecbdata));
+               ecbdata.label_path = lbl;
+               ecbdata.color_diff = DIFF_OPT_TST(o, COLOR_DIFF);
+               ecbdata.found_changesp = &o->found_changes;
+               xpp.flags = XDF_NEED_MINIMAL | o->xdl_opts;
+               xecfg.ctxlen = o->context;
+               xecfg.flags = XDL_EMIT_FUNCNAMES;
+               if (funcname_pattern)
+                       xdiff_set_find_func(&xecfg, funcname_pattern);
+               if (!diffopts)
+                       ;
+               else if (!prefixcmp(diffopts, "--unified="))
+                       xecfg.ctxlen = strtoul(diffopts + 10, NULL, 10);
+               else if (!prefixcmp(diffopts, "-u"))
+                       xecfg.ctxlen = strtoul(diffopts + 2, NULL, 10);
+               ecb.outf = xdiff_outf;
+               ecb.priv = &ecbdata;
+               ecbdata.xm.consume = fn_out_consume;
+               if (DIFF_OPT_TST(o, COLOR_DIFF_WORDS))
+                       ecbdata.diff_words =
+                               xcalloc(1, sizeof(struct diff_words_data));
+               xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb);
+               if (DIFF_OPT_TST(o, COLOR_DIFF_WORDS))
+                       free_diff_words_data(&ecbdata);
+       }
+
+ free_ab_and_return:
+       diff_free_filespec_data(one);
+       diff_free_filespec_data(two);
+       free(a_one);
+       free(b_two);
+       return;
+}
+
+static void builtin_diffstat(const char *name_a, const char *name_b,
+                            struct diff_filespec *one,
+                            struct diff_filespec *two,
+                            struct diffstat_t *diffstat,
+                            struct diff_options *o,
+                            int complete_rewrite)
+{
+       mmfile_t mf1, mf2;
+       struct diffstat_file *data;
+
+       data = diffstat_add(diffstat, name_a, name_b);
+
+       if (!one || !two) {
+               data->is_unmerged = 1;
+               return;
+       }
+       if (complete_rewrite) {
+               diff_populate_filespec(one, 0);
+               diff_populate_filespec(two, 0);
+               data->deleted = count_lines(one->data, one->size);
+               data->added = count_lines(two->data, two->size);
+               goto free_and_return;
+       }
+       if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
+               die("unable to read files to diff");
+
+       if (diff_filespec_is_binary(one) || diff_filespec_is_binary(two)) {
+               data->is_binary = 1;
+               data->added = mf2.size;
+               data->deleted = mf1.size;
+       } else {
+               /* Crazy xdl interfaces.. */
+               xpparam_t xpp;
+               xdemitconf_t xecfg;
+               xdemitcb_t ecb;
+
+               memset(&xecfg, 0, sizeof(xecfg));
+               xpp.flags = XDF_NEED_MINIMAL | o->xdl_opts;
+               ecb.outf = xdiff_outf;
+               ecb.priv = diffstat;
+               xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb);
+       }
+
+ free_and_return:
+       diff_free_filespec_data(one);
+       diff_free_filespec_data(two);
+}
+
+static void builtin_checkdiff(const char *name_a, const char *name_b,
+                            struct diff_filespec *one,
+                            struct diff_filespec *two, struct diff_options *o)
+{
+       mmfile_t mf1, mf2;
+       struct checkdiff_t data;
+
+       if (!two)
+               return;
+
+       memset(&data, 0, sizeof(data));
+       data.xm.consume = checkdiff_consume;
+       data.filename = name_b ? name_b : name_a;
+       data.lineno = 0;
+       data.color_diff = DIFF_OPT_TST(o, COLOR_DIFF);
+
+       if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
+               die("unable to read files to diff");
+
+       if (diff_filespec_is_binary(two))
+               goto free_and_return;
+       else {
+               /* Crazy xdl interfaces.. */
+               xpparam_t xpp;
+               xdemitconf_t xecfg;
+               xdemitcb_t ecb;
+
+               memset(&xecfg, 0, sizeof(xecfg));
+               xpp.flags = XDF_NEED_MINIMAL;
+               ecb.outf = xdiff_outf;
+               ecb.priv = &data;
+               xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb);
+       }
+ free_and_return:
+       diff_free_filespec_data(one);
+       diff_free_filespec_data(two);
+}
+
+struct diff_filespec *alloc_filespec(const char *path)
+{
+       int namelen = strlen(path);
+       struct diff_filespec *spec = xmalloc(sizeof(*spec) + namelen + 1);
+
+       memset(spec, 0, sizeof(*spec));
+       spec->path = (char *)(spec + 1);
+       memcpy(spec->path, path, namelen+1);
+       spec->count = 1;
+       return spec;
+}
+
+void free_filespec(struct diff_filespec *spec)
+{
+       if (!--spec->count) {
+               diff_free_filespec_data(spec);
+               free(spec);
+       }
+}
+
+void fill_filespec(struct diff_filespec *spec, const unsigned char *sha1,
+                  unsigned short mode)
+{
+       if (mode) {
+               spec->mode = canon_mode(mode);
+               hashcpy(spec->sha1, sha1);
+               spec->sha1_valid = !is_null_sha1(sha1);
+       }
+}
+
+/*
+ * Given a name and sha1 pair, if the index tells us the file in
+ * the work tree has that object contents, return true, so that
+ * prepare_temp_file() does not have to inflate and extract.
+ */
+static int reuse_worktree_file(const char *name, const unsigned char *sha1, int want_file)
+{
+       struct cache_entry *ce;
+       struct stat st;
+       int pos, len;
+
+       /* We do not read the cache ourselves here, because the
+        * benchmark with my previous version that always reads cache
+        * shows that it makes things worse for diff-tree comparing
+        * two linux-2.6 kernel trees in an already checked out work
+        * tree.  This is because most diff-tree comparisons deal with
+        * only a small number of files, while reading the cache is
+        * expensive for a large project, and its cost outweighs the
+        * savings we get by not inflating the object to a temporary
+        * file.  Practically, this code only helps when we are used
+        * by diff-cache --cached, which does read the cache before
+        * calling us.
+        */
+       if (!active_cache)
+               return 0;
+
+       /* We want to avoid the working directory if our caller
+        * doesn't need the data in a normal file, this system
+        * is rather slow with its stat/open/mmap/close syscalls,
+        * and the object is contained in a pack file.  The pack
+        * is probably already open and will be faster to obtain
+        * the data through than the working directory.  Loose
+        * objects however would tend to be slower as they need
+        * to be individually opened and inflated.
+        */
+       if (!FAST_WORKING_DIRECTORY && !want_file && has_sha1_pack(sha1, NULL))
+               return 0;
+
+       len = strlen(name);
+       pos = cache_name_pos(name, len);
+       if (pos < 0)
+               return 0;
+       ce = active_cache[pos];
+       if ((lstat(name, &st) < 0) ||
+           !S_ISREG(st.st_mode) || /* careful! */
+           ce_match_stat(ce, &st, 0) ||
+           hashcmp(sha1, ce->sha1))
+               return 0;
+       /* we return 1 only when we can stat, it is a regular file,
+        * stat information matches, and sha1 recorded in the cache
+        * matches.  I.e. we know the file in the work tree really is
+        * the same as the <name, sha1> pair.
+        */
+       return 1;
+}
+
+static int populate_from_stdin(struct diff_filespec *s)
+{
+       struct strbuf buf;
+       size_t size = 0;
+
+       strbuf_init(&buf, 0);
+       if (strbuf_read(&buf, 0, 0) < 0)
+               return error("error while reading from stdin %s",
+                                    strerror(errno));
+
+       s->should_munmap = 0;
+       s->data = strbuf_detach(&buf, &size);
+       s->size = size;
+       s->should_free = 1;
+       return 0;
+}
+
+static int diff_populate_gitlink(struct diff_filespec *s, int size_only)
+{
+       int len;
+       char *data = xmalloc(100);
+       len = snprintf(data, 100,
+               "Subproject commit %s\n", sha1_to_hex(s->sha1));
+       s->data = data;
+       s->size = len;
+       s->should_free = 1;
+       if (size_only) {
+               s->data = NULL;
+               free(data);
+       }
+       return 0;
+}
+
+/*
+ * While doing rename detection and pickaxe operation, we may need to
+ * grab the data for the blob (or file) for our own in-core comparison.
+ * diff_filespec has data and size fields for this purpose.
+ */
+int diff_populate_filespec(struct diff_filespec *s, int size_only)
+{
+       int err = 0;
+       if (!DIFF_FILE_VALID(s))
+               die("internal error: asking to populate invalid file.");
+       if (S_ISDIR(s->mode))
+               return -1;
+
+       if (s->data)
+               return 0;
+
+       if (size_only && 0 < s->size)
+               return 0;
+
+       if (S_ISGITLINK(s->mode))
+               return diff_populate_gitlink(s, size_only);
+
+       if (!s->sha1_valid ||
+           reuse_worktree_file(s->path, s->sha1, 0)) {
+               struct strbuf buf;
+               struct stat st;
+               int fd;
+
+               if (!strcmp(s->path, "-"))
+                       return populate_from_stdin(s);
+
+               if (lstat(s->path, &st) < 0) {
+                       if (errno == ENOENT) {
+                       err_empty:
+                               err = -1;
+                       empty:
+                               s->data = (char *)"";
+                               s->size = 0;
+                               return err;
+                       }
+               }
+               s->size = xsize_t(st.st_size);
+               if (!s->size)
+                       goto empty;
+               if (size_only)
+                       return 0;
+               if (S_ISLNK(st.st_mode)) {
+                       int ret;
+                       s->data = xmalloc(s->size);
+                       s->should_free = 1;
+                       ret = readlink(s->path, s->data, s->size);
+                       if (ret < 0) {
+                               free(s->data);
+                               goto err_empty;
+                       }
+                       return 0;
+               }
+               fd = open(s->path, O_RDONLY);
+               if (fd < 0)
+                       goto err_empty;
+               s->data = xmmap(NULL, s->size, PROT_READ, MAP_PRIVATE, fd, 0);
+               close(fd);
+               s->should_munmap = 1;
+
+               /*
+                * Convert from working tree format to canonical git format
+                */
+               strbuf_init(&buf, 0);
+               if (convert_to_git(s->path, s->data, s->size, &buf)) {
+                       size_t size = 0;
+                       munmap(s->data, s->size);
+                       s->should_munmap = 0;
+                       s->data = strbuf_detach(&buf, &size);
+                       s->size = size;
+                       s->should_free = 1;
+               }
+       }
+       else {
+               enum object_type type;
+               if (size_only)
+                       type = sha1_object_info(s->sha1, &s->size);
+               else {
+                       s->data = read_sha1_file(s->sha1, &type, &s->size);
+                       s->should_free = 1;
+               }
+       }
+       return 0;
+}
+
+void diff_free_filespec_blob(struct diff_filespec *s)
+{
+       if (s->should_free)
+               free(s->data);
+       else if (s->should_munmap)
+               munmap(s->data, s->size);
+
+       if (s->should_free || s->should_munmap) {
+               s->should_free = s->should_munmap = 0;
+               s->data = NULL;
+       }
+}
+
+void diff_free_filespec_data(struct diff_filespec *s)
+{
+       diff_free_filespec_blob(s);
+       free(s->cnt_data);
+       s->cnt_data = NULL;
+}
+
+static void prep_temp_blob(struct diff_tempfile *temp,
+                          void *blob,
+                          unsigned long size,
+                          const unsigned char *sha1,
+                          int mode)
+{
+       int fd;
+
+       fd = git_mkstemp(temp->tmp_path, PATH_MAX, ".diff_XXXXXX");
+       if (fd < 0)
+               die("unable to create temp-file: %s", strerror(errno));
+       if (write_in_full(fd, blob, size) != size)
+               die("unable to write temp-file");
+       close(fd);
+       temp->name = temp->tmp_path;
+       strcpy(temp->hex, sha1_to_hex(sha1));
+       temp->hex[40] = 0;
+       sprintf(temp->mode, "%06o", mode);
+}
+
+static void prepare_temp_file(const char *name,
+                             struct diff_tempfile *temp,
+                             struct diff_filespec *one)
+{
+       if (!DIFF_FILE_VALID(one)) {
+       not_a_valid_file:
+               /* A '-' entry produces this for file-2, and
+                * a '+' entry produces this for file-1.
+                */
+               temp->name = "/dev/null";
+               strcpy(temp->hex, ".");
+               strcpy(temp->mode, ".");
+               return;
+       }
+
+       if (!one->sha1_valid ||
+           reuse_worktree_file(name, one->sha1, 1)) {
+               struct stat st;
+               if (lstat(name, &st) < 0) {
+                       if (errno == ENOENT)
+                               goto not_a_valid_file;
+                       die("stat(%s): %s", name, strerror(errno));
+               }
+               if (S_ISLNK(st.st_mode)) {
+                       int ret;
+                       char buf[PATH_MAX + 1]; /* ought to be SYMLINK_MAX */
+                       size_t sz = xsize_t(st.st_size);
+                       if (sizeof(buf) <= st.st_size)
+                               die("symlink too long: %s", name);
+                       ret = readlink(name, buf, sz);
+                       if (ret < 0)
+                               die("readlink(%s)", name);
+                       prep_temp_blob(temp, buf, sz,
+                                      (one->sha1_valid ?
+                                       one->sha1 : null_sha1),
+                                      (one->sha1_valid ?
+                                       one->mode : S_IFLNK));
+               }
+               else {
+                       /* we can borrow from the file in the work tree */
+                       temp->name = name;
+                       if (!one->sha1_valid)
+                               strcpy(temp->hex, sha1_to_hex(null_sha1));
+                       else
+                               strcpy(temp->hex, sha1_to_hex(one->sha1));
+                       /* Even though we may sometimes borrow the
+                        * contents from the work tree, we always want
+                        * one->mode.  mode is trustworthy even when
+                        * !(one->sha1_valid), as long as
+                        * DIFF_FILE_VALID(one).
+                        */
+                       sprintf(temp->mode, "%06o", one->mode);
+               }
+               return;
+       }
+       else {
+               if (diff_populate_filespec(one, 0))
+                       die("cannot read data blob for %s", one->path);
+               prep_temp_blob(temp, one->data, one->size,
+                              one->sha1, one->mode);
+       }
+}
+
+static void remove_tempfile(void)
+{
+       int i;
+
+       for (i = 0; i < 2; i++)
+               if (diff_temp[i].name == diff_temp[i].tmp_path) {
+                       unlink(diff_temp[i].name);
+                       diff_temp[i].name = NULL;
+               }
+}
+
+static void remove_tempfile_on_signal(int signo)
+{
+       remove_tempfile();
+       signal(SIGINT, SIG_DFL);
+       raise(signo);
+}
+
+/* An external diff command takes:
+ *
+ * diff-cmd name infile1 infile1-sha1 infile1-mode \
+ *               infile2 infile2-sha1 infile2-mode [ rename-to ]
+ *
+ */
+static void run_external_diff(const char *pgm,
+                             const char *name,
+                             const char *other,
+                             struct diff_filespec *one,
+                             struct diff_filespec *two,
+                             const char *xfrm_msg,
+                             int complete_rewrite)
+{
+       const char *spawn_arg[10];
+       struct diff_tempfile *temp = diff_temp;
+       int retval;
+       static int atexit_asked = 0;
+       const char *othername;
+       const char **arg = &spawn_arg[0];
+
+       othername = (other? other : name);
+       if (one && two) {
+               prepare_temp_file(name, &temp[0], one);
+               prepare_temp_file(othername, &temp[1], two);
+               if (! atexit_asked &&
+                   (temp[0].name == temp[0].tmp_path ||
+                    temp[1].name == temp[1].tmp_path)) {
+                       atexit_asked = 1;
+                       atexit(remove_tempfile);
+               }
+               signal(SIGINT, remove_tempfile_on_signal);
+       }
+
+       if (one && two) {
+               *arg++ = pgm;
+               *arg++ = name;
+               *arg++ = temp[0].name;
+               *arg++ = temp[0].hex;
+               *arg++ = temp[0].mode;
+               *arg++ = temp[1].name;
+               *arg++ = temp[1].hex;
+               *arg++ = temp[1].mode;
+               if (other) {
+                       *arg++ = other;
+                       *arg++ = xfrm_msg;
+               }
+       } else {
+               *arg++ = pgm;
+               *arg++ = name;
+       }
+       *arg = NULL;
+       fflush(NULL);
+       retval = run_command_v_opt(spawn_arg, 0);
+       remove_tempfile();
+       if (retval) {
+               fprintf(stderr, "external diff died, stopping at %s.\n", name);
+               exit(1);
+       }
+}
+
+static const char *external_diff_attr(const char *name)
+{
+       struct git_attr_check attr_diff_check;
+
+       setup_diff_attr_check(&attr_diff_check);
+       if (!git_checkattr(name, 1, &attr_diff_check)) {
+               const char *value = attr_diff_check.value;
+               if (!ATTR_TRUE(value) &&
+                   !ATTR_FALSE(value) &&
+                   !ATTR_UNSET(value)) {
+                       struct ll_diff_driver *drv;
+
+                       read_config_if_needed();
+                       for (drv = user_diff; drv; drv = drv->next)
+                               if (!strcmp(drv->name, value))
+                                       return drv->cmd;
+               }
+       }
+       return NULL;
+}
+
+static void run_diff_cmd(const char *pgm,
+                        const char *name,
+                        const char *other,
+                        struct diff_filespec *one,
+                        struct diff_filespec *two,
+                        const char *xfrm_msg,
+                        struct diff_options *o,
+                        int complete_rewrite)
+{
+       if (!DIFF_OPT_TST(o, ALLOW_EXTERNAL))
+               pgm = NULL;
+       else {
+               const char *cmd = external_diff_attr(name);
+               if (cmd)
+                       pgm = cmd;
+       }
+
+       if (pgm) {
+               run_external_diff(pgm, name, other, one, two, xfrm_msg,
+                                 complete_rewrite);
+               return;
+       }
+       if (one && two)
+               builtin_diff(name, other ? other : name,
+                            one, two, xfrm_msg, o, complete_rewrite);
+       else
+               printf("* Unmerged path %s\n", name);
+}
+
+static void diff_fill_sha1_info(struct diff_filespec *one)
+{
+       if (DIFF_FILE_VALID(one)) {
+               if (!one->sha1_valid) {
+                       struct stat st;
+                       if (!strcmp(one->path, "-")) {
+                               hashcpy(one->sha1, null_sha1);
+                               return;
+                       }
+                       if (lstat(one->path, &st) < 0)
+                               die("stat %s", one->path);
+                       if (index_path(one->sha1, one->path, &st, 0))
+                               die("cannot hash %s\n", one->path);
+               }
+       }
+       else
+               hashclr(one->sha1);
+}
+
+static int similarity_index(struct diff_filepair *p)
+{
+       return p->score * 100 / MAX_SCORE;
+}
+
+static void run_diff(struct diff_filepair *p, struct diff_options *o)
+{
+       const char *pgm = external_diff();
+       struct strbuf msg;
+       char *xfrm_msg;
+       struct diff_filespec *one = p->one;
+       struct diff_filespec *two = p->two;
+       const char *name;
+       const char *other;
+       int complete_rewrite = 0;
+
+
+       if (DIFF_PAIR_UNMERGED(p)) {
+               run_diff_cmd(pgm, p->one->path, NULL, NULL, NULL, NULL, o, 0);
+               return;
+       }
+
+       name  = p->one->path;
+       other = (strcmp(name, p->two->path) ? p->two->path : NULL);
+       diff_fill_sha1_info(one);
+       diff_fill_sha1_info(two);
+
+       strbuf_init(&msg, PATH_MAX * 2 + 300);
+       switch (p->status) {
+       case DIFF_STATUS_COPIED:
+               strbuf_addf(&msg, "similarity index %d%%", similarity_index(p));
+               strbuf_addstr(&msg, "\ncopy from ");
+               quote_c_style(name, &msg, NULL, 0);
+               strbuf_addstr(&msg, "\ncopy to ");
+               quote_c_style(other, &msg, NULL, 0);
+               strbuf_addch(&msg, '\n');
+               break;
+       case DIFF_STATUS_RENAMED:
+               strbuf_addf(&msg, "similarity index %d%%", similarity_index(p));
+               strbuf_addstr(&msg, "\nrename from ");
+               quote_c_style(name, &msg, NULL, 0);
+               strbuf_addstr(&msg, "\nrename to ");
+               quote_c_style(other, &msg, NULL, 0);
+               strbuf_addch(&msg, '\n');
+               break;
+       case DIFF_STATUS_MODIFIED:
+               if (p->score) {
+                       strbuf_addf(&msg, "dissimilarity index %d%%\n",
+                                       similarity_index(p));
+                       complete_rewrite = 1;
+                       break;
+               }
+               /* fallthru */
+       default:
+               /* nothing */
+               ;
+       }
+
+       if (hashcmp(one->sha1, two->sha1)) {
+               int abbrev = DIFF_OPT_TST(o, FULL_INDEX) ? 40 : DEFAULT_ABBREV;
+
+               if (DIFF_OPT_TST(o, BINARY)) {
+                       mmfile_t mf;
+                       if ((!fill_mmfile(&mf, one) && diff_filespec_is_binary(one)) ||
+                           (!fill_mmfile(&mf, two) && diff_filespec_is_binary(two)))
+                               abbrev = 40;
+               }
+               strbuf_addf(&msg, "index %.*s..%.*s",
+                               abbrev, sha1_to_hex(one->sha1),
+                               abbrev, sha1_to_hex(two->sha1));
+               if (one->mode == two->mode)
+                       strbuf_addf(&msg, " %06o", one->mode);
+               strbuf_addch(&msg, '\n');
+       }
+
+       if (msg.len)
+               strbuf_setlen(&msg, msg.len - 1);
+       xfrm_msg = msg.len ? msg.buf : NULL;
+
+       if (!pgm &&
+           DIFF_FILE_VALID(one) && DIFF_FILE_VALID(two) &&
+           (S_IFMT & one->mode) != (S_IFMT & two->mode)) {
+               /* a filepair that changes between file and symlink
+                * needs to be split into deletion and creation.
+                */
+               struct diff_filespec *null = alloc_filespec(two->path);
+               run_diff_cmd(NULL, name, other, one, null, xfrm_msg, o, 0);
+               free(null);
+               null = alloc_filespec(one->path);
+               run_diff_cmd(NULL, name, other, null, two, xfrm_msg, o, 0);
+               free(null);
+       }
+       else
+               run_diff_cmd(pgm, name, other, one, two, xfrm_msg, o,
+                            complete_rewrite);
+
+       strbuf_release(&msg);
+}
+
+static void run_diffstat(struct diff_filepair *p, struct diff_options *o,
+                        struct diffstat_t *diffstat)
+{
+       const char *name;
+       const char *other;
+       int complete_rewrite = 0;
+
+       if (DIFF_PAIR_UNMERGED(p)) {
+               /* unmerged */
+               builtin_diffstat(p->one->path, NULL, NULL, NULL, diffstat, o, 0);
+               return;
+       }
+
+       name = p->one->path;
+       other = (strcmp(name, p->two->path) ? p->two->path : NULL);
+
+       diff_fill_sha1_info(p->one);
+       diff_fill_sha1_info(p->two);
+
+       if (p->status == DIFF_STATUS_MODIFIED && p->score)
+               complete_rewrite = 1;
+       builtin_diffstat(name, other, p->one, p->two, diffstat, o, complete_rewrite);
+}
+
+static void run_checkdiff(struct diff_filepair *p, struct diff_options *o)
+{
+       const char *name;
+       const char *other;
+
+       if (DIFF_PAIR_UNMERGED(p)) {
+               /* unmerged */
+               return;
+       }
+
+       name = p->one->path;
+       other = (strcmp(name, p->two->path) ? p->two->path : NULL);
+
+       diff_fill_sha1_info(p->one);
+       diff_fill_sha1_info(p->two);
+
+       builtin_checkdiff(name, other, p->one, p->two, o);
+}
+
+void diff_setup(struct diff_options *options)
+{
+       memset(options, 0, sizeof(*options));
+       options->line_termination = '\n';
+       options->break_opt = -1;
+       options->rename_limit = -1;
+       options->context = 3;
+       options->msg_sep = "";
+
+       options->change = diff_change;
+       options->add_remove = diff_addremove;
+       if (diff_use_color_default)
+               DIFF_OPT_SET(options, COLOR_DIFF);
+       else
+               DIFF_OPT_CLR(options, COLOR_DIFF);
+       options->detect_rename = diff_detect_rename_default;
+}
+
+int diff_setup_done(struct diff_options *options)
+{
+       int count = 0;
+
+       if (options->output_format & DIFF_FORMAT_NAME)
+               count++;
+       if (options->output_format & DIFF_FORMAT_NAME_STATUS)
+               count++;
+       if (options->output_format & DIFF_FORMAT_CHECKDIFF)
+               count++;
+       if (options->output_format & DIFF_FORMAT_NO_OUTPUT)
+               count++;
+       if (count > 1)
+               die("--name-only, --name-status, --check and -s are mutually exclusive");
+
+       if (DIFF_OPT_TST(options, FIND_COPIES_HARDER))
+               options->detect_rename = DIFF_DETECT_COPY;
+
+       if (options->output_format & (DIFF_FORMAT_NAME |
+                                     DIFF_FORMAT_NAME_STATUS |
+                                     DIFF_FORMAT_CHECKDIFF |
+                                     DIFF_FORMAT_NO_OUTPUT))
+               options->output_format &= ~(DIFF_FORMAT_RAW |
+                                           DIFF_FORMAT_NUMSTAT |
+                                           DIFF_FORMAT_DIFFSTAT |
+                                           DIFF_FORMAT_SHORTSTAT |
+                                           DIFF_FORMAT_SUMMARY |
+                                           DIFF_FORMAT_PATCH);
+
+       /*
+        * These cases always need recursive; we do not drop caller-supplied
+        * recursive bits for other formats here.
+        */
+       if (options->output_format & (DIFF_FORMAT_PATCH |
+                                     DIFF_FORMAT_NUMSTAT |
+                                     DIFF_FORMAT_DIFFSTAT |
+                                     DIFF_FORMAT_SHORTSTAT |
+                                     DIFF_FORMAT_SUMMARY |
+                                     DIFF_FORMAT_CHECKDIFF))
+               DIFF_OPT_SET(options, RECURSIVE);
+       /*
+        * Also pickaxe would not work very well if you do not say recursive
+        */
+       if (options->pickaxe)
+               DIFF_OPT_SET(options, RECURSIVE);
+
+       if (options->detect_rename && options->rename_limit < 0)
+               options->rename_limit = diff_rename_limit_default;
+       if (options->setup & DIFF_SETUP_USE_CACHE) {
+               if (!active_cache)
+                       /* read-cache does not die even when it fails
+                        * so it is safe for us to do this here.  Also
+                        * it does not smudge active_cache or active_nr
+                        * when it fails, so we do not have to worry about
+                        * cleaning it up ourselves either.
+                        */
+                       read_cache();
+       }
+       if (options->abbrev <= 0 || 40 < options->abbrev)
+               options->abbrev = 40; /* full */
+
+       /*
+        * It does not make sense to show the first hit we happened
+        * to have found.  It does not make sense not to return with
+        * exit code in such a case either.
+        */
+       if (DIFF_OPT_TST(options, QUIET)) {
+               options->output_format = DIFF_FORMAT_NO_OUTPUT;
+               DIFF_OPT_SET(options, EXIT_WITH_STATUS);
+       }
+
+       /*
+        * If we postprocess in diffcore, we cannot simply return
+        * upon the first hit.  We need to run diff as usual.
+        */
+       if (options->pickaxe || options->filter)
+               DIFF_OPT_CLR(options, QUIET);
+
+       return 0;
+}
+
+static int opt_arg(const char *arg, int arg_short, const char *arg_long, int *val)
+{
+       char c, *eq;
+       int len;
+
+       if (*arg != '-')
+               return 0;
+       c = *++arg;
+       if (!c)
+               return 0;
+       if (c == arg_short) {
+               c = *++arg;
+               if (!c)
+                       return 1;
+               if (val && isdigit(c)) {
+                       char *end;
+                       int n = strtoul(arg, &end, 10);
+                       if (*end)
+                               return 0;
+                       *val = n;
+                       return 1;
+               }
+               return 0;
+       }
+       if (c != '-')
+               return 0;
+       arg++;
+       eq = strchr(arg, '=');
+       if (eq)
+               len = eq - arg;
+       else
+               len = strlen(arg);
+       if (!len || strncmp(arg, arg_long, len))
+               return 0;
+       if (eq) {
+               int n;
+               char *end;
+               if (!isdigit(*++eq))
+                       return 0;
+               n = strtoul(eq, &end, 10);
+               if (*end)
+                       return 0;
+               *val = n;
+       }
+       return 1;
+}
+
+static int diff_scoreopt_parse(const char *opt);
+
+int diff_opt_parse(struct diff_options *options, const char **av, int ac)
+{
+       const char *arg = av[0];
+
+       /* Output format options */
+       if (!strcmp(arg, "-p") || !strcmp(arg, "-u"))
+               options->output_format |= DIFF_FORMAT_PATCH;
+       else if (opt_arg(arg, 'U', "unified", &options->context))
+               options->output_format |= DIFF_FORMAT_PATCH;
+       else if (!strcmp(arg, "--raw"))
+               options->output_format |= DIFF_FORMAT_RAW;
+       else if (!strcmp(arg, "--patch-with-raw"))
+               options->output_format |= DIFF_FORMAT_PATCH | DIFF_FORMAT_RAW;
+       else if (!strcmp(arg, "--numstat"))
+               options->output_format |= DIFF_FORMAT_NUMSTAT;
+       else if (!strcmp(arg, "--shortstat"))
+               options->output_format |= DIFF_FORMAT_SHORTSTAT;
+       else if (!strcmp(arg, "--check"))
+               options->output_format |= DIFF_FORMAT_CHECKDIFF;
+       else if (!strcmp(arg, "--summary"))
+               options->output_format |= DIFF_FORMAT_SUMMARY;
+       else if (!strcmp(arg, "--patch-with-stat"))
+               options->output_format |= DIFF_FORMAT_PATCH | DIFF_FORMAT_DIFFSTAT;
+       else if (!strcmp(arg, "--name-only"))
+               options->output_format |= DIFF_FORMAT_NAME;
+       else if (!strcmp(arg, "--name-status"))
+               options->output_format |= DIFF_FORMAT_NAME_STATUS;
+       else if (!strcmp(arg, "-s"))
+               options->output_format |= DIFF_FORMAT_NO_OUTPUT;
+       else if (!prefixcmp(arg, "--stat")) {
+               char *end;
+               int width = options->stat_width;
+               int name_width = options->stat_name_width;
+               arg += 6;
+               end = (char *)arg;
+
+               switch (*arg) {
+               case '-':
+                       if (!prefixcmp(arg, "-width="))
+                               width = strtoul(arg + 7, &end, 10);
+                       else if (!prefixcmp(arg, "-name-width="))
+                               name_width = strtoul(arg + 12, &end, 10);
+                       break;
+               case '=':
+                       width = strtoul(arg+1, &end, 10);
+                       if (*end == ',')
+                               name_width = strtoul(end+1, &end, 10);
+               }
+
+               /* Important! This checks all the error cases! */
+               if (*end)
+                       return 0;
+               options->output_format |= DIFF_FORMAT_DIFFSTAT;
+               options->stat_name_width = name_width;
+               options->stat_width = width;
+       }
+
+       /* renames options */
+       else if (!prefixcmp(arg, "-B")) {
+               if ((options->break_opt = diff_scoreopt_parse(arg)) == -1)
+                       return -1;
+       }
+       else if (!prefixcmp(arg, "-M")) {
+               if ((options->rename_score = diff_scoreopt_parse(arg)) == -1)
+                       return -1;
+               options->detect_rename = DIFF_DETECT_RENAME;
+       }
+       else if (!prefixcmp(arg, "-C")) {
+               if (options->detect_rename == DIFF_DETECT_COPY)
+                       DIFF_OPT_SET(options, FIND_COPIES_HARDER);
+               if ((options->rename_score = diff_scoreopt_parse(arg)) == -1)
+                       return -1;
+               options->detect_rename = DIFF_DETECT_COPY;
+       }
+       else if (!strcmp(arg, "--no-renames"))
+               options->detect_rename = 0;
+
+       /* xdiff options */
+       else if (!strcmp(arg, "-w") || !strcmp(arg, "--ignore-all-space"))
+               options->xdl_opts |= XDF_IGNORE_WHITESPACE;
+       else if (!strcmp(arg, "-b") || !strcmp(arg, "--ignore-space-change"))
+               options->xdl_opts |= XDF_IGNORE_WHITESPACE_CHANGE;
+       else if (!strcmp(arg, "--ignore-space-at-eol"))
+               options->xdl_opts |= XDF_IGNORE_WHITESPACE_AT_EOL;
+
+       /* flags options */
+       else if (!strcmp(arg, "--binary")) {
+               options->output_format |= DIFF_FORMAT_PATCH;
+               DIFF_OPT_SET(options, BINARY);
+       }
+       else if (!strcmp(arg, "--full-index"))
+               DIFF_OPT_SET(options, FULL_INDEX);
+       else if (!strcmp(arg, "-a") || !strcmp(arg, "--text"))
+               DIFF_OPT_SET(options, TEXT);
+       else if (!strcmp(arg, "-R"))
+               DIFF_OPT_SET(options, REVERSE_DIFF);
+       else if (!strcmp(arg, "--find-copies-harder"))
+               DIFF_OPT_SET(options, FIND_COPIES_HARDER);
+       else if (!strcmp(arg, "--follow"))
+               DIFF_OPT_SET(options, FOLLOW_RENAMES);
+       else if (!strcmp(arg, "--color"))
+               DIFF_OPT_SET(options, COLOR_DIFF);
+       else if (!strcmp(arg, "--no-color"))
+               DIFF_OPT_CLR(options, COLOR_DIFF);
+       else if (!strcmp(arg, "--color-words"))
+               options->flags |= DIFF_OPT_COLOR_DIFF | DIFF_OPT_COLOR_DIFF_WORDS;
+       else if (!strcmp(arg, "--exit-code"))
+               DIFF_OPT_SET(options, EXIT_WITH_STATUS);
+       else if (!strcmp(arg, "--quiet"))
+               DIFF_OPT_SET(options, QUIET);
+       else if (!strcmp(arg, "--ext-diff"))
+               DIFF_OPT_SET(options, ALLOW_EXTERNAL);
+       else if (!strcmp(arg, "--no-ext-diff"))
+               DIFF_OPT_CLR(options, ALLOW_EXTERNAL);
+
+       /* misc options */
+       else if (!strcmp(arg, "-z"))
+               options->line_termination = 0;
+       else if (!prefixcmp(arg, "-l"))
+               options->rename_limit = strtoul(arg+2, NULL, 10);
+       else if (!prefixcmp(arg, "-S"))
+               options->pickaxe = arg + 2;
+       else if (!strcmp(arg, "--pickaxe-all"))
+               options->pickaxe_opts = DIFF_PICKAXE_ALL;
+       else if (!strcmp(arg, "--pickaxe-regex"))
+               options->pickaxe_opts = DIFF_PICKAXE_REGEX;
+       else if (!prefixcmp(arg, "-O"))
+               options->orderfile = arg + 2;
+       else if (!prefixcmp(arg, "--diff-filter="))
+               options->filter = arg + 14;
+       else if (!strcmp(arg, "--abbrev"))
+               options->abbrev = DEFAULT_ABBREV;
+       else if (!prefixcmp(arg, "--abbrev=")) {
+               options->abbrev = strtoul(arg + 9, NULL, 10);
+               if (options->abbrev < MINIMUM_ABBREV)
+                       options->abbrev = MINIMUM_ABBREV;
+               else if (40 < options->abbrev)
+                       options->abbrev = 40;
+       }
+       else
+               return 0;
+       return 1;
+}
+
+static int parse_num(const char **cp_p)
+{
+       unsigned long num, scale;
+       int ch, dot;
+       const char *cp = *cp_p;
+
+       num = 0;
+       scale = 1;
+       dot = 0;
+       for(;;) {
+               ch = *cp;
+               if ( !dot && ch == '.' ) {
+                       scale = 1;
+                       dot = 1;
+               } else if ( ch == '%' ) {
+                       scale = dot ? scale*100 : 100;
+                       cp++;   /* % is always at the end */
+                       break;
+               } else if ( ch >= '0' && ch <= '9' ) {
+                       if ( scale < 100000 ) {
+                               scale *= 10;
+                               num = (num*10) + (ch-'0');
+                       }
+               } else {
+                       break;
+               }
+               cp++;
+       }
+       *cp_p = cp;
+
+       /* user says num divided by scale and we say internally that
+        * is MAX_SCORE * num / scale.
+        */
+       return (int)((num >= scale) ? MAX_SCORE : (MAX_SCORE * num / scale));
+}
+
+static int diff_scoreopt_parse(const char *opt)
+{
+       int opt1, opt2, cmd;
+
+       if (*opt++ != '-')
+               return -1;
+       cmd = *opt++;
+       if (cmd != 'M' && cmd != 'C' && cmd != 'B')
+               return -1; /* that is not a -M, -C nor -B option */
+
+       opt1 = parse_num(&opt);
+       if (cmd != 'B')
+               opt2 = 0;
+       else {
+               if (*opt == 0)
+                       opt2 = 0;
+               else if (*opt != '/')
+                       return -1; /* we expect -B80/99 or -B80 */
+               else {
+                       opt++;
+                       opt2 = parse_num(&opt);
+               }
+       }
+       if (*opt != 0)
+               return -1;
+       return opt1 | (opt2 << 16);
+}
+
+struct diff_queue_struct diff_queued_diff;
+
+void diff_q(struct diff_queue_struct *queue, struct diff_filepair *dp)
+{
+       if (queue->alloc <= queue->nr) {
+               queue->alloc = alloc_nr(queue->alloc);
+               queue->queue = xrealloc(queue->queue,
+                                       sizeof(dp) * queue->alloc);
+       }
+       queue->queue[queue->nr++] = dp;
+}
+
+struct diff_filepair *diff_queue(struct diff_queue_struct *queue,
+                                struct diff_filespec *one,
+                                struct diff_filespec *two)
+{
+       struct diff_filepair *dp = xcalloc(1, sizeof(*dp));
+       dp->one = one;
+       dp->two = two;
+       if (queue)
+               diff_q(queue, dp);
+       return dp;
+}
+
+void diff_free_filepair(struct diff_filepair *p)
+{
+       free_filespec(p->one);
+       free_filespec(p->two);
+       free(p);
+}
+
+/* This is different from find_unique_abbrev() in that
+ * it stuffs the result with dots for alignment.
+ */
+const char *diff_unique_abbrev(const unsigned char *sha1, int len)
+{
+       int abblen;
+       const char *abbrev;
+       if (len == 40)
+               return sha1_to_hex(sha1);
+
+       abbrev = find_unique_abbrev(sha1, len);
+       if (!abbrev)
+               return sha1_to_hex(sha1);
+       abblen = strlen(abbrev);
+       if (abblen < 37) {
+               static char hex[41];
+               if (len < abblen && abblen <= len + 2)
+                       sprintf(hex, "%s%.*s", abbrev, len+3-abblen, "..");
+               else
+                       sprintf(hex, "%s...", abbrev);
+               return hex;
+       }
+       return sha1_to_hex(sha1);
+}
+
+static void diff_flush_raw(struct diff_filepair *p, struct diff_options *opt)
+{
+       int line_termination = opt->line_termination;
+       int inter_name_termination = line_termination ? '\t' : '\0';
+
+       if (!(opt->output_format & DIFF_FORMAT_NAME_STATUS)) {
+               printf(":%06o %06o %s ", p->one->mode, p->two->mode,
+                      diff_unique_abbrev(p->one->sha1, opt->abbrev));
+               printf("%s ", diff_unique_abbrev(p->two->sha1, opt->abbrev));
+       }
+       if (p->score) {
+               printf("%c%03d%c", p->status, similarity_index(p),
+                          inter_name_termination);
+       } else {
+               printf("%c%c", p->status, inter_name_termination);
+       }
+
+       if (p->status == DIFF_STATUS_COPIED || p->status == DIFF_STATUS_RENAMED) {
+               write_name_quoted(p->one->path, stdout, inter_name_termination);
+               write_name_quoted(p->two->path, stdout, line_termination);
+       } else {
+               const char *path = p->one->mode ? p->one->path : p->two->path;
+               write_name_quoted(path, stdout, line_termination);
+       }
+}
+
+int diff_unmodified_pair(struct diff_filepair *p)
+{
+       /* This function is written stricter than necessary to support
+        * the currently implemented transformers, but the idea is to
+        * let transformers to produce diff_filepairs any way they want,
+        * and filter and clean them up here before producing the output.
+        */
+       struct diff_filespec *one = p->one, *two = p->two;
+
+       if (DIFF_PAIR_UNMERGED(p))
+               return 0; /* unmerged is interesting */
+
+       /* deletion, addition, mode or type change
+        * and rename are all interesting.
+        */
+       if (DIFF_FILE_VALID(one) != DIFF_FILE_VALID(two) ||
+           DIFF_PAIR_MODE_CHANGED(p) ||
+           strcmp(one->path, two->path))
+               return 0;
+
+       /* both are valid and point at the same path.  that is, we are
+        * dealing with a change.
+        */
+       if (one->sha1_valid && two->sha1_valid &&
+           !hashcmp(one->sha1, two->sha1))
+               return 1; /* no change */
+       if (!one->sha1_valid && !two->sha1_valid)
+               return 1; /* both look at the same file on the filesystem. */
+       return 0;
+}
+
+static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)
+{
+       if (diff_unmodified_pair(p))
+               return;
+
+       if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||
+           (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))
+               return; /* no tree diffs in patch format */
+
+       run_diff(p, o);
+}
+
+static void diff_flush_stat(struct diff_filepair *p, struct diff_options *o,
+                           struct diffstat_t *diffstat)
+{
+       if (diff_unmodified_pair(p))
+               return;
+
+       if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||
+           (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))
+               return; /* no tree diffs in patch format */
+
+       run_diffstat(p, o, diffstat);
+}
+
+static void diff_flush_checkdiff(struct diff_filepair *p,
+               struct diff_options *o)
+{
+       if (diff_unmodified_pair(p))
+               return;
+
+       if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||
+           (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))
+               return; /* no tree diffs in patch format */
+
+       run_checkdiff(p, o);
+}
+
+int diff_queue_is_empty(void)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       int i;
+       for (i = 0; i < q->nr; i++)
+               if (!diff_unmodified_pair(q->queue[i]))
+                       return 0;
+       return 1;
+}
+
+#if DIFF_DEBUG
+void diff_debug_filespec(struct diff_filespec *s, int x, const char *one)
+{
+       fprintf(stderr, "queue[%d] %s (%s) %s %06o %s\n",
+               x, one ? one : "",
+               s->path,
+               DIFF_FILE_VALID(s) ? "valid" : "invalid",
+               s->mode,
+               s->sha1_valid ? sha1_to_hex(s->sha1) : "");
+       fprintf(stderr, "queue[%d] %s size %lu flags %d\n",
+               x, one ? one : "",
+               s->size, s->xfrm_flags);
+}
+
+void diff_debug_filepair(const struct diff_filepair *p, int i)
+{
+       diff_debug_filespec(p->one, i, "one");
+       diff_debug_filespec(p->two, i, "two");
+       fprintf(stderr, "score %d, status %c rename_used %d broken %d\n",
+               p->score, p->status ? p->status : '?',
+               p->one->rename_used, p->broken_pair);
+}
+
+void diff_debug_queue(const char *msg, struct diff_queue_struct *q)
+{
+       int i;
+       if (msg)
+               fprintf(stderr, "%s\n", msg);
+       fprintf(stderr, "q->nr = %d\n", q->nr);
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filepair *p = q->queue[i];
+               diff_debug_filepair(p, i);
+       }
+}
+#endif
+
+static void diff_resolve_rename_copy(void)
+{
+       int i;
+       struct diff_filepair *p;
+       struct diff_queue_struct *q = &diff_queued_diff;
+
+       diff_debug_queue("resolve-rename-copy", q);
+
+       for (i = 0; i < q->nr; i++) {
+               p = q->queue[i];
+               p->status = 0; /* undecided */
+               if (DIFF_PAIR_UNMERGED(p))
+                       p->status = DIFF_STATUS_UNMERGED;
+               else if (!DIFF_FILE_VALID(p->one))
+                       p->status = DIFF_STATUS_ADDED;
+               else if (!DIFF_FILE_VALID(p->two))
+                       p->status = DIFF_STATUS_DELETED;
+               else if (DIFF_PAIR_TYPE_CHANGED(p))
+                       p->status = DIFF_STATUS_TYPE_CHANGED;
+
+               /* from this point on, we are dealing with a pair
+                * whose both sides are valid and of the same type, i.e.
+                * either in-place edit or rename/copy edit.
+                */
+               else if (DIFF_PAIR_RENAME(p)) {
+                       /*
+                        * A rename might have re-connected a broken
+                        * pair up, causing the pathnames to be the
+                        * same again. If so, that's not a rename at
+                        * all, just a modification..
+                        *
+                        * Otherwise, see if this source was used for
+                        * multiple renames, in which case we decrement
+                        * the count, and call it a copy.
+                        */
+                       if (!strcmp(p->one->path, p->two->path))
+                               p->status = DIFF_STATUS_MODIFIED;
+                       else if (--p->one->rename_used > 0)
+                               p->status = DIFF_STATUS_COPIED;
+                       else
+                               p->status = DIFF_STATUS_RENAMED;
+               }
+               else if (hashcmp(p->one->sha1, p->two->sha1) ||
+                        p->one->mode != p->two->mode ||
+                        is_null_sha1(p->one->sha1))
+                       p->status = DIFF_STATUS_MODIFIED;
+               else {
+                       /* This is a "no-change" entry and should not
+                        * happen anymore, but prepare for broken callers.
+                        */
+                       error("feeding unmodified %s to diffcore",
+                             p->one->path);
+                       p->status = DIFF_STATUS_UNKNOWN;
+               }
+       }
+       diff_debug_queue("resolve-rename-copy done", q);
+}
+
+static int check_pair_status(struct diff_filepair *p)
+{
+       switch (p->status) {
+       case DIFF_STATUS_UNKNOWN:
+               return 0;
+       case 0:
+               die("internal error in diff-resolve-rename-copy");
+       default:
+               return 1;
+       }
+}
+
+static void flush_one_pair(struct diff_filepair *p, struct diff_options *opt)
+{
+       int fmt = opt->output_format;
+
+       if (fmt & DIFF_FORMAT_CHECKDIFF)
+               diff_flush_checkdiff(p, opt);
+       else if (fmt & (DIFF_FORMAT_RAW | DIFF_FORMAT_NAME_STATUS))
+               diff_flush_raw(p, opt);
+       else if (fmt & DIFF_FORMAT_NAME)
+               write_name_quoted(p->two->path, stdout, opt->line_termination);
+}
+
+static void show_file_mode_name(const char *newdelete, struct diff_filespec *fs)
+{
+       if (fs->mode)
+               printf(" %s mode %06o ", newdelete, fs->mode);
+       else
+               printf(" %s ", newdelete);
+       write_name_quoted(fs->path, stdout, '\n');
+}
+
+
+static void show_mode_change(struct diff_filepair *p, int show_name)
+{
+       if (p->one->mode && p->two->mode && p->one->mode != p->two->mode) {
+               printf(" mode change %06o => %06o%c", p->one->mode, p->two->mode,
+                       show_name ? ' ' : '\n');
+               if (show_name) {
+                       write_name_quoted(p->two->path, stdout, '\n');
+               }
+       }
+}
+
+static void show_rename_copy(const char *renamecopy, struct diff_filepair *p)
+{
+       char *names = pprint_rename(p->one->path, p->two->path);
+
+       printf(" %s %s (%d%%)\n", renamecopy, names, similarity_index(p));
+       free(names);
+       show_mode_change(p, 0);
+}
+
+static void diff_summary(struct diff_filepair *p)
+{
+       switch(p->status) {
+       case DIFF_STATUS_DELETED:
+               show_file_mode_name("delete", p->one);
+               break;
+       case DIFF_STATUS_ADDED:
+               show_file_mode_name("create", p->two);
+               break;
+       case DIFF_STATUS_COPIED:
+               show_rename_copy("copy", p);
+               break;
+       case DIFF_STATUS_RENAMED:
+               show_rename_copy("rename", p);
+               break;
+       default:
+               if (p->score) {
+                       fputs(" rewrite ", stdout);
+                       write_name_quoted(p->two->path, stdout, ' ');
+                       printf("(%d%%)\n", similarity_index(p));
+               }
+               show_mode_change(p, !p->score);
+               break;
+       }
+}
+
+struct patch_id_t {
+       struct xdiff_emit_state xm;
+       SHA_CTX *ctx;
+       int patchlen;
+};
+
+static int remove_space(char *line, int len)
+{
+       int i;
+       char *dst = line;
+       unsigned char c;
+
+       for (i = 0; i < len; i++)
+               if (!isspace((c = line[i])))
+                       *dst++ = c;
+
+       return dst - line;
+}
+
+static void patch_id_consume(void *priv, char *line, unsigned long len)
+{
+       struct patch_id_t *data = priv;
+       int new_len;
+
+       /* Ignore line numbers when computing the SHA1 of the patch */
+       if (!prefixcmp(line, "@@ -"))
+               return;
+
+       new_len = remove_space(line, len);
+
+       SHA1_Update(data->ctx, line, new_len);
+       data->patchlen += new_len;
+}
+
+/* returns 0 upon success, and writes result into sha1 */
+static int diff_get_patch_id(struct diff_options *options, unsigned char *sha1)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       int i;
+       SHA_CTX ctx;
+       struct patch_id_t data;
+       char buffer[PATH_MAX * 4 + 20];
+
+       SHA1_Init(&ctx);
+       memset(&data, 0, sizeof(struct patch_id_t));
+       data.ctx = &ctx;
+       data.xm.consume = patch_id_consume;
+
+       for (i = 0; i < q->nr; i++) {
+               xpparam_t xpp;
+               xdemitconf_t xecfg;
+               xdemitcb_t ecb;
+               mmfile_t mf1, mf2;
+               struct diff_filepair *p = q->queue[i];
+               int len1, len2;
+
+               memset(&xecfg, 0, sizeof(xecfg));
+               if (p->status == 0)
+                       return error("internal diff status error");
+               if (p->status == DIFF_STATUS_UNKNOWN)
+                       continue;
+               if (diff_unmodified_pair(p))
+                       continue;
+               if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||
+                   (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))
+                       continue;
+               if (DIFF_PAIR_UNMERGED(p))
+                       continue;
+
+               diff_fill_sha1_info(p->one);
+               diff_fill_sha1_info(p->two);
+               if (fill_mmfile(&mf1, p->one) < 0 ||
+                               fill_mmfile(&mf2, p->two) < 0)
+                       return error("unable to read files to diff");
+
+               len1 = remove_space(p->one->path, strlen(p->one->path));
+               len2 = remove_space(p->two->path, strlen(p->two->path));
+               if (p->one->mode == 0)
+                       len1 = snprintf(buffer, sizeof(buffer),
+                                       "diff--gita/%.*sb/%.*s"
+                                       "newfilemode%06o"
+                                       "---/dev/null"
+                                       "+++b/%.*s",
+                                       len1, p->one->path,
+                                       len2, p->two->path,
+                                       p->two->mode,
+                                       len2, p->two->path);
+               else if (p->two->mode == 0)
+                       len1 = snprintf(buffer, sizeof(buffer),
+                                       "diff--gita/%.*sb/%.*s"
+                                       "deletedfilemode%06o"
+                                       "---a/%.*s"
+                                       "+++/dev/null",
+                                       len1, p->one->path,
+                                       len2, p->two->path,
+                                       p->one->mode,
+                                       len1, p->one->path);
+               else
+                       len1 = snprintf(buffer, sizeof(buffer),
+                                       "diff--gita/%.*sb/%.*s"
+                                       "---a/%.*s"
+                                       "+++b/%.*s",
+                                       len1, p->one->path,
+                                       len2, p->two->path,
+                                       len1, p->one->path,
+                                       len2, p->two->path);
+               SHA1_Update(&ctx, buffer, len1);
+
+               xpp.flags = XDF_NEED_MINIMAL;
+               xecfg.ctxlen = 3;
+               xecfg.flags = XDL_EMIT_FUNCNAMES;
+               ecb.outf = xdiff_outf;
+               ecb.priv = &data;
+               xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb);
+       }
+
+       SHA1_Final(sha1, &ctx);
+       return 0;
+}
+
+int diff_flush_patch_id(struct diff_options *options, unsigned char *sha1)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       int i;
+       int result = diff_get_patch_id(options, sha1);
+
+       for (i = 0; i < q->nr; i++)
+               diff_free_filepair(q->queue[i]);
+
+       free(q->queue);
+       q->queue = NULL;
+       q->nr = q->alloc = 0;
+
+       return result;
+}
+
+static int is_summary_empty(const struct diff_queue_struct *q)
+{
+       int i;
+
+       for (i = 0; i < q->nr; i++) {
+               const struct diff_filepair *p = q->queue[i];
+
+               switch (p->status) {
+               case DIFF_STATUS_DELETED:
+               case DIFF_STATUS_ADDED:
+               case DIFF_STATUS_COPIED:
+               case DIFF_STATUS_RENAMED:
+                       return 0;
+               default:
+                       if (p->score)
+                               return 0;
+                       if (p->one->mode && p->two->mode &&
+                           p->one->mode != p->two->mode)
+                               return 0;
+                       break;
+               }
+       }
+       return 1;
+}
+
+void diff_flush(struct diff_options *options)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       int i, output_format = options->output_format;
+       int separator = 0;
+
+       /*
+        * Order: raw, stat, summary, patch
+        * or:    name/name-status/checkdiff (other bits clear)
+        */
+       if (!q->nr)
+               goto free_queue;
+
+       if (output_format & (DIFF_FORMAT_RAW |
+                            DIFF_FORMAT_NAME |
+                            DIFF_FORMAT_NAME_STATUS |
+                            DIFF_FORMAT_CHECKDIFF)) {
+               for (i = 0; i < q->nr; i++) {
+                       struct diff_filepair *p = q->queue[i];
+                       if (check_pair_status(p))
+                               flush_one_pair(p, options);
+               }
+               separator++;
+       }
+
+       if (output_format & (DIFF_FORMAT_DIFFSTAT|DIFF_FORMAT_SHORTSTAT|DIFF_FORMAT_NUMSTAT)) {
+               struct diffstat_t diffstat;
+
+               memset(&diffstat, 0, sizeof(struct diffstat_t));
+               diffstat.xm.consume = diffstat_consume;
+               for (i = 0; i < q->nr; i++) {
+                       struct diff_filepair *p = q->queue[i];
+                       if (check_pair_status(p))
+                               diff_flush_stat(p, options, &diffstat);
+               }
+               if (output_format & DIFF_FORMAT_NUMSTAT)
+                       show_numstat(&diffstat, options);
+               if (output_format & DIFF_FORMAT_DIFFSTAT)
+                       show_stats(&diffstat, options);
+               else if (output_format & DIFF_FORMAT_SHORTSTAT)
+                       show_shortstats(&diffstat);
+               separator++;
+       }
+
+       if (output_format & DIFF_FORMAT_SUMMARY && !is_summary_empty(q)) {
+               for (i = 0; i < q->nr; i++)
+                       diff_summary(q->queue[i]);
+               separator++;
+       }
+
+       if (output_format & DIFF_FORMAT_PATCH) {
+               if (separator) {
+                       if (options->stat_sep) {
+                               /* attach patch instead of inline */
+                               fputs(options->stat_sep, stdout);
+                       } else {
+                               putchar(options->line_termination);
+                       }
+               }
+
+               for (i = 0; i < q->nr; i++) {
+                       struct diff_filepair *p = q->queue[i];
+                       if (check_pair_status(p))
+                               diff_flush_patch(p, options);
+               }
+       }
+
+       if (output_format & DIFF_FORMAT_CALLBACK)
+               options->format_callback(q, options, options->format_callback_data);
+
+       for (i = 0; i < q->nr; i++)
+               diff_free_filepair(q->queue[i]);
+free_queue:
+       free(q->queue);
+       q->queue = NULL;
+       q->nr = q->alloc = 0;
+}
+
+static void diffcore_apply_filter(const char *filter)
+{
+       int i;
+       struct diff_queue_struct *q = &diff_queued_diff;
+       struct diff_queue_struct outq;
+       outq.queue = NULL;
+       outq.nr = outq.alloc = 0;
+
+       if (!filter)
+               return;
+
+       if (strchr(filter, DIFF_STATUS_FILTER_AON)) {
+               int found;
+               for (i = found = 0; !found && i < q->nr; i++) {
+                       struct diff_filepair *p = q->queue[i];
+                       if (((p->status == DIFF_STATUS_MODIFIED) &&
+                            ((p->score &&
+                              strchr(filter, DIFF_STATUS_FILTER_BROKEN)) ||
+                             (!p->score &&
+                              strchr(filter, DIFF_STATUS_MODIFIED)))) ||
+                           ((p->status != DIFF_STATUS_MODIFIED) &&
+                            strchr(filter, p->status)))
+                               found++;
+               }
+               if (found)
+                       return;
+
+               /* otherwise we will clear the whole queue
+                * by copying the empty outq at the end of this
+                * function, but first clear the current entries
+                * in the queue.
+                */
+               for (i = 0; i < q->nr; i++)
+                       diff_free_filepair(q->queue[i]);
+       }
+       else {
+               /* Only the matching ones */
+               for (i = 0; i < q->nr; i++) {
+                       struct diff_filepair *p = q->queue[i];
+
+                       if (((p->status == DIFF_STATUS_MODIFIED) &&
+                            ((p->score &&
+                              strchr(filter, DIFF_STATUS_FILTER_BROKEN)) ||
+                             (!p->score &&
+                              strchr(filter, DIFF_STATUS_MODIFIED)))) ||
+                           ((p->status != DIFF_STATUS_MODIFIED) &&
+                            strchr(filter, p->status)))
+                               diff_q(&outq, p);
+                       else
+                               diff_free_filepair(p);
+               }
+       }
+       free(q->queue);
+       *q = outq;
+}
+
+/* Check whether two filespecs with the same mode and size are identical */
+static int diff_filespec_is_identical(struct diff_filespec *one,
+                                     struct diff_filespec *two)
+{
+       if (S_ISGITLINK(one->mode)) {
+               diff_fill_sha1_info(one);
+               diff_fill_sha1_info(two);
+               return !hashcmp(one->sha1, two->sha1);
+       }
+       if (diff_populate_filespec(one, 0))
+               return 0;
+       if (diff_populate_filespec(two, 0))
+               return 0;
+       return !memcmp(one->data, two->data, one->size);
+}
+
+static void diffcore_skip_stat_unmatch(struct diff_options *diffopt)
+{
+       int i;
+       struct diff_queue_struct *q = &diff_queued_diff;
+       struct diff_queue_struct outq;
+       outq.queue = NULL;
+       outq.nr = outq.alloc = 0;
+
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filepair *p = q->queue[i];
+
+               /*
+                * 1. Entries that come from stat info dirtyness
+                *    always have both sides (iow, not create/delete),
+                *    one side of the object name is unknown, with
+                *    the same mode and size.  Keep the ones that
+                *    do not match these criteria.  They have real
+                *    differences.
+                *
+                * 2. At this point, the file is known to be modified,
+                *    with the same mode and size, and the object
+                *    name of one side is unknown.  Need to inspect
+                *    the identical contents.
+                */
+               if (!DIFF_FILE_VALID(p->one) || /* (1) */
+                   !DIFF_FILE_VALID(p->two) ||
+                   (p->one->sha1_valid && p->two->sha1_valid) ||
+                   (p->one->mode != p->two->mode) ||
+                   diff_populate_filespec(p->one, 1) ||
+                   diff_populate_filespec(p->two, 1) ||
+                   (p->one->size != p->two->size) ||
+                   !diff_filespec_is_identical(p->one, p->two)) /* (2) */
+                       diff_q(&outq, p);
+               else {
+                       /*
+                        * The caller can subtract 1 from skip_stat_unmatch
+                        * to determine how many paths were dirty only
+                        * due to stat info mismatch.
+                        */
+                       if (!DIFF_OPT_TST(diffopt, NO_INDEX))
+                               diffopt->skip_stat_unmatch++;
+                       diff_free_filepair(p);
+               }
+       }
+       free(q->queue);
+       *q = outq;
+}
+
+void diffcore_std(struct diff_options *options)
+{
+       if (DIFF_OPT_TST(options, QUIET))
+               return;
+
+       if (options->skip_stat_unmatch && !DIFF_OPT_TST(options, FIND_COPIES_HARDER))
+               diffcore_skip_stat_unmatch(options);
+       if (options->break_opt != -1)
+               diffcore_break(options->break_opt);
+       if (options->detect_rename)
+               diffcore_rename(options);
+       if (options->break_opt != -1)
+               diffcore_merge_broken();
+       if (options->pickaxe)
+               diffcore_pickaxe(options->pickaxe, options->pickaxe_opts);
+       if (options->orderfile)
+               diffcore_order(options->orderfile);
+       diff_resolve_rename_copy();
+       diffcore_apply_filter(options->filter);
+
+       if (diff_queued_diff.nr)
+               DIFF_OPT_SET(options, HAS_CHANGES);
+       else
+               DIFF_OPT_CLR(options, HAS_CHANGES);
+}
+
+
+void diff_addremove(struct diff_options *options,
+                   int addremove, unsigned mode,
+                   const unsigned char *sha1,
+                   const char *base, const char *path)
+{
+       char concatpath[PATH_MAX];
+       struct diff_filespec *one, *two;
+
+       /* This may look odd, but it is a preparation for
+        * feeding "there are unchanged files which should
+        * not produce diffs, but when you are doing copy
+        * detection you would need them, so here they are"
+        * entries to the diff-core.  They will be prefixed
+        * with something like '=' or '*' (I haven't decided
+        * which but should not make any difference).
+        * Feeding the same new and old to diff_change()
+        * also has the same effect.
+        * Before the final output happens, they are pruned after
+        * merged into rename/copy pairs as appropriate.
+        */
+       if (DIFF_OPT_TST(options, REVERSE_DIFF))
+               addremove = (addremove == '+' ? '-' :
+                            addremove == '-' ? '+' : addremove);
+
+       if (!path) path = "";
+       sprintf(concatpath, "%s%s", base, path);
+       one = alloc_filespec(concatpath);
+       two = alloc_filespec(concatpath);
+
+       if (addremove != '+')
+               fill_filespec(one, sha1, mode);
+       if (addremove != '-')
+               fill_filespec(two, sha1, mode);
+
+       diff_queue(&diff_queued_diff, one, two);
+       DIFF_OPT_SET(options, HAS_CHANGES);
+}
+
+void diff_change(struct diff_options *options,
+                unsigned old_mode, unsigned new_mode,
+                const unsigned char *old_sha1,
+                const unsigned char *new_sha1,
+                const char *base, const char *path)
+{
+       char concatpath[PATH_MAX];
+       struct diff_filespec *one, *two;
+
+       if (DIFF_OPT_TST(options, REVERSE_DIFF)) {
+               unsigned tmp;
+               const unsigned char *tmp_c;
+               tmp = old_mode; old_mode = new_mode; new_mode = tmp;
+               tmp_c = old_sha1; old_sha1 = new_sha1; new_sha1 = tmp_c;
+       }
+       if (!path) path = "";
+       sprintf(concatpath, "%s%s", base, path);
+       one = alloc_filespec(concatpath);
+       two = alloc_filespec(concatpath);
+       fill_filespec(one, old_sha1, old_mode);
+       fill_filespec(two, new_sha1, new_mode);
+
+       diff_queue(&diff_queued_diff, one, two);
+       DIFF_OPT_SET(options, HAS_CHANGES);
+}
+
+void diff_unmerge(struct diff_options *options,
+                 const char *path,
+                 unsigned mode, const unsigned char *sha1)
+{
+       struct diff_filespec *one, *two;
+       one = alloc_filespec(path);
+       two = alloc_filespec(path);
+       fill_filespec(one, sha1, mode);
+       diff_queue(&diff_queued_diff, one, two)->is_unmerged = 1;
+}
diff --git a/diff.h b/diff.h
new file mode 100644 (file)
index 0000000..a52496a
--- /dev/null
+++ b/diff.h
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2005 Junio C Hamano
+ */
+#ifndef DIFF_H
+#define DIFF_H
+
+#include "tree-walk.h"
+
+struct rev_info;
+struct diff_options;
+struct diff_queue_struct;
+
+typedef void (*change_fn_t)(struct diff_options *options,
+                unsigned old_mode, unsigned new_mode,
+                const unsigned char *old_sha1,
+                const unsigned char *new_sha1,
+                const char *base, const char *path);
+
+typedef void (*add_remove_fn_t)(struct diff_options *options,
+                   int addremove, unsigned mode,
+                   const unsigned char *sha1,
+                   const char *base, const char *path);
+
+typedef void (*diff_format_fn_t)(struct diff_queue_struct *q,
+               struct diff_options *options, void *data);
+
+#define DIFF_FORMAT_RAW                0x0001
+#define DIFF_FORMAT_DIFFSTAT   0x0002
+#define DIFF_FORMAT_NUMSTAT    0x0004
+#define DIFF_FORMAT_SUMMARY    0x0008
+#define DIFF_FORMAT_PATCH      0x0010
+#define DIFF_FORMAT_SHORTSTAT  0x0020
+
+/* These override all above */
+#define DIFF_FORMAT_NAME       0x0100
+#define DIFF_FORMAT_NAME_STATUS        0x0200
+#define DIFF_FORMAT_CHECKDIFF  0x0400
+
+/* Same as output_format = 0 but we know that -s flag was given
+ * and we should not give default value to output_format.
+ */
+#define DIFF_FORMAT_NO_OUTPUT  0x0800
+
+#define DIFF_FORMAT_CALLBACK   0x1000
+
+#define DIFF_OPT_RECURSIVE           (1 <<  0)
+#define DIFF_OPT_TREE_IN_RECURSIVE   (1 <<  1)
+#define DIFF_OPT_BINARY              (1 <<  2)
+#define DIFF_OPT_TEXT                (1 <<  3)
+#define DIFF_OPT_FULL_INDEX          (1 <<  4)
+#define DIFF_OPT_SILENT_ON_REMOVE    (1 <<  5)
+#define DIFF_OPT_FIND_COPIES_HARDER  (1 <<  6)
+#define DIFF_OPT_FOLLOW_RENAMES      (1 <<  7)
+#define DIFF_OPT_COLOR_DIFF          (1 <<  8)
+#define DIFF_OPT_COLOR_DIFF_WORDS    (1 <<  9)
+#define DIFF_OPT_HAS_CHANGES         (1 << 10)
+#define DIFF_OPT_QUIET               (1 << 11)
+#define DIFF_OPT_NO_INDEX            (1 << 12)
+#define DIFF_OPT_ALLOW_EXTERNAL      (1 << 13)
+#define DIFF_OPT_EXIT_WITH_STATUS    (1 << 14)
+#define DIFF_OPT_REVERSE_DIFF        (1 << 15)
+#define DIFF_OPT_TST(opts, flag)    ((opts)->flags & DIFF_OPT_##flag)
+#define DIFF_OPT_SET(opts, flag)    ((opts)->flags |= DIFF_OPT_##flag)
+#define DIFF_OPT_CLR(opts, flag)    ((opts)->flags &= ~DIFF_OPT_##flag)
+
+struct diff_options {
+       const char *filter;
+       const char *orderfile;
+       const char *pickaxe;
+       const char *single_follow;
+       unsigned flags;
+       int context;
+       int break_opt;
+       int detect_rename;
+       int skip_stat_unmatch;
+       int line_termination;
+       int output_format;
+       int pickaxe_opts;
+       int rename_score;
+       int rename_limit;
+       int setup;
+       int abbrev;
+       const char *msg_sep;
+       const char *stat_sep;
+       long xdl_opts;
+
+       int stat_width;
+       int stat_name_width;
+
+       /* this is set by diffcore for DIFF_FORMAT_PATCH */
+       int found_changes;
+
+       int nr_paths;
+       const char **paths;
+       int *pathlens;
+       change_fn_t change;
+       add_remove_fn_t add_remove;
+       diff_format_fn_t format_callback;
+       void *format_callback_data;
+};
+
+enum color_diff {
+       DIFF_RESET = 0,
+       DIFF_PLAIN = 1,
+       DIFF_METAINFO = 2,
+       DIFF_FRAGINFO = 3,
+       DIFF_FILE_OLD = 4,
+       DIFF_FILE_NEW = 5,
+       DIFF_COMMIT = 6,
+       DIFF_WHITESPACE = 7,
+};
+const char *diff_get_color(int diff_use_color, enum color_diff ix);
+#define diff_get_color_opt(o, ix) \
+       diff_get_color(DIFF_OPT_TST((o), COLOR_DIFF), ix)
+
+
+extern const char mime_boundary_leader[];
+
+extern void diff_tree_setup_paths(const char **paths, struct diff_options *);
+extern void diff_tree_release_paths(struct diff_options *);
+extern int diff_tree(struct tree_desc *t1, struct tree_desc *t2,
+                    const char *base, struct diff_options *opt);
+extern int diff_tree_sha1(const unsigned char *old, const unsigned char *new,
+                         const char *base, struct diff_options *opt);
+extern int diff_root_tree_sha1(const unsigned char *new, const char *base,
+                               struct diff_options *opt);
+
+struct combine_diff_path {
+       struct combine_diff_path *next;
+       int len;
+       char *path;
+       unsigned int mode;
+       unsigned char sha1[20];
+       struct combine_diff_parent {
+               char status;
+               unsigned int mode;
+               unsigned char sha1[20];
+       } parent[FLEX_ARRAY];
+};
+#define combine_diff_path_size(n, l) \
+       (sizeof(struct combine_diff_path) + \
+        sizeof(struct combine_diff_parent) * (n) + (l) + 1)
+
+extern void show_combined_diff(struct combine_diff_path *elem, int num_parent,
+                             int dense, struct rev_info *);
+
+extern void diff_tree_combined(const unsigned char *sha1, const unsigned char parent[][20], int num_parent, int dense, struct rev_info *rev);
+
+extern void diff_tree_combined_merge(const unsigned char *sha1, int, struct rev_info *);
+
+extern void diff_addremove(struct diff_options *,
+                          int addremove,
+                          unsigned mode,
+                          const unsigned char *sha1,
+                          const char *base,
+                          const char *path);
+
+extern void diff_change(struct diff_options *,
+                       unsigned mode1, unsigned mode2,
+                       const unsigned char *sha1,
+                       const unsigned char *sha2,
+                       const char *base, const char *path);
+
+extern void diff_unmerge(struct diff_options *,
+                        const char *path,
+                        unsigned mode,
+                        const unsigned char *sha1);
+
+#define DIFF_SETUP_REVERSE             1
+#define DIFF_SETUP_USE_CACHE           2
+#define DIFF_SETUP_USE_SIZE_CACHE      4
+
+extern int git_diff_ui_config(const char *var, const char *value);
+extern void diff_setup(struct diff_options *);
+extern int diff_opt_parse(struct diff_options *, const char **, int);
+extern int diff_setup_done(struct diff_options *);
+
+#define DIFF_DETECT_RENAME     1
+#define DIFF_DETECT_COPY       2
+
+#define DIFF_PICKAXE_ALL       1
+#define DIFF_PICKAXE_REGEX     2
+
+extern void diffcore_std(struct diff_options *);
+
+#define COMMON_DIFF_OPTIONS_HELP \
+"\ncommon diff options:\n" \
+"  -z            output diff-raw with lines terminated with NUL.\n" \
+"  -p            output patch format.\n" \
+"  -u            synonym for -p.\n" \
+"  --patch-with-raw\n" \
+"                output both a patch and the diff-raw format.\n" \
+"  --stat        show diffstat instead of patch.\n" \
+"  --numstat     show numeric diffstat instead of patch.\n" \
+"  --patch-with-stat\n" \
+"                output a patch and prepend its diffstat.\n" \
+"  --name-only   show only names of changed files.\n" \
+"  --name-status show names and status of changed files.\n" \
+"  --full-index  show full object name on index lines.\n" \
+"  --abbrev=<n>  abbreviate object names in diff-tree header and diff-raw.\n" \
+"  -R            swap input file pairs.\n" \
+"  -B            detect complete rewrites.\n" \
+"  -M            detect renames.\n" \
+"  -C            detect copies.\n" \
+"  --find-copies-harder\n" \
+"                try unchanged files as candidate for copy detection.\n" \
+"  -l<n>         limit rename attempts up to <n> paths.\n" \
+"  -O<file>      reorder diffs according to the <file>.\n" \
+"  -S<string>    find filepair whose only one side contains the string.\n" \
+"  --pickaxe-all\n" \
+"                show all files diff when -S is used and hit is found.\n" \
+"  -a  --text    treat all files as text.\n"
+
+extern int diff_queue_is_empty(void);
+extern void diff_flush(struct diff_options*);
+
+/* diff-raw status letters */
+#define DIFF_STATUS_ADDED              'A'
+#define DIFF_STATUS_COPIED             'C'
+#define DIFF_STATUS_DELETED            'D'
+#define DIFF_STATUS_MODIFIED           'M'
+#define DIFF_STATUS_RENAMED            'R'
+#define DIFF_STATUS_TYPE_CHANGED       'T'
+#define DIFF_STATUS_UNKNOWN            'X'
+#define DIFF_STATUS_UNMERGED           'U'
+
+/* these are not diff-raw status letters proper, but used by
+ * diffcore-filter insn to specify additional restrictions.
+ */
+#define DIFF_STATUS_FILTER_AON         '*'
+#define DIFF_STATUS_FILTER_BROKEN      'B'
+
+extern const char *diff_unique_abbrev(const unsigned char *, int);
+
+/* do not report anything on removed paths */
+#define DIFF_SILENT_ON_REMOVED 01
+/* report racily-clean paths as modified */
+#define DIFF_RACY_IS_MODIFIED 02
+extern int run_diff_files(struct rev_info *revs, unsigned int option);
+extern int setup_diff_no_index(struct rev_info *revs,
+               int argc, const char ** argv, int nongit, const char *prefix);
+extern int run_diff_files_cmd(struct rev_info *revs, int argc, const char **argv);
+
+extern int run_diff_index(struct rev_info *revs, int cached);
+
+extern int do_diff_cache(const unsigned char *, struct diff_options *);
+extern int diff_flush_patch_id(struct diff_options *, unsigned char *);
+
+#endif /* DIFF_H */
diff --git a/diffcore-break.c b/diffcore-break.c
new file mode 100644 (file)
index 0000000..31cdcfe
--- /dev/null
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2005 Junio C Hamano
+ */
+#include "cache.h"
+#include "diff.h"
+#include "diffcore.h"
+
+static int should_break(struct diff_filespec *src,
+                       struct diff_filespec *dst,
+                       int break_score,
+                       int *merge_score_p)
+{
+       /* dst is recorded as a modification of src.  Are they so
+        * different that we are better off recording this as a pair
+        * of delete and create?
+        *
+        * There are two criteria used in this algorithm.  For the
+        * purposes of helping later rename/copy, we take both delete
+        * and insert into account and estimate the amount of "edit".
+        * If the edit is very large, we break this pair so that
+        * rename/copy can pick the pieces up to match with other
+        * files.
+        *
+        * On the other hand, we would want to ignore inserts for the
+        * pure "complete rewrite" detection.  As long as most of the
+        * existing contents were removed from the file, it is a
+        * complete rewrite, and if sizable chunk from the original
+        * still remains in the result, it is not a rewrite.  It does
+        * not matter how much or how little new material is added to
+        * the file.
+        *
+        * The score we leave for such a broken filepair uses the
+        * latter definition so that later clean-up stage can find the
+        * pieces that should not have been broken according to the
+        * latter definition after rename/copy runs, and merge the
+        * broken pair that have a score lower than given criteria
+        * back together.  The break operation itself happens
+        * according to the former definition.
+        *
+        * The minimum_edit parameter tells us when to break (the
+        * amount of "edit" required for us to consider breaking the
+        * pair).  We leave the amount of deletion in *merge_score_p
+        * when we return.
+        *
+        * The value we return is 1 if we want the pair to be broken,
+        * or 0 if we do not.
+        */
+       unsigned long delta_size, base_size, max_size;
+       unsigned long src_copied, literal_added, src_removed;
+
+       *merge_score_p = 0; /* assume no deletion --- "do not break"
+                            * is the default.
+                            */
+
+       if (S_ISREG(src->mode) != S_ISREG(dst->mode)) {
+               *merge_score_p = (int)MAX_SCORE;
+               return 1; /* even their types are different */
+       }
+
+       if (src->sha1_valid && dst->sha1_valid &&
+           !hashcmp(src->sha1, dst->sha1))
+               return 0; /* they are the same */
+
+       if (diff_populate_filespec(src, 0) || diff_populate_filespec(dst, 0))
+               return 0; /* error but caught downstream */
+
+       base_size = ((src->size < dst->size) ? src->size : dst->size);
+       max_size = ((src->size > dst->size) ? src->size : dst->size);
+       if (max_size < MINIMUM_BREAK_SIZE)
+               return 0; /* we do not break too small filepair */
+
+       if (diffcore_count_changes(src, dst,
+                                  NULL, NULL,
+                                  0,
+                                  &src_copied, &literal_added))
+               return 0;
+
+       /* sanity */
+       if (src->size < src_copied)
+               src_copied = src->size;
+       if (dst->size < literal_added + src_copied) {
+               if (src_copied < dst->size)
+                       literal_added = dst->size - src_copied;
+               else
+                       literal_added = 0;
+       }
+       src_removed = src->size - src_copied;
+
+       /* Compute merge-score, which is "how much is removed
+        * from the source material".  The clean-up stage will
+        * merge the surviving pair together if the score is
+        * less than the minimum, after rename/copy runs.
+        */
+       *merge_score_p = (int)(src_removed * MAX_SCORE / src->size);
+       if (*merge_score_p > break_score)
+               return 1;
+
+       /* Extent of damage, which counts both inserts and
+        * deletes.
+        */
+       delta_size = src_removed + literal_added;
+       if (delta_size * MAX_SCORE / max_size < break_score)
+               return 0;
+
+       /* If you removed a lot without adding new material, that is
+        * not really a rewrite.
+        */
+       if ((src->size * break_score < src_removed * MAX_SCORE) &&
+           (literal_added * 20 < src_removed) &&
+           (literal_added * 20 < src_copied))
+               return 0;
+
+       return 1;
+}
+
+void diffcore_break(int break_score)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       struct diff_queue_struct outq;
+
+       /* When the filepair has this much edit (insert and delete),
+        * it is first considered to be a rewrite and broken into a
+        * create and delete filepair.  This is to help breaking a
+        * file that had too much new stuff added, possibly from
+        * moving contents from another file, so that rename/copy can
+        * match it with the other file.
+        *
+        * int break_score; we reuse incoming parameter for this.
+        */
+
+       /* After a pair is broken according to break_score and
+        * subjected to rename/copy, both of them may survive intact,
+        * due to lack of suitable rename/copy peer.  Or, the caller
+        * may be calling us without using rename/copy.  When that
+        * happens, we merge the broken pieces back into one
+        * modification together if the pair did not have more than
+        * this much delete.  For this computation, we do not take
+        * insert into account at all.  If you start from a 100-line
+        * file and delete 97 lines of it, it does not matter if you
+        * add 27 lines to it to make a new 30-line file or if you add
+        * 997 lines to it to make a 1000-line file.  Either way what
+        * you did was a rewrite of 97%.  On the other hand, if you
+        * delete 3 lines, keeping 97 lines intact, it does not matter
+        * if you add 3 lines to it to make a new 100-line file or if
+        * you add 903 lines to it to make a new 1000-line file.
+        * Either way you did a lot of additions and not a rewrite.
+        * This merge happens to catch the latter case.  A merge_score
+        * of 80% would be a good default value (a broken pair that
+        * has score lower than merge_score will be merged back
+        * together).
+        */
+       int merge_score;
+       int i;
+
+       /* See comment on DEFAULT_BREAK_SCORE and
+        * DEFAULT_MERGE_SCORE in diffcore.h
+        */
+       merge_score = (break_score >> 16) & 0xFFFF;
+       break_score = (break_score & 0xFFFF);
+
+       if (!break_score)
+               break_score = DEFAULT_BREAK_SCORE;
+       if (!merge_score)
+               merge_score = DEFAULT_MERGE_SCORE;
+
+       outq.nr = outq.alloc = 0;
+       outq.queue = NULL;
+
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filepair *p = q->queue[i];
+               int score;
+
+               /*
+                * We deal only with in-place edit of blobs.
+                * We do not break anything else.
+                */
+               if (DIFF_FILE_VALID(p->one) && DIFF_FILE_VALID(p->two) &&
+                   object_type(p->one->mode) == OBJ_BLOB &&
+                   object_type(p->two->mode) == OBJ_BLOB &&
+                   !strcmp(p->one->path, p->two->path)) {
+                       if (should_break(p->one, p->two,
+                                        break_score, &score)) {
+                               /* Split this into delete and create */
+                               struct diff_filespec *null_one, *null_two;
+                               struct diff_filepair *dp;
+
+                               /* Set score to 0 for the pair that
+                                * needs to be merged back together
+                                * should they survive rename/copy.
+                                * Also we do not want to break very
+                                * small files.
+                                */
+                               if (score < merge_score)
+                                       score = 0;
+
+                               /* deletion of one */
+                               null_one = alloc_filespec(p->one->path);
+                               dp = diff_queue(&outq, p->one, null_one);
+                               dp->score = score;
+                               dp->broken_pair = 1;
+
+                               /* creation of two */
+                               null_two = alloc_filespec(p->two->path);
+                               dp = diff_queue(&outq, null_two, p->two);
+                               dp->score = score;
+                               dp->broken_pair = 1;
+
+                               free(p); /* not diff_free_filepair(), we are
+                                         * reusing one and two here.
+                                         */
+                               continue;
+                       }
+               }
+               diff_q(&outq, p);
+       }
+       free(q->queue);
+       *q = outq;
+
+       return;
+}
+
+static void merge_broken(struct diff_filepair *p,
+                        struct diff_filepair *pp,
+                        struct diff_queue_struct *outq)
+{
+       /* p and pp are broken pairs we want to merge */
+       struct diff_filepair *c = p, *d = pp, *dp;
+       if (DIFF_FILE_VALID(p->one)) {
+               /* this must be a delete half */
+               d = p; c = pp;
+       }
+       /* Sanity check */
+       if (!DIFF_FILE_VALID(d->one))
+               die("internal error in merge #1");
+       if (DIFF_FILE_VALID(d->two))
+               die("internal error in merge #2");
+       if (DIFF_FILE_VALID(c->one))
+               die("internal error in merge #3");
+       if (!DIFF_FILE_VALID(c->two))
+               die("internal error in merge #4");
+
+       dp = diff_queue(outq, d->one, c->two);
+       dp->score = p->score;
+       diff_free_filespec_data(d->two);
+       diff_free_filespec_data(c->one);
+       free(d);
+       free(c);
+}
+
+void diffcore_merge_broken(void)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       struct diff_queue_struct outq;
+       int i, j;
+
+       outq.nr = outq.alloc = 0;
+       outq.queue = NULL;
+
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filepair *p = q->queue[i];
+               if (!p)
+                       /* we already merged this with its peer */
+                       continue;
+               else if (p->broken_pair &&
+                        !strcmp(p->one->path, p->two->path)) {
+                       /* If the peer also survived rename/copy, then
+                        * we merge them back together.
+                        */
+                       for (j = i + 1; j < q->nr; j++) {
+                               struct diff_filepair *pp = q->queue[j];
+                               if (pp->broken_pair &&
+                                   !strcmp(pp->one->path, pp->two->path) &&
+                                   !strcmp(p->one->path, pp->two->path)) {
+                                       /* Peer survived.  Merge them */
+                                       merge_broken(p, pp, &outq);
+                                       q->queue[j] = NULL;
+                                       break;
+                               }
+                       }
+                       if (q->nr <= j)
+                               /* The peer did not survive, so we keep
+                                * it in the output.
+                                */
+                               diff_q(&outq, p);
+               }
+               else
+                       diff_q(&outq, p);
+       }
+       free(q->queue);
+       *q = outq;
+
+       return;
+}
diff --git a/diffcore-delta.c b/diffcore-delta.c
new file mode 100644 (file)
index 0000000..e670f85
--- /dev/null
@@ -0,0 +1,224 @@
+#include "cache.h"
+#include "diff.h"
+#include "diffcore.h"
+
+/*
+ * Idea here is very simple.
+ *
+ * Almost all data we are interested in are text, but sometimes we have
+ * to deal with binary data.  So we cut them into chunks delimited by
+ * LF byte, or 64-byte sequence, whichever comes first, and hash them.
+ *
+ * For those chunks, if the source buffer has more instances of it
+ * than the destination buffer, that means the difference are the
+ * number of bytes not copied from source to destination.  If the
+ * counts are the same, everything was copied from source to
+ * destination.  If the destination has more, everything was copied,
+ * and destination added more.
+ *
+ * We are doing an approximation so we do not really have to waste
+ * memory by actually storing the sequence.  We just hash them into
+ * somewhere around 2^16 hashbuckets and count the occurrences.
+ */
+
+/* Wild guess at the initial hash size */
+#define INITIAL_HASH_SIZE 9
+
+/* We leave more room in smaller hash but do not let it
+ * grow to have unused hole too much.
+ */
+#define INITIAL_FREE(sz_log2) ((1<<(sz_log2))*(sz_log2-3)/(sz_log2))
+
+/* A prime rather carefully chosen between 2^16..2^17, so that
+ * HASHBASE < INITIAL_FREE(17).  We want to keep the maximum hashtable
+ * size under the current 2<<17 maximum, which can hold this many
+ * different values before overflowing to hashtable of size 2<<18.
+ */
+#define HASHBASE 107927
+
+struct spanhash {
+       unsigned int hashval;
+       unsigned int cnt;
+};
+struct spanhash_top {
+       int alloc_log2;
+       int free;
+       struct spanhash data[FLEX_ARRAY];
+};
+
+static struct spanhash_top *spanhash_rehash(struct spanhash_top *orig)
+{
+       struct spanhash_top *new;
+       int i;
+       int osz = 1 << orig->alloc_log2;
+       int sz = osz << 1;
+
+       new = xmalloc(sizeof(*orig) + sizeof(struct spanhash) * sz);
+       new->alloc_log2 = orig->alloc_log2 + 1;
+       new->free = INITIAL_FREE(new->alloc_log2);
+       memset(new->data, 0, sizeof(struct spanhash) * sz);
+       for (i = 0; i < osz; i++) {
+               struct spanhash *o = &(orig->data[i]);
+               int bucket;
+               if (!o->cnt)
+                       continue;
+               bucket = o->hashval & (sz - 1);
+               while (1) {
+                       struct spanhash *h = &(new->data[bucket++]);
+                       if (!h->cnt) {
+                               h->hashval = o->hashval;
+                               h->cnt = o->cnt;
+                               new->free--;
+                               break;
+                       }
+                       if (sz <= bucket)
+                               bucket = 0;
+               }
+       }
+       free(orig);
+       return new;
+}
+
+static struct spanhash_top *add_spanhash(struct spanhash_top *top,
+                                        unsigned int hashval, int cnt)
+{
+       int bucket, lim;
+       struct spanhash *h;
+
+       lim = (1 << top->alloc_log2);
+       bucket = hashval & (lim - 1);
+       while (1) {
+               h = &(top->data[bucket++]);
+               if (!h->cnt) {
+                       h->hashval = hashval;
+                       h->cnt = cnt;
+                       top->free--;
+                       if (top->free < 0)
+                               return spanhash_rehash(top);
+                       return top;
+               }
+               if (h->hashval == hashval) {
+                       h->cnt += cnt;
+                       return top;
+               }
+               if (lim <= bucket)
+                       bucket = 0;
+       }
+}
+
+static int spanhash_cmp(const void *a_, const void *b_)
+{
+       const struct spanhash *a = a_;
+       const struct spanhash *b = b_;
+
+       /* A count of zero compares at the end.. */
+       if (!a->cnt)
+               return !b->cnt ? 0 : 1;
+       if (!b->cnt)
+               return -1;
+       return a->hashval < b->hashval ? -1 :
+               a->hashval > b->hashval ? 1 : 0;
+}
+
+static struct spanhash_top *hash_chars(struct diff_filespec *one)
+{
+       int i, n;
+       unsigned int accum1, accum2, hashval;
+       struct spanhash_top *hash;
+       unsigned char *buf = one->data;
+       unsigned int sz = one->size;
+       int is_text = !diff_filespec_is_binary(one);
+
+       i = INITIAL_HASH_SIZE;
+       hash = xmalloc(sizeof(*hash) + sizeof(struct spanhash) * (1<<i));
+       hash->alloc_log2 = i;
+       hash->free = INITIAL_FREE(i);
+       memset(hash->data, 0, sizeof(struct spanhash) * (1<<i));
+
+       n = 0;
+       accum1 = accum2 = 0;
+       while (sz) {
+               unsigned int c = *buf++;
+               unsigned int old_1 = accum1;
+               sz--;
+
+               /* Ignore CR in CRLF sequence if text */
+               if (is_text && c == '\r' && sz && *buf == '\n')
+                       continue;
+
+               accum1 = (accum1 << 7) ^ (accum2 >> 25);
+               accum2 = (accum2 << 7) ^ (old_1 >> 25);
+               accum1 += c;
+               if (++n < 64 && c != '\n')
+                       continue;
+               hashval = (accum1 + accum2 * 0x61) % HASHBASE;
+               hash = add_spanhash(hash, hashval, n);
+               n = 0;
+               accum1 = accum2 = 0;
+       }
+       qsort(hash->data,
+               1ul << hash->alloc_log2,
+               sizeof(hash->data[0]),
+               spanhash_cmp);
+       return hash;
+}
+
+int diffcore_count_changes(struct diff_filespec *src,
+                          struct diff_filespec *dst,
+                          void **src_count_p,
+                          void **dst_count_p,
+                          unsigned long delta_limit,
+                          unsigned long *src_copied,
+                          unsigned long *literal_added)
+{
+       struct spanhash *s, *d;
+       struct spanhash_top *src_count, *dst_count;
+       unsigned long sc, la;
+
+       src_count = dst_count = NULL;
+       if (src_count_p)
+               src_count = *src_count_p;
+       if (!src_count) {
+               src_count = hash_chars(src);
+               if (src_count_p)
+                       *src_count_p = src_count;
+       }
+       if (dst_count_p)
+               dst_count = *dst_count_p;
+       if (!dst_count) {
+               dst_count = hash_chars(dst);
+               if (dst_count_p)
+                       *dst_count_p = dst_count;
+       }
+       sc = la = 0;
+
+       s = src_count->data;
+       d = dst_count->data;
+       for (;;) {
+               unsigned dst_cnt, src_cnt;
+               if (!s->cnt)
+                       break; /* we checked all in src */
+               while (d->cnt) {
+                       if (d->hashval >= s->hashval)
+                               break;
+                       d++;
+               }
+               src_cnt = s->cnt;
+               dst_cnt = d->hashval == s->hashval ? d->cnt : 0;
+               if (src_cnt < dst_cnt) {
+                       la += dst_cnt - src_cnt;
+                       sc += src_cnt;
+               }
+               else
+                       sc += dst_cnt;
+               s++;
+       }
+
+       if (!src_count_p)
+               free(src_count);
+       if (!dst_count_p)
+               free(dst_count);
+       *src_copied = sc;
+       *literal_added = la;
+       return 0;
+}
diff --git a/diffcore-order.c b/diffcore-order.c
new file mode 100644 (file)
index 0000000..23e9385
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2005 Junio C Hamano
+ */
+#include "cache.h"
+#include "diff.h"
+#include "diffcore.h"
+
+static char **order;
+static int order_cnt;
+
+static void prepare_order(const char *orderfile)
+{
+       int fd, cnt, pass;
+       void *map;
+       char *cp, *endp;
+       struct stat st;
+       size_t sz;
+
+       if (order)
+               return;
+
+       fd = open(orderfile, O_RDONLY);
+       if (fd < 0)
+               return;
+       if (fstat(fd, &st)) {
+               close(fd);
+               return;
+       }
+       sz = xsize_t(st.st_size);
+       map = mmap(NULL, sz, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
+       close(fd);
+       if (map == MAP_FAILED)
+               return;
+       endp = (char *) map + sz;
+       for (pass = 0; pass < 2; pass++) {
+               cnt = 0;
+               cp = map;
+               while (cp < endp) {
+                       char *ep;
+                       for (ep = cp; ep < endp && *ep != '\n'; ep++)
+                               ;
+                       /* cp to ep has one line */
+                       if (*cp == '\n' || *cp == '#')
+                               ; /* comment */
+                       else if (pass == 0)
+                               cnt++;
+                       else {
+                               if (*ep == '\n') {
+                                       *ep = 0;
+                                       order[cnt] = cp;
+                               } else {
+                                       order[cnt] = xmemdupz(cp, ep - cp);
+                               }
+                               cnt++;
+                       }
+                       if (ep < endp)
+                               ep++;
+                       cp = ep;
+               }
+               if (pass == 0) {
+                       order_cnt = cnt;
+                       order = xmalloc(sizeof(*order) * cnt);
+               }
+       }
+}
+
+struct pair_order {
+       struct diff_filepair *pair;
+       int orig_order;
+       int order;
+};
+
+static int match_order(const char *path)
+{
+       int i;
+       char p[PATH_MAX];
+
+       for (i = 0; i < order_cnt; i++) {
+               strcpy(p, path);
+               while (p[0]) {
+                       char *cp;
+                       if (!fnmatch(order[i], p, 0))
+                               return i;
+                       cp = strrchr(p, '/');
+                       if (!cp)
+                               break;
+                       *cp = 0;
+               }
+       }
+       return order_cnt;
+}
+
+static int compare_pair_order(const void *a_, const void *b_)
+{
+       struct pair_order const *a, *b;
+       a = (struct pair_order const *)a_;
+       b = (struct pair_order const *)b_;
+       if (a->order != b->order)
+               return a->order - b->order;
+       return a->orig_order - b->orig_order;
+}
+
+void diffcore_order(const char *orderfile)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       struct pair_order *o;
+       int i;
+
+       if (!q->nr)
+               return;
+
+       o = xmalloc(sizeof(*o) * q->nr);
+       prepare_order(orderfile);
+       for (i = 0; i < q->nr; i++) {
+               o[i].pair = q->queue[i];
+               o[i].orig_order = i;
+               o[i].order = match_order(o[i].pair->two->path);
+       }
+       qsort(o, q->nr, sizeof(*o), compare_pair_order);
+       for (i = 0; i < q->nr; i++)
+               q->queue[i] = o[i].pair;
+       free(o);
+       return;
+}
diff --git a/diffcore-pickaxe.c b/diffcore-pickaxe.c
new file mode 100644 (file)
index 0000000..af9fffe
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2005 Junio C Hamano
+ */
+#include "cache.h"
+#include "diff.h"
+#include "diffcore.h"
+
+static unsigned int contains(struct diff_filespec *one,
+                            const char *needle, unsigned long len,
+                            regex_t *regexp)
+{
+       unsigned int cnt;
+       unsigned long offset, sz;
+       const char *data;
+       if (diff_populate_filespec(one, 0))
+               return 0;
+       if (!len)
+               return 0;
+
+       sz = one->size;
+       data = one->data;
+       cnt = 0;
+
+       if (regexp) {
+               regmatch_t regmatch;
+               int flags = 0;
+
+               while (*data && !regexec(regexp, data, 1, &regmatch, flags)) {
+                       flags |= REG_NOTBOL;
+                       data += regmatch.rm_so;
+                       if (*data) data++;
+                       cnt++;
+               }
+
+       } else { /* Classic exact string match */
+               /* Yes, I've heard of strstr(), but the thing is *data may
+                * not be NUL terminated.  Sue me.
+                */
+               for (offset = 0; offset + len <= sz; offset++) {
+                       /* we count non-overlapping occurrences of needle */
+                       if (!memcmp(needle, data + offset, len)) {
+                               offset += len - 1;
+                               cnt++;
+                       }
+               }
+       }
+       diff_free_filespec_data(one);
+       return cnt;
+}
+
+void diffcore_pickaxe(const char *needle, int opts)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       unsigned long len = strlen(needle);
+       int i, has_changes;
+       regex_t regex, *regexp = NULL;
+       struct diff_queue_struct outq;
+       outq.queue = NULL;
+       outq.nr = outq.alloc = 0;
+
+       if (opts & DIFF_PICKAXE_REGEX) {
+               int err;
+               err = regcomp(&regex, needle, REG_EXTENDED | REG_NEWLINE);
+               if (err) {
+                       /* The POSIX.2 people are surely sick */
+                       char errbuf[1024];
+                       regerror(err, &regex, errbuf, 1024);
+                       regfree(&regex);
+                       die("invalid pickaxe regex: %s", errbuf);
+               }
+               regexp = &regex;
+       }
+
+       if (opts & DIFF_PICKAXE_ALL) {
+               /* Showing the whole changeset if needle exists */
+               for (i = has_changes = 0; !has_changes && i < q->nr; i++) {
+                       struct diff_filepair *p = q->queue[i];
+                       if (!DIFF_FILE_VALID(p->one)) {
+                               if (!DIFF_FILE_VALID(p->two))
+                                       continue; /* ignore unmerged */
+                               /* created */
+                               if (contains(p->two, needle, len, regexp))
+                                       has_changes++;
+                       }
+                       else if (!DIFF_FILE_VALID(p->two)) {
+                               if (contains(p->one, needle, len, regexp))
+                                       has_changes++;
+                       }
+                       else if (!diff_unmodified_pair(p) &&
+                                contains(p->one, needle, len, regexp) !=
+                                contains(p->two, needle, len, regexp))
+                               has_changes++;
+               }
+               if (has_changes)
+                       return; /* not munge the queue */
+
+               /* otherwise we will clear the whole queue
+                * by copying the empty outq at the end of this
+                * function, but first clear the current entries
+                * in the queue.
+                */
+               for (i = 0; i < q->nr; i++)
+                       diff_free_filepair(q->queue[i]);
+       }
+       else
+               /* Showing only the filepairs that has the needle */
+               for (i = 0; i < q->nr; i++) {
+                       struct diff_filepair *p = q->queue[i];
+                       has_changes = 0;
+                       if (!DIFF_FILE_VALID(p->one)) {
+                               if (!DIFF_FILE_VALID(p->two))
+                                       ; /* ignore unmerged */
+                               /* created */
+                               else if (contains(p->two, needle, len, regexp))
+                                       has_changes = 1;
+                       }
+                       else if (!DIFF_FILE_VALID(p->two)) {
+                               if (contains(p->one, needle, len, regexp))
+                                       has_changes = 1;
+                       }
+                       else if (!diff_unmodified_pair(p) &&
+                                contains(p->one, needle, len, regexp) !=
+                                contains(p->two, needle, len, regexp))
+                               has_changes = 1;
+
+                       if (has_changes)
+                               diff_q(&outq, p);
+                       else
+                               diff_free_filepair(p);
+               }
+
+       if (opts & DIFF_PICKAXE_REGEX) {
+               regfree(&regex);
+       }
+
+       free(q->queue);
+       *q = outq;
+       return;
+}
diff --git a/diffcore-rename.c b/diffcore-rename.c
new file mode 100644 (file)
index 0000000..3d37725
--- /dev/null
@@ -0,0 +1,613 @@
+/*
+ * Copyright (C) 2005 Junio C Hamano
+ */
+#include "cache.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "hash.h"
+
+/* Table of rename/copy destinations */
+
+static struct diff_rename_dst {
+       struct diff_filespec *two;
+       struct diff_filepair *pair;
+} *rename_dst;
+static int rename_dst_nr, rename_dst_alloc;
+
+static struct diff_rename_dst *locate_rename_dst(struct diff_filespec *two,
+                                                int insert_ok)
+{
+       int first, last;
+
+       first = 0;
+       last = rename_dst_nr;
+       while (last > first) {
+               int next = (last + first) >> 1;
+               struct diff_rename_dst *dst = &(rename_dst[next]);
+               int cmp = strcmp(two->path, dst->two->path);
+               if (!cmp)
+                       return dst;
+               if (cmp < 0) {
+                       last = next;
+                       continue;
+               }
+               first = next+1;
+       }
+       /* not found */
+       if (!insert_ok)
+               return NULL;
+       /* insert to make it at "first" */
+       if (rename_dst_alloc <= rename_dst_nr) {
+               rename_dst_alloc = alloc_nr(rename_dst_alloc);
+               rename_dst = xrealloc(rename_dst,
+                                     rename_dst_alloc * sizeof(*rename_dst));
+       }
+       rename_dst_nr++;
+       if (first < rename_dst_nr)
+               memmove(rename_dst + first + 1, rename_dst + first,
+                       (rename_dst_nr - first - 1) * sizeof(*rename_dst));
+       rename_dst[first].two = alloc_filespec(two->path);
+       fill_filespec(rename_dst[first].two, two->sha1, two->mode);
+       rename_dst[first].pair = NULL;
+       return &(rename_dst[first]);
+}
+
+/* Table of rename/copy src files */
+static struct diff_rename_src {
+       struct diff_filespec *one;
+       unsigned short score; /* to remember the break score */
+} *rename_src;
+static int rename_src_nr, rename_src_alloc;
+
+static struct diff_rename_src *register_rename_src(struct diff_filespec *one,
+                                                  unsigned short score)
+{
+       int first, last;
+
+       first = 0;
+       last = rename_src_nr;
+       while (last > first) {
+               int next = (last + first) >> 1;
+               struct diff_rename_src *src = &(rename_src[next]);
+               int cmp = strcmp(one->path, src->one->path);
+               if (!cmp)
+                       return src;
+               if (cmp < 0) {
+                       last = next;
+                       continue;
+               }
+               first = next+1;
+       }
+
+       /* insert to make it at "first" */
+       if (rename_src_alloc <= rename_src_nr) {
+               rename_src_alloc = alloc_nr(rename_src_alloc);
+               rename_src = xrealloc(rename_src,
+                                     rename_src_alloc * sizeof(*rename_src));
+       }
+       rename_src_nr++;
+       if (first < rename_src_nr)
+               memmove(rename_src + first + 1, rename_src + first,
+                       (rename_src_nr - first - 1) * sizeof(*rename_src));
+       rename_src[first].one = one;
+       rename_src[first].score = score;
+       return &(rename_src[first]);
+}
+
+static int basename_same(struct diff_filespec *src, struct diff_filespec *dst)
+{
+       int src_len = strlen(src->path), dst_len = strlen(dst->path);
+       while (src_len && dst_len) {
+               char c1 = src->path[--src_len];
+               char c2 = dst->path[--dst_len];
+               if (c1 != c2)
+                       return 0;
+               if (c1 == '/')
+                       return 1;
+       }
+       return (!src_len || src->path[src_len - 1] == '/') &&
+               (!dst_len || dst->path[dst_len - 1] == '/');
+}
+
+struct diff_score {
+       int src; /* index in rename_src */
+       int dst; /* index in rename_dst */
+       int score;
+       int name_score;
+};
+
+static int estimate_similarity(struct diff_filespec *src,
+                              struct diff_filespec *dst,
+                              int minimum_score)
+{
+       /* src points at a file that existed in the original tree (or
+        * optionally a file in the destination tree) and dst points
+        * at a newly created file.  They may be quite similar, in which
+        * case we want to say src is renamed to dst or src is copied into
+        * dst, and then some edit has been applied to dst.
+        *
+        * Compare them and return how similar they are, representing
+        * the score as an integer between 0 and MAX_SCORE.
+        *
+        * When there is an exact match, it is considered a better
+        * match than anything else; the destination does not even
+        * call into this function in that case.
+        */
+       unsigned long max_size, delta_size, base_size, src_copied, literal_added;
+       unsigned long delta_limit;
+       int score;
+
+       /* We deal only with regular files.  Symlink renames are handled
+        * only when they are exact matches --- in other words, no edits
+        * after renaming.
+        */
+       if (!S_ISREG(src->mode) || !S_ISREG(dst->mode))
+               return 0;
+
+       /*
+        * Need to check that source and destination sizes are
+        * filled in before comparing them.
+        *
+        * If we already have "cnt_data" filled in, we know it's
+        * all good (avoid checking the size for zero, as that
+        * is a possible size - we really should have a flag to
+        * say whether the size is valid or not!)
+        */
+       if (!src->cnt_data && diff_populate_filespec(src, 0))
+               return 0;
+       if (!dst->cnt_data && diff_populate_filespec(dst, 0))
+               return 0;
+
+       max_size = ((src->size > dst->size) ? src->size : dst->size);
+       base_size = ((src->size < dst->size) ? src->size : dst->size);
+       delta_size = max_size - base_size;
+
+       /* We would not consider edits that change the file size so
+        * drastically.  delta_size must be smaller than
+        * (MAX_SCORE-minimum_score)/MAX_SCORE * min(src->size, dst->size).
+        *
+        * Note that base_size == 0 case is handled here already
+        * and the final score computation below would not have a
+        * divide-by-zero issue.
+        */
+       if (base_size * (MAX_SCORE-minimum_score) < delta_size * MAX_SCORE)
+               return 0;
+
+       delta_limit = (unsigned long)
+               (base_size * (MAX_SCORE-minimum_score) / MAX_SCORE);
+       if (diffcore_count_changes(src, dst,
+                                  &src->cnt_data, &dst->cnt_data,
+                                  delta_limit,
+                                  &src_copied, &literal_added))
+               return 0;
+
+       /* How similar are they?
+        * what percentage of material in dst are from source?
+        */
+       if (!dst->size)
+               score = 0; /* should not happen */
+       else
+               score = (int)(src_copied * MAX_SCORE / max_size);
+       return score;
+}
+
+static void record_rename_pair(int dst_index, int src_index, int score)
+{
+       struct diff_filespec *src, *dst;
+       struct diff_filepair *dp;
+
+       if (rename_dst[dst_index].pair)
+               die("internal error: dst already matched.");
+
+       src = rename_src[src_index].one;
+       src->rename_used++;
+       src->count++;
+
+       dst = rename_dst[dst_index].two;
+       dst->count++;
+
+       dp = diff_queue(NULL, src, dst);
+       dp->renamed_pair = 1;
+       if (!strcmp(src->path, dst->path))
+               dp->score = rename_src[src_index].score;
+       else
+               dp->score = score;
+       rename_dst[dst_index].pair = dp;
+}
+
+/*
+ * We sort the rename similarity matrix with the score, in descending
+ * order (the most similar first).
+ */
+static int score_compare(const void *a_, const void *b_)
+{
+       const struct diff_score *a = a_, *b = b_;
+
+       if (a->score == b->score)
+               return b->name_score - a->name_score;
+
+       return b->score - a->score;
+}
+
+struct file_similarity {
+       int src_dst, index;
+       struct diff_filespec *filespec;
+       struct file_similarity *next;
+};
+
+static int find_identical_files(struct file_similarity *src,
+                               struct file_similarity *dst)
+{
+       int renames = 0;
+
+       /*
+        * Walk over all the destinations ...
+        */
+       do {
+               struct diff_filespec *target = dst->filespec;
+               struct file_similarity *p, *best;
+               int i = 100, best_score = -1;
+
+               /*
+                * .. to find the best source match
+                */
+               best = NULL;
+               for (p = src; p; p = p->next) {
+                       int score;
+                       struct diff_filespec *source = p->filespec;
+
+                       /* False hash collission? */
+                       if (hashcmp(source->sha1, target->sha1))
+                               continue;
+                       /* Non-regular files? If so, the modes must match! */
+                       if (!S_ISREG(source->mode) || !S_ISREG(target->mode)) {
+                               if (source->mode != target->mode)
+                                       continue;
+                       }
+                       /* Give higher scores to sources that haven't been used already */
+                       score = !source->rename_used;
+                       score += basename_same(source, target);
+                       if (score > best_score) {
+                               best = p;
+                               best_score = score;
+                               if (score == 2)
+                                       break;
+                       }
+
+                       /* Too many identical alternatives? Pick one */
+                       if (!--i)
+                               break;
+               }
+               if (best) {
+                       record_rename_pair(dst->index, best->index, MAX_SCORE);
+                       renames++;
+               }
+       } while ((dst = dst->next) != NULL);
+       return renames;
+}
+
+static void free_similarity_list(struct file_similarity *p)
+{
+       while (p) {
+               struct file_similarity *entry = p;
+               p = p->next;
+               free(entry);
+       }
+}
+
+static int find_same_files(void *ptr)
+{
+       int ret;
+       struct file_similarity *p = ptr;
+       struct file_similarity *src = NULL, *dst = NULL;
+
+       /* Split the hash list up into sources and destinations */
+       do {
+               struct file_similarity *entry = p;
+               p = p->next;
+               if (entry->src_dst < 0) {
+                       entry->next = src;
+                       src = entry;
+               } else {
+                       entry->next = dst;
+                       dst = entry;
+               }
+       } while (p);
+
+       /*
+        * If we have both sources *and* destinations, see if
+        * we can match them up
+        */
+       ret = (src && dst) ? find_identical_files(src, dst) : 0;
+
+       /* Free the hashes and return the number of renames found */
+       free_similarity_list(src);
+       free_similarity_list(dst);
+       return ret;
+}
+
+static unsigned int hash_filespec(struct diff_filespec *filespec)
+{
+       unsigned int hash;
+       if (!filespec->sha1_valid) {
+               if (diff_populate_filespec(filespec, 0))
+                       return 0;
+               hash_sha1_file(filespec->data, filespec->size, "blob", filespec->sha1);
+       }
+       memcpy(&hash, filespec->sha1, sizeof(hash));
+       return hash;
+}
+
+static void insert_file_table(struct hash_table *table, int src_dst, int index, struct diff_filespec *filespec)
+{
+       void **pos;
+       unsigned int hash;
+       struct file_similarity *entry = xmalloc(sizeof(*entry));
+
+       entry->src_dst = src_dst;
+       entry->index = index;
+       entry->filespec = filespec;
+       entry->next = NULL;
+
+       hash = hash_filespec(filespec);
+       pos = insert_hash(hash, entry, table);
+
+       /* We already had an entry there? */
+       if (pos) {
+               entry->next = *pos;
+               *pos = entry;
+       }
+}
+
+/*
+ * Find exact renames first.
+ *
+ * The first round matches up the up-to-date entries,
+ * and then during the second round we try to match
+ * cache-dirty entries as well.
+ */
+static int find_exact_renames(void)
+{
+       int i;
+       struct hash_table file_table;
+
+       init_hash(&file_table);
+       for (i = 0; i < rename_src_nr; i++)
+               insert_file_table(&file_table, -1, i, rename_src[i].one);
+
+       for (i = 0; i < rename_dst_nr; i++)
+               insert_file_table(&file_table, 1, i, rename_dst[i].two);
+
+       /* Find the renames */
+       i = for_each_hash(&file_table, find_same_files);
+
+       /* .. and free the hash data structure */
+       free_hash(&file_table);
+
+       return i;
+}
+
+void diffcore_rename(struct diff_options *options)
+{
+       int detect_rename = options->detect_rename;
+       int minimum_score = options->rename_score;
+       int rename_limit = options->rename_limit;
+       struct diff_queue_struct *q = &diff_queued_diff;
+       struct diff_queue_struct outq;
+       struct diff_score *mx;
+       int i, j, rename_count;
+       int num_create, num_src, dst_cnt;
+
+       if (!minimum_score)
+               minimum_score = DEFAULT_RENAME_SCORE;
+
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filepair *p = q->queue[i];
+               if (!DIFF_FILE_VALID(p->one)) {
+                       if (!DIFF_FILE_VALID(p->two))
+                               continue; /* unmerged */
+                       else if (options->single_follow &&
+                                strcmp(options->single_follow, p->two->path))
+                               continue; /* not interested */
+                       else
+                               locate_rename_dst(p->two, 1);
+               }
+               else if (!DIFF_FILE_VALID(p->two)) {
+                       /*
+                        * If the source is a broken "delete", and
+                        * they did not really want to get broken,
+                        * that means the source actually stays.
+                        * So we increment the "rename_used" score
+                        * by one, to indicate ourselves as a user
+                        */
+                       if (p->broken_pair && !p->score)
+                               p->one->rename_used++;
+                       register_rename_src(p->one, p->score);
+               }
+               else if (detect_rename == DIFF_DETECT_COPY) {
+                       /*
+                        * Increment the "rename_used" score by
+                        * one, to indicate ourselves as a user.
+                        */
+                       p->one->rename_used++;
+                       register_rename_src(p->one, p->score);
+               }
+       }
+       if (rename_dst_nr == 0 || rename_src_nr == 0)
+               goto cleanup; /* nothing to do */
+
+       /*
+        * We really want to cull the candidates list early
+        * with cheap tests in order to avoid doing deltas.
+        */
+       rename_count = find_exact_renames();
+
+       /* Did we only want exact renames? */
+       if (minimum_score == MAX_SCORE)
+               goto cleanup;
+
+       /*
+        * Calculate how many renames are left (but all the source
+        * files still remain as options for rename/copies!)
+        */
+       num_create = (rename_dst_nr - rename_count);
+       num_src = rename_src_nr;
+
+       /* All done? */
+       if (!num_create)
+               goto cleanup;
+
+       /*
+        * This basically does a test for the rename matrix not
+        * growing larger than a "rename_limit" square matrix, ie:
+        *
+        *    num_create * num_src > rename_limit * rename_limit
+        *
+        * but handles the potential overflow case specially (and we
+        * assume at least 32-bit integers)
+        */
+       if (rename_limit <= 0 || rename_limit > 32767)
+               rename_limit = 32767;
+       if (num_create > rename_limit && num_src > rename_limit)
+               goto cleanup;
+       if (num_create * num_src > rename_limit * rename_limit)
+               goto cleanup;
+
+       mx = xmalloc(sizeof(*mx) * num_create * num_src);
+       for (dst_cnt = i = 0; i < rename_dst_nr; i++) {
+               int base = dst_cnt * num_src;
+               struct diff_filespec *two = rename_dst[i].two;
+               if (rename_dst[i].pair)
+                       continue; /* dealt with exact match already. */
+               for (j = 0; j < rename_src_nr; j++) {
+                       struct diff_filespec *one = rename_src[j].one;
+                       struct diff_score *m = &mx[base+j];
+                       m->src = j;
+                       m->dst = i;
+                       m->score = estimate_similarity(one, two,
+                                                      minimum_score);
+                       m->name_score = basename_same(one, two);
+                       diff_free_filespec_blob(one);
+               }
+               /* We do not need the text anymore */
+               diff_free_filespec_blob(two);
+               dst_cnt++;
+       }
+       /* cost matrix sorted by most to least similar pair */
+       qsort(mx, num_create * num_src, sizeof(*mx), score_compare);
+       for (i = 0; i < num_create * num_src; i++) {
+               struct diff_rename_dst *dst = &rename_dst[mx[i].dst];
+               struct diff_filespec *src;
+               if (dst->pair)
+                       continue; /* already done, either exact or fuzzy. */
+               if (mx[i].score < minimum_score)
+                       break; /* there is no more usable pair. */
+               src = rename_src[mx[i].src].one;
+               if (src->rename_used)
+                       continue;
+               record_rename_pair(mx[i].dst, mx[i].src, mx[i].score);
+               rename_count++;
+       }
+       for (i = 0; i < num_create * num_src; i++) {
+               struct diff_rename_dst *dst = &rename_dst[mx[i].dst];
+               if (dst->pair)
+                       continue; /* already done, either exact or fuzzy. */
+               if (mx[i].score < minimum_score)
+                       break; /* there is no more usable pair. */
+               record_rename_pair(mx[i].dst, mx[i].src, mx[i].score);
+               rename_count++;
+       }
+       free(mx);
+
+ cleanup:
+       /* At this point, we have found some renames and copies and they
+        * are recorded in rename_dst.  The original list is still in *q.
+        */
+       outq.queue = NULL;
+       outq.nr = outq.alloc = 0;
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filepair *p = q->queue[i];
+               struct diff_filepair *pair_to_free = NULL;
+
+               if (!DIFF_FILE_VALID(p->one) && DIFF_FILE_VALID(p->two)) {
+                       /*
+                        * Creation
+                        *
+                        * We would output this create record if it has
+                        * not been turned into a rename/copy already.
+                        */
+                       struct diff_rename_dst *dst =
+                               locate_rename_dst(p->two, 0);
+                       if (dst && dst->pair) {
+                               diff_q(&outq, dst->pair);
+                               pair_to_free = p;
+                       }
+                       else
+                               /* no matching rename/copy source, so
+                                * record this as a creation.
+                                */
+                               diff_q(&outq, p);
+               }
+               else if (DIFF_FILE_VALID(p->one) && !DIFF_FILE_VALID(p->two)) {
+                       /*
+                        * Deletion
+                        *
+                        * We would output this delete record if:
+                        *
+                        * (1) this is a broken delete and the counterpart
+                        *     broken create remains in the output; or
+                        * (2) this is not a broken delete, and rename_dst
+                        *     does not have a rename/copy to move p->one->path
+                        *     out of existence.
+                        *
+                        * Otherwise, the counterpart broken create
+                        * has been turned into a rename-edit; or
+                        * delete did not have a matching create to
+                        * begin with.
+                        */
+                       if (DIFF_PAIR_BROKEN(p)) {
+                               /* broken delete */
+                               struct diff_rename_dst *dst =
+                                       locate_rename_dst(p->one, 0);
+                               if (dst && dst->pair)
+                                       /* counterpart is now rename/copy */
+                                       pair_to_free = p;
+                       }
+                       else {
+                               if (p->one->rename_used)
+                                       /* this path remains */
+                                       pair_to_free = p;
+                       }
+
+                       if (pair_to_free)
+                               ;
+                       else
+                               diff_q(&outq, p);
+               }
+               else if (!diff_unmodified_pair(p))
+                       /* all the usual ones need to be kept */
+                       diff_q(&outq, p);
+               else
+                       /* no need to keep unmodified pairs */
+                       pair_to_free = p;
+
+               if (pair_to_free)
+                       diff_free_filepair(pair_to_free);
+       }
+       diff_debug_queue("done copying original", &outq);
+
+       free(q->queue);
+       *q = outq;
+       diff_debug_queue("done collapsing", q);
+
+       for (i = 0; i < rename_dst_nr; i++)
+               free_filespec(rename_dst[i].two);
+
+       free(rename_dst);
+       rename_dst = NULL;
+       rename_dst_nr = rename_dst_alloc = 0;
+       free(rename_src);
+       rename_src = NULL;
+       rename_src_nr = rename_src_alloc = 0;
+       return;
+}
diff --git a/diffcore.h b/diffcore.h
new file mode 100644 (file)
index 0000000..cc96c20
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2005 Junio C Hamano
+ */
+#ifndef DIFFCORE_H
+#define DIFFCORE_H
+
+/* This header file is internal between diff.c and its diff transformers
+ * (e.g. diffcore-rename, diffcore-pickaxe).  Never include this header
+ * in anything else.
+ */
+
+/* We internally use unsigned short as the score value,
+ * and rely on an int capable to hold 32-bits.  -B can take
+ * -Bmerge_score/break_score format and the two scores are
+ * passed around in one int (high 16-bit for merge and low 16-bit
+ * for break).
+ */
+#define MAX_SCORE 60000.0
+#define DEFAULT_RENAME_SCORE 30000 /* rename/copy similarity minimum (50%) */
+#define DEFAULT_BREAK_SCORE  30000 /* minimum for break to happen (50%) */
+#define DEFAULT_MERGE_SCORE  36000 /* maximum for break-merge to happen 60%) */
+
+#define MINIMUM_BREAK_SIZE     400 /* do not break a file smaller than this */
+
+struct diff_filespec {
+       unsigned char sha1[20];
+       char *path;
+       void *data;
+       void *cnt_data;
+       const char *funcname_pattern_ident;
+       unsigned long size;
+       int count;               /* Reference count */
+       int xfrm_flags;          /* for use by the xfrm */
+       int rename_used;         /* Count of rename users */
+       unsigned short mode;     /* file mode */
+       unsigned sha1_valid : 1; /* if true, use sha1 and trust mode;
+                                 * if false, use the name and read from
+                                 * the filesystem.
+                                 */
+#define DIFF_FILE_VALID(spec) (((spec)->mode) != 0)
+       unsigned should_free : 1; /* data should be free()'ed */
+       unsigned should_munmap : 1; /* data should be munmap()'ed */
+       unsigned checked_attr : 1;
+       unsigned is_binary : 1; /* data should be considered "binary" */
+};
+
+extern struct diff_filespec *alloc_filespec(const char *);
+extern void free_filespec(struct diff_filespec *);
+extern void fill_filespec(struct diff_filespec *, const unsigned char *,
+                         unsigned short);
+
+extern int diff_populate_filespec(struct diff_filespec *, int);
+extern void diff_free_filespec_data(struct diff_filespec *);
+extern void diff_free_filespec_blob(struct diff_filespec *);
+extern int diff_filespec_is_binary(struct diff_filespec *);
+
+struct diff_filepair {
+       struct diff_filespec *one;
+       struct diff_filespec *two;
+       unsigned short int score;
+       char status; /* M C R N D U (see Documentation/diff-format.txt) */
+       unsigned broken_pair : 1;
+       unsigned renamed_pair : 1;
+       unsigned is_unmerged : 1;
+};
+#define DIFF_PAIR_UNMERGED(p) ((p)->is_unmerged)
+
+#define DIFF_PAIR_RENAME(p) ((p)->renamed_pair)
+
+#define DIFF_PAIR_BROKEN(p) \
+       ( (!DIFF_FILE_VALID((p)->one) != !DIFF_FILE_VALID((p)->two)) && \
+         ((p)->broken_pair != 0) )
+
+#define DIFF_PAIR_TYPE_CHANGED(p) \
+       ((S_IFMT & (p)->one->mode) != (S_IFMT & (p)->two->mode))
+
+#define DIFF_PAIR_MODE_CHANGED(p) ((p)->one->mode != (p)->two->mode)
+
+extern void diff_free_filepair(struct diff_filepair *);
+
+extern int diff_unmodified_pair(struct diff_filepair *);
+
+struct diff_queue_struct {
+       struct diff_filepair **queue;
+       int alloc;
+       int nr;
+};
+
+extern struct diff_queue_struct diff_queued_diff;
+extern struct diff_filepair *diff_queue(struct diff_queue_struct *,
+                                       struct diff_filespec *,
+                                       struct diff_filespec *);
+extern void diff_q(struct diff_queue_struct *, struct diff_filepair *);
+
+extern void diffcore_pathspec(const char **pathspec);
+extern void diffcore_break(int);
+extern void diffcore_rename(struct diff_options *);
+extern void diffcore_merge_broken(void);
+extern void diffcore_pickaxe(const char *needle, int opts);
+extern void diffcore_order(const char *orderfile);
+
+#define DIFF_DEBUG 0
+#if DIFF_DEBUG
+void diff_debug_filespec(struct diff_filespec *, int, const char *);
+void diff_debug_filepair(const struct diff_filepair *, int);
+void diff_debug_queue(const char *, struct diff_queue_struct *);
+#else
+#define diff_debug_filespec(a,b,c) do {} while(0)
+#define diff_debug_filepair(a,b) do {} while(0)
+#define diff_debug_queue(a,b) do {} while(0)
+#endif
+
+extern int diffcore_count_changes(struct diff_filespec *src,
+                                 struct diff_filespec *dst,
+                                 void **src_count_p,
+                                 void **dst_count_p,
+                                 unsigned long delta_limit,
+                                 unsigned long *src_copied,
+                                 unsigned long *literal_added);
+
+#endif
diff --git a/dir.c b/dir.c
new file mode 100644 (file)
index 0000000..6b3273d
--- /dev/null
+++ b/dir.c
@@ -0,0 +1,792 @@
+/*
+ * This handles recursive filename detection with exclude
+ * files, index knowledge etc..
+ *
+ * Copyright (C) Linus Torvalds, 2005-2006
+ *              Junio Hamano, 2005-2006
+ */
+#include "cache.h"
+#include "dir.h"
+#include "refs.h"
+
+struct path_simplify {
+       int len;
+       const char *path;
+};
+
+static int read_directory_recursive(struct dir_struct *dir,
+       const char *path, const char *base, int baselen,
+       int check_only, const struct path_simplify *simplify);
+
+int common_prefix(const char **pathspec)
+{
+       const char *path, *slash, *next;
+       int prefix;
+
+       if (!pathspec)
+               return 0;
+
+       path = *pathspec;
+       slash = strrchr(path, '/');
+       if (!slash)
+               return 0;
+
+       prefix = slash - path + 1;
+       while ((next = *++pathspec) != NULL) {
+               int len = strlen(next);
+               if (len >= prefix && !memcmp(path, next, prefix))
+                       continue;
+               len = prefix - 1;
+               for (;;) {
+                       if (!len)
+                               return 0;
+                       if (next[--len] != '/')
+                               continue;
+                       if (memcmp(path, next, len+1))
+                               continue;
+                       prefix = len + 1;
+                       break;
+               }
+       }
+       return prefix;
+}
+
+/*
+ * Does 'match' matches the given name?
+ * A match is found if
+ *
+ * (1) the 'match' string is leading directory of 'name', or
+ * (2) the 'match' string is a wildcard and matches 'name', or
+ * (3) the 'match' string is exactly the same as 'name'.
+ *
+ * and the return value tells which case it was.
+ *
+ * It returns 0 when there is no match.
+ */
+static int match_one(const char *match, const char *name, int namelen)
+{
+       int matchlen;
+
+       /* If the match was just the prefix, we matched */
+       matchlen = strlen(match);
+       if (!matchlen)
+               return MATCHED_RECURSIVELY;
+
+       /*
+        * If we don't match the matchstring exactly,
+        * we need to match by fnmatch
+        */
+       if (strncmp(match, name, matchlen))
+               return !fnmatch(match, name, 0) ? MATCHED_FNMATCH : 0;
+
+       if (!name[matchlen])
+               return MATCHED_EXACTLY;
+       if (match[matchlen-1] == '/' || name[matchlen] == '/')
+               return MATCHED_RECURSIVELY;
+       return 0;
+}
+
+/*
+ * Given a name and a list of pathspecs, see if the name matches
+ * any of the pathspecs.  The caller is also interested in seeing
+ * all pathspec matches some names it calls this function with
+ * (otherwise the user could have mistyped the unmatched pathspec),
+ * and a mark is left in seen[] array for pathspec element that
+ * actually matched anything.
+ */
+int match_pathspec(const char **pathspec, const char *name, int namelen, int prefix, char *seen)
+{
+       int retval;
+       const char *match;
+
+       name += prefix;
+       namelen -= prefix;
+
+       for (retval = 0; (match = *pathspec++) != NULL; seen++) {
+               int how;
+               if (retval && *seen == MATCHED_EXACTLY)
+                       continue;
+               match += prefix;
+               how = match_one(match, name, namelen);
+               if (how) {
+                       if (retval < how)
+                               retval = how;
+                       if (*seen < how)
+                               *seen = how;
+               }
+       }
+       return retval;
+}
+
+static int no_wildcard(const char *string)
+{
+       return string[strcspn(string, "*?[{")] == '\0';
+}
+
+void add_exclude(const char *string, const char *base,
+                int baselen, struct exclude_list *which)
+{
+       struct exclude *x = xmalloc(sizeof (*x));
+
+       x->to_exclude = 1;
+       if (*string == '!') {
+               x->to_exclude = 0;
+               string++;
+       }
+       x->pattern = string;
+       x->patternlen = strlen(string);
+       x->base = base;
+       x->baselen = baselen;
+       x->flags = 0;
+       if (!strchr(string, '/'))
+               x->flags |= EXC_FLAG_NODIR;
+       if (no_wildcard(string))
+               x->flags |= EXC_FLAG_NOWILDCARD;
+       if (*string == '*' && no_wildcard(string+1))
+               x->flags |= EXC_FLAG_ENDSWITH;
+       ALLOC_GROW(which->excludes, which->nr + 1, which->alloc);
+       which->excludes[which->nr++] = x;
+}
+
+static int add_excludes_from_file_1(const char *fname,
+                                   const char *base,
+                                   int baselen,
+                                   char **buf_p,
+                                   struct exclude_list *which)
+{
+       struct stat st;
+       int fd, i;
+       size_t size;
+       char *buf, *entry;
+
+       fd = open(fname, O_RDONLY);
+       if (fd < 0 || fstat(fd, &st) < 0)
+               goto err;
+       size = xsize_t(st.st_size);
+       if (size == 0) {
+               close(fd);
+               return 0;
+       }
+       buf = xmalloc(size+1);
+       if (read_in_full(fd, buf, size) != size)
+               goto err;
+       close(fd);
+
+       if (buf_p)
+               *buf_p = buf;
+       buf[size++] = '\n';
+       entry = buf;
+       for (i = 0; i < size; i++) {
+               if (buf[i] == '\n') {
+                       if (entry != buf + i && entry[0] != '#') {
+                               buf[i - (i && buf[i-1] == '\r')] = 0;
+                               add_exclude(entry, base, baselen, which);
+                       }
+                       entry = buf + i + 1;
+               }
+       }
+       return 0;
+
+ err:
+       if (0 <= fd)
+               close(fd);
+       return -1;
+}
+
+void add_excludes_from_file(struct dir_struct *dir, const char *fname)
+{
+       if (add_excludes_from_file_1(fname, "", 0, NULL,
+                                    &dir->exclude_list[EXC_FILE]) < 0)
+               die("cannot use %s as an exclude file", fname);
+}
+
+static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
+{
+       struct exclude_list *el;
+       struct exclude_stack *stk = NULL;
+       int current;
+
+       if ((!dir->exclude_per_dir) ||
+           (baselen + strlen(dir->exclude_per_dir) >= PATH_MAX))
+               return; /* too long a path -- ignore */
+
+       /* Pop the ones that are not the prefix of the path being checked. */
+       el = &dir->exclude_list[EXC_DIRS];
+       while ((stk = dir->exclude_stack) != NULL) {
+               if (stk->baselen <= baselen &&
+                   !strncmp(dir->basebuf, base, stk->baselen))
+                       break;
+               dir->exclude_stack = stk->prev;
+               while (stk->exclude_ix < el->nr)
+                       free(el->excludes[--el->nr]);
+               free(stk->filebuf);
+               free(stk);
+       }
+
+       /* Read from the parent directories and push them down. */
+       current = stk ? stk->baselen : -1;
+       while (current < baselen) {
+               struct exclude_stack *stk = xcalloc(1, sizeof(*stk));
+               const char *cp;
+
+               if (current < 0) {
+                       cp = base;
+                       current = 0;
+               }
+               else {
+                       cp = strchr(base + current + 1, '/');
+                       if (!cp)
+                               die("oops in prep_exclude");
+                       cp++;
+               }
+               stk->prev = dir->exclude_stack;
+               stk->baselen = cp - base;
+               stk->exclude_ix = el->nr;
+               memcpy(dir->basebuf + current, base + current,
+                      stk->baselen - current);
+               strcpy(dir->basebuf + stk->baselen, dir->exclude_per_dir);
+               add_excludes_from_file_1(dir->basebuf,
+                                        dir->basebuf, stk->baselen,
+                                        &stk->filebuf, el);
+               dir->exclude_stack = stk;
+               current = stk->baselen;
+       }
+       dir->basebuf[baselen] = '\0';
+}
+
+/* Scan the list and let the last match determines the fate.
+ * Return 1 for exclude, 0 for include and -1 for undecided.
+ */
+static int excluded_1(const char *pathname,
+                     int pathlen, const char *basename,
+                     struct exclude_list *el)
+{
+       int i;
+
+       if (el->nr) {
+               for (i = el->nr - 1; 0 <= i; i--) {
+                       struct exclude *x = el->excludes[i];
+                       const char *exclude = x->pattern;
+                       int to_exclude = x->to_exclude;
+
+                       if (x->flags & EXC_FLAG_NODIR) {
+                               /* match basename */
+                               if (x->flags & EXC_FLAG_NOWILDCARD) {
+                                       if (!strcmp(exclude, basename))
+                                               return to_exclude;
+                               } else if (x->flags & EXC_FLAG_ENDSWITH) {
+                                       if (x->patternlen - 1 <= pathlen &&
+                                           !strcmp(exclude + 1, pathname + pathlen - x->patternlen + 1))
+                                               return to_exclude;
+                               } else {
+                                       if (fnmatch(exclude, basename, 0) == 0)
+                                               return to_exclude;
+                               }
+                       }
+                       else {
+                               /* match with FNM_PATHNAME:
+                                * exclude has base (baselen long) implicitly
+                                * in front of it.
+                                */
+                               int baselen = x->baselen;
+                               if (*exclude == '/')
+                                       exclude++;
+
+                               if (pathlen < baselen ||
+                                   (baselen && pathname[baselen-1] != '/') ||
+                                   strncmp(pathname, x->base, baselen))
+                                   continue;
+
+                               if (x->flags & EXC_FLAG_NOWILDCARD) {
+                                       if (!strcmp(exclude, pathname + baselen))
+                                               return to_exclude;
+                               } else {
+                                       if (fnmatch(exclude, pathname+baselen,
+                                                   FNM_PATHNAME) == 0)
+                                           return to_exclude;
+                               }
+                       }
+               }
+       }
+       return -1; /* undecided */
+}
+
+int excluded(struct dir_struct *dir, const char *pathname)
+{
+       int pathlen = strlen(pathname);
+       int st;
+       const char *basename = strrchr(pathname, '/');
+       basename = (basename) ? basename+1 : pathname;
+
+       prep_exclude(dir, pathname, basename-pathname);
+       for (st = EXC_CMDL; st <= EXC_FILE; st++) {
+               switch (excluded_1(pathname, pathlen, basename, &dir->exclude_list[st])) {
+               case 0:
+                       return 0;
+               case 1:
+                       return 1;
+               }
+       }
+       return 0;
+}
+
+static struct dir_entry *dir_entry_new(const char *pathname, int len)
+{
+       struct dir_entry *ent;
+
+       ent = xmalloc(sizeof(*ent) + len + 1);
+       ent->len = len;
+       memcpy(ent->name, pathname, len);
+       ent->name[len] = 0;
+       return ent;
+}
+
+struct dir_entry *dir_add_name(struct dir_struct *dir, const char *pathname, int len)
+{
+       if (cache_name_pos(pathname, len) >= 0)
+               return NULL;
+
+       ALLOC_GROW(dir->entries, dir->nr+1, dir->alloc);
+       return dir->entries[dir->nr++] = dir_entry_new(pathname, len);
+}
+
+struct dir_entry *dir_add_ignored(struct dir_struct *dir, const char *pathname, int len)
+{
+       if (cache_name_pos(pathname, len) >= 0)
+               return NULL;
+
+       ALLOC_GROW(dir->ignored, dir->ignored_nr+1, dir->ignored_alloc);
+       return dir->ignored[dir->ignored_nr++] = dir_entry_new(pathname, len);
+}
+
+enum exist_status {
+       index_nonexistent = 0,
+       index_directory,
+       index_gitdir,
+};
+
+/*
+ * The index sorts alphabetically by entry name, which
+ * means that a gitlink sorts as '\0' at the end, while
+ * a directory (which is defined not as an entry, but as
+ * the files it contains) will sort with the '/' at the
+ * end.
+ */
+static enum exist_status directory_exists_in_index(const char *dirname, int len)
+{
+       int pos = cache_name_pos(dirname, len);
+       if (pos < 0)
+               pos = -pos-1;
+       while (pos < active_nr) {
+               struct cache_entry *ce = active_cache[pos++];
+               unsigned char endchar;
+
+               if (strncmp(ce->name, dirname, len))
+                       break;
+               endchar = ce->name[len];
+               if (endchar > '/')
+                       break;
+               if (endchar == '/')
+                       return index_directory;
+               if (!endchar && S_ISGITLINK(ntohl(ce->ce_mode)))
+                       return index_gitdir;
+       }
+       return index_nonexistent;
+}
+
+/*
+ * When we find a directory when traversing the filesystem, we
+ * have three distinct cases:
+ *
+ *  - ignore it
+ *  - see it as a directory
+ *  - recurse into it
+ *
+ * and which one we choose depends on a combination of existing
+ * git index contents and the flags passed into the directory
+ * traversal routine.
+ *
+ * Case 1: If we *already* have entries in the index under that
+ * directory name, we always recurse into the directory to see
+ * all the files.
+ *
+ * Case 2: If we *already* have that directory name as a gitlink,
+ * we always continue to see it as a gitlink, regardless of whether
+ * there is an actual git directory there or not (it might not
+ * be checked out as a subproject!)
+ *
+ * Case 3: if we didn't have it in the index previously, we
+ * have a few sub-cases:
+ *
+ *  (a) if "show_other_directories" is true, we show it as
+ *      just a directory, unless "hide_empty_directories" is
+ *      also true and the directory is empty, in which case
+ *      we just ignore it entirely.
+ *  (b) if it looks like a git directory, and we don't have
+ *      'no_gitlinks' set we treat it as a gitlink, and show it
+ *      as a directory.
+ *  (c) otherwise, we recurse into it.
+ */
+enum directory_treatment {
+       show_directory,
+       ignore_directory,
+       recurse_into_directory,
+};
+
+static enum directory_treatment treat_directory(struct dir_struct *dir,
+       const char *dirname, int len,
+       const struct path_simplify *simplify)
+{
+       /* The "len-1" is to strip the final '/' */
+       switch (directory_exists_in_index(dirname, len-1)) {
+       case index_directory:
+               return recurse_into_directory;
+
+       case index_gitdir:
+               if (dir->show_other_directories)
+                       return ignore_directory;
+               return show_directory;
+
+       case index_nonexistent:
+               if (dir->show_other_directories)
+                       break;
+               if (!dir->no_gitlinks) {
+                       unsigned char sha1[20];
+                       if (resolve_gitlink_ref(dirname, "HEAD", sha1) == 0)
+                               return show_directory;
+               }
+               return recurse_into_directory;
+       }
+
+       /* This is the "show_other_directories" case */
+       if (!dir->hide_empty_directories)
+               return show_directory;
+       if (!read_directory_recursive(dir, dirname, dirname, len, 1, simplify))
+               return ignore_directory;
+       return show_directory;
+}
+
+/*
+ * This is an inexact early pruning of any recursive directory
+ * reading - if the path cannot possibly be in the pathspec,
+ * return true, and we'll skip it early.
+ */
+static int simplify_away(const char *path, int pathlen, const struct path_simplify *simplify)
+{
+       if (simplify) {
+               for (;;) {
+                       const char *match = simplify->path;
+                       int len = simplify->len;
+
+                       if (!match)
+                               break;
+                       if (len > pathlen)
+                               len = pathlen;
+                       if (!memcmp(path, match, len))
+                               return 0;
+                       simplify++;
+               }
+               return 1;
+       }
+       return 0;
+}
+
+static int in_pathspec(const char *path, int len, const struct path_simplify *simplify)
+{
+       if (simplify) {
+               for (; simplify->path; simplify++) {
+                       if (len == simplify->len
+                           && !memcmp(path, simplify->path, len))
+                               return 1;
+               }
+       }
+       return 0;
+}
+
+static int get_dtype(struct dirent *de, const char *path)
+{
+       int dtype = DTYPE(de);
+       struct stat st;
+
+       if (dtype != DT_UNKNOWN)
+               return dtype;
+       if (lstat(path, &st))
+               return dtype;
+       if (S_ISREG(st.st_mode))
+               return DT_REG;
+       if (S_ISDIR(st.st_mode))
+               return DT_DIR;
+       if (S_ISLNK(st.st_mode))
+               return DT_LNK;
+       return dtype;
+}
+
+/*
+ * Read a directory tree. We currently ignore anything but
+ * directories, regular files and symlinks. That's because git
+ * doesn't handle them at all yet. Maybe that will change some
+ * day.
+ *
+ * Also, we ignore the name ".git" (even if it is not a directory).
+ * That likely will not change.
+ */
+static int read_directory_recursive(struct dir_struct *dir, const char *path, const char *base, int baselen, int check_only, const struct path_simplify *simplify)
+{
+       DIR *fdir = opendir(path);
+       int contents = 0;
+
+       if (fdir) {
+               struct dirent *de;
+               char fullname[PATH_MAX + 1];
+               memcpy(fullname, base, baselen);
+
+               while ((de = readdir(fdir)) != NULL) {
+                       int len, dtype;
+                       int exclude;
+
+                       if ((de->d_name[0] == '.') &&
+                           (de->d_name[1] == 0 ||
+                            !strcmp(de->d_name + 1, ".") ||
+                            !strcmp(de->d_name + 1, "git")))
+                               continue;
+                       len = strlen(de->d_name);
+                       /* Ignore overly long pathnames! */
+                       if (len + baselen + 8 > sizeof(fullname))
+                               continue;
+                       memcpy(fullname + baselen, de->d_name, len+1);
+                       if (simplify_away(fullname, baselen + len, simplify))
+                               continue;
+
+                       exclude = excluded(dir, fullname);
+                       if (exclude && dir->collect_ignored
+                           && in_pathspec(fullname, baselen + len, simplify))
+                               dir_add_ignored(dir, fullname, baselen + len);
+
+                       /*
+                        * Excluded? If we don't explicitly want to show
+                        * ignored files, ignore it
+                        */
+                       if (exclude && !dir->show_ignored)
+                               continue;
+
+                       dtype = get_dtype(de, fullname);
+
+                       /*
+                        * Do we want to see just the ignored files?
+                        * We still need to recurse into directories,
+                        * even if we don't ignore them, since the
+                        * directory may contain files that we do..
+                        */
+                       if (!exclude && dir->show_ignored) {
+                               if (dtype != DT_DIR)
+                                       continue;
+                       }
+
+                       switch (dtype) {
+                       default:
+                               continue;
+                       case DT_DIR:
+                               memcpy(fullname + baselen + len, "/", 2);
+                               len++;
+                               switch (treat_directory(dir, fullname, baselen + len, simplify)) {
+                               case show_directory:
+                                       if (exclude != dir->show_ignored)
+                                               continue;
+                                       break;
+                               case recurse_into_directory:
+                                       contents += read_directory_recursive(dir,
+                                               fullname, fullname, baselen + len, 0, simplify);
+                                       continue;
+                               case ignore_directory:
+                                       continue;
+                               }
+                               break;
+                       case DT_REG:
+                       case DT_LNK:
+                               break;
+                       }
+                       contents++;
+                       if (check_only)
+                               goto exit_early;
+                       else
+                               dir_add_name(dir, fullname, baselen + len);
+               }
+exit_early:
+               closedir(fdir);
+       }
+
+       return contents;
+}
+
+static int cmp_name(const void *p1, const void *p2)
+{
+       const struct dir_entry *e1 = *(const struct dir_entry **)p1;
+       const struct dir_entry *e2 = *(const struct dir_entry **)p2;
+
+       return cache_name_compare(e1->name, e1->len,
+                                 e2->name, e2->len);
+}
+
+/*
+ * Return the length of the "simple" part of a path match limiter.
+ */
+static int simple_length(const char *match)
+{
+       const char special[256] = {
+               [0] = 1, ['?'] = 1,
+               ['\\'] = 1, ['*'] = 1,
+               ['['] = 1
+       };
+       int len = -1;
+
+       for (;;) {
+               unsigned char c = *match++;
+               len++;
+               if (special[c])
+                       return len;
+       }
+}
+
+static struct path_simplify *create_simplify(const char **pathspec)
+{
+       int nr, alloc = 0;
+       struct path_simplify *simplify = NULL;
+
+       if (!pathspec)
+               return NULL;
+
+       for (nr = 0 ; ; nr++) {
+               const char *match;
+               if (nr >= alloc) {
+                       alloc = alloc_nr(alloc);
+                       simplify = xrealloc(simplify, alloc * sizeof(*simplify));
+               }
+               match = *pathspec++;
+               if (!match)
+                       break;
+               simplify[nr].path = match;
+               simplify[nr].len = simple_length(match);
+       }
+       simplify[nr].path = NULL;
+       simplify[nr].len = 0;
+       return simplify;
+}
+
+static void free_simplify(struct path_simplify *simplify)
+{
+       if (simplify)
+               free(simplify);
+}
+
+int read_directory(struct dir_struct *dir, const char *path, const char *base, int baselen, const char **pathspec)
+{
+       struct path_simplify *simplify = create_simplify(pathspec);
+
+       read_directory_recursive(dir, path, base, baselen, 0, simplify);
+       free_simplify(simplify);
+       qsort(dir->entries, dir->nr, sizeof(struct dir_entry *), cmp_name);
+       qsort(dir->ignored, dir->ignored_nr, sizeof(struct dir_entry *), cmp_name);
+       return dir->nr;
+}
+
+int file_exists(const char *f)
+{
+       struct stat sb;
+       return lstat(f, &sb) == 0;
+}
+
+/*
+ * get_relative_cwd() gets the prefix of the current working directory
+ * relative to 'dir'.  If we are not inside 'dir', it returns NULL.
+ *
+ * As a convenience, it also returns NULL if 'dir' is already NULL.  The
+ * reason for this behaviour is that it is natural for functions returning
+ * directory names to return NULL to say "this directory does not exist"
+ * or "this directory is invalid".  These cases are usually handled the
+ * same as if the cwd is not inside 'dir' at all, so get_relative_cwd()
+ * returns NULL for both of them.
+ *
+ * Most notably, get_relative_cwd(buffer, size, get_git_work_tree())
+ * unifies the handling of "outside work tree" with "no work tree at all".
+ */
+char *get_relative_cwd(char *buffer, int size, const char *dir)
+{
+       char *cwd = buffer;
+
+       if (!dir)
+               return NULL;
+       if (!getcwd(buffer, size))
+               die("can't find the current directory: %s", strerror(errno));
+
+       if (!is_absolute_path(dir))
+               dir = make_absolute_path(dir);
+
+       while (*dir && *dir == *cwd) {
+               dir++;
+               cwd++;
+       }
+       if (*dir)
+               return NULL;
+       if (*cwd == '/')
+               return cwd + 1;
+       return cwd;
+}
+
+int is_inside_dir(const char *dir)
+{
+       char buffer[PATH_MAX];
+       return get_relative_cwd(buffer, sizeof(buffer), dir) != NULL;
+}
+
+int remove_dir_recursively(struct strbuf *path, int only_empty)
+{
+       DIR *dir = opendir(path->buf);
+       struct dirent *e;
+       int ret = 0, original_len = path->len, len;
+
+       if (!dir)
+               return -1;
+       if (path->buf[original_len - 1] != '/')
+               strbuf_addch(path, '/');
+
+       len = path->len;
+       while ((e = readdir(dir)) != NULL) {
+               struct stat st;
+               if ((e->d_name[0] == '.') &&
+                   ((e->d_name[1] == 0) ||
+                    ((e->d_name[1] == '.') && e->d_name[2] == 0)))
+                       continue; /* "." and ".." */
+
+               strbuf_setlen(path, len);
+               strbuf_addstr(path, e->d_name);
+               if (lstat(path->buf, &st))
+                       ; /* fall thru */
+               else if (S_ISDIR(st.st_mode)) {
+                       if (!remove_dir_recursively(path, only_empty))
+                               continue; /* happy */
+               } else if (!only_empty && !unlink(path->buf))
+                       continue; /* happy, too */
+
+               /* path too long, stat fails, or non-directory still exists */
+               ret = -1;
+               break;
+       }
+       closedir(dir);
+
+       strbuf_setlen(path, original_len);
+       if (!ret)
+               ret = rmdir(path->buf);
+       return ret;
+}
+
+void setup_standard_excludes(struct dir_struct *dir)
+{
+       const char *path;
+
+       dir->exclude_per_dir = ".gitignore";
+       path = git_path("info/exclude");
+       if (!access(path, R_OK))
+               add_excludes_from_file(dir, path);
+       if (excludes_file && !access(excludes_file, R_OK))
+               add_excludes_from_file(dir, excludes_file);
+}
diff --git a/dir.h b/dir.h
new file mode 100644 (file)
index 0000000..d8814dc
--- /dev/null
+++ b/dir.h
@@ -0,0 +1,83 @@
+#ifndef DIR_H
+#define DIR_H
+
+struct dir_entry {
+       unsigned int len;
+       char name[FLEX_ARRAY]; /* more */
+};
+
+#define EXC_FLAG_NODIR 1
+#define EXC_FLAG_NOWILDCARD 2
+#define EXC_FLAG_ENDSWITH 4
+
+struct exclude_list {
+       int nr;
+       int alloc;
+       struct exclude {
+               const char *pattern;
+               int patternlen;
+               const char *base;
+               int baselen;
+               int to_exclude;
+               int flags;
+       } **excludes;
+};
+
+struct exclude_stack {
+       struct exclude_stack *prev;
+       char *filebuf;
+       int baselen;
+       int exclude_ix;
+};
+
+struct dir_struct {
+       int nr, alloc;
+       int ignored_nr, ignored_alloc;
+       unsigned int show_ignored:1,
+                    show_other_directories:1,
+                    hide_empty_directories:1,
+                    no_gitlinks:1,
+                    collect_ignored:1;
+       struct dir_entry **entries;
+       struct dir_entry **ignored;
+
+       /* Exclude info */
+       const char *exclude_per_dir;
+       struct exclude_list exclude_list[3];
+       /*
+        * We maintain three exclude pattern lists:
+        * EXC_CMDL lists patterns explicitly given on the command line.
+        * EXC_DIRS lists patterns obtained from per-directory ignore files.
+        * EXC_FILE lists patterns from fallback ignore files.
+        */
+#define EXC_CMDL 0
+#define EXC_DIRS 1
+#define EXC_FILE 2
+
+       struct exclude_stack *exclude_stack;
+       char basebuf[PATH_MAX];
+};
+
+extern int common_prefix(const char **pathspec);
+
+#define MATCHED_RECURSIVELY 1
+#define MATCHED_FNMATCH 2
+#define MATCHED_EXACTLY 3
+extern int match_pathspec(const char **pathspec, const char *name, int namelen, int prefix, char *seen);
+
+extern int read_directory(struct dir_struct *, const char *path, const char *base, int baselen, const char **pathspec);
+
+extern int excluded(struct dir_struct *, const char *);
+extern void add_excludes_from_file(struct dir_struct *, const char *fname);
+extern void add_exclude(const char *string, const char *base,
+                       int baselen, struct exclude_list *which);
+extern int file_exists(const char *);
+extern struct dir_entry *dir_add_name(struct dir_struct *dir, const char *pathname, int len);
+
+extern char *get_relative_cwd(char *buffer, int size, const char *dir);
+extern int is_inside_dir(const char *dir);
+
+extern void setup_standard_excludes(struct dir_struct *dir);
+extern int remove_dir_recursively(struct strbuf *path, int only_empty);
+
+#endif
diff --git a/dump-cache-tree.c b/dump-cache-tree.c
new file mode 100644 (file)
index 0000000..1f73f1e
--- /dev/null
@@ -0,0 +1,64 @@
+#include "cache.h"
+#include "tree.h"
+#include "cache-tree.h"
+
+
+static void dump_one(struct cache_tree *it, const char *pfx, const char *x)
+{
+       if (it->entry_count < 0)
+               printf("%-40s %s%s (%d subtrees)\n",
+                      "invalid", x, pfx, it->subtree_nr);
+       else
+               printf("%s %s%s (%d entries, %d subtrees)\n",
+                      sha1_to_hex(it->sha1), x, pfx,
+                      it->entry_count, it->subtree_nr);
+}
+
+static int dump_cache_tree(struct cache_tree *it,
+                          struct cache_tree *ref,
+                          const char *pfx)
+{
+       int i;
+       int errs = 0;
+
+       if (!it || !ref)
+               /* missing in either */
+               return 0;
+
+       if (it->entry_count < 0) {
+               dump_one(it, pfx, "");
+               dump_one(ref, pfx, "#(ref) ");
+               if (it->subtree_nr != ref->subtree_nr)
+                       errs = 1;
+       }
+       else {
+               dump_one(it, pfx, "");
+               if (hashcmp(it->sha1, ref->sha1) ||
+                   ref->entry_count != it->entry_count ||
+                   ref->subtree_nr != it->subtree_nr) {
+                       dump_one(ref, pfx, "#(ref) ");
+                       errs = 1;
+               }
+       }
+
+       for (i = 0; i < it->subtree_nr; i++) {
+               char path[PATH_MAX];
+               struct cache_tree_sub *down = it->down[i];
+               struct cache_tree_sub *rdwn;
+
+               rdwn = cache_tree_sub(ref, down->name);
+               sprintf(path, "%s%.*s/", pfx, down->namelen, down->name);
+               if (dump_cache_tree(down->cache_tree, rdwn->cache_tree, path))
+                       errs = 1;
+       }
+       return errs;
+}
+
+int main(int ac, char **av)
+{
+       struct cache_tree *another = cache_tree();
+       if (read_cache() < 0)
+               die("unable to read index file");
+       cache_tree_update(another, active_cache, active_nr, 0, 1);
+       return dump_cache_tree(active_cache_tree, another, "");
+}
diff --git a/entry.c b/entry.c
new file mode 100644 (file)
index 0000000..257ab46
--- /dev/null
+++ b/entry.c
@@ -0,0 +1,234 @@
+#include "cache.h"
+#include "blob.h"
+
+static void create_directories(const char *path, const struct checkout *state)
+{
+       int len = strlen(path);
+       char *buf = xmalloc(len + 1);
+       const char *slash = path;
+
+       while ((slash = strchr(slash+1, '/')) != NULL) {
+               struct stat st;
+               int stat_status;
+
+               len = slash - path;
+               memcpy(buf, path, len);
+               buf[len] = 0;
+
+               if (len <= state->base_dir_len)
+                       /*
+                        * checkout-index --prefix=<dir>; <dir> is
+                        * allowed to be a symlink to an existing
+                        * directory.
+                        */
+                       stat_status = stat(buf, &st);
+               else
+                       /*
+                        * if there currently is a symlink, we would
+                        * want to replace it with a real directory.
+                        */
+                       stat_status = lstat(buf, &st);
+
+               if (!stat_status && S_ISDIR(st.st_mode))
+                       continue; /* ok, it is already a directory. */
+
+               /*
+                * We know stat_status == 0 means something exists
+                * there and this mkdir would fail, but that is an
+                * error codepath; we do not care, as we unlink and
+                * mkdir again in such a case.
+                */
+               if (mkdir(buf, 0777)) {
+                       if (errno == EEXIST && state->force &&
+                           !unlink(buf) && !mkdir(buf, 0777))
+                               continue;
+                       die("cannot create directory at %s", buf);
+               }
+       }
+       free(buf);
+}
+
+static void remove_subtree(const char *path)
+{
+       DIR *dir = opendir(path);
+       struct dirent *de;
+       char pathbuf[PATH_MAX];
+       char *name;
+
+       if (!dir)
+               die("cannot opendir %s (%s)", path, strerror(errno));
+       strcpy(pathbuf, path);
+       name = pathbuf + strlen(path);
+       *name++ = '/';
+       while ((de = readdir(dir)) != NULL) {
+               struct stat st;
+               if ((de->d_name[0] == '.') &&
+                   ((de->d_name[1] == 0) ||
+                    ((de->d_name[1] == '.') && de->d_name[2] == 0)))
+                       continue;
+               strcpy(name, de->d_name);
+               if (lstat(pathbuf, &st))
+                       die("cannot lstat %s (%s)", pathbuf, strerror(errno));
+               if (S_ISDIR(st.st_mode))
+                       remove_subtree(pathbuf);
+               else if (unlink(pathbuf))
+                       die("cannot unlink %s (%s)", pathbuf, strerror(errno));
+       }
+       closedir(dir);
+       if (rmdir(path))
+               die("cannot rmdir %s (%s)", path, strerror(errno));
+}
+
+static int create_file(const char *path, unsigned int mode)
+{
+       mode = (mode & 0100) ? 0777 : 0666;
+       return open(path, O_WRONLY | O_CREAT | O_EXCL, mode);
+}
+
+static void *read_blob_entry(struct cache_entry *ce, const char *path, unsigned long *size)
+{
+       enum object_type type;
+       void *new = read_sha1_file(ce->sha1, &type, size);
+
+       if (new) {
+               if (type == OBJ_BLOB)
+                       return new;
+               free(new);
+       }
+       return NULL;
+}
+
+static int write_entry(struct cache_entry *ce, char *path, const struct checkout *state, int to_tempfile)
+{
+       int fd;
+       long wrote;
+
+       switch (ntohl(ce->ce_mode) & S_IFMT) {
+               char *new;
+               struct strbuf buf;
+               unsigned long size;
+
+       case S_IFREG:
+               new = read_blob_entry(ce, path, &size);
+               if (!new)
+                       return error("git-checkout-index: unable to read sha1 file of %s (%s)",
+                               path, sha1_to_hex(ce->sha1));
+
+               /*
+                * Convert from git internal format to working tree format
+                */
+               strbuf_init(&buf, 0);
+               if (convert_to_working_tree(ce->name, new, size, &buf)) {
+                       size_t newsize = 0;
+                       free(new);
+                       new = strbuf_detach(&buf, &newsize);
+                       size = newsize;
+               }
+
+               if (to_tempfile) {
+                       strcpy(path, ".merge_file_XXXXXX");
+                       fd = mkstemp(path);
+               } else
+                       fd = create_file(path, ntohl(ce->ce_mode));
+               if (fd < 0) {
+                       free(new);
+                       return error("git-checkout-index: unable to create file %s (%s)",
+                               path, strerror(errno));
+               }
+
+               wrote = write_in_full(fd, new, size);
+               close(fd);
+               free(new);
+               if (wrote != size)
+                       return error("git-checkout-index: unable to write file %s", path);
+               break;
+       case S_IFLNK:
+               new = read_blob_entry(ce, path, &size);
+               if (!new)
+                       return error("git-checkout-index: unable to read sha1 file of %s (%s)",
+                               path, sha1_to_hex(ce->sha1));
+               if (to_tempfile || !has_symlinks) {
+                       if (to_tempfile) {
+                               strcpy(path, ".merge_link_XXXXXX");
+                               fd = mkstemp(path);
+                       } else
+                               fd = create_file(path, 0666);
+                       if (fd < 0) {
+                               free(new);
+                               return error("git-checkout-index: unable to create "
+                                                "file %s (%s)", path, strerror(errno));
+                       }
+                       wrote = write_in_full(fd, new, size);
+                       close(fd);
+                       free(new);
+                       if (wrote != size)
+                               return error("git-checkout-index: unable to write file %s",
+                                       path);
+               } else {
+                       wrote = symlink(new, path);
+                       free(new);
+                       if (wrote)
+                               return error("git-checkout-index: unable to create "
+                                                "symlink %s (%s)", path, strerror(errno));
+               }
+               break;
+       case S_IFGITLINK:
+               if (to_tempfile)
+                       return error("git-checkout-index: cannot create temporary subproject %s", path);
+               if (mkdir(path, 0777) < 0)
+                       return error("git-checkout-index: cannot create subproject directory %s", path);
+               break;
+       default:
+               return error("git-checkout-index: unknown file mode for %s", path);
+       }
+
+       if (state->refresh_cache) {
+               struct stat st;
+               lstat(ce->name, &st);
+               fill_stat_cache_info(ce, &st);
+       }
+       return 0;
+}
+
+int checkout_entry(struct cache_entry *ce, const struct checkout *state, char *topath)
+{
+       static char path[PATH_MAX + 1];
+       struct stat st;
+       int len = state->base_dir_len;
+
+       if (topath)
+               return write_entry(ce, topath, state, 1);
+
+       memcpy(path, state->base_dir, len);
+       strcpy(path + len, ce->name);
+
+       if (!lstat(path, &st)) {
+               unsigned changed = ce_match_stat(ce, &st, CE_MATCH_IGNORE_VALID);
+               if (!changed)
+                       return 0;
+               if (!state->force) {
+                       if (!state->quiet)
+                               fprintf(stderr, "git-checkout-index: %s already exists\n", path);
+                       return -1;
+               }
+
+               /*
+                * We unlink the old file, to get the new one with the
+                * right permissions (including umask, which is nasty
+                * to emulate by hand - much easier to let the system
+                * just do the right thing)
+                */
+               unlink(path);
+               if (S_ISDIR(st.st_mode)) {
+                       /* If it is a gitlink, leave it alone! */
+                       if (S_ISGITLINK(ntohl(ce->ce_mode)))
+                               return 0;
+                       if (!state->force)
+                               return error("%s is a directory", path);
+                       remove_subtree(path);
+               }
+       } else if (state->not_new)
+               return 0;
+       create_directories(path, state);
+       return write_entry(ce, path, state, 0);
+}
diff --git a/environment.c b/environment.c
new file mode 100644 (file)
index 0000000..1dab72e
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * We put all the git config variables in this same object
+ * file, so that programs can link against the config parser
+ * without having to link against all the rest of git.
+ *
+ * In particular, no need to bring in libz etc unless needed,
+ * even if you might want to know where the git directory etc
+ * are.
+ */
+#include "cache.h"
+
+char git_default_email[MAX_GITNAME];
+char git_default_name[MAX_GITNAME];
+int trust_executable_bit = 1;
+int quote_path_fully = 1;
+int has_symlinks = 1;
+int assume_unchanged;
+int prefer_symlink_refs;
+int is_bare_repository_cfg = -1; /* unspecified */
+int log_all_ref_updates = -1; /* unspecified */
+int warn_ambiguous_refs = 1;
+int repository_format_version;
+const char *git_commit_encoding;
+const char *git_log_output_encoding;
+int shared_repository = PERM_UMASK;
+const char *apply_default_whitespace;
+int zlib_compression_level = Z_BEST_SPEED;
+int core_compression_level;
+int core_compression_seen;
+size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;
+size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;
+size_t delta_base_cache_limit = 16 * 1024 * 1024;
+char *pager_program;
+int pager_in_use;
+int pager_use_color = 1;
+char *editor_program;
+char *excludes_file;
+int auto_crlf = 0;     /* 1: both ways, -1: only when adding git objects */
+
+/* This is set by setup_git_dir_gently() and/or git_default_config() */
+char *git_work_tree_cfg;
+static const char *work_tree;
+
+static const char *git_dir;
+static char *git_object_dir, *git_index_file, *git_refs_dir, *git_graft_file;
+
+static void setup_git_env(void)
+{
+       git_dir = getenv(GIT_DIR_ENVIRONMENT);
+       if (!git_dir)
+               git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
+       git_object_dir = getenv(DB_ENVIRONMENT);
+       if (!git_object_dir) {
+               git_object_dir = xmalloc(strlen(git_dir) + 9);
+               sprintf(git_object_dir, "%s/objects", git_dir);
+       }
+       git_refs_dir = xmalloc(strlen(git_dir) + 6);
+       sprintf(git_refs_dir, "%s/refs", git_dir);
+       git_index_file = getenv(INDEX_ENVIRONMENT);
+       if (!git_index_file) {
+               git_index_file = xmalloc(strlen(git_dir) + 7);
+               sprintf(git_index_file, "%s/index", git_dir);
+       }
+       git_graft_file = getenv(GRAFT_ENVIRONMENT);
+       if (!git_graft_file)
+               git_graft_file = xstrdup(git_path("info/grafts"));
+}
+
+int is_bare_repository(void)
+{
+       /* if core.bare is not 'false', let's see if there is a work tree */
+       return is_bare_repository_cfg && !get_git_work_tree();
+}
+
+const char *get_git_dir(void)
+{
+       if (!git_dir)
+               setup_git_env();
+       return git_dir;
+}
+
+const char *get_git_work_tree(void)
+{
+       static int initialized = 0;
+       if (!initialized) {
+               work_tree = getenv(GIT_WORK_TREE_ENVIRONMENT);
+               /* core.bare = true overrides implicit and config work tree */
+               if (!work_tree && is_bare_repository_cfg < 1) {
+                       work_tree = git_work_tree_cfg;
+                       /* make_absolute_path also normalizes the path */
+                       if (work_tree && !is_absolute_path(work_tree))
+                               work_tree = xstrdup(make_absolute_path(git_path(work_tree)));
+               } else if (work_tree)
+                       work_tree = xstrdup(make_absolute_path(work_tree));
+               initialized = 1;
+               if (work_tree)
+                       is_bare_repository_cfg = 0;
+       }
+       return work_tree;
+}
+
+char *get_object_directory(void)
+{
+       if (!git_object_dir)
+               setup_git_env();
+       return git_object_dir;
+}
+
+char *get_refs_directory(void)
+{
+       if (!git_refs_dir)
+               setup_git_env();
+       return git_refs_dir;
+}
+
+char *get_index_file(void)
+{
+       if (!git_index_file)
+               setup_git_env();
+       return git_index_file;
+}
+
+char *get_graft_file(void)
+{
+       if (!git_graft_file)
+               setup_git_env();
+       return git_graft_file;
+}
+
+int set_git_dir(const char *path)
+{
+       if (setenv(GIT_DIR_ENVIRONMENT, path, 1))
+               return error("Could not set GIT_DIR to '%s'", path);
+       setup_git_env();
+       return 0;
+}
diff --git a/exec_cmd.c b/exec_cmd.c
new file mode 100644 (file)
index 0000000..e189cac
--- /dev/null
@@ -0,0 +1,119 @@
+#include "cache.h"
+#include "exec_cmd.h"
+#include "quote.h"
+#define MAX_ARGS       32
+
+extern char **environ;
+static const char *builtin_exec_path = GIT_EXEC_PATH;
+static const char *argv_exec_path;
+
+void git_set_argv_exec_path(const char *exec_path)
+{
+       argv_exec_path = exec_path;
+}
+
+
+/* Returns the highest-priority, location to look for git programs. */
+const char *git_exec_path(void)
+{
+       const char *env;
+
+       if (argv_exec_path)
+               return argv_exec_path;
+
+       env = getenv(EXEC_PATH_ENVIRONMENT);
+       if (env && *env) {
+               return env;
+       }
+
+       return builtin_exec_path;
+}
+
+static void add_path(struct strbuf *out, const char *path)
+{
+       if (path && *path) {
+               if (is_absolute_path(path))
+                       strbuf_addstr(out, path);
+               else
+                       strbuf_addstr(out, make_absolute_path(path));
+
+               strbuf_addch(out, ':');
+       }
+}
+
+void setup_path(const char *cmd_path)
+{
+       const char *old_path = getenv("PATH");
+       struct strbuf new_path;
+
+       strbuf_init(&new_path, 0);
+
+       add_path(&new_path, argv_exec_path);
+       add_path(&new_path, getenv(EXEC_PATH_ENVIRONMENT));
+       add_path(&new_path, builtin_exec_path);
+       add_path(&new_path, cmd_path);
+
+       if (old_path)
+               strbuf_addstr(&new_path, old_path);
+       else
+               strbuf_addstr(&new_path, "/usr/local/bin:/usr/bin:/bin");
+
+       setenv("PATH", new_path.buf, 1);
+
+       strbuf_release(&new_path);
+}
+
+int execv_git_cmd(const char **argv)
+{
+       struct strbuf cmd;
+       const char *tmp;
+
+       strbuf_init(&cmd, 0);
+       strbuf_addf(&cmd, "git-%s", argv[0]);
+
+       /*
+        * argv[0] must be the git command, but the argv array
+        * belongs to the caller, and may be reused in
+        * subsequent loop iterations. Save argv[0] and
+        * restore it on error.
+        */
+       tmp = argv[0];
+       argv[0] = cmd.buf;
+
+       trace_argv_printf(argv, "trace: exec:");
+
+       /* execvp() can only ever return if it fails */
+       execvp(cmd.buf, (char **)argv);
+
+       trace_printf("trace: exec failed: %s\n", strerror(errno));
+
+       argv[0] = tmp;
+
+       strbuf_release(&cmd);
+
+       return -1;
+}
+
+
+int execl_git_cmd(const char *cmd,...)
+{
+       int argc;
+       const char *argv[MAX_ARGS + 1];
+       const char *arg;
+       va_list param;
+
+       va_start(param, cmd);
+       argv[0] = cmd;
+       argc = 1;
+       while (argc < MAX_ARGS) {
+               arg = argv[argc++] = va_arg(param, char *);
+               if (!arg)
+                       break;
+       }
+       va_end(param);
+       if (MAX_ARGS <= argc)
+               return error("too many args to run %s", cmd);
+
+       argv[argc] = NULL;
+       return execv_git_cmd(argv);
+}
diff --git a/exec_cmd.h b/exec_cmd.h
new file mode 100644 (file)
index 0000000..a892355
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef GIT_EXEC_CMD_H
+#define GIT_EXEC_CMD_H
+
+extern void git_set_argv_exec_path(const char *exec_path);
+extern const char* git_exec_path(void);
+extern void setup_path(const char *);
+extern int execv_git_cmd(const char **argv); /* NULL terminated */
+extern int execl_git_cmd(const char *cmd, ...);
+
+
+#endif /* GIT_EXEC_CMD_H */
diff --git a/fast-import.c b/fast-import.c
new file mode 100644 (file)
index 0000000..98c2bd5
--- /dev/null
@@ -0,0 +1,2386 @@
+/*
+Format of STDIN stream:
+
+  stream ::= cmd*;
+
+  cmd ::= new_blob
+        | new_commit
+        | new_tag
+        | reset_branch
+        | checkpoint
+        | progress
+        ;
+
+  new_blob ::= 'blob' lf
+    mark?
+    file_content;
+  file_content ::= data;
+
+  new_commit ::= 'commit' sp ref_str lf
+    mark?
+    ('author' sp name '<' email '>' when lf)?
+    'committer' sp name '<' email '>' when lf
+    commit_msg
+    ('from' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf)?
+    ('merge' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf)*
+    file_change*
+    lf?;
+  commit_msg ::= data;
+
+  file_change ::= file_clr
+    | file_del
+    | file_rnm
+    | file_cpy
+    | file_obm
+    | file_inm;
+  file_clr ::= 'deleteall' lf;
+  file_del ::= 'D' sp path_str lf;
+  file_rnm ::= 'R' sp path_str sp path_str lf;
+  file_cpy ::= 'C' sp path_str sp path_str lf;
+  file_obm ::= 'M' sp mode sp (hexsha1 | idnum) sp path_str lf;
+  file_inm ::= 'M' sp mode sp 'inline' sp path_str lf
+    data;
+
+  new_tag ::= 'tag' sp tag_str lf
+    'from' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf
+    'tagger' sp name '<' email '>' when lf
+    tag_msg;
+  tag_msg ::= data;
+
+  reset_branch ::= 'reset' sp ref_str lf
+    ('from' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf)?
+    lf?;
+
+  checkpoint ::= 'checkpoint' lf
+    lf?;
+
+  progress ::= 'progress' sp not_lf* lf
+    lf?;
+
+     # note: the first idnum in a stream should be 1 and subsequent
+     # idnums should not have gaps between values as this will cause
+     # the stream parser to reserve space for the gapped values.  An
+     # idnum can be updated in the future to a new object by issuing
+     # a new mark directive with the old idnum.
+     #
+  mark ::= 'mark' sp idnum lf;
+  data ::= (delimited_data | exact_data)
+    lf?;
+
+    # note: delim may be any string but must not contain lf.
+    # data_line may contain any data but must not be exactly
+    # delim.
+  delimited_data ::= 'data' sp '<<' delim lf
+    (data_line lf)*
+    delim lf;
+
+     # note: declen indicates the length of binary_data in bytes.
+     # declen does not include the lf preceeding the binary data.
+     #
+  exact_data ::= 'data' sp declen lf
+    binary_data;
+
+     # note: quoted strings are C-style quoting supporting \c for
+     # common escapes of 'c' (e..g \n, \t, \\, \") or \nnn where nnn
+     # is the signed byte value in octal.  Note that the only
+     # characters which must actually be escaped to protect the
+     # stream formatting is: \, " and LF.  Otherwise these values
+     # are UTF8.
+     #
+  ref_str     ::= ref;
+  sha1exp_str ::= sha1exp;
+  tag_str     ::= tag;
+  path_str    ::= path    | '"' quoted(path)    '"' ;
+  mode        ::= '100644' | '644'
+                | '100755' | '755'
+                | '120000'
+                ;
+
+  declen ::= # unsigned 32 bit value, ascii base10 notation;
+  bigint ::= # unsigned integer value, ascii base10 notation;
+  binary_data ::= # file content, not interpreted;
+
+  when         ::= raw_when | rfc2822_when;
+  raw_when     ::= ts sp tz;
+  rfc2822_when ::= # Valid RFC 2822 date and time;
+
+  sp ::= # ASCII space character;
+  lf ::= # ASCII newline (LF) character;
+
+     # note: a colon (':') must precede the numerical value assigned to
+     # an idnum.  This is to distinguish it from a ref or tag name as
+     # GIT does not permit ':' in ref or tag strings.
+     #
+  idnum   ::= ':' bigint;
+  path    ::= # GIT style file path, e.g. "a/b/c";
+  ref     ::= # GIT ref name, e.g. "refs/heads/MOZ_GECKO_EXPERIMENT";
+  tag     ::= # GIT tag name, e.g. "FIREFOX_1_5";
+  sha1exp ::= # Any valid GIT SHA1 expression;
+  hexsha1 ::= # SHA1 in hexadecimal format;
+
+     # note: name and email are UTF8 strings, however name must not
+     # contain '<' or lf and email must not contain any of the
+     # following: '<', '>', lf.
+     #
+  name  ::= # valid GIT author/committer name;
+  email ::= # valid GIT author/committer email;
+  ts    ::= # time since the epoch in seconds, ascii base10 notation;
+  tz    ::= # GIT style timezone;
+
+     # note: comments may appear anywhere in the input, except
+     # within a data command.  Any form of the data command
+     # always escapes the related input from comment processing.
+     #
+     # In case it is not clear, the '#' that starts the comment
+     # must be the first character on that the line (an lf have
+     # preceeded it).
+     #
+  comment ::= '#' not_lf* lf;
+  not_lf  ::= # Any byte that is not ASCII newline (LF);
+*/
+
+#include "builtin.h"
+#include "cache.h"
+#include "object.h"
+#include "blob.h"
+#include "tree.h"
+#include "commit.h"
+#include "delta.h"
+#include "pack.h"
+#include "refs.h"
+#include "csum-file.h"
+#include "quote.h"
+
+#define PACK_ID_BITS 16
+#define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
+#define DEPTH_BITS 13
+#define MAX_DEPTH ((1<<DEPTH_BITS)-1)
+
+struct object_entry
+{
+       struct object_entry *next;
+       uint32_t offset;
+       uint32_t type : TYPE_BITS,
+               pack_id : PACK_ID_BITS,
+               depth : DEPTH_BITS;
+       unsigned char sha1[20];
+};
+
+struct object_entry_pool
+{
+       struct object_entry_pool *next_pool;
+       struct object_entry *next_free;
+       struct object_entry *end;
+       struct object_entry entries[FLEX_ARRAY]; /* more */
+};
+
+struct mark_set
+{
+       union {
+               struct object_entry *marked[1024];
+               struct mark_set *sets[1024];
+       } data;
+       unsigned int shift;
+};
+
+struct last_object
+{
+       struct strbuf data;
+       uint32_t offset;
+       unsigned int depth;
+       unsigned no_swap : 1;
+};
+
+struct mem_pool
+{
+       struct mem_pool *next_pool;
+       char *next_free;
+       char *end;
+       char space[FLEX_ARRAY]; /* more */
+};
+
+struct atom_str
+{
+       struct atom_str *next_atom;
+       unsigned short str_len;
+       char str_dat[FLEX_ARRAY]; /* more */
+};
+
+struct tree_content;
+struct tree_entry
+{
+       struct tree_content *tree;
+       struct atom_str* name;
+       struct tree_entry_ms
+       {
+               uint16_t mode;
+               unsigned char sha1[20];
+       } versions[2];
+};
+
+struct tree_content
+{
+       unsigned int entry_capacity; /* must match avail_tree_content */
+       unsigned int entry_count;
+       unsigned int delta_depth;
+       struct tree_entry *entries[FLEX_ARRAY]; /* more */
+};
+
+struct avail_tree_content
+{
+       unsigned int entry_capacity; /* must match tree_content */
+       struct avail_tree_content *next_avail;
+};
+
+struct branch
+{
+       struct branch *table_next_branch;
+       struct branch *active_next_branch;
+       const char *name;
+       struct tree_entry branch_tree;
+       uintmax_t last_commit;
+       unsigned active : 1;
+       unsigned pack_id : PACK_ID_BITS;
+       unsigned char sha1[20];
+};
+
+struct tag
+{
+       struct tag *next_tag;
+       const char *name;
+       unsigned int pack_id;
+       unsigned char sha1[20];
+};
+
+struct hash_list
+{
+       struct hash_list *next;
+       unsigned char sha1[20];
+};
+
+typedef enum {
+       WHENSPEC_RAW = 1,
+       WHENSPEC_RFC2822,
+       WHENSPEC_NOW,
+} whenspec_type;
+
+struct recent_command
+{
+       struct recent_command *prev;
+       struct recent_command *next;
+       char *buf;
+};
+
+/* Configured limits on output */
+static unsigned long max_depth = 10;
+static off_t max_packsize = (1LL << 32) - 1;
+static int force_update;
+
+/* Stats and misc. counters */
+static uintmax_t alloc_count;
+static uintmax_t marks_set_count;
+static uintmax_t object_count_by_type[1 << TYPE_BITS];
+static uintmax_t duplicate_count_by_type[1 << TYPE_BITS];
+static uintmax_t delta_count_by_type[1 << TYPE_BITS];
+static unsigned long object_count;
+static unsigned long branch_count;
+static unsigned long branch_load_count;
+static int failure;
+static FILE *pack_edges;
+
+/* Memory pools */
+static size_t mem_pool_alloc = 2*1024*1024 - sizeof(struct mem_pool);
+static size_t total_allocd;
+static struct mem_pool *mem_pool;
+
+/* Atom management */
+static unsigned int atom_table_sz = 4451;
+static unsigned int atom_cnt;
+static struct atom_str **atom_table;
+
+/* The .pack file being generated */
+static unsigned int pack_id;
+static struct packed_git *pack_data;
+static struct packed_git **all_packs;
+static unsigned long pack_size;
+
+/* Table of objects we've written. */
+static unsigned int object_entry_alloc = 5000;
+static struct object_entry_pool *blocks;
+static struct object_entry *object_table[1 << 16];
+static struct mark_set *marks;
+static const char* mark_file;
+
+/* Our last blob */
+static struct last_object last_blob = { STRBUF_INIT, 0, 0, 0 };
+
+/* Tree management */
+static unsigned int tree_entry_alloc = 1000;
+static void *avail_tree_entry;
+static unsigned int avail_tree_table_sz = 100;
+static struct avail_tree_content **avail_tree_table;
+static struct strbuf old_tree = STRBUF_INIT;
+static struct strbuf new_tree = STRBUF_INIT;
+
+/* Branch data */
+static unsigned long max_active_branches = 5;
+static unsigned long cur_active_branches;
+static unsigned long branch_table_sz = 1039;
+static struct branch **branch_table;
+static struct branch *active_branches;
+
+/* Tag data */
+static struct tag *first_tag;
+static struct tag *last_tag;
+
+/* Input stream parsing */
+static whenspec_type whenspec = WHENSPEC_RAW;
+static struct strbuf command_buf = STRBUF_INIT;
+static int unread_command_buf;
+static struct recent_command cmd_hist = {&cmd_hist, &cmd_hist, NULL};
+static struct recent_command *cmd_tail = &cmd_hist;
+static struct recent_command *rc_free;
+static unsigned int cmd_save = 100;
+static uintmax_t next_mark;
+static struct strbuf new_data = STRBUF_INIT;
+
+static void write_branch_report(FILE *rpt, struct branch *b)
+{
+       fprintf(rpt, "%s:\n", b->name);
+
+       fprintf(rpt, "  status      :");
+       if (b->active)
+               fputs(" active", rpt);
+       if (b->branch_tree.tree)
+               fputs(" loaded", rpt);
+       if (is_null_sha1(b->branch_tree.versions[1].sha1))
+               fputs(" dirty", rpt);
+       fputc('\n', rpt);
+
+       fprintf(rpt, "  tip commit  : %s\n", sha1_to_hex(b->sha1));
+       fprintf(rpt, "  old tree    : %s\n", sha1_to_hex(b->branch_tree.versions[0].sha1));
+       fprintf(rpt, "  cur tree    : %s\n", sha1_to_hex(b->branch_tree.versions[1].sha1));
+       fprintf(rpt, "  commit clock: %" PRIuMAX "\n", b->last_commit);
+
+       fputs("  last pack   : ", rpt);
+       if (b->pack_id < MAX_PACK_ID)
+               fprintf(rpt, "%u", b->pack_id);
+       fputc('\n', rpt);
+
+       fputc('\n', rpt);
+}
+
+static void write_crash_report(const char *err)
+{
+       char *loc = git_path("fast_import_crash_%d", getpid());
+       FILE *rpt = fopen(loc, "w");
+       struct branch *b;
+       unsigned long lu;
+       struct recent_command *rc;
+
+       if (!rpt) {
+               error("can't write crash report %s: %s", loc, strerror(errno));
+               return;
+       }
+
+       fprintf(stderr, "fast-import: dumping crash report to %s\n", loc);
+
+       fprintf(rpt, "fast-import crash report:\n");
+       fprintf(rpt, "    fast-import process: %d\n", getpid());
+       fprintf(rpt, "    parent process     : %d\n", getppid());
+       fprintf(rpt, "    at %s\n", show_date(time(NULL), 0, DATE_LOCAL));
+       fputc('\n', rpt);
+
+       fputs("fatal: ", rpt);
+       fputs(err, rpt);
+       fputc('\n', rpt);
+
+       fputc('\n', rpt);
+       fputs("Most Recent Commands Before Crash\n", rpt);
+       fputs("---------------------------------\n", rpt);
+       for (rc = cmd_hist.next; rc != &cmd_hist; rc = rc->next) {
+               if (rc->next == &cmd_hist)
+                       fputs("* ", rpt);
+               else
+                       fputs("  ", rpt);
+               fputs(rc->buf, rpt);
+               fputc('\n', rpt);
+       }
+
+       fputc('\n', rpt);
+       fputs("Active Branch LRU\n", rpt);
+       fputs("-----------------\n", rpt);
+       fprintf(rpt, "    active_branches = %lu cur, %lu max\n",
+               cur_active_branches,
+               max_active_branches);
+       fputc('\n', rpt);
+       fputs("  pos  clock name\n", rpt);
+       fputs("  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", rpt);
+       for (b = active_branches, lu = 0; b; b = b->active_next_branch)
+               fprintf(rpt, "  %2lu) %6" PRIuMAX" %s\n",
+                       ++lu, b->last_commit, b->name);
+
+       fputc('\n', rpt);
+       fputs("Inactive Branches\n", rpt);
+       fputs("-----------------\n", rpt);
+       for (lu = 0; lu < branch_table_sz; lu++) {
+               for (b = branch_table[lu]; b; b = b->table_next_branch)
+                       write_branch_report(rpt, b);
+       }
+
+       fputc('\n', rpt);
+       fputs("-------------------\n", rpt);
+       fputs("END OF CRASH REPORT\n", rpt);
+       fclose(rpt);
+}
+
+static NORETURN void die_nicely(const char *err, va_list params)
+{
+       static int zombie;
+       char message[2 * PATH_MAX];
+
+       vsnprintf(message, sizeof(message), err, params);
+       fputs("fatal: ", stderr);
+       fputs(message, stderr);
+       fputc('\n', stderr);
+
+       if (!zombie) {
+               zombie = 1;
+               write_crash_report(message);
+       }
+       exit(128);
+}
+
+static void alloc_objects(unsigned int cnt)
+{
+       struct object_entry_pool *b;
+
+       b = xmalloc(sizeof(struct object_entry_pool)
+               + cnt * sizeof(struct object_entry));
+       b->next_pool = blocks;
+       b->next_free = b->entries;
+       b->end = b->entries + cnt;
+       blocks = b;
+       alloc_count += cnt;
+}
+
+static struct object_entry *new_object(unsigned char *sha1)
+{
+       struct object_entry *e;
+
+       if (blocks->next_free == blocks->end)
+               alloc_objects(object_entry_alloc);
+
+       e = blocks->next_free++;
+       hashcpy(e->sha1, sha1);
+       return e;
+}
+
+static struct object_entry *find_object(unsigned char *sha1)
+{
+       unsigned int h = sha1[0] << 8 | sha1[1];
+       struct object_entry *e;
+       for (e = object_table[h]; e; e = e->next)
+               if (!hashcmp(sha1, e->sha1))
+                       return e;
+       return NULL;
+}
+
+static struct object_entry *insert_object(unsigned char *sha1)
+{
+       unsigned int h = sha1[0] << 8 | sha1[1];
+       struct object_entry *e = object_table[h];
+       struct object_entry *p = NULL;
+
+       while (e) {
+               if (!hashcmp(sha1, e->sha1))
+                       return e;
+               p = e;
+               e = e->next;
+       }
+
+       e = new_object(sha1);
+       e->next = NULL;
+       e->offset = 0;
+       if (p)
+               p->next = e;
+       else
+               object_table[h] = e;
+       return e;
+}
+
+static unsigned int hc_str(const char *s, size_t len)
+{
+       unsigned int r = 0;
+       while (len-- > 0)
+               r = r * 31 + *s++;
+       return r;
+}
+
+static void *pool_alloc(size_t len)
+{
+       struct mem_pool *p;
+       void *r;
+
+       for (p = mem_pool; p; p = p->next_pool)
+               if ((p->end - p->next_free >= len))
+                       break;
+
+       if (!p) {
+               if (len >= (mem_pool_alloc/2)) {
+                       total_allocd += len;
+                       return xmalloc(len);
+               }
+               total_allocd += sizeof(struct mem_pool) + mem_pool_alloc;
+               p = xmalloc(sizeof(struct mem_pool) + mem_pool_alloc);
+               p->next_pool = mem_pool;
+               p->next_free = p->space;
+               p->end = p->next_free + mem_pool_alloc;
+               mem_pool = p;
+       }
+
+       r = p->next_free;
+       /* round out to a pointer alignment */
+       if (len & (sizeof(void*) - 1))
+               len += sizeof(void*) - (len & (sizeof(void*) - 1));
+       p->next_free += len;
+       return r;
+}
+
+static void *pool_calloc(size_t count, size_t size)
+{
+       size_t len = count * size;
+       void *r = pool_alloc(len);
+       memset(r, 0, len);
+       return r;
+}
+
+static char *pool_strdup(const char *s)
+{
+       char *r = pool_alloc(strlen(s) + 1);
+       strcpy(r, s);
+       return r;
+}
+
+static void insert_mark(uintmax_t idnum, struct object_entry *oe)
+{
+       struct mark_set *s = marks;
+       while ((idnum >> s->shift) >= 1024) {
+               s = pool_calloc(1, sizeof(struct mark_set));
+               s->shift = marks->shift + 10;
+               s->data.sets[0] = marks;
+               marks = s;
+       }
+       while (s->shift) {
+               uintmax_t i = idnum >> s->shift;
+               idnum -= i << s->shift;
+               if (!s->data.sets[i]) {
+                       s->data.sets[i] = pool_calloc(1, sizeof(struct mark_set));
+                       s->data.sets[i]->shift = s->shift - 10;
+               }
+               s = s->data.sets[i];
+       }
+       if (!s->data.marked[idnum])
+               marks_set_count++;
+       s->data.marked[idnum] = oe;
+}
+
+static struct object_entry *find_mark(uintmax_t idnum)
+{
+       uintmax_t orig_idnum = idnum;
+       struct mark_set *s = marks;
+       struct object_entry *oe = NULL;
+       if ((idnum >> s->shift) < 1024) {
+               while (s && s->shift) {
+                       uintmax_t i = idnum >> s->shift;
+                       idnum -= i << s->shift;
+                       s = s->data.sets[i];
+               }
+               if (s)
+                       oe = s->data.marked[idnum];
+       }
+       if (!oe)
+               die("mark :%" PRIuMAX " not declared", orig_idnum);
+       return oe;
+}
+
+static struct atom_str *to_atom(const char *s, unsigned short len)
+{
+       unsigned int hc = hc_str(s, len) % atom_table_sz;
+       struct atom_str *c;
+
+       for (c = atom_table[hc]; c; c = c->next_atom)
+               if (c->str_len == len && !strncmp(s, c->str_dat, len))
+                       return c;
+
+       c = pool_alloc(sizeof(struct atom_str) + len + 1);
+       c->str_len = len;
+       strncpy(c->str_dat, s, len);
+       c->str_dat[len] = 0;
+       c->next_atom = atom_table[hc];
+       atom_table[hc] = c;
+       atom_cnt++;
+       return c;
+}
+
+static struct branch *lookup_branch(const char *name)
+{
+       unsigned int hc = hc_str(name, strlen(name)) % branch_table_sz;
+       struct branch *b;
+
+       for (b = branch_table[hc]; b; b = b->table_next_branch)
+               if (!strcmp(name, b->name))
+                       return b;
+       return NULL;
+}
+
+static struct branch *new_branch(const char *name)
+{
+       unsigned int hc = hc_str(name, strlen(name)) % branch_table_sz;
+       struct branch* b = lookup_branch(name);
+
+       if (b)
+               die("Invalid attempt to create duplicate branch: %s", name);
+       switch (check_ref_format(name)) {
+       case  0: break; /* its valid */
+       case -2: break; /* valid, but too few '/', allow anyway */
+       default:
+               die("Branch name doesn't conform to GIT standards: %s", name);
+       }
+
+       b = pool_calloc(1, sizeof(struct branch));
+       b->name = pool_strdup(name);
+       b->table_next_branch = branch_table[hc];
+       b->branch_tree.versions[0].mode = S_IFDIR;
+       b->branch_tree.versions[1].mode = S_IFDIR;
+       b->active = 0;
+       b->pack_id = MAX_PACK_ID;
+       branch_table[hc] = b;
+       branch_count++;
+       return b;
+}
+
+static unsigned int hc_entries(unsigned int cnt)
+{
+       cnt = cnt & 7 ? (cnt / 8) + 1 : cnt / 8;
+       return cnt < avail_tree_table_sz ? cnt : avail_tree_table_sz - 1;
+}
+
+static struct tree_content *new_tree_content(unsigned int cnt)
+{
+       struct avail_tree_content *f, *l = NULL;
+       struct tree_content *t;
+       unsigned int hc = hc_entries(cnt);
+
+       for (f = avail_tree_table[hc]; f; l = f, f = f->next_avail)
+               if (f->entry_capacity >= cnt)
+                       break;
+
+       if (f) {
+               if (l)
+                       l->next_avail = f->next_avail;
+               else
+                       avail_tree_table[hc] = f->next_avail;
+       } else {
+               cnt = cnt & 7 ? ((cnt / 8) + 1) * 8 : cnt;
+               f = pool_alloc(sizeof(*t) + sizeof(t->entries[0]) * cnt);
+               f->entry_capacity = cnt;
+       }
+
+       t = (struct tree_content*)f;
+       t->entry_count = 0;
+       t->delta_depth = 0;
+       return t;
+}
+
+static void release_tree_entry(struct tree_entry *e);
+static void release_tree_content(struct tree_content *t)
+{
+       struct avail_tree_content *f = (struct avail_tree_content*)t;
+       unsigned int hc = hc_entries(f->entry_capacity);
+       f->next_avail = avail_tree_table[hc];
+       avail_tree_table[hc] = f;
+}
+
+static void release_tree_content_recursive(struct tree_content *t)
+{
+       unsigned int i;
+       for (i = 0; i < t->entry_count; i++)
+               release_tree_entry(t->entries[i]);
+       release_tree_content(t);
+}
+
+static struct tree_content *grow_tree_content(
+       struct tree_content *t,
+       int amt)
+{
+       struct tree_content *r = new_tree_content(t->entry_count + amt);
+       r->entry_count = t->entry_count;
+       r->delta_depth = t->delta_depth;
+       memcpy(r->entries,t->entries,t->entry_count*sizeof(t->entries[0]));
+       release_tree_content(t);
+       return r;
+}
+
+static struct tree_entry *new_tree_entry(void)
+{
+       struct tree_entry *e;
+
+       if (!avail_tree_entry) {
+               unsigned int n = tree_entry_alloc;
+               total_allocd += n * sizeof(struct tree_entry);
+               avail_tree_entry = e = xmalloc(n * sizeof(struct tree_entry));
+               while (n-- > 1) {
+                       *((void**)e) = e + 1;
+                       e++;
+               }
+               *((void**)e) = NULL;
+       }
+
+       e = avail_tree_entry;
+       avail_tree_entry = *((void**)e);
+       return e;
+}
+
+static void release_tree_entry(struct tree_entry *e)
+{
+       if (e->tree)
+               release_tree_content_recursive(e->tree);
+       *((void**)e) = avail_tree_entry;
+       avail_tree_entry = e;
+}
+
+static struct tree_content *dup_tree_content(struct tree_content *s)
+{
+       struct tree_content *d;
+       struct tree_entry *a, *b;
+       unsigned int i;
+
+       if (!s)
+               return NULL;
+       d = new_tree_content(s->entry_count);
+       for (i = 0; i < s->entry_count; i++) {
+               a = s->entries[i];
+               b = new_tree_entry();
+               memcpy(b, a, sizeof(*a));
+               if (a->tree && is_null_sha1(b->versions[1].sha1))
+                       b->tree = dup_tree_content(a->tree);
+               else
+                       b->tree = NULL;
+               d->entries[i] = b;
+       }
+       d->entry_count = s->entry_count;
+       d->delta_depth = s->delta_depth;
+
+       return d;
+}
+
+static void start_packfile(void)
+{
+       static char tmpfile[PATH_MAX];
+       struct packed_git *p;
+       struct pack_header hdr;
+       int pack_fd;
+
+       snprintf(tmpfile, sizeof(tmpfile),
+               "%s/tmp_pack_XXXXXX", get_object_directory());
+       pack_fd = xmkstemp(tmpfile);
+       p = xcalloc(1, sizeof(*p) + strlen(tmpfile) + 2);
+       strcpy(p->pack_name, tmpfile);
+       p->pack_fd = pack_fd;
+
+       hdr.hdr_signature = htonl(PACK_SIGNATURE);
+       hdr.hdr_version = htonl(2);
+       hdr.hdr_entries = 0;
+       write_or_die(p->pack_fd, &hdr, sizeof(hdr));
+
+       pack_data = p;
+       pack_size = sizeof(hdr);
+       object_count = 0;
+
+       all_packs = xrealloc(all_packs, sizeof(*all_packs) * (pack_id + 1));
+       all_packs[pack_id] = p;
+}
+
+static int oecmp (const void *a_, const void *b_)
+{
+       struct object_entry *a = *((struct object_entry**)a_);
+       struct object_entry *b = *((struct object_entry**)b_);
+       return hashcmp(a->sha1, b->sha1);
+}
+
+static char *create_index(void)
+{
+       static char tmpfile[PATH_MAX];
+       SHA_CTX ctx;
+       struct sha1file *f;
+       struct object_entry **idx, **c, **last, *e;
+       struct object_entry_pool *o;
+       uint32_t array[256];
+       int i, idx_fd;
+
+       /* Build the sorted table of object IDs. */
+       idx = xmalloc(object_count * sizeof(struct object_entry*));
+       c = idx;
+       for (o = blocks; o; o = o->next_pool)
+               for (e = o->next_free; e-- != o->entries;)
+                       if (pack_id == e->pack_id)
+                               *c++ = e;
+       last = idx + object_count;
+       if (c != last)
+               die("internal consistency error creating the index");
+       qsort(idx, object_count, sizeof(struct object_entry*), oecmp);
+
+       /* Generate the fan-out array. */
+       c = idx;
+       for (i = 0; i < 256; i++) {
+               struct object_entry **next = c;;
+               while (next < last) {
+                       if ((*next)->sha1[0] != i)
+                               break;
+                       next++;
+               }
+               array[i] = htonl(next - idx);
+               c = next;
+       }
+
+       snprintf(tmpfile, sizeof(tmpfile),
+               "%s/tmp_idx_XXXXXX", get_object_directory());
+       idx_fd = xmkstemp(tmpfile);
+       f = sha1fd(idx_fd, tmpfile);
+       sha1write(f, array, 256 * sizeof(int));
+       SHA1_Init(&ctx);
+       for (c = idx; c != last; c++) {
+               uint32_t offset = htonl((*c)->offset);
+               sha1write(f, &offset, 4);
+               sha1write(f, (*c)->sha1, sizeof((*c)->sha1));
+               SHA1_Update(&ctx, (*c)->sha1, 20);
+       }
+       sha1write(f, pack_data->sha1, sizeof(pack_data->sha1));
+       sha1close(f, NULL, 1);
+       free(idx);
+       SHA1_Final(pack_data->sha1, &ctx);
+       return tmpfile;
+}
+
+static char *keep_pack(char *curr_index_name)
+{
+       static char name[PATH_MAX];
+       static const char *keep_msg = "fast-import";
+       int keep_fd;
+
+       chmod(pack_data->pack_name, 0444);
+       chmod(curr_index_name, 0444);
+
+       snprintf(name, sizeof(name), "%s/pack/pack-%s.keep",
+                get_object_directory(), sha1_to_hex(pack_data->sha1));
+       keep_fd = open(name, O_RDWR|O_CREAT|O_EXCL, 0600);
+       if (keep_fd < 0)
+               die("cannot create keep file");
+       write(keep_fd, keep_msg, strlen(keep_msg));
+       close(keep_fd);
+
+       snprintf(name, sizeof(name), "%s/pack/pack-%s.pack",
+                get_object_directory(), sha1_to_hex(pack_data->sha1));
+       if (move_temp_to_file(pack_data->pack_name, name))
+               die("cannot store pack file");
+
+       snprintf(name, sizeof(name), "%s/pack/pack-%s.idx",
+                get_object_directory(), sha1_to_hex(pack_data->sha1));
+       if (move_temp_to_file(curr_index_name, name))
+               die("cannot store index file");
+       return name;
+}
+
+static void unkeep_all_packs(void)
+{
+       static char name[PATH_MAX];
+       int k;
+
+       for (k = 0; k < pack_id; k++) {
+               struct packed_git *p = all_packs[k];
+               snprintf(name, sizeof(name), "%s/pack/pack-%s.keep",
+                        get_object_directory(), sha1_to_hex(p->sha1));
+               unlink(name);
+       }
+}
+
+static void end_packfile(void)
+{
+       struct packed_git *old_p = pack_data, *new_p;
+
+       if (object_count) {
+               char *idx_name;
+               int i;
+               struct branch *b;
+               struct tag *t;
+
+               fixup_pack_header_footer(pack_data->pack_fd, pack_data->sha1,
+                                   pack_data->pack_name, object_count);
+               close(pack_data->pack_fd);
+               idx_name = keep_pack(create_index());
+
+               /* Register the packfile with core git's machinary. */
+               new_p = add_packed_git(idx_name, strlen(idx_name), 1);
+               if (!new_p)
+                       die("core git rejected index %s", idx_name);
+               new_p->windows = old_p->windows;
+               all_packs[pack_id] = new_p;
+               install_packed_git(new_p);
+
+               /* Print the boundary */
+               if (pack_edges) {
+                       fprintf(pack_edges, "%s:", new_p->pack_name);
+                       for (i = 0; i < branch_table_sz; i++) {
+                               for (b = branch_table[i]; b; b = b->table_next_branch) {
+                                       if (b->pack_id == pack_id)
+                                               fprintf(pack_edges, " %s", sha1_to_hex(b->sha1));
+                               }
+                       }
+                       for (t = first_tag; t; t = t->next_tag) {
+                               if (t->pack_id == pack_id)
+                                       fprintf(pack_edges, " %s", sha1_to_hex(t->sha1));
+                       }
+                       fputc('\n', pack_edges);
+                       fflush(pack_edges);
+               }
+
+               pack_id++;
+       }
+       else
+               unlink(old_p->pack_name);
+       free(old_p);
+
+       /* We can't carry a delta across packfiles. */
+       strbuf_release(&last_blob.data);
+       last_blob.offset = 0;
+       last_blob.depth = 0;
+}
+
+static void cycle_packfile(void)
+{
+       end_packfile();
+       start_packfile();
+}
+
+static size_t encode_header(
+       enum object_type type,
+       size_t size,
+       unsigned char *hdr)
+{
+       int n = 1;
+       unsigned char c;
+
+       if (type < OBJ_COMMIT || type > OBJ_REF_DELTA)
+               die("bad type %d", type);
+
+       c = (type << 4) | (size & 15);
+       size >>= 4;
+       while (size) {
+               *hdr++ = c | 0x80;
+               c = size & 0x7f;
+               size >>= 7;
+               n++;
+       }
+       *hdr = c;
+       return n;
+}
+
+static int store_object(
+       enum object_type type,
+       struct strbuf *dat,
+       struct last_object *last,
+       unsigned char *sha1out,
+       uintmax_t mark)
+{
+       void *out, *delta;
+       struct object_entry *e;
+       unsigned char hdr[96];
+       unsigned char sha1[20];
+       unsigned long hdrlen, deltalen;
+       SHA_CTX c;
+       z_stream s;
+
+       hdrlen = sprintf((char*)hdr,"%s %lu", typename(type),
+               (unsigned long)dat->len) + 1;
+       SHA1_Init(&c);
+       SHA1_Update(&c, hdr, hdrlen);
+       SHA1_Update(&c, dat->buf, dat->len);
+       SHA1_Final(sha1, &c);
+       if (sha1out)
+               hashcpy(sha1out, sha1);
+
+       e = insert_object(sha1);
+       if (mark)
+               insert_mark(mark, e);
+       if (e->offset) {
+               duplicate_count_by_type[type]++;
+               return 1;
+       } else if (find_sha1_pack(sha1, packed_git)) {
+               e->type = type;
+               e->pack_id = MAX_PACK_ID;
+               e->offset = 1; /* just not zero! */
+               duplicate_count_by_type[type]++;
+               return 1;
+       }
+
+       if (last && last->data.buf && last->depth < max_depth) {
+               delta = diff_delta(last->data.buf, last->data.len,
+                       dat->buf, dat->len,
+                       &deltalen, 0);
+               if (delta && deltalen >= dat->len) {
+                       free(delta);
+                       delta = NULL;
+               }
+       } else
+               delta = NULL;
+
+       memset(&s, 0, sizeof(s));
+       deflateInit(&s, zlib_compression_level);
+       if (delta) {
+               s.next_in = delta;
+               s.avail_in = deltalen;
+       } else {
+               s.next_in = (void *)dat->buf;
+               s.avail_in = dat->len;
+       }
+       s.avail_out = deflateBound(&s, s.avail_in);
+       s.next_out = out = xmalloc(s.avail_out);
+       while (deflate(&s, Z_FINISH) == Z_OK)
+               /* nothing */;
+       deflateEnd(&s);
+
+       /* Determine if we should auto-checkpoint. */
+       if ((pack_size + 60 + s.total_out) > max_packsize
+               || (pack_size + 60 + s.total_out) < pack_size) {
+
+               /* This new object needs to *not* have the current pack_id. */
+               e->pack_id = pack_id + 1;
+               cycle_packfile();
+
+               /* We cannot carry a delta into the new pack. */
+               if (delta) {
+                       free(delta);
+                       delta = NULL;
+
+                       memset(&s, 0, sizeof(s));
+                       deflateInit(&s, zlib_compression_level);
+                       s.next_in = (void *)dat->buf;
+                       s.avail_in = dat->len;
+                       s.avail_out = deflateBound(&s, s.avail_in);
+                       s.next_out = out = xrealloc(out, s.avail_out);
+                       while (deflate(&s, Z_FINISH) == Z_OK)
+                               /* nothing */;
+                       deflateEnd(&s);
+               }
+       }
+
+       e->type = type;
+       e->pack_id = pack_id;
+       e->offset = pack_size;
+       object_count++;
+       object_count_by_type[type]++;
+
+       if (delta) {
+               unsigned long ofs = e->offset - last->offset;
+               unsigned pos = sizeof(hdr) - 1;
+
+               delta_count_by_type[type]++;
+               e->depth = last->depth + 1;
+
+               hdrlen = encode_header(OBJ_OFS_DELTA, deltalen, hdr);
+               write_or_die(pack_data->pack_fd, hdr, hdrlen);
+               pack_size += hdrlen;
+
+               hdr[pos] = ofs & 127;
+               while (ofs >>= 7)
+                       hdr[--pos] = 128 | (--ofs & 127);
+               write_or_die(pack_data->pack_fd, hdr + pos, sizeof(hdr) - pos);
+               pack_size += sizeof(hdr) - pos;
+       } else {
+               e->depth = 0;
+               hdrlen = encode_header(type, dat->len, hdr);
+               write_or_die(pack_data->pack_fd, hdr, hdrlen);
+               pack_size += hdrlen;
+       }
+
+       write_or_die(pack_data->pack_fd, out, s.total_out);
+       pack_size += s.total_out;
+
+       free(out);
+       free(delta);
+       if (last) {
+               if (last->no_swap) {
+                       last->data = *dat;
+               } else {
+                       strbuf_swap(&last->data, dat);
+               }
+               last->offset = e->offset;
+               last->depth = e->depth;
+       }
+       return 0;
+}
+
+static void *gfi_unpack_entry(
+       struct object_entry *oe,
+       unsigned long *sizep)
+{
+       enum object_type type;
+       struct packed_git *p = all_packs[oe->pack_id];
+       if (p == pack_data)
+               p->pack_size = pack_size + 20;
+       return unpack_entry(p, oe->offset, &type, sizep);
+}
+
+static const char *get_mode(const char *str, uint16_t *modep)
+{
+       unsigned char c;
+       uint16_t mode = 0;
+
+       while ((c = *str++) != ' ') {
+               if (c < '0' || c > '7')
+                       return NULL;
+               mode = (mode << 3) + (c - '0');
+       }
+       *modep = mode;
+       return str;
+}
+
+static void load_tree(struct tree_entry *root)
+{
+       unsigned char* sha1 = root->versions[1].sha1;
+       struct object_entry *myoe;
+       struct tree_content *t;
+       unsigned long size;
+       char *buf;
+       const char *c;
+
+       root->tree = t = new_tree_content(8);
+       if (is_null_sha1(sha1))
+               return;
+
+       myoe = find_object(sha1);
+       if (myoe && myoe->pack_id != MAX_PACK_ID) {
+               if (myoe->type != OBJ_TREE)
+                       die("Not a tree: %s", sha1_to_hex(sha1));
+               t->delta_depth = myoe->depth;
+               buf = gfi_unpack_entry(myoe, &size);
+       } else {
+               enum object_type type;
+               buf = read_sha1_file(sha1, &type, &size);
+               if (!buf || type != OBJ_TREE)
+                       die("Can't load tree %s", sha1_to_hex(sha1));
+       }
+
+       c = buf;
+       while (c != (buf + size)) {
+               struct tree_entry *e = new_tree_entry();
+
+               if (t->entry_count == t->entry_capacity)
+                       root->tree = t = grow_tree_content(t, t->entry_count);
+               t->entries[t->entry_count++] = e;
+
+               e->tree = NULL;
+               c = get_mode(c, &e->versions[1].mode);
+               if (!c)
+                       die("Corrupt mode in %s", sha1_to_hex(sha1));
+               e->versions[0].mode = e->versions[1].mode;
+               e->name = to_atom(c, strlen(c));
+               c += e->name->str_len + 1;
+               hashcpy(e->versions[0].sha1, (unsigned char*)c);
+               hashcpy(e->versions[1].sha1, (unsigned char*)c);
+               c += 20;
+       }
+       free(buf);
+}
+
+static int tecmp0 (const void *_a, const void *_b)
+{
+       struct tree_entry *a = *((struct tree_entry**)_a);
+       struct tree_entry *b = *((struct tree_entry**)_b);
+       return base_name_compare(
+               a->name->str_dat, a->name->str_len, a->versions[0].mode,
+               b->name->str_dat, b->name->str_len, b->versions[0].mode);
+}
+
+static int tecmp1 (const void *_a, const void *_b)
+{
+       struct tree_entry *a = *((struct tree_entry**)_a);
+       struct tree_entry *b = *((struct tree_entry**)_b);
+       return base_name_compare(
+               a->name->str_dat, a->name->str_len, a->versions[1].mode,
+               b->name->str_dat, b->name->str_len, b->versions[1].mode);
+}
+
+static void mktree(struct tree_content *t, int v, struct strbuf *b)
+{
+       size_t maxlen = 0;
+       unsigned int i;
+
+       if (!v)
+               qsort(t->entries,t->entry_count,sizeof(t->entries[0]),tecmp0);
+       else
+               qsort(t->entries,t->entry_count,sizeof(t->entries[0]),tecmp1);
+
+       for (i = 0; i < t->entry_count; i++) {
+               if (t->entries[i]->versions[v].mode)
+                       maxlen += t->entries[i]->name->str_len + 34;
+       }
+
+       strbuf_reset(b);
+       strbuf_grow(b, maxlen);
+       for (i = 0; i < t->entry_count; i++) {
+               struct tree_entry *e = t->entries[i];
+               if (!e->versions[v].mode)
+                       continue;
+               strbuf_addf(b, "%o %s%c", (unsigned int)e->versions[v].mode,
+                                       e->name->str_dat, '\0');
+               strbuf_add(b, e->versions[v].sha1, 20);
+       }
+}
+
+static void store_tree(struct tree_entry *root)
+{
+       struct tree_content *t = root->tree;
+       unsigned int i, j, del;
+       struct last_object lo = { STRBUF_INIT, 0, 0, /* no_swap */ 1 };
+       struct object_entry *le;
+
+       if (!is_null_sha1(root->versions[1].sha1))
+               return;
+
+       for (i = 0; i < t->entry_count; i++) {
+               if (t->entries[i]->tree)
+                       store_tree(t->entries[i]);
+       }
+
+       le = find_object(root->versions[0].sha1);
+       if (S_ISDIR(root->versions[0].mode) && le && le->pack_id == pack_id) {
+               mktree(t, 0, &old_tree);
+               lo.data = old_tree;
+               lo.offset = le->offset;
+               lo.depth = t->delta_depth;
+       }
+
+       mktree(t, 1, &new_tree);
+       store_object(OBJ_TREE, &new_tree, &lo, root->versions[1].sha1, 0);
+
+       t->delta_depth = lo.depth;
+       for (i = 0, j = 0, del = 0; i < t->entry_count; i++) {
+               struct tree_entry *e = t->entries[i];
+               if (e->versions[1].mode) {
+                       e->versions[0].mode = e->versions[1].mode;
+                       hashcpy(e->versions[0].sha1, e->versions[1].sha1);
+                       t->entries[j++] = e;
+               } else {
+                       release_tree_entry(e);
+                       del++;
+               }
+       }
+       t->entry_count -= del;
+}
+
+static int tree_content_set(
+       struct tree_entry *root,
+       const char *p,
+       const unsigned char *sha1,
+       const uint16_t mode,
+       struct tree_content *subtree)
+{
+       struct tree_content *t = root->tree;
+       const char *slash1;
+       unsigned int i, n;
+       struct tree_entry *e;
+
+       slash1 = strchr(p, '/');
+       if (slash1)
+               n = slash1 - p;
+       else
+               n = strlen(p);
+       if (!n)
+               die("Empty path component found in input");
+       if (!slash1 && !S_ISDIR(mode) && subtree)
+               die("Non-directories cannot have subtrees");
+
+       for (i = 0; i < t->entry_count; i++) {
+               e = t->entries[i];
+               if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) {
+                       if (!slash1) {
+                               if (!S_ISDIR(mode)
+                                               && e->versions[1].mode == mode
+                                               && !hashcmp(e->versions[1].sha1, sha1))
+                                       return 0;
+                               e->versions[1].mode = mode;
+                               hashcpy(e->versions[1].sha1, sha1);
+                               if (e->tree)
+                                       release_tree_content_recursive(e->tree);
+                               e->tree = subtree;
+                               hashclr(root->versions[1].sha1);
+                               return 1;
+                       }
+                       if (!S_ISDIR(e->versions[1].mode)) {
+                               e->tree = new_tree_content(8);
+                               e->versions[1].mode = S_IFDIR;
+                       }
+                       if (!e->tree)
+                               load_tree(e);
+                       if (tree_content_set(e, slash1 + 1, sha1, mode, subtree)) {
+                               hashclr(root->versions[1].sha1);
+                               return 1;
+                       }
+                       return 0;
+               }
+       }
+
+       if (t->entry_count == t->entry_capacity)
+               root->tree = t = grow_tree_content(t, t->entry_count);
+       e = new_tree_entry();
+       e->name = to_atom(p, n);
+       e->versions[0].mode = 0;
+       hashclr(e->versions[0].sha1);
+       t->entries[t->entry_count++] = e;
+       if (slash1) {
+               e->tree = new_tree_content(8);
+               e->versions[1].mode = S_IFDIR;
+               tree_content_set(e, slash1 + 1, sha1, mode, subtree);
+       } else {
+               e->tree = subtree;
+               e->versions[1].mode = mode;
+               hashcpy(e->versions[1].sha1, sha1);
+       }
+       hashclr(root->versions[1].sha1);
+       return 1;
+}
+
+static int tree_content_remove(
+       struct tree_entry *root,
+       const char *p,
+       struct tree_entry *backup_leaf)
+{
+       struct tree_content *t = root->tree;
+       const char *slash1;
+       unsigned int i, n;
+       struct tree_entry *e;
+
+       slash1 = strchr(p, '/');
+       if (slash1)
+               n = slash1 - p;
+       else
+               n = strlen(p);
+
+       for (i = 0; i < t->entry_count; i++) {
+               e = t->entries[i];
+               if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) {
+                       if (!slash1 || !S_ISDIR(e->versions[1].mode))
+                               goto del_entry;
+                       if (!e->tree)
+                               load_tree(e);
+                       if (tree_content_remove(e, slash1 + 1, backup_leaf)) {
+                               for (n = 0; n < e->tree->entry_count; n++) {
+                                       if (e->tree->entries[n]->versions[1].mode) {
+                                               hashclr(root->versions[1].sha1);
+                                               return 1;
+                                       }
+                               }
+                               backup_leaf = NULL;
+                               goto del_entry;
+                       }
+                       return 0;
+               }
+       }
+       return 0;
+
+del_entry:
+       if (backup_leaf)
+               memcpy(backup_leaf, e, sizeof(*backup_leaf));
+       else if (e->tree)
+               release_tree_content_recursive(e->tree);
+       e->tree = NULL;
+       e->versions[1].mode = 0;
+       hashclr(e->versions[1].sha1);
+       hashclr(root->versions[1].sha1);
+       return 1;
+}
+
+static int tree_content_get(
+       struct tree_entry *root,
+       const char *p,
+       struct tree_entry *leaf)
+{
+       struct tree_content *t = root->tree;
+       const char *slash1;
+       unsigned int i, n;
+       struct tree_entry *e;
+
+       slash1 = strchr(p, '/');
+       if (slash1)
+               n = slash1 - p;
+       else
+               n = strlen(p);
+
+       for (i = 0; i < t->entry_count; i++) {
+               e = t->entries[i];
+               if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) {
+                       if (!slash1) {
+                               memcpy(leaf, e, sizeof(*leaf));
+                               if (e->tree && is_null_sha1(e->versions[1].sha1))
+                                       leaf->tree = dup_tree_content(e->tree);
+                               else
+                                       leaf->tree = NULL;
+                               return 1;
+                       }
+                       if (!S_ISDIR(e->versions[1].mode))
+                               return 0;
+                       if (!e->tree)
+                               load_tree(e);
+                       return tree_content_get(e, slash1 + 1, leaf);
+               }
+       }
+       return 0;
+}
+
+static int update_branch(struct branch *b)
+{
+       static const char *msg = "fast-import";
+       struct ref_lock *lock;
+       unsigned char old_sha1[20];
+
+       if (read_ref(b->name, old_sha1))
+               hashclr(old_sha1);
+       lock = lock_any_ref_for_update(b->name, old_sha1, 0);
+       if (!lock)
+               return error("Unable to lock %s", b->name);
+       if (!force_update && !is_null_sha1(old_sha1)) {
+               struct commit *old_cmit, *new_cmit;
+
+               old_cmit = lookup_commit_reference_gently(old_sha1, 0);
+               new_cmit = lookup_commit_reference_gently(b->sha1, 0);
+               if (!old_cmit || !new_cmit) {
+                       unlock_ref(lock);
+                       return error("Branch %s is missing commits.", b->name);
+               }
+
+               if (!in_merge_bases(old_cmit, &new_cmit, 1)) {
+                       unlock_ref(lock);
+                       warning("Not updating %s"
+                               " (new tip %s does not contain %s)",
+                               b->name, sha1_to_hex(b->sha1), sha1_to_hex(old_sha1));
+                       return -1;
+               }
+       }
+       if (write_ref_sha1(lock, b->sha1, msg) < 0)
+               return error("Unable to update %s", b->name);
+       return 0;
+}
+
+static void dump_branches(void)
+{
+       unsigned int i;
+       struct branch *b;
+
+       for (i = 0; i < branch_table_sz; i++) {
+               for (b = branch_table[i]; b; b = b->table_next_branch)
+                       failure |= update_branch(b);
+       }
+}
+
+static void dump_tags(void)
+{
+       static const char *msg = "fast-import";
+       struct tag *t;
+       struct ref_lock *lock;
+       char ref_name[PATH_MAX];
+
+       for (t = first_tag; t; t = t->next_tag) {
+               sprintf(ref_name, "tags/%s", t->name);
+               lock = lock_ref_sha1(ref_name, NULL);
+               if (!lock || write_ref_sha1(lock, t->sha1, msg) < 0)
+                       failure |= error("Unable to update %s", ref_name);
+       }
+}
+
+static void dump_marks_helper(FILE *f,
+       uintmax_t base,
+       struct mark_set *m)
+{
+       uintmax_t k;
+       if (m->shift) {
+               for (k = 0; k < 1024; k++) {
+                       if (m->data.sets[k])
+                               dump_marks_helper(f, (base + k) << m->shift,
+                                       m->data.sets[k]);
+               }
+       } else {
+               for (k = 0; k < 1024; k++) {
+                       if (m->data.marked[k])
+                               fprintf(f, ":%" PRIuMAX " %s\n", base + k,
+                                       sha1_to_hex(m->data.marked[k]->sha1));
+               }
+       }
+}
+
+static void dump_marks(void)
+{
+       static struct lock_file mark_lock;
+       int mark_fd;
+       FILE *f;
+
+       if (!mark_file)
+               return;
+
+       mark_fd = hold_lock_file_for_update(&mark_lock, mark_file, 0);
+       if (mark_fd < 0) {
+               failure |= error("Unable to write marks file %s: %s",
+                       mark_file, strerror(errno));
+               return;
+       }
+
+       f = fdopen(mark_fd, "w");
+       if (!f) {
+               rollback_lock_file(&mark_lock);
+               failure |= error("Unable to write marks file %s: %s",
+                       mark_file, strerror(errno));
+               return;
+       }
+
+       dump_marks_helper(f, 0, marks);
+       fclose(f);
+       if (commit_lock_file(&mark_lock))
+               failure |= error("Unable to write marks file %s: %s",
+                       mark_file, strerror(errno));
+}
+
+static int read_next_command(void)
+{
+       static int stdin_eof = 0;
+
+       if (stdin_eof) {
+               unread_command_buf = 0;
+               return EOF;
+       }
+
+       do {
+               if (unread_command_buf) {
+                       unread_command_buf = 0;
+               } else {
+                       struct recent_command *rc;
+
+                       strbuf_detach(&command_buf, NULL);
+                       stdin_eof = strbuf_getline(&command_buf, stdin, '\n');
+                       if (stdin_eof)
+                               return EOF;
+
+                       rc = rc_free;
+                       if (rc)
+                               rc_free = rc->next;
+                       else {
+                               rc = cmd_hist.next;
+                               cmd_hist.next = rc->next;
+                               cmd_hist.next->prev = &cmd_hist;
+                               free(rc->buf);
+                       }
+
+                       rc->buf = command_buf.buf;
+                       rc->prev = cmd_tail;
+                       rc->next = cmd_hist.prev;
+                       rc->prev->next = rc;
+                       cmd_tail = rc;
+               }
+       } while (command_buf.buf[0] == '#');
+
+       return 0;
+}
+
+static void skip_optional_lf(void)
+{
+       int term_char = fgetc(stdin);
+       if (term_char != '\n' && term_char != EOF)
+               ungetc(term_char, stdin);
+}
+
+static void cmd_mark(void)
+{
+       if (!prefixcmp(command_buf.buf, "mark :")) {
+               next_mark = strtoumax(command_buf.buf + 6, NULL, 10);
+               read_next_command();
+       }
+       else
+               next_mark = 0;
+}
+
+static void cmd_data(struct strbuf *sb)
+{
+       strbuf_reset(sb);
+
+       if (prefixcmp(command_buf.buf, "data "))
+               die("Expected 'data n' command, found: %s", command_buf.buf);
+
+       if (!prefixcmp(command_buf.buf + 5, "<<")) {
+               char *term = xstrdup(command_buf.buf + 5 + 2);
+               size_t term_len = command_buf.len - 5 - 2;
+
+               strbuf_detach(&command_buf, NULL);
+               for (;;) {
+                       if (strbuf_getline(&command_buf, stdin, '\n') == EOF)
+                               die("EOF in data (terminator '%s' not found)", term);
+                       if (term_len == command_buf.len
+                               && !strcmp(term, command_buf.buf))
+                               break;
+                       strbuf_addbuf(sb, &command_buf);
+                       strbuf_addch(sb, '\n');
+               }
+               free(term);
+       }
+       else {
+               size_t n = 0, length;
+
+               length = strtoul(command_buf.buf + 5, NULL, 10);
+
+               while (n < length) {
+                       size_t s = strbuf_fread(sb, length - n, stdin);
+                       if (!s && feof(stdin))
+                               die("EOF in data (%lu bytes remaining)",
+                                       (unsigned long)(length - n));
+                       n += s;
+               }
+       }
+
+       skip_optional_lf();
+}
+
+static int validate_raw_date(const char *src, char *result, int maxlen)
+{
+       const char *orig_src = src;
+       char *endp, sign;
+
+       strtoul(src, &endp, 10);
+       if (endp == src || *endp != ' ')
+               return -1;
+
+       src = endp + 1;
+       if (*src != '-' && *src != '+')
+               return -1;
+       sign = *src;
+
+       strtoul(src + 1, &endp, 10);
+       if (endp == src || *endp || (endp - orig_src) >= maxlen)
+               return -1;
+
+       strcpy(result, orig_src);
+       return 0;
+}
+
+static char *parse_ident(const char *buf)
+{
+       const char *gt;
+       size_t name_len;
+       char *ident;
+
+       gt = strrchr(buf, '>');
+       if (!gt)
+               die("Missing > in ident string: %s", buf);
+       gt++;
+       if (*gt != ' ')
+               die("Missing space after > in ident string: %s", buf);
+       gt++;
+       name_len = gt - buf;
+       ident = xmalloc(name_len + 24);
+       strncpy(ident, buf, name_len);
+
+       switch (whenspec) {
+       case WHENSPEC_RAW:
+               if (validate_raw_date(gt, ident + name_len, 24) < 0)
+                       die("Invalid raw date \"%s\" in ident: %s", gt, buf);
+               break;
+       case WHENSPEC_RFC2822:
+               if (parse_date(gt, ident + name_len, 24) < 0)
+                       die("Invalid rfc2822 date \"%s\" in ident: %s", gt, buf);
+               break;
+       case WHENSPEC_NOW:
+               if (strcmp("now", gt))
+                       die("Date in ident must be 'now': %s", buf);
+               datestamp(ident + name_len, 24);
+               break;
+       }
+
+       return ident;
+}
+
+static void cmd_new_blob(void)
+{
+       static struct strbuf buf = STRBUF_INIT;
+
+       read_next_command();
+       cmd_mark();
+       cmd_data(&buf);
+       store_object(OBJ_BLOB, &buf, &last_blob, NULL, next_mark);
+}
+
+static void unload_one_branch(void)
+{
+       while (cur_active_branches
+               && cur_active_branches >= max_active_branches) {
+               uintmax_t min_commit = ULONG_MAX;
+               struct branch *e, *l = NULL, *p = NULL;
+
+               for (e = active_branches; e; e = e->active_next_branch) {
+                       if (e->last_commit < min_commit) {
+                               p = l;
+                               min_commit = e->last_commit;
+                       }
+                       l = e;
+               }
+
+               if (p) {
+                       e = p->active_next_branch;
+                       p->active_next_branch = e->active_next_branch;
+               } else {
+                       e = active_branches;
+                       active_branches = e->active_next_branch;
+               }
+               e->active = 0;
+               e->active_next_branch = NULL;
+               if (e->branch_tree.tree) {
+                       release_tree_content_recursive(e->branch_tree.tree);
+                       e->branch_tree.tree = NULL;
+               }
+               cur_active_branches--;
+       }
+}
+
+static void load_branch(struct branch *b)
+{
+       load_tree(&b->branch_tree);
+       if (!b->active) {
+               b->active = 1;
+               b->active_next_branch = active_branches;
+               active_branches = b;
+               cur_active_branches++;
+               branch_load_count++;
+       }
+}
+
+static void file_change_m(struct branch *b)
+{
+       const char *p = command_buf.buf + 2;
+       static struct strbuf uq = STRBUF_INIT;
+       const char *endp;
+       struct object_entry *oe = oe;
+       unsigned char sha1[20];
+       uint16_t mode, inline_data = 0;
+
+       p = get_mode(p, &mode);
+       if (!p)
+               die("Corrupt mode: %s", command_buf.buf);
+       switch (mode) {
+       case S_IFREG | 0644:
+       case S_IFREG | 0755:
+       case S_IFLNK:
+       case 0644:
+       case 0755:
+               /* ok */
+               break;
+       default:
+               die("Corrupt mode: %s", command_buf.buf);
+       }
+
+       if (*p == ':') {
+               char *x;
+               oe = find_mark(strtoumax(p + 1, &x, 10));
+               hashcpy(sha1, oe->sha1);
+               p = x;
+       } else if (!prefixcmp(p, "inline")) {
+               inline_data = 1;
+               p += 6;
+       } else {
+               if (get_sha1_hex(p, sha1))
+                       die("Invalid SHA1: %s", command_buf.buf);
+               oe = find_object(sha1);
+               p += 40;
+       }
+       if (*p++ != ' ')
+               die("Missing space after SHA1: %s", command_buf.buf);
+
+       strbuf_reset(&uq);
+       if (!unquote_c_style(&uq, p, &endp)) {
+               if (*endp)
+                       die("Garbage after path in: %s", command_buf.buf);
+               p = uq.buf;
+       }
+
+       if (inline_data) {
+               static struct strbuf buf = STRBUF_INIT;
+
+               if (p != uq.buf) {
+                       strbuf_addstr(&uq, p);
+                       p = uq.buf;
+               }
+               read_next_command();
+               cmd_data(&buf);
+               store_object(OBJ_BLOB, &buf, &last_blob, sha1, 0);
+       } else if (oe) {
+               if (oe->type != OBJ_BLOB)
+                       die("Not a blob (actually a %s): %s",
+                               typename(oe->type), command_buf.buf);
+       } else {
+               enum object_type type = sha1_object_info(sha1, NULL);
+               if (type < 0)
+                       die("Blob not found: %s", command_buf.buf);
+               if (type != OBJ_BLOB)
+                       die("Not a blob (actually a %s): %s",
+                           typename(type), command_buf.buf);
+       }
+
+       tree_content_set(&b->branch_tree, p, sha1, S_IFREG | mode, NULL);
+}
+
+static void file_change_d(struct branch *b)
+{
+       const char *p = command_buf.buf + 2;
+       static struct strbuf uq = STRBUF_INIT;
+       const char *endp;
+
+       strbuf_reset(&uq);
+       if (!unquote_c_style(&uq, p, &endp)) {
+               if (*endp)
+                       die("Garbage after path in: %s", command_buf.buf);
+               p = uq.buf;
+       }
+       tree_content_remove(&b->branch_tree, p, NULL);
+}
+
+static void file_change_cr(struct branch *b, int rename)
+{
+       const char *s, *d;
+       static struct strbuf s_uq = STRBUF_INIT;
+       static struct strbuf d_uq = STRBUF_INIT;
+       const char *endp;
+       struct tree_entry leaf;
+
+       s = command_buf.buf + 2;
+       strbuf_reset(&s_uq);
+       if (!unquote_c_style(&s_uq, s, &endp)) {
+               if (*endp != ' ')
+                       die("Missing space after source: %s", command_buf.buf);
+       } else {
+               endp = strchr(s, ' ');
+               if (!endp)
+                       die("Missing space after source: %s", command_buf.buf);
+               strbuf_add(&s_uq, s, endp - s);
+       }
+       s = s_uq.buf;
+
+       endp++;
+       if (!*endp)
+               die("Missing dest: %s", command_buf.buf);
+
+       d = endp;
+       strbuf_reset(&d_uq);
+       if (!unquote_c_style(&d_uq, d, &endp)) {
+               if (*endp)
+                       die("Garbage after dest in: %s", command_buf.buf);
+               d = d_uq.buf;
+       }
+
+       memset(&leaf, 0, sizeof(leaf));
+       if (rename)
+               tree_content_remove(&b->branch_tree, s, &leaf);
+       else
+               tree_content_get(&b->branch_tree, s, &leaf);
+       if (!leaf.versions[1].mode)
+               die("Path %s not in branch", s);
+       tree_content_set(&b->branch_tree, d,
+               leaf.versions[1].sha1,
+               leaf.versions[1].mode,
+               leaf.tree);
+}
+
+static void file_change_deleteall(struct branch *b)
+{
+       release_tree_content_recursive(b->branch_tree.tree);
+       hashclr(b->branch_tree.versions[0].sha1);
+       hashclr(b->branch_tree.versions[1].sha1);
+       load_tree(&b->branch_tree);
+}
+
+static void cmd_from_commit(struct branch *b, char *buf, unsigned long size)
+{
+       if (!buf || size < 46)
+               die("Not a valid commit: %s", sha1_to_hex(b->sha1));
+       if (memcmp("tree ", buf, 5)
+               || get_sha1_hex(buf + 5, b->branch_tree.versions[1].sha1))
+               die("The commit %s is corrupt", sha1_to_hex(b->sha1));
+       hashcpy(b->branch_tree.versions[0].sha1,
+               b->branch_tree.versions[1].sha1);
+}
+
+static void cmd_from_existing(struct branch *b)
+{
+       if (is_null_sha1(b->sha1)) {
+               hashclr(b->branch_tree.versions[0].sha1);
+               hashclr(b->branch_tree.versions[1].sha1);
+       } else {
+               unsigned long size;
+               char *buf;
+
+               buf = read_object_with_reference(b->sha1,
+                       commit_type, &size, b->sha1);
+               cmd_from_commit(b, buf, size);
+               free(buf);
+       }
+}
+
+static int cmd_from(struct branch *b)
+{
+       const char *from;
+       struct branch *s;
+
+       if (prefixcmp(command_buf.buf, "from "))
+               return 0;
+
+       if (b->branch_tree.tree) {
+               release_tree_content_recursive(b->branch_tree.tree);
+               b->branch_tree.tree = NULL;
+       }
+
+       from = strchr(command_buf.buf, ' ') + 1;
+       s = lookup_branch(from);
+       if (b == s)
+               die("Can't create a branch from itself: %s", b->name);
+       else if (s) {
+               unsigned char *t = s->branch_tree.versions[1].sha1;
+               hashcpy(b->sha1, s->sha1);
+               hashcpy(b->branch_tree.versions[0].sha1, t);
+               hashcpy(b->branch_tree.versions[1].sha1, t);
+       } else if (*from == ':') {
+               uintmax_t idnum = strtoumax(from + 1, NULL, 10);
+               struct object_entry *oe = find_mark(idnum);
+               if (oe->type != OBJ_COMMIT)
+                       die("Mark :%" PRIuMAX " not a commit", idnum);
+               hashcpy(b->sha1, oe->sha1);
+               if (oe->pack_id != MAX_PACK_ID) {
+                       unsigned long size;
+                       char *buf = gfi_unpack_entry(oe, &size);
+                       cmd_from_commit(b, buf, size);
+                       free(buf);
+               } else
+                       cmd_from_existing(b);
+       } else if (!get_sha1(from, b->sha1))
+               cmd_from_existing(b);
+       else
+               die("Invalid ref name or SHA1 expression: %s", from);
+
+       read_next_command();
+       return 1;
+}
+
+static struct hash_list *cmd_merge(unsigned int *count)
+{
+       struct hash_list *list = NULL, *n, *e = e;
+       const char *from;
+       struct branch *s;
+
+       *count = 0;
+       while (!prefixcmp(command_buf.buf, "merge ")) {
+               from = strchr(command_buf.buf, ' ') + 1;
+               n = xmalloc(sizeof(*n));
+               s = lookup_branch(from);
+               if (s)
+                       hashcpy(n->sha1, s->sha1);
+               else if (*from == ':') {
+                       uintmax_t idnum = strtoumax(from + 1, NULL, 10);
+                       struct object_entry *oe = find_mark(idnum);
+                       if (oe->type != OBJ_COMMIT)
+                               die("Mark :%" PRIuMAX " not a commit", idnum);
+                       hashcpy(n->sha1, oe->sha1);
+               } else if (!get_sha1(from, n->sha1)) {
+                       unsigned long size;
+                       char *buf = read_object_with_reference(n->sha1,
+                               commit_type, &size, n->sha1);
+                       if (!buf || size < 46)
+                               die("Not a valid commit: %s", from);
+                       free(buf);
+               } else
+                       die("Invalid ref name or SHA1 expression: %s", from);
+
+               n->next = NULL;
+               if (list)
+                       e->next = n;
+               else
+                       list = n;
+               e = n;
+               (*count)++;
+               read_next_command();
+       }
+       return list;
+}
+
+static void cmd_new_commit(void)
+{
+       static struct strbuf msg = STRBUF_INIT;
+       struct branch *b;
+       char *sp;
+       char *author = NULL;
+       char *committer = NULL;
+       struct hash_list *merge_list = NULL;
+       unsigned int merge_count;
+
+       /* Obtain the branch name from the rest of our command */
+       sp = strchr(command_buf.buf, ' ') + 1;
+       b = lookup_branch(sp);
+       if (!b)
+               b = new_branch(sp);
+
+       read_next_command();
+       cmd_mark();
+       if (!prefixcmp(command_buf.buf, "author ")) {
+               author = parse_ident(command_buf.buf + 7);
+               read_next_command();
+       }
+       if (!prefixcmp(command_buf.buf, "committer ")) {
+               committer = parse_ident(command_buf.buf + 10);
+               read_next_command();
+       }
+       if (!committer)
+               die("Expected committer but didn't get one");
+       cmd_data(&msg);
+       read_next_command();
+       cmd_from(b);
+       merge_list = cmd_merge(&merge_count);
+
+       /* ensure the branch is active/loaded */
+       if (!b->branch_tree.tree || !max_active_branches) {
+               unload_one_branch();
+               load_branch(b);
+       }
+
+       /* file_change* */
+       while (command_buf.len > 0) {
+               if (!prefixcmp(command_buf.buf, "M "))
+                       file_change_m(b);
+               else if (!prefixcmp(command_buf.buf, "D "))
+                       file_change_d(b);
+               else if (!prefixcmp(command_buf.buf, "R "))
+                       file_change_cr(b, 1);
+               else if (!prefixcmp(command_buf.buf, "C "))
+                       file_change_cr(b, 0);
+               else if (!strcmp("deleteall", command_buf.buf))
+                       file_change_deleteall(b);
+               else {
+                       unread_command_buf = 1;
+                       break;
+               }
+               if (read_next_command() == EOF)
+                       break;
+       }
+
+       /* build the tree and the commit */
+       store_tree(&b->branch_tree);
+       hashcpy(b->branch_tree.versions[0].sha1,
+               b->branch_tree.versions[1].sha1);
+
+       strbuf_reset(&new_data);
+       strbuf_addf(&new_data, "tree %s\n",
+               sha1_to_hex(b->branch_tree.versions[1].sha1));
+       if (!is_null_sha1(b->sha1))
+               strbuf_addf(&new_data, "parent %s\n", sha1_to_hex(b->sha1));
+       while (merge_list) {
+               struct hash_list *next = merge_list->next;
+               strbuf_addf(&new_data, "parent %s\n", sha1_to_hex(merge_list->sha1));
+               free(merge_list);
+               merge_list = next;
+       }
+       strbuf_addf(&new_data,
+               "author %s\n"
+               "committer %s\n"
+               "\n",
+               author ? author : committer, committer);
+       strbuf_addbuf(&new_data, &msg);
+       free(author);
+       free(committer);
+
+       if (!store_object(OBJ_COMMIT, &new_data, NULL, b->sha1, next_mark))
+               b->pack_id = pack_id;
+       b->last_commit = object_count_by_type[OBJ_COMMIT];
+}
+
+static void cmd_new_tag(void)
+{
+       static struct strbuf msg = STRBUF_INIT;
+       char *sp;
+       const char *from;
+       char *tagger;
+       struct branch *s;
+       struct tag *t;
+       uintmax_t from_mark = 0;
+       unsigned char sha1[20];
+
+       /* Obtain the new tag name from the rest of our command */
+       sp = strchr(command_buf.buf, ' ') + 1;
+       t = pool_alloc(sizeof(struct tag));
+       t->next_tag = NULL;
+       t->name = pool_strdup(sp);
+       if (last_tag)
+               last_tag->next_tag = t;
+       else
+               first_tag = t;
+       last_tag = t;
+       read_next_command();
+
+       /* from ... */
+       if (prefixcmp(command_buf.buf, "from "))
+               die("Expected from command, got %s", command_buf.buf);
+       from = strchr(command_buf.buf, ' ') + 1;
+       s = lookup_branch(from);
+       if (s) {
+               hashcpy(sha1, s->sha1);
+       } else if (*from == ':') {
+               struct object_entry *oe;
+               from_mark = strtoumax(from + 1, NULL, 10);
+               oe = find_mark(from_mark);
+               if (oe->type != OBJ_COMMIT)
+                       die("Mark :%" PRIuMAX " not a commit", from_mark);
+               hashcpy(sha1, oe->sha1);
+       } else if (!get_sha1(from, sha1)) {
+               unsigned long size;
+               char *buf;
+
+               buf = read_object_with_reference(sha1,
+                       commit_type, &size, sha1);
+               if (!buf || size < 46)
+                       die("Not a valid commit: %s", from);
+               free(buf);
+       } else
+               die("Invalid ref name or SHA1 expression: %s", from);
+       read_next_command();
+
+       /* tagger ... */
+       if (prefixcmp(command_buf.buf, "tagger "))
+               die("Expected tagger command, got %s", command_buf.buf);
+       tagger = parse_ident(command_buf.buf + 7);
+
+       /* tag payload/message */
+       read_next_command();
+       cmd_data(&msg);
+
+       /* build the tag object */
+       strbuf_reset(&new_data);
+       strbuf_addf(&new_data,
+               "object %s\n"
+               "type %s\n"
+               "tag %s\n"
+               "tagger %s\n"
+               "\n",
+               sha1_to_hex(sha1), commit_type, t->name, tagger);
+       strbuf_addbuf(&new_data, &msg);
+       free(tagger);
+
+       if (store_object(OBJ_TAG, &new_data, NULL, t->sha1, 0))
+               t->pack_id = MAX_PACK_ID;
+       else
+               t->pack_id = pack_id;
+}
+
+static void cmd_reset_branch(void)
+{
+       struct branch *b;
+       char *sp;
+
+       /* Obtain the branch name from the rest of our command */
+       sp = strchr(command_buf.buf, ' ') + 1;
+       b = lookup_branch(sp);
+       if (b) {
+               hashclr(b->sha1);
+               hashclr(b->branch_tree.versions[0].sha1);
+               hashclr(b->branch_tree.versions[1].sha1);
+               if (b->branch_tree.tree) {
+                       release_tree_content_recursive(b->branch_tree.tree);
+                       b->branch_tree.tree = NULL;
+               }
+       }
+       else
+               b = new_branch(sp);
+       read_next_command();
+       if (!cmd_from(b) && command_buf.len > 0)
+               unread_command_buf = 1;
+}
+
+static void cmd_checkpoint(void)
+{
+       if (object_count) {
+               cycle_packfile();
+               dump_branches();
+               dump_tags();
+               dump_marks();
+       }
+       skip_optional_lf();
+}
+
+static void cmd_progress(void)
+{
+       fwrite(command_buf.buf, 1, command_buf.len, stdout);
+       fputc('\n', stdout);
+       fflush(stdout);
+       skip_optional_lf();
+}
+
+static void import_marks(const char *input_file)
+{
+       char line[512];
+       FILE *f = fopen(input_file, "r");
+       if (!f)
+               die("cannot read %s: %s", input_file, strerror(errno));
+       while (fgets(line, sizeof(line), f)) {
+               uintmax_t mark;
+               char *end;
+               unsigned char sha1[20];
+               struct object_entry *e;
+
+               end = strchr(line, '\n');
+               if (line[0] != ':' || !end)
+                       die("corrupt mark line: %s", line);
+               *end = 0;
+               mark = strtoumax(line + 1, &end, 10);
+               if (!mark || end == line + 1
+                       || *end != ' ' || get_sha1(end + 1, sha1))
+                       die("corrupt mark line: %s", line);
+               e = find_object(sha1);
+               if (!e) {
+                       enum object_type type = sha1_object_info(sha1, NULL);
+                       if (type < 0)
+                               die("object not found: %s", sha1_to_hex(sha1));
+                       e = insert_object(sha1);
+                       e->type = type;
+                       e->pack_id = MAX_PACK_ID;
+                       e->offset = 1; /* just not zero! */
+               }
+               insert_mark(mark, e);
+       }
+       fclose(f);
+}
+
+static const char fast_import_usage[] =
+"git-fast-import [--date-format=f] [--max-pack-size=n] [--depth=n] [--active-branches=n] [--export-marks=marks.file]";
+
+int main(int argc, const char **argv)
+{
+       unsigned int i, show_stats = 1;
+
+       git_config(git_default_config);
+       alloc_objects(object_entry_alloc);
+       strbuf_init(&command_buf, 0);
+       atom_table = xcalloc(atom_table_sz, sizeof(struct atom_str*));
+       branch_table = xcalloc(branch_table_sz, sizeof(struct branch*));
+       avail_tree_table = xcalloc(avail_tree_table_sz, sizeof(struct avail_tree_content*));
+       marks = pool_calloc(1, sizeof(struct mark_set));
+
+       for (i = 1; i < argc; i++) {
+               const char *a = argv[i];
+
+               if (*a != '-' || !strcmp(a, "--"))
+                       break;
+               else if (!prefixcmp(a, "--date-format=")) {
+                       const char *fmt = a + 14;
+                       if (!strcmp(fmt, "raw"))
+                               whenspec = WHENSPEC_RAW;
+                       else if (!strcmp(fmt, "rfc2822"))
+                               whenspec = WHENSPEC_RFC2822;
+                       else if (!strcmp(fmt, "now"))
+                               whenspec = WHENSPEC_NOW;
+                       else
+                               die("unknown --date-format argument %s", fmt);
+               }
+               else if (!prefixcmp(a, "--max-pack-size="))
+                       max_packsize = strtoumax(a + 16, NULL, 0) * 1024 * 1024;
+               else if (!prefixcmp(a, "--depth=")) {
+                       max_depth = strtoul(a + 8, NULL, 0);
+                       if (max_depth > MAX_DEPTH)
+                               die("--depth cannot exceed %u", MAX_DEPTH);
+               }
+               else if (!prefixcmp(a, "--active-branches="))
+                       max_active_branches = strtoul(a + 18, NULL, 0);
+               else if (!prefixcmp(a, "--import-marks="))
+                       import_marks(a + 15);
+               else if (!prefixcmp(a, "--export-marks="))
+                       mark_file = a + 15;
+               else if (!prefixcmp(a, "--export-pack-edges=")) {
+                       if (pack_edges)
+                               fclose(pack_edges);
+                       pack_edges = fopen(a + 20, "a");
+                       if (!pack_edges)
+                               die("Cannot open %s: %s", a + 20, strerror(errno));
+               } else if (!strcmp(a, "--force"))
+                       force_update = 1;
+               else if (!strcmp(a, "--quiet"))
+                       show_stats = 0;
+               else if (!strcmp(a, "--stats"))
+                       show_stats = 1;
+               else
+                       die("unknown option %s", a);
+       }
+       if (i != argc)
+               usage(fast_import_usage);
+
+       rc_free = pool_alloc(cmd_save * sizeof(*rc_free));
+       for (i = 0; i < (cmd_save - 1); i++)
+               rc_free[i].next = &rc_free[i + 1];
+       rc_free[cmd_save - 1].next = NULL;
+
+       prepare_packed_git();
+       start_packfile();
+       set_die_routine(die_nicely);
+       while (read_next_command() != EOF) {
+               if (!strcmp("blob", command_buf.buf))
+                       cmd_new_blob();
+               else if (!prefixcmp(command_buf.buf, "commit "))
+                       cmd_new_commit();
+               else if (!prefixcmp(command_buf.buf, "tag "))
+                       cmd_new_tag();
+               else if (!prefixcmp(command_buf.buf, "reset "))
+                       cmd_reset_branch();
+               else if (!strcmp("checkpoint", command_buf.buf))
+                       cmd_checkpoint();
+               else if (!prefixcmp(command_buf.buf, "progress "))
+                       cmd_progress();
+               else
+                       die("Unsupported command: %s", command_buf.buf);
+       }
+       end_packfile();
+
+       dump_branches();
+       dump_tags();
+       unkeep_all_packs();
+       dump_marks();
+
+       if (pack_edges)
+               fclose(pack_edges);
+
+       if (show_stats) {
+               uintmax_t total_count = 0, duplicate_count = 0;
+               for (i = 0; i < ARRAY_SIZE(object_count_by_type); i++)
+                       total_count += object_count_by_type[i];
+               for (i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++)
+                       duplicate_count += duplicate_count_by_type[i];
+
+               fprintf(stderr, "%s statistics:\n", argv[0]);
+               fprintf(stderr, "---------------------------------------------------------------------\n");
+               fprintf(stderr, "Alloc'd objects: %10" PRIuMAX "\n", alloc_count);
+               fprintf(stderr, "Total objects:   %10" PRIuMAX " (%10" PRIuMAX " duplicates                  )\n", total_count, duplicate_count);
+               fprintf(stderr, "      blobs  :   %10" PRIuMAX " (%10" PRIuMAX " duplicates %10" PRIuMAX " deltas)\n", object_count_by_type[OBJ_BLOB], duplicate_count_by_type[OBJ_BLOB], delta_count_by_type[OBJ_BLOB]);
+               fprintf(stderr, "      trees  :   %10" PRIuMAX " (%10" PRIuMAX " duplicates %10" PRIuMAX " deltas)\n", object_count_by_type[OBJ_TREE], duplicate_count_by_type[OBJ_TREE], delta_count_by_type[OBJ_TREE]);
+               fprintf(stderr, "      commits:   %10" PRIuMAX " (%10" PRIuMAX " duplicates %10" PRIuMAX " deltas)\n", object_count_by_type[OBJ_COMMIT], duplicate_count_by_type[OBJ_COMMIT], delta_count_by_type[OBJ_COMMIT]);
+               fprintf(stderr, "      tags   :   %10" PRIuMAX " (%10" PRIuMAX " duplicates %10" PRIuMAX " deltas)\n", object_count_by_type[OBJ_TAG], duplicate_count_by_type[OBJ_TAG], delta_count_by_type[OBJ_TAG]);
+               fprintf(stderr, "Total branches:  %10lu (%10lu loads     )\n", branch_count, branch_load_count);
+               fprintf(stderr, "      marks:     %10" PRIuMAX " (%10" PRIuMAX " unique    )\n", (((uintmax_t)1) << marks->shift) * 1024, marks_set_count);
+               fprintf(stderr, "      atoms:     %10u\n", atom_cnt);
+               fprintf(stderr, "Memory total:    %10" PRIuMAX " KiB\n", (total_allocd + alloc_count*sizeof(struct object_entry))/1024);
+               fprintf(stderr, "       pools:    %10lu KiB\n", (unsigned long)(total_allocd/1024));
+               fprintf(stderr, "     objects:    %10" PRIuMAX " KiB\n", (alloc_count*sizeof(struct object_entry))/1024);
+               fprintf(stderr, "---------------------------------------------------------------------\n");
+               pack_report();
+               fprintf(stderr, "---------------------------------------------------------------------\n");
+               fprintf(stderr, "\n");
+       }
+
+       return failure ? 1 : 0;
+}
diff --git a/fetch-pack.h b/fetch-pack.h
new file mode 100644 (file)
index 0000000..a7888ea
--- /dev/null
@@ -0,0 +1,24 @@
+#ifndef FETCH_PACK_H
+#define FETCH_PACK_H
+
+struct fetch_pack_args
+{
+       const char *uploadpack;
+       int unpacklimit;
+       int depth;
+       unsigned quiet:1,
+               keep_pack:1,
+               lock_pack:1,
+               use_thin_pack:1,
+               fetch_all:1,
+               verbose:1,
+               no_progress:1;
+};
+
+struct ref *fetch_pack(struct fetch_pack_args *args,
+               const char *dest,
+               int nr_heads,
+               char **heads,
+               char **pack_lockfile);
+
+#endif
diff --git a/fixup-builtins b/fixup-builtins
new file mode 100755 (executable)
index 0000000..49e861d
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/sh
+while [ "$1" ]
+do
+       old="$1"
+       new=$(echo "$1" | sed 's/git-/git /')
+       echo "Converting '$old' to '$new'"
+       git ls-files '*.sh' | while read file
+       do
+               sed "s/\\<$old\\>/$new/g" < $file > $file.new
+               chmod --reference=$file $file.new
+               mv $file.new $file
+       done
+       shift
+done
+git update-index --refresh >& /dev/null
+exit 0
diff --git a/generate-cmdlist.sh b/generate-cmdlist.sh
new file mode 100755 (executable)
index 0000000..a2913c2
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+echo "/* Automatically generated by $0 */
+struct cmdname_help
+{
+    char name[16];
+    char help[80];
+};
+
+static struct cmdname_help common_cmds[] = {"
+
+sed -n -e 's/^git-\([^         ]*\)[   ].* common.*/\1/p' command-list.txt |
+sort |
+while read cmd
+do
+     sed -n '
+     /NAME/,/git-'"$cmd"'/H
+     ${
+            x
+            s/.*git-'"$cmd"' - \(.*\)/  {"'"$cmd"'", "\1"},/
+           p
+     }' "Documentation/git-$cmd.txt"
+done
+echo "};"
diff --git a/git-add--interactive.perl b/git-add--interactive.perl
new file mode 100755 (executable)
index 0000000..1019a72
--- /dev/null
@@ -0,0 +1,1046 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Git;
+
+# Prompt colors:
+my ($prompt_color, $header_color, $help_color, $normal_color);
+# Diff colors:
+my ($new_color, $old_color, $fraginfo_color, $metainfo_color, $whitespace_color);
+
+my ($use_color, $diff_use_color);
+my $repo = Git->repository();
+
+$use_color = $repo->get_colorbool('color.interactive');
+
+if ($use_color) {
+       # Set interactive colors:
+
+       # Grab the 3 main colors in git color string format, with sane
+       # (visible) defaults:
+       $prompt_color = $repo->get_color("color.interactive.prompt", "bold blue");
+       $header_color = $repo->get_color("color.interactive.header", "bold");
+       $help_color = $repo->get_color("color.interactive.help", "red bold");
+       $normal_color = $repo->get_color("", "reset");
+
+       # Do we also set diff colors?
+       $diff_use_color = $repo->get_colorbool('color.diff');
+       if ($diff_use_color) {
+               $new_color = $repo->get_color("color.diff.new", "green");
+               $old_color = $repo->get_color("color.diff.old", "red");
+               $fraginfo_color = $repo->get_color("color.diff.frag", "cyan");
+               $metainfo_color = $repo->get_color("color.diff.meta", "bold");
+               $whitespace_color = $repo->get_color("color.diff.whitespace", "normal red");
+       }
+}
+
+sub colored {
+       my $color = shift;
+       my $string = join("", @_);
+
+       if ($use_color) {
+               # Put a color code at the beginning of each line, a reset at the end
+               # color after newlines that are not at the end of the string
+               $string =~ s/(\n+)(.)/$1$color$2/g;
+               # reset before newlines
+               $string =~ s/(\n+)/$normal_color$1/g;
+               # codes at beginning and end (if necessary):
+               $string =~ s/^/$color/;
+               $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
+       }
+       return $string;
+}
+
+# command line options
+my $patch_mode;
+
+sub run_cmd_pipe {
+       if ($^O eq 'MSWin32') {
+               my @invalid = grep {m/[":*]/} @_;
+               die "$^O does not support: @invalid\n" if @invalid;
+               my @args = map { m/ /o ? "\"$_\"": $_ } @_;
+               return qx{@args};
+       } else {
+               my $fh = undef;
+               open($fh, '-|', @_) or die;
+               return <$fh>;
+       }
+}
+
+my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
+
+if (!defined $GIT_DIR) {
+       exit(1); # rev-parse would have already said "not a git repo"
+}
+chomp($GIT_DIR);
+
+sub refresh {
+       my $fh;
+       open $fh, 'git update-index --refresh |'
+           or die;
+       while (<$fh>) {
+               ;# ignore 'needs update'
+       }
+       close $fh;
+}
+
+sub list_untracked {
+       map {
+               chomp $_;
+               $_;
+       }
+       run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
+}
+
+my $status_fmt = '%12s %12s %s';
+my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');
+
+# Returns list of hashes, contents of each of which are:
+# VALUE:       pathname
+# BINARY:      is a binary path
+# INDEX:       is index different from HEAD?
+# FILE:                is file different from index?
+# INDEX_ADDDEL:        is it add/delete between HEAD and index?
+# FILE_ADDDEL: is it add/delete between index and file?
+
+sub list_modified {
+       my ($only) = @_;
+       my (%data, @return);
+       my ($add, $del, $adddel, $file);
+       my @tracked = ();
+
+       if (@ARGV) {
+               @tracked = map {
+                       chomp $_; $_;
+               } run_cmd_pipe(qw(git ls-files --exclude-standard --), @ARGV);
+               return if (!@tracked);
+       }
+
+       for (run_cmd_pipe(qw(git diff-index --cached
+                            --numstat --summary HEAD --), @tracked)) {
+               if (($add, $del, $file) =
+                   /^([-\d]+)  ([-\d]+)        (.*)/) {
+                       my ($change, $bin);
+                       if ($add eq '-' && $del eq '-') {
+                               $change = 'binary';
+                               $bin = 1;
+                       }
+                       else {
+                               $change = "+$add/-$del";
+                       }
+                       $data{$file} = {
+                               INDEX => $change,
+                               BINARY => $bin,
+                               FILE => 'nothing',
+                       }
+               }
+               elsif (($adddel, $file) =
+                      /^ (create|delete) mode [0-7]+ (.*)$/) {
+                       $data{$file}{INDEX_ADDDEL} = $adddel;
+               }
+       }
+
+       for (run_cmd_pipe(qw(git diff-files --numstat --summary --), @tracked)) {
+               if (($add, $del, $file) =
+                   /^([-\d]+)  ([-\d]+)        (.*)/) {
+                       if (!exists $data{$file}) {
+                               $data{$file} = +{
+                                       INDEX => 'unchanged',
+                                       BINARY => 0,
+                               };
+                       }
+                       my ($change, $bin);
+                       if ($add eq '-' && $del eq '-') {
+                               $change = 'binary';
+                               $bin = 1;
+                       }
+                       else {
+                               $change = "+$add/-$del";
+                       }
+                       $data{$file}{FILE} = $change;
+                       if ($bin) {
+                               $data{$file}{BINARY} = 1;
+                       }
+               }
+               elsif (($adddel, $file) =
+                      /^ (create|delete) mode [0-7]+ (.*)$/) {
+                       $data{$file}{FILE_ADDDEL} = $adddel;
+               }
+       }
+
+       for (sort keys %data) {
+               my $it = $data{$_};
+
+               if ($only) {
+                       if ($only eq 'index-only') {
+                               next if ($it->{INDEX} eq 'unchanged');
+                       }
+                       if ($only eq 'file-only') {
+                               next if ($it->{FILE} eq 'nothing');
+                       }
+               }
+               push @return, +{
+                       VALUE => $_,
+                       %$it,
+               };
+       }
+       return @return;
+}
+
+sub find_unique {
+       my ($string, @stuff) = @_;
+       my $found = undef;
+       for (my $i = 0; $i < @stuff; $i++) {
+               my $it = $stuff[$i];
+               my $hit = undef;
+               if (ref $it) {
+                       if ((ref $it) eq 'ARRAY') {
+                               $it = $it->[0];
+                       }
+                       else {
+                               $it = $it->{VALUE};
+                       }
+               }
+               eval {
+                       if ($it =~ /^$string/) {
+                               $hit = 1;
+                       };
+               };
+               if (defined $hit && defined $found) {
+                       return undef;
+               }
+               if ($hit) {
+                       $found = $i + 1;
+               }
+       }
+       return $found;
+}
+
+# inserts string into trie and updates count for each character
+sub update_trie {
+       my ($trie, $string) = @_;
+       foreach (split //, $string) {
+               $trie = $trie->{$_} ||= {COUNT => 0};
+               $trie->{COUNT}++;
+       }
+}
+
+# returns an array of tuples (prefix, remainder)
+sub find_unique_prefixes {
+       my @stuff = @_;
+       my @return = ();
+
+       # any single prefix exceeding the soft limit is omitted
+       # if any prefix exceeds the hard limit all are omitted
+       # 0 indicates no limit
+       my $soft_limit = 0;
+       my $hard_limit = 3;
+
+       # build a trie modelling all possible options
+       my %trie;
+       foreach my $print (@stuff) {
+               if ((ref $print) eq 'ARRAY') {
+                       $print = $print->[0];
+               }
+               elsif ((ref $print) eq 'HASH') {
+                       $print = $print->{VALUE};
+               }
+               update_trie(\%trie, $print);
+               push @return, $print;
+       }
+
+       # use the trie to find the unique prefixes
+       for (my $i = 0; $i < @return; $i++) {
+               my $ret = $return[$i];
+               my @letters = split //, $ret;
+               my %search = %trie;
+               my ($prefix, $remainder);
+               my $j;
+               for ($j = 0; $j < @letters; $j++) {
+                       my $letter = $letters[$j];
+                       if ($search{$letter}{COUNT} == 1) {
+                               $prefix = substr $ret, 0, $j + 1;
+                               $remainder = substr $ret, $j + 1;
+                               last;
+                       }
+                       else {
+                               my $prefix = substr $ret, 0, $j;
+                               return ()
+                                   if ($hard_limit && $j + 1 > $hard_limit);
+                       }
+                       %search = %{$search{$letter}};
+               }
+               if ($soft_limit && $j + 1 > $soft_limit) {
+                       $prefix = undef;
+                       $remainder = $ret;
+               }
+               $return[$i] = [$prefix, $remainder];
+       }
+       return @return;
+}
+
+# filters out prefixes which have special meaning to list_and_choose()
+sub is_valid_prefix {
+       my $prefix = shift;
+       return (defined $prefix) &&
+           !($prefix =~ /[\s,]/) && # separators
+           !($prefix =~ /^-/) &&    # deselection
+           !($prefix =~ /^\d+/) &&  # selection
+           ($prefix ne '*') &&      # "all" wildcard
+           ($prefix ne '?');        # prompt help
+}
+
+# given a prefix/remainder tuple return a string with the prefix highlighted
+# for now use square brackets; later might use ANSI colors (underline, bold)
+sub highlight_prefix {
+       my $prefix = shift;
+       my $remainder = shift;
+
+       if (!defined $prefix) {
+               return $remainder;
+       }
+
+       if (!is_valid_prefix($prefix)) {
+               return "$prefix$remainder";
+       }
+
+       if (!$use_color) {
+               return "[$prefix]$remainder";
+       }
+
+       return "$prompt_color$prefix$normal_color$remainder";
+}
+
+sub list_and_choose {
+       my ($opts, @stuff) = @_;
+       my (@chosen, @return);
+       my $i;
+       my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
+
+      TOPLOOP:
+       while (1) {
+               my $last_lf = 0;
+
+               if ($opts->{HEADER}) {
+                       if (!$opts->{LIST_FLAT}) {
+                               print "     ";
+                       }
+                       print colored $header_color, "$opts->{HEADER}\n";
+               }
+               for ($i = 0; $i < @stuff; $i++) {
+                       my $chosen = $chosen[$i] ? '*' : ' ';
+                       my $print = $stuff[$i];
+                       my $ref = ref $print;
+                       my $highlighted = highlight_prefix(@{$prefixes[$i]})
+                           if @prefixes;
+                       if ($ref eq 'ARRAY') {
+                               $print = $highlighted || $print->[0];
+                       }
+                       elsif ($ref eq 'HASH') {
+                               my $value = $highlighted || $print->{VALUE};
+                               $print = sprintf($status_fmt,
+                                   $print->{INDEX},
+                                   $print->{FILE},
+                                   $value);
+                       }
+                       else {
+                               $print = $highlighted || $print;
+                       }
+                       printf("%s%2d: %s", $chosen, $i+1, $print);
+                       if (($opts->{LIST_FLAT}) &&
+                           (($i + 1) % ($opts->{LIST_FLAT}))) {
+                               print "\t";
+                               $last_lf = 0;
+                       }
+                       else {
+                               print "\n";
+                               $last_lf = 1;
+                       }
+               }
+               if (!$last_lf) {
+                       print "\n";
+               }
+
+               return if ($opts->{LIST_ONLY});
+
+               print colored $prompt_color, $opts->{PROMPT};
+               if ($opts->{SINGLETON}) {
+                       print "> ";
+               }
+               else {
+                       print ">> ";
+               }
+               my $line = <STDIN>;
+               if (!$line) {
+                       print "\n";
+                       $opts->{ON_EOF}->() if $opts->{ON_EOF};
+                       last;
+               }
+               chomp $line;
+               last if $line eq '';
+               if ($line eq '?') {
+                       $opts->{SINGLETON} ?
+                           singleton_prompt_help_cmd() :
+                           prompt_help_cmd();
+                       next TOPLOOP;
+               }
+               for my $choice (split(/[\s,]+/, $line)) {
+                       my $choose = 1;
+                       my ($bottom, $top);
+
+                       # Input that begins with '-'; unchoose
+                       if ($choice =~ s/^-//) {
+                               $choose = 0;
+                       }
+                       # A range can be specified like 5-7
+                       if ($choice =~ /^(\d+)-(\d+)$/) {
+                               ($bottom, $top) = ($1, $2);
+                       }
+                       elsif ($choice =~ /^\d+$/) {
+                               $bottom = $top = $choice;
+                       }
+                       elsif ($choice eq '*') {
+                               $bottom = 1;
+                               $top = 1 + @stuff;
+                       }
+                       else {
+                               $bottom = $top = find_unique($choice, @stuff);
+                               if (!defined $bottom) {
+                                       print "Huh ($choice)?\n";
+                                       next TOPLOOP;
+                               }
+                       }
+                       if ($opts->{SINGLETON} && $bottom != $top) {
+                               print "Huh ($choice)?\n";
+                               next TOPLOOP;
+                       }
+                       for ($i = $bottom-1; $i <= $top-1; $i++) {
+                               next if (@stuff <= $i || $i < 0);
+                               $chosen[$i] = $choose;
+                       }
+               }
+               last if ($opts->{IMMEDIATE} || $line eq '*');
+       }
+       for ($i = 0; $i < @stuff; $i++) {
+               if ($chosen[$i]) {
+                       push @return, $stuff[$i];
+               }
+       }
+       return @return;
+}
+
+sub singleton_prompt_help_cmd {
+       print colored $help_color, <<\EOF ;
+Prompt help:
+1          - select a numbered item
+foo        - select item based on unique prefix
+           - (empty) select nothing
+EOF
+}
+
+sub prompt_help_cmd {
+       print colored $help_color, <<\EOF ;
+Prompt help:
+1          - select a single item
+3-5        - select a range of items
+2-3,6-9    - select multiple ranges
+foo        - select item based on unique prefix
+-...       - unselect specified items
+*          - choose all items
+           - (empty) finish selecting
+EOF
+}
+
+sub status_cmd {
+       list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
+                       list_modified());
+       print "\n";
+}
+
+sub say_n_paths {
+       my $did = shift @_;
+       my $cnt = scalar @_;
+       print "$did ";
+       if (1 < $cnt) {
+               print "$cnt paths\n";
+       }
+       else {
+               print "one path\n";
+       }
+}
+
+sub update_cmd {
+       my @mods = list_modified('file-only');
+       return if (!@mods);
+
+       my @update = list_and_choose({ PROMPT => 'Update',
+                                      HEADER => $status_head, },
+                                    @mods);
+       if (@update) {
+               system(qw(git update-index --add --remove --),
+                      map { $_->{VALUE} } @update);
+               say_n_paths('updated', @update);
+       }
+       print "\n";
+}
+
+sub revert_cmd {
+       my @update = list_and_choose({ PROMPT => 'Revert',
+                                      HEADER => $status_head, },
+                                    list_modified());
+       if (@update) {
+               my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
+                                        map { $_->{VALUE} } @update);
+               my $fh;
+               open $fh, '| git update-index --index-info'
+                   or die;
+               for (@lines) {
+                       print $fh $_;
+               }
+               close($fh);
+               for (@update) {
+                       if ($_->{INDEX_ADDDEL} &&
+                           $_->{INDEX_ADDDEL} eq 'create') {
+                               system(qw(git update-index --force-remove --),
+                                      $_->{VALUE});
+                               print "note: $_->{VALUE} is untracked now.\n";
+                       }
+               }
+               refresh();
+               say_n_paths('reverted', @update);
+       }
+       print "\n";
+}
+
+sub add_untracked_cmd {
+       my @add = list_and_choose({ PROMPT => 'Add untracked' },
+                                 list_untracked());
+       if (@add) {
+               system(qw(git update-index --add --), @add);
+               say_n_paths('added', @add);
+       }
+       print "\n";
+}
+
+sub parse_diff {
+       my ($path) = @_;
+       my @diff = run_cmd_pipe(qw(git diff-files -p --), $path);
+       my (@hunk) = { TEXT => [] };
+
+       for (@diff) {
+               if (/^@@ /) {
+                       push @hunk, { TEXT => [] };
+               }
+               push @{$hunk[-1]{TEXT}}, $_;
+       }
+       return @hunk;
+}
+
+sub colored_diff_hunk {
+       my ($text) = @_;
+       # return the text, so that it can be passed to print()
+       my @ret;
+       for (@$text) {
+               if (!$diff_use_color) {
+                       push @ret, $_;
+                       next;
+               }
+
+               if (/^\+/) {
+                       push @ret, colored($new_color, $_);
+               } elsif (/^\-/) {
+                       push @ret, colored($old_color, $_);
+               } elsif (/^\@/) {
+                       push @ret, colored($fraginfo_color, $_);
+               } elsif (/^ /) {
+                       push @ret, colored($normal_color, $_);
+               } else {
+                       push @ret, colored($metainfo_color, $_);
+               }
+       }
+       return @ret;
+}
+
+sub hunk_splittable {
+       my ($text) = @_;
+
+       my @s = split_hunk($text);
+       return (1 < @s);
+}
+
+sub parse_hunk_header {
+       my ($line) = @_;
+       my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
+           $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
+       $o_cnt = 1 unless defined $o_cnt;
+       $n_cnt = 1 unless defined $n_cnt;
+       return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
+}
+
+sub split_hunk {
+       my ($text) = @_;
+       my @split = ();
+
+       # If there are context lines in the middle of a hunk,
+       # it can be split, but we would need to take care of
+       # overlaps later.
+
+       my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
+       my $hunk_start = 1;
+
+      OUTER:
+       while (1) {
+               my $next_hunk_start = undef;
+               my $i = $hunk_start - 1;
+               my $this = +{
+                       TEXT => [],
+                       OLD => $o_ofs,
+                       NEW => $n_ofs,
+                       OCNT => 0,
+                       NCNT => 0,
+                       ADDDEL => 0,
+                       POSTCTX => 0,
+               };
+
+               while (++$i < @$text) {
+                       my $line = $text->[$i];
+                       if ($line =~ /^ /) {
+                               if ($this->{ADDDEL} &&
+                                   !defined $next_hunk_start) {
+                                       # We have seen leading context and
+                                       # adds/dels and then here is another
+                                       # context, which is trailing for this
+                                       # split hunk and leading for the next
+                                       # one.
+                                       $next_hunk_start = $i;
+                               }
+                               push @{$this->{TEXT}}, $line;
+                               $this->{OCNT}++;
+                               $this->{NCNT}++;
+                               if (defined $next_hunk_start) {
+                                       $this->{POSTCTX}++;
+                               }
+                               next;
+                       }
+
+                       # add/del
+                       if (defined $next_hunk_start) {
+                               # We are done with the current hunk and
+                               # this is the first real change for the
+                               # next split one.
+                               $hunk_start = $next_hunk_start;
+                               $o_ofs = $this->{OLD} + $this->{OCNT};
+                               $n_ofs = $this->{NEW} + $this->{NCNT};
+                               $o_ofs -= $this->{POSTCTX};
+                               $n_ofs -= $this->{POSTCTX};
+                               push @split, $this;
+                               redo OUTER;
+                       }
+                       push @{$this->{TEXT}}, $line;
+                       $this->{ADDDEL}++;
+                       if ($line =~ /^-/) {
+                               $this->{OCNT}++;
+                       }
+                       else {
+                               $this->{NCNT}++;
+                       }
+               }
+
+               push @split, $this;
+               last;
+       }
+
+       for my $hunk (@split) {
+               $o_ofs = $hunk->{OLD};
+               $n_ofs = $hunk->{NEW};
+               my $o_cnt = $hunk->{OCNT};
+               my $n_cnt = $hunk->{NCNT};
+
+               my $head = ("@@ -$o_ofs" .
+                           (($o_cnt != 1) ? ",$o_cnt" : '') .
+                           " +$n_ofs" .
+                           (($n_cnt != 1) ? ",$n_cnt" : '') .
+                           " @@\n");
+               unshift @{$hunk->{TEXT}}, $head;
+       }
+       return map { $_->{TEXT} } @split;
+}
+
+sub find_last_o_ctx {
+       my ($it) = @_;
+       my $text = $it->{TEXT};
+       my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
+       my $i = @{$text};
+       my $last_o_ctx = $o_ofs + $o_cnt;
+       while (0 < --$i) {
+               my $line = $text->[$i];
+               if ($line =~ /^ /) {
+                       $last_o_ctx--;
+                       next;
+               }
+               last;
+       }
+       return $last_o_ctx;
+}
+
+sub merge_hunk {
+       my ($prev, $this) = @_;
+       my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
+           parse_hunk_header($prev->{TEXT}[0]);
+       my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
+           parse_hunk_header($this->{TEXT}[0]);
+
+       my (@line, $i, $ofs, $o_cnt, $n_cnt);
+       $ofs = $o0_ofs;
+       $o_cnt = $n_cnt = 0;
+       for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
+               my $line = $prev->{TEXT}[$i];
+               if ($line =~ /^\+/) {
+                       $n_cnt++;
+                       push @line, $line;
+                       next;
+               }
+
+               last if ($o1_ofs <= $ofs);
+
+               $o_cnt++;
+               $ofs++;
+               if ($line =~ /^ /) {
+                       $n_cnt++;
+               }
+               push @line, $line;
+       }
+
+       for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
+               my $line = $this->{TEXT}[$i];
+               if ($line =~ /^\+/) {
+                       $n_cnt++;
+                       push @line, $line;
+                       next;
+               }
+               $ofs++;
+               $o_cnt++;
+               if ($line =~ /^ /) {
+                       $n_cnt++;
+               }
+               push @line, $line;
+       }
+       my $head = ("@@ -$o0_ofs" .
+                   (($o_cnt != 1) ? ",$o_cnt" : '') .
+                   " +$n0_ofs" .
+                   (($n_cnt != 1) ? ",$n_cnt" : '') .
+                   " @@\n");
+       @{$prev->{TEXT}} = ($head, @line);
+}
+
+sub coalesce_overlapping_hunks {
+       my (@in) = @_;
+       my @out = ();
+
+       my ($last_o_ctx);
+
+       for (grep { $_->{USE} } @in) {
+               my $text = $_->{TEXT};
+               my ($o_ofs) = parse_hunk_header($text->[0]);
+               if (defined $last_o_ctx &&
+                   $o_ofs <= $last_o_ctx) {
+                       merge_hunk($out[-1], $_);
+               }
+               else {
+                       push @out, $_;
+               }
+               $last_o_ctx = find_last_o_ctx($out[-1]);
+       }
+       return @out;
+}
+
+sub help_patch_cmd {
+       print colored $help_color, <<\EOF ;
+y - stage this hunk
+n - do not stage this hunk
+a - stage this and all the remaining hunks in the file
+d - do not stage this hunk nor any of the remaining hunks in the file
+j - leave this hunk undecided, see next undecided hunk
+J - leave this hunk undecided, see next hunk
+k - leave this hunk undecided, see previous undecided hunk
+K - leave this hunk undecided, see previous hunk
+s - split the current hunk into smaller hunks
+? - print help
+EOF
+}
+
+sub patch_update_cmd {
+       my @mods = grep { !($_->{BINARY}) } list_modified('file-only');
+       my @them;
+
+       if (!@mods) {
+               print STDERR "No changes.\n";
+               return 0;
+       }
+       if ($patch_mode) {
+               @them = @mods;
+       }
+       else {
+               @them = list_and_choose({ PROMPT => 'Patch update',
+                                         HEADER => $status_head, },
+                                       @mods);
+       }
+       for (@them) {
+               patch_update_file($_->{VALUE});
+       }
+}
+
+sub patch_update_file {
+       my ($ix, $num);
+       my $path = shift;
+       my ($head, @hunk) = parse_diff($path);
+       print colored_diff_hunk($head->{TEXT});
+       $num = scalar @hunk;
+       $ix = 0;
+
+       while (1) {
+               my ($prev, $next, $other, $undecided, $i);
+               $other = '';
+
+               if ($num <= $ix) {
+                       $ix = 0;
+               }
+               for ($i = 0; $i < $ix; $i++) {
+                       if (!defined $hunk[$i]{USE}) {
+                               $prev = 1;
+                               $other .= '/k';
+                               last;
+                       }
+               }
+               if ($ix) {
+                       $other .= '/K';
+               }
+               for ($i = $ix + 1; $i < $num; $i++) {
+                       if (!defined $hunk[$i]{USE}) {
+                               $next = 1;
+                               $other .= '/j';
+                               last;
+                       }
+               }
+               if ($ix < $num - 1) {
+                       $other .= '/J';
+               }
+               for ($i = 0; $i < $num; $i++) {
+                       if (!defined $hunk[$i]{USE}) {
+                               $undecided = 1;
+                               last;
+                       }
+               }
+               last if (!$undecided);
+
+               if (hunk_splittable($hunk[$ix]{TEXT})) {
+                       $other .= '/s';
+               }
+               print colored_diff_hunk($hunk[$ix]{TEXT});
+               print colored $prompt_color, "Stage this hunk [y/n/a/d$other/?]? ";
+               my $line = <STDIN>;
+               if ($line) {
+                       if ($line =~ /^y/i) {
+                               $hunk[$ix]{USE} = 1;
+                       }
+                       elsif ($line =~ /^n/i) {
+                               $hunk[$ix]{USE} = 0;
+                       }
+                       elsif ($line =~ /^a/i) {
+                               while ($ix < $num) {
+                                       if (!defined $hunk[$ix]{USE}) {
+                                               $hunk[$ix]{USE} = 1;
+                                       }
+                                       $ix++;
+                               }
+                               next;
+                       }
+                       elsif ($line =~ /^d/i) {
+                               while ($ix < $num) {
+                                       if (!defined $hunk[$ix]{USE}) {
+                                               $hunk[$ix]{USE} = 0;
+                                       }
+                                       $ix++;
+                               }
+                               next;
+                       }
+                       elsif ($other =~ /K/ && $line =~ /^K/) {
+                               $ix--;
+                               next;
+                       }
+                       elsif ($other =~ /J/ && $line =~ /^J/) {
+                               $ix++;
+                               next;
+                       }
+                       elsif ($other =~ /k/ && $line =~ /^k/) {
+                               while (1) {
+                                       $ix--;
+                                       last if (!$ix ||
+                                                !defined $hunk[$ix]{USE});
+                               }
+                               next;
+                       }
+                       elsif ($other =~ /j/ && $line =~ /^j/) {
+                               while (1) {
+                                       $ix++;
+                                       last if ($ix >= $num ||
+                                                !defined $hunk[$ix]{USE});
+                               }
+                               next;
+                       }
+                       elsif ($other =~ /s/ && $line =~ /^s/) {
+                               my @split = split_hunk($hunk[$ix]{TEXT});
+                               if (1 < @split) {
+                                       print colored $header_color, "Split into ",
+                                       scalar(@split), " hunks.\n";
+                               }
+                               splice(@hunk, $ix, 1,
+                                      map { +{ TEXT => $_, USE => undef } }
+                                      @split);
+                               $num = scalar @hunk;
+                               next;
+                       }
+                       else {
+                               help_patch_cmd($other);
+                               next;
+                       }
+                       # soft increment
+                       while (1) {
+                               $ix++;
+                               last if ($ix >= $num ||
+                                        !defined $hunk[$ix]{USE});
+                       }
+               }
+       }
+
+       @hunk = coalesce_overlapping_hunks(@hunk);
+
+       my $n_lofs = 0;
+       my @result = ();
+       for (@hunk) {
+               my $text = $_->{TEXT};
+               my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
+                   parse_hunk_header($text->[0]);
+
+               if (!$_->{USE}) {
+                       # We would have added ($n_cnt - $o_cnt) lines
+                       # to the postimage if we were to use this hunk,
+                       # but we didn't.  So the line number that the next
+                       # hunk starts at would be shifted by that much.
+                       $n_lofs -= ($n_cnt - $o_cnt);
+                       next;
+               }
+               else {
+                       if ($n_lofs) {
+                               $n_ofs += $n_lofs;
+                               $text->[0] = ("@@ -$o_ofs" .
+                                             (($o_cnt != 1)
+                                              ? ",$o_cnt" : '') .
+                                             " +$n_ofs" .
+                                             (($n_cnt != 1)
+                                              ? ",$n_cnt" : '') .
+                                             " @@\n");
+                       }
+                       for (@$text) {
+                               push @result, $_;
+                       }
+               }
+       }
+
+       if (@result) {
+               my $fh;
+
+               open $fh, '| git apply --cached';
+               for (@{$head->{TEXT}}, @result) {
+                       print $fh $_;
+               }
+               if (!close $fh) {
+                       for (@{$head->{TEXT}}, @result) {
+                               print STDERR $_;
+                       }
+               }
+               refresh();
+       }
+
+       print "\n";
+}
+
+sub diff_cmd {
+       my @mods = list_modified('index-only');
+       @mods = grep { !($_->{BINARY}) } @mods;
+       return if (!@mods);
+       my (@them) = list_and_choose({ PROMPT => 'Review diff',
+                                    IMMEDIATE => 1,
+                                    HEADER => $status_head, },
+                                  @mods);
+       return if (!@them);
+       system(qw(git diff -p --cached HEAD --), map { $_->{VALUE} } @them);
+}
+
+sub quit_cmd {
+       print "Bye.\n";
+       exit(0);
+}
+
+sub help_cmd {
+       print colored $help_color, <<\EOF ;
+status        - show paths with changes
+update        - add working tree state to the staged set of changes
+revert        - revert staged set of changes back to the HEAD version
+patch         - pick hunks and update selectively
+diff         - view diff between HEAD and index
+add untracked - add contents of untracked files to the staged set of changes
+EOF
+}
+
+sub process_args {
+       return unless @ARGV;
+       my $arg = shift @ARGV;
+       if ($arg eq "--patch") {
+               $patch_mode = 1;
+               $arg = shift @ARGV or die "missing --";
+               die "invalid argument $arg, expecting --"
+                   unless $arg eq "--";
+       }
+       elsif ($arg ne "--") {
+               die "invalid argument $arg, expecting --";
+       }
+}
+
+sub main_loop {
+       my @cmd = ([ 'status', \&status_cmd, ],
+                  [ 'update', \&update_cmd, ],
+                  [ 'revert', \&revert_cmd, ],
+                  [ 'add untracked', \&add_untracked_cmd, ],
+                  [ 'patch', \&patch_update_cmd, ],
+                  [ 'diff', \&diff_cmd, ],
+                  [ 'quit', \&quit_cmd, ],
+                  [ 'help', \&help_cmd, ],
+       );
+       while (1) {
+               my ($it) = list_and_choose({ PROMPT => 'What now',
+                                            SINGLETON => 1,
+                                            LIST_FLAT => 4,
+                                            HEADER => '*** Commands ***',
+                                            ON_EOF => \&quit_cmd,
+                                            IMMEDIATE => 1 }, @cmd);
+               if ($it) {
+                       eval {
+                               $it->[1]->();
+                       };
+                       if ($@) {
+                               print "$@";
+                       }
+               }
+       }
+}
+
+process_args();
+refresh();
+if ($patch_mode) {
+       patch_update_cmd();
+}
+else {
+       status_cmd();
+       main_loop();
+}
diff --git a/git-am.sh b/git-am.sh
new file mode 100755 (executable)
index 0000000..65c634f
--- /dev/null
+++ b/git-am.sh
@@ -0,0 +1,477 @@
+#!/bin/sh
+#
+# Copyright (c) 2005, 2006 Junio C Hamano
+
+OPTIONS_KEEPDASHDASH=
+OPTIONS_SPEC="\
+git-am [options] <mbox>|<Maildir>...
+git-am [options] --resolved
+git-am [options] --skip
+--
+d,dotest=       use <dir> and not .dotest
+i,interactive   run interactively
+b,binary        pass --allo-binary-replacement to git-apply
+3,3way          allow fall back on 3way merging if needed
+s,signoff       add a Signed-off-by line to the commit message
+u,utf8          recode into utf8 (default)
+k,keep          pass -k flagg to git-mailinfo
+whitespace=     pass it through git-apply
+C=              pass it through git-apply
+p=              pass it through git-apply
+resolvemsg=     override error message when patch failure occurs
+r,resolved      to be used after a patch failure
+skip            skip the current patch"
+
+. git-sh-setup
+set_reflog_action am
+require_work_tree
+
+git var GIT_COMMITTER_IDENT >/dev/null || exit
+
+stop_here () {
+    echo "$1" >"$dotest/next"
+    exit 1
+}
+
+stop_here_user_resolve () {
+    if [ -n "$resolvemsg" ]; then
+           printf '%s\n' "$resolvemsg"
+           stop_here $1
+    fi
+    cmdline=$(basename $0)
+    if test '' != "$interactive"
+    then
+        cmdline="$cmdline -i"
+    fi
+    if test '' != "$threeway"
+    then
+        cmdline="$cmdline -3"
+    fi
+    if test '.dotest' != "$dotest"
+    then
+        cmdline="$cmdline -d=$dotest"
+    fi
+    echo "When you have resolved this problem run \"$cmdline --resolved\"."
+    echo "If you would prefer to skip this patch, instead run \"$cmdline --skip\"."
+
+    stop_here $1
+}
+
+go_next () {
+       rm -f "$dotest/$msgnum" "$dotest/msg" "$dotest/msg-clean" \
+               "$dotest/patch" "$dotest/info"
+       echo "$next" >"$dotest/next"
+       this=$next
+}
+
+cannot_fallback () {
+       echo "$1"
+       echo "Cannot fall back to three-way merge."
+       exit 1
+}
+
+fall_back_3way () {
+    O_OBJECT=`cd "$GIT_OBJECT_DIRECTORY" && pwd`
+
+    rm -fr "$dotest"/patch-merge-*
+    mkdir "$dotest/patch-merge-tmp-dir"
+
+    # First see if the patch records the index info that we can use.
+    git apply --build-fake-ancestor "$dotest/patch-merge-tmp-index" \
+       "$dotest/patch" &&
+    GIT_INDEX_FILE="$dotest/patch-merge-tmp-index" \
+    git write-tree >"$dotest/patch-merge-base+" ||
+    cannot_fallback "Repository lacks necessary blobs to fall back on 3-way merge."
+
+    echo Using index info to reconstruct a base tree...
+    if GIT_INDEX_FILE="$dotest/patch-merge-tmp-index" \
+       git apply $binary --cached <"$dotest/patch"
+    then
+       mv "$dotest/patch-merge-base+" "$dotest/patch-merge-base"
+       mv "$dotest/patch-merge-tmp-index" "$dotest/patch-merge-index"
+    else
+        cannot_fallback "Did you hand edit your patch?
+It does not apply to blobs recorded in its index."
+    fi
+
+    test -f "$dotest/patch-merge-index" &&
+    his_tree=$(GIT_INDEX_FILE="$dotest/patch-merge-index" git write-tree) &&
+    orig_tree=$(cat "$dotest/patch-merge-base") &&
+    rm -fr "$dotest"/patch-merge-* || exit 1
+
+    echo Falling back to patching base and 3-way merge...
+
+    # This is not so wrong.  Depending on which base we picked,
+    # orig_tree may be wildly different from ours, but his_tree
+    # has the same set of wildly different changes in parts the
+    # patch did not touch, so recursive ends up canceling them,
+    # saying that we reverted all those changes.
+
+    eval GITHEAD_$his_tree='"$SUBJECT"'
+    export GITHEAD_$his_tree
+    git-merge-recursive $orig_tree -- HEAD $his_tree || {
+           git rerere
+           echo Failed to merge in the changes.
+           exit 1
+    }
+    unset GITHEAD_$his_tree
+}
+
+reread_subject () {
+       git stripspace <"$1" | sed -e 1q
+}
+
+prec=4
+dotest=.dotest sign= utf8=t keep= skip= interactive= resolved= binary=
+resolvemsg= resume=
+git_apply_opt=
+
+while test $# != 0
+do
+       case "$1" in
+       -i|--interactive)
+               interactive=t ;;
+       -b|--binary)
+               binary=t ;;
+       -3|--3way)
+               threeway=t ;;
+       -s|--signoff)
+               sign=t ;;
+       -u|--utf8)
+               utf8=t ;; # this is now default
+       --no-utf8)
+               utf8= ;;
+       -k|--keep)
+               keep=t ;;
+       -r|--resolved)
+               resolved=t ;;
+       --skip)
+               skip=t ;;
+       -d|--dotest)
+               shift; dotest=$1;;
+       --resolvemsg)
+               shift; resolvemsg=$1 ;;
+       --whitespace)
+               git_apply_opt="$git_apply_opt $1=$2"; shift ;;
+       -C|-p)
+               git_apply_opt="$git_apply_opt $1$2"; shift ;;
+       --)
+               shift; break ;;
+       *)
+               usage ;;
+       esac
+       shift
+done
+
+# If the dotest directory exists, but we have finished applying all the
+# patches in them, clear it out.
+if test -d "$dotest" &&
+   last=$(cat "$dotest/last") &&
+   next=$(cat "$dotest/next") &&
+   test $# != 0 &&
+   test "$next" -gt "$last"
+then
+   rm -fr "$dotest"
+fi
+
+if test -d "$dotest"
+then
+       case "$#,$skip$resolved" in
+       0,*t*)
+               # Explicit resume command and we do not have file, so
+               # we are happy.
+               : ;;
+       0,)
+               # No file input but without resume parameters; catch
+               # user error to feed us a patch from standard input
+               # when there is already .dotest.  This is somewhat
+               # unreliable -- stdin could be /dev/null for example
+               # and the caller did not intend to feed us a patch but
+               # wanted to continue unattended.
+               tty -s
+               ;;
+       *)
+               false
+               ;;
+       esac ||
+       die "previous dotest directory $dotest still exists but mbox given."
+       resume=yes
+else
+       # Make sure we are not given --skip nor --resolved
+       test ",$skip,$resolved," = ,,, ||
+               die "Resolve operation not in progress, we are not resuming."
+
+       # Start afresh.
+       mkdir -p "$dotest" || exit
+
+       git mailsplit -d"$prec" -o"$dotest" -b -- "$@" > "$dotest/last" ||  {
+               rm -fr "$dotest"
+               exit 1
+       }
+
+       # -b, -s, -u, -k and --whitespace flags are kept for the
+       # resuming session after a patch failure.
+       # -3 and -i can and must be given when resuming.
+       echo "$binary" >"$dotest/binary"
+       echo " $ws" >"$dotest/whitespace"
+       echo "$sign" >"$dotest/sign"
+       echo "$utf8" >"$dotest/utf8"
+       echo "$keep" >"$dotest/keep"
+       echo 1 >"$dotest/next"
+fi
+
+case "$resolved" in
+'')
+       files=$(git diff-index --cached --name-only HEAD --) || exit
+       if [ "$files" ]; then
+          echo "Dirty index: cannot apply patches (dirty: $files)" >&2
+          exit 1
+       fi
+esac
+
+if test "$(cat "$dotest/binary")" = t
+then
+       binary=--allow-binary-replacement
+fi
+if test "$(cat "$dotest/utf8")" = t
+then
+       utf8=-u
+else
+       utf8=-n
+fi
+if test "$(cat "$dotest/keep")" = t
+then
+       keep=-k
+fi
+ws=`cat "$dotest/whitespace"`
+if test "$(cat "$dotest/sign")" = t
+then
+       SIGNOFF=`git-var GIT_COMMITTER_IDENT | sed -e '
+                       s/>.*/>/
+                       s/^/Signed-off-by: /'
+               `
+else
+       SIGNOFF=
+fi
+
+last=`cat "$dotest/last"`
+this=`cat "$dotest/next"`
+if test "$skip" = t
+then
+       git rerere clear
+       this=`expr "$this" + 1`
+       resume=
+fi
+
+if test "$this" -gt "$last"
+then
+       echo Nothing to do.
+       rm -fr "$dotest"
+       exit
+fi
+
+while test "$this" -le "$last"
+do
+       msgnum=`printf "%0${prec}d" $this`
+       next=`expr "$this" + 1`
+       test -f "$dotest/$msgnum" || {
+               resume=
+               go_next
+               continue
+       }
+
+       # If we are not resuming, parse and extract the patch information
+       # into separate files:
+       #  - info records the authorship and title
+       #  - msg is the rest of commit log message
+       #  - patch is the patch body.
+       #
+       # When we are resuming, these files are either already prepared
+       # by the user, or the user can tell us to do so by --resolved flag.
+       case "$resume" in
+       '')
+               git mailinfo $keep $utf8 "$dotest/msg" "$dotest/patch" \
+                       <"$dotest/$msgnum" >"$dotest/info" ||
+                       stop_here $this
+
+               # skip pine's internal folder data
+               grep '^Author: Mail System Internal Data$' \
+                       <"$dotest"/info >/dev/null &&
+                       go_next && continue
+
+               test -s $dotest/patch || {
+                       echo "Patch is empty.  Was it split wrong?"
+                       stop_here $this
+               }
+               git stripspace < "$dotest/msg" > "$dotest/msg-clean"
+               ;;
+       esac
+
+       GIT_AUTHOR_NAME="$(sed -n '/^Author/ s/Author: //p' "$dotest/info")"
+       GIT_AUTHOR_EMAIL="$(sed -n '/^Email/ s/Email: //p' "$dotest/info")"
+       GIT_AUTHOR_DATE="$(sed -n '/^Date/ s/Date: //p' "$dotest/info")"
+
+       if test -z "$GIT_AUTHOR_EMAIL"
+       then
+               echo "Patch does not have a valid e-mail address."
+               stop_here $this
+       fi
+
+       export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
+
+       SUBJECT="$(sed -n '/^Subject/ s/Subject: //p' "$dotest/info")"
+       case "$keep_subject" in -k)  SUBJECT="[PATCH] $SUBJECT" ;; esac
+
+       case "$resume" in
+       '')
+           if test '' != "$SIGNOFF"
+           then
+               LAST_SIGNED_OFF_BY=`
+                   sed -ne '/^Signed-off-by: /p' \
+                   "$dotest/msg-clean" |
+                   tail -n 1
+               `
+               ADD_SIGNOFF=`
+                   test "$LAST_SIGNED_OFF_BY" = "$SIGNOFF" || {
+                   test '' = "$LAST_SIGNED_OFF_BY" && echo
+                   echo "$SIGNOFF"
+               }`
+           else
+               ADD_SIGNOFF=
+           fi
+           {
+               printf '%s\n' "$SUBJECT"
+               if test -s "$dotest/msg-clean"
+               then
+                       echo
+                       cat "$dotest/msg-clean"
+               fi
+               if test '' != "$ADD_SIGNOFF"
+               then
+                       echo "$ADD_SIGNOFF"
+               fi
+           } >"$dotest/final-commit"
+           ;;
+       *)
+               case "$resolved$interactive" in
+               tt)
+                       # This is used only for interactive view option.
+                       git diff-index -p --cached HEAD -- >"$dotest/patch"
+                       ;;
+               esac
+       esac
+
+       resume=
+       if test "$interactive" = t
+       then
+           test -t 0 ||
+           die "cannot be interactive without stdin connected to a terminal."
+           action=again
+           while test "$action" = again
+           do
+               echo "Commit Body is:"
+               echo "--------------------------"
+               cat "$dotest/final-commit"
+               echo "--------------------------"
+               printf "Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all "
+               read reply
+               case "$reply" in
+               [yY]*) action=yes ;;
+               [aA]*) action=yes interactive= ;;
+               [nN]*) action=skip ;;
+               [eE]*) git_editor "$dotest/final-commit"
+                      SUBJECT=$(reread_subject "$dotest/final-commit")
+                      action=again ;;
+               [vV]*) action=again
+                      LESS=-S ${PAGER:-less} "$dotest/patch" ;;
+               *)     action=again ;;
+               esac
+           done
+       else
+           action=yes
+       fi
+
+       if test $action = skip
+       then
+               go_next
+               continue
+       fi
+
+       if test -x "$GIT_DIR"/hooks/applypatch-msg
+       then
+               "$GIT_DIR"/hooks/applypatch-msg "$dotest/final-commit" ||
+               stop_here $this
+       fi
+
+       printf 'Applying %s\n' "$SUBJECT"
+
+       case "$resolved" in
+       '')
+               git apply $git_apply_opt $binary --index "$dotest/patch"
+               apply_status=$?
+               ;;
+       t)
+               # Resolved means the user did all the hard work, and
+               # we do not have to do any patch application.  Just
+               # trust what the user has in the index file and the
+               # working tree.
+               resolved=
+               git diff-index --quiet --cached HEAD -- && {
+                       echo "No changes - did you forget to use 'git add'?"
+                       stop_here_user_resolve $this
+               }
+               unmerged=$(git ls-files -u)
+               if test -n "$unmerged"
+               then
+                       echo "You still have unmerged paths in your index"
+                       echo "did you forget to use 'git add'?"
+                       stop_here_user_resolve $this
+               fi
+               apply_status=0
+               git rerere
+               ;;
+       esac
+
+       if test $apply_status = 1 && test "$threeway" = t
+       then
+               if (fall_back_3way)
+               then
+                   # Applying the patch to an earlier tree and merging the
+                   # result may have produced the same tree as ours.
+                   git diff-index --quiet --cached HEAD -- && {
+                       echo No changes -- Patch already applied.
+                       go_next
+                       continue
+                   }
+                   # clear apply_status -- we have successfully merged.
+                   apply_status=0
+               fi
+       fi
+       if test $apply_status != 0
+       then
+               echo Patch failed at $msgnum.
+               stop_here_user_resolve $this
+       fi
+
+       if test -x "$GIT_DIR"/hooks/pre-applypatch
+       then
+               "$GIT_DIR"/hooks/pre-applypatch || stop_here $this
+       fi
+
+       tree=$(git write-tree) &&
+       parent=$(git rev-parse --verify HEAD) &&
+       commit=$(git commit-tree $tree -p $parent <"$dotest/final-commit") &&
+       git update-ref -m "$GIT_REFLOG_ACTION: $SUBJECT" HEAD $commit $parent ||
+       stop_here $this
+
+       if test -x "$GIT_DIR"/hooks/post-applypatch
+       then
+               "$GIT_DIR"/hooks/post-applypatch
+       fi
+
+       git gc --auto
+
+       go_next
+done
+
+rm -fr "$dotest"
diff --git a/git-archimport.perl b/git-archimport.perl
new file mode 100755 (executable)
index 0000000..9a7a906
--- /dev/null
@@ -0,0 +1,1133 @@
+#!/usr/bin/perl -w
+#
+# This tool is copyright (c) 2005, Martin Langhoff.
+# It is released under the Gnu Public License, version 2.
+#
+# The basic idea is to walk the output of tla abrowse,
+# fetch the changesets and apply them.
+#
+
+=head1 Invocation
+
+    git-archimport [ -h ] [ -v ] [ -o ] [ -a ] [ -f ] [ -T ]
+       [ -D depth] [ -t tempdir ] <archive>/<branch> [ <archive>/<branch> ]
+
+Imports a project from one or more Arch repositories. It will follow branches
+and repositories within the namespaces defined by the <archive/branch>
+parameters supplied. If it cannot find the remote branch a merge comes from
+it will just import it as a regular commit. If it can find it, it will mark it
+as a merge whenever possible.
+
+See man (1) git-archimport for more details.
+
+=head1 TODO
+
+ - create tag objects instead of ref tags
+ - audit shell-escaping of filenames
+ - hide our private tags somewhere smarter
+ - find a way to make "cat *patches | patch" safe even when patchfiles are missing newlines
+ - sort and apply patches by graphing ancestry relations instead of just
+   relying in dates supplied in the changeset itself.
+   tla ancestry-graph -m could be helpful here...
+
+=head1 Devel tricks
+
+Add print in front of the shell commands invoked via backticks.
+
+=head1 Devel Notes
+
+There are several places where Arch and git terminology are intermixed
+and potentially confused.
+
+The notion of a "branch" in git is approximately equivalent to
+a "archive/category--branch--version" in Arch.  Also, it should be noted
+that the "--branch" portion of "archive/category--branch--version" is really
+optional in Arch although not many people (nor tools!) seem to know this.
+This means that "archive/category--version" is also a valid "branch"
+in git terms.
+
+We always refer to Arch names by their fully qualified variant (which
+means the "archive" name is prefixed.
+
+For people unfamiliar with Arch, an "archive" is the term for "repository",
+and can contain multiple, unrelated branches.
+
+=cut
+
+use strict;
+use warnings;
+use Getopt::Std;
+use File::Temp qw(tempdir);
+use File::Path qw(mkpath rmtree);
+use File::Basename qw(basename dirname);
+use Data::Dumper qw/ Dumper /;
+use IPC::Open2;
+
+$SIG{'PIPE'}="IGNORE";
+$ENV{'TZ'}="UTC";
+
+my $git_dir = $ENV{"GIT_DIR"} || ".git";
+$ENV{"GIT_DIR"} = $git_dir;
+my $ptag_dir = "$git_dir/archimport/tags";
+
+our($opt_h,$opt_f,$opt_v,$opt_T,$opt_t,$opt_D,$opt_a,$opt_o);
+
+sub usage() {
+    print STDERR <<END;
+Usage: ${\basename $0}     # fetch/update GIT from Arch
+       [ -h ] [ -v ] [ -o ] [ -a ] [ -f ] [ -T ] [ -D depth ] [ -t tempdir ]
+       repository/arch-branch [ repository/arch-branch] ...
+END
+    exit(1);
+}
+
+getopts("fThvat:D:") or usage();
+usage if $opt_h;
+
+@ARGV >= 1 or usage();
+# $arch_branches:
+# values associated with keys:
+#   =1 - Arch version / git 'branch' detected via abrowse on a limit
+#   >1 - Arch version / git 'branch' of an auxiliary branch we've merged
+my %arch_branches = map { my $branch = $_; $branch =~ s/:[^:]*$//; $branch => 1 } @ARGV;
+
+# $branch_name_map:
+# maps arch branches to git branch names
+my %branch_name_map = map { m/^(.*):([^:]*)$/; $1 => $2 } grep { m/:/ } @ARGV;
+
+$ENV{'TMPDIR'} = $opt_t if $opt_t; # $ENV{TMPDIR} will affect tempdir() calls:
+my $tmp = tempdir('git-archimport-XXXXXX', TMPDIR => 1, CLEANUP => 1);
+$opt_v && print "+ Using $tmp as temporary directory\n";
+
+unless (-d $git_dir) { # initial import needs empty directory
+    opendir DIR, '.' or die "Unable to open current directory: $!\n";
+    while (my $entry = readdir DIR) {
+        $entry =~ /^\.\.?$/ or
+            die "Initial import needs an empty current working directory.\n"
+    }
+    closedir DIR
+}
+
+my $default_archive;           # default Arch archive
+my %reachable = ();             # Arch repositories we can access
+my %unreachable = ();           # Arch repositories we can't access :<
+my @psets  = ();                # the collection
+my %psets  = ();                # the collection, by name
+my %stats  = (                 # Track which strategy we used to import:
+       get_tag => 0, replay => 0, get_new => 0, get_delta => 0,
+        simple_changeset => 0, import_or_tag => 0
+);
+
+my %rptags = ();                # my reverse private tags
+                                # to map a SHA1 to a commitid
+my $TLA = $ENV{'ARCH_CLIENT'} || 'tla';
+
+sub do_abrowse {
+    my $stage = shift;
+    while (my ($limit, $level) = each %arch_branches) {
+        next unless $level == $stage;
+
+       open ABROWSE, "$TLA abrowse -fkD --merges $limit |"
+                                or die "Problems with tla abrowse: $!";
+
+        my %ps        = ();         # the current one
+        my $lastseen  = '';
+
+        while (<ABROWSE>) {
+            chomp;
+
+            # first record padded w 8 spaces
+            if (s/^\s{8}\b//) {
+                my ($id, $type) = split(m/\s+/, $_, 2);
+
+                my %last_ps;
+                # store the record we just captured
+                if (%ps && !exists $psets{ $ps{id} }) {
+                    %last_ps = %ps; # break references
+                    push (@psets, \%last_ps);
+                    $psets{ $last_ps{id} } = \%last_ps;
+                }
+
+                my $branch = extract_versionname($id);
+                %ps = ( id => $id, branch => $branch );
+                if (%last_ps && ($last_ps{branch} eq $branch)) {
+                    $ps{parent_id} = $last_ps{id};
+                }
+
+                $arch_branches{$branch} = 1;
+                $lastseen = 'id';
+
+                # deal with types (should work with baz or tla):
+                if ($type =~ m/\(.*changeset\)/) {
+                    $ps{type} = 's';
+                } elsif ($type =~ /\(.*import\)/) {
+                    $ps{type} = 'i';
+                } elsif ($type =~ m/\(tag.*?(\S+\@\S+).*?\)/) {
+                    $ps{type} = 't';
+                    # read which revision we've tagged when we parse the log
+                    $ps{tag}  = $1;
+                } else {
+                    warn "Unknown type $type";
+                }
+
+                $arch_branches{$branch} = 1;
+                $lastseen = 'id';
+            } elsif (s/^\s{10}//) {
+                # 10 leading spaces or more
+                # indicate commit metadata
+
+                # date
+                if ($lastseen eq 'id' && m/^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)/){
+                    $ps{date}   = $1;
+                    $lastseen = 'date';
+                } elsif ($_ eq 'merges in:') {
+                    $ps{merges} = [];
+                    $lastseen = 'merges';
+                } elsif ($lastseen eq 'merges' && s/^\s{2}//) {
+                    my $id = $_;
+                    push (@{$ps{merges}}, $id);
+
+                    # aggressive branch finding:
+                    if ($opt_D) {
+                        my $branch = extract_versionname($id);
+                        my $repo = extract_reponame($branch);
+
+                        if (archive_reachable($repo) &&
+                                !defined $arch_branches{$branch}) {
+                            $arch_branches{$branch} = $stage + 1;
+                        }
+                    }
+                } else {
+                    warn "more metadata after merges!?: $_\n" unless /^\s*$/;
+                }
+            }
+        }
+
+        if (%ps && !exists $psets{ $ps{id} }) {
+            my %temp = %ps;         # break references
+            if (@psets && $psets[$#psets]{branch} eq $ps{branch}) {
+                $temp{parent_id} = $psets[$#psets]{id};
+            }
+            push (@psets, \%temp);
+            $psets{ $temp{id} } = \%temp;
+        }
+
+        close ABROWSE or die "$TLA abrowse failed on $limit\n";
+    }
+}                               # end foreach $root
+
+do_abrowse(1);
+my $depth = 2;
+$opt_D ||= 0;
+while ($depth <= $opt_D) {
+    do_abrowse($depth);
+    $depth++;
+}
+
+## Order patches by time
+# FIXME see if we can find a more optimal way to do this by graphing
+# the ancestry data and walking it, that way we won't have to rely on
+# client-supplied dates
+@psets = sort {$a->{date}.$b->{id} cmp $b->{date}.$b->{id}} @psets;
+
+#print Dumper \@psets;
+
+##
+## TODO cleanup irrelevant patches
+##      and put an initial import
+##      or a full tag
+my $import = 0;
+unless (-d $git_dir) { # initial import
+    if ($psets[0]{type} eq 'i' || $psets[0]{type} eq 't') {
+        print "Starting import from $psets[0]{id}\n";
+       `git-init`;
+       die $! if $?;
+       $import = 1;
+    } else {
+        die "Need to start from an import or a tag -- cannot use $psets[0]{id}";
+    }
+} else {    # progressing an import
+    # load the rptags
+    opendir(DIR, $ptag_dir)
+       || die "can't opendir: $!";
+    while (my $file = readdir(DIR)) {
+        # skip non-interesting-files
+        next unless -f "$ptag_dir/$file";
+
+        # convert first '--' to '/' from old git-archimport to use
+        # as an archivename/c--b--v private tag
+        if ($file !~ m!,!) {
+            my $oldfile = $file;
+            $file =~ s!--!,!;
+            print STDERR "converting old tag $oldfile to $file\n";
+            rename("$ptag_dir/$oldfile", "$ptag_dir/$file") or die $!;
+        }
+       my $sha = ptag($file);
+       chomp $sha;
+       $rptags{$sha} = $file;
+    }
+    closedir DIR;
+}
+
+# process patchsets
+# extract the Arch repository name (Arch "archive" in Arch-speak)
+sub extract_reponame {
+    my $fq_cvbr = shift; # archivename/[[[[category]branch]version]revision]
+    return (split(/\//, $fq_cvbr))[0];
+}
+
+sub extract_versionname {
+    my $name = shift;
+    $name =~ s/--(?:patch|version(?:fix)?|base)-\d+$//;
+    return $name;
+}
+
+# convert a fully-qualified revision or version to a unique dirname:
+#   normalperson@yhbt.net-05/mpd--uclinux--1--patch-2
+# becomes: normalperson@yhbt.net-05,mpd--uclinux--1
+#
+# the git notion of a branch is closer to
+# archive/category--branch--version than archive/category--branch, so we
+# use this to convert to git branch names.
+# Also, keep archive names but replace '/' with ',' since it won't require
+# subdirectories, and is safer than swapping '--' which could confuse
+# reverse-mapping when dealing with bastard branches that
+# are just archive/category--version  (no --branch)
+sub tree_dirname {
+    my $revision = shift;
+    my $name = extract_versionname($revision);
+    $name =~ s#/#,#;
+    return $name;
+}
+
+# old versions of git-archimport just use the <category--branch> part:
+sub old_style_branchname {
+    my $id = shift;
+    my $ret = safe_pipe_capture($TLA,'parse-package-name','-p',$id);
+    chomp $ret;
+    return $ret;
+}
+
+*git_default_branchname = $opt_o ? *old_style_branchname : *tree_dirname;
+
+# retrieve default archive, since $branch_name_map keys might not include it
+sub get_default_archive {
+    if (!defined $default_archive) {
+        $default_archive = safe_pipe_capture($TLA,'my-default-archive');
+        chomp $default_archive;
+    }
+    return $default_archive;
+}
+
+sub git_branchname {
+    my $revision = shift;
+    my $name = extract_versionname($revision);
+
+    if (exists $branch_name_map{$name}) {
+       return $branch_name_map{$name};
+
+    } elsif ($name =~ m#^([^/]*)/(.*)$#
+            && $1 eq get_default_archive()
+            && exists $branch_name_map{$2}) {
+       # the names given in the command-line lacked the archive.
+       return $branch_name_map{$2};
+
+    } else {
+       return git_default_branchname($revision);
+    }
+}
+
+sub process_patchset_accurate {
+    my $ps = shift;
+
+    # switch to that branch if we're not already in that branch:
+    if (-e "$git_dir/refs/heads/$ps->{branch}") {
+       system('git-checkout','-f',$ps->{branch}) == 0 or die "$! $?\n";
+
+       # remove any old stuff that got leftover:
+       my $rm = safe_pipe_capture('git-ls-files','--others','-z');
+       rmtree(split(/\0/,$rm)) if $rm;
+    }
+
+    # Apply the import/changeset/merge into the working tree
+    my $dir = sync_to_ps($ps);
+    # read the new log entry:
+    my @commitlog = safe_pipe_capture($TLA,'cat-log','-d',$dir,$ps->{id});
+    die "Error in cat-log: $!" if $?;
+    chomp @commitlog;
+
+    # grab variables we want from the log, new fields get added to $ps:
+    # (author, date, email, summary, message body ...)
+    parselog($ps, \@commitlog);
+
+    if ($ps->{id} =~ /--base-0$/ && $ps->{id} ne $psets[0]{id}) {
+        # this should work when importing continuations
+        if ($ps->{tag} && (my $branchpoint = eval { ptag($ps->{tag}) })) {
+
+            # find where we are supposed to branch from
+           if (! -e "$git_dir/refs/heads/$ps->{branch}") {
+               system('git-branch',$ps->{branch},$branchpoint) == 0 or die "$! $?\n";
+
+               # We trust Arch with the fact that this is just a tag,
+               # and it does not affect the state of the tree, so
+               # we just tag and move on.  If the user really wants us
+               # to consolidate more branches into one, don't tag because
+               # the tag name would be already taken.
+               tag($ps->{id}, $branchpoint);
+               ptag($ps->{id}, $branchpoint);
+               print " * Tagged $ps->{id} at $branchpoint\n";
+           }
+           system('git-checkout','-f',$ps->{branch}) == 0 or die "$! $?\n";
+
+            # remove any old stuff that got leftover:
+            my $rm = safe_pipe_capture('git-ls-files','--others','-z');
+            rmtree(split(/\0/,$rm)) if $rm;
+            return 0;
+        } else {
+            warn "Tagging from unknown id unsupported\n" if $ps->{tag};
+        }
+        # allow multiple bases/imports here since Arch supports cherry-picks
+        # from unrelated trees
+    }
+
+    # update the index with all the changes we got
+    system('git-diff-files --name-only -z | '.
+            'git-update-index --remove -z --stdin') == 0 or die "$! $?\n";
+    system('git-ls-files --others -z | '.
+            'git-update-index --add -z --stdin') == 0 or die "$! $?\n";
+    return 1;
+}
+
+# the native changeset processing strategy.  This is very fast, but
+# does not handle permissions or any renames involving directories
+sub process_patchset_fast {
+    my $ps = shift;
+    #
+    # create the branch if needed
+    #
+    if ($ps->{type} eq 'i' && !$import) {
+        die "Should not have more than one 'Initial import' per GIT import: $ps->{id}";
+    }
+
+    unless ($import) { # skip for import
+        if ( -e "$git_dir/refs/heads/$ps->{branch}") {
+            # we know about this branch
+            system('git-checkout',$ps->{branch});
+        } else {
+            # new branch! we need to verify a few things
+            die "Branch on a non-tag!" unless $ps->{type} eq 't';
+            my $branchpoint = ptag($ps->{tag});
+            die "Tagging from unknown id unsupported: $ps->{tag}"
+                unless $branchpoint;
+
+            # find where we are supposed to branch from
+           if (! -e "$git_dir/refs/heads/$ps->{branch}") {
+               system('git-branch',$ps->{branch},$branchpoint) == 0 or die "$! $?\n";
+
+               # We trust Arch with the fact that this is just a tag,
+               # and it does not affect the state of the tree, so
+               # we just tag and move on.  If the user really wants us
+               # to consolidate more branches into one, don't tag because
+               # the tag name would be already taken.
+               tag($ps->{id}, $branchpoint);
+               ptag($ps->{id}, $branchpoint);
+               print " * Tagged $ps->{id} at $branchpoint\n";
+            }
+            system('git-checkout',$ps->{branch}) == 0 or die "$! $?\n";
+            return 0;
+        }
+        die $! if $?;
+    }
+
+    #
+    # Apply the import/changeset/merge into the working tree
+    #
+    if ($ps->{type} eq 'i' || $ps->{type} eq 't') {
+        apply_import($ps) or die $!;
+        $stats{import_or_tag}++;
+        $import=0;
+    } elsif ($ps->{type} eq 's') {
+        apply_cset($ps);
+        $stats{simple_changeset}++;
+    }
+
+    #
+    # prepare update git's index, based on what arch knows
+    # about the pset, resolve parents, etc
+    #
+
+    my @commitlog = safe_pipe_capture($TLA,'cat-archive-log',$ps->{id});
+    die "Error in cat-archive-log: $!" if $?;
+
+    parselog($ps,\@commitlog);
+
+    # imports don't give us good info
+    # on added files. Shame on them
+    if ($ps->{type} eq 'i' || $ps->{type} eq 't') {
+        system('git-ls-files --deleted -z | '.
+                'git-update-index --remove -z --stdin') == 0 or die "$! $?\n";
+        system('git-ls-files --others -z | '.
+                'git-update-index --add -z --stdin') == 0 or die "$! $?\n";
+    }
+
+    # TODO: handle removed_directories and renamed_directories:
+
+    if (my $del = $ps->{removed_files}) {
+        unlink @$del;
+        while (@$del) {
+            my @slice = splice(@$del, 0, 100);
+            system('git-update-index','--remove','--',@slice) == 0 or
+                            die "Error in git-update-index --remove: $! $?\n";
+        }
+    }
+
+    if (my $ren = $ps->{renamed_files}) {                # renamed
+        if (@$ren % 2) {
+            die "Odd number of entries in rename!?";
+        }
+
+        while (@$ren) {
+            my $from = shift @$ren;
+            my $to   = shift @$ren;
+
+            unless (-d dirname($to)) {
+                mkpath(dirname($to)); # will die on err
+            }
+            # print "moving $from $to";
+            rename($from, $to) or die "Error renaming '$from' '$to': $!\n";
+            system('git-update-index','--remove','--',$from) == 0 or
+                            die "Error in git-update-index --remove: $! $?\n";
+            system('git-update-index','--add','--',$to) == 0 or
+                            die "Error in git-update-index --add: $! $?\n";
+        }
+    }
+
+    if (my $add = $ps->{new_files}) {
+        while (@$add) {
+            my @slice = splice(@$add, 0, 100);
+            system('git-update-index','--add','--',@slice) == 0 or
+                            die "Error in git-update-index --add: $! $?\n";
+        }
+    }
+
+    if (my $mod = $ps->{modified_files}) {
+        while (@$mod) {
+            my @slice = splice(@$mod, 0, 100);
+            system('git-update-index','--',@slice) == 0 or
+                            die "Error in git-update-index: $! $?\n";
+        }
+    }
+    return 1; # we successfully applied the changeset
+}
+
+if ($opt_f) {
+    print "Will import patchsets using the fast strategy\n",
+            "Renamed directories and permission changes will be missed\n";
+    *process_patchset = *process_patchset_fast;
+} else {
+    print "Using the default (accurate) import strategy.\n",
+            "Things may be a bit slow\n";
+    *process_patchset = *process_patchset_accurate;
+}
+
+foreach my $ps (@psets) {
+    # process patchsets
+    $ps->{branch} = git_branchname($ps->{id});
+
+    #
+    # ensure we have a clean state
+    #
+    if (my $dirty = `git-diff-files`) {
+        die "Unclean tree when about to process $ps->{id} " .
+            " - did we fail to commit cleanly before?\n$dirty";
+    }
+    die $! if $?;
+
+    #
+    # skip commits already in repo
+    #
+    if (ptag($ps->{id})) {
+      $opt_v && print " * Skipping already imported: $ps->{id}\n";
+      next;
+    }
+
+    print " * Starting to work on $ps->{id}\n";
+
+    process_patchset($ps) or next;
+
+    # warn "errors when running git-update-index! $!";
+    my $tree = `git-write-tree`;
+    die "cannot write tree $!" if $?;
+    chomp $tree;
+
+    #
+    # Who's your daddy?
+    #
+    my @par;
+    if ( -e "$git_dir/refs/heads/$ps->{branch}") {
+        if (open HEAD, "<","$git_dir/refs/heads/$ps->{branch}") {
+            my $p = <HEAD>;
+            close HEAD;
+            chomp $p;
+            push @par, '-p', $p;
+        } else {
+            if ($ps->{type} eq 's') {
+                warn "Could not find the right head for the branch $ps->{branch}";
+            }
+        }
+    }
+
+    if ($ps->{merges}) {
+        push @par, find_parents($ps);
+    }
+
+    #
+    # Commit, tag and clean state
+    #
+    $ENV{TZ}                  = 'GMT';
+    $ENV{GIT_AUTHOR_NAME}     = $ps->{author};
+    $ENV{GIT_AUTHOR_EMAIL}    = $ps->{email};
+    $ENV{GIT_AUTHOR_DATE}     = $ps->{date};
+    $ENV{GIT_COMMITTER_NAME}  = $ps->{author};
+    $ENV{GIT_COMMITTER_EMAIL} = $ps->{email};
+    $ENV{GIT_COMMITTER_DATE}  = $ps->{date};
+
+    my $pid = open2(*READER, *WRITER,'git-commit-tree',$tree,@par)
+        or die $!;
+    print WRITER $ps->{summary},"\n\n";
+
+    # only print message if it's not empty, to avoid a spurious blank line;
+    # also append an extra newline, so there's a blank line before the
+    # following "git-archimport-id:" line.
+    print WRITER $ps->{message},"\n\n" if ($ps->{message} ne "");
+
+    # make it easy to backtrack and figure out which Arch revision this was:
+    print WRITER 'git-archimport-id: ',$ps->{id},"\n";
+
+    close WRITER;
+    my $commitid = <READER>;    # read
+    chomp $commitid;
+    close READER;
+    waitpid $pid,0;             # close;
+
+    if (length $commitid != 40) {
+        die "Something went wrong with the commit! $! $commitid";
+    }
+    #
+    # Update the branch
+    #
+    open  HEAD, ">","$git_dir/refs/heads/$ps->{branch}";
+    print HEAD $commitid;
+    close HEAD;
+    system('git-update-ref', 'HEAD', "$ps->{branch}");
+
+    # tag accordingly
+    ptag($ps->{id}, $commitid); # private tag
+    if ($opt_T || $ps->{type} eq 't' || $ps->{type} eq 'i') {
+        tag($ps->{id}, $commitid);
+    }
+    print " * Committed $ps->{id}\n";
+    print "   + tree   $tree\n";
+    print "   + commit $commitid\n";
+    $opt_v && print "   + commit date is  $ps->{date} \n";
+    $opt_v && print "   + parents:  ",join(' ',@par),"\n";
+}
+
+if ($opt_v) {
+    foreach (sort keys %stats) {
+        print" $_: $stats{$_}\n";
+    }
+}
+exit 0;
+
+# used by the accurate strategy:
+sub sync_to_ps {
+    my $ps = shift;
+    my $tree_dir = $tmp.'/'.tree_dirname($ps->{id});
+
+    $opt_v && print "sync_to_ps($ps->{id}) method: ";
+
+    if (-d $tree_dir) {
+        if ($ps->{type} eq 't') {
+           $opt_v && print "get (tag)\n";
+            # looks like a tag-only or (worse,) a mixed tags/changeset branch,
+            # can't rely on replay to work correctly on these
+            rmtree($tree_dir);
+            safe_pipe_capture($TLA,'get','--no-pristine',$ps->{id},$tree_dir);
+            $stats{get_tag}++;
+        } else {
+                my $tree_id = arch_tree_id($tree_dir);
+                if ($ps->{parent_id} && ($ps->{parent_id} eq $tree_id)) {
+                    # the common case (hopefully)
+                   $opt_v && print "replay\n";
+                    safe_pipe_capture($TLA,'replay','-d',$tree_dir,$ps->{id});
+                    $stats{replay}++;
+                } else {
+                    # getting one tree is usually faster than getting two trees
+                    # and applying the delta ...
+                    rmtree($tree_dir);
+                   $opt_v && print "apply-delta\n";
+                    safe_pipe_capture($TLA,'get','--no-pristine',
+                                        $ps->{id},$tree_dir);
+                    $stats{get_delta}++;
+                }
+        }
+    } else {
+        # new branch work
+        $opt_v && print "get (new tree)\n";
+        safe_pipe_capture($TLA,'get','--no-pristine',$ps->{id},$tree_dir);
+        $stats{get_new}++;
+    }
+
+    # added -I flag to rsync since we're going to fast! AIEEEEE!!!!
+    system('rsync','-aI','--delete','--exclude',$git_dir,
+#               '--exclude','.arch-inventory',
+                '--exclude','.arch-ids','--exclude','{arch}',
+                '--exclude','+*','--exclude',',*',
+                "$tree_dir/",'./') == 0 or die "Cannot rsync $tree_dir: $! $?";
+    return $tree_dir;
+}
+
+sub apply_import {
+    my $ps = shift;
+    my $bname = git_branchname($ps->{id});
+
+    mkpath($tmp);
+
+    safe_pipe_capture($TLA,'get','-s','--no-pristine',$ps->{id},"$tmp/import");
+    die "Cannot get import: $!" if $?;
+    system('rsync','-aI','--delete', '--exclude',$git_dir,
+               '--exclude','.arch-ids','--exclude','{arch}',
+               "$tmp/import/", './');
+    die "Cannot rsync import:$!" if $?;
+
+    rmtree("$tmp/import");
+    die "Cannot remove tempdir: $!" if $?;
+
+
+    return 1;
+}
+
+sub apply_cset {
+    my $ps = shift;
+
+    mkpath($tmp);
+
+    # get the changeset
+    safe_pipe_capture($TLA,'get-changeset',$ps->{id},"$tmp/changeset");
+    die "Cannot get changeset: $!" if $?;
+
+    # apply patches
+    if (`find $tmp/changeset/patches -type f -name '*.patch'`) {
+        # this can be sped up considerably by doing
+        #    (find | xargs cat) | patch
+        # but that can get mucked up by patches
+        # with missing trailing newlines or the standard
+        # 'missing newline' flag in the patch - possibly
+        # produced with an old/buggy diff.
+        # slow and safe, we invoke patch once per patchfile
+        `find $tmp/changeset/patches -type f -name '*.patch' -print0 | grep -zv '{arch}' | xargs -iFILE -0 --no-run-if-empty patch -p1 --forward -iFILE`;
+        die "Problem applying patches! $!" if $?;
+    }
+
+    # apply changed binary files
+    if (my @modified = `find $tmp/changeset/patches -type f -name '*.modified'`) {
+        foreach my $mod (@modified) {
+            chomp $mod;
+            my $orig = $mod;
+            $orig =~ s/\.modified$//; # lazy
+            $orig =~ s!^\Q$tmp\E/changeset/patches/!!;
+            #print "rsync -p '$mod' '$orig'";
+            system('rsync','-p',$mod,"./$orig");
+            die "Problem applying binary changes! $!" if $?;
+        }
+    }
+
+    # bring in new files
+    system('rsync','-aI','--exclude',$git_dir,
+               '--exclude','.arch-ids',
+               '--exclude', '{arch}',
+               "$tmp/changeset/new-files-archive/",'./');
+
+    # deleted files are hinted from the commitlog processing
+
+    rmtree("$tmp/changeset");
+}
+
+
+# =for reference
+# notes: *-files/-directories keys cannot have spaces, they're always
+# pika-escaped.  Everything after the first newline
+# A log entry looks like:
+# Revision: moodle-org--moodle--1.3.3--patch-15
+# Archive: arch-eduforge@catalyst.net.nz--2004
+# Creator: Penny Leach <penny@catalyst.net.nz>
+# Date: Wed May 25 14:15:34 NZST 2005
+# Standard-date: 2005-05-25 02:15:34 GMT
+# New-files: lang/de/.arch-ids/block_glossary_random.php.id
+#     lang/de/.arch-ids/block_html.php.id
+# New-directories: lang/de/help/questionnaire
+#     lang/de/help/questionnaire/.arch-ids
+# Renamed-files: .arch-ids/db_sears.sql.id db/.arch-ids/db_sears.sql.id
+#    db_sears.sql db/db_sears.sql
+# Removed-files: lang/be/docs/.arch-ids/release.html.id
+#     lang/be/docs/.arch-ids/releaseold.html.id
+# Modified-files: admin/cron.php admin/delete.php
+#     admin/editor.html backup/lib.php backup/restore.php
+# New-patches: arch-eduforge@catalyst.net.nz--2004/moodle-org--moodle--1.3.3--patch-15
+# Summary: Updating to latest from MOODLE_14_STABLE (1.4.5+)
+#   summary can be multiline with a leading space just like the above fields
+# Keywords:
+#
+# Updating yadda tadda tadda madda
+sub parselog {
+    my ($ps, $log) = @_;
+    my $key = undef;
+
+    # headers we want that contain filenames:
+    my %want_headers = (
+        new_files => 1,
+        modified_files => 1,
+        renamed_files => 1,
+        renamed_directories => 1,
+        removed_files => 1,
+        removed_directories => 1,
+    );
+
+    chomp (@$log);
+    while ($_ = shift @$log) {
+        if (/^Continuation-of:\s*(.*)/) {
+            $ps->{tag} = $1;
+            $key = undef;
+        } elsif (/^Summary:\s*(.*)$/ ) {
+            # summary can be multiline as long as it has a leading space.
+           # we squeeze it onto a single line, though.
+            $ps->{summary} = [ $1 ];
+            $key = 'summary';
+        } elsif (/^Creator: (.*)\s*<([^\>]+)>/) {
+            $ps->{author} = $1;
+            $ps->{email} = $2;
+            $key = undef;
+        # any *-files or *-directories can be read here:
+        } elsif (/^([A-Z][a-z\-]+):\s*(.*)$/) {
+            my $val = $2;
+            $key = lc $1;
+            $key =~ tr/-/_/; # too lazy to quote :P
+            if ($want_headers{$key}) {
+                push @{$ps->{$key}}, split(/\s+/, $val);
+            } else {
+                $key = undef;
+            }
+        } elsif (/^$/) {
+            last; # remainder of @$log that didn't get shifted off is message
+        } elsif ($key) {
+            if (/^\s+(.*)$/) {
+                if ($key eq 'summary') {
+                    push @{$ps->{$key}}, $1;
+                } else { # files/directories:
+                    push @{$ps->{$key}}, split(/\s+/, $1);
+                }
+            } else {
+                $key = undef;
+            }
+        }
+    }
+
+    # drop leading empty lines from the log message
+    while (@$log && $log->[0] eq '') {
+       shift @$log;
+    }
+    if (exists $ps->{summary} && @{$ps->{summary}}) {
+       $ps->{summary} = join(' ', @{$ps->{summary}});
+    }
+    elsif (@$log == 0) {
+       $ps->{summary} = 'empty commit message';
+    } else {
+       $ps->{summary} = $log->[0] . '...';
+    }
+    $ps->{message} = join("\n",@$log);
+
+    # skip Arch control files, unescape pika-escaped files
+    foreach my $k (keys %want_headers) {
+        next unless (defined $ps->{$k});
+        my @tmp = ();
+        foreach my $t (@{$ps->{$k}}) {
+           next unless length ($t);
+           next if $t =~ m!\{arch\}/!;
+           next if $t =~ m!\.arch-ids/!;
+           # should we skip this?
+           next if $t =~ m!\.arch-inventory$!;
+           # tla cat-archive-log will give us filenames with spaces as file\(sp)name - why?
+           # we can assume that any filename with \ indicates some pika escaping that we want to get rid of.
+           if ($t =~ /\\/ ){
+               $t = (safe_pipe_capture($TLA,'escape','--unescaped',$t))[0];
+           }
+           push @tmp, $t;
+        }
+        $ps->{$k} = \@tmp;
+    }
+}
+
+# write/read a tag
+sub tag {
+    my ($tag, $commit) = @_;
+
+    if ($opt_o) {
+        $tag =~ s|/|--|g;
+    } else {
+       my $patchname = $tag;
+       $patchname =~ s/.*--//;
+        $tag = git_branchname ($tag) . '--' . $patchname;
+    }
+
+    if ($commit) {
+        open(C,">","$git_dir/refs/tags/$tag")
+            or die "Cannot create tag $tag: $!\n";
+        print C "$commit\n"
+            or die "Cannot write tag $tag: $!\n";
+        close(C)
+            or die "Cannot write tag $tag: $!\n";
+        print " * Created tag '$tag' on '$commit'\n" if $opt_v;
+    } else {                    # read
+        open(C,"<","$git_dir/refs/tags/$tag")
+            or die "Cannot read tag $tag: $!\n";
+        $commit = <C>;
+        chomp $commit;
+        die "Error reading tag $tag: $!\n" unless length $commit == 40;
+        close(C)
+            or die "Cannot read tag $tag: $!\n";
+        return $commit;
+    }
+}
+
+# write/read a private tag
+# reads fail softly if the tag isn't there
+sub ptag {
+    my ($tag, $commit) = @_;
+
+    # don't use subdirs for tags yet, it could screw up other porcelains
+    $tag =~ s|/|,|g;
+
+    my $tag_file = "$ptag_dir/$tag";
+    my $tag_branch_dir = dirname($tag_file);
+    mkpath($tag_branch_dir) unless (-d $tag_branch_dir);
+
+    if ($commit) {              # write
+        open(C,">",$tag_file)
+            or die "Cannot create tag $tag: $!\n";
+        print C "$commit\n"
+            or die "Cannot write tag $tag: $!\n";
+        close(C)
+            or die "Cannot write tag $tag: $!\n";
+       $rptags{$commit} = $tag
+           unless $tag =~ m/--base-0$/;
+    } else {                    # read
+        # if the tag isn't there, return 0
+        unless ( -s $tag_file) {
+            return 0;
+        }
+        open(C,"<",$tag_file)
+            or die "Cannot read tag $tag: $!\n";
+        $commit = <C>;
+        chomp $commit;
+        die "Error reading tag $tag: $!\n" unless length $commit == 40;
+        close(C)
+            or die "Cannot read tag $tag: $!\n";
+       unless (defined $rptags{$commit}) {
+           $rptags{$commit} = $tag;
+       }
+        return $commit;
+    }
+}
+
+sub find_parents {
+    #
+    # Identify what branches are merging into me
+    # and whether we are fully merged
+    # git-merge-base <headsha> <headsha> should tell
+    # me what the base of the merge should be
+    #
+    my $ps = shift;
+
+    my %branches; # holds an arrayref per branch
+                  # the arrayref contains a list of
+                  # merged patches between the base
+                  # of the merge and the current head
+
+    my @parents;  # parents found for this commit
+
+    # simple loop to split the merges
+    # per branch
+    foreach my $merge (@{$ps->{merges}}) {
+       my $branch = git_branchname($merge);
+       unless (defined $branches{$branch} ){
+           $branches{$branch} = [];
+       }
+       push @{$branches{$branch}}, $merge;
+    }
+
+    #
+    # foreach branch find a merge base and walk it to the
+    # head where we are, collecting the merged patchsets that
+    # Arch has recorded. Keep that in @have
+    # Compare that with the commits on the other branch
+    # between merge-base and the tip of the branch (@need)
+    # and see if we have a series of consecutive patches
+    # starting from the merge base. The tip of the series
+    # of consecutive patches merged is our new parent for
+    # that branch.
+    #
+    foreach my $branch (keys %branches) {
+
+       # check that we actually know about the branch
+       next unless -e "$git_dir/refs/heads/$branch";
+
+       my $mergebase = `git-merge-base $branch $ps->{branch}`;
+       if ($?) {
+           # Don't die here, Arch supports one-way cherry-picking
+           # between branches with no common base (or any relationship
+           # at all beforehand)
+           warn "Cannot find merge base for $branch and $ps->{branch}";
+           next;
+       }
+       chomp $mergebase;
+
+       # now walk up to the mergepoint collecting what patches we have
+       my $branchtip = git_rev_parse($ps->{branch});
+       my @ancestors = `git-rev-list --topo-order $branchtip ^$mergebase`;
+       my %have; # collected merges this branch has
+       foreach my $merge (@{$ps->{merges}}) {
+           $have{$merge} = 1;
+       }
+       my %ancestorshave;
+       foreach my $par (@ancestors) {
+           $par = commitid2pset($par);
+           if (defined $par->{merges}) {
+               foreach my $merge (@{$par->{merges}}) {
+                   $ancestorshave{$merge}=1;
+               }
+           }
+       }
+       # print "++++ Merges in $ps->{id} are....\n";
+       # my @have = sort keys %have;   print Dumper(\@have);
+
+       # merge what we have with what ancestors have
+       %have = (%have, %ancestorshave);
+
+       # see what the remote branch has - these are the merges we
+       # will want to have in a consecutive series from the mergebase
+       my $otherbranchtip = git_rev_parse($branch);
+       my @needraw = `git-rev-list --topo-order $otherbranchtip ^$mergebase`;
+       my @need;
+       foreach my $needps (@needraw) {         # get the psets
+           $needps = commitid2pset($needps);
+           # git-rev-list will also
+           # list commits merged in via earlier
+           # merges. we are only interested in commits
+           # from the branch we're looking at
+           if ($branch eq $needps->{branch}) {
+               push @need, $needps->{id};
+           }
+       }
+
+       # print "++++ Merges from $branch we want are....\n";
+       # print Dumper(\@need);
+
+       my $newparent;
+       while (my $needed_commit = pop @need) {
+           if ($have{$needed_commit}) {
+               $newparent = $needed_commit;
+           } else {
+               last; # break out of the while
+           }
+       }
+       if ($newparent) {
+           push @parents, $newparent;
+       }
+
+
+    } # end foreach branch
+
+    # prune redundant parents
+    my %parents;
+    foreach my $p (@parents) {
+       $parents{$p} = 1;
+    }
+    foreach my $p (@parents) {
+       next unless exists $psets{$p}{merges};
+       next unless ref    $psets{$p}{merges};
+       my @merges = @{$psets{$p}{merges}};
+       foreach my $merge (@merges) {
+           if ($parents{$merge}) {
+               delete $parents{$merge};
+           }
+       }
+    }
+
+    @parents = ();
+    foreach (keys %parents) {
+        push @parents, '-p', ptag($_);
+    }
+    return @parents;
+}
+
+sub git_rev_parse {
+    my $name = shift;
+    my $val  = `git-rev-parse $name`;
+    die "Error: git-rev-parse $name" if $?;
+    chomp $val;
+    return $val;
+}
+
+# resolve a SHA1 to a known patchset
+sub commitid2pset {
+    my $commitid = shift;
+    chomp $commitid;
+    my $name = $rptags{$commitid}
+       || die "Cannot find reverse tag mapping for $commitid";
+    $name =~ s|,|/|;
+    my $ps   = $psets{$name}
+       || (print Dumper(sort keys %psets)) && die "Cannot find patchset for $name";
+    return $ps;
+}
+
+
+# an alternative to `command` that allows input to be passed as an array
+# to work around shell problems with weird characters in arguments
+sub safe_pipe_capture {
+    my @output;
+    if (my $pid = open my $child, '-|') {
+        @output = (<$child>);
+        close $child or die join(' ',@_).": $! $?";
+    } else {
+       exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
+    }
+    return wantarray ? @output : join('',@output);
+}
+
+# `tla logs -rf -d <dir> | head -n1` or `baz tree-id <dir>`
+sub arch_tree_id {
+    my $dir = shift;
+    chomp( my $ret = (safe_pipe_capture($TLA,'logs','-rf','-d',$dir))[0] );
+    return $ret;
+}
+
+sub archive_reachable {
+    my $archive = shift;
+    return 1 if $reachable{$archive};
+    return 0 if $unreachable{$archive};
+
+    if (system "$TLA whereis-archive $archive >/dev/null") {
+        if ($opt_a && (system($TLA,'register-archive',
+                      "http://mirrors.sourcecontrol.net/$archive") == 0)) {
+            $reachable{$archive} = 1;
+            return 1;
+        }
+        print STDERR "Archive is unreachable: $archive\n";
+        $unreachable{$archive} = 1;
+        return 0;
+    } else {
+        $reachable{$archive} = 1;
+        return 1;
+    }
+}
diff --git a/git-bisect.sh b/git-bisect.sh
new file mode 100755 (executable)
index 0000000..7a6521e
--- /dev/null
@@ -0,0 +1,465 @@
+#!/bin/sh
+
+USAGE='[start|bad|good|skip|next|reset|visualize|replay|log|run]'
+LONG_USAGE='git bisect start [<bad> [<good>...]] [--] [<pathspec>...]
+        reset bisect state and start bisection.
+git bisect bad [<rev>]
+        mark <rev> a known-bad revision.
+git bisect good [<rev>...]
+        mark <rev>... known-good revisions.
+git bisect skip [<rev>...]
+        mark <rev>... untestable revisions.
+git bisect next
+        find next bisection to test and check it out.
+git bisect reset [<branch>]
+        finish bisection search and go back to branch.
+git bisect visualize
+        show bisect status in gitk.
+git bisect replay <logfile>
+        replay bisection log.
+git bisect log
+        show bisect log.
+git bisect run <cmd>...
+        use <cmd>... to automatically bisect.'
+
+OPTIONS_SPEC=
+. git-sh-setup
+require_work_tree
+
+sq() {
+       @@PERL@@ -e '
+               for (@ARGV) {
+                       s/'\''/'\'\\\\\'\''/g;
+                       print " '\''$_'\''";
+               }
+               print "\n";
+       ' "$@"
+}
+
+bisect_autostart() {
+       test -f "$GIT_DIR/BISECT_NAMES" || {
+               echo >&2 'You need to start by "git bisect start"'
+               if test -t 0
+               then
+                       echo >&2 -n 'Do you want me to do it for you [Y/n]? '
+                       read yesno
+                       case "$yesno" in
+                       [Nn]*)
+                               exit ;;
+                       esac
+                       bisect_start
+               else
+                       exit 1
+               fi
+       }
+}
+
+bisect_start() {
+       #
+       # Verify HEAD. If we were bisecting before this, reset to the
+       # top-of-line master first!
+       #
+       head=$(GIT_DIR="$GIT_DIR" git symbolic-ref HEAD) ||
+       die "Bad HEAD - I need a symbolic ref"
+       case "$head" in
+       refs/heads/bisect)
+               if [ -s "$GIT_DIR/head-name" ]; then
+                   branch=`cat "$GIT_DIR/head-name"`
+               else
+                   branch=master
+               fi
+               git checkout $branch || exit
+               ;;
+       refs/heads/*)
+               [ -s "$GIT_DIR/head-name" ] && die "won't bisect on seeked tree"
+               echo "${head#refs/heads/}" >"$GIT_DIR/head-name"
+               ;;
+       *)
+               die "Bad HEAD - strange symbolic ref"
+               ;;
+       esac
+
+       #
+       # Get rid of any old bisect state
+       #
+       bisect_clean_state
+
+       #
+       # Check for one bad and then some good revisions.
+       #
+       has_double_dash=0
+       for arg; do
+           case "$arg" in --) has_double_dash=1; break ;; esac
+       done
+       orig_args=$(sq "$@")
+       bad_seen=0
+       while [ $# -gt 0 ]; do
+           arg="$1"
+           case "$arg" in
+           --)
+               shift
+               break
+               ;;
+           *)
+               rev=$(git rev-parse --verify "$arg^{commit}" 2>/dev/null) || {
+                   test $has_double_dash -eq 1 &&
+                       die "'$arg' does not appear to be a valid revision"
+                   break
+               }
+               case $bad_seen in
+               0) state='bad' ; bad_seen=1 ;;
+               *) state='good' ;;
+               esac
+               bisect_write "$state" "$rev" 'nolog'
+               shift
+               ;;
+           esac
+       done
+
+       sq "$@" >"$GIT_DIR/BISECT_NAMES"
+       echo "git-bisect start$orig_args" >>"$GIT_DIR/BISECT_LOG"
+       bisect_auto_next
+}
+
+bisect_write() {
+       state="$1"
+       rev="$2"
+       nolog="$3"
+       case "$state" in
+               bad)            tag="$state" ;;
+               good|skip)      tag="$state"-"$rev" ;;
+               *)              die "Bad bisect_write argument: $state" ;;
+       esac
+       git update-ref "refs/bisect/$tag" "$rev"
+       echo "# $state: "$(git show-branch $rev) >>"$GIT_DIR/BISECT_LOG"
+       test -z "$nolog" && echo "git-bisect $state $rev" >>"$GIT_DIR/BISECT_LOG"
+}
+
+bisect_state() {
+       bisect_autostart
+       state=$1
+       case "$#,$state" in
+       0,*)
+               die "Please call 'bisect_state' with at least one argument." ;;
+       1,bad|1,good|1,skip)
+               rev=$(git rev-parse --verify HEAD) ||
+                       die "Bad rev input: HEAD"
+               bisect_write "$state" "$rev" ;;
+       2,bad)
+               rev=$(git rev-parse --verify "$2^{commit}") ||
+                       die "Bad rev input: $2"
+               bisect_write "$state" "$rev" ;;
+       *,good|*,skip)
+               shift
+               revs=$(git rev-parse --revs-only --no-flags "$@") &&
+                       test '' != "$revs" || die "Bad rev input: $@"
+               for rev in $revs
+               do
+                       rev=$(git rev-parse --verify "$rev^{commit}") ||
+                               die "Bad rev commit: $rev^{commit}"
+                       bisect_write "$state" "$rev"
+               done ;;
+       *)
+               usage ;;
+       esac
+       bisect_auto_next
+}
+
+bisect_next_check() {
+       missing_good= missing_bad=
+       git show-ref -q --verify refs/bisect/bad || missing_bad=t
+       test -n "$(git for-each-ref "refs/bisect/good-*")" || missing_good=t
+
+       case "$missing_good,$missing_bad,$1" in
+       ,,*)
+               : have both good and bad - ok
+               ;;
+       *,)
+               # do not have both but not asked to fail - just report.
+               false
+               ;;
+       t,,good)
+               # have bad but not good.  we could bisect although
+               # this is less optimum.
+               echo >&2 'Warning: bisecting only with a bad commit.'
+               if test -t 0
+               then
+                       printf >&2 'Are you sure [Y/n]? '
+                       case "$(read yesno)" in [Nn]*) exit 1 ;; esac
+               fi
+               : bisect without good...
+               ;;
+       *)
+               THEN=''
+               test -f "$GIT_DIR/BISECT_NAMES" || {
+                       echo >&2 'You need to start by "git bisect start".'
+                       THEN='then '
+               }
+               echo >&2 'You '$THEN'need to give me at least one good' \
+                       'and one bad revisions.'
+               echo >&2 '(You can use "git bisect bad" and' \
+                       '"git bisect good" for that.)'
+               exit 1 ;;
+       esac
+}
+
+bisect_auto_next() {
+       bisect_next_check && bisect_next || :
+}
+
+filter_skipped() {
+       _eval="$1"
+       _skip="$2"
+
+       if [ -z "$_skip" ]; then
+               eval $_eval
+               return
+       fi
+
+       # Let's parse the output of:
+       # "git rev-list --bisect-vars --bisect-all ..."
+       eval $_eval | while read hash line
+       do
+               case "$VARS,$FOUND,$TRIED,$hash" in
+                       # We display some vars.
+                       1,*,*,*) echo "$hash $line" ;;
+
+                       # Split line.
+                       ,*,*,---*) ;;
+
+                       # We had nothing to search.
+                       ,,,bisect_rev*)
+                               echo "bisect_rev="
+                               VARS=1
+                               ;;
+
+                       # We did not find a good bisect rev.
+                       # This should happen only if the "bad"
+                       # commit is also a "skip" commit.
+                       ,,*,bisect_rev*)
+                               echo "bisect_rev=$TRIED"
+                               VARS=1
+                               ;;
+
+                       # We are searching.
+                       ,,*,*)
+                               TRIED="${TRIED:+$TRIED|}$hash"
+                               case "$_skip" in
+                               *$hash*) ;;
+                               *)
+                                       echo "bisect_rev=$hash"
+                                       echo "bisect_tried=\"$TRIED\""
+                                       FOUND=1
+                                       ;;
+                               esac
+                               ;;
+
+                       # We have already found a rev to be tested.
+                       ,1,*,bisect_rev*) VARS=1 ;;
+                       ,1,*,*) ;;
+
+                       # ???
+                       *) die "filter_skipped error " \
+                           "VARS: '$VARS' " \
+                           "FOUND: '$FOUND' " \
+                           "TRIED: '$TRIED' " \
+                           "hash: '$hash' " \
+                           "line: '$line'"
+                       ;;
+               esac
+       done
+}
+
+exit_if_skipped_commits () {
+       _tried=$1
+       if expr "$_tried" : ".*[|].*" > /dev/null ; then
+               echo "There are only 'skip'ped commit left to test."
+               echo "The first bad commit could be any of:"
+               echo "$_tried" | tr '[|]' '[\012]'
+               echo "We cannot bisect more!"
+               exit 2
+       fi
+}
+
+bisect_next() {
+       case "$#" in 0) ;; *) usage ;; esac
+       bisect_autostart
+       bisect_next_check good
+
+       skip=$(git for-each-ref --format='%(objectname)' \
+               "refs/bisect/skip-*" | tr '[\012]' ' ') || exit
+
+       BISECT_OPT=''
+       test -n "$skip" && BISECT_OPT='--bisect-all'
+
+       bad=$(git rev-parse --verify refs/bisect/bad) &&
+       good=$(git for-each-ref --format='^%(objectname)' \
+               "refs/bisect/good-*" | tr '[\012]' ' ') &&
+       eval="git rev-list --bisect-vars $BISECT_OPT $good $bad --" &&
+       eval="$eval $(cat "$GIT_DIR/BISECT_NAMES")" &&
+       eval=$(filter_skipped "$eval" "$skip") &&
+       eval "$eval" || exit
+
+       if [ -z "$bisect_rev" ]; then
+               echo "$bad was both good and bad"
+               exit 1
+       fi
+       if [ "$bisect_rev" = "$bad" ]; then
+               exit_if_skipped_commits "$bisect_tried"
+               echo "$bisect_rev is first bad commit"
+               git diff-tree --pretty $bisect_rev
+               exit 0
+       fi
+
+       # We should exit here only if the "bad"
+       # commit is also a "skip" commit (see above).
+       exit_if_skipped_commits "$bisect_rev"
+
+       echo "Bisecting: $bisect_nr revisions left to test after this"
+       git branch -f new-bisect "$bisect_rev"
+       git checkout -q new-bisect || exit
+       git branch -M new-bisect bisect
+       git show-branch "$bisect_rev"
+}
+
+bisect_visualize() {
+       bisect_next_check fail
+       not=$(git for-each-ref --format='%(refname)' "refs/bisect/good-*")
+       eval gitk refs/bisect/bad --not $not -- $(cat "$GIT_DIR/BISECT_NAMES")
+}
+
+bisect_reset() {
+       test -f "$GIT_DIR/BISECT_NAMES" || {
+               echo "We are not bisecting."
+               return
+       }
+       case "$#" in
+       0) if [ -s "$GIT_DIR/head-name" ]; then
+              branch=`cat "$GIT_DIR/head-name"`
+          else
+              branch=master
+          fi ;;
+       1) git show-ref --verify --quiet -- "refs/heads/$1" ||
+              die "$1 does not seem to be a valid branch"
+          branch="$1" ;;
+       *)
+           usage ;;
+       esac
+       if git checkout "$branch"; then
+               rm -f "$GIT_DIR/head-name"
+               bisect_clean_state
+       fi
+}
+
+bisect_clean_state() {
+       # There may be some refs packed during bisection.
+       git for-each-ref --format='%(refname) %(objectname)' refs/bisect/\* refs/heads/bisect |
+       while read ref hash
+       do
+               git update-ref -d $ref $hash
+       done
+       rm -f "$GIT_DIR/BISECT_LOG"
+       rm -f "$GIT_DIR/BISECT_NAMES"
+       rm -f "$GIT_DIR/BISECT_RUN"
+}
+
+bisect_replay () {
+       test -r "$1" || die "cannot read $1 for replaying"
+       bisect_reset
+       while read bisect command rev
+       do
+               test "$bisect" = "git-bisect" || continue
+               case "$command" in
+               start)
+                       cmd="bisect_start $rev"
+                       eval "$cmd" ;;
+               good|bad|skip)
+                       bisect_write "$command" "$rev" ;;
+               *)
+                       die "?? what are you talking about?" ;;
+               esac
+       done <"$1"
+       bisect_auto_next
+}
+
+bisect_run () {
+    bisect_next_check fail
+
+    while true
+    do
+      echo "running $@"
+      "$@"
+      res=$?
+
+      # Check for really bad run error.
+      if [ $res -lt 0 -o $res -ge 128 ]; then
+         echo >&2 "bisect run failed:"
+         echo >&2 "exit code $res from '$@' is < 0 or >= 128"
+         exit $res
+      fi
+
+      # Find current state depending on run success or failure.
+      # A special exit code of 125 means cannot test.
+      if [ $res -eq 125 ]; then
+         state='skip'
+      elif [ $res -gt 0 ]; then
+         state='bad'
+      else
+         state='good'
+      fi
+
+      # We have to use a subshell because "bisect_state" can exit.
+      ( bisect_state $state > "$GIT_DIR/BISECT_RUN" )
+      res=$?
+
+      cat "$GIT_DIR/BISECT_RUN"
+
+      if grep "first bad commit could be any of" "$GIT_DIR/BISECT_RUN" \
+               > /dev/null; then
+         echo >&2 "bisect run cannot continue any more"
+         exit $res
+      fi
+
+      if [ $res -ne 0 ]; then
+         echo >&2 "bisect run failed:"
+         echo >&2 "'bisect_state $state' exited with error code $res"
+         exit $res
+      fi
+
+      if grep "is first bad commit" "$GIT_DIR/BISECT_RUN" > /dev/null; then
+         echo "bisect run success"
+         exit 0;
+      fi
+
+    done
+}
+
+
+case "$#" in
+0)
+    usage ;;
+*)
+    cmd="$1"
+    shift
+    case "$cmd" in
+    start)
+        bisect_start "$@" ;;
+    bad|good|skip)
+        bisect_state "$cmd" "$@" ;;
+    next)
+        # Not sure we want "next" at the UI level anymore.
+        bisect_next "$@" ;;
+    visualize)
+       bisect_visualize "$@" ;;
+    reset)
+        bisect_reset "$@" ;;
+    replay)
+       bisect_replay "$@" ;;
+    log)
+       cat "$GIT_DIR/BISECT_LOG" ;;
+    run)
+        bisect_run "$@" ;;
+    *)
+        usage ;;
+    esac
+esac
diff --git a/git-checkout.sh b/git-checkout.sh
new file mode 100755 (executable)
index 0000000..f6d58ac
--- /dev/null
@@ -0,0 +1,298 @@
+#!/bin/sh
+
+OPTIONS_KEEPDASHDASH=t
+OPTIONS_SPEC="\
+git-branch [options] [<branch>] [<paths>...]
+--
+b=          create a new branch started at <branch>
+l           create the new branchs reflog
+track       tells if the new branch should track the remote branch
+f           proceed even if the index or working tree is not HEAD
+m           performa  three-way merge on local modifications if needed
+q,quiet     be quiet
+"
+SUBDIRECTORY_OK=Sometimes
+. git-sh-setup
+require_work_tree
+
+old_name=HEAD
+old=$(git rev-parse --verify $old_name 2>/dev/null)
+oldbranch=$(git symbolic-ref $old_name 2>/dev/null)
+new=
+new_name=
+force=
+branch=
+track=
+newbranch=
+newbranch_log=
+merge=
+quiet=
+v=-v
+LF='
+'
+
+while test $# != 0; do
+       case "$1" in
+       -b)
+               shift
+               newbranch="$1"
+               [ -z "$newbranch" ] &&
+                       die "git checkout: -b needs a branch name"
+               git show-ref --verify --quiet -- "refs/heads/$newbranch" &&
+                       die "git checkout: branch $newbranch already exists"
+               git check-ref-format "heads/$newbranch" ||
+                       die "git checkout: we do not like '$newbranch' as a branch name."
+               ;;
+       -l)
+               newbranch_log=-l
+               ;;
+       --track|--no-track)
+               track="$1"
+               ;;
+       -f)
+               force=1
+               ;;
+       -m)
+               merge=1
+               ;;
+       -q|--quiet)
+               quiet=1
+               v=
+               ;;
+       --)
+               shift
+               break
+               ;;
+       *)
+               usage
+               ;;
+       esac
+       shift
+done
+
+arg="$1"
+if rev=$(git rev-parse --verify "$arg^0" 2>/dev/null)
+then
+       [ -z "$rev" ] && die "unknown flag $arg"
+       new_name="$arg"
+       if git show-ref --verify --quiet -- "refs/heads/$arg"
+       then
+               rev=$(git rev-parse --verify "refs/heads/$arg^0")
+               branch="$arg"
+       fi
+       new="$rev"
+       shift
+elif rev=$(git rev-parse --verify "$arg^{tree}" 2>/dev/null)
+then
+       # checking out selected paths from a tree-ish.
+       new="$rev"
+       new_name="$arg^{tree}"
+       shift
+fi
+[ "$1" = "--" ] && shift
+
+case "$newbranch,$track" in
+,--*)
+       die "git checkout: --track and --no-track require -b"
+esac
+
+case "$force$merge" in
+11)
+       die "git checkout: -f and -m are incompatible"
+esac
+
+# The behaviour of the command with and without explicit path
+# parameters is quite different.
+#
+# Without paths, we are checking out everything in the work tree,
+# possibly switching branches.  This is the traditional behaviour.
+#
+# With paths, we are _never_ switching branch, but checking out
+# the named paths from either index (when no rev is given),
+# or the named tree-ish (when rev is given).
+
+if test "$#" -ge 1
+then
+       hint=
+       if test "$#" -eq 1
+       then
+               hint="
+Did you intend to checkout '$@' which can not be resolved as commit?"
+       fi
+       if test '' != "$newbranch$force$merge"
+       then
+               die "git checkout: updating paths is incompatible with switching branches/forcing$hint"
+       fi
+       if test '' != "$new"
+       then
+               # from a specific tree-ish; note that this is for
+               # rescuing paths and is never meant to remove what
+               # is not in the named tree-ish.
+               git ls-tree --full-name -r "$new" "$@" |
+               git update-index --index-info || exit $?
+       fi
+
+       # Make sure the request is about existing paths.
+       git ls-files --full-name --error-unmatch -- "$@" >/dev/null || exit
+       git ls-files --full-name -- "$@" |
+               (cd_to_toplevel && git checkout-index -f -u --stdin)
+
+       # Run a post-checkout hook -- the HEAD does not change so the
+       # current HEAD is passed in for both args
+       if test -x "$GIT_DIR"/hooks/post-checkout; then
+           "$GIT_DIR"/hooks/post-checkout $old $old 0
+       fi
+
+       exit $?
+else
+       # Make sure we did not fall back on $arg^{tree} codepath
+       # since we are not checking out from an arbitrary tree-ish,
+       # but switching branches.
+       if test '' != "$new"
+       then
+               git rev-parse --verify "$new^{commit}" >/dev/null 2>&1 ||
+               die "Cannot switch branch to a non-commit."
+       fi
+fi
+
+# We are switching branches and checking out trees, so
+# we *NEED* to be at the toplevel.
+cd_to_toplevel
+
+[ -z "$new" ] && new=$old && new_name="$old_name"
+
+# If we don't have an existing branch that we're switching to,
+# and we don't have a new branch name for the target we
+# are switching to, then we are detaching our HEAD from any
+# branch.  However, if "git checkout HEAD" detaches the HEAD
+# from the current branch, even though that may be logically
+# correct, it feels somewhat funny.  More importantly, we do not
+# want "git checkout" nor "git checkout -f" to detach HEAD.
+
+detached=
+detach_warn=
+
+describe_detached_head () {
+       test -n "$quiet" || {
+               printf >&2 "$1 "
+               GIT_PAGER= git log >&2 -1 --pretty=oneline --abbrev-commit "$2" --
+       }
+}
+
+if test -z "$branch$newbranch" && test "$new_name" != "$old_name"
+then
+       detached="$new"
+       if test -n "$oldbranch" && test -z "$quiet"
+       then
+               detach_warn="Note: moving to \"$new_name\" which isn't a local branch
+If you want to create a new branch from this checkout, you may do so
+(now or later) by using -b with the checkout command again. Example:
+  git checkout -b <new_branch_name>"
+       fi
+elif test -z "$oldbranch" && test "$new" != "$old"
+then
+       describe_detached_head 'Previous HEAD position was' "$old"
+fi
+
+if [ "X$old" = X ]
+then
+       if test -z "$quiet"
+       then
+               echo >&2 "warning: You appear to be on a branch yet to be born."
+               echo >&2 "warning: Forcing checkout of $new_name."
+       fi
+       force=1
+fi
+
+if [ "$force" ]
+then
+    git read-tree $v --reset -u $new
+else
+    git update-index --refresh >/dev/null
+    merge_error=$(git read-tree -m -u --exclude-per-directory=.gitignore $old $new 2>&1) || (
+       case "$merge" in
+       '')
+               echo >&2 "$merge_error"
+               exit 1 ;;
+       esac
+
+       # Match the index to the working tree, and do a three-way.
+       git diff-files --name-only | git update-index --remove --stdin &&
+       work=`git write-tree` &&
+       git read-tree $v --reset -u $new || exit
+
+       eval GITHEAD_$new='${new_name:-${branch:-$new}}' &&
+       eval GITHEAD_$work=local &&
+       export GITHEAD_$new GITHEAD_$work &&
+       git merge-recursive $old -- $new $work
+
+       # Do not register the cleanly merged paths in the index yet.
+       # this is not a real merge before committing, but just carrying
+       # the working tree changes along.
+       unmerged=`git ls-files -u`
+       git read-tree $v --reset $new
+       case "$unmerged" in
+       '')     ;;
+       *)
+               (
+                       z40=0000000000000000000000000000000000000000
+                       echo "$unmerged" |
+                       sed -e 's/^[0-7]* [0-9a-f]* /'"0 $z40 /"
+                       echo "$unmerged"
+               ) | git update-index --index-info
+               ;;
+       esac
+       exit 0
+    )
+    saved_err=$?
+    if test "$saved_err" = 0 && test -z "$quiet"
+    then
+       git diff-index --name-status "$new"
+    fi
+    (exit $saved_err)
+fi
+
+#
+# Switch the HEAD pointer to the new branch if we
+# checked out a branch head, and remove any potential
+# old MERGE_HEAD's (subsequent commits will clearly not
+# be based on them, since we re-set the index)
+#
+if [ "$?" -eq 0 ]; then
+       if [ "$newbranch" ]; then
+               git branch $track $newbranch_log "$newbranch" "$new_name" || exit
+               branch="$newbranch"
+       fi
+       if test -n "$branch"
+       then
+               old_branch_name=`expr "z$oldbranch" : 'zrefs/heads/\(.*\)'`
+               GIT_DIR="$GIT_DIR" git symbolic-ref -m "checkout: moving from ${old_branch_name:-$old} to $branch" HEAD "refs/heads/$branch"
+               if test -n "$quiet"
+               then
+                       true    # nothing
+               elif test "refs/heads/$branch" = "$oldbranch"
+               then
+                       echo >&2 "Already on branch \"$branch\""
+               else
+                       echo >&2 "Switched to${newbranch:+ a new} branch \"$branch\""
+               fi
+       elif test -n "$detached"
+       then
+               old_branch_name=`expr "z$oldbranch" : 'zrefs/heads/\(.*\)'`
+               git update-ref --no-deref -m "checkout: moving from ${old_branch_name:-$old} to $arg" HEAD "$detached" ||
+                       die "Cannot detach HEAD"
+               if test -n "$detach_warn"
+               then
+                       echo >&2 "$detach_warn"
+               fi
+               describe_detached_head 'HEAD is now at' HEAD
+       fi
+       rm -f "$GIT_DIR/MERGE_HEAD"
+else
+       exit 1
+fi
+
+# Run a post-checkout hook
+if test -x "$GIT_DIR"/hooks/post-checkout; then
+       "$GIT_DIR"/hooks/post-checkout $old $new 1
+fi
diff --git a/git-clone.sh b/git-clone.sh
new file mode 100755 (executable)
index 0000000..ecf9d89
--- /dev/null
@@ -0,0 +1,482 @@
+#!/bin/sh
+#
+# Copyright (c) 2005, Linus Torvalds
+# Copyright (c) 2005, Junio C Hamano
+#
+# Clone a repository into a different directory that does not yet exist.
+
+# See git-sh-setup why.
+unset CDPATH
+
+OPTIONS_SPEC="\
+git-clone [options] [--] <repo> [<dir>]
+--
+n,no-checkout        don't create a checkout
+bare                 create a bare repository
+naked                create a bare repository
+l,local              to clone from a local repository
+no-hardlinks         don't use local hardlinks, always copy
+s,shared             setup as a shared repository
+template=            path to the template directory
+q,quiet              be quiet
+reference=           reference repository
+o,origin=            use <name> instead of 'origin' to track upstream
+u,upload-pack=       path to git-upload-pack on the remote
+depth=               create a shallow clone of that depth
+
+use-separate-remote  compatibility, do not use
+no-separate-remote   compatibility, do not use"
+
+die() {
+       echo >&2 "$@"
+       exit 1
+}
+
+usage() {
+       exec "$0" -h
+}
+
+eval "$(echo "$OPTIONS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
+
+get_repo_base() {
+       (
+               cd "`/bin/pwd`" &&
+               cd "$1" || cd "$1.git" &&
+               {
+                       cd .git
+                       pwd
+               }
+       ) 2>/dev/null
+}
+
+if [ -n "$GIT_SSL_NO_VERIFY" -o \
+       "`git config --bool http.sslVerify`" = false ]; then
+    curl_extra_args="-k"
+fi
+
+http_fetch () {
+       # $1 = Remote, $2 = Local
+       curl -nsfL $curl_extra_args "$1" >"$2" ||
+               case $? in
+               126|127) exit ;;
+               *)       return $? ;;
+               esac
+}
+
+clone_dumb_http () {
+       # $1 - remote, $2 - local
+       cd "$2" &&
+       clone_tmp="$GIT_DIR/clone-tmp" &&
+       mkdir -p "$clone_tmp" || exit 1
+       if [ -n "$GIT_CURL_FTP_NO_EPSV" -o \
+               "`git config --bool http.noEPSV`" = true ]; then
+               curl_extra_args="${curl_extra_args} --disable-epsv"
+       fi
+       http_fetch "$1/info/refs" "$clone_tmp/refs" ||
+               die "Cannot get remote repository information.
+Perhaps git-update-server-info needs to be run there?"
+       test "z$quiet" = z && v=-v || v=
+       while read sha1 refname
+       do
+               name=`expr "z$refname" : 'zrefs/\(.*\)'` &&
+               case "$name" in
+               *^*)    continue;;
+               esac
+               case "$bare,$name" in
+               yes,* | ,heads/* | ,tags/*) ;;
+               *)      continue ;;
+               esac
+               if test -n "$use_separate_remote" &&
+                  branch_name=`expr "z$name" : 'zheads/\(.*\)'`
+               then
+                       tname="remotes/$origin/$branch_name"
+               else
+                       tname=$name
+               fi
+               git-http-fetch $v -a -w "$tname" "$sha1" "$1" || exit 1
+       done <"$clone_tmp/refs"
+       rm -fr "$clone_tmp"
+       http_fetch "$1/HEAD" "$GIT_DIR/REMOTE_HEAD" ||
+       rm -f "$GIT_DIR/REMOTE_HEAD"
+       if test -f "$GIT_DIR/REMOTE_HEAD"; then
+               head_sha1=`cat "$GIT_DIR/REMOTE_HEAD"`
+               case "$head_sha1" in
+               'ref: refs/'*)
+                       ;;
+               *)
+                       git-http-fetch $v -a "$head_sha1" "$1" ||
+                       rm -f "$GIT_DIR/REMOTE_HEAD"
+                       ;;
+               esac
+       fi
+}
+
+quiet=
+local=no
+use_local_hardlink=yes
+local_shared=no
+unset template
+no_checkout=
+upload_pack=
+bare=
+reference=
+origin=
+origin_override=
+use_separate_remote=t
+depth=
+no_progress=
+local_explicitly_asked_for=
+test -t 1 || no_progress=--no-progress
+
+while test $# != 0
+do
+       case "$1" in
+       -n|--no-checkout)
+               no_checkout=yes ;;
+       --naked|--bare)
+               bare=yes ;;
+       -l|--local)
+               local_explicitly_asked_for=yes
+               use_local_hardlink=yes
+               ;;
+       --no-hardlinks)
+               use_local_hardlink=no ;;
+       -s|--shared)
+               local_shared=yes ;;
+       --template)
+               shift; template="--template=$1" ;;
+       -q|--quiet)
+               quiet=-q ;;
+       --use-separate-remote|--no-separate-remote)
+               die "clones are always made with separate-remote layout" ;;
+       --reference)
+               shift; reference="$1" ;;
+       -o,--origin)
+               shift;
+               case "$1" in
+               '')
+                   usage ;;
+               */*)
+                   die "'$1' is not suitable for an origin name"
+               esac
+               git check-ref-format "heads/$1" ||
+                   die "'$1' is not suitable for a branch name"
+               test -z "$origin_override" ||
+                   die "Do not give more than one --origin options."
+               origin_override=yes
+               origin="$1"
+               ;;
+       -u|--upload-pack)
+               shift
+               upload_pack="--upload-pack=$1" ;;
+       --depth)
+               shift
+               depth="--depth=$1" ;;
+       --)
+               shift
+               break ;;
+       *)
+               usage ;;
+       esac
+       shift
+done
+
+repo="$1"
+test -n "$repo" ||
+    die 'you must specify a repository to clone.'
+
+# --bare implies --no-checkout and --no-separate-remote
+if test yes = "$bare"
+then
+       if test yes = "$origin_override"
+       then
+               die '--bare and --origin $origin options are incompatible.'
+       fi
+       no_checkout=yes
+       use_separate_remote=
+fi
+
+if test -z "$origin"
+then
+       origin=origin
+fi
+
+# Turn the source into an absolute path if
+# it is local
+if base=$(get_repo_base "$repo"); then
+       repo="$base"
+       local=yes
+fi
+
+dir="$2"
+# Try using "humanish" part of source repo if user didn't specify one
+[ -z "$dir" ] && dir=$(echo "$repo" | sed -e 's|/$||' -e 's|:*/*\.git$||' -e 's|.*[/:]||g')
+[ -e "$dir" ] && die "destination directory '$dir' already exists."
+[ yes = "$bare" ] && unset GIT_WORK_TREE
+[ -n "$GIT_WORK_TREE" ] && [ -e "$GIT_WORK_TREE" ] &&
+die "working tree '$GIT_WORK_TREE' already exists."
+D=
+W=
+cleanup() {
+       err=$?
+       test -z "$D" && rm -rf "$dir"
+       test -z "$W" && test -n "$GIT_WORK_TREE" && rm -rf "$GIT_WORK_TREE"
+       cd ..
+       test -n "$D" && rm -rf "$D"
+       test -n "$W" && rm -rf "$W"
+       exit $err
+}
+trap cleanup 0
+mkdir -p "$dir" && D=$(cd "$dir" && pwd) || usage
+test -n "$GIT_WORK_TREE" && mkdir -p "$GIT_WORK_TREE" &&
+W=$(cd "$GIT_WORK_TREE" && pwd) && GIT_WORK_TREE="$W" && export GIT_WORK_TREE
+if test yes = "$bare" || test -n "$GIT_WORK_TREE"; then
+       GIT_DIR="$D"
+else
+       GIT_DIR="$D/.git"
+fi &&
+export GIT_DIR &&
+GIT_CONFIG="$GIT_DIR/config" git-init $quiet ${template+"$template"} || usage
+
+if test -n "$bare"
+then
+       GIT_CONFIG="$GIT_DIR/config" git config core.bare true
+fi
+
+if test -n "$reference"
+then
+       ref_git=
+       if test -d "$reference"
+       then
+               if test -d "$reference/.git/objects"
+               then
+                       ref_git="$reference/.git"
+               elif test -d "$reference/objects"
+               then
+                       ref_git="$reference"
+               fi
+       fi
+       if test -n "$ref_git"
+       then
+               ref_git=$(cd "$ref_git" && pwd)
+               echo "$ref_git/objects" >"$GIT_DIR/objects/info/alternates"
+               (
+                       GIT_DIR="$ref_git" git for-each-ref \
+                               --format='%(objectname) %(*objectname)'
+               ) |
+               while read a b
+               do
+                       test -z "$a" ||
+                       git update-ref "refs/reference-tmp/$a" "$a"
+                       test -z "$b" ||
+                       git update-ref "refs/reference-tmp/$b" "$b"
+               done
+       else
+               die "reference repository '$reference' is not a local directory."
+       fi
+fi
+
+rm -f "$GIT_DIR/CLONE_HEAD"
+
+# We do local magic only when the user tells us to.
+case "$local" in
+yes)
+       ( cd "$repo/objects" ) ||
+               die "cannot chdir to local '$repo/objects'."
+
+       if test "$local_shared" = yes
+       then
+               mkdir -p "$GIT_DIR/objects/info"
+               echo "$repo/objects" >>"$GIT_DIR/objects/info/alternates"
+       else
+               l= &&
+               if test "$use_local_hardlink" = yes
+               then
+                       # See if we can hardlink and drop "l" if not.
+                       sample_file=$(cd "$repo" && \
+                                     find objects -type f -print | sed -e 1q)
+                       # objects directory should not be empty because
+                       # we are cloning!
+                       test -f "$repo/$sample_file" || exit
+                       if ln "$repo/$sample_file" "$GIT_DIR/objects/sample" 2>/dev/null
+                       then
+                               rm -f "$GIT_DIR/objects/sample"
+                               l=l
+                       elif test -n "$local_explicitly_asked_for"
+                       then
+                               echo >&2 "Warning: -l asked but cannot hardlink to $repo"
+                       fi
+               fi &&
+               cd "$repo" &&
+               find objects -depth -print | cpio -pumd$l "$GIT_DIR/" || exit 1
+       fi
+       git-ls-remote "$repo" >"$GIT_DIR/CLONE_HEAD" || exit 1
+       ;;
+*)
+       case "$repo" in
+       rsync://*)
+               case "$depth" in
+               "") ;;
+               *) die "shallow over rsync not supported" ;;
+               esac
+               rsync $quiet -av --ignore-existing  \
+                       --exclude info "$repo/objects/" "$GIT_DIR/objects/" ||
+               exit
+               # Look at objects/info/alternates for rsync -- http will
+               # support it natively and git native ones will do it on the
+               # remote end.  Not having that file is not a crime.
+               rsync -q "$repo/objects/info/alternates" \
+                       "$GIT_DIR/TMP_ALT" 2>/dev/null ||
+                       rm -f "$GIT_DIR/TMP_ALT"
+               if test -f "$GIT_DIR/TMP_ALT"
+               then
+                   ( cd "$D" &&
+                     . git-parse-remote &&
+                     resolve_alternates "$repo" <"$GIT_DIR/TMP_ALT" ) |
+                   while read alt
+                   do
+                       case "$alt" in 'bad alternate: '*) die "$alt";; esac
+                       case "$quiet" in
+                       '')     echo >&2 "Getting alternate: $alt" ;;
+                       esac
+                       rsync $quiet -av --ignore-existing  \
+                           --exclude info "$alt" "$GIT_DIR/objects" || exit
+                   done
+                   rm -f "$GIT_DIR/TMP_ALT"
+               fi
+               git-ls-remote "$repo" >"$GIT_DIR/CLONE_HEAD" || exit 1
+               ;;
+       https://*|http://*|ftp://*)
+               case "$depth" in
+               "") ;;
+               *) die "shallow over http or ftp not supported" ;;
+               esac
+               if test -z "@@NO_CURL@@"
+               then
+                       clone_dumb_http "$repo" "$D"
+               else
+                       die "http transport not supported, rebuild Git with curl support"
+               fi
+               ;;
+       *)
+               case "$upload_pack" in
+               '') git-fetch-pack --all -k $quiet $depth $no_progress "$repo";;
+               *) git-fetch-pack --all -k $quiet "$upload_pack" $depth $no_progress "$repo" ;;
+               esac >"$GIT_DIR/CLONE_HEAD" ||
+                       die "fetch-pack from '$repo' failed."
+               ;;
+       esac
+       ;;
+esac
+test -d "$GIT_DIR/refs/reference-tmp" && rm -fr "$GIT_DIR/refs/reference-tmp"
+
+if test -f "$GIT_DIR/CLONE_HEAD"
+then
+       # Read git-fetch-pack -k output and store the remote branches.
+       if [ -n "$use_separate_remote" ]
+       then
+               branch_top="remotes/$origin"
+       else
+               branch_top="heads"
+       fi
+       tag_top="tags"
+       while read sha1 name
+       do
+               case "$name" in
+               *'^{}')
+                       continue ;;
+               HEAD)
+                       destname="REMOTE_HEAD" ;;
+               refs/heads/*)
+                       destname="refs/$branch_top/${name#refs/heads/}" ;;
+               refs/tags/*)
+                       destname="refs/$tag_top/${name#refs/tags/}" ;;
+               *)
+                       continue ;;
+               esac
+               git update-ref -m "clone: from $repo" "$destname" "$sha1" ""
+       done < "$GIT_DIR/CLONE_HEAD"
+fi
+
+if test -n "$W"; then
+       cd "$W" || exit
+else
+       cd "$D" || exit
+fi
+
+if test -z "$bare" && test -f "$GIT_DIR/REMOTE_HEAD"
+then
+       # a non-bare repository is always in separate-remote layout
+       remote_top="refs/remotes/$origin"
+       head_sha1=`cat "$GIT_DIR/REMOTE_HEAD"`
+       case "$head_sha1" in
+       'ref: refs/'*)
+               # Uh-oh, the remote told us (http transport done against
+               # new style repository with a symref HEAD).
+               # Ideally we should skip the guesswork but for now
+               # opt for minimum change.
+               head_sha1=`expr "z$head_sha1" : 'zref: refs/heads/\(.*\)'`
+               head_sha1=`cat "$GIT_DIR/$remote_top/$head_sha1"`
+               ;;
+       esac
+
+       # The name under $remote_top the remote HEAD seems to point at.
+       head_points_at=$(
+               (
+                       test -f "$GIT_DIR/$remote_top/master" && echo "master"
+                       cd "$GIT_DIR/$remote_top" &&
+                       find . -type f -print | sed -e 's/^\.\///'
+               ) | (
+               done=f
+               while read name
+               do
+                       test t = $done && continue
+                       branch_tip=`cat "$GIT_DIR/$remote_top/$name"`
+                       if test "$head_sha1" = "$branch_tip"
+                       then
+                               echo "$name"
+                               done=t
+                       fi
+               done
+               )
+       )
+
+       # Upstream URL
+       git config remote."$origin".url "$repo" &&
+
+       # Set up the mappings to track the remote branches.
+       git config remote."$origin".fetch \
+               "+refs/heads/*:$remote_top/*" '^$' &&
+
+       # Write out remote.$origin config, and update our "$head_points_at".
+       case "$head_points_at" in
+       ?*)
+               # Local default branch
+               git symbolic-ref HEAD "refs/heads/$head_points_at" &&
+
+               # Tracking branch for the primary branch at the remote.
+               git update-ref HEAD "$head_sha1" &&
+
+               rm -f "refs/remotes/$origin/HEAD"
+               git symbolic-ref "refs/remotes/$origin/HEAD" \
+                       "refs/remotes/$origin/$head_points_at" &&
+
+               git config branch."$head_points_at".remote "$origin" &&
+               git config branch."$head_points_at".merge "refs/heads/$head_points_at"
+               ;;
+       '')
+               # Source had detached HEAD pointing nowhere
+               git update-ref --no-deref HEAD "$head_sha1" &&
+               rm -f "refs/remotes/$origin/HEAD"
+               ;;
+       esac
+
+       case "$no_checkout" in
+       '')
+               test "z$quiet" = z -a "z$no_progress" = z && v=-v || v=
+               git read-tree -m -u $v HEAD HEAD
+       esac
+fi
+rm -f "$GIT_DIR/CLONE_HEAD" "$GIT_DIR/REMOTE_HEAD"
+
+trap - 0
diff --git a/git-compat-util.h b/git-compat-util.h
new file mode 100644 (file)
index 0000000..79eb10e
--- /dev/null
@@ -0,0 +1,430 @@
+#ifndef GIT_COMPAT_UTIL_H
+#define GIT_COMPAT_UTIL_H
+
+#define _FILE_OFFSET_BITS 64
+
+#ifndef FLEX_ARRAY
+/*
+ * See if our compiler is known to support flexible array members.
+ */
+#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)
+# define FLEX_ARRAY /* empty */
+#elif defined(__GNUC__)
+# if (__GNUC__ >= 3)
+#  define FLEX_ARRAY /* empty */
+# else
+#  define FLEX_ARRAY 0 /* older GNU extension */
+# endif
+#endif
+
+/*
+ * Otherwise, default to safer but a bit wasteful traditional style
+ */
+#ifndef FLEX_ARRAY
+# define FLEX_ARRAY 1
+#endif
+#endif
+
+#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
+
+#ifdef __GNUC__
+#define TYPEOF(x) (__typeof__(x))
+#else
+#define TYPEOF(x)
+#endif
+
+#define MSB(x, bits) ((x) & TYPEOF(x)(~0ULL << (sizeof(x) * 8 - (bits))))
+#define HAS_MULTI_BITS(i)  ((i) & ((i) - 1))  /* checks if an integer has more than 1 bit set */
+
+/* Approximation of the length of the decimal representation of this type. */
+#define decimal_length(x)      ((int)(sizeof(x) * 2.56 + 0.5) + 1)
+
+#if !defined(__APPLE__) && !defined(__FreeBSD__)
+#define _XOPEN_SOURCE 600 /* glibc2 and AIX 5.3L need 500, OpenBSD needs 600 for S_ISLNK() */
+#define _XOPEN_SOURCE_EXTENDED 1 /* AIX 5.3L needs this */
+#endif
+#define _ALL_SOURCE 1
+#define _GNU_SOURCE 1
+#define _BSD_SOURCE 1
+
+#include <unistd.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/param.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <sys/time.h>
+#include <time.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <fnmatch.h>
+#include <sys/poll.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <assert.h>
+#include <regex.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <pwd.h>
+#include <inttypes.h>
+#if defined(__CYGWIN__)
+#undef _XOPEN_SOURCE
+#include <grp.h>
+#define _XOPEN_SOURCE 600
+#else
+#undef _ALL_SOURCE /* AIX 5.3L defines a struct list with _ALL_SOURCE. */
+#include <grp.h>
+#define _ALL_SOURCE 1
+#endif
+
+#ifndef NO_ICONV
+#include <iconv.h>
+#endif
+
+/* On most systems <limits.h> would have given us this, but
+ * not on some systems (e.g. GNU/Hurd).
+ */
+#ifndef PATH_MAX
+#define PATH_MAX 4096
+#endif
+
+#ifndef PRIuMAX
+#define PRIuMAX "llu"
+#endif
+
+#ifdef __GNUC__
+#define NORETURN __attribute__((__noreturn__))
+#else
+#define NORETURN
+#ifndef __attribute__
+#define __attribute__(x)
+#endif
+#endif
+
+/* General helper functions */
+extern void usage(const char *err) NORETURN;
+extern void die(const char *err, ...) NORETURN __attribute__((format (printf, 1, 2)));
+extern int error(const char *err, ...) __attribute__((format (printf, 1, 2)));
+extern void warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
+
+extern void set_usage_routine(void (*routine)(const char *err) NORETURN);
+extern void set_die_routine(void (*routine)(const char *err, va_list params) NORETURN);
+extern void set_error_routine(void (*routine)(const char *err, va_list params));
+extern void set_warn_routine(void (*routine)(const char *warn, va_list params));
+
+#ifdef NO_MMAP
+
+#ifndef PROT_READ
+#define PROT_READ 1
+#define PROT_WRITE 2
+#define MAP_PRIVATE 1
+#define MAP_FAILED ((void*)-1)
+#endif
+
+#define mmap git_mmap
+#define munmap git_munmap
+extern void *git_mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
+extern int git_munmap(void *start, size_t length);
+
+/* This value must be multiple of (pagesize * 2) */
+#define DEFAULT_PACKED_GIT_WINDOW_SIZE (1 * 1024 * 1024)
+
+#else /* NO_MMAP */
+
+#include <sys/mman.h>
+
+/* This value must be multiple of (pagesize * 2) */
+#define DEFAULT_PACKED_GIT_WINDOW_SIZE \
+       (sizeof(void*) >= 8 \
+               ?  1 * 1024 * 1024 * 1024 \
+               : 32 * 1024 * 1024)
+
+#endif /* NO_MMAP */
+
+#define DEFAULT_PACKED_GIT_LIMIT \
+       ((1024L * 1024L) * (sizeof(void*) >= 8 ? 8192 : 256))
+
+#ifdef NO_PREAD
+#define pread git_pread
+extern ssize_t git_pread(int fd, void *buf, size_t count, off_t offset);
+#endif
+
+#ifdef NO_SETENV
+#define setenv gitsetenv
+extern int gitsetenv(const char *, const char *, int);
+#endif
+
+#ifdef NO_MKDTEMP
+#define mkdtemp gitmkdtemp
+extern char *gitmkdtemp(char *);
+#endif
+
+#ifdef NO_UNSETENV
+#define unsetenv gitunsetenv
+extern void gitunsetenv(const char *);
+#endif
+
+#ifdef NO_STRCASESTR
+#define strcasestr gitstrcasestr
+extern char *gitstrcasestr(const char *haystack, const char *needle);
+#endif
+
+#ifdef NO_STRLCPY
+#define strlcpy gitstrlcpy
+extern size_t gitstrlcpy(char *, const char *, size_t);
+#endif
+
+#ifdef NO_STRTOUMAX
+#define strtoumax gitstrtoumax
+extern uintmax_t gitstrtoumax(const char *, char **, int);
+#endif
+
+#ifdef NO_HSTRERROR
+#define hstrerror githstrerror
+extern const char *githstrerror(int herror);
+#endif
+
+#ifdef NO_MEMMEM
+#define memmem gitmemmem
+void *gitmemmem(const void *haystack, size_t haystacklen,
+                const void *needle, size_t needlelen);
+#endif
+
+#ifdef __GLIBC_PREREQ
+#if __GLIBC_PREREQ(2, 1)
+#define HAVE_STRCHRNUL
+#endif
+#endif
+
+#ifndef HAVE_STRCHRNUL
+#define strchrnul gitstrchrnul
+static inline char *gitstrchrnul(const char *s, int c)
+{
+       while (*s && *s != c)
+               s++;
+       return (char *)s;
+}
+#endif
+
+extern void release_pack_memory(size_t, int);
+
+static inline char* xstrdup(const char *str)
+{
+       char *ret = strdup(str);
+       if (!ret) {
+               release_pack_memory(strlen(str) + 1, -1);
+               ret = strdup(str);
+               if (!ret)
+                       die("Out of memory, strdup failed");
+       }
+       return ret;
+}
+
+static inline void *xmalloc(size_t size)
+{
+       void *ret = malloc(size);
+       if (!ret && !size)
+               ret = malloc(1);
+       if (!ret) {
+               release_pack_memory(size, -1);
+               ret = malloc(size);
+               if (!ret && !size)
+                       ret = malloc(1);
+               if (!ret)
+                       die("Out of memory, malloc failed");
+       }
+#ifdef XMALLOC_POISON
+       memset(ret, 0xA5, size);
+#endif
+       return ret;
+}
+
+static inline void *xmemdupz(const void *data, size_t len)
+{
+       char *p = xmalloc(len + 1);
+       memcpy(p, data, len);
+       p[len] = '\0';
+       return p;
+}
+
+static inline char *xstrndup(const char *str, size_t len)
+{
+       char *p = memchr(str, '\0', len);
+       return xmemdupz(str, p ? p - str : len);
+}
+
+static inline void *xrealloc(void *ptr, size_t size)
+{
+       void *ret = realloc(ptr, size);
+       if (!ret && !size)
+               ret = realloc(ptr, 1);
+       if (!ret) {
+               release_pack_memory(size, -1);
+               ret = realloc(ptr, size);
+               if (!ret && !size)
+                       ret = realloc(ptr, 1);
+               if (!ret)
+                       die("Out of memory, realloc failed");
+       }
+       return ret;
+}
+
+static inline void *xcalloc(size_t nmemb, size_t size)
+{
+       void *ret = calloc(nmemb, size);
+       if (!ret && (!nmemb || !size))
+               ret = calloc(1, 1);
+       if (!ret) {
+               release_pack_memory(nmemb * size, -1);
+               ret = calloc(nmemb, size);
+               if (!ret && (!nmemb || !size))
+                       ret = calloc(1, 1);
+               if (!ret)
+                       die("Out of memory, calloc failed");
+       }
+       return ret;
+}
+
+static inline void *xmmap(void *start, size_t length,
+       int prot, int flags, int fd, off_t offset)
+{
+       void *ret = mmap(start, length, prot, flags, fd, offset);
+       if (ret == MAP_FAILED) {
+               if (!length)
+                       return NULL;
+               release_pack_memory(length, fd);
+               ret = mmap(start, length, prot, flags, fd, offset);
+               if (ret == MAP_FAILED)
+                       die("Out of memory? mmap failed: %s", strerror(errno));
+       }
+       return ret;
+}
+
+static inline ssize_t xread(int fd, void *buf, size_t len)
+{
+       ssize_t nr;
+       while (1) {
+               nr = read(fd, buf, len);
+               if ((nr < 0) && (errno == EAGAIN || errno == EINTR))
+                       continue;
+               return nr;
+       }
+}
+
+static inline ssize_t xwrite(int fd, const void *buf, size_t len)
+{
+       ssize_t nr;
+       while (1) {
+               nr = write(fd, buf, len);
+               if ((nr < 0) && (errno == EAGAIN || errno == EINTR))
+                       continue;
+               return nr;
+       }
+}
+
+static inline int xdup(int fd)
+{
+       int ret = dup(fd);
+       if (ret < 0)
+               die("dup failed: %s", strerror(errno));
+       return ret;
+}
+
+static inline FILE *xfdopen(int fd, const char *mode)
+{
+       FILE *stream = fdopen(fd, mode);
+       if (stream == NULL)
+               die("Out of memory? fdopen failed: %s", strerror(errno));
+       return stream;
+}
+
+static inline int xmkstemp(char *template)
+{
+       int fd;
+
+       fd = mkstemp(template);
+       if (fd < 0)
+               die("Unable to create temporary file: %s", strerror(errno));
+       return fd;
+}
+
+static inline size_t xsize_t(off_t len)
+{
+       return (size_t)len;
+}
+
+static inline int has_extension(const char *filename, const char *ext)
+{
+       size_t len = strlen(filename);
+       size_t extlen = strlen(ext);
+       return len > extlen && !memcmp(filename + len - extlen, ext, extlen);
+}
+
+/* Sane ctype - no locale, and works with signed chars */
+#undef isspace
+#undef isdigit
+#undef isalpha
+#undef isalnum
+#undef tolower
+#undef toupper
+extern unsigned char sane_ctype[256];
+#define GIT_SPACE 0x01
+#define GIT_DIGIT 0x02
+#define GIT_ALPHA 0x04
+#define sane_istest(x,mask) ((sane_ctype[(unsigned char)(x)] & (mask)) != 0)
+#define isspace(x) sane_istest(x,GIT_SPACE)
+#define isdigit(x) sane_istest(x,GIT_DIGIT)
+#define isalpha(x) sane_istest(x,GIT_ALPHA)
+#define isalnum(x) sane_istest(x,GIT_ALPHA | GIT_DIGIT)
+#define tolower(x) sane_case((unsigned char)(x), 0x20)
+#define toupper(x) sane_case((unsigned char)(x), 0)
+
+static inline int sane_case(int x, int high)
+{
+       if (sane_istest(x, GIT_ALPHA))
+               x = (x & ~0x20) | high;
+       return x;
+}
+
+static inline int prefixcmp(const char *str, const char *prefix)
+{
+       return strncmp(str, prefix, strlen(prefix));
+}
+
+static inline int strtoul_ui(char const *s, int base, unsigned int *result)
+{
+       unsigned long ul;
+       char *p;
+
+       errno = 0;
+       ul = strtoul(s, &p, base);
+       if (errno || *p || p == s || (unsigned int) ul != ul)
+               return -1;
+       *result = ul;
+       return 0;
+}
+
+static inline int strtol_i(char const *s, int base, int *result)
+{
+       long ul;
+       char *p;
+
+       errno = 0;
+       ul = strtol(s, &p, base);
+       if (errno || *p || p == s || (int) ul != ul)
+               return -1;
+       *result = ul;
+       return 0;
+}
+
+#endif
diff --git a/git-cvsexportcommit.perl b/git-cvsexportcommit.perl
new file mode 100755 (executable)
index 0000000..92e4162
--- /dev/null
@@ -0,0 +1,349 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Getopt::Std;
+use File::Temp qw(tempdir);
+use Data::Dumper;
+use File::Basename qw(basename dirname);
+
+our ($opt_h, $opt_P, $opt_p, $opt_v, $opt_c, $opt_f, $opt_a, $opt_m, $opt_d, $opt_u, $opt_w);
+
+getopts('uhPpvcfam:d:w:');
+
+$opt_h && usage();
+
+die "Need at least one commit identifier!" unless @ARGV;
+
+if ($opt_w) {
+       unless ($ENV{GIT_DIR}) {
+               # Remember where our GIT_DIR is before changing to CVS checkout
+               my $gd =`git-rev-parse --git-dir`;
+               chomp($gd);
+               if ($gd eq '.git') {
+                       my $wd = `pwd`;
+                       chomp($wd);
+                       $gd = $wd."/.git"       ;
+               }
+               $ENV{GIT_DIR} = $gd;
+       }
+
+       if (! -d $opt_w."/CVS" ) {
+               die "$opt_w is not a CVS checkout";
+       }
+       chdir $opt_w or die "Cannot change to CVS checkout at $opt_w";
+}
+unless ($ENV{GIT_DIR} && -r $ENV{GIT_DIR}){
+    die "GIT_DIR is not defined or is unreadable";
+}
+
+
+my @cvs;
+if ($opt_d) {
+       @cvs = ('cvs', '-d', $opt_d);
+} else {
+       @cvs = ('cvs');
+}
+
+# resolve target commit
+my $commit;
+$commit = pop @ARGV;
+$commit = safe_pipe_capture('git-rev-parse', '--verify', "$commit^0");
+chomp $commit;
+if ($?) {
+    die "The commit reference $commit did not resolve!";
+}
+
+# resolve what parent we want
+my $parent;
+if (@ARGV) {
+    $parent = pop @ARGV;
+    $parent =  safe_pipe_capture('git-rev-parse', '--verify', "$parent^0");
+    chomp $parent;
+    if ($?) {
+       die "The parent reference did not resolve!";
+    }
+}
+
+# find parents from the commit itself
+my @commit  = safe_pipe_capture('git-cat-file', 'commit', $commit);
+my @parents;
+my $committer;
+my $author;
+my $stage = 'headers'; # headers, msg
+my $title;
+my $msg = '';
+
+foreach my $line (@commit) {
+    chomp $line;
+    if ($stage eq 'headers' && $line eq '') {
+       $stage = 'msg';
+       next;
+    }
+
+    if ($stage eq 'headers') {
+       if ($line =~ m/^parent (\w{40})$/) { # found a parent
+           push @parents, $1;
+       } elsif ($line =~ m/^author (.+) \d+ [-+]\d+$/) {
+           $author = $1;
+       } elsif ($line =~ m/^committer (.+) \d+ [-+]\d+$/) {
+           $committer = $1;
+       }
+    } else {
+       $msg .= $line . "\n";
+       unless ($title) {
+           $title = $line;
+       }
+    }
+}
+
+my $noparent = "0000000000000000000000000000000000000000";
+if ($parent) {
+    my $found;
+    # double check that it's a valid parent
+    foreach my $p (@parents) {
+       if ($p eq $parent) {
+           $found = 1;
+           last;
+       }; # found it
+    }
+    die "Did not find $parent in the parents for this commit!" if !$found and !$opt_P;
+} else { # we don't have a parent from the cmdline...
+    if (@parents == 1) { # it's safe to get it from the commit
+       $parent = $parents[0];
+    } elsif (@parents == 0) { # there is no parent
+        $parent = $noparent;
+    } else { # cannot choose automatically from multiple parents
+        die "This commit has more than one parent -- please name the parent you want to use explicitly";
+    }
+}
+
+$opt_v && print "Applying to CVS commit $commit from parent $parent\n";
+
+# grab the commit message
+open(MSG, ">.msg") or die "Cannot open .msg for writing";
+if ($opt_m) {
+    print MSG $opt_m;
+}
+print MSG $msg;
+if ($opt_a) {
+    print MSG "\n\nAuthor: $author\n";
+    if ($author ne $committer) {
+       print MSG "Committer: $committer\n";
+    }
+}
+close MSG;
+
+if ($parent eq $noparent) {
+    `git-diff-tree --binary -p --root $commit >.cvsexportcommit.diff`;# || die "Cannot diff";
+} else {
+    `git-diff-tree --binary -p $parent $commit >.cvsexportcommit.diff`;# || die "Cannot diff";
+}
+
+## apply non-binary changes
+
+# In pedantic mode require all lines of context to match.  In normal
+# mode, be compatible with diff/patch: assume 3 lines of context and
+# require at least one line match, i.e. ignore at most 2 lines of
+# context, like diff/patch do by default.
+my $context = $opt_p ? '' : '-C1';
+
+print "Checking if patch will apply\n";
+
+my @stat;
+open APPLY, "GIT_DIR= git-apply $context --summary --numstat<.cvsexportcommit.diff|" || die "cannot patch";
+@stat=<APPLY>;
+close APPLY || die "Cannot patch";
+my (@bfiles,@files,@afiles,@dfiles);
+chomp @stat;
+foreach (@stat) {
+       push (@bfiles,$1) if m/^-\t-\t(.*)$/;
+       push (@files, $1) if m/^-\t-\t(.*)$/;
+       push (@files, $1) if m/^\d+\t\d+\t(.*)$/;
+       push (@afiles,$1) if m/^ create mode [0-7]+ (.*)$/;
+       push (@dfiles,$1) if m/^ delete mode [0-7]+ (.*)$/;
+}
+map { s/^"(.*)"$/$1/g } @bfiles,@files;
+map { s/\\([0-7]{3})/sprintf('%c',oct $1)/eg } @bfiles,@files;
+
+# check that the files are clean and up to date according to cvs
+my $dirty;
+my @dirs;
+foreach my $p (@afiles) {
+    my $path = dirname $p;
+    while (!-d $path and ! grep { $_ eq $path } @dirs) {
+       unshift @dirs, $path;
+       $path = dirname $path;
+    }
+}
+
+# ... check dirs,
+foreach my $d (@dirs) {
+    if (-e $d) {
+       $dirty = 1;
+       warn "$d exists and is not a directory!\n";
+    }
+}
+
+# ... query status of all files that we have a directory for and parse output of 'cvs status' to %cvsstat.
+my @canstatusfiles;
+foreach my $f (@files) {
+    my $path = dirname $f;
+    next if (grep { $_ eq $path } @dirs);
+    push @canstatusfiles, $f;
+}
+
+my %cvsstat;
+if (@canstatusfiles) {
+    if ($opt_u) {
+      my @updated = safe_pipe_capture(@cvs, 'update', @canstatusfiles);
+      print @updated;
+    }
+    my @cvsoutput;
+    @cvsoutput= safe_pipe_capture(@cvs, 'status', @canstatusfiles);
+    my $matchcount = 0;
+    foreach my $l (@cvsoutput) {
+        chomp $l;
+        if ( $l =~ /^File:/ and  $l =~ /Status: (.*)$/ ) {
+            $cvsstat{$canstatusfiles[$matchcount]} = $1;
+            $matchcount++;
+        }
+    }
+}
+
+# ... validate new files,
+foreach my $f (@afiles) {
+    if (defined ($cvsstat{$f}) and $cvsstat{$f} ne "Unknown") {
+       $dirty = 1;
+       warn "File $f is already known in your CVS checkout -- perhaps it has been added by another user. Or this may indicate that it exists on a different branch. If this is the case, use -f to force the merge.\n";
+       warn "Status was: $cvsstat{$f}\n";
+    }
+}
+# ... validate known files.
+foreach my $f (@files) {
+    next if grep { $_ eq $f } @afiles;
+    # TODO:we need to handle removed in cvs
+    unless (defined ($cvsstat{$f}) and $cvsstat{$f} eq "Up-to-date") {
+       $dirty = 1;
+       warn "File $f not up to date but has status '$cvsstat{$f}' in your CVS checkout!\n";
+    }
+}
+if ($dirty) {
+    if ($opt_f) {      warn "The tree is not clean -- forced merge\n";
+       $dirty = 0;
+    } else {
+       die "Exiting: your CVS tree is not clean for this merge.";
+    }
+}
+
+print "Applying\n";
+`GIT_DIR= git-apply $context --summary --numstat --apply <.cvsexportcommit.diff` || die "cannot patch";
+
+print "Patch applied successfully. Adding new files and directories to CVS\n";
+my $dirtypatch = 0;
+
+#
+# We have to add the directories in order otherwise we will have
+# problems when we try and add the sub-directory of a directory we
+# have not added yet.
+#
+# Luckily this is easy to deal with by sorting the directories and
+# dealing with the shortest ones first.
+#
+@dirs = sort { length $a <=> length $b} @dirs;
+
+foreach my $d (@dirs) {
+    if (system(@cvs,'add',$d)) {
+       $dirtypatch = 1;
+       warn "Failed to cvs add directory $d -- you may need to do it manually";
+    }
+}
+
+foreach my $f (@afiles) {
+    if (grep { $_ eq $f } @bfiles) {
+      system(@cvs, 'add','-kb',$f);
+    } else {
+      system(@cvs, 'add', $f);
+    }
+    if ($?) {
+       $dirtypatch = 1;
+       warn "Failed to cvs add $f -- you may need to do it manually";
+    }
+}
+
+foreach my $f (@dfiles) {
+    system(@cvs, 'rm', '-f', $f);
+    if ($?) {
+       $dirtypatch = 1;
+       warn "Failed to cvs rm -f $f -- you may need to do it manually";
+    }
+}
+
+print "Commit to CVS\n";
+print "Patch title (first comment line): $title\n";
+my @commitfiles = map { unless (m/\s/) { '\''.$_.'\''; } else { $_; }; } (@files);
+my $cmd = join(' ', @cvs)." commit -F .msg @commitfiles";
+
+if ($dirtypatch) {
+    print "NOTE: One or more hunks failed to apply cleanly.\n";
+    print "You'll need to apply the patch in .cvsexportcommit.diff manually\n";
+    print "using a patch program. After applying the patch and resolving the\n";
+    print "problems you may commit using:";
+    print "\n    cd \"$opt_w\"" if $opt_w;
+    print "\n    $cmd\n\n";
+    exit(1);
+}
+
+if ($opt_c) {
+    print "Autocommit\n  $cmd\n";
+    print safe_pipe_capture(@cvs, 'commit', '-F', '.msg', @files);
+    if ($?) {
+       die "Exiting: The commit did not succeed";
+    }
+    print "Committed successfully to CVS\n";
+    # clean up
+    unlink(".msg");
+} else {
+    print "Ready for you to commit, just run:\n\n   $cmd\n";
+}
+
+# clean up
+unlink(".cvsexportcommit.diff");
+
+# CVS version 1.11.x and 1.12.x sleeps the wrong way to ensure the timestamp
+# used by CVS and the one set by subsequence file modifications are different.
+# If they are not different CVS will not detect changes.
+sleep(1);
+
+sub usage {
+       print STDERR <<END;
+Usage: GIT_DIR=/path/to/.git ${\basename $0} [-h] [-p] [-v] [-c] [-f] [-u] [-w cvsworkdir] [-m msgprefix] [ parent ] commit
+END
+       exit(1);
+}
+
+# An alternative to `command` that allows input to be passed as an array
+# to work around shell problems with weird characters in arguments
+# if the exec returns non-zero we die
+sub safe_pipe_capture {
+    my @output;
+    if (my $pid = open my $child, '-|') {
+       @output = (<$child>);
+       close $child or die join(' ',@_).": $! $?";
+    } else {
+       exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
+    }
+    return wantarray ? @output : join('',@output);
+}
+
+sub safe_pipe_capture_blob {
+    my $output;
+    if (my $pid = open my $child, '-|') {
+        local $/;
+       undef $/;
+       $output = (<$child>);
+       close $child or die join(' ',@_).": $! $?";
+    } else {
+       exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
+    }
+    return $output;
+}
diff --git a/git-cvsimport.perl b/git-cvsimport.perl
new file mode 100755 (executable)
index 0000000..92648f4
--- /dev/null
@@ -0,0 +1,1008 @@
+#!/usr/bin/perl -w
+
+# This tool is copyright (c) 2005, Matthias Urlichs.
+# It is released under the Gnu Public License, version 2.
+#
+# The basic idea is to aggregate CVS check-ins into related changes.
+# Fortunately, "cvsps" does that for us; all we have to do is to parse
+# its output.
+#
+# Checking out the files is done by a single long-running CVS connection
+# / server process.
+#
+# The head revision is on branch "origin" by default.
+# You can change that with the '-o' option.
+
+use strict;
+use warnings;
+use Getopt::Std;
+use File::Spec;
+use File::Temp qw(tempfile tmpnam);
+use File::Path qw(mkpath);
+use File::Basename qw(basename dirname);
+use Time::Local;
+use IO::Socket;
+use IO::Pipe;
+use POSIX qw(strftime dup2 ENOENT);
+use IPC::Open2;
+
+$SIG{'PIPE'}="IGNORE";
+$ENV{'TZ'}="UTC";
+
+our ($opt_h,$opt_o,$opt_v,$opt_k,$opt_u,$opt_d,$opt_p,$opt_C,$opt_z,$opt_i,$opt_P, $opt_s,$opt_m,$opt_M,$opt_A,$opt_S,$opt_L, $opt_a, $opt_r);
+my (%conv_author_name, %conv_author_email);
+
+sub usage(;$) {
+       my $msg = shift;
+       print(STDERR "Error: $msg\n") if $msg;
+       print STDERR <<END;
+Usage: ${\basename $0}     # fetch/update GIT from CVS
+       [-o branch-for-HEAD] [-h] [-v] [-d CVSROOT] [-A author-conv-file]
+       [-p opts-for-cvsps] [-P file] [-C GIT_repository] [-z fuzz] [-i] [-k]
+       [-u] [-s subst] [-a] [-m] [-M regex] [-S regex] [-L commitlimit]
+       [-r remote] [CVS_module]
+END
+       exit(1);
+}
+
+sub read_author_info($) {
+       my ($file) = @_;
+       my $user;
+       open my $f, '<', "$file" or die("Failed to open $file: $!\n");
+
+       while (<$f>) {
+               # Expected format is this:
+               #   exon=Andreas Ericsson <ae@op5.se>
+               if (m/^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/) {
+                       $user = $1;
+                       $conv_author_name{$user} = $2;
+                       $conv_author_email{$user} = $3;
+               }
+               # However, we also read from CVSROOT/users format
+               # to ease migration.
+               elsif (/^(\w+):(['"]?)(.+?)\2\s*$/) {
+                       my $mapped;
+                       ($user, $mapped) = ($1, $3);
+                       if ($mapped =~ /^\s*(.*?)\s*<(.*)>\s*$/) {
+                               $conv_author_name{$user} = $1;
+                               $conv_author_email{$user} = $2;
+                       }
+                       elsif ($mapped =~ /^<?(.*)>?$/) {
+                               $conv_author_name{$user} = $user;
+                               $conv_author_email{$user} = $1;
+                       }
+               }
+               # NEEDSWORK: Maybe warn on unrecognized lines?
+       }
+       close ($f);
+}
+
+sub write_author_info($) {
+       my ($file) = @_;
+       open my $f, '>', $file or
+         die("Failed to open $file for writing: $!");
+
+       foreach (keys %conv_author_name) {
+               print $f "$_=$conv_author_name{$_} <$conv_author_email{$_}>\n";
+       }
+       close ($f);
+}
+
+# convert getopts specs for use by git-repo-config
+sub read_repo_config {
+    # Split the string between characters, unless there is a ':'
+    # So "abc:de" becomes ["a", "b", "c:", "d", "e"]
+       my @opts = split(/ *(?!:)/, shift);
+       foreach my $o (@opts) {
+               my $key = $o;
+               $key =~ s/://g;
+               my $arg = 'git-repo-config';
+               $arg .= ' --bool' if ($o !~ /:$/);
+
+        chomp(my $tmp = `$arg --get cvsimport.$key`);
+               if ($tmp && !($arg =~ /--bool/ && $tmp eq 'false')) {
+            no strict 'refs';
+            my $opt_name = "opt_" . $key;
+            if (!$$opt_name) {
+                $$opt_name = $tmp;
+            }
+               }
+       }
+}
+
+my $opts = "haivmkuo:d:p:r:C:z:s:M:P:A:S:L:";
+read_repo_config($opts);
+getopts($opts) or usage();
+usage if $opt_h;
+
+if (@ARGV == 0) {
+               chomp(my $module = `git-repo-config --get cvsimport.module`);
+               push(@ARGV, $module) if $? == 0;
+}
+@ARGV <= 1 or usage("You can't specify more than one CVS module");
+
+if ($opt_d) {
+       $ENV{"CVSROOT"} = $opt_d;
+} elsif (-f 'CVS/Root') {
+       open my $f, '<', 'CVS/Root' or die 'Failed to open CVS/Root';
+       $opt_d = <$f>;
+       chomp $opt_d;
+       close $f;
+       $ENV{"CVSROOT"} = $opt_d;
+} elsif ($ENV{"CVSROOT"}) {
+       $opt_d = $ENV{"CVSROOT"};
+} else {
+       usage("CVSROOT needs to be set");
+}
+$opt_s ||= "-";
+$opt_a ||= 0;
+
+my $git_tree = $opt_C;
+$git_tree ||= ".";
+
+my $remote;
+if (defined $opt_r) {
+       $remote = 'refs/remotes/' . $opt_r;
+       $opt_o ||= "master";
+} else {
+       $opt_o ||= "origin";
+       $remote = 'refs/heads';
+}
+
+my $cvs_tree;
+if ($#ARGV == 0) {
+       $cvs_tree = $ARGV[0];
+} elsif (-f 'CVS/Repository') {
+       open my $f, '<', 'CVS/Repository' or
+           die 'Failed to open CVS/Repository';
+       $cvs_tree = <$f>;
+       chomp $cvs_tree;
+       close $f;
+} else {
+       usage("CVS module has to be specified");
+}
+
+our @mergerx = ();
+if ($opt_m) {
+       @mergerx = ( qr/\W(?:from|of|merge|merging|merged) (\w+)/i );
+}
+if ($opt_M) {
+       push (@mergerx, qr/$opt_M/);
+}
+
+# Remember UTC of our starting time
+# we'll want to avoid importing commits
+# that are too recent
+our $starttime = time();
+
+select(STDERR); $|=1; select(STDOUT);
+
+
+package CVSconn;
+# Basic CVS dialog.
+# We're only interested in connecting and downloading, so ...
+
+use File::Spec;
+use File::Temp qw(tempfile);
+use POSIX qw(strftime dup2);
+
+sub new {
+       my ($what,$repo,$subdir) = @_;
+       $what=ref($what) if ref($what);
+
+       my $self = {};
+       $self->{'buffer'} = "";
+       bless($self,$what);
+
+       $repo =~ s#/+$##;
+       $self->{'fullrep'} = $repo;
+       $self->conn();
+
+       $self->{'subdir'} = $subdir;
+       $self->{'lines'} = undef;
+
+       return $self;
+}
+
+sub conn {
+       my $self = shift;
+       my $repo = $self->{'fullrep'};
+       if ($repo =~ s/^:pserver(?:([^:]*)):(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?//) {
+               my ($param,$user,$pass,$serv,$port) = ($1,$2,$3,$4,$5);
+
+               my ($proxyhost,$proxyport);
+               if ($param && ($param =~ m/proxy=([^;]+)/)) {
+                       $proxyhost = $1;
+                       # Default proxyport, if not specified, is 8080.
+                       $proxyport = 8080;
+                       if ($ENV{"CVS_PROXY_PORT"}) {
+                               $proxyport = $ENV{"CVS_PROXY_PORT"};
+                       }
+                       if ($param =~ m/proxyport=([^;]+)/) {
+                               $proxyport = $1;
+                       }
+               }
+
+               # if username is not explicit in CVSROOT, then use current user, as cvs would
+               $user=(getlogin() || $ENV{'LOGNAME'} || $ENV{'USER'} || "anonymous") unless $user;
+               my $rr2 = "-";
+               unless ($port) {
+                       $rr2 = ":pserver:$user\@$serv:$repo";
+                       $port=2401;
+               }
+               my $rr = ":pserver:$user\@$serv:$port$repo";
+
+               unless ($pass) {
+                       open(H,$ENV{'HOME'}."/.cvspass") and do {
+                               # :pserver:cvs@mea.tmt.tele.fi:/cvsroot/zmailer Ah<Z
+                               while (<H>) {
+                                       chomp;
+                                       s/^\/\d+\s+//;
+                                       my ($w,$p) = split(/\s/,$_,2);
+                                       if ($w eq $rr or $w eq $rr2) {
+                                               $pass = $p;
+                                               last;
+                                       }
+                               }
+                       };
+               }
+               $pass="A" unless $pass;
+
+               my ($s, $rep);
+               if ($proxyhost) {
+
+                       # Use a HTTP Proxy. Only works for HTTP proxies that
+                       # don't require user authentication
+                       #
+                       # See: http://www.ietf.org/rfc/rfc2817.txt
+
+                       $s = IO::Socket::INET->new(PeerHost => $proxyhost, PeerPort => $proxyport);
+                       die "Socket to $proxyhost: $!\n" unless defined $s;
+                       $s->write("CONNECT $serv:$port HTTP/1.1\r\nHost: $serv:$port\r\n\r\n")
+                               or die "Write to $proxyhost: $!\n";
+                       $s->flush();
+
+                       $rep = <$s>;
+
+                       # The answer should look like 'HTTP/1.x 2yy ....'
+                       if (!($rep =~ m#^HTTP/1\.. 2[0-9][0-9]#)) {
+                               die "Proxy connect: $rep\n";
+                       }
+                       # Skip up to the empty line of the proxy server output
+                       # including the response headers.
+                       while ($rep = <$s>) {
+                               last if (!defined $rep ||
+                                        $rep eq "\n" ||
+                                        $rep eq "\r\n");
+                       }
+               } else {
+                       $s = IO::Socket::INET->new(PeerHost => $serv, PeerPort => $port);
+                       die "Socket to $serv: $!\n" unless defined $s;
+               }
+
+               $s->write("BEGIN AUTH REQUEST\n$repo\n$user\n$pass\nEND AUTH REQUEST\n")
+                       or die "Write to $serv: $!\n";
+               $s->flush();
+
+               $rep = <$s>;
+
+               if ($rep ne "I LOVE YOU\n") {
+                       $rep="<unknown>" unless $rep;
+                       die "AuthReply: $rep\n";
+               }
+               $self->{'socketo'} = $s;
+               $self->{'socketi'} = $s;
+       } else { # local or ext: Fork off our own cvs server.
+               my $pr = IO::Pipe->new();
+               my $pw = IO::Pipe->new();
+               my $pid = fork();
+               die "Fork: $!\n" unless defined $pid;
+               my $cvs = 'cvs';
+               $cvs = $ENV{CVS_SERVER} if exists $ENV{CVS_SERVER};
+               my $rsh = 'rsh';
+               $rsh = $ENV{CVS_RSH} if exists $ENV{CVS_RSH};
+
+               my @cvs = ($cvs, 'server');
+               my ($local, $user, $host);
+               $local = $repo =~ s/:local://;
+               if (!$local) {
+                   $repo =~ s/:ext://;
+                   $local = !($repo =~ s/^(?:([^\@:]+)\@)?([^:]+)://);
+                   ($user, $host) = ($1, $2);
+               }
+               if (!$local) {
+                   if ($user) {
+                       unshift @cvs, $rsh, '-l', $user, $host;
+                   } else {
+                       unshift @cvs, $rsh, $host;
+                   }
+               }
+
+               unless ($pid) {
+                       $pr->writer();
+                       $pw->reader();
+                       dup2($pw->fileno(),0);
+                       dup2($pr->fileno(),1);
+                       $pr->close();
+                       $pw->close();
+                       exec(@cvs);
+               }
+               $pw->writer();
+               $pr->reader();
+               $self->{'socketo'} = $pw;
+               $self->{'socketi'} = $pr;
+       }
+       $self->{'socketo'}->write("Root $repo\n");
+
+       # Trial and error says that this probably is the minimum set
+       $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mode M Mbinary E Checked-in Created Updated Merged Removed\n");
+
+       $self->{'socketo'}->write("valid-requests\n");
+       $self->{'socketo'}->flush();
+
+       chomp(my $rep=$self->readline());
+       if ($rep !~ s/^Valid-requests\s*//) {
+               $rep="<unknown>" unless $rep;
+               die "Expected Valid-requests from server, but got: $rep\n";
+       }
+       chomp(my $res=$self->readline());
+       die "validReply: $res\n" if $res ne "ok";
+
+       $self->{'socketo'}->write("UseUnchanged\n") if $rep =~ /\bUseUnchanged\b/;
+       $self->{'repo'} = $repo;
+}
+
+sub readline {
+       my ($self) = @_;
+       return $self->{'socketi'}->getline();
+}
+
+sub _file {
+       # Request a file with a given revision.
+       # Trial and error says this is a good way to do it. :-/
+       my ($self,$fn,$rev) = @_;
+       $self->{'socketo'}->write("Argument -N\n") or return undef;
+       $self->{'socketo'}->write("Argument -P\n") or return undef;
+       # -kk: Linus' version doesn't use it - defaults to off
+       if ($opt_k) {
+           $self->{'socketo'}->write("Argument -kk\n") or return undef;
+       }
+       $self->{'socketo'}->write("Argument -r\n") or return undef;
+       $self->{'socketo'}->write("Argument $rev\n") or return undef;
+       $self->{'socketo'}->write("Argument --\n") or return undef;
+       $self->{'socketo'}->write("Argument $self->{'subdir'}/$fn\n") or return undef;
+       $self->{'socketo'}->write("Directory .\n") or return undef;
+       $self->{'socketo'}->write("$self->{'repo'}\n") or return undef;
+       # $self->{'socketo'}->write("Sticky T1.0\n") or return undef;
+       $self->{'socketo'}->write("co\n") or return undef;
+       $self->{'socketo'}->flush() or return undef;
+       $self->{'lines'} = 0;
+       return 1;
+}
+sub _line {
+       # Read a line from the server.
+       # ... except that 'line' may be an entire file. ;-)
+       my ($self, $fh) = @_;
+       die "Not in lines" unless defined $self->{'lines'};
+
+       my $line;
+       my $res=0;
+       while (defined($line = $self->readline())) {
+               # M U gnupg-cvs-rep/AUTHORS
+               # Updated gnupg-cvs-rep/
+               # /daten/src/rsync/gnupg-cvs-rep/AUTHORS
+               # /AUTHORS/1.1///T1.1
+               # u=rw,g=rw,o=rw
+               # 0
+               # ok
+
+               if ($line =~ s/^(?:Created|Updated) //) {
+                       $line = $self->readline(); # path
+                       $line = $self->readline(); # Entries line
+                       my $mode = $self->readline(); chomp $mode;
+                       $self->{'mode'} = $mode;
+                       defined (my $cnt = $self->readline())
+                               or die "EOF from server after 'Changed'\n";
+                       chomp $cnt;
+                       die "Duh: Filesize $cnt" if $cnt !~ /^\d+$/;
+                       $line="";
+                       $res = $self->_fetchfile($fh, $cnt);
+               } elsif ($line =~ s/^ //) {
+                       print $fh $line;
+                       $res += length($line);
+               } elsif ($line =~ /^M\b/) {
+                       # output, do nothing
+               } elsif ($line =~ /^Mbinary\b/) {
+                       my $cnt;
+                       die "EOF from server after 'Mbinary'" unless defined ($cnt = $self->readline());
+                       chomp $cnt;
+                       die "Duh: Mbinary $cnt" if $cnt !~ /^\d+$/ or $cnt<1;
+                       $line="";
+                       $res += $self->_fetchfile($fh, $cnt);
+               } else {
+                       chomp $line;
+                       if ($line eq "ok") {
+                               # print STDERR "S: ok (".length($res).")\n";
+                               return $res;
+                       } elsif ($line =~ s/^E //) {
+                               # print STDERR "S: $line\n";
+                       } elsif ($line =~ /^(Remove-entry|Removed) /i) {
+                               $line = $self->readline(); # filename
+                               $line = $self->readline(); # OK
+                               chomp $line;
+                               die "Unknown: $line" if $line ne "ok";
+                               return -1;
+                       } else {
+                               die "Unknown: $line\n";
+                       }
+               }
+       }
+       return undef;
+}
+sub file {
+       my ($self,$fn,$rev) = @_;
+       my $res;
+
+       my ($fh, $name) = tempfile('gitcvs.XXXXXX',
+                   DIR => File::Spec->tmpdir(), UNLINK => 1);
+
+       $self->_file($fn,$rev) and $res = $self->_line($fh);
+
+       if (!defined $res) {
+           print STDERR "Server has gone away while fetching $fn $rev, retrying...\n";
+           truncate $fh, 0;
+           $self->conn();
+           $self->_file($fn,$rev) or die "No file command send";
+           $res = $self->_line($fh);
+           die "Retry failed" unless defined $res;
+       }
+       close ($fh);
+
+       return ($name, $res);
+}
+sub _fetchfile {
+       my ($self, $fh, $cnt) = @_;
+       my $res = 0;
+       my $bufsize = 1024 * 1024;
+       while ($cnt) {
+           if ($bufsize > $cnt) {
+               $bufsize = $cnt;
+           }
+           my $buf;
+           my $num = $self->{'socketi'}->read($buf,$bufsize);
+           die "Server: Filesize $cnt: $num: $!\n" if not defined $num or $num<=0;
+           print $fh $buf;
+           $res += $num;
+           $cnt -= $num;
+       }
+       return $res;
+}
+
+
+package main;
+
+my $cvs = CVSconn->new($opt_d, $cvs_tree);
+
+
+sub pdate($) {
+       my ($d) = @_;
+       m#(\d{2,4})/(\d\d)/(\d\d)\s(\d\d):(\d\d)(?::(\d\d))?#
+               or die "Unparseable date: $d\n";
+       my $y=$1; $y-=1900 if $y>1900;
+       return timegm($6||0,$5,$4,$3,$2-1,$y);
+}
+
+sub pmode($) {
+       my ($mode) = @_;
+       my $m = 0;
+       my $mm = 0;
+       my $um = 0;
+       for my $x(split(//,$mode)) {
+               if ($x eq ",") {
+                       $m |= $mm&$um;
+                       $mm = 0;
+                       $um = 0;
+               } elsif ($x eq "u") { $um |= 0700;
+               } elsif ($x eq "g") { $um |= 0070;
+               } elsif ($x eq "o") { $um |= 0007;
+               } elsif ($x eq "r") { $mm |= 0444;
+               } elsif ($x eq "w") { $mm |= 0222;
+               } elsif ($x eq "x") { $mm |= 0111;
+               } elsif ($x eq "=") { # do nothing
+               } else { die "Unknown mode: $mode\n";
+               }
+       }
+       $m |= $mm&$um;
+       return $m;
+}
+
+sub getwd() {
+       my $pwd = `pwd`;
+       chomp $pwd;
+       return $pwd;
+}
+
+sub is_sha1 {
+       my $s = shift;
+       return $s =~ /^[a-f0-9]{40}$/;
+}
+
+sub get_headref ($) {
+       my $name = shift;
+       my $r = `git rev-parse --verify '$name' 2>/dev/null`;
+       return undef unless $? == 0;
+       chomp $r;
+       return $r;
+}
+
+-d $git_tree
+       or mkdir($git_tree,0777)
+       or die "Could not create $git_tree: $!";
+chdir($git_tree);
+
+my $last_branch = "";
+my $orig_branch = "";
+my %branch_date;
+my $tip_at_start = undef;
+
+my $git_dir = $ENV{"GIT_DIR"} || ".git";
+$git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#;
+$ENV{"GIT_DIR"} = $git_dir;
+my $orig_git_index;
+$orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE};
+
+my %index; # holds filenames of one index per branch
+
+unless (-d $git_dir) {
+       system("git-init");
+       die "Cannot init the GIT db at $git_tree: $?\n" if $?;
+       system("git-read-tree");
+       die "Cannot init an empty tree: $?\n" if $?;
+
+       $last_branch = $opt_o;
+       $orig_branch = "";
+} else {
+       open(F, "git-symbolic-ref HEAD |") or
+               die "Cannot run git-symbolic-ref: $!\n";
+       chomp ($last_branch = <F>);
+       $last_branch = basename($last_branch);
+       close(F);
+       unless ($last_branch) {
+               warn "Cannot read the last branch name: $! -- assuming 'master'\n";
+               $last_branch = "master";
+       }
+       $orig_branch = $last_branch;
+       $tip_at_start = `git-rev-parse --verify HEAD`;
+
+       # Get the last import timestamps
+       my $fmt = '($ref, $author) = (%(refname), %(author));';
+       open(H, "git-for-each-ref --perl --format='$fmt' $remote |") or
+               die "Cannot run git-for-each-ref: $!\n";
+       while (defined(my $entry = <H>)) {
+               my ($ref, $author);
+               eval($entry) || die "cannot eval refs list: $@";
+               my ($head) = ($ref =~ m|^$remote/(.*)|);
+               $author =~ /^.*\s(\d+)\s[-+]\d{4}$/;
+               $branch_date{$head} = $1;
+       }
+       close(H);
+        if (!exists $branch_date{$opt_o}) {
+               die "Branch '$opt_o' does not exist.\n".
+                      "Either use the correct '-o branch' option,\n".
+                      "or import to a new repository.\n";
+        }
+}
+
+-d $git_dir
+       or die "Could not create git subdir ($git_dir).\n";
+
+# now we read (and possibly save) author-info as well
+-f "$git_dir/cvs-authors" and
+  read_author_info("$git_dir/cvs-authors");
+if ($opt_A) {
+       read_author_info($opt_A);
+       write_author_info("$git_dir/cvs-authors");
+}
+
+
+#
+# run cvsps into a file unless we are getting
+# it passed as a file via $opt_P
+#
+my $cvspsfile;
+unless ($opt_P) {
+       print "Running cvsps...\n" if $opt_v;
+       my $pid = open(CVSPS,"-|");
+       my $cvspsfh;
+       die "Cannot fork: $!\n" unless defined $pid;
+       unless ($pid) {
+               my @opt;
+               @opt = split(/,/,$opt_p) if defined $opt_p;
+               unshift @opt, '-z', $opt_z if defined $opt_z;
+               unshift @opt, '-q'         unless defined $opt_v;
+               unless (defined($opt_p) && $opt_p =~ m/--no-cvs-direct/) {
+                       push @opt, '--cvs-direct';
+               }
+               exec("cvsps","--norc",@opt,"-u","-A",'--root',$opt_d,$cvs_tree);
+               die "Could not start cvsps: $!\n";
+       }
+       ($cvspsfh, $cvspsfile) = tempfile('gitXXXXXX', SUFFIX => '.cvsps',
+                                         DIR => File::Spec->tmpdir());
+       while (<CVSPS>) {
+           print $cvspsfh $_;
+       }
+       close CVSPS;
+       close $cvspsfh;
+} else {
+       $cvspsfile = $opt_P;
+}
+
+open(CVS, "<$cvspsfile") or die $!;
+
+## cvsps output:
+#---------------------
+#PatchSet 314
+#Date: 1999/09/18 13:03:59
+#Author: wkoch
+#Branch: STABLE-BRANCH-1-0
+#Ancestor branch: HEAD
+#Tag: (none)
+#Log:
+#    See ChangeLog: Sat Sep 18 13:03:28 CEST 1999  Werner Koch
+#Members:
+#      README:1.57->1.57.2.1
+#      VERSION:1.96->1.96.2.1
+#
+#---------------------
+
+my $state = 0;
+
+sub update_index (\@\@) {
+       my $old = shift;
+       my $new = shift;
+       open(my $fh, '|-', qw(git-update-index -z --index-info))
+               or die "unable to open git-update-index: $!";
+       print $fh
+               (map { "0 0000000000000000000000000000000000000000\t$_\0" }
+                       @$old),
+               (map { '100' . sprintf('%o', $_->[0]) . " $_->[1]\t$_->[2]\0" }
+                       @$new)
+               or die "unable to write to git-update-index: $!";
+       close $fh
+               or die "unable to write to git-update-index: $!";
+       $? and die "git-update-index reported error: $?";
+}
+
+sub write_tree () {
+       open(my $fh, '-|', qw(git-write-tree))
+               or die "unable to open git-write-tree: $!";
+       chomp(my $tree = <$fh>);
+       is_sha1($tree)
+               or die "Cannot get tree id ($tree): $!";
+       close($fh)
+               or die "Error running git-write-tree: $?\n";
+       print "Tree ID $tree\n" if $opt_v;
+       return $tree;
+}
+
+my ($patchset,$date,$author_name,$author_email,$branch,$ancestor,$tag,$logmsg);
+my (@old,@new,@skipped,%ignorebranch);
+
+# commits that cvsps cannot place anywhere...
+$ignorebranch{'#CVSPS_NO_BRANCH'} = 1;
+
+sub commit {
+       if ($branch eq $opt_o && !$index{branch} &&
+               !get_headref("$remote/$branch")) {
+           # looks like an initial commit
+           # use the index primed by git-init
+           $ENV{GIT_INDEX_FILE} = "$git_dir/index";
+           $index{$branch} = "$git_dir/index";
+       } else {
+           # use an index per branch to speed up
+           # imports of projects with many branches
+           unless ($index{$branch}) {
+               $index{$branch} = tmpnam();
+               $ENV{GIT_INDEX_FILE} = $index{$branch};
+               if ($ancestor) {
+                   system("git-read-tree", "$remote/$ancestor");
+               } else {
+                   system("git-read-tree", "$remote/$branch");
+               }
+               die "read-tree failed: $?\n" if $?;
+           }
+       }
+        $ENV{GIT_INDEX_FILE} = $index{$branch};
+
+       update_index(@old, @new);
+       @old = @new = ();
+       my $tree = write_tree();
+       my $parent = get_headref("$remote/$last_branch");
+       print "Parent ID " . ($parent ? $parent : "(empty)") . "\n" if $opt_v;
+
+       my @commit_args;
+       push @commit_args, ("-p", $parent) if $parent;
+
+       # loose detection of merges
+       # based on the commit msg
+       foreach my $rx (@mergerx) {
+               next unless $logmsg =~ $rx && $1;
+               my $mparent = $1 eq 'HEAD' ? $opt_o : $1;
+               if (my $sha1 = get_headref("$remote/$mparent")) {
+                       push @commit_args, '-p', $mparent;
+                       print "Merge parent branch: $mparent\n" if $opt_v;
+               }
+       }
+
+       my $commit_date = strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date));
+       $ENV{GIT_AUTHOR_NAME} = $author_name;
+       $ENV{GIT_AUTHOR_EMAIL} = $author_email;
+       $ENV{GIT_AUTHOR_DATE} = $commit_date;
+       $ENV{GIT_COMMITTER_NAME} = $author_name;
+       $ENV{GIT_COMMITTER_EMAIL} = $author_email;
+       $ENV{GIT_COMMITTER_DATE} = $commit_date;
+       my $pid = open2(my $commit_read, my $commit_write,
+               'git-commit-tree', $tree, @commit_args);
+
+       # compatibility with git2cvs
+       substr($logmsg,32767) = "" if length($logmsg) > 32767;
+       $logmsg =~ s/[\s\n]+\z//;
+
+       if (@skipped) {
+           $logmsg .= "\n\n\nSKIPPED:\n\t";
+           $logmsg .= join("\n\t", @skipped) . "\n";
+           @skipped = ();
+       }
+
+       print($commit_write "$logmsg\n") && close($commit_write)
+               or die "Error writing to git-commit-tree: $!\n";
+
+       print "Committed patch $patchset ($branch $commit_date)\n" if $opt_v;
+       chomp(my $cid = <$commit_read>);
+       is_sha1($cid) or die "Cannot get commit id ($cid): $!\n";
+       print "Commit ID $cid\n" if $opt_v;
+       close($commit_read);
+
+       waitpid($pid,0);
+       die "Error running git-commit-tree: $?\n" if $?;
+
+       system("git-update-ref $remote/$branch $cid") == 0
+               or die "Cannot write branch $branch for update: $!\n";
+
+       if ($tag) {
+               my ($xtag) = $tag;
+               $xtag =~ s/\s+\*\*.*$//; # Remove stuff like ** INVALID ** and ** FUNKY **
+               $xtag =~ tr/_/\./ if ( $opt_u );
+               $xtag =~ s/[\/]/$opt_s/g;
+
+               system('git-tag', '-f', $xtag, $cid) == 0
+                       or die "Cannot create tag $xtag: $!\n";
+
+               print "Created tag '$xtag' on '$branch'\n" if $opt_v;
+       }
+};
+
+my $commitcount = 1;
+while (<CVS>) {
+       chomp;
+       if ($state == 0 and /^-+$/) {
+               $state = 1;
+       } elsif ($state == 0) {
+               $state = 1;
+               redo;
+       } elsif (($state==0 or $state==1) and s/^PatchSet\s+//) {
+               $patchset = 0+$_;
+               $state=2;
+       } elsif ($state == 2 and s/^Date:\s+//) {
+               $date = pdate($_);
+               unless ($date) {
+                       print STDERR "Could not parse date: $_\n";
+                       $state=0;
+                       next;
+               }
+               $state=3;
+       } elsif ($state == 3 and s/^Author:\s+//) {
+               s/\s+$//;
+               if (/^(.*?)\s+<(.*)>/) {
+                   ($author_name, $author_email) = ($1, $2);
+               } elsif ($conv_author_name{$_}) {
+                       $author_name = $conv_author_name{$_};
+                       $author_email = $conv_author_email{$_};
+               } else {
+                   $author_name = $author_email = $_;
+               }
+               $state = 4;
+       } elsif ($state == 4 and s/^Branch:\s+//) {
+               s/\s+$//;
+               tr/_/\./ if ( $opt_u );
+               s/[\/]/$opt_s/g;
+               $branch = $_;
+               $state = 5;
+       } elsif ($state == 5 and s/^Ancestor branch:\s+//) {
+               s/\s+$//;
+               $ancestor = $_;
+               $ancestor = $opt_o if $ancestor eq "HEAD";
+               $state = 6;
+       } elsif ($state == 5) {
+               $ancestor = undef;
+               $state = 6;
+               redo;
+       } elsif ($state == 6 and s/^Tag:\s+//) {
+               s/\s+$//;
+               if ($_ eq "(none)") {
+                       $tag = undef;
+               } else {
+                       $tag = $_;
+               }
+               $state = 7;
+       } elsif ($state == 7 and /^Log:/) {
+               $logmsg = "";
+               $state = 8;
+       } elsif ($state == 8 and /^Members:/) {
+               $branch = $opt_o if $branch eq "HEAD";
+               if (defined $branch_date{$branch} and $branch_date{$branch} >= $date) {
+                       # skip
+                       print "skip patchset $patchset: $date before $branch_date{$branch}\n" if $opt_v;
+                       $state = 11;
+                       next;
+               }
+               if (!$opt_a && $starttime - 300 - (defined $opt_z ? $opt_z : 300) <= $date) {
+                       # skip if the commit is too recent
+                       # that the cvsps default fuzz is 300s, we give ourselves another
+                       # 300s just in case -- this also prevents skipping commits
+                       # due to server clock drift
+                       print "skip patchset $patchset: $date too recent\n" if $opt_v;
+                       $state = 11;
+                       next;
+               }
+               if (exists $ignorebranch{$branch}) {
+                       print STDERR "Skipping $branch\n";
+                       $state = 11;
+                       next;
+               }
+               if ($ancestor) {
+                       if ($ancestor eq $branch) {
+                               print STDERR "Branch $branch erroneously stems from itself -- changed ancestor to $opt_o\n";
+                               $ancestor = $opt_o;
+                       }
+                       if (defined get_headref("$remote/$branch")) {
+                               print STDERR "Branch $branch already exists!\n";
+                               $state=11;
+                               next;
+                       }
+                       my $id = get_headref("$remote/$ancestor");
+                       if (!$id) {
+                               print STDERR "Branch $ancestor does not exist!\n";
+                               $ignorebranch{$branch} = 1;
+                               $state=11;
+                               next;
+                       }
+
+                       system(qw(git update-ref -m cvsimport),
+                               "$remote/$branch", $id);
+                       if($? != 0) {
+                               print STDERR "Could not create branch $branch\n";
+                               $ignorebranch{$branch} = 1;
+                               $state=11;
+                               next;
+                       }
+               }
+               $last_branch = $branch if $branch ne $last_branch;
+               $state = 9;
+       } elsif ($state == 8) {
+               $logmsg .= "$_\n";
+       } elsif ($state == 9 and /^\s+(.+?):(INITIAL|\d+(?:\.\d+)+)->(\d+(?:\.\d+)+)\s*$/) {
+#      VERSION:1.96->1.96.2.1
+               my $init = ($2 eq "INITIAL");
+               my $fn = $1;
+               my $rev = $3;
+               $fn =~ s#^/+##;
+               if ($opt_S && $fn =~ m/$opt_S/) {
+                   print "SKIPPING $fn v $rev\n";
+                   push(@skipped, $fn);
+                   next;
+               }
+               print "Fetching $fn   v $rev\n" if $opt_v;
+               my ($tmpname, $size) = $cvs->file($fn,$rev);
+               if ($size == -1) {
+                       push(@old,$fn);
+                       print "Drop $fn\n" if $opt_v;
+               } else {
+                       print "".($init ? "New" : "Update")." $fn: $size bytes\n" if $opt_v;
+                       my $pid = open(my $F, '-|');
+                       die $! unless defined $pid;
+                       if (!$pid) {
+                           exec("git-hash-object", "-w", $tmpname)
+                               or die "Cannot create object: $!\n";
+                       }
+                       my $sha = <$F>;
+                       chomp $sha;
+                       close $F;
+                       my $mode = pmode($cvs->{'mode'});
+                       push(@new,[$mode, $sha, $fn]); # may be resurrected!
+               }
+               unlink($tmpname);
+       } elsif ($state == 9 and /^\s+(.+?):\d+(?:\.\d+)+->(\d+(?:\.\d+)+)\(DEAD\)\s*$/) {
+               my $fn = $1;
+               $fn =~ s#^/+##;
+               push(@old,$fn);
+               print "Delete $fn\n" if $opt_v;
+       } elsif ($state == 9 and /^\s*$/) {
+               $state = 10;
+       } elsif (($state == 9 or $state == 10) and /^-+$/) {
+               $commitcount++;
+               if ($opt_L && $commitcount > $opt_L) {
+                       last;
+               }
+               commit();
+               if (($commitcount & 1023) == 0) {
+                       system("git repack -a -d");
+               }
+               $state = 1;
+       } elsif ($state == 11 and /^-+$/) {
+               $state = 1;
+       } elsif (/^-+$/) { # end of unknown-line processing
+               $state = 1;
+       } elsif ($state != 11) { # ignore stuff when skipping
+               print "* UNKNOWN LINE * $_\n";
+       }
+}
+commit() if $branch and $state != 11;
+
+unless ($opt_P) {
+       unlink($cvspsfile);
+}
+
+# The heuristic of repacking every 1024 commits can leave a
+# lot of unpacked data.  If there is more than 1MB worth of
+# not-packed objects, repack once more.
+my $line = `git-count-objects`;
+if ($line =~ /^(\d+) objects, (\d+) kilobytes$/) {
+  my ($n_objects, $kb) = ($1, $2);
+  1024 < $kb
+    and system("git repack -a -d");
+}
+
+foreach my $git_index (values %index) {
+    if ($git_index ne "$git_dir/index") {
+       unlink($git_index);
+    }
+}
+
+if (defined $orig_git_index) {
+       $ENV{GIT_INDEX_FILE} = $orig_git_index;
+} else {
+       delete $ENV{GIT_INDEX_FILE};
+}
+
+# Now switch back to the branch we were in before all of this happened
+if ($orig_branch) {
+       print "DONE.\n" if $opt_v;
+       if ($opt_i) {
+               exit 0;
+       }
+       my $tip_at_end = `git-rev-parse --verify HEAD`;
+       if ($tip_at_start ne $tip_at_end) {
+               for ($tip_at_start, $tip_at_end) { chomp; }
+               print "Fetched into the current branch.\n" if $opt_v;
+               system(qw(git-read-tree -u -m),
+                      $tip_at_start, $tip_at_end);
+               die "Fast-forward update failed: $?\n" if $?;
+       }
+       else {
+               system(qw(git-merge cvsimport HEAD), "$remote/$opt_o");
+               die "Could not merge $opt_o into the current branch.\n" if $?;
+       }
+} else {
+       $orig_branch = "master";
+       print "DONE; creating $orig_branch branch\n" if $opt_v;
+       system("git-update-ref", "refs/heads/master", "$remote/$opt_o")
+               unless defined get_headref('refs/heads/master');
+       system("git-symbolic-ref", "$remote/HEAD", "$remote/$opt_o")
+               if ($opt_r && $opt_o ne 'HEAD');
+       system('git-update-ref', 'HEAD', "$orig_branch");
+       unless ($opt_i) {
+               system('git checkout -f');
+               die "checkout failed: $?\n" if $?;
+       }
+}
diff --git a/git-cvsserver.perl b/git-cvsserver.perl
new file mode 100755 (executable)
index 0000000..ecded3b
--- /dev/null
@@ -0,0 +1,3057 @@
+#!/usr/bin/perl
+
+####
+#### This application is a CVS emulation layer for git.
+#### It is intended for clients to connect over SSH.
+#### See the documentation for more details.
+####
+#### Copyright The Open University UK - 2006.
+####
+#### Authors: Martyn Smith    <martyn@catalyst.net.nz>
+####          Martin Langhoff <martin@catalyst.net.nz>
+####
+####
+#### Released under the GNU Public License, version 2.
+####
+####
+
+use strict;
+use warnings;
+use bytes;
+
+use Fcntl;
+use File::Temp qw/tempdir tempfile/;
+use File::Basename;
+use Getopt::Long qw(:config require_order no_ignore_case);
+
+my $VERSION = '@@GIT_VERSION@@';
+
+my $log = GITCVS::log->new();
+my $cfg;
+
+my $DATE_LIST = {
+    Jan => "01",
+    Feb => "02",
+    Mar => "03",
+    Apr => "04",
+    May => "05",
+    Jun => "06",
+    Jul => "07",
+    Aug => "08",
+    Sep => "09",
+    Oct => "10",
+    Nov => "11",
+    Dec => "12",
+};
+
+# Enable autoflush for STDOUT (otherwise the whole thing falls apart)
+$| = 1;
+
+#### Definition and mappings of functions ####
+
+my $methods = {
+    'Root'            => \&req_Root,
+    'Valid-responses' => \&req_Validresponses,
+    'valid-requests'  => \&req_validrequests,
+    'Directory'       => \&req_Directory,
+    'Entry'           => \&req_Entry,
+    'Modified'        => \&req_Modified,
+    'Unchanged'       => \&req_Unchanged,
+    'Questionable'    => \&req_Questionable,
+    'Argument'        => \&req_Argument,
+    'Argumentx'       => \&req_Argument,
+    'expand-modules'  => \&req_expandmodules,
+    'add'             => \&req_add,
+    'remove'          => \&req_remove,
+    'co'              => \&req_co,
+    'update'          => \&req_update,
+    'ci'              => \&req_ci,
+    'diff'            => \&req_diff,
+    'log'             => \&req_log,
+    'rlog'            => \&req_log,
+    'tag'             => \&req_CATCHALL,
+    'status'          => \&req_status,
+    'admin'           => \&req_CATCHALL,
+    'history'         => \&req_CATCHALL,
+    'watchers'        => \&req_CATCHALL,
+    'editors'         => \&req_CATCHALL,
+    'annotate'        => \&req_annotate,
+    'Global_option'   => \&req_Globaloption,
+    #'annotate'        => \&req_CATCHALL,
+};
+
+##############################################
+
+
+# $state holds all the bits of information the clients sends us that could
+# potentially be useful when it comes to actually _doing_ something.
+my $state = { prependdir => '' };
+$log->info("--------------- STARTING -----------------");
+
+my $usage =
+    "Usage: git-cvsserver [options] [pserver|server] [<directory> ...]\n".
+    "    --base-path <path>  : Prepend to requested CVSROOT\n".
+    "    --strict-paths      : Don't allow recursing into subdirectories\n".
+    "    --export-all        : Don't check for gitcvs.enabled in config\n".
+    "    --version, -V       : Print version information and exit\n".
+    "    --help, -h, -H      : Print usage information and exit\n".
+    "\n".
+    "<directory> ... is a list of allowed directories. If no directories\n".
+    "are given, all are allowed. This is an additional restriction, gitcvs\n".
+    "access still needs to be enabled by the gitcvs.enabled config option.\n";
+
+my @opts = ( 'help|h|H', 'version|V',
+            'base-path=s', 'strict-paths', 'export-all' );
+GetOptions( $state, @opts )
+    or die $usage;
+
+if ($state->{version}) {
+    print "git-cvsserver version $VERSION\n";
+    exit;
+}
+if ($state->{help}) {
+    print $usage;
+    exit;
+}
+
+my $TEMP_DIR = tempdir( CLEANUP => 1 );
+$log->debug("Temporary directory is '$TEMP_DIR'");
+
+$state->{method} = 'ext';
+if (@ARGV) {
+    if ($ARGV[0] eq 'pserver') {
+       $state->{method} = 'pserver';
+       shift @ARGV;
+    } elsif ($ARGV[0] eq 'server') {
+       shift @ARGV;
+    }
+}
+
+# everything else is a directory
+$state->{allowed_roots} = [ @ARGV ];
+
+# don't export the whole system unless the users requests it
+if ($state->{'export-all'} && !@{$state->{allowed_roots}}) {
+    die "--export-all can only be used together with an explicit whitelist\n";
+}
+
+# if we are called with a pserver argument,
+# deal with the authentication cat before entering the
+# main loop
+if ($state->{method} eq 'pserver') {
+    my $line = <STDIN>; chomp $line;
+    unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
+       die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
+    }
+    my $request = $1;
+    $line = <STDIN>; chomp $line;
+    unless (req_Root('root', $line)) { # reuse Root
+       print "E Invalid root $line \n";
+       exit 1;
+    }
+    $line = <STDIN>; chomp $line;
+    unless ($line eq 'anonymous') {
+       print "E Only anonymous user allowed via pserver\n";
+       print "I HATE YOU\n";
+       exit 1;
+    }
+    $line = <STDIN>; chomp $line;    # validate the password?
+    $line = <STDIN>; chomp $line;
+    unless ($line eq "END $request REQUEST") {
+       die "E Do not understand $line -- expecting END $request REQUEST\n";
+    }
+    print "I LOVE YOU\n";
+    exit if $request eq 'VERIFICATION'; # cvs login
+    # and now back to our regular programme...
+}
+
+# Keep going until the client closes the connection
+while (<STDIN>)
+{
+    chomp;
+
+    # Check to see if we've seen this method, and call appropriate function.
+    if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
+    {
+        # use the $methods hash to call the appropriate sub for this command
+        #$log->info("Method : $1");
+        &{$methods->{$1}}($1,$2);
+    } else {
+        # log fatal because we don't understand this function. If this happens
+        # we're fairly screwed because we don't know if the client is expecting
+        # a response. If it is, the client will hang, we'll hang, and the whole
+        # thing will be custard.
+        $log->fatal("Don't understand command $_\n");
+        die("Unknown command $_");
+    }
+}
+
+$log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
+$log->info("--------------- FINISH -----------------");
+
+# Magic catchall method.
+#    This is the method that will handle all commands we haven't yet
+#    implemented. It simply sends a warning to the log file indicating a
+#    command that hasn't been implemented has been invoked.
+sub req_CATCHALL
+{
+    my ( $cmd, $data ) = @_;
+    $log->warn("Unhandled command : req_$cmd : $data");
+}
+
+
+# Root pathname \n
+#     Response expected: no. Tell the server which CVSROOT to use. Note that
+#     pathname is a local directory and not a fully qualified CVSROOT variable.
+#     pathname must already exist; if creating a new root, use the init
+#     request, not Root. pathname does not include the hostname of the server,
+#     how to access the server, etc.; by the time the CVS protocol is in use,
+#     connection, authentication, etc., are already taken care of. The Root
+#     request must be sent only once, and it must be sent before any requests
+#     other than Valid-responses, valid-requests, UseUnchanged, Set or init.
+sub req_Root
+{
+    my ( $cmd, $data ) = @_;
+    $log->debug("req_Root : $data");
+
+    unless ($data =~ m#^/#) {
+       print "error 1 Root must be an absolute pathname\n";
+       return 0;
+    }
+
+    my $cvsroot = $state->{'base-path'} || '';
+    $cvsroot =~ s#/+$##;
+    $cvsroot .= $data;
+
+    if ($state->{CVSROOT}
+       && ($state->{CVSROOT} ne $cvsroot)) {
+       print "error 1 Conflicting roots specified\n";
+       return 0;
+    }
+
+    $state->{CVSROOT} = $cvsroot;
+
+    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+
+    if (@{$state->{allowed_roots}}) {
+       my $allowed = 0;
+       foreach my $dir (@{$state->{allowed_roots}}) {
+           next unless $dir =~ m#^/#;
+           $dir =~ s#/+$##;
+           if ($state->{'strict-paths'}) {
+               if ($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) {
+                   $allowed = 1;
+                   last;
+               }
+           } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) {
+               $allowed = 1;
+               last;
+           }
+       }
+
+       unless ($allowed) {
+           print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
+           print "E \n";
+           print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
+           return 0;
+       }
+    }
+
+    unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
+       print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
+       print "E \n";
+       print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
+       return 0;
+    }
+
+    my @gitvars = `git-config -l`;
+    if ($?) {
+       print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
+        print "E \n";
+        print "error 1 - problem executing git-config\n";
+       return 0;
+    }
+    foreach my $line ( @gitvars )
+    {
+        next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/ );
+        unless ($2) {
+            $cfg->{$1}{$3} = $4;
+        } else {
+            $cfg->{$1}{$2}{$3} = $4;
+        }
+    }
+
+    my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled}
+                  || $cfg->{gitcvs}{enabled});
+    unless ($state->{'export-all'} ||
+           ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) {
+        print "E GITCVS emulation needs to be enabled on this repo\n";
+        print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
+        print "E \n";
+        print "error 1 GITCVS emulation disabled\n";
+        return 0;
+    }
+
+    my $logfile = $cfg->{gitcvs}{$state->{method}}{logfile} || $cfg->{gitcvs}{logfile};
+    if ( $logfile )
+    {
+        $log->setfile($logfile);
+    } else {
+        $log->nofile();
+    }
+
+    return 1;
+}
+
+# Global_option option \n
+#     Response expected: no. Transmit one of the global options `-q', `-Q',
+#     `-l', `-t', `-r', or `-n'. option must be one of those strings, no
+#     variations (such as combining of options) are allowed. For graceful
+#     handling of valid-requests, it is probably better to make new global
+#     options separate requests, rather than trying to add them to this
+#     request.
+sub req_Globaloption
+{
+    my ( $cmd, $data ) = @_;
+    $log->debug("req_Globaloption : $data");
+    $state->{globaloptions}{$data} = 1;
+}
+
+# Valid-responses request-list \n
+#     Response expected: no. Tell the server what responses the client will
+#     accept. request-list is a space separated list of tokens.
+sub req_Validresponses
+{
+    my ( $cmd, $data ) = @_;
+    $log->debug("req_Validresponses : $data");
+
+    # TODO : re-enable this, currently it's not particularly useful
+    #$state->{validresponses} = [ split /\s+/, $data ];
+}
+
+# valid-requests \n
+#     Response expected: yes. Ask the server to send back a Valid-requests
+#     response.
+sub req_validrequests
+{
+    my ( $cmd, $data ) = @_;
+
+    $log->debug("req_validrequests");
+
+    $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
+    $log->debug("SEND : ok");
+
+    print "Valid-requests " . join(" ",keys %$methods) . "\n";
+    print "ok\n";
+}
+
+# Directory local-directory \n
+#     Additional data: repository \n. Response expected: no. Tell the server
+#     what directory to use. The repository should be a directory name from a
+#     previous server response. Note that this both gives a default for Entry
+#     and Modified and also for ci and the other commands; normal usage is to
+#     send Directory for each directory in which there will be an Entry or
+#     Modified, and then a final Directory for the original directory, then the
+#     command. The local-directory is relative to the top level at which the
+#     command is occurring (i.e. the last Directory which is sent before the
+#     command); to indicate that top level, `.' should be sent for
+#     local-directory.
+sub req_Directory
+{
+    my ( $cmd, $data ) = @_;
+
+    my $repository = <STDIN>;
+    chomp $repository;
+
+
+    $state->{localdir} = $data;
+    $state->{repository} = $repository;
+    $state->{path} = $repository;
+    $state->{path} =~ s/^$state->{CVSROOT}\///;
+    $state->{module} = $1 if ($state->{path} =~ s/^(.*?)(\/|$)//);
+    $state->{path} .= "/" if ( $state->{path} =~ /\S/ );
+
+    $state->{directory} = $state->{localdir};
+    $state->{directory} = "" if ( $state->{directory} eq "." );
+    $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
+
+    if ( (not defined($state->{prependdir}) or $state->{prependdir} eq '') and $state->{localdir} eq "." and $state->{path} =~ /\S/ )
+    {
+        $log->info("Setting prepend to '$state->{path}'");
+        $state->{prependdir} = $state->{path};
+        foreach my $entry ( keys %{$state->{entries}} )
+        {
+            $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
+            delete $state->{entries}{$entry};
+        }
+    }
+
+    if ( defined ( $state->{prependdir} ) )
+    {
+        $log->debug("Prepending '$state->{prependdir}' to state|directory");
+        $state->{directory} = $state->{prependdir} . $state->{directory}
+    }
+    $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
+}
+
+# Entry entry-line \n
+#     Response expected: no. Tell the server what version of a file is on the
+#     local machine. The name in entry-line is a name relative to the directory
+#     most recently specified with Directory. If the user is operating on only
+#     some files in a directory, Entry requests for only those files need be
+#     included. If an Entry request is sent without Modified, Is-modified, or
+#     Unchanged, it means the file is lost (does not exist in the working
+#     directory). If both Entry and one of Modified, Is-modified, or Unchanged
+#     are sent for the same file, Entry must be sent first. For a given file,
+#     one can send Modified, Is-modified, or Unchanged, but not more than one
+#     of these three.
+sub req_Entry
+{
+    my ( $cmd, $data ) = @_;
+
+    #$log->debug("req_Entry : $data");
+
+    my @data = split(/\//, $data);
+
+    $state->{entries}{$state->{directory}.$data[1]} = {
+        revision    => $data[2],
+        conflict    => $data[3],
+        options     => $data[4],
+        tag_or_date => $data[5],
+    };
+
+    $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
+}
+
+# Questionable filename \n
+#     Response expected: no. Additional data: no. Tell the server to check
+#     whether filename should be ignored, and if not, next time the server
+#     sends responses, send (in a M response) `?' followed by the directory and
+#     filename. filename must not contain `/'; it needs to be a file in the
+#     directory named by the most recent Directory request.
+sub req_Questionable
+{
+    my ( $cmd, $data ) = @_;
+
+    $log->debug("req_Questionable : $data");
+    $state->{entries}{$state->{directory}.$data}{questionable} = 1;
+}
+
+# add \n
+#     Response expected: yes. Add a file or directory. This uses any previous
+#     Argument, Directory, Entry, or Modified requests, if they have been sent.
+#     The last Directory sent specifies the working directory at the time of
+#     the operation. To add a directory, send the directory to be added using
+#     Directory and Argument requests.
+sub req_add
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("add");
+
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    argsfromdir($updater);
+
+    my $addcount = 0;
+
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        my $meta = $updater->getmeta($filename);
+        my $wrev = revparse($filename);
+
+        if ($wrev && $meta && ($wrev < 0))
+        {
+            # previously removed file, add back
+            $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
+
+            print "MT +updated\n";
+            print "MT text U \n";
+            print "MT fname $filename\n";
+            print "MT newline\n";
+            print "MT -updated\n";
+
+            unless ( $state->{globaloptions}{-n} )
+            {
+                my ( $filepart, $dirpart ) = filenamesplit($filename,1);
+
+                print "Created $dirpart\n";
+                print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+
+                # this is an "entries" line
+                my $kopts = kopts_from_path($filepart);
+                $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
+                print "/$filepart/1.$meta->{revision}//$kopts/\n";
+                # permissions
+                $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
+                print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
+                # transmit file
+                transmitfile($meta->{filehash});
+            }
+
+            next;
+        }
+
+        unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
+        {
+            print "E cvs add: nothing known about `$filename'\n";
+            next;
+        }
+        # TODO : check we're not squashing an already existing file
+        if ( defined ( $state->{entries}{$filename}{revision} ) )
+        {
+            print "E cvs add: `$filename' has already been entered\n";
+            next;
+        }
+
+        my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
+
+        print "E cvs add: scheduling file `$filename' for addition\n";
+
+        print "Checked-in $dirpart\n";
+        print "$filename\n";
+        my $kopts = kopts_from_path($filepart);
+        print "/$filepart/0//$kopts/\n";
+
+        $addcount++;
+    }
+
+    if ( $addcount == 1 )
+    {
+        print "E cvs add: use `cvs commit' to add this file permanently\n";
+    }
+    elsif ( $addcount > 1 )
+    {
+        print "E cvs add: use `cvs commit' to add these files permanently\n";
+    }
+
+    print "ok\n";
+}
+
+# remove \n
+#     Response expected: yes. Remove a file. This uses any previous Argument,
+#     Directory, Entry, or Modified requests, if they have been sent. The last
+#     Directory sent specifies the working directory at the time of the
+#     operation. Note that this request does not actually do anything to the
+#     repository; the only effect of a successful remove request is to supply
+#     the client with a new entries line containing `-' to indicate a removed
+#     file. In fact, the client probably could perform this operation without
+#     contacting the server, although using remove may cause the server to
+#     perform a few more checks. The client sends a subsequent ci request to
+#     actually record the removal in the repository.
+sub req_remove
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("remove");
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    #$log->debug("add state : " . Dumper($state));
+
+    my $rmcount = 0;
+
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
+        {
+            print "E cvs remove: file `$filename' still in working directory\n";
+            next;
+        }
+
+        my $meta = $updater->getmeta($filename);
+        my $wrev = revparse($filename);
+
+        unless ( defined ( $wrev ) )
+        {
+            print "E cvs remove: nothing known about `$filename'\n";
+            next;
+        }
+
+        if ( defined($wrev) and $wrev < 0 )
+        {
+            print "E cvs remove: file `$filename' already scheduled for removal\n";
+            next;
+        }
+
+        unless ( $wrev == $meta->{revision} )
+        {
+            # TODO : not sure if the format of this message is quite correct.
+            print "E cvs remove: Up to date check failed for `$filename'\n";
+            next;
+        }
+
+
+        my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
+
+        print "E cvs remove: scheduling `$filename' for removal\n";
+
+        print "Checked-in $dirpart\n";
+        print "$filename\n";
+        my $kopts = kopts_from_path($filepart);
+        print "/$filepart/-1.$wrev//$kopts/\n";
+
+        $rmcount++;
+    }
+
+    if ( $rmcount == 1 )
+    {
+        print "E cvs remove: use `cvs commit' to remove this file permanently\n";
+    }
+    elsif ( $rmcount > 1 )
+    {
+        print "E cvs remove: use `cvs commit' to remove these files permanently\n";
+    }
+
+    print "ok\n";
+}
+
+# Modified filename \n
+#     Response expected: no. Additional data: mode, \n, file transmission. Send
+#     the server a copy of one locally modified file. filename is a file within
+#     the most recent directory sent with Directory; it must not contain `/'.
+#     If the user is operating on only some files in a directory, only those
+#     files need to be included. This can also be sent without Entry, if there
+#     is no entry for the file.
+sub req_Modified
+{
+    my ( $cmd, $data ) = @_;
+
+    my $mode = <STDIN>;
+    defined $mode
+        or (print "E end of file reading mode for $data\n"), return;
+    chomp $mode;
+    my $size = <STDIN>;
+    defined $size
+        or (print "E end of file reading size of $data\n"), return;
+    chomp $size;
+
+    # Grab config information
+    my $blocksize = 8192;
+    my $bytesleft = $size;
+    my $tmp;
+
+    # Get a filehandle/name to write it to
+    my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
+
+    # Loop over file data writing out to temporary file.
+    while ( $bytesleft )
+    {
+        $blocksize = $bytesleft if ( $bytesleft < $blocksize );
+        read STDIN, $tmp, $blocksize;
+        print $fh $tmp;
+        $bytesleft -= $blocksize;
+    }
+
+    close $fh
+        or (print "E failed to write temporary, $filename: $!\n"), return;
+
+    # Ensure we have something sensible for the file mode
+    if ( $mode =~ /u=(\w+)/ )
+    {
+        $mode = $1;
+    } else {
+        $mode = "rw";
+    }
+
+    # Save the file data in $state
+    $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
+    $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
+    $state->{entries}{$state->{directory}.$data}{modified_hash} = `git-hash-object $filename`;
+    $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
+
+    #$log->debug("req_Modified : file=$data mode=$mode size=$size");
+}
+
+# Unchanged filename \n
+#     Response expected: no. Tell the server that filename has not been
+#     modified in the checked out directory. The filename is a file within the
+#     most recent directory sent with Directory; it must not contain `/'.
+sub req_Unchanged
+{
+    my ( $cmd, $data ) = @_;
+
+    $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
+
+    #$log->debug("req_Unchanged : $data");
+}
+
+# Argument text \n
+#     Response expected: no. Save argument for use in a subsequent command.
+#     Arguments accumulate until an argument-using command is given, at which
+#     point they are forgotten.
+# Argumentx text \n
+#     Response expected: no. Append \n followed by text to the current argument
+#     being saved.
+sub req_Argument
+{
+    my ( $cmd, $data ) = @_;
+
+    # Argumentx means: append to last Argument (with a newline in front)
+
+    $log->debug("$cmd : $data");
+
+    if ( $cmd eq 'Argumentx') {
+        ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" . $data;
+    } else {
+        push @{$state->{arguments}}, $data;
+    }
+}
+
+# expand-modules \n
+#     Response expected: yes. Expand the modules which are specified in the
+#     arguments. Returns the data in Module-expansion responses. Note that the
+#     server can assume that this is checkout or export, not rtag or rdiff; the
+#     latter do not access the working directory and thus have no need to
+#     expand modules on the client side. Expand may not be the best word for
+#     what this request does. It does not necessarily tell you all the files
+#     contained in a module, for example. Basically it is a way of telling you
+#     which working directories the server needs to know about in order to
+#     handle a checkout of the specified modules. For example, suppose that the
+#     server has a module defined by
+#   aliasmodule -a 1dir
+#     That is, one can check out aliasmodule and it will take 1dir in the
+#     repository and check it out to 1dir in the working directory. Now suppose
+#     the client already has this module checked out and is planning on using
+#     the co request to update it. Without using expand-modules, the client
+#     would have two bad choices: it could either send information about all
+#     working directories under the current directory, which could be
+#     unnecessarily slow, or it could be ignorant of the fact that aliasmodule
+#     stands for 1dir, and neglect to send information for 1dir, which would
+#     lead to incorrect operation. With expand-modules, the client would first
+#     ask for the module to be expanded:
+sub req_expandmodules
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit();
+
+    $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
+
+    unless ( ref $state->{arguments} eq "ARRAY" )
+    {
+        print "ok\n";
+        return;
+    }
+
+    foreach my $module ( @{$state->{arguments}} )
+    {
+        $log->debug("SEND : Module-expansion $module");
+        print "Module-expansion $module\n";
+    }
+
+    print "ok\n";
+    statecleanup();
+}
+
+# co \n
+#     Response expected: yes. Get files from the repository. This uses any
+#     previous Argument, Directory, Entry, or Modified requests, if they have
+#     been sent. Arguments to this command are module names; the client cannot
+#     know what directories they correspond to except by (1) just sending the
+#     co request, and then seeing what directory names the server sends back in
+#     its responses, and (2) the expand-modules request.
+sub req_co
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("co");
+
+    my $module = $state->{args}[0];
+    my $checkout_path = $module;
+
+    # use the user specified directory if we're given it
+    $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
+
+    $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
+
+    $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
+
+    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
+    $updater->update();
+
+    $checkout_path =~ s|/$||; # get rid of trailing slashes
+
+    # Eclipse seems to need the Clear-sticky command
+    # to prepare the 'Entries' file for the new directory.
+    print "Clear-sticky $checkout_path/\n";
+    print $state->{CVSROOT} . "/$module/\n";
+    print "Clear-static-directory $checkout_path/\n";
+    print $state->{CVSROOT} . "/$module/\n";
+    print "Clear-sticky $checkout_path/\n"; # yes, twice
+    print $state->{CVSROOT} . "/$module/\n";
+    print "Template $checkout_path/\n";
+    print $state->{CVSROOT} . "/$module/\n";
+    print "0\n";
+
+    # instruct the client that we're checking out to $checkout_path
+    print "E cvs checkout: Updating $checkout_path\n";
+
+    my %seendirs = ();
+    my $lastdir ='';
+
+    # recursive
+    sub prepdir {
+       my ($dir, $repodir, $remotedir, $seendirs) = @_;
+       my $parent = dirname($dir);
+       $dir       =~ s|/+$||;
+       $repodir   =~ s|/+$||;
+       $remotedir =~ s|/+$||;
+       $parent    =~ s|/+$||;
+       $log->debug("announcedir $dir, $repodir, $remotedir" );
+
+       if ($parent eq '.' || $parent eq './') {
+           $parent = '';
+       }
+       # recurse to announce unseen parents first
+       if (length($parent) && !exists($seendirs->{$parent})) {
+           prepdir($parent, $repodir, $remotedir, $seendirs);
+       }
+       # Announce that we are going to modify at the parent level
+       if ($parent) {
+           print "E cvs checkout: Updating $remotedir/$parent\n";
+       } else {
+           print "E cvs checkout: Updating $remotedir\n";
+       }
+       print "Clear-sticky $remotedir/$parent/\n";
+       print "$repodir/$parent/\n";
+
+       print "Clear-static-directory $remotedir/$dir/\n";
+       print "$repodir/$dir/\n";
+       print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
+       print "$repodir/$parent/\n";
+       print "Template $remotedir/$dir/\n";
+       print "$repodir/$dir/\n";
+       print "0\n";
+
+       $seendirs->{$dir} = 1;
+    }
+
+    foreach my $git ( @{$updater->gethead} )
+    {
+        # Don't want to check out deleted files
+        next if ( $git->{filehash} eq "deleted" );
+
+        ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
+
+       if (length($git->{dir}) && $git->{dir} ne './'
+           && $git->{dir} ne $lastdir ) {
+           unless (exists($seendirs{$git->{dir}})) {
+               prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
+                       $checkout_path, \%seendirs);
+               $lastdir = $git->{dir};
+               $seendirs{$git->{dir}} = 1;
+           }
+           print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
+       }
+
+        # modification time of this file
+        print "Mod-time $git->{modified}\n";
+
+        # print some information to the client
+        if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
+        {
+            print "M U $checkout_path/$git->{dir}$git->{name}\n";
+        } else {
+            print "M U $checkout_path/$git->{name}\n";
+        }
+
+       # instruct client we're sending a file to put in this path
+       print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
+
+       print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
+
+        # this is an "entries" line
+        my $kopts = kopts_from_path($git->{name});
+        print "/$git->{name}/1.$git->{revision}//$kopts/\n";
+        # permissions
+        print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
+
+        # transmit file
+        transmitfile($git->{filehash});
+    }
+
+    print "ok\n";
+
+    statecleanup();
+}
+
+# update \n
+#     Response expected: yes. Actually do a cvs update command. This uses any
+#     previous Argument, Directory, Entry, or Modified requests, if they have
+#     been sent. The last Directory sent specifies the working directory at the
+#     time of the operation. The -I option is not used--files which the client
+#     can decide whether to ignore are not mentioned and the client sends the
+#     Questionable request for others.
+sub req_update
+{
+    my ( $cmd, $data ) = @_;
+
+    $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
+
+    argsplit("update");
+
+    #
+    # It may just be a client exploring the available heads/modules
+    # in that case, list them as top level directories and leave it
+    # at that. Eclipse uses this technique to offer you a list of
+    # projects (heads in this case) to checkout.
+    #
+    if ($state->{module} eq '') {
+       my $heads_dir = $state->{CVSROOT} . '/refs/heads';
+       if (!opendir HEADS, $heads_dir) {
+           print "E [server aborted]: Failed to open directory, "
+             . "$heads_dir: $!\nerror\n";
+           return 0;
+       }
+        print "E cvs update: Updating .\n";
+       while (my $head = readdir(HEADS)) {
+           if (-f $state->{CVSROOT} . '/refs/heads/' . $head) {
+               print "E cvs update: New directory `$head'\n";
+           }
+       }
+       closedir HEADS;
+       print "ok\n";
+       return 1;
+    }
+
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+
+    $updater->update();
+
+    argsfromdir($updater);
+
+    #$log->debug("update state : " . Dumper($state));
+
+    # foreach file specified on the command line ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        $log->debug("Processing file $filename");
+
+        # if we have a -C we should pretend we never saw modified stuff
+        if ( exists ( $state->{opt}{C} ) )
+        {
+            delete $state->{entries}{$filename}{modified_hash};
+            delete $state->{entries}{$filename}{modified_filename};
+            $state->{entries}{$filename}{unchanged} = 1;
+        }
+
+        my $meta;
+        if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
+        {
+            $meta = $updater->getmeta($filename, $1);
+        } else {
+            $meta = $updater->getmeta($filename);
+        }
+
+       if ( ! defined $meta )
+       {
+           $meta = {
+               name => $filename,
+               revision => 0,
+               filehash => 'added'
+           };
+       }
+
+        my $oldmeta = $meta;
+
+        my $wrev = revparse($filename);
+
+        # If the working copy is an old revision, lets get that version too for comparison.
+        if ( defined($wrev) and $wrev != $meta->{revision} )
+        {
+            $oldmeta = $updater->getmeta($filename, $wrev);
+        }
+
+        #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
+
+        # Files are up to date if the working copy and repo copy have the same revision,
+        # and the working copy is unmodified _and_ the user hasn't specified -C
+        next if ( defined ( $wrev )
+                  and defined($meta->{revision})
+                  and $wrev == $meta->{revision}
+                  and $state->{entries}{$filename}{unchanged}
+                  and not exists ( $state->{opt}{C} ) );
+
+        # If the working copy and repo copy have the same revision,
+        # but the working copy is modified, tell the client it's modified
+        if ( defined ( $wrev )
+             and defined($meta->{revision})
+             and $wrev == $meta->{revision}
+             and defined($state->{entries}{$filename}{modified_hash})
+             and not exists ( $state->{opt}{C} ) )
+        {
+            $log->info("Tell the client the file is modified");
+            print "MT text M \n";
+            print "MT fname $filename\n";
+            print "MT newline\n";
+            next;
+        }
+
+        if ( $meta->{filehash} eq "deleted" )
+        {
+            my ( $filepart, $dirpart ) = filenamesplit($filename,1);
+
+            $log->info("Removing '$filename' from working copy (no longer in the repo)");
+
+            print "E cvs update: `$filename' is no longer in the repository\n";
+            # Don't want to actually _DO_ the update if -n specified
+            unless ( $state->{globaloptions}{-n} ) {
+               print "Removed $dirpart\n";
+               print "$filepart\n";
+           }
+        }
+        elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
+               or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash}
+               or $meta->{filehash} eq 'added' )
+        {
+            # normal update, just send the new revision (either U=Update,
+            # or A=Add, or R=Remove)
+           if ( defined($wrev) && $wrev < 0 )
+           {
+               $log->info("Tell the client the file is scheduled for removal");
+               print "MT text R \n";
+                print "MT fname $filename\n";
+                print "MT newline\n";
+               next;
+           }
+           elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) )
+           {
+               $log->info("Tell the client the file is scheduled for addition");
+               print "MT text A \n";
+                print "MT fname $filename\n";
+                print "MT newline\n";
+               next;
+
+           }
+           else {
+                $log->info("Updating '$filename' to ".$meta->{revision});
+                print "MT +updated\n";
+                print "MT text U \n";
+                print "MT fname $filename\n";
+                print "MT newline\n";
+               print "MT -updated\n";
+           }
+
+            my ( $filepart, $dirpart ) = filenamesplit($filename,1);
+
+           # Don't want to actually _DO_ the update if -n specified
+           unless ( $state->{globaloptions}{-n} )
+           {
+               if ( defined ( $wrev ) )
+               {
+                   # instruct client we're sending a file to put in this path as a replacement
+                   print "Update-existing $dirpart\n";
+                   $log->debug("Updating existing file 'Update-existing $dirpart'");
+               } else {
+                   # instruct client we're sending a file to put in this path as a new file
+                   print "Clear-static-directory $dirpart\n";
+                   print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
+                   print "Clear-sticky $dirpart\n";
+                   print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
+
+                   $log->debug("Creating new file 'Created $dirpart'");
+                   print "Created $dirpart\n";
+               }
+               print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+
+               # this is an "entries" line
+               my $kopts = kopts_from_path($filepart);
+               $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
+               print "/$filepart/1.$meta->{revision}//$kopts/\n";
+
+               # permissions
+               $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
+               print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
+
+               # transmit file
+               transmitfile($meta->{filehash});
+           }
+        } else {
+            $log->info("Updating '$filename'");
+            my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
+
+            my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/";
+
+            chdir $dir;
+            my $file_local = $filepart . ".mine";
+            system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
+            my $file_old = $filepart . "." . $oldmeta->{revision};
+            transmitfile($oldmeta->{filehash}, $file_old);
+            my $file_new = $filepart . "." . $meta->{revision};
+            transmitfile($meta->{filehash}, $file_new);
+
+            # we need to merge with the local changes ( M=successful merge, C=conflict merge )
+            $log->info("Merging $file_local, $file_old, $file_new");
+            print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
+
+            $log->debug("Temporary directory for merge is $dir");
+
+            my $return = system("git", "merge-file", $file_local, $file_old, $file_new);
+            $return >>= 8;
+
+            if ( $return == 0 )
+            {
+                $log->info("Merged successfully");
+                print "M M $filename\n";
+                $log->debug("Merged $dirpart");
+
+                # Don't want to actually _DO_ the update if -n specified
+                unless ( $state->{globaloptions}{-n} )
+                {
+                    print "Merged $dirpart\n";
+                    $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
+                    print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+                    my $kopts = kopts_from_path($filepart);
+                    $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
+                    print "/$filepart/1.$meta->{revision}//$kopts/\n";
+                }
+            }
+            elsif ( $return == 1 )
+            {
+                $log->info("Merged with conflicts");
+                print "E cvs update: conflicts found in $filename\n";
+                print "M C $filename\n";
+
+                # Don't want to actually _DO_ the update if -n specified
+                unless ( $state->{globaloptions}{-n} )
+                {
+                    print "Merged $dirpart\n";
+                    print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+                    my $kopts = kopts_from_path($filepart);
+                    print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
+                }
+            }
+            else
+            {
+                $log->warn("Merge failed");
+                next;
+            }
+
+            # Don't want to actually _DO_ the update if -n specified
+            unless ( $state->{globaloptions}{-n} )
+            {
+                # permissions
+                $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
+                print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
+
+                # transmit file, format is single integer on a line by itself (file
+                # size) followed by the file contents
+                # TODO : we should copy files in blocks
+                my $data = `cat $file_local`;
+                $log->debug("File size : " . length($data));
+                print length($data) . "\n";
+                print $data;
+            }
+
+            chdir "/";
+        }
+
+    }
+
+    print "ok\n";
+}
+
+sub req_ci
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("ci");
+
+    #$log->debug("State : " . Dumper($state));
+
+    $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
+
+    if ( $state->{method} eq 'pserver')
+    {
+        print "error 1 pserver access cannot commit\n";
+        exit;
+    }
+
+    if ( -e $state->{CVSROOT} . "/index" )
+    {
+        $log->warn("file 'index' already exists in the git repository");
+        print "error 1 Index already exists in git repo\n";
+        exit;
+    }
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    my $tmpdir = tempdir ( DIR => $TEMP_DIR );
+    my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
+    $log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");
+
+    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+    $ENV{GIT_WORK_TREE} = ".";
+    $ENV{GIT_INDEX_FILE} = $file_index;
+
+    # Remember where the head was at the beginning.
+    my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
+    chomp $parenthash;
+    if ($parenthash !~ /^[0-9a-f]{40}$/) {
+           print "error 1 pserver cannot find the current HEAD of module";
+           exit;
+    }
+
+    chdir $tmpdir;
+
+    # populate the temporary index
+    system("git-read-tree", $parenthash);
+    unless ($? == 0)
+    {
+       die "Error running git-read-tree $state->{module} $file_index $!";
+    }
+    $log->info("Created index '$file_index' for head $state->{module} - exit status $?");
+
+    my @committedfiles = ();
+    my %oldmeta;
+
+    # foreach file specified on the command line ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        my $committedfile = $filename;
+        $filename = filecleanup($filename);
+
+        next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
+
+        my $meta = $updater->getmeta($filename);
+       $oldmeta{$filename} = $meta;
+
+        my $wrev = revparse($filename);
+
+        my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+       # do a checkout of the file if it is part of this tree
+        if ($wrev) {
+            system('git-checkout-index', '-f', '-u', $filename);
+            unless ($? == 0) {
+                die "Error running git-checkout-index -f -u $filename : $!";
+            }
+        }
+
+        my $addflag = 0;
+        my $rmflag = 0;
+        $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
+        $addflag = 1 unless ( -e $filename );
+
+        # Do up to date checking
+        unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
+        {
+            # fail everything if an up to date check fails
+            print "error 1 Up to date check failed for $filename\n";
+            chdir "/";
+            exit;
+        }
+
+        push @committedfiles, $committedfile;
+        $log->info("Committing $filename");
+
+        system("mkdir","-p",$dirpart) unless ( -d $dirpart );
+
+        unless ( $rmflag )
+        {
+            $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
+            rename $state->{entries}{$filename}{modified_filename},$filename;
+
+            # Calculate modes to remove
+            my $invmode = "";
+            foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
+
+            $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
+            system("chmod","u+" .  $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
+        }
+
+        if ( $rmflag )
+        {
+            $log->info("Removing file '$filename'");
+            unlink($filename);
+            system("git-update-index", "--remove", $filename);
+        }
+        elsif ( $addflag )
+        {
+            $log->info("Adding file '$filename'");
+            system("git-update-index", "--add", $filename);
+        } else {
+            $log->info("Updating file '$filename'");
+            system("git-update-index", $filename);
+        }
+    }
+
+    unless ( scalar(@committedfiles) > 0 )
+    {
+        print "E No files to commit\n";
+        print "ok\n";
+        chdir "/";
+        return;
+    }
+
+    my $treehash = `git-write-tree`;
+    chomp $treehash;
+
+    $log->debug("Treehash : $treehash, Parenthash : $parenthash");
+
+    # write our commit message out if we have one ...
+    my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
+    print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
+    print $msg_fh "\n\nvia git-CVS emulator\n";
+    close $msg_fh;
+
+    my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`;
+    chomp($commithash);
+    $log->info("Commit hash : $commithash");
+
+    unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
+    {
+        $log->warn("Commit failed (Invalid commit hash)");
+        print "error 1 Commit failed (unknown reason)\n";
+        chdir "/";
+        exit;
+    }
+
+       ### Emulate git-receive-pack by running hooks/update
+       my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
+                       $parenthash, $commithash );
+       if( -x $hook[0] ) {
+               unless( system( @hook ) == 0 )
+               {
+                       $log->warn("Commit failed (update hook declined to update ref)");
+                       print "error 1 Commit failed (update hook declined)\n";
+                       chdir "/";
+                       exit;
+               }
+       }
+
+       ### Update the ref
+       if (system(qw(git update-ref -m), "cvsserver ci",
+                       "refs/heads/$state->{module}", $commithash, $parenthash)) {
+               $log->warn("update-ref for $state->{module} failed.");
+               print "error 1 Cannot commit -- update first\n";
+               exit;
+       }
+
+       ### Emulate git-receive-pack by running hooks/post-receive
+       my $hook = $ENV{GIT_DIR}.'hooks/post-receive';
+       if( -x $hook ) {
+               open(my $pipe, "| $hook") || die "can't fork $!";
+
+               local $SIG{PIPE} = sub { die 'pipe broke' };
+
+               print $pipe "$parenthash $commithash refs/heads/$state->{module}\n";
+
+               close $pipe || die "bad pipe: $! $?";
+       }
+
+       ### Then hooks/post-update
+       $hook = $ENV{GIT_DIR}.'hooks/post-update';
+       if (-x $hook) {
+               system($hook, "refs/heads/$state->{module}");
+       }
+
+    $updater->update();
+
+    # foreach file specified on the command line ...
+    foreach my $filename ( @committedfiles )
+    {
+        $filename = filecleanup($filename);
+
+        my $meta = $updater->getmeta($filename);
+       unless (defined $meta->{revision}) {
+         $meta->{revision} = 1;
+       }
+
+        my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
+
+        $log->debug("Checked-in $dirpart : $filename");
+
+       print "M $state->{CVSROOT}/$state->{module}/$filename,v  <--  $dirpart$filepart\n";
+        if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
+        {
+            print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
+            print "Remove-entry $dirpart\n";
+            print "$filename\n";
+        } else {
+            if ($meta->{revision} == 1) {
+               print "M initial revision: 1.1\n";
+            } else {
+               print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
+            }
+            print "Checked-in $dirpart\n";
+            print "$filename\n";
+            my $kopts = kopts_from_path($filepart);
+            print "/$filepart/1.$meta->{revision}//$kopts/\n";
+        }
+    }
+
+    chdir "/";
+    print "ok\n";
+}
+
+sub req_status
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("status");
+
+    $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
+    #$log->debug("status state : " . Dumper($state));
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    # if no files were specified, we need to work out what files we should be providing status on ...
+    argsfromdir($updater);
+
+    # foreach file specified on the command line ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        my $meta = $updater->getmeta($filename);
+        my $oldmeta = $meta;
+
+        my $wrev = revparse($filename);
+
+        # If the working copy is an old revision, lets get that version too for comparison.
+        if ( defined($wrev) and $wrev != $meta->{revision} )
+        {
+            $oldmeta = $updater->getmeta($filename, $wrev);
+        }
+
+        # TODO : All possible statuses aren't yet implemented
+        my $status;
+        # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
+        $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
+                                    and
+                                    ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
+                                      or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
+                                   );
+
+        # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
+        $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
+                                          and
+                                          ( $state->{entries}{$filename}{unchanged}
+                                            or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
+                                        );
+
+        # Need checkout if it exists in the repo but doesn't have a working copy
+        $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
+
+        # Locally modified if working copy and repo copy have the same revision but there are local changes
+        $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
+
+        # Needs Merge if working copy revision is less than repo copy and there are local changes
+        $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
+
+        $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
+        $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
+        $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
+        $status ||= "File had conflicts on merge" if ( 0 );
+
+        $status ||= "Unknown";
+
+        print "M ===================================================================\n";
+        print "M File: $filename\tStatus: $status\n";
+        if ( defined($state->{entries}{$filename}{revision}) )
+        {
+            print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
+        } else {
+            print "M Working revision:\tNo entry for $filename\n";
+        }
+        if ( defined($meta->{revision}) )
+        {
+            print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
+            print "M Sticky Tag:\t\t(none)\n";
+            print "M Sticky Date:\t\t(none)\n";
+            print "M Sticky Options:\t\t(none)\n";
+        } else {
+            print "M Repository revision:\tNo revision control file\n";
+        }
+        print "M\n";
+    }
+
+    print "ok\n";
+}
+
+sub req_diff
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("diff");
+
+    $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
+    #$log->debug("status state : " . Dumper($state));
+
+    my ($revision1, $revision2);
+    if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
+    {
+        $revision1 = $state->{opt}{r}[0];
+        $revision2 = $state->{opt}{r}[1];
+    } else {
+        $revision1 = $state->{opt}{r};
+    }
+
+    $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
+    $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
+
+    $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    # if no files were specified, we need to work out what files we should be providing status on ...
+    argsfromdir($updater);
+
+    # foreach file specified on the command line ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
+
+        my $wrev = revparse($filename);
+
+        # We need _something_ to diff against
+        next unless ( defined ( $wrev ) );
+
+        # if we have a -r switch, use it
+        if ( defined ( $revision1 ) )
+        {
+            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            $meta1 = $updater->getmeta($filename, $revision1);
+            unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
+            {
+                print "E File $filename at revision 1.$revision1 doesn't exist\n";
+                next;
+            }
+            transmitfile($meta1->{filehash}, $file1);
+        }
+        # otherwise we just use the working copy revision
+        else
+        {
+            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            $meta1 = $updater->getmeta($filename, $wrev);
+            transmitfile($meta1->{filehash}, $file1);
+        }
+
+        # if we have a second -r switch, use it too
+        if ( defined ( $revision2 ) )
+        {
+            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            $meta2 = $updater->getmeta($filename, $revision2);
+
+            unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
+            {
+                print "E File $filename at revision 1.$revision2 doesn't exist\n";
+                next;
+            }
+
+            transmitfile($meta2->{filehash}, $file2);
+        }
+        # otherwise we just use the working copy
+        else
+        {
+            $file2 = $state->{entries}{$filename}{modified_filename};
+        }
+
+        # if we have been given -r, and we don't have a $file2 yet, lets get one
+        if ( defined ( $revision1 ) and not defined ( $file2 ) )
+        {
+            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            $meta2 = $updater->getmeta($filename, $wrev);
+            transmitfile($meta2->{filehash}, $file2);
+        }
+
+        # We need to have retrieved something useful
+        next unless ( defined ( $meta1 ) );
+
+        # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
+        next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
+                  and
+                   ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
+                     or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
+                  );
+
+        # Apparently we only show diffs for locally modified files
+        next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
+
+        print "M Index: $filename\n";
+        print "M ===================================================================\n";
+        print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
+        print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
+        print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
+        print "M diff ";
+        foreach my $opt ( keys %{$state->{opt}} )
+        {
+            if ( ref $state->{opt}{$opt} eq "ARRAY" )
+            {
+                foreach my $value ( @{$state->{opt}{$opt}} )
+                {
+                    print "-$opt $value ";
+                }
+            } else {
+                print "-$opt ";
+                print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
+            }
+        }
+        print "$filename\n";
+
+        $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
+
+        ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
+
+        if ( exists $state->{opt}{u} )
+        {
+            system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
+        } else {
+            system("diff $file1 $file2 > $filediff");
+        }
+
+        while ( <$fh> )
+        {
+            print "M $_";
+        }
+        close $fh;
+    }
+
+    print "ok\n";
+}
+
+sub req_log
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("log");
+
+    $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
+    #$log->debug("log state : " . Dumper($state));
+
+    my ( $minrev, $maxrev );
+    if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
+    {
+        my $control = $2;
+        $minrev = $1;
+        $maxrev = $3;
+        $minrev =~ s/^1\.// if ( defined ( $minrev ) );
+        $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
+        $minrev++ if ( defined($minrev) and $control eq "::" );
+    }
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    # if no files were specified, we need to work out what files we should be providing status on ...
+    argsfromdir($updater);
+
+    # foreach file specified on the command line ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        my $headmeta = $updater->getmeta($filename);
+
+        my $revisions = $updater->getlog($filename);
+        my $totalrevisions = scalar(@$revisions);
+
+        if ( defined ( $minrev ) )
+        {
+            $log->debug("Removing revisions less than $minrev");
+            while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
+            {
+                pop @$revisions;
+            }
+        }
+        if ( defined ( $maxrev ) )
+        {
+            $log->debug("Removing revisions greater than $maxrev");
+            while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
+            {
+                shift @$revisions;
+            }
+        }
+
+        next unless ( scalar(@$revisions) );
+
+        print "M \n";
+        print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
+        print "M Working file: $filename\n";
+        print "M head: 1.$headmeta->{revision}\n";
+        print "M branch:\n";
+        print "M locks: strict\n";
+        print "M access list:\n";
+        print "M symbolic names:\n";
+        print "M keyword substitution: kv\n";
+        print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
+        print "M description:\n";
+
+        foreach my $revision ( @$revisions )
+        {
+            print "M ----------------------------\n";
+            print "M revision 1.$revision->{revision}\n";
+            # reformat the date for log output
+            $revision->{modified} = sprintf('%04d/%02d/%02d %s', $3, $DATE_LIST->{$2}, $1, $4 ) if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and defined($DATE_LIST->{$2}) );
+            $revision->{author} =~ s/\s+.*//;
+            $revision->{author} =~ s/^(.{8}).*/$1/;
+            print "M date: $revision->{modified};  author: $revision->{author};  state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . ";  lines: +2 -3\n";
+            my $commitmessage = $updater->commitmessage($revision->{commithash});
+            $commitmessage =~ s/^/M /mg;
+            print $commitmessage . "\n";
+        }
+        print "M =============================================================================\n";
+    }
+
+    print "ok\n";
+}
+
+sub req_annotate
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("annotate");
+
+    $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
+    #$log->debug("status state : " . Dumper($state));
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    # if no files were specified, we need to work out what files we should be providing annotate on ...
+    argsfromdir($updater);
+
+    # we'll need a temporary checkout dir
+    my $tmpdir = tempdir ( DIR => $TEMP_DIR );
+    my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
+    $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");
+
+    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+    $ENV{GIT_WORK_TREE} = ".";
+    $ENV{GIT_INDEX_FILE} = $file_index;
+
+    chdir $tmpdir;
+
+    # foreach file specified on the command line ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        my $meta = $updater->getmeta($filename);
+
+        next unless ( $meta->{revision} );
+
+       # get all the commits that this file was in
+       # in dense format -- aka skip dead revisions
+        my $revisions   = $updater->gethistorydense($filename);
+       my $lastseenin  = $revisions->[0][2];
+
+       # populate the temporary index based on the latest commit were we saw
+       # the file -- but do it cheaply without checking out any files
+       # TODO: if we got a revision from the client, use that instead
+       # to look up the commithash in sqlite (still good to default to
+       # the current head as we do now)
+       system("git-read-tree", $lastseenin);
+       unless ($? == 0)
+       {
+           print "E error running git-read-tree $lastseenin $file_index $!\n";
+           return;
+       }
+       $log->info("Created index '$file_index' with commit $lastseenin - exit status $?");
+
+        # do a checkout of the file
+        system('git-checkout-index', '-f', '-u', $filename);
+        unless ($? == 0) {
+            print "E error running git-checkout-index -f -u $filename : $!\n";
+            return;
+        }
+
+        $log->info("Annotate $filename");
+
+        # Prepare a file with the commits from the linearized
+        # history that annotate should know about. This prevents
+        # git-jsannotate telling us about commits we are hiding
+        # from the client.
+
+        my $a_hints = "$tmpdir/.annotate_hints";
+        if (!open(ANNOTATEHINTS, '>', $a_hints)) {
+            print "E failed to open '$a_hints' for writing: $!\n";
+            return;
+        }
+        for (my $i=0; $i < @$revisions; $i++)
+        {
+            print ANNOTATEHINTS $revisions->[$i][2];
+            if ($i+1 < @$revisions) { # have we got a parent?
+                print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
+            }
+            print ANNOTATEHINTS "\n";
+        }
+
+        print ANNOTATEHINTS "\n";
+        close ANNOTATEHINTS
+            or (print "E failed to write $a_hints: $!\n"), return;
+
+        my @cmd = (qw(git-annotate -l -S), $a_hints, $filename);
+        if (!open(ANNOTATE, "-|", @cmd)) {
+            print "E error invoking ". join(' ',@cmd) .": $!\n";
+            return;
+        }
+        my $metadata = {};
+        print "E Annotations for $filename\n";
+        print "E ***************\n";
+        while ( <ANNOTATE> )
+        {
+            if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
+            {
+                my $commithash = $1;
+                my $data = $2;
+                unless ( defined ( $metadata->{$commithash} ) )
+                {
+                    $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
+                    $metadata->{$commithash}{author} =~ s/\s+.*//;
+                    $metadata->{$commithash}{author} =~ s/^(.{8}).*/$1/;
+                    $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
+                }
+                printf("M 1.%-5d      (%-8s %10s): %s\n",
+                    $metadata->{$commithash}{revision},
+                    $metadata->{$commithash}{author},
+                    $metadata->{$commithash}{modified},
+                    $data
+                );
+            } else {
+                $log->warn("Error in annotate output! LINE: $_");
+                print "E Annotate error \n";
+                next;
+            }
+        }
+        close ANNOTATE;
+    }
+
+    # done; get out of the tempdir
+    chdir "/";
+
+    print "ok\n";
+
+}
+
+# This method takes the state->{arguments} array and produces two new arrays.
+# The first is $state->{args} which is everything before the '--' argument, and
+# the second is $state->{files} which is everything after it.
+sub argsplit
+{
+    $state->{args} = [];
+    $state->{files} = [];
+    $state->{opt} = {};
+
+    return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
+
+    my $type = shift;
+
+    if ( defined($type) )
+    {
+        my $opt = {};
+        $opt = { A => 0, N => 0, P => 0, R => 0, c => 0, f => 0, l => 0, n => 0, p => 0, s => 0, r => 1, D => 1, d => 1, k => 1, j => 1, } if ( $type eq "co" );
+        $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
+        $opt = { A => 0, P => 0, C => 0, d => 0, f => 0, l => 0, R => 0, p => 0, k => 1, r => 1, D => 1, j => 1, I => 1, W => 1 } if ( $type eq "update" );
+        $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
+        $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
+        $opt = { k => 1, m => 1 } if ( $type eq "add" );
+        $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
+        $opt = { l => 0, b => 0, h => 0, R => 0, t => 0, N => 0, S => 0, r => 1, d => 1, s => 1, w => 1 } if ( $type eq "log" );
+
+
+        while ( scalar ( @{$state->{arguments}} ) > 0 )
+        {
+            my $arg = shift @{$state->{arguments}};
+
+            next if ( $arg eq "--" );
+            next unless ( $arg =~ /\S/ );
+
+            # if the argument looks like a switch
+            if ( $arg =~ /^-(\w)(.*)/ )
+            {
+                # if it's a switch that takes an argument
+                if ( $opt->{$1} )
+                {
+                    # If this switch has already been provided
+                    if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
+                    {
+                        $state->{opt}{$1} = [ $state->{opt}{$1} ];
+                        if ( length($2) > 0 )
+                        {
+                            push @{$state->{opt}{$1}},$2;
+                        } else {
+                            push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
+                        }
+                    } else {
+                        # if there's extra data in the arg, use that as the argument for the switch
+                        if ( length($2) > 0 )
+                        {
+                            $state->{opt}{$1} = $2;
+                        } else {
+                            $state->{opt}{$1} = shift @{$state->{arguments}};
+                        }
+                    }
+                } else {
+                    $state->{opt}{$1} = undef;
+                }
+            }
+            else
+            {
+                push @{$state->{args}}, $arg;
+            }
+        }
+    }
+    else
+    {
+        my $mode = 0;
+
+        foreach my $value ( @{$state->{arguments}} )
+        {
+            if ( $value eq "--" )
+            {
+                $mode++;
+                next;
+            }
+            push @{$state->{args}}, $value if ( $mode == 0 );
+            push @{$state->{files}}, $value if ( $mode == 1 );
+        }
+    }
+}
+
+# This method uses $state->{directory} to populate $state->{args} with a list of filenames
+sub argsfromdir
+{
+    my $updater = shift;
+
+    $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
+
+    return if ( scalar ( @{$state->{args}} ) > 1 );
+
+    my @gethead = @{$updater->gethead};
+
+    # push added files
+    foreach my $file (keys %{$state->{entries}}) {
+       if ( exists $state->{entries}{$file}{revision} &&
+               $state->{entries}{$file}{revision} == 0 )
+       {
+           push @gethead, { name => $file, filehash => 'added' };
+       }
+    }
+
+    if ( scalar(@{$state->{args}}) == 1 )
+    {
+        my $arg = $state->{args}[0];
+        $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
+
+        $log->info("Only one arg specified, checking for directory expansion on '$arg'");
+
+        foreach my $file ( @gethead )
+        {
+            next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
+            next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg  );
+            push @{$state->{args}}, $file->{name};
+        }
+
+        shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
+    } else {
+        $log->info("Only one arg specified, populating file list automatically");
+
+        $state->{args} = [];
+
+        foreach my $file ( @gethead )
+        {
+            next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
+            next unless ( $file->{name} =~ s/^$state->{prependdir}// );
+            push @{$state->{args}}, $file->{name};
+        }
+    }
+}
+
+# This method cleans up the $state variable after a command that uses arguments has run
+sub statecleanup
+{
+    $state->{files} = [];
+    $state->{args} = [];
+    $state->{arguments} = [];
+    $state->{entries} = {};
+}
+
+sub revparse
+{
+    my $filename = shift;
+
+    return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
+
+    return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
+    return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
+
+    return undef;
+}
+
+# This method takes a file hash and does a CVS "file transfer" which transmits the
+# size of the file, and then the file contents.
+# If a second argument $targetfile is given, the file is instead written out to
+# a file by the name of $targetfile
+sub transmitfile
+{
+    my $filehash = shift;
+    my $targetfile = shift;
+
+    if ( defined ( $filehash ) and $filehash eq "deleted" )
+    {
+        $log->warn("filehash is 'deleted'");
+        return;
+    }
+
+    die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
+
+    my $type = `git-cat-file -t $filehash`;
+    chomp $type;
+
+    die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
+
+    my $size = `git-cat-file -s $filehash`;
+    chomp $size;
+
+    $log->debug("transmitfile($filehash) size=$size, type=$type");
+
+    if ( open my $fh, '-|', "git-cat-file", "blob", $filehash )
+    {
+        if ( defined ( $targetfile ) )
+        {
+            open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
+            print NEWFILE $_ while ( <$fh> );
+            close NEWFILE or die("Failed to write '$targetfile': $!");
+        } else {
+            print "$size\n";
+            print while ( <$fh> );
+        }
+        close $fh or die ("Couldn't close filehandle for transmitfile(): $!");
+    } else {
+        die("Couldn't execute git-cat-file");
+    }
+}
+
+# This method takes a file name, and returns ( $dirpart, $filepart ) which
+# refers to the directory portion and the file portion of the filename
+# respectively
+sub filenamesplit
+{
+    my $filename = shift;
+    my $fixforlocaldir = shift;
+
+    my ( $filepart, $dirpart ) = ( $filename, "." );
+    ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
+    $dirpart .= "/";
+
+    if ( $fixforlocaldir )
+    {
+        $dirpart =~ s/^$state->{prependdir}//;
+    }
+
+    return ( $filepart, $dirpart );
+}
+
+sub filecleanup
+{
+    my $filename = shift;
+
+    return undef unless(defined($filename));
+    if ( $filename =~ /^\// )
+    {
+        print "E absolute filenames '$filename' not supported by server\n";
+        return undef;
+    }
+
+    $filename =~ s/^\.\///g;
+    $filename = $state->{prependdir} . $filename;
+    return $filename;
+}
+
+# Given a path, this function returns a string containing the kopts
+# that should go into that path's Entries line.  For example, a binary
+# file should get -kb.
+sub kopts_from_path
+{
+       my ($path) = @_;
+
+       # Once it exists, the git attributes system should be used to look up
+       # what attributes apply to this path.
+
+       # Until then, take the setting from the config file
+    unless ( defined ( $cfg->{gitcvs}{allbinary} ) and $cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i )
+    {
+               # Return "" to give no special treatment to any path
+               return "";
+    } else {
+               # Alternatively, to have all files treated as if they are binary (which
+               # is more like git itself), always return the "-kb" option
+               return "-kb";
+    }
+}
+
+package GITCVS::log;
+
+####
+#### Copyright The Open University UK - 2006.
+####
+#### Authors: Martyn Smith    <martyn@catalyst.net.nz>
+####          Martin Langhoff <martin@catalyst.net.nz>
+####
+####
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+GITCVS::log
+
+=head1 DESCRIPTION
+
+This module provides very crude logging with a similar interface to
+Log::Log4perl
+
+=head1 METHODS
+
+=cut
+
+=head2 new
+
+Creates a new log object, optionally you can specify a filename here to
+indicate the file to log to. If no log file is specified, you can specify one
+later with method setfile, or indicate you no longer want logging with method
+nofile.
+
+Until one of these methods is called, all log calls will buffer messages ready
+to write out.
+
+=cut
+sub new
+{
+    my $class = shift;
+    my $filename = shift;
+
+    my $self = {};
+
+    bless $self, $class;
+
+    if ( defined ( $filename ) )
+    {
+        open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
+    }
+
+    return $self;
+}
+
+=head2 setfile
+
+This methods takes a filename, and attempts to open that file as the log file.
+If successful, all buffered data is written out to the file, and any further
+logging is written directly to the file.
+
+=cut
+sub setfile
+{
+    my $self = shift;
+    my $filename = shift;
+
+    if ( defined ( $filename ) )
+    {
+        open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
+    }
+
+    return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
+
+    while ( my $line = shift @{$self->{buffer}} )
+    {
+        print {$self->{fh}} $line;
+    }
+}
+
+=head2 nofile
+
+This method indicates no logging is going to be used. It flushes any entries in
+the internal buffer, and sets a flag to ensure no further data is put there.
+
+=cut
+sub nofile
+{
+    my $self = shift;
+
+    $self->{nolog} = 1;
+
+    return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
+
+    $self->{buffer} = [];
+}
+
+=head2 _logopen
+
+Internal method. Returns true if the log file is open, false otherwise.
+
+=cut
+sub _logopen
+{
+    my $self = shift;
+
+    return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
+    return 0;
+}
+
+=head2 debug info warn fatal
+
+These four methods are wrappers to _log. They provide the actual interface for
+logging data.
+
+=cut
+sub debug { my $self = shift; $self->_log("debug", @_); }
+sub info  { my $self = shift; $self->_log("info" , @_); }
+sub warn  { my $self = shift; $self->_log("warn" , @_); }
+sub fatal { my $self = shift; $self->_log("fatal", @_); }
+
+=head2 _log
+
+This is an internal method called by the logging functions. It generates a
+timestamp and pushes the logged line either to file, or internal buffer.
+
+=cut
+sub _log
+{
+    my $self = shift;
+    my $level = shift;
+
+    return if ( $self->{nolog} );
+
+    my @time = localtime;
+    my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
+        $time[5] + 1900,
+        $time[4] + 1,
+        $time[3],
+        $time[2],
+        $time[1],
+        $time[0],
+        uc $level,
+    );
+
+    if ( $self->_logopen )
+    {
+        print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
+    } else {
+        push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
+    }
+}
+
+=head2 DESTROY
+
+This method simply closes the file handle if one is open
+
+=cut
+sub DESTROY
+{
+    my $self = shift;
+
+    if ( $self->_logopen )
+    {
+        close $self->{fh};
+    }
+}
+
+package GITCVS::updater;
+
+####
+#### Copyright The Open University UK - 2006.
+####
+#### Authors: Martyn Smith    <martyn@catalyst.net.nz>
+####          Martin Langhoff <martin@catalyst.net.nz>
+####
+####
+
+use strict;
+use warnings;
+use DBI;
+
+=head1 METHODS
+
+=cut
+
+=head2 new
+
+=cut
+sub new
+{
+    my $class = shift;
+    my $config = shift;
+    my $module = shift;
+    my $log = shift;
+
+    die "Need to specify a git repository" unless ( defined($config) and -d $config );
+    die "Need to specify a module" unless ( defined($module) );
+
+    $class = ref($class) || $class;
+
+    my $self = {};
+
+    bless $self, $class;
+
+    $self->{module} = $module;
+    $self->{git_path} = $config . "/";
+
+    $self->{log} = $log;
+
+    die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
+
+    $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
+        $cfg->{gitcvs}{dbdriver} || "SQLite";
+    $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
+        $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
+    $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
+        $cfg->{gitcvs}{dbuser} || "";
+    $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
+        $cfg->{gitcvs}{dbpass} || "";
+    my %mapping = ( m => $module,
+                    a => $state->{method},
+                    u => getlogin || getpwuid($<) || $<,
+                    G => $self->{git_path},
+                    g => mangle_dirname($self->{git_path}),
+                    );
+    $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
+    $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
+
+    die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
+    die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
+    $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
+                                $self->{dbuser},
+                                $self->{dbpass});
+    die "Error connecting to database\n" unless defined $self->{dbh};
+
+    $self->{tables} = {};
+    foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
+    {
+        $self->{tables}{$table} = 1;
+    }
+
+    # Construct the revision table if required
+    unless ( $self->{tables}{revision} )
+    {
+        $self->{dbh}->do("
+            CREATE TABLE revision (
+                name       TEXT NOT NULL,
+                revision   INTEGER NOT NULL,
+                filehash   TEXT NOT NULL,
+                commithash TEXT NOT NULL,
+                author     TEXT NOT NULL,
+                modified   TEXT NOT NULL,
+                mode       TEXT NOT NULL
+            )
+        ");
+        $self->{dbh}->do("
+            CREATE INDEX revision_ix1
+            ON revision (name,revision)
+        ");
+        $self->{dbh}->do("
+            CREATE INDEX revision_ix2
+            ON revision (name,commithash)
+        ");
+    }
+
+    # Construct the head table if required
+    unless ( $self->{tables}{head} )
+    {
+        $self->{dbh}->do("
+            CREATE TABLE head (
+                name       TEXT NOT NULL,
+                revision   INTEGER NOT NULL,
+                filehash   TEXT NOT NULL,
+                commithash TEXT NOT NULL,
+                author     TEXT NOT NULL,
+                modified   TEXT NOT NULL,
+                mode       TEXT NOT NULL
+            )
+        ");
+        $self->{dbh}->do("
+            CREATE INDEX head_ix1
+            ON head (name)
+        ");
+    }
+
+    # Construct the properties table if required
+    unless ( $self->{tables}{properties} )
+    {
+        $self->{dbh}->do("
+            CREATE TABLE properties (
+                key        TEXT NOT NULL PRIMARY KEY,
+                value      TEXT
+            )
+        ");
+    }
+
+    # Construct the commitmsgs table if required
+    unless ( $self->{tables}{commitmsgs} )
+    {
+        $self->{dbh}->do("
+            CREATE TABLE commitmsgs (
+                key        TEXT NOT NULL PRIMARY KEY,
+                value      TEXT
+            )
+        ");
+    }
+
+    return $self;
+}
+
+=head2 update
+
+=cut
+sub update
+{
+    my $self = shift;
+
+    # first lets get the commit list
+    $ENV{GIT_DIR} = $self->{git_path};
+
+    my $commitsha1 = `git rev-parse $self->{module}`;
+    chomp $commitsha1;
+
+    my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
+    unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
+    {
+        die("Invalid module '$self->{module}'");
+    }
+
+
+    my $git_log;
+    my $lastcommit = $self->_get_prop("last_commit");
+
+    if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
+         return 1;
+    }
+
+    # Start exclusive lock here...
+    $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
+
+    # TODO: log processing is memory bound
+    # if we can parse into a 2nd file that is in reverse order
+    # we can probably do something really efficient
+    my @git_log_params = ('--pretty', '--parents', '--topo-order');
+
+    if (defined $lastcommit) {
+        push @git_log_params, "$lastcommit..$self->{module}";
+    } else {
+        push @git_log_params, $self->{module};
+    }
+    # git-rev-list is the backend / plumbing version of git-log
+    open(GITLOG, '-|', 'git-rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
+
+    my @commits;
+
+    my %commit = ();
+
+    while ( <GITLOG> )
+    {
+        chomp;
+        if (m/^commit\s+(.*)$/) {
+            # on ^commit lines put the just seen commit in the stack
+            # and prime things for the next one
+            if (keys %commit) {
+                my %copy = %commit;
+                unshift @commits, \%copy;
+                %commit = ();
+            }
+            my @parents = split(m/\s+/, $1);
+            $commit{hash} = shift @parents;
+            $commit{parents} = \@parents;
+        } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
+            # on rfc822-like lines seen before we see any message,
+            # lowercase the entry and put it in the hash as key-value
+            $commit{lc($1)} = $2;
+        } else {
+            # message lines - skip initial empty line
+            # and trim whitespace
+            if (!exists($commit{message}) && m/^\s*$/) {
+                # define it to mark the end of headers
+                $commit{message} = '';
+                next;
+            }
+            s/^\s+//; s/\s+$//; # trim ws
+            $commit{message} .= $_ . "\n";
+        }
+    }
+    close GITLOG;
+
+    unshift @commits, \%commit if ( keys %commit );
+
+    # Now all the commits are in the @commits bucket
+    # ordered by time DESC. for each commit that needs processing,
+    # determine whether it's following the last head we've seen or if
+    # it's on its own branch, grab a file list, and add whatever's changed
+    # NOTE: $lastcommit refers to the last commit from previous run
+    #       $lastpicked is the last commit we picked in this run
+    my $lastpicked;
+    my $head = {};
+    if (defined $lastcommit) {
+        $lastpicked = $lastcommit;
+    }
+
+    my $committotal = scalar(@commits);
+    my $commitcount = 0;
+
+    # Load the head table into $head (for cached lookups during the update process)
+    foreach my $file ( @{$self->gethead()} )
+    {
+        $head->{$file->{name}} = $file;
+    }
+
+    foreach my $commit ( @commits )
+    {
+        $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
+        if (defined $lastpicked)
+        {
+            if (!in_array($lastpicked, @{$commit->{parents}}))
+            {
+                # skip, we'll see this delta
+                # as part of a merge later
+                # warn "skipping off-track  $commit->{hash}\n";
+                next;
+            } elsif (@{$commit->{parents}} > 1) {
+                # it is a merge commit, for each parent that is
+                # not $lastpicked, see if we can get a log
+                # from the merge-base to that parent to put it
+                # in the message as a merge summary.
+                my @parents = @{$commit->{parents}};
+                foreach my $parent (@parents) {
+                    # git-merge-base can potentially (but rarely) throw
+                    # several candidate merge bases. let's assume
+                    # that the first one is the best one.
+                    if ($parent eq $lastpicked) {
+                        next;
+                    }
+                    my $base = safe_pipe_capture('git-merge-base',
+                                                $lastpicked, $parent);
+                    chomp $base;
+                    if ($base) {
+                        my @merged;
+                        # print "want to log between  $base $parent \n";
+                        open(GITLOG, '-|', 'git-log', "$base..$parent")
+                         or die "Cannot call git-log: $!";
+                        my $mergedhash;
+                        while (<GITLOG>) {
+                            chomp;
+                            if (!defined $mergedhash) {
+                                if (m/^commit\s+(.+)$/) {
+                                    $mergedhash = $1;
+                                } else {
+                                    next;
+                                }
+                            } else {
+                                # grab the first line that looks non-rfc822
+                                # aka has content after leading space
+                                if (m/^\s+(\S.*)$/) {
+                                    my $title = $1;
+                                    $title = substr($title,0,100); # truncate
+                                    unshift @merged, "$mergedhash $title";
+                                    undef $mergedhash;
+                                }
+                            }
+                        }
+                        close GITLOG;
+                        if (@merged) {
+                            $commit->{mergemsg} = $commit->{message};
+                            $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
+                            foreach my $summary (@merged) {
+                                $commit->{mergemsg} .= "\t$summary\n";
+                            }
+                            $commit->{mergemsg} .= "\n\n";
+                            # print "Message for $commit->{hash} \n$commit->{mergemsg}";
+                        }
+                    }
+                }
+            }
+        }
+
+        # convert the date to CVS-happy format
+        $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
+
+        if ( defined ( $lastpicked ) )
+        {
+            my $filepipe = open(FILELIST, '-|', 'git-diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
+           local ($/) = "\0";
+            while ( <FILELIST> )
+            {
+               chomp;
+                unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
+                {
+                    die("Couldn't process git-diff-tree line : $_");
+                }
+               my ($mode, $hash, $change) = ($1, $2, $3);
+               my $name = <FILELIST>;
+               chomp($name);
+
+                # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
+
+                my $git_perms = "";
+                $git_perms .= "r" if ( $mode & 4 );
+                $git_perms .= "w" if ( $mode & 2 );
+                $git_perms .= "x" if ( $mode & 1 );
+                $git_perms = "rw" if ( $git_perms eq "" );
+
+                if ( $change eq "D" )
+                {
+                    #$log->debug("DELETE   $name");
+                    $head->{$name} = {
+                        name => $name,
+                        revision => $head->{$name}{revision} + 1,
+                        filehash => "deleted",
+                        commithash => $commit->{hash},
+                        modified => $commit->{date},
+                        author => $commit->{author},
+                        mode => $git_perms,
+                    };
+                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                }
+                elsif ( $change eq "M" )
+                {
+                    #$log->debug("MODIFIED $name");
+                    $head->{$name} = {
+                        name => $name,
+                        revision => $head->{$name}{revision} + 1,
+                        filehash => $hash,
+                        commithash => $commit->{hash},
+                        modified => $commit->{date},
+                        author => $commit->{author},
+                        mode => $git_perms,
+                    };
+                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                }
+                elsif ( $change eq "A" )
+                {
+                    #$log->debug("ADDED    $name");
+                    $head->{$name} = {
+                        name => $name,
+                        revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
+                        filehash => $hash,
+                        commithash => $commit->{hash},
+                        modified => $commit->{date},
+                        author => $commit->{author},
+                        mode => $git_perms,
+                    };
+                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                }
+                else
+                {
+                    $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
+                    die;
+                }
+            }
+            close FILELIST;
+        } else {
+            # this is used to detect files removed from the repo
+            my $seen_files = {};
+
+            my $filepipe = open(FILELIST, '-|', 'git-ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
+           local $/ = "\0";
+            while ( <FILELIST> )
+            {
+               chomp;
+                unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+                {
+                    die("Couldn't process git-ls-tree line : $_");
+                }
+
+                my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
+
+                $seen_files->{$git_filename} = 1;
+
+                my ( $oldhash, $oldrevision, $oldmode ) = (
+                    $head->{$git_filename}{filehash},
+                    $head->{$git_filename}{revision},
+                    $head->{$git_filename}{mode}
+                );
+
+                if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
+                {
+                    $git_perms = "";
+                    $git_perms .= "r" if ( $1 & 4 );
+                    $git_perms .= "w" if ( $1 & 2 );
+                    $git_perms .= "x" if ( $1 & 1 );
+                } else {
+                    $git_perms = "rw";
+                }
+
+                # unless the file exists with the same hash, we need to update it ...
+                unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
+                {
+                    my $newrevision = ( $oldrevision or 0 ) + 1;
+
+                    $head->{$git_filename} = {
+                        name => $git_filename,
+                        revision => $newrevision,
+                        filehash => $git_hash,
+                        commithash => $commit->{hash},
+                        modified => $commit->{date},
+                        author => $commit->{author},
+                        mode => $git_perms,
+                    };
+
+
+                    $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                }
+            }
+            close FILELIST;
+
+            # Detect deleted files
+            foreach my $file ( keys %$head )
+            {
+                unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
+                {
+                    $head->{$file}{revision}++;
+                    $head->{$file}{filehash} = "deleted";
+                    $head->{$file}{commithash} = $commit->{hash};
+                    $head->{$file}{modified} = $commit->{date};
+                    $head->{$file}{author} = $commit->{author};
+
+                    $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
+                }
+            }
+            # END : "Detect deleted files"
+        }
+
+
+        if (exists $commit->{mergemsg})
+        {
+            $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
+        }
+
+        $lastpicked = $commit->{hash};
+
+        $self->_set_prop("last_commit", $commit->{hash});
+    }
+
+    $self->delete_head();
+    foreach my $file ( keys %$head )
+    {
+        $self->insert_head(
+            $file,
+            $head->{$file}{revision},
+            $head->{$file}{filehash},
+            $head->{$file}{commithash},
+            $head->{$file}{modified},
+            $head->{$file}{author},
+            $head->{$file}{mode},
+        );
+    }
+    # invalidate the gethead cache
+    $self->{gethead_cache} = undef;
+
+
+    # Ending exclusive lock here
+    $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
+}
+
+sub insert_rev
+{
+    my $self = shift;
+    my $name = shift;
+    my $revision = shift;
+    my $filehash = shift;
+    my $commithash = shift;
+    my $modified = shift;
+    my $author = shift;
+    my $mode = shift;
+
+    my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
+    $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
+}
+
+sub insert_mergelog
+{
+    my $self = shift;
+    my $key = shift;
+    my $value = shift;
+
+    my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);
+    $insert_mergelog->execute($key, $value);
+}
+
+sub delete_head
+{
+    my $self = shift;
+
+    my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM head",{},1);
+    $delete_head->execute();
+}
+
+sub insert_head
+{
+    my $self = shift;
+    my $name = shift;
+    my $revision = shift;
+    my $filehash = shift;
+    my $commithash = shift;
+    my $modified = shift;
+    my $author = shift;
+    my $mode = shift;
+
+    my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
+    $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
+}
+
+sub _headrev
+{
+    my $self = shift;
+    my $filename = shift;
+
+    my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);
+    $db_query->execute($filename);
+    my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
+
+    return ( $hash, $revision, $mode );
+}
+
+sub _get_prop
+{
+    my $self = shift;
+    my $key = shift;
+
+    my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);
+    $db_query->execute($key);
+    my ( $value ) = $db_query->fetchrow_array;
+
+    return $value;
+}
+
+sub _set_prop
+{
+    my $self = shift;
+    my $key = shift;
+    my $value = shift;
+
+    my $db_query = $self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);
+    $db_query->execute($value, $key);
+
+    unless ( $db_query->rows )
+    {
+        $db_query = $self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);
+        $db_query->execute($key, $value);
+    }
+
+    return $value;
+}
+
+=head2 gethead
+
+=cut
+
+sub gethead
+{
+    my $self = shift;
+
+    return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
+
+    my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);
+    $db_query->execute();
+
+    my $tree = [];
+    while ( my $file = $db_query->fetchrow_hashref )
+    {
+        push @$tree, $file;
+    }
+
+    $self->{gethead_cache} = $tree;
+
+    return $tree;
+}
+
+=head2 getlog
+
+=cut
+
+sub getlog
+{
+    my $self = shift;
+    my $filename = shift;
+
+    my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
+    $db_query->execute($filename);
+
+    my $tree = [];
+    while ( my $file = $db_query->fetchrow_hashref )
+    {
+        push @$tree, $file;
+    }
+
+    return $tree;
+}
+
+=head2 getmeta
+
+This function takes a filename (with path) argument and returns a hashref of
+metadata for that file.
+
+=cut
+
+sub getmeta
+{
+    my $self = shift;
+    my $filename = shift;
+    my $revision = shift;
+
+    my $db_query;
+    if ( defined($revision) and $revision =~ /^\d+$/ )
+    {
+        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);
+        $db_query->execute($filename, $revision);
+    }
+    elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
+    {
+        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);
+        $db_query->execute($filename, $revision);
+    } else {
+        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);
+        $db_query->execute($filename);
+    }
+
+    return $db_query->fetchrow_hashref;
+}
+
+=head2 commitmessage
+
+this function takes a commithash and returns the commit message for that commit
+
+=cut
+sub commitmessage
+{
+    my $self = shift;
+    my $commithash = shift;
+
+    die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
+
+    my $db_query;
+    $db_query = $self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);
+    $db_query->execute($commithash);
+
+    my ( $message ) = $db_query->fetchrow_array;
+
+    if ( defined ( $message ) )
+    {
+        $message .= " " if ( $message =~ /\n$/ );
+        return $message;
+    }
+
+    my @lines = safe_pipe_capture("git-cat-file", "commit", $commithash);
+    shift @lines while ( $lines[0] =~ /\S/ );
+    $message = join("",@lines);
+    $message .= " " if ( $message =~ /\n$/ );
+    return $message;
+}
+
+=head2 gethistory
+
+This function takes a filename (with path) argument and returns an arrayofarrays
+containing revision,filehash,commithash ordered by revision descending
+
+=cut
+sub gethistory
+{
+    my $self = shift;
+    my $filename = shift;
+
+    my $db_query;
+    $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
+    $db_query->execute($filename);
+
+    return $db_query->fetchall_arrayref;
+}
+
+=head2 gethistorydense
+
+This function takes a filename (with path) argument and returns an arrayofarrays
+containing revision,filehash,commithash ordered by revision descending.
+
+This version of gethistory skips deleted entries -- so it is useful for annotate.
+The 'dense' part is a reference to a '--dense' option available for git-rev-list
+and other git tools that depend on it.
+
+=cut
+sub gethistorydense
+{
+    my $self = shift;
+    my $filename = shift;
+
+    my $db_query;
+    $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
+    $db_query->execute($filename);
+
+    return $db_query->fetchall_arrayref;
+}
+
+=head2 in_array()
+
+from Array::PAT - mimics the in_array() function
+found in PHP. Yuck but works for small arrays.
+
+=cut
+sub in_array
+{
+    my ($check, @array) = @_;
+    my $retval = 0;
+    foreach my $test (@array){
+        if($check eq $test){
+            $retval =  1;
+        }
+    }
+    return $retval;
+}
+
+=head2 safe_pipe_capture
+
+an alternative to `command` that allows input to be passed as an array
+to work around shell problems with weird characters in arguments
+
+=cut
+sub safe_pipe_capture {
+
+    my @output;
+
+    if (my $pid = open my $child, '-|') {
+        @output = (<$child>);
+        close $child or die join(' ',@_).": $! $?";
+    } else {
+        exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
+    }
+    return wantarray ? @output : join('',@output);
+}
+
+=head2 mangle_dirname
+
+create a string from a directory name that is suitable to use as
+part of a filename, mainly by converting all chars except \w.- to _
+
+=cut
+sub mangle_dirname {
+    my $dirname = shift;
+    return unless defined $dirname;
+
+    $dirname =~ s/[^\w.-]/_/g;
+
+    return $dirname;
+}
+
+1;
diff --git a/git-filter-branch.sh b/git-filter-branch.sh
new file mode 100755 (executable)
index 0000000..29d35fd
--- /dev/null
@@ -0,0 +1,438 @@
+#!/bin/sh
+#
+# Rewrite revision history
+# Copyright (c) Petr Baudis, 2006
+# Minimal changes to "port" it to core-git (c) Johannes Schindelin, 2007
+#
+# Lets you rewrite the revision history of the current branch, creating
+# a new branch. You can specify a number of filters to modify the commits,
+# files and trees.
+
+# The following functions will also be available in the commit filter:
+
+functions=$(cat << \EOF
+warn () {
+        echo "$*" >&2
+}
+
+map()
+{
+       # if it was not rewritten, take the original
+       if test -r "$workdir/../map/$1"
+       then
+               cat "$workdir/../map/$1"
+       else
+               echo "$1"
+       fi
+}
+
+# if you run 'skip_commit "$@"' in a commit filter, it will print
+# the (mapped) parents, effectively skipping the commit.
+
+skip_commit()
+{
+       shift;
+       while [ -n "$1" ];
+       do
+               shift;
+               map "$1";
+               shift;
+       done;
+}
+
+# override die(): this version puts in an extra line break, so that
+# the progress is still visible
+
+die()
+{
+       echo >&2
+       echo "$*" >&2
+       exit 1
+}
+EOF
+)
+
+eval "$functions"
+
+# When piped a commit, output a script to set the ident of either
+# "author" or "committer
+
+set_ident () {
+       lid="$(echo "$1" | tr "A-Z" "a-z")"
+       uid="$(echo "$1" | tr "a-z" "A-Z")"
+       pick_id_script='
+               /^'$lid' /{
+                       s/'\''/'\''\\'\'\''/g
+                       h
+                       s/^'$lid' \([^<]*\) <[^>]*> .*$/\1/
+                       s/'\''/'\''\'\'\''/g
+                       s/.*/GIT_'$uid'_NAME='\''&'\''; export GIT_'$uid'_NAME/p
+
+                       g
+                       s/^'$lid' [^<]* <\([^>]*\)> .*$/\1/
+                       s/'\''/'\''\'\'\''/g
+                       s/.*/GIT_'$uid'_EMAIL='\''&'\''; export GIT_'$uid'_EMAIL/p
+
+                       g
+                       s/^'$lid' [^<]* <[^>]*> \(.*\)$/\1/
+                       s/'\''/'\''\'\'\''/g
+                       s/.*/GIT_'$uid'_DATE='\''&'\''; export GIT_'$uid'_DATE/p
+
+                       q
+               }
+       '
+
+       LANG=C LC_ALL=C sed -ne "$pick_id_script"
+       # Ensure non-empty id name.
+       echo "case \"\$GIT_${uid}_NAME\" in \"\") GIT_${uid}_NAME=\"\${GIT_${uid}_EMAIL%%@*}\" && export GIT_${uid}_NAME;; esac"
+}
+
+USAGE="[--env-filter <command>] [--tree-filter <command>] \
+[--index-filter <command>] [--parent-filter <command>] \
+[--msg-filter <command>] [--commit-filter <command>] \
+[--tag-name-filter <command>] [--subdirectory-filter <directory>] \
+[--original <namespace>] [-d <directory>] [-f | --force] \
+[<rev-list options>...]"
+
+OPTIONS_SPEC=
+. git-sh-setup
+
+git diff-files --quiet &&
+       git diff-index --cached --quiet HEAD -- ||
+       die "Cannot rewrite branch(es) with a dirty working directory."
+
+tempdir=.git-rewrite
+filter_env=
+filter_tree=
+filter_index=
+filter_parent=
+filter_msg=cat
+filter_commit='git commit-tree "$@"'
+filter_tag_name=
+filter_subdir=
+orig_namespace=refs/original/
+force=
+while :
+do
+       test $# = 0 && usage
+       case "$1" in
+       --)
+               shift
+               break
+               ;;
+       --force|-f)
+               shift
+               force=t
+               continue
+               ;;
+       -*)
+               ;;
+       *)
+               break;
+       esac
+
+       # all switches take one argument
+       ARG="$1"
+       case "$#" in 1) usage ;; esac
+       shift
+       OPTARG="$1"
+       shift
+
+       case "$ARG" in
+       -d)
+               tempdir="$OPTARG"
+               ;;
+       --env-filter)
+               filter_env="$OPTARG"
+               ;;
+       --tree-filter)
+               filter_tree="$OPTARG"
+               ;;
+       --index-filter)
+               filter_index="$OPTARG"
+               ;;
+       --parent-filter)
+               filter_parent="$OPTARG"
+               ;;
+       --msg-filter)
+               filter_msg="$OPTARG"
+               ;;
+       --commit-filter)
+               filter_commit="$functions; $OPTARG"
+               ;;
+       --tag-name-filter)
+               filter_tag_name="$OPTARG"
+               ;;
+       --subdirectory-filter)
+               filter_subdir="$OPTARG"
+               ;;
+       --original)
+               orig_namespace=$(expr "$OPTARG/" : '\(.*[^/]\)/*$')/
+               ;;
+       *)
+               usage
+               ;;
+       esac
+done
+
+case "$force" in
+t)
+       rm -rf "$tempdir"
+;;
+'')
+       test -d "$tempdir" &&
+               die "$tempdir already exists, please remove it"
+esac
+mkdir -p "$tempdir/t" &&
+tempdir="$(cd "$tempdir"; pwd)" &&
+cd "$tempdir/t" &&
+workdir="$(pwd)" ||
+die ""
+
+# Make sure refs/original is empty
+git for-each-ref > "$tempdir"/backup-refs
+while read sha1 type name
+do
+       case "$force,$name" in
+       ,$orig_namespace*)
+               die "Namespace $orig_namespace not empty"
+       ;;
+       t,$orig_namespace*)
+               git update-ref -d "$name" $sha1
+       ;;
+       esac
+done < "$tempdir"/backup-refs
+
+ORIG_GIT_DIR="$GIT_DIR"
+ORIG_GIT_WORK_TREE="$GIT_WORK_TREE"
+ORIG_GIT_INDEX_FILE="$GIT_INDEX_FILE"
+GIT_WORK_TREE=.
+export GIT_DIR GIT_WORK_TREE
+
+# These refs should be updated if their heads were rewritten
+
+git rev-parse --revs-only --symbolic "$@" |
+while read ref
+do
+       # normalize ref
+       case "$ref" in
+       HEAD)
+               ref="$(git symbolic-ref "$ref")"
+       ;;
+       refs/*)
+       ;;
+       *)
+               ref="$(git for-each-ref --format='%(refname)' |
+                       grep /"$ref")"
+       esac
+
+       git check-ref-format "$ref" && echo "$ref"
+done > "$tempdir"/heads
+
+test -s "$tempdir"/heads ||
+       die "Which ref do you want to rewrite?"
+
+GIT_INDEX_FILE="$(pwd)/../index"
+export GIT_INDEX_FILE
+git read-tree || die "Could not seed the index"
+
+ret=0
+
+# map old->new commit ids for rewriting parents
+mkdir ../map || die "Could not create map/ directory"
+
+case "$filter_subdir" in
+"")
+       git rev-list --reverse --topo-order --default HEAD \
+               --parents "$@"
+       ;;
+*)
+       git rev-list --reverse --topo-order --default HEAD \
+               --parents --full-history "$@" -- "$filter_subdir"
+esac > ../revs || die "Could not get the commits"
+commits=$(wc -l <../revs | tr -d " ")
+
+test $commits -eq 0 && die "Found nothing to rewrite"
+
+# Rewrite the commits
+
+i=0
+while read commit parents; do
+       i=$(($i+1))
+       printf "\rRewrite $commit ($i/$commits)"
+
+       case "$filter_subdir" in
+       "")
+               git read-tree -i -m $commit
+               ;;
+       *)
+               git read-tree -i -m $commit:"$filter_subdir"
+       esac || die "Could not initialize the index"
+
+       GIT_COMMIT=$commit
+       export GIT_COMMIT
+       git cat-file commit "$commit" >../commit ||
+               die "Cannot read commit $commit"
+
+       eval "$(set_ident AUTHOR <../commit)" ||
+               die "setting author failed for commit $commit"
+       eval "$(set_ident COMMITTER <../commit)" ||
+               die "setting committer failed for commit $commit"
+       eval "$filter_env" < /dev/null ||
+               die "env filter failed: $filter_env"
+
+       if [ "$filter_tree" ]; then
+               git checkout-index -f -u -a ||
+                       die "Could not checkout the index"
+               # files that $commit removed are now still in the working tree;
+               # remove them, else they would be added again
+               git ls-files -z --others | xargs -0 rm -f
+               eval "$filter_tree" < /dev/null ||
+                       die "tree filter failed: $filter_tree"
+
+               git diff-index -r $commit | cut -f 2- | tr '\n' '\0' | \
+                       xargs -0 git update-index --add --replace --remove
+               git ls-files -z --others | \
+                       xargs -0 git update-index --add --replace --remove
+       fi
+
+       eval "$filter_index" < /dev/null ||
+               die "index filter failed: $filter_index"
+
+       parentstr=
+       for parent in $parents; do
+               for reparent in $(map "$parent"); do
+                       parentstr="$parentstr -p $reparent"
+               done
+       done
+       if [ "$filter_parent" ]; then
+               parentstr="$(echo "$parentstr" | eval "$filter_parent")" ||
+                               die "parent filter failed: $filter_parent"
+       fi
+
+       sed -e '1,/^$/d' <../commit | \
+               eval "$filter_msg" > ../message ||
+                       die "msg filter failed: $filter_msg"
+       sh -c "$filter_commit" "git commit-tree" \
+               $(git write-tree) $parentstr < ../message > ../map/$commit
+done <../revs
+
+# In case of a subdirectory filter, it is possible that a specified head
+# is not in the set of rewritten commits, because it was pruned by the
+# revision walker.  Fix it by mapping these heads to the next rewritten
+# ancestor(s), i.e. the boundaries in the set of rewritten commits.
+
+# NEEDSWORK: we should sort the unmapped refs topologically first
+while read ref
+do
+       sha1=$(git rev-parse "$ref"^0)
+       test -f "$workdir"/../map/$sha1 && continue
+       # Assign the boundarie(s) in the set of rewritten commits
+       # as the replacement commit(s).
+       # (This would look a bit nicer if --not --stdin worked.)
+       for p in $( (cd "$workdir"/../map; ls | sed "s/^/^/") |
+               git rev-list $ref --boundary --stdin |
+               sed -n "s/^-//p")
+       do
+               map $p >> "$workdir"/../map/$sha1
+       done
+done < "$tempdir"/heads
+
+# Finally update the refs
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+count=0
+echo
+while read ref
+do
+       # avoid rewriting a ref twice
+       test -f "$orig_namespace$ref" && continue
+
+       sha1=$(git rev-parse "$ref"^0)
+       rewritten=$(map $sha1)
+
+       test $sha1 = "$rewritten" &&
+               warn "WARNING: Ref '$ref' is unchanged" &&
+               continue
+
+       case "$rewritten" in
+       '')
+               echo "Ref '$ref' was deleted"
+               git update-ref -m "filter-branch: delete" -d "$ref" $sha1 ||
+                       die "Could not delete $ref"
+       ;;
+       $_x40)
+               echo "Ref '$ref' was rewritten"
+               git update-ref -m "filter-branch: rewrite" \
+                               "$ref" $rewritten $sha1 ||
+                       die "Could not rewrite $ref"
+       ;;
+       *)
+               # NEEDSWORK: possibly add -Werror, making this an error
+               warn "WARNING: '$ref' was rewritten into multiple commits:"
+               warn "$rewritten"
+               warn "WARNING: Ref '$ref' points to the first one now."
+               rewritten=$(echo "$rewritten" | head -n 1)
+               git update-ref -m "filter-branch: rewrite to first" \
+                               "$ref" $rewritten $sha1 ||
+                       die "Could not rewrite $ref"
+       ;;
+       esac
+       git update-ref -m "filter-branch: backup" "$orig_namespace$ref" $sha1
+       count=$(($count+1))
+done < "$tempdir"/heads
+
+# TODO: This should possibly go, with the semantics that all positive given
+#       refs are updated, and their original heads stored in refs/original/
+# Filter tags
+
+if [ "$filter_tag_name" ]; then
+       git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags |
+       while read sha1 type ref; do
+               ref="${ref#refs/tags/}"
+               # XXX: Rewrite tagged trees as well?
+               if [ "$type" != "commit" -a "$type" != "tag" ]; then
+                       continue;
+               fi
+
+               if [ "$type" = "tag" ]; then
+                       # Dereference to a commit
+                       sha1t="$sha1"
+                       sha1="$(git rev-parse "$sha1"^{commit} 2>/dev/null)" || continue
+               fi
+
+               [ -f "../map/$sha1" ] || continue
+               new_sha1="$(cat "../map/$sha1")"
+               GIT_COMMIT="$sha1"
+               export GIT_COMMIT
+               new_ref="$(echo "$ref" | eval "$filter_tag_name")" ||
+                       die "tag name filter failed: $filter_tag_name"
+
+               echo "$ref -> $new_ref ($sha1 -> $new_sha1)"
+
+               if [ "$type" = "tag" ]; then
+                       # Warn that we are not rewriting the tag object itself.
+                       warn "unreferencing tag object $sha1t"
+               fi
+
+               git update-ref "refs/tags/$new_ref" "$new_sha1" ||
+                       die "Could not write tag $new_ref"
+       done
+fi
+
+cd ../..
+rm -rf "$tempdir"
+echo
+test $count -gt 0 && echo "These refs were rewritten:"
+git show-ref | grep ^"$orig_namespace"
+
+unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
+test -z "$ORIG_GIT_DIR" || GIT_DIR="$ORIG_GIT_DIR" && export GIT_DIR
+test -z "$ORIG_GIT_WORK_TREE" || GIT_WORK_TREE="$ORIG_GIT_WORK_TREE" &&
+       export GIT_WORK_TREE
+test -z "$ORIG_GIT_INDEX_FILE" || GIT_INDEX_FILE="$ORIG_GIT_INDEX_FILE" &&
+       export GIT_INDEX_FILE
+git read-tree -u -m HEAD
+
+exit $ret
diff --git a/git-gui.sh b/git-gui.sh
deleted file mode 100755 (executable)
index 1fca11f..0000000
+++ /dev/null
@@ -1,2841 +0,0 @@
-#!/bin/sh
-# Tcl ignores the next line -*- tcl -*- \
- if test "z$*" = zversion \
- || test "z$*" = z--version; \
- then \
-       echo 'git-gui version @@GITGUI_VERSION@@'; \
-       exit; \
- fi; \
- argv0=$0; \
- exec wish "$argv0" -- "$@"
-
-set appvers {@@GITGUI_VERSION@@}
-set copyright [encoding convertfrom utf-8 {
-Copyright © 2006, 2007 Shawn Pearce, et. al.
-
-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}]
-
-######################################################################
-##
-## Tcl/Tk sanity check
-
-if {[catch {package require Tcl 8.4} err]
- || [catch {package require Tk  8.4} err]
-} {
-       catch {wm withdraw .}
-       tk_messageBox \
-               -icon error \
-               -type ok \
-               -title [mc "git-gui: fatal error"] \
-               -message $err
-       exit 1
-}
-
-catch {rename send {}} ; # What an evil concept...
-
-######################################################################
-##
-## locate our library
-
-set oguilib {@@GITGUI_LIBDIR@@}
-set oguirel {@@GITGUI_RELATIVE@@}
-if {$oguirel eq {1}} {
-       set oguilib [file dirname [file dirname [file normalize $argv0]]]
-       set oguilib [file join $oguilib share git-gui lib]
-       set oguimsg [file join $oguilib msgs]
-} elseif {[string match @@* $oguirel]} {
-       set oguilib [file join [file dirname [file normalize $argv0]] lib]
-       set oguimsg [file join [file dirname [file normalize $argv0]] po]
-} else {
-       set oguimsg [file join $oguilib msgs]
-}
-unset oguirel
-
-######################################################################
-##
-## enable verbose loading?
-
-if {![catch {set _verbose $env(GITGUI_VERBOSE)}]} {
-       unset _verbose
-       rename auto_load real__auto_load
-       proc auto_load {name args} {
-               puts stderr "auto_load $name"
-               return [uplevel 1 real__auto_load $name $args]
-       }
-       rename source real__source
-       proc source {name} {
-               puts stderr "source    $name"
-               uplevel 1 real__source $name
-       }
-}
-
-######################################################################
-##
-## Internationalization (i18n) through msgcat and gettext. See
-## http://www.gnu.org/software/gettext/manual/html_node/Tcl.html
-
-package require msgcat
-
-proc _mc_trim {fmt} {
-       set cmk [string first @@ $fmt]
-       if {$cmk > 0} {
-               return [string range $fmt 0 [expr {$cmk - 1}]]
-       }
-       return $fmt
-}
-
-proc mc {en_fmt args} {
-       set fmt [_mc_trim [::msgcat::mc $en_fmt]]
-       if {[catch {set msg [eval [list format $fmt] $args]} err]} {
-               set msg [eval [list format [_mc_trim $en_fmt]] $args]
-       }
-       return $msg
-}
-
-proc strcat {args} {
-       return [join $args {}]
-}
-
-::msgcat::mcload $oguimsg
-unset oguimsg
-
-######################################################################
-##
-## read only globals
-
-set _appname {Git Gui}
-set _gitdir {}
-set _gitexec {}
-set _reponame {}
-set _iscygwin {}
-set _search_path {}
-
-proc appname {} {
-       global _appname
-       return $_appname
-}
-
-proc gitdir {args} {
-       global _gitdir
-       if {$args eq {}} {
-               return $_gitdir
-       }
-       return [eval [list file join $_gitdir] $args]
-}
-
-proc gitexec {args} {
-       global _gitexec
-       if {$_gitexec eq {}} {
-               if {[catch {set _gitexec [git --exec-path]} err]} {
-                       error "Git not installed?\n\n$err"
-               }
-               if {[is_Cygwin]} {
-                       set _gitexec [exec cygpath \
-                               --windows \
-                               --absolute \
-                               $_gitexec]
-               } else {
-                       set _gitexec [file normalize $_gitexec]
-               }
-       }
-       if {$args eq {}} {
-               return $_gitexec
-       }
-       return [eval [list file join $_gitexec] $args]
-}
-
-proc reponame {} {
-       return $::_reponame
-}
-
-proc is_MacOSX {} {
-       if {[tk windowingsystem] eq {aqua}} {
-               return 1
-       }
-       return 0
-}
-
-proc is_Windows {} {
-       if {$::tcl_platform(platform) eq {windows}} {
-               return 1
-       }
-       return 0
-}
-
-proc is_Cygwin {} {
-       global _iscygwin
-       if {$_iscygwin eq {}} {
-               if {$::tcl_platform(platform) eq {windows}} {
-                       if {[catch {set p [exec cygpath --windir]} err]} {
-                               set _iscygwin 0
-                       } else {
-                               set _iscygwin 1
-                       }
-               } else {
-                       set _iscygwin 0
-               }
-       }
-       return $_iscygwin
-}
-
-proc is_enabled {option} {
-       global enabled_options
-       if {[catch {set on $enabled_options($option)}]} {return 0}
-       return $on
-}
-
-proc enable_option {option} {
-       global enabled_options
-       set enabled_options($option) 1
-}
-
-proc disable_option {option} {
-       global enabled_options
-       set enabled_options($option) 0
-}
-
-######################################################################
-##
-## config
-
-proc is_many_config {name} {
-       switch -glob -- $name {
-       gui.recentrepo -
-       remote.*.fetch -
-       remote.*.push
-               {return 1}
-       *
-               {return 0}
-       }
-}
-
-proc is_config_true {name} {
-       global repo_config
-       if {[catch {set v $repo_config($name)}]} {
-               return 0
-       } elseif {$v eq {true} || $v eq {1} || $v eq {yes}} {
-               return 1
-       } else {
-               return 0
-       }
-}
-
-proc get_config {name} {
-       global repo_config
-       if {[catch {set v $repo_config($name)}]} {
-               return {}
-       } else {
-               return $v
-       }
-}
-
-######################################################################
-##
-## handy utils
-
-proc _git_cmd {name} {
-       global _git_cmd_path
-
-       if {[catch {set v $_git_cmd_path($name)}]} {
-               switch -- $name {
-                 version   -
-               --version   -
-               --exec-path { return [list $::_git $name] }
-               }
-
-               set p [gitexec git-$name$::_search_exe]
-               if {[file exists $p]} {
-                       set v [list $p]
-               } elseif {[is_Windows] && [file exists [gitexec git-$name]]} {
-                       # Try to determine what sort of magic will make
-                       # git-$name go and do its thing, because native
-                       # Tcl on Windows doesn't know it.
-                       #
-                       set p [gitexec git-$name]
-                       set f [open $p r]
-                       set s [gets $f]
-                       close $f
-
-                       switch -glob -- [lindex $s 0] {
-                       #!*sh     { set i sh     }
-                       #!*perl   { set i perl   }
-                       #!*python { set i python }
-                       default   { error "git-$name is not supported: $s" }
-                       }
-
-                       upvar #0 _$i interp
-                       if {![info exists interp]} {
-                               set interp [_which $i]
-                       }
-                       if {$interp eq {}} {
-                               error "git-$name requires $i (not in PATH)"
-                       }
-                       set v [concat [list $interp] [lrange $s 1 end] [list $p]]
-               } else {
-                       # Assume it is builtin to git somehow and we
-                       # aren't actually able to see a file for it.
-                       #
-                       set v [list $::_git $name]
-               }
-               set _git_cmd_path($name) $v
-       }
-       return $v
-}
-
-proc _which {what} {
-       global env _search_exe _search_path
-
-       if {$_search_path eq {}} {
-               if {[is_Cygwin] && [regexp {^(/|\.:)} $env(PATH)]} {
-                       set _search_path [split [exec cygpath \
-                               --windows \
-                               --path \
-                               --absolute \
-                               $env(PATH)] {;}]
-                       set _search_exe .exe
-               } elseif {[is_Windows]} {
-                       set gitguidir [file dirname [info script]]
-                       regsub -all ";" $gitguidir "\\;" gitguidir
-                       set env(PATH) "$gitguidir;$env(PATH)"
-                       set _search_path [split $env(PATH) {;}]
-                       set _search_exe .exe
-               } else {
-                       set _search_path [split $env(PATH) :]
-                       set _search_exe {}
-               }
-       }
-
-       foreach p $_search_path {
-               set p [file join $p $what$_search_exe]
-               if {[file exists $p]} {
-                       return [file normalize $p]
-               }
-       }
-       return {}
-}
-
-proc _lappend_nice {cmd_var} {
-       global _nice
-       upvar $cmd_var cmd
-
-       if {![info exists _nice]} {
-               set _nice [_which nice]
-       }
-       if {$_nice ne {}} {
-               lappend cmd $_nice
-       }
-}
-
-proc git {args} {
-       set opt [list exec]
-
-       while {1} {
-               switch -- [lindex $args 0] {
-               --nice {
-                       _lappend_nice opt
-               }
-
-               default {
-                       break
-               }
-
-               }
-
-               set args [lrange $args 1 end]
-       }
-
-       set cmdp [_git_cmd [lindex $args 0]]
-       set args [lrange $args 1 end]
-
-       return [eval $opt $cmdp $args]
-}
-
-proc _open_stdout_stderr {cmd} {
-       if {[catch {
-                       set fd [open $cmd r]
-               } err]} {
-               if {   [lindex $cmd end] eq {2>@1}
-                   && $err eq {can not find channel named "1"}
-                       } {
-                       # Older versions of Tcl 8.4 don't have this 2>@1 IO
-                       # redirect operator.  Fallback to |& cat for those.
-                       # The command was not actually started, so its safe
-                       # to try to start it a second time.
-                       #
-                       set fd [open [concat \
-                               [lrange $cmd 0 end-1] \
-                               [list |& cat] \
-                               ] r]
-               } else {
-                       error $err
-               }
-       }
-       fconfigure $fd -eofchar {}
-       return $fd
-}
-
-proc git_read {args} {
-       set opt [list |]
-
-       while {1} {
-               switch -- [lindex $args 0] {
-               --nice {
-                       _lappend_nice opt
-               }
-
-               --stderr {
-                       lappend args 2>@1
-               }
-
-               default {
-                       break
-               }
-
-               }
-
-               set args [lrange $args 1 end]
-       }
-
-       set cmdp [_git_cmd [lindex $args 0]]
-       set args [lrange $args 1 end]
-
-       return [_open_stdout_stderr [concat $opt $cmdp $args]]
-}
-
-proc git_write {args} {
-       set opt [list |]
-
-       while {1} {
-               switch -- [lindex $args 0] {
-               --nice {
-                       _lappend_nice opt
-               }
-
-               default {
-                       break
-               }
-
-               }
-
-               set args [lrange $args 1 end]
-       }
-
-       set cmdp [_git_cmd [lindex $args 0]]
-       set args [lrange $args 1 end]
-
-       return [open [concat $opt $cmdp $args] w]
-}
-
-proc sq {value} {
-       regsub -all ' $value "'\\''" value
-       return "'$value'"
-}
-
-proc load_current_branch {} {
-       global current_branch is_detached
-
-       set fd [open [gitdir HEAD] r]
-       if {[gets $fd ref] < 1} {
-               set ref {}
-       }
-       close $fd
-
-       set pfx {ref: refs/heads/}
-       set len [string length $pfx]
-       if {[string equal -length $len $pfx $ref]} {
-               # We're on a branch.  It might not exist.  But
-               # HEAD looks good enough to be a branch.
-               #
-               set current_branch [string range $ref $len end]
-               set is_detached 0
-       } else {
-               # Assume this is a detached head.
-               #
-               set current_branch HEAD
-               set is_detached 1
-       }
-}
-
-auto_load tk_optionMenu
-rename tk_optionMenu real__tkOptionMenu
-proc tk_optionMenu {w varName args} {
-       set m [eval real__tkOptionMenu $w $varName $args]
-       $m configure -font font_ui
-       $w configure -font font_ui
-       return $m
-}
-
-proc rmsel_tag {text} {
-       $text tag conf sel \
-               -background [$text cget -background] \
-               -foreground [$text cget -foreground] \
-               -borderwidth 0
-       $text tag conf in_sel -background lightgray
-       bind $text <Motion> break
-       return $text
-}
-
-set root_exists 0
-bind . <Visibility> {
-       bind . <Visibility> {}
-       set root_exists 1
-}
-
-if {[is_Windows]} {
-       wm iconbitmap . -default $oguilib/git-gui.ico
-}
-
-######################################################################
-##
-## config defaults
-
-set cursor_ptr arrow
-font create font_diff -family Courier -size 10
-font create font_ui
-catch {
-       label .dummy
-       eval font configure font_ui [font actual [.dummy cget -font]]
-       destroy .dummy
-}
-
-font create font_uiitalic
-font create font_uibold
-font create font_diffbold
-font create font_diffitalic
-
-foreach class {Button Checkbutton Entry Label
-               Labelframe Listbox Menu Message
-               Radiobutton Spinbox Text} {
-       option add *$class.font font_ui
-}
-unset class
-
-if {[is_Windows] || [is_MacOSX]} {
-       option add *Menu.tearOff 0
-}
-
-if {[is_MacOSX]} {
-       set M1B M1
-       set M1T Cmd
-} else {
-       set M1B Control
-       set M1T Ctrl
-}
-
-proc bind_button3 {w cmd} {
-       bind $w <Any-Button-3> $cmd
-       if {[is_MacOSX]} {
-               # Mac OS X sends Button-2 on right click through three-button mouse,
-               # or through trackpad right-clicking (two-finger touch + click).
-               bind $w <Any-Button-2> $cmd
-               bind $w <Control-Button-1> $cmd
-       }
-}
-
-proc apply_config {} {
-       global repo_config font_descs
-
-       foreach option $font_descs {
-               set name [lindex $option 0]
-               set font [lindex $option 1]
-               if {[catch {
-                       set need_weight 1
-                       foreach {cn cv} $repo_config(gui.$name) {
-                               if {$cn eq {-weight}} {
-                                       set need_weight 0
-                               }
-                               font configure $font $cn $cv
-                       }
-                       if {$need_weight} {
-                               font configure $font -weight normal
-                       }
-                       } err]} {
-                       error_popup [strcat [mc "Invalid font specified in %s:" "gui.$name"] "\n\n$err"]
-               }
-               foreach {cn cv} [font configure $font] {
-                       font configure ${font}bold $cn $cv
-                       font configure ${font}italic $cn $cv
-               }
-               font configure ${font}bold -weight bold
-               font configure ${font}italic -slant italic
-       }
-}
-
-set default_config(merge.diffstat) true
-set default_config(merge.summary) false
-set default_config(merge.verbosity) 2
-set default_config(user.name) {}
-set default_config(user.email) {}
-
-set default_config(gui.matchtrackingbranch) false
-set default_config(gui.pruneduringfetch) false
-set default_config(gui.trustmtime) false
-set default_config(gui.diffcontext) 5
-set default_config(gui.newbranchtemplate) {}
-set default_config(gui.fontui) [font configure font_ui]
-set default_config(gui.fontdiff) [font configure font_diff]
-set font_descs {
-       {fontui   font_ui   {mc "Main Font"}}
-       {fontdiff font_diff {mc "Diff/Console Font"}}
-}
-
-######################################################################
-##
-## find git
-
-set _git  [_which git]
-if {$_git eq {}} {
-       catch {wm withdraw .}
-       tk_messageBox \
-               -icon error \
-               -type ok \
-               -title [mc "git-gui: fatal error"] \
-               -message [mc "Cannot find git in PATH."]
-       exit 1
-}
-
-######################################################################
-##
-## version check
-
-if {[catch {set _git_version [git --version]} err]} {
-       catch {wm withdraw .}
-       tk_messageBox \
-               -icon error \
-               -type ok \
-               -title [mc "git-gui: fatal error"] \
-               -message "Cannot determine Git version:
-
-$err
-
-[appname] requires Git 1.5.0 or later."
-       exit 1
-}
-if {![regsub {^git version } $_git_version {} _git_version]} {
-       catch {wm withdraw .}
-       tk_messageBox \
-               -icon error \
-               -type ok \
-               -title [mc "git-gui: fatal error"] \
-               -message [strcat [mc "Cannot parse Git version string:"] "\n\n$_git_version"]
-       exit 1
-}
-
-set _real_git_version $_git_version
-regsub -- {-dirty$} $_git_version {} _git_version
-regsub {\.[0-9]+\.g[0-9a-f]+$} $_git_version {} _git_version
-regsub {\.rc[0-9]+$} $_git_version {} _git_version
-regsub {\.GIT$} $_git_version {} _git_version
-regsub {\.[a-zA-Z]+\.[0-9]+$} $_git_version {} _git_version
-
-if {![regexp {^[1-9]+(\.[0-9]+)+$} $_git_version]} {
-       catch {wm withdraw .}
-       if {[tk_messageBox \
-               -icon warning \
-               -type yesno \
-               -default no \
-               -title "[appname]: warning" \
-                -message [mc "Git version cannot be determined.
-
-%s claims it is version '%s'.
-
-%s requires at least Git 1.5.0 or later.
-
-Assume '%s' is version 1.5.0?
-" $_git $_real_git_version [appname] $_real_git_version]] eq {yes}} {
-               set _git_version 1.5.0
-       } else {
-               exit 1
-       }
-}
-unset _real_git_version
-
-proc git-version {args} {
-       global _git_version
-
-       switch [llength $args] {
-       0 {
-               return $_git_version
-       }
-
-       2 {
-               set op [lindex $args 0]
-               set vr [lindex $args 1]
-               set cm [package vcompare $_git_version $vr]
-               return [expr $cm $op 0]
-       }
-
-       4 {
-               set type [lindex $args 0]
-               set name [lindex $args 1]
-               set parm [lindex $args 2]
-               set body [lindex $args 3]
-
-               if {($type ne {proc} && $type ne {method})} {
-                       error "Invalid arguments to git-version"
-               }
-               if {[llength $body] < 2 || [lindex $body end-1] ne {default}} {
-                       error "Last arm of $type $name must be default"
-               }
-
-               foreach {op vr cb} [lrange $body 0 end-2] {
-                       if {[git-version $op $vr]} {
-                               return [uplevel [list $type $name $parm $cb]]
-                       }
-               }
-
-               return [uplevel [list $type $name $parm [lindex $body end]]]
-       }
-
-       default {
-               error "git-version >= x"
-       }
-
-       }
-}
-
-if {[git-version < 1.5]} {
-       catch {wm withdraw .}
-       tk_messageBox \
-               -icon error \
-               -type ok \
-               -title [mc "git-gui: fatal error"] \
-               -message "[appname] requires Git 1.5.0 or later.
-
-You are using [git-version]:
-
-[git --version]"
-       exit 1
-}
-
-######################################################################
-##
-## configure our library
-
-set idx [file join $oguilib tclIndex]
-if {[catch {set fd [open $idx r]} err]} {
-       catch {wm withdraw .}
-       tk_messageBox \
-               -icon error \
-               -type ok \
-               -title [mc "git-gui: fatal error"] \
-               -message $err
-       exit 1
-}
-if {[gets $fd] eq {# Autogenerated by git-gui Makefile}} {
-       set idx [list]
-       while {[gets $fd n] >= 0} {
-               if {$n ne {} && ![string match #* $n]} {
-                       lappend idx $n
-               }
-       }
-} else {
-       set idx {}
-}
-close $fd
-
-if {$idx ne {}} {
-       set loaded [list]
-       foreach p $idx {
-               if {[lsearch -exact $loaded $p] >= 0} continue
-               source [file join $oguilib $p]
-               lappend loaded $p
-       }
-       unset loaded p
-} else {
-       set auto_path [concat [list $oguilib] $auto_path]
-}
-unset -nocomplain idx fd
-
-######################################################################
-##
-## config file parsing
-
-git-version proc _parse_config {arr_name args} {
-       >= 1.5.3 {
-               upvar $arr_name arr
-               array unset arr
-               set buf {}
-               catch {
-                       set fd_rc [eval \
-                               [list git_read config] \
-                               $args \
-                               [list --null --list]]
-                       fconfigure $fd_rc -translation binary
-                       set buf [read $fd_rc]
-                       close $fd_rc
-               }
-               foreach line [split $buf "\0"] {
-                       if {[regexp {^([^\n]+)\n(.*)$} $line line name value]} {
-                               if {[is_many_config $name]} {
-                                       lappend arr($name) $value
-                               } else {
-                                       set arr($name) $value
-                               }
-                       }
-               }
-       }
-       default {
-               upvar $arr_name arr
-               array unset arr
-               catch {
-                       set fd_rc [eval [list git_read config --list] $args]
-                       while {[gets $fd_rc line] >= 0} {
-                               if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
-                                       if {[is_many_config $name]} {
-                                               lappend arr($name) $value
-                                       } else {
-                                               set arr($name) $value
-                                       }
-                               }
-                       }
-                       close $fd_rc
-               }
-       }
-}
-
-proc load_config {include_global} {
-       global repo_config global_config default_config
-
-       if {$include_global} {
-               _parse_config global_config --global
-       }
-       _parse_config repo_config
-
-       foreach name [array names default_config] {
-               if {[catch {set v $global_config($name)}]} {
-                       set global_config($name) $default_config($name)
-               }
-               if {[catch {set v $repo_config($name)}]} {
-                       set repo_config($name) $default_config($name)
-               }
-       }
-}
-
-######################################################################
-##
-## feature option selection
-
-if {[regexp {^git-(.+)$} [file tail $argv0] _junk subcommand]} {
-       unset _junk
-} else {
-       set subcommand gui
-}
-if {$subcommand eq {gui.sh}} {
-       set subcommand gui
-}
-if {$subcommand eq {gui} && [llength $argv] > 0} {
-       set subcommand [lindex $argv 0]
-       set argv [lrange $argv 1 end]
-}
-
-enable_option multicommit
-enable_option branch
-enable_option transport
-disable_option bare
-
-switch -- $subcommand {
-browser -
-blame {
-       enable_option bare
-
-       disable_option multicommit
-       disable_option branch
-       disable_option transport
-}
-citool {
-       enable_option singlecommit
-
-       disable_option multicommit
-       disable_option branch
-       disable_option transport
-}
-}
-
-######################################################################
-##
-## repository setup
-
-if {[catch {
-               set _gitdir $env(GIT_DIR)
-               set _prefix {}
-               }]
-       && [catch {
-               set _gitdir [git rev-parse --git-dir]
-               set _prefix [git rev-parse --show-prefix]
-       } err]} {
-       load_config 1
-       apply_config
-       choose_repository::pick
-}
-if {![file isdirectory $_gitdir] && [is_Cygwin]} {
-       catch {set _gitdir [exec cygpath --windows $_gitdir]}
-}
-if {![file isdirectory $_gitdir]} {
-       catch {wm withdraw .}
-       error_popup [strcat [mc "Git directory not found:"] "\n\n$_gitdir"]
-       exit 1
-}
-if {$_prefix ne {}} {
-       regsub -all {[^/]+/} $_prefix ../ cdup
-       if {[catch {cd $cdup} err]} {
-               catch {wm withdraw .}
-               error_popup [strcat [mc "Cannot move to top of working directory:"] "\n\n$err"]
-               exit 1
-       }
-       unset cdup
-} elseif {![is_enabled bare]} {
-       if {[lindex [file split $_gitdir] end] ne {.git}} {
-               catch {wm withdraw .}
-               error_popup [strcat [mc "Cannot use funny .git directory:"] "\n\n$_gitdir"]
-               exit 1
-       }
-       if {[catch {cd [file dirname $_gitdir]} err]} {
-               catch {wm withdraw .}
-               error_popup [strcat [mc "No working directory"] " [file dirname $_gitdir]:\n\n$err"]
-               exit 1
-       }
-}
-set _reponame [file split [file normalize $_gitdir]]
-if {[lindex $_reponame end] eq {.git}} {
-       set _reponame [lindex $_reponame end-1]
-} else {
-       set _reponame [lindex $_reponame end]
-}
-
-######################################################################
-##
-## global init
-
-set current_diff_path {}
-set current_diff_side {}
-set diff_actions [list]
-
-set HEAD {}
-set PARENT {}
-set MERGE_HEAD [list]
-set commit_type {}
-set empty_tree {}
-set current_branch {}
-set is_detached 0
-set current_diff_path {}
-set is_3way_diff 0
-set selected_commit_type new
-
-######################################################################
-##
-## task management
-
-set rescan_active 0
-set diff_active 0
-set last_clicked {}
-
-set disable_on_lock [list]
-set index_lock_type none
-
-proc lock_index {type} {
-       global index_lock_type disable_on_lock
-
-       if {$index_lock_type eq {none}} {
-               set index_lock_type $type
-               foreach w $disable_on_lock {
-                       uplevel #0 $w disabled
-               }
-               return 1
-       } elseif {$index_lock_type eq "begin-$type"} {
-               set index_lock_type $type
-               return 1
-       }
-       return 0
-}
-
-proc unlock_index {} {
-       global index_lock_type disable_on_lock
-
-       set index_lock_type none
-       foreach w $disable_on_lock {
-               uplevel #0 $w normal
-       }
-}
-
-######################################################################
-##
-## status
-
-proc repository_state {ctvar hdvar mhvar} {
-       global current_branch
-       upvar $ctvar ct $hdvar hd $mhvar mh
-
-       set mh [list]
-
-       load_current_branch
-       if {[catch {set hd [git rev-parse --verify HEAD]}]} {
-               set hd {}
-               set ct initial
-               return
-       }
-
-       set merge_head [gitdir MERGE_HEAD]
-       if {[file exists $merge_head]} {
-               set ct merge
-               set fd_mh [open $merge_head r]
-               while {[gets $fd_mh line] >= 0} {
-                       lappend mh $line
-               }
-               close $fd_mh
-               return
-       }
-
-       set ct normal
-}
-
-proc PARENT {} {
-       global PARENT empty_tree
-
-       set p [lindex $PARENT 0]
-       if {$p ne {}} {
-               return $p
-       }
-       if {$empty_tree eq {}} {
-               set empty_tree [git mktree << {}]
-       }
-       return $empty_tree
-}
-
-proc rescan {after {honor_trustmtime 1}} {
-       global HEAD PARENT MERGE_HEAD commit_type
-       global ui_index ui_workdir ui_comm
-       global rescan_active file_states
-       global repo_config
-
-       if {$rescan_active > 0 || ![lock_index read]} return
-
-       repository_state newType newHEAD newMERGE_HEAD
-       if {[string match amend* $commit_type]
-               && $newType eq {normal}
-               && $newHEAD eq $HEAD} {
-       } else {
-               set HEAD $newHEAD
-               set PARENT $newHEAD
-               set MERGE_HEAD $newMERGE_HEAD
-               set commit_type $newType
-       }
-
-       array unset file_states
-
-       if {!$::GITGUI_BCK_exists &&
-               (![$ui_comm edit modified]
-               || [string trim [$ui_comm get 0.0 end]] eq {})} {
-               if {[string match amend* $commit_type]} {
-               } elseif {[load_message GITGUI_MSG]} {
-               } elseif {[load_message MERGE_MSG]} {
-               } elseif {[load_message SQUASH_MSG]} {
-               }
-               $ui_comm edit reset
-               $ui_comm edit modified false
-       }
-
-       if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} {
-               rescan_stage2 {} $after
-       } else {
-               set rescan_active 1
-               ui_status [mc "Refreshing file status..."]
-               set fd_rf [git_read update-index \
-                       -q \
-                       --unmerged \
-                       --ignore-missing \
-                       --refresh \
-                       ]
-               fconfigure $fd_rf -blocking 0 -translation binary
-               fileevent $fd_rf readable \
-                       [list rescan_stage2 $fd_rf $after]
-       }
-}
-
-if {[is_Cygwin]} {
-       set is_git_info_link {}
-       set is_git_info_exclude {}
-       proc have_info_exclude {} {
-               global is_git_info_link is_git_info_exclude
-
-               if {$is_git_info_link eq {}} {
-                       set is_git_info_link [file isfile [gitdir info.lnk]]
-               }
-
-               if {$is_git_info_link} {
-                       if {$is_git_info_exclude eq {}} {
-                               if {[catch {exec test -f [gitdir info exclude]}]} {
-                                       set is_git_info_exclude 0
-                               } else {
-                                       set is_git_info_exclude 1
-                               }
-                       }
-                       return $is_git_info_exclude
-               } else {
-                       return [file readable [gitdir info exclude]]
-               }
-       }
-} else {
-       proc have_info_exclude {} {
-               return [file readable [gitdir info exclude]]
-       }
-}
-
-proc rescan_stage2 {fd after} {
-       global rescan_active buf_rdi buf_rdf buf_rlo
-
-       if {$fd ne {}} {
-               read $fd
-               if {![eof $fd]} return
-               close $fd
-       }
-
-       set ls_others [list --exclude-per-directory=.gitignore]
-       if {[have_info_exclude]} {
-               lappend ls_others "--exclude-from=[gitdir info exclude]"
-       }
-       set user_exclude [get_config core.excludesfile]
-       if {$user_exclude ne {} && [file readable $user_exclude]} {
-               lappend ls_others "--exclude-from=$user_exclude"
-       }
-
-       set buf_rdi {}
-       set buf_rdf {}
-       set buf_rlo {}
-
-       set rescan_active 3
-       ui_status [mc "Scanning for modified files ..."]
-       set fd_di [git_read diff-index --cached -z [PARENT]]
-       set fd_df [git_read diff-files -z]
-       set fd_lo [eval git_read ls-files --others -z $ls_others]
-
-       fconfigure $fd_di -blocking 0 -translation binary -encoding binary
-       fconfigure $fd_df -blocking 0 -translation binary -encoding binary
-       fconfigure $fd_lo -blocking 0 -translation binary -encoding binary
-       fileevent $fd_di readable [list read_diff_index $fd_di $after]
-       fileevent $fd_df readable [list read_diff_files $fd_df $after]
-       fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
-}
-
-proc load_message {file} {
-       global ui_comm
-
-       set f [gitdir $file]
-       if {[file isfile $f]} {
-               if {[catch {set fd [open $f r]}]} {
-                       return 0
-               }
-               fconfigure $fd -eofchar {}
-               set content [string trim [read $fd]]
-               close $fd
-               regsub -all -line {[ \r\t]+$} $content {} content
-               $ui_comm delete 0.0 end
-               $ui_comm insert end $content
-               return 1
-       }
-       return 0
-}
-
-proc read_diff_index {fd after} {
-       global buf_rdi
-
-       append buf_rdi [read $fd]
-       set c 0
-       set n [string length $buf_rdi]
-       while {$c < $n} {
-               set z1 [string first "\0" $buf_rdi $c]
-               if {$z1 == -1} break
-               incr z1
-               set z2 [string first "\0" $buf_rdi $z1]
-               if {$z2 == -1} break
-
-               incr c
-               set i [split [string range $buf_rdi $c [expr {$z1 - 2}]] { }]
-               set p [string range $buf_rdi $z1 [expr {$z2 - 1}]]
-               merge_state \
-                       [encoding convertfrom $p] \
-                       [lindex $i 4]? \
-                       [list [lindex $i 0] [lindex $i 2]] \
-                       [list]
-               set c $z2
-               incr c
-       }
-       if {$c < $n} {
-               set buf_rdi [string range $buf_rdi $c end]
-       } else {
-               set buf_rdi {}
-       }
-
-       rescan_done $fd buf_rdi $after
-}
-
-proc read_diff_files {fd after} {
-       global buf_rdf
-
-       append buf_rdf [read $fd]
-       set c 0
-       set n [string length $buf_rdf]
-       while {$c < $n} {
-               set z1 [string first "\0" $buf_rdf $c]
-               if {$z1 == -1} break
-               incr z1
-               set z2 [string first "\0" $buf_rdf $z1]
-               if {$z2 == -1} break
-
-               incr c
-               set i [split [string range $buf_rdf $c [expr {$z1 - 2}]] { }]
-               set p [string range $buf_rdf $z1 [expr {$z2 - 1}]]
-               merge_state \
-                       [encoding convertfrom $p] \
-                       ?[lindex $i 4] \
-                       [list] \
-                       [list [lindex $i 0] [lindex $i 2]]
-               set c $z2
-               incr c
-       }
-       if {$c < $n} {
-               set buf_rdf [string range $buf_rdf $c end]
-       } else {
-               set buf_rdf {}
-       }
-
-       rescan_done $fd buf_rdf $after
-}
-
-proc read_ls_others {fd after} {
-       global buf_rlo
-
-       append buf_rlo [read $fd]
-       set pck [split $buf_rlo "\0"]
-       set buf_rlo [lindex $pck end]
-       foreach p [lrange $pck 0 end-1] {
-               set p [encoding convertfrom $p]
-               if {[string index $p end] eq {/}} {
-                       set p [string range $p 0 end-1]
-               }
-               merge_state $p ?O
-       }
-       rescan_done $fd buf_rlo $after
-}
-
-proc rescan_done {fd buf after} {
-       global rescan_active current_diff_path
-       global file_states repo_config
-       upvar $buf to_clear
-
-       if {![eof $fd]} return
-       set to_clear {}
-       close $fd
-       if {[incr rescan_active -1] > 0} return
-
-       prune_selection
-       unlock_index
-       display_all_files
-       if {$current_diff_path ne {}} reshow_diff
-       uplevel #0 $after
-}
-
-proc prune_selection {} {
-       global file_states selected_paths
-
-       foreach path [array names selected_paths] {
-               if {[catch {set still_here $file_states($path)}]} {
-                       unset selected_paths($path)
-               }
-       }
-}
-
-######################################################################
-##
-## ui helpers
-
-proc mapicon {w state path} {
-       global all_icons
-
-       if {[catch {set r $all_icons($state$w)}]} {
-               puts "error: no icon for $w state={$state} $path"
-               return file_plain
-       }
-       return $r
-}
-
-proc mapdesc {state path} {
-       global all_descs
-
-       if {[catch {set r $all_descs($state)}]} {
-               puts "error: no desc for state={$state} $path"
-               return $state
-       }
-       return $r
-}
-
-proc ui_status {msg} {
-       global main_status
-       if {[info exists main_status]} {
-               $main_status show $msg
-       }
-}
-
-proc ui_ready {{test {}}} {
-       global main_status
-       if {[info exists main_status]} {
-               $main_status show [mc "Ready."] $test
-       }
-}
-
-proc escape_path {path} {
-       regsub -all {\\} $path "\\\\" path
-       regsub -all "\n" $path "\\n" path
-       return $path
-}
-
-proc short_path {path} {
-       return [escape_path [lindex [file split $path] end]]
-}
-
-set next_icon_id 0
-set null_sha1 [string repeat 0 40]
-
-proc merge_state {path new_state {head_info {}} {index_info {}}} {
-       global file_states next_icon_id null_sha1
-
-       set s0 [string index $new_state 0]
-       set s1 [string index $new_state 1]
-
-       if {[catch {set info $file_states($path)}]} {
-               set state __
-               set icon n[incr next_icon_id]
-       } else {
-               set state [lindex $info 0]
-               set icon [lindex $info 1]
-               if {$head_info eq {}}  {set head_info  [lindex $info 2]}
-               if {$index_info eq {}} {set index_info [lindex $info 3]}
-       }
-
-       if     {$s0 eq {?}} {set s0 [string index $state 0]} \
-       elseif {$s0 eq {_}} {set s0 _}
-
-       if     {$s1 eq {?}} {set s1 [string index $state 1]} \
-       elseif {$s1 eq {_}} {set s1 _}
-
-       if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} {
-               set head_info [list 0 $null_sha1]
-       } elseif {$s0 ne {_} && [string index $state 0] eq {_}
-               && $head_info eq {}} {
-               set head_info $index_info
-       }
-
-       set file_states($path) [list $s0$s1 $icon \
-               $head_info $index_info \
-               ]
-       return $state
-}
-
-proc display_file_helper {w path icon_name old_m new_m} {
-       global file_lists
-
-       if {$new_m eq {_}} {
-               set lno [lsearch -sorted -exact $file_lists($w) $path]
-               if {$lno >= 0} {
-                       set file_lists($w) [lreplace $file_lists($w) $lno $lno]
-                       incr lno
-                       $w conf -state normal
-                       $w delete $lno.0 [expr {$lno + 1}].0
-                       $w conf -state disabled
-               }
-       } elseif {$old_m eq {_} && $new_m ne {_}} {
-               lappend file_lists($w) $path
-               set file_lists($w) [lsort -unique $file_lists($w)]
-               set lno [lsearch -sorted -exact $file_lists($w) $path]
-               incr lno
-               $w conf -state normal
-               $w image create $lno.0 \
-                       -align center -padx 5 -pady 1 \
-                       -name $icon_name \
-                       -image [mapicon $w $new_m $path]
-               $w insert $lno.1 "[escape_path $path]\n"
-               $w conf -state disabled
-       } elseif {$old_m ne $new_m} {
-               $w conf -state normal
-               $w image conf $icon_name -image [mapicon $w $new_m $path]
-               $w conf -state disabled
-       }
-}
-
-proc display_file {path state} {
-       global file_states selected_paths
-       global ui_index ui_workdir
-
-       set old_m [merge_state $path $state]
-       set s $file_states($path)
-       set new_m [lindex $s 0]
-       set icon_name [lindex $s 1]
-
-       set o [string index $old_m 0]
-       set n [string index $new_m 0]
-       if {$o eq {U}} {
-               set o _
-       }
-       if {$n eq {U}} {
-               set n _
-       }
-       display_file_helper     $ui_index $path $icon_name $o $n
-
-       if {[string index $old_m 0] eq {U}} {
-               set o U
-       } else {
-               set o [string index $old_m 1]
-       }
-       if {[string index $new_m 0] eq {U}} {
-               set n U
-       } else {
-               set n [string index $new_m 1]
-       }
-       display_file_helper     $ui_workdir $path $icon_name $o $n
-
-       if {$new_m eq {__}} {
-               unset file_states($path)
-               catch {unset selected_paths($path)}
-       }
-}
-
-proc display_all_files_helper {w path icon_name m} {
-       global file_lists
-
-       lappend file_lists($w) $path
-       set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
-       $w image create end \
-               -align center -padx 5 -pady 1 \
-               -name $icon_name \
-               -image [mapicon $w $m $path]
-       $w insert end "[escape_path $path]\n"
-}
-
-proc display_all_files {} {
-       global ui_index ui_workdir
-       global file_states file_lists
-       global last_clicked
-
-       $ui_index conf -state normal
-       $ui_workdir conf -state normal
-
-       $ui_index delete 0.0 end
-       $ui_workdir delete 0.0 end
-       set last_clicked {}
-
-       set file_lists($ui_index) [list]
-       set file_lists($ui_workdir) [list]
-
-       foreach path [lsort [array names file_states]] {
-               set s $file_states($path)
-               set m [lindex $s 0]
-               set icon_name [lindex $s 1]
-
-               set s [string index $m 0]
-               if {$s ne {U} && $s ne {_}} {
-                       display_all_files_helper $ui_index $path \
-                               $icon_name $s
-               }
-
-               if {[string index $m 0] eq {U}} {
-                       set s U
-               } else {
-                       set s [string index $m 1]
-               }
-               if {$s ne {_}} {
-                       display_all_files_helper $ui_workdir $path \
-                               $icon_name $s
-               }
-       }
-
-       $ui_index conf -state disabled
-       $ui_workdir conf -state disabled
-}
-
-######################################################################
-##
-## icons
-
-set filemask {
-#define mask_width 14
-#define mask_height 15
-static unsigned char mask_bits[] = {
-   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
-   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
-   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
-}
-
-image create bitmap file_plain -background white -foreground black -data {
-#define plain_width 14
-#define plain_height 15
-static unsigned char plain_bits[] = {
-   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
-   0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
-   0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
-} -maskdata $filemask
-
-image create bitmap file_mod -background white -foreground blue -data {
-#define mod_width 14
-#define mod_height 15
-static unsigned char mod_bits[] = {
-   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
-   0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
-   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
-} -maskdata $filemask
-
-image create bitmap file_fulltick -background white -foreground "#007000" -data {
-#define file_fulltick_width 14
-#define file_fulltick_height 15
-static unsigned char file_fulltick_bits[] = {
-   0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
-   0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
-   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
-} -maskdata $filemask
-
-image create bitmap file_parttick -background white -foreground "#005050" -data {
-#define parttick_width 14
-#define parttick_height 15
-static unsigned char parttick_bits[] = {
-   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
-   0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
-   0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
-} -maskdata $filemask
-
-image create bitmap file_question -background white -foreground black -data {
-#define file_question_width 14
-#define file_question_height 15
-static unsigned char file_question_bits[] = {
-   0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
-   0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
-   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
-} -maskdata $filemask
-
-image create bitmap file_removed -background white -foreground red -data {
-#define file_removed_width 14
-#define file_removed_height 15
-static unsigned char file_removed_bits[] = {
-   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
-   0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
-   0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
-} -maskdata $filemask
-
-image create bitmap file_merge -background white -foreground blue -data {
-#define file_merge_width 14
-#define file_merge_height 15
-static unsigned char file_merge_bits[] = {
-   0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
-   0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
-   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
-} -maskdata $filemask
-
-set ui_index .vpane.files.index.list
-set ui_workdir .vpane.files.workdir.list
-
-set all_icons(_$ui_index)   file_plain
-set all_icons(A$ui_index)   file_fulltick
-set all_icons(M$ui_index)   file_fulltick
-set all_icons(D$ui_index)   file_removed
-set all_icons(U$ui_index)   file_merge
-
-set all_icons(_$ui_workdir) file_plain
-set all_icons(M$ui_workdir) file_mod
-set all_icons(D$ui_workdir) file_question
-set all_icons(U$ui_workdir) file_merge
-set all_icons(O$ui_workdir) file_plain
-
-set max_status_desc 0
-foreach i {
-               {__ {mc "Unmodified"}}
-
-               {_M {mc "Modified, not staged"}}
-               {M_ {mc "Staged for commit"}}
-               {MM {mc "Portions staged for commit"}}
-               {MD {mc "Staged for commit, missing"}}
-
-               {_O {mc "Untracked, not staged"}}
-               {A_ {mc "Staged for commit"}}
-               {AM {mc "Portions staged for commit"}}
-               {AD {mc "Staged for commit, missing"}}
-
-               {_D {mc "Missing"}}
-               {D_ {mc "Staged for removal"}}
-               {DO {mc "Staged for removal, still present"}}
-
-               {U_ {mc "Requires merge resolution"}}
-               {UU {mc "Requires merge resolution"}}
-               {UM {mc "Requires merge resolution"}}
-               {UD {mc "Requires merge resolution"}}
-       } {
-       set text [eval [lindex $i 1]]
-       if {$max_status_desc < [string length $text]} {
-               set max_status_desc [string length $text]
-       }
-       set all_descs([lindex $i 0]) $text
-}
-unset i
-
-######################################################################
-##
-## util
-
-proc scrollbar2many {list mode args} {
-       foreach w $list {eval $w $mode $args}
-}
-
-proc many2scrollbar {list mode sb top bottom} {
-       $sb set $top $bottom
-       foreach w $list {$w $mode moveto $top}
-}
-
-proc incr_font_size {font {amt 1}} {
-       set sz [font configure $font -size]
-       incr sz $amt
-       font configure $font -size $sz
-       font configure ${font}bold -size $sz
-       font configure ${font}italic -size $sz
-}
-
-######################################################################
-##
-## ui commands
-
-set starting_gitk_msg [mc "Starting gitk... please wait..."]
-
-proc do_gitk {revs} {
-       # -- Always start gitk through whatever we were loaded with.  This
-       #    lets us bypass using shell process on Windows systems.
-       #
-       set exe [file join [file dirname $::_git] gitk]
-       set cmd [list [info nameofexecutable] $exe]
-       if {! [file exists $exe]} {
-               error_popup [mc "Unable to start gitk:\n\n%s does not exist" $exe]
-       } else {
-               global env
-
-               if {[info exists env(GIT_DIR)]} {
-                       set old_GIT_DIR $env(GIT_DIR)
-               } else {
-                       set old_GIT_DIR {}
-               }
-
-               set pwd [pwd]
-               cd [file dirname [gitdir]]
-               set env(GIT_DIR) [file tail [gitdir]]
-
-               eval exec $cmd $revs &
-
-               if {$old_GIT_DIR eq {}} {
-                       unset env(GIT_DIR)
-               } else {
-                       set env(GIT_DIR) $old_GIT_DIR
-               }
-               cd $pwd
-
-               ui_status $::starting_gitk_msg
-               after 10000 {
-                       ui_ready $starting_gitk_msg
-               }
-       }
-}
-
-set is_quitting 0
-
-proc do_quit {} {
-       global ui_comm is_quitting repo_config commit_type
-       global GITGUI_BCK_exists GITGUI_BCK_i
-
-       if {$is_quitting} return
-       set is_quitting 1
-
-       if {[winfo exists $ui_comm]} {
-               # -- Stash our current commit buffer.
-               #
-               set save [gitdir GITGUI_MSG]
-               if {$GITGUI_BCK_exists && ![$ui_comm edit modified]} {
-                       file rename -force [gitdir GITGUI_BCK] $save
-                       set GITGUI_BCK_exists 0
-               } else {
-                       set msg [string trim [$ui_comm get 0.0 end]]
-                       regsub -all -line {[ \r\t]+$} $msg {} msg
-                       if {(![string match amend* $commit_type]
-                               || [$ui_comm edit modified])
-                               && $msg ne {}} {
-                               catch {
-                                       set fd [open $save w]
-                                       puts -nonewline $fd $msg
-                                       close $fd
-                               }
-                       } else {
-                               catch {file delete $save}
-                       }
-               }
-
-               # -- Remove our editor backup, its not needed.
-               #
-               after cancel $GITGUI_BCK_i
-               if {$GITGUI_BCK_exists} {
-                       catch {file delete [gitdir GITGUI_BCK]}
-               }
-
-               # -- Stash our current window geometry into this repository.
-               #
-               set cfg_geometry [list]
-               lappend cfg_geometry [wm geometry .]
-               lappend cfg_geometry [lindex [.vpane sash coord 0] 0]
-               lappend cfg_geometry [lindex [.vpane.files sash coord 0] 1]
-               if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
-                       set rc_geometry {}
-               }
-               if {$cfg_geometry ne $rc_geometry} {
-                       catch {git config gui.geometry $cfg_geometry}
-               }
-       }
-
-       destroy .
-}
-
-proc do_rescan {} {
-       rescan ui_ready
-}
-
-proc do_commit {} {
-       commit_tree
-}
-
-proc toggle_or_diff {w x y} {
-       global file_states file_lists current_diff_path ui_index ui_workdir
-       global last_clicked selected_paths
-
-       set pos [split [$w index @$x,$y] .]
-       set lno [lindex $pos 0]
-       set col [lindex $pos 1]
-       set path [lindex $file_lists($w) [expr {$lno - 1}]]
-       if {$path eq {}} {
-               set last_clicked {}
-               return
-       }
-
-       set last_clicked [list $w $lno]
-       array unset selected_paths
-       $ui_index tag remove in_sel 0.0 end
-       $ui_workdir tag remove in_sel 0.0 end
-
-       if {$col == 0} {
-               if {$current_diff_path eq $path} {
-                       set after {reshow_diff;}
-               } else {
-                       set after {}
-               }
-               if {$w eq $ui_index} {
-                       update_indexinfo \
-                               "Unstaging [short_path $path] from commit" \
-                               [list $path] \
-                               [concat $after [list ui_ready]]
-               } elseif {$w eq $ui_workdir} {
-                       update_index \
-                               "Adding [short_path $path]" \
-                               [list $path] \
-                               [concat $after [list ui_ready]]
-               }
-       } else {
-               show_diff $path $w $lno
-       }
-}
-
-proc add_one_to_selection {w x y} {
-       global file_lists last_clicked selected_paths
-
-       set lno [lindex [split [$w index @$x,$y] .] 0]
-       set path [lindex $file_lists($w) [expr {$lno - 1}]]
-       if {$path eq {}} {
-               set last_clicked {}
-               return
-       }
-
-       if {$last_clicked ne {}
-               && [lindex $last_clicked 0] ne $w} {
-               array unset selected_paths
-               [lindex $last_clicked 0] tag remove in_sel 0.0 end
-       }
-
-       set last_clicked [list $w $lno]
-       if {[catch {set in_sel $selected_paths($path)}]} {
-               set in_sel 0
-       }
-       if {$in_sel} {
-               unset selected_paths($path)
-               $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
-       } else {
-               set selected_paths($path) 1
-               $w tag add in_sel $lno.0 [expr {$lno + 1}].0
-       }
-}
-
-proc add_range_to_selection {w x y} {
-       global file_lists last_clicked selected_paths
-
-       if {[lindex $last_clicked 0] ne $w} {
-               toggle_or_diff $w $x $y
-               return
-       }
-
-       set lno [lindex [split [$w index @$x,$y] .] 0]
-       set lc [lindex $last_clicked 1]
-       if {$lc < $lno} {
-               set begin $lc
-               set end $lno
-       } else {
-               set begin $lno
-               set end $lc
-       }
-
-       foreach path [lrange $file_lists($w) \
-               [expr {$begin - 1}] \
-               [expr {$end - 1}]] {
-               set selected_paths($path) 1
-       }
-       $w tag add in_sel $begin.0 [expr {$end + 1}].0
-}
-
-######################################################################
-##
-## ui construction
-
-load_config 0
-apply_config
-set ui_comm {}
-
-# -- Menu Bar
-#
-menu .mbar -tearoff 0
-.mbar add cascade -label [mc Repository] -menu .mbar.repository
-.mbar add cascade -label [mc Edit] -menu .mbar.edit
-if {[is_enabled branch]} {
-       .mbar add cascade -label [mc Branch] -menu .mbar.branch
-}
-if {[is_enabled multicommit] || [is_enabled singlecommit]} {
-       .mbar add cascade -label [mc Commit@@noun] -menu .mbar.commit
-}
-if {[is_enabled transport]} {
-       .mbar add cascade -label [mc Merge] -menu .mbar.merge
-       .mbar add cascade -label [mc Remote] -menu .mbar.remote
-}
-. configure -menu .mbar
-
-# -- Repository Menu
-#
-menu .mbar.repository
-
-.mbar.repository add command \
-       -label [mc "Browse Current Branch's Files"] \
-       -command {browser::new $current_branch}
-set ui_browse_current [.mbar.repository index last]
-.mbar.repository add command \
-       -label [mc "Browse Branch Files..."] \
-       -command browser_open::dialog
-.mbar.repository add separator
-
-.mbar.repository add command \
-       -label [mc "Visualize Current Branch's History"] \
-       -command {do_gitk $current_branch}
-set ui_visualize_current [.mbar.repository index last]
-.mbar.repository add command \
-       -label [mc "Visualize All Branch History"] \
-       -command {do_gitk --all}
-.mbar.repository add separator
-
-proc current_branch_write {args} {
-       global current_branch
-       .mbar.repository entryconf $::ui_browse_current \
-               -label [mc "Browse %s's Files" $current_branch]
-       .mbar.repository entryconf $::ui_visualize_current \
-               -label [mc "Visualize %s's History" $current_branch]
-}
-trace add variable current_branch write current_branch_write
-
-if {[is_enabled multicommit]} {
-       .mbar.repository add command -label [mc "Database Statistics"] \
-               -command do_stats
-
-       .mbar.repository add command -label [mc "Compress Database"] \
-               -command do_gc
-
-       .mbar.repository add command -label [mc "Verify Database"] \
-               -command do_fsck_objects
-
-       .mbar.repository add separator
-
-       if {[is_Cygwin]} {
-               .mbar.repository add command \
-                       -label [mc "Create Desktop Icon"] \
-                       -command do_cygwin_shortcut
-       } elseif {[is_Windows]} {
-               .mbar.repository add command \
-                       -label [mc "Create Desktop Icon"] \
-                       -command do_windows_shortcut
-       } elseif {[is_MacOSX]} {
-               .mbar.repository add command \
-                       -label [mc "Create Desktop Icon"] \
-                       -command do_macosx_app
-       }
-}
-
-.mbar.repository add command -label [mc Quit] \
-       -command do_quit \
-       -accelerator $M1T-Q
-
-# -- Edit Menu
-#
-menu .mbar.edit
-.mbar.edit add command -label [mc Undo] \
-       -command {catch {[focus] edit undo}} \
-       -accelerator $M1T-Z
-.mbar.edit add command -label [mc Redo] \
-       -command {catch {[focus] edit redo}} \
-       -accelerator $M1T-Y
-.mbar.edit add separator
-.mbar.edit add command -label [mc Cut] \
-       -command {catch {tk_textCut [focus]}} \
-       -accelerator $M1T-X
-.mbar.edit add command -label [mc Copy] \
-       -command {catch {tk_textCopy [focus]}} \
-       -accelerator $M1T-C
-.mbar.edit add command -label [mc Paste] \
-       -command {catch {tk_textPaste [focus]; [focus] see insert}} \
-       -accelerator $M1T-V
-.mbar.edit add command -label [mc Delete] \
-       -command {catch {[focus] delete sel.first sel.last}} \
-       -accelerator Del
-.mbar.edit add separator
-.mbar.edit add command -label [mc "Select All"] \
-       -command {catch {[focus] tag add sel 0.0 end}} \
-       -accelerator $M1T-A
-
-# -- Branch Menu
-#
-if {[is_enabled branch]} {
-       menu .mbar.branch
-
-       .mbar.branch add command -label [mc "Create..."] \
-               -command branch_create::dialog \
-               -accelerator $M1T-N
-       lappend disable_on_lock [list .mbar.branch entryconf \
-               [.mbar.branch index last] -state]
-
-       .mbar.branch add command -label [mc "Checkout..."] \
-               -command branch_checkout::dialog \
-               -accelerator $M1T-O
-       lappend disable_on_lock [list .mbar.branch entryconf \
-               [.mbar.branch index last] -state]
-
-       .mbar.branch add command -label [mc "Rename..."] \
-               -command branch_rename::dialog
-       lappend disable_on_lock [list .mbar.branch entryconf \
-               [.mbar.branch index last] -state]
-
-       .mbar.branch add command -label [mc "Delete..."] \
-               -command branch_delete::dialog
-       lappend disable_on_lock [list .mbar.branch entryconf \
-               [.mbar.branch index last] -state]
-
-       .mbar.branch add command -label [mc "Reset..."] \
-               -command merge::reset_hard
-       lappend disable_on_lock [list .mbar.branch entryconf \
-               [.mbar.branch index last] -state]
-}
-
-# -- Commit Menu
-#
-if {[is_enabled multicommit] || [is_enabled singlecommit]} {
-       menu .mbar.commit
-
-       .mbar.commit add radiobutton \
-               -label [mc "New Commit"] \
-               -command do_select_commit_type \
-               -variable selected_commit_type \
-               -value new
-       lappend disable_on_lock \
-               [list .mbar.commit entryconf [.mbar.commit index last] -state]
-
-       .mbar.commit add radiobutton \
-               -label [mc "Amend Last Commit"] \
-               -command do_select_commit_type \
-               -variable selected_commit_type \
-               -value amend
-       lappend disable_on_lock \
-               [list .mbar.commit entryconf [.mbar.commit index last] -state]
-
-       .mbar.commit add separator
-
-       .mbar.commit add command -label [mc Rescan] \
-               -command do_rescan \
-               -accelerator F5
-       lappend disable_on_lock \
-               [list .mbar.commit entryconf [.mbar.commit index last] -state]
-
-       .mbar.commit add command -label [mc "Stage To Commit"] \
-               -command do_add_selection \
-               -accelerator $M1T-T
-       lappend disable_on_lock \
-               [list .mbar.commit entryconf [.mbar.commit index last] -state]
-
-       .mbar.commit add command -label [mc "Stage Changed Files To Commit"] \
-               -command do_add_all \
-               -accelerator $M1T-I
-       lappend disable_on_lock \
-               [list .mbar.commit entryconf [.mbar.commit index last] -state]
-
-       .mbar.commit add command -label [mc "Unstage From Commit"] \
-               -command do_unstage_selection
-       lappend disable_on_lock \
-               [list .mbar.commit entryconf [.mbar.commit index last] -state]
-
-       .mbar.commit add command -label [mc "Revert Changes"] \
-               -command do_revert_selection
-       lappend disable_on_lock \
-               [list .mbar.commit entryconf [.mbar.commit index last] -state]
-
-       .mbar.commit add separator
-
-       .mbar.commit add command -label [mc "Sign Off"] \
-               -command do_signoff \
-               -accelerator $M1T-S
-
-       .mbar.commit add command -label [mc Commit@@verb] \
-               -command do_commit \
-               -accelerator $M1T-Return
-       lappend disable_on_lock \
-               [list .mbar.commit entryconf [.mbar.commit index last] -state]
-}
-
-# -- Merge Menu
-#
-if {[is_enabled branch]} {
-       menu .mbar.merge
-       .mbar.merge add command -label [mc "Local Merge..."] \
-               -command merge::dialog \
-               -accelerator $M1T-M
-       lappend disable_on_lock \
-               [list .mbar.merge entryconf [.mbar.merge index last] -state]
-       .mbar.merge add command -label [mc "Abort Merge..."] \
-               -command merge::reset_hard
-       lappend disable_on_lock \
-               [list .mbar.merge entryconf [.mbar.merge index last] -state]
-}
-
-# -- Transport Menu
-#
-if {[is_enabled transport]} {
-       menu .mbar.remote
-
-       .mbar.remote add command \
-               -label [mc "Push..."] \
-               -command do_push_anywhere \
-               -accelerator $M1T-P
-       .mbar.remote add command \
-               -label [mc "Delete..."] \
-               -command remote_branch_delete::dialog
-}
-
-if {[is_MacOSX]} {
-       # -- Apple Menu (Mac OS X only)
-       #
-       .mbar add cascade -label [mc Apple] -menu .mbar.apple
-       menu .mbar.apple
-
-       .mbar.apple add command -label [mc "About %s" [appname]] \
-               -command do_about
-       .mbar.apple add separator
-       .mbar.apple add command \
-               -label [mc "Preferences..."] \
-               -command do_options \
-               -accelerator $M1T-,
-       bind . <$M1B-,> do_options
-} else {
-       # -- Edit Menu
-       #
-       .mbar.edit add separator
-       .mbar.edit add command -label [mc "Options..."] \
-               -command do_options
-}
-
-# -- Help Menu
-#
-.mbar add cascade -label [mc Help] -menu .mbar.help
-menu .mbar.help
-
-if {![is_MacOSX]} {
-       .mbar.help add command -label [mc "About %s" [appname]] \
-               -command do_about
-}
-
-set browser {}
-catch {set browser $repo_config(instaweb.browser)}
-set doc_path [file dirname [gitexec]]
-set doc_path [file join $doc_path Documentation index.html]
-
-if {[is_Cygwin]} {
-       set doc_path [exec cygpath --mixed $doc_path]
-}
-
-if {$browser eq {}} {
-       if {[is_MacOSX]} {
-               set browser open
-       } elseif {[is_Cygwin]} {
-               set program_files [file dirname [exec cygpath --windir]]
-               set program_files [file join $program_files {Program Files}]
-               set firefox [file join $program_files {Mozilla Firefox} firefox.exe]
-               set ie [file join $program_files {Internet Explorer} IEXPLORE.EXE]
-               if {[file exists $firefox]} {
-                       set browser $firefox
-               } elseif {[file exists $ie]} {
-                       set browser $ie
-               }
-               unset program_files firefox ie
-       }
-}
-
-if {[file isfile $doc_path]} {
-       set doc_url "file:$doc_path"
-} else {
-       set doc_url {http://www.kernel.org/pub/software/scm/git/docs/}
-}
-
-if {$browser ne {}} {
-       .mbar.help add command -label [mc "Online Documentation"] \
-               -command [list exec $browser $doc_url &]
-}
-unset browser doc_path doc_url
-
-# -- Standard bindings
-#
-wm protocol . WM_DELETE_WINDOW do_quit
-bind all <$M1B-Key-q> do_quit
-bind all <$M1B-Key-Q> do_quit
-bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
-bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
-
-set subcommand_args {}
-proc usage {} {
-       puts stderr "usage: $::argv0 $::subcommand $::subcommand_args"
-       exit 1
-}
-
-# -- Not a normal commit type invocation?  Do that instead!
-#
-switch -- $subcommand {
-browser -
-blame {
-       set subcommand_args {rev? path}
-       if {$argv eq {}} usage
-       set head {}
-       set path {}
-       set is_path 0
-       foreach a $argv {
-               if {$is_path || [file exists $_prefix$a]} {
-                       if {$path ne {}} usage
-                       set path $_prefix$a
-                       break
-               } elseif {$a eq {--}} {
-                       if {$path ne {}} {
-                               if {$head ne {}} usage
-                               set head $path
-                               set path {}
-                       }
-                       set is_path 1
-               } elseif {$head eq {}} {
-                       if {$head ne {}} usage
-                       set head $a
-                       set is_path 1
-               } else {
-                       usage
-               }
-       }
-       unset is_path
-
-       if {$head ne {} && $path eq {}} {
-               set path $_prefix$head
-               set head {}
-       }
-
-       if {$head eq {}} {
-               load_current_branch
-       } else {
-               if {[regexp {^[0-9a-f]{1,39}$} $head]} {
-                       if {[catch {
-                                       set head [git rev-parse --verify $head]
-                               } err]} {
-                               puts stderr $err
-                               exit 1
-                       }
-               }
-               set current_branch $head
-       }
-
-       switch -- $subcommand {
-       browser {
-               if {$head eq {}} {
-                       if {$path ne {} && [file isdirectory $path]} {
-                               set head $current_branch
-                       } else {
-                               set head $path
-                               set path {}
-                       }
-               }
-               browser::new $head $path
-       }
-       blame   {
-               if {$head eq {} && ![file exists $path]} {
-                       puts stderr [mc "fatal: cannot stat path %s: No such file or directory" $path]
-                       exit 1
-               }
-               blame::new $head $path
-       }
-       }
-       return
-}
-citool -
-gui {
-       if {[llength $argv] != 0} {
-               puts -nonewline stderr "usage: $argv0"
-               if {$subcommand ne {gui}
-                       && [file tail $argv0] ne "git-$subcommand"} {
-                       puts -nonewline stderr " $subcommand"
-               }
-               puts stderr {}
-               exit 1
-       }
-       # fall through to setup UI for commits
-}
-default {
-       puts stderr "usage: $argv0 \[{blame|browser|citool}\]"
-       exit 1
-}
-}
-
-# -- Branch Control
-#
-frame .branch \
-       -borderwidth 1 \
-       -relief sunken
-label .branch.l1 \
-       -text [mc "Current Branch:"] \
-       -anchor w \
-       -justify left
-label .branch.cb \
-       -textvariable current_branch \
-       -anchor w \
-       -justify left
-pack .branch.l1 -side left
-pack .branch.cb -side left -fill x
-pack .branch -side top -fill x
-
-# -- Main Window Layout
-#
-panedwindow .vpane -orient horizontal
-panedwindow .vpane.files -orient vertical
-.vpane add .vpane.files -sticky nsew -height 100 -width 200
-pack .vpane -anchor n -side top -fill both -expand 1
-
-# -- Index File List
-#
-frame .vpane.files.index -height 100 -width 200
-label .vpane.files.index.title -text [mc "Staged Changes (Will Commit)"] \
-       -background lightgreen
-text $ui_index -background white -borderwidth 0 \
-       -width 20 -height 10 \
-       -wrap none \
-       -cursor $cursor_ptr \
-       -xscrollcommand {.vpane.files.index.sx set} \
-       -yscrollcommand {.vpane.files.index.sy set} \
-       -state disabled
-scrollbar .vpane.files.index.sx -orient h -command [list $ui_index xview]
-scrollbar .vpane.files.index.sy -orient v -command [list $ui_index yview]
-pack .vpane.files.index.title -side top -fill x
-pack .vpane.files.index.sx -side bottom -fill x
-pack .vpane.files.index.sy -side right -fill y
-pack $ui_index -side left -fill both -expand 1
-
-# -- Working Directory File List
-#
-frame .vpane.files.workdir -height 100 -width 200
-label .vpane.files.workdir.title -text [mc "Unstaged Changes"] \
-       -background lightsalmon
-text $ui_workdir -background white -borderwidth 0 \
-       -width 20 -height 10 \
-       -wrap none \
-       -cursor $cursor_ptr \
-       -xscrollcommand {.vpane.files.workdir.sx set} \
-       -yscrollcommand {.vpane.files.workdir.sy set} \
-       -state disabled
-scrollbar .vpane.files.workdir.sx -orient h -command [list $ui_workdir xview]
-scrollbar .vpane.files.workdir.sy -orient v -command [list $ui_workdir yview]
-pack .vpane.files.workdir.title -side top -fill x
-pack .vpane.files.workdir.sx -side bottom -fill x
-pack .vpane.files.workdir.sy -side right -fill y
-pack $ui_workdir -side left -fill both -expand 1
-
-.vpane.files add .vpane.files.workdir -sticky nsew
-.vpane.files add .vpane.files.index -sticky nsew
-
-foreach i [list $ui_index $ui_workdir] {
-       rmsel_tag $i
-       $i tag conf in_diff -background [$i tag cget in_sel -background]
-}
-unset i
-
-# -- Diff and Commit Area
-#
-frame .vpane.lower -height 300 -width 400
-frame .vpane.lower.commarea
-frame .vpane.lower.diff -relief sunken -borderwidth 1
-pack .vpane.lower.diff -fill both -expand 1
-pack .vpane.lower.commarea -side bottom -fill x
-.vpane add .vpane.lower -sticky nsew
-
-# -- Commit Area Buttons
-#
-frame .vpane.lower.commarea.buttons
-label .vpane.lower.commarea.buttons.l -text {} \
-       -anchor w \
-       -justify left
-pack .vpane.lower.commarea.buttons.l -side top -fill x
-pack .vpane.lower.commarea.buttons -side left -fill y
-
-button .vpane.lower.commarea.buttons.rescan -text [mc Rescan] \
-       -command do_rescan
-pack .vpane.lower.commarea.buttons.rescan -side top -fill x
-lappend disable_on_lock \
-       {.vpane.lower.commarea.buttons.rescan conf -state}
-
-button .vpane.lower.commarea.buttons.incall -text [mc "Stage Changed"] \
-       -command do_add_all
-pack .vpane.lower.commarea.buttons.incall -side top -fill x
-lappend disable_on_lock \
-       {.vpane.lower.commarea.buttons.incall conf -state}
-
-button .vpane.lower.commarea.buttons.signoff -text [mc "Sign Off"] \
-       -command do_signoff
-pack .vpane.lower.commarea.buttons.signoff -side top -fill x
-
-button .vpane.lower.commarea.buttons.commit -text [mc Commit@@verb] \
-       -command do_commit
-pack .vpane.lower.commarea.buttons.commit -side top -fill x
-lappend disable_on_lock \
-       {.vpane.lower.commarea.buttons.commit conf -state}
-
-button .vpane.lower.commarea.buttons.push -text [mc Push] \
-       -command do_push_anywhere
-pack .vpane.lower.commarea.buttons.push -side top -fill x
-
-# -- Commit Message Buffer
-#
-frame .vpane.lower.commarea.buffer
-frame .vpane.lower.commarea.buffer.header
-set ui_comm .vpane.lower.commarea.buffer.t
-set ui_coml .vpane.lower.commarea.buffer.header.l
-radiobutton .vpane.lower.commarea.buffer.header.new \
-       -text [mc "New Commit"] \
-       -command do_select_commit_type \
-       -variable selected_commit_type \
-       -value new
-lappend disable_on_lock \
-       [list .vpane.lower.commarea.buffer.header.new conf -state]
-radiobutton .vpane.lower.commarea.buffer.header.amend \
-       -text [mc "Amend Last Commit"] \
-       -command do_select_commit_type \
-       -variable selected_commit_type \
-       -value amend
-lappend disable_on_lock \
-       [list .vpane.lower.commarea.buffer.header.amend conf -state]
-label $ui_coml \
-       -anchor w \
-       -justify left
-proc trace_commit_type {varname args} {
-       global ui_coml commit_type
-       switch -glob -- $commit_type {
-       initial       {set txt [mc "Initial Commit Message:"]}
-       amend         {set txt [mc "Amended Commit Message:"]}
-       amend-initial {set txt [mc "Amended Initial Commit Message:"]}
-       amend-merge   {set txt [mc "Amended Merge Commit Message:"]}
-       merge         {set txt [mc "Merge Commit Message:"]}
-       *             {set txt [mc "Commit Message:"]}
-       }
-       $ui_coml conf -text $txt
-}
-trace add variable commit_type write trace_commit_type
-pack $ui_coml -side left -fill x
-pack .vpane.lower.commarea.buffer.header.amend -side right
-pack .vpane.lower.commarea.buffer.header.new -side right
-
-text $ui_comm -background white -borderwidth 1 \
-       -undo true \
-       -maxundo 20 \
-       -autoseparators true \
-       -relief sunken \
-       -width 75 -height 9 -wrap none \
-       -font font_diff \
-       -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
-scrollbar .vpane.lower.commarea.buffer.sby \
-       -command [list $ui_comm yview]
-pack .vpane.lower.commarea.buffer.header -side top -fill x
-pack .vpane.lower.commarea.buffer.sby -side right -fill y
-pack $ui_comm -side left -fill y
-pack .vpane.lower.commarea.buffer -side left -fill y
-
-# -- Commit Message Buffer Context Menu
-#
-set ctxm .vpane.lower.commarea.buffer.ctxm
-menu $ctxm -tearoff 0
-$ctxm add command \
-       -label [mc Cut] \
-       -command {tk_textCut $ui_comm}
-$ctxm add command \
-       -label [mc Copy] \
-       -command {tk_textCopy $ui_comm}
-$ctxm add command \
-       -label [mc Paste] \
-       -command {tk_textPaste $ui_comm}
-$ctxm add command \
-       -label [mc Delete] \
-       -command {$ui_comm delete sel.first sel.last}
-$ctxm add separator
-$ctxm add command \
-       -label [mc "Select All"] \
-       -command {focus $ui_comm;$ui_comm tag add sel 0.0 end}
-$ctxm add command \
-       -label [mc "Copy All"] \
-       -command {
-               $ui_comm tag add sel 0.0 end
-               tk_textCopy $ui_comm
-               $ui_comm tag remove sel 0.0 end
-       }
-$ctxm add separator
-$ctxm add command \
-       -label [mc "Sign Off"] \
-       -command do_signoff
-bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
-
-# -- Diff Header
-#
-proc trace_current_diff_path {varname args} {
-       global current_diff_path diff_actions file_states
-       if {$current_diff_path eq {}} {
-               set s {}
-               set f {}
-               set p {}
-               set o disabled
-       } else {
-               set p $current_diff_path
-               set s [mapdesc [lindex $file_states($p) 0] $p]
-               set f [mc "File:"]
-               set p [escape_path $p]
-               set o normal
-       }
-
-       .vpane.lower.diff.header.status configure -text $s
-       .vpane.lower.diff.header.file configure -text $f
-       .vpane.lower.diff.header.path configure -text $p
-       foreach w $diff_actions {
-               uplevel #0 $w $o
-       }
-}
-trace add variable current_diff_path write trace_current_diff_path
-
-frame .vpane.lower.diff.header -background gold
-label .vpane.lower.diff.header.status \
-       -background gold \
-       -width $max_status_desc \
-       -anchor w \
-       -justify left
-label .vpane.lower.diff.header.file \
-       -background gold \
-       -anchor w \
-       -justify left
-label .vpane.lower.diff.header.path \
-       -background gold \
-       -anchor w \
-       -justify left
-pack .vpane.lower.diff.header.status -side left
-pack .vpane.lower.diff.header.file -side left
-pack .vpane.lower.diff.header.path -fill x
-set ctxm .vpane.lower.diff.header.ctxm
-menu $ctxm -tearoff 0
-$ctxm add command \
-       -label [mc Copy] \
-       -command {
-               clipboard clear
-               clipboard append \
-                       -format STRING \
-                       -type STRING \
-                       -- $current_diff_path
-       }
-lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
-bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
-
-# -- Diff Body
-#
-frame .vpane.lower.diff.body
-set ui_diff .vpane.lower.diff.body.t
-text $ui_diff -background white -borderwidth 0 \
-       -width 80 -height 15 -wrap none \
-       -font font_diff \
-       -xscrollcommand {.vpane.lower.diff.body.sbx set} \
-       -yscrollcommand {.vpane.lower.diff.body.sby set} \
-       -state disabled
-scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
-       -command [list $ui_diff xview]
-scrollbar .vpane.lower.diff.body.sby -orient vertical \
-       -command [list $ui_diff yview]
-pack .vpane.lower.diff.body.sbx -side bottom -fill x
-pack .vpane.lower.diff.body.sby -side right -fill y
-pack $ui_diff -side left -fill both -expand 1
-pack .vpane.lower.diff.header -side top -fill x
-pack .vpane.lower.diff.body -side bottom -fill both -expand 1
-
-$ui_diff tag conf d_cr -elide true
-$ui_diff tag conf d_@ -foreground blue -font font_diffbold
-$ui_diff tag conf d_+ -foreground {#00a000}
-$ui_diff tag conf d_- -foreground red
-
-$ui_diff tag conf d_++ -foreground {#00a000}
-$ui_diff tag conf d_-- -foreground red
-$ui_diff tag conf d_+s \
-       -foreground {#00a000} \
-       -background {#e2effa}
-$ui_diff tag conf d_-s \
-       -foreground red \
-       -background {#e2effa}
-$ui_diff tag conf d_s+ \
-       -foreground {#00a000} \
-       -background ivory1
-$ui_diff tag conf d_s- \
-       -foreground red \
-       -background ivory1
-
-$ui_diff tag conf d<<<<<<< \
-       -foreground orange \
-       -font font_diffbold
-$ui_diff tag conf d======= \
-       -foreground orange \
-       -font font_diffbold
-$ui_diff tag conf d>>>>>>> \
-       -foreground orange \
-       -font font_diffbold
-
-$ui_diff tag raise sel
-
-# -- Diff Body Context Menu
-#
-set ctxm .vpane.lower.diff.body.ctxm
-menu $ctxm -tearoff 0
-$ctxm add command \
-       -label [mc Refresh] \
-       -command reshow_diff
-lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
-$ctxm add command \
-       -label [mc Copy] \
-       -command {tk_textCopy $ui_diff}
-lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
-$ctxm add command \
-       -label [mc "Select All"] \
-       -command {focus $ui_diff;$ui_diff tag add sel 0.0 end}
-lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
-$ctxm add command \
-       -label [mc "Copy All"] \
-       -command {
-               $ui_diff tag add sel 0.0 end
-               tk_textCopy $ui_diff
-               $ui_diff tag remove sel 0.0 end
-       }
-lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
-$ctxm add separator
-$ctxm add command \
-       -label [mc "Apply/Reverse Hunk"] \
-       -command {apply_hunk $cursorX $cursorY}
-set ui_diff_applyhunk [$ctxm index last]
-lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state]
-$ctxm add separator
-$ctxm add command \
-       -label [mc "Decrease Font Size"] \
-       -command {incr_font_size font_diff -1}
-lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
-$ctxm add command \
-       -label [mc "Increase Font Size"] \
-       -command {incr_font_size font_diff 1}
-lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
-$ctxm add separator
-$ctxm add command \
-       -label [mc "Show Less Context"] \
-       -command {if {$repo_config(gui.diffcontext) >= 1} {
-               incr repo_config(gui.diffcontext) -1
-               reshow_diff
-       }}
-lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
-$ctxm add command \
-       -label [mc "Show More Context"] \
-       -command {if {$repo_config(gui.diffcontext) < 99} {
-               incr repo_config(gui.diffcontext)
-               reshow_diff
-       }}
-lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
-$ctxm add separator
-$ctxm add command -label [mc "Options..."] \
-       -command do_options
-proc popup_diff_menu {ctxm x y X Y} {
-       global current_diff_path file_states
-       set ::cursorX $x
-       set ::cursorY $y
-       if {$::ui_index eq $::current_diff_side} {
-               set l [mc "Unstage Hunk From Commit"]
-       } else {
-               set l [mc "Stage Hunk For Commit"]
-       }
-       if {$::is_3way_diff
-               || $current_diff_path eq {}
-               || ![info exists file_states($current_diff_path)]
-               || {_O} eq [lindex $file_states($current_diff_path) 0]} {
-               set s disabled
-       } else {
-               set s normal
-       }
-       $ctxm entryconf $::ui_diff_applyhunk -state $s -label $l
-       tk_popup $ctxm $X $Y
-}
-bind_button3 $ui_diff [list popup_diff_menu $ctxm %x %y %X %Y]
-
-# -- Status Bar
-#
-set main_status [::status_bar::new .status]
-pack .status -anchor w -side bottom -fill x
-$main_status show [mc "Initializing..."]
-
-# -- Load geometry
-#
-catch {
-set gm $repo_config(gui.geometry)
-wm geometry . [lindex $gm 0]
-.vpane sash place 0 \
-       [lindex $gm 1] \
-       [lindex [.vpane sash coord 0] 1]
-.vpane.files sash place 0 \
-       [lindex [.vpane.files sash coord 0] 0] \
-       [lindex $gm 2]
-unset gm
-}
-
-# -- Key Bindings
-#
-bind $ui_comm <$M1B-Key-Return> {do_commit;break}
-bind $ui_comm <$M1B-Key-t> {do_add_selection;break}
-bind $ui_comm <$M1B-Key-T> {do_add_selection;break}
-bind $ui_comm <$M1B-Key-i> {do_add_all;break}
-bind $ui_comm <$M1B-Key-I> {do_add_all;break}
-bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
-bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
-bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
-bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
-bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
-bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
-bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
-bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
-
-bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
-bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
-bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
-bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
-bind $ui_diff <$M1B-Key-v> {break}
-bind $ui_diff <$M1B-Key-V> {break}
-bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
-bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
-bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
-bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
-bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
-bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
-bind $ui_diff <Key-k>         {catch {%W yview scroll -1 units};break}
-bind $ui_diff <Key-j>         {catch {%W yview scroll  1 units};break}
-bind $ui_diff <Key-h>         {catch {%W xview scroll -1 units};break}
-bind $ui_diff <Key-l>         {catch {%W xview scroll  1 units};break}
-bind $ui_diff <Control-Key-b> {catch {%W yview scroll -1 pages};break}
-bind $ui_diff <Control-Key-f> {catch {%W yview scroll  1 pages};break}
-bind $ui_diff <Button-1>   {focus %W}
-
-if {[is_enabled branch]} {
-       bind . <$M1B-Key-n> branch_create::dialog
-       bind . <$M1B-Key-N> branch_create::dialog
-       bind . <$M1B-Key-o> branch_checkout::dialog
-       bind . <$M1B-Key-O> branch_checkout::dialog
-       bind . <$M1B-Key-m> merge::dialog
-       bind . <$M1B-Key-M> merge::dialog
-}
-if {[is_enabled transport]} {
-       bind . <$M1B-Key-p> do_push_anywhere
-       bind . <$M1B-Key-P> do_push_anywhere
-}
-
-bind .   <Key-F5>     do_rescan
-bind .   <$M1B-Key-r> do_rescan
-bind .   <$M1B-Key-R> do_rescan
-bind .   <$M1B-Key-s> do_signoff
-bind .   <$M1B-Key-S> do_signoff
-bind .   <$M1B-Key-t> do_add_selection
-bind .   <$M1B-Key-T> do_add_selection
-bind .   <$M1B-Key-i> do_add_all
-bind .   <$M1B-Key-I> do_add_all
-bind .   <$M1B-Key-Return> do_commit
-foreach i [list $ui_index $ui_workdir] {
-       bind $i <Button-1>       "toggle_or_diff         $i %x %y; break"
-       bind $i <$M1B-Button-1>  "add_one_to_selection   $i %x %y; break"
-       bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
-}
-unset i
-
-set file_lists($ui_index) [list]
-set file_lists($ui_workdir) [list]
-
-wm title . "[appname] ([reponame]) [file normalize [file dirname [gitdir]]]"
-focus -force $ui_comm
-
-# -- Warn the user about environmental problems.  Cygwin's Tcl
-#    does *not* pass its env array onto any processes it spawns.
-#    This means that git processes get none of our environment.
-#
-if {[is_Cygwin]} {
-       set ignored_env 0
-       set suggest_user {}
-       set msg [mc "Possible environment issues exist.
-
-The following environment variables are probably
-going to be ignored by any Git subprocess run
-by %s:
-
-" [appname]]
-       foreach name [array names env] {
-               switch -regexp -- $name {
-               {^GIT_INDEX_FILE$} -
-               {^GIT_OBJECT_DIRECTORY$} -
-               {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} -
-               {^GIT_DIFF_OPTS$} -
-               {^GIT_EXTERNAL_DIFF$} -
-               {^GIT_PAGER$} -
-               {^GIT_TRACE$} -
-               {^GIT_CONFIG$} -
-               {^GIT_CONFIG_LOCAL$} -
-               {^GIT_(AUTHOR|COMMITTER)_DATE$} {
-                       append msg " - $name\n"
-                       incr ignored_env
-               }
-               {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} {
-                       append msg " - $name\n"
-                       incr ignored_env
-                       set suggest_user $name
-               }
-               }
-       }
-       if {$ignored_env > 0} {
-               append msg [mc "
-This is due to a known issue with the
-Tcl binary distributed by Cygwin."]
-
-               if {$suggest_user ne {}} {
-                       append msg [mc "
-
-A good replacement for %s
-is placing values for the user.name and
-user.email settings into your personal
-~/.gitconfig file.
-" $suggest_user]
-               }
-               warn_popup $msg
-       }
-       unset ignored_env msg suggest_user name
-}
-
-# -- Only initialize complex UI if we are going to stay running.
-#
-if {[is_enabled transport]} {
-       load_all_remotes
-
-       set n [.mbar.remote index end]
-       populate_push_menu
-       populate_fetch_menu
-       set n [expr {[.mbar.remote index end] - $n}]
-       if {$n > 0} {
-               .mbar.remote insert $n separator
-       }
-       unset n
-}
-
-if {[winfo exists $ui_comm]} {
-       set GITGUI_BCK_exists [load_message GITGUI_BCK]
-
-       # -- If both our backup and message files exist use the
-       #    newer of the two files to initialize the buffer.
-       #
-       if {$GITGUI_BCK_exists} {
-               set m [gitdir GITGUI_MSG]
-               if {[file isfile $m]} {
-                       if {[file mtime [gitdir GITGUI_BCK]] > [file mtime $m]} {
-                               catch {file delete [gitdir GITGUI_MSG]}
-                       } else {
-                               $ui_comm delete 0.0 end
-                               $ui_comm edit reset
-                               $ui_comm edit modified false
-                               catch {file delete [gitdir GITGUI_BCK]}
-                               set GITGUI_BCK_exists 0
-                       }
-               }
-               unset m
-       }
-
-       proc backup_commit_buffer {} {
-               global ui_comm GITGUI_BCK_exists
-
-               set m [$ui_comm edit modified]
-               if {$m || $GITGUI_BCK_exists} {
-                       set msg [string trim [$ui_comm get 0.0 end]]
-                       regsub -all -line {[ \r\t]+$} $msg {} msg
-
-                       if {$msg eq {}} {
-                               if {$GITGUI_BCK_exists} {
-                                       catch {file delete [gitdir GITGUI_BCK]}
-                                       set GITGUI_BCK_exists 0
-                               }
-                       } elseif {$m} {
-                               catch {
-                                       set fd [open [gitdir GITGUI_BCK] w]
-                                       puts -nonewline $fd $msg
-                                       close $fd
-                                       set GITGUI_BCK_exists 1
-                               }
-                       }
-
-                       $ui_comm edit modified false
-               }
-
-               set ::GITGUI_BCK_i [after 2000 backup_commit_buffer]
-       }
-
-       backup_commit_buffer
-}
-
-lock_index begin-read
-if {![winfo ismapped .]} {
-       wm deiconify .
-}
-after 1 do_rescan
-if {[is_enabled multicommit]} {
-       after 1000 hint_gc
-}
diff --git a/git-gui/.gitignore b/git-gui/.gitignore
new file mode 100644 (file)
index 0000000..6483b21
--- /dev/null
@@ -0,0 +1,8 @@
+.DS_Store
+config.mak
+Git Gui.app*
+git-gui.tcl
+GIT-VERSION-FILE
+GIT-GUI-VARS
+git-gui
+lib/tclIndex
diff --git a/git-gui/GIT-VERSION-GEN b/git-gui/GIT-VERSION-GEN
new file mode 100755 (executable)
index 0000000..cfe46a8
--- /dev/null
@@ -0,0 +1,80 @@
+#!/bin/sh
+
+GVF=GIT-VERSION-FILE
+DEF_VER=0.9.GITGUI
+
+LF='
+'
+
+tree_search ()
+{
+       head=$1
+       tree=$2
+       for p in $(git rev-list --parents --max-count=1 $head 2>/dev/null)
+       do
+               test $tree = $(git rev-parse $p^{tree} 2>/dev/null) &&
+               vn=$(git describe --abbrev=4 $p 2>/dev/null) &&
+               case "$vn" in
+               gitgui-[0-9]*) echo $vn; break;;
+               esac
+       done
+}
+
+# Always use the tarball version file if found, just
+# in case we are somehow contained in a larger git
+# repository that doesn't actually track our state.
+# (At least one package manager is doing this.)
+#
+# We may be a subproject, so try looking for the merge
+# commit that supplied this directory content if we are
+# not at the toplevel.  We probably will always be the
+# second parent in the commit, but we shouldn't rely on
+# that fact.
+#
+# If we are at the toplevel or the merge assumption fails
+# try looking for a gitgui-* tag.
+
+if test -f version &&
+   VN=$(cat version)
+then
+       : happy
+elif prefix="$(git rev-parse --show-prefix 2>/dev/null)"
+   test -n "$prefix" &&
+   head=$(git rev-list --max-count=1 HEAD -- . 2>/dev/null) &&
+   tree=$(git rev-parse --verify "HEAD:$prefix" 2>/dev/null) &&
+   VN=$(tree_search $head $tree)
+   case "$VN" in
+   gitgui-[0-9]*) : happy ;;
+   *) (exit 1) ;;
+   esac
+then
+       VN=$(echo "$VN" | sed -e 's/^gitgui-//;s/-/./g');
+elif VN=$(git describe --abbrev=4 HEAD 2>/dev/null) &&
+   case "$VN" in
+   gitgui-[0-9]*) : happy ;;
+   *) (exit 1) ;;
+   esac
+then
+       VN=$(echo "$VN" | sed -e 's/^gitgui-//;s/-/./g');
+else
+       VN="$DEF_VER"
+fi
+
+dirty=$(sh -c 'git diff-index --name-only HEAD' 2>/dev/null) || dirty=
+case "$dirty" in
+'')
+       ;;
+*)
+       VN="$VN-dirty" ;;
+esac
+
+if test -r $GVF
+then
+       VC=$(sed -e 's/^GITGUI_VERSION = //' <$GVF)
+else
+       VC=unset
+fi
+test "$VN" = "$VC" || {
+       echo >&2 "GITGUI_VERSION = $VN"
+       echo "GITGUI_VERSION = $VN" >$GVF
+}
diff --git a/git-gui/Makefile b/git-gui/Makefile
new file mode 100644 (file)
index 0000000..26ac4b6
--- /dev/null
@@ -0,0 +1,309 @@
+all::
+
+# Define V=1 to have a more verbose compile.
+#
+# Define NO_MSGFMT if you do not have msgfmt from the GNU gettext
+# package and want to use our rough pure Tcl po->msg translator.
+# TCL_PATH must be vaild for this to work.
+#
+
+GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
+       @$(SHELL_PATH) ./GIT-VERSION-GEN
+-include GIT-VERSION-FILE
+
+uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
+uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not')
+
+SCRIPT_SH = git-gui.sh
+GITGUI_MAIN := git-gui
+GITGUI_BUILT_INS = git-citool
+ALL_LIBFILES = $(wildcard lib/*.tcl)
+PRELOAD_FILES = lib/class.tcl
+NONTCL_LIBFILES = \
+       lib/git-gui.ico \
+       $(wildcard lib/win32_*.js) \
+#end NONTCL_LIBFILES
+
+ifndef SHELL_PATH
+       SHELL_PATH = /bin/sh
+endif
+
+ifndef gitexecdir
+       gitexecdir := $(shell git --exec-path)
+endif
+
+ifndef sharedir
+       sharedir := $(dir $(gitexecdir))share
+endif
+
+ifndef INSTALL
+       INSTALL = install
+endif
+
+RM_RF     ?= rm -rf
+RMDIR     ?= rmdir
+
+INSTALL_D0 = $(INSTALL) -d -m 755 # space is required here
+INSTALL_D1 =
+INSTALL_R0 = $(INSTALL) -m 644 # space is required here
+INSTALL_R1 =
+INSTALL_X0 = $(INSTALL) -m 755 # space is required here
+INSTALL_X1 =
+INSTALL_A0 = find # space is required here
+INSTALL_A1 = | cpio -pud
+INSTALL_L0 = rm -f # space is required here
+INSTALL_L1 = && ln # space is required here
+INSTALL_L2 =
+INSTALL_L3 =
+
+REMOVE_D0  = $(RMDIR) # space is required here
+REMOVE_D1  = || true
+REMOVE_F0  = $(RM_RF) # space is required here
+REMOVE_F1  =
+CLEAN_DST  = true
+
+ifndef V
+       QUIET          = @
+       QUIET_GEN      = $(QUIET)echo '   ' GEN '$@' &&
+       QUIET_INDEX    = $(QUIET)echo '   ' INDEX $(dir $@) &&
+       QUIET_MSGFMT0  = $(QUIET)printf '    MSGFMT %12s ' $@ && v=`
+       QUIET_MSGFMT1  = 2>&1` && echo "$$v" | sed -e 's/fuzzy translations/fuzzy/' | sed -e 's/ messages//g'
+       QUIET_2DEVNULL = 2>/dev/null
+
+       INSTALL_D0 = dir=
+       INSTALL_D1 = && echo ' ' DEST $$dir && $(INSTALL) -d -m 755 "$$dir"
+       INSTALL_R0 = src=
+       INSTALL_R1 = && echo '   ' INSTALL 644 `basename $$src` && $(INSTALL) -m 644 $$src
+       INSTALL_X0 = src=
+       INSTALL_X1 = && echo '   ' INSTALL 755 `basename $$src` && $(INSTALL) -m 755 $$src
+       INSTALL_A0 = src=
+       INSTALL_A1 = && echo '   ' INSTALL '   ' `basename "$$src"` && find "$$src" | cpio -pud
+
+       INSTALL_L0 = dst=
+       INSTALL_L1 = && src=
+       INSTALL_L2 = && dst=
+       INSTALL_L3 = && echo '   ' 'LINK       ' `basename "$$dst"` '->' `basename "$$src"` && rm -f "$$dst" && ln "$$src" "$$dst"
+
+       CLEAN_DST = echo ' ' UNINSTALL
+       REMOVE_D0 = dir=
+       REMOVE_D1 = && echo ' ' REMOVE $$dir && test -d "$$dir" && $(RMDIR) "$$dir" || true
+       REMOVE_F0 = dst=
+       REMOVE_F1 = && echo '   ' REMOVE `basename "$$dst"` && $(RM_RF) "$$dst"
+endif
+
+TCL_PATH   ?= tclsh
+TCLTK_PATH ?= wish
+TKFRAMEWORK = /Library/Frameworks/Tk.framework/Resources/Wish.app
+
+ifeq ($(findstring $(MAKEFLAGS),s),s)
+QUIET_GEN =
+endif
+
+-include config.mak
+
+DESTDIR_SQ = $(subst ','\'',$(DESTDIR))
+gitexecdir_SQ = $(subst ','\'',$(gitexecdir))
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+TCL_PATH_SQ = $(subst ','\'',$(TCL_PATH))
+TCLTK_PATH_SQ = $(subst ','\'',$(TCLTK_PATH))
+TCLTK_PATH_SED = $(subst ','\'',$(subst \,\\,$(TCLTK_PATH)))
+
+gg_libdir ?= $(sharedir)/git-gui/lib
+libdir_SQ  = $(subst ','\'',$(gg_libdir))
+libdir_SED = $(subst ','\'',$(subst \,\\,$(gg_libdir)))
+exedir     = $(dir $(gitexecdir))share/git-gui/lib
+
+GITGUI_SCRIPT   := $$0
+GITGUI_RELATIVE :=
+GITGUI_MACOSXAPP :=
+
+ifeq ($(exedir),$(gg_libdir))
+       GITGUI_RELATIVE := 1
+endif
+
+ifeq ($(uname_O),Cygwin)
+       GITGUI_SCRIPT := `cygpath --windows --absolute "$(GITGUI_SCRIPT)"`
+       ifeq ($(GITGUI_RELATIVE),)
+               gg_libdir := $(shell cygpath --windows --absolute "$(gg_libdir)")
+       endif
+endif
+ifeq ($(uname_S),Darwin)
+       ifeq ($(shell test -d $(TKFRAMEWORK) && echo y),y)
+               GITGUI_MACOSXAPP := YesPlease
+       endif
+endif
+ifneq (,$(findstring MINGW,$(uname_S)))
+       NO_MSGFMT=1
+       GITGUI_WINDOWS_WRAPPER := YesPlease
+endif
+
+ifdef GITGUI_MACOSXAPP
+GITGUI_MAIN := git-gui.tcl
+
+git-gui: GIT-VERSION-FILE GIT-GUI-VARS
+       $(QUIET_GEN)rm -f $@ $@+ && \
+       echo '#!$(SHELL_PATH_SQ)' >$@+ && \
+       echo 'if test "z$$*" = zversion ||' >>$@+ && \
+       echo '   test "z$$*" = z--version' >>$@+ && \
+       echo then >>$@+ && \
+       echo '  'echo \'git-gui version '$(GITGUI_VERSION)'\' >>$@+ && \
+       echo else >>$@+ && \
+       echo '  'exec \''$(libdir_SQ)/Git Gui.app/Contents/MacOS/Wish'\' \
+               '"$$0" "$$@"' >>$@+ && \
+       echo fi >>$@+ && \
+       chmod +x $@+ && \
+       mv $@+ $@
+
+Git\ Gui.app: GIT-VERSION-FILE GIT-GUI-VARS \
+               macosx/Info.plist \
+               macosx/git-gui.icns \
+               macosx/AppMain.tcl \
+               $(TKFRAMEWORK)/Contents/MacOS/Wish
+       $(QUIET_GEN)rm -rf '$@' '$@'+ && \
+       mkdir -p '$@'+/Contents/MacOS && \
+       mkdir -p '$@'+/Contents/Resources/Scripts && \
+       cp '$(subst ','\'',$(TKFRAMEWORK))/Contents/MacOS/Wish' \
+               '$@'+/Contents/MacOS && \
+       cp macosx/git-gui.icns '$@'+/Contents/Resources && \
+       sed -e 's/@@GITGUI_VERSION@@/$(GITGUI_VERSION)/g' \
+               macosx/Info.plist \
+               >'$@'+/Contents/Info.plist && \
+       sed -e 's|@@gitexecdir@@|$(gitexecdir_SQ)|' \
+               -e 's|@@GITGUI_LIBDIR@@|$(libdir_SED)|' \
+               macosx/AppMain.tcl \
+               >'$@'+/Contents/Resources/Scripts/AppMain.tcl && \
+       mv '$@'+ '$@'
+endif
+
+ifdef GITGUI_WINDOWS_WRAPPER
+GITGUI_MAIN := git-gui.tcl
+
+git-gui: windows/git-gui.sh
+       cp $< $@
+endif
+
+$(GITGUI_MAIN): git-gui.sh GIT-VERSION-FILE GIT-GUI-VARS
+       $(QUIET_GEN)rm -f $@ $@+ && \
+       sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
+               -e '1,30s|^ argv0=$$0| argv0=$(GITGUI_SCRIPT)|' \
+               -e '1,30s|^ exec wish | exec '\''$(TCLTK_PATH_SED)'\'' |' \
+               -e 's/@@GITGUI_VERSION@@/$(GITGUI_VERSION)/g' \
+               -e 's|@@GITGUI_RELATIVE@@|$(GITGUI_RELATIVE)|' \
+               -e '$(GITGUI_RELATIVE)s|@@GITGUI_LIBDIR@@|$(libdir_SED)|' \
+               git-gui.sh >$@+ && \
+       chmod +x $@+ && \
+       mv $@+ $@
+
+XGETTEXT   ?= xgettext
+ifdef NO_MSGFMT
+       MSGFMT ?= $(TCL_PATH) po/po2msg.sh
+else
+       MSGFMT ?= msgfmt
+endif
+
+msgsdir     = $(gg_libdir)/msgs
+msgsdir_SQ  = $(subst ','\'',$(msgsdir))
+PO_TEMPLATE = po/git-gui.pot
+ALL_POFILES = $(wildcard po/*.po)
+ALL_MSGFILES = $(subst .po,.msg,$(ALL_POFILES))
+
+$(PO_TEMPLATE): $(SCRIPT_SH) $(ALL_LIBFILES)
+       $(XGETTEXT) -kmc -LTcl -o $@ $(SCRIPT_SH) $(ALL_LIBFILES)
+update-po:: $(PO_TEMPLATE)
+       $(foreach p, $(ALL_POFILES), echo Updating $p ; msgmerge -U $p $(PO_TEMPLATE) ; )
+$(ALL_MSGFILES): %.msg : %.po
+       $(QUIET_MSGFMT0)$(MSGFMT) --statistics --tcl -l $(basename $(notdir $<)) -d $(dir $@) $< $(QUIET_MSGFMT1)
+
+lib/tclIndex: $(ALL_LIBFILES) GIT-GUI-VARS
+       $(QUIET_INDEX)if echo \
+         $(foreach p,$(PRELOAD_FILES),source $p\;) \
+         auto_mkindex lib '*.tcl' \
+       | $(TCL_PATH) $(QUIET_2DEVNULL); then : ok; \
+       else \
+        echo 1>&2 "    * $(TCL_PATH) failed; using unoptimized loading"; \
+        rm -f $@ ; \
+        echo '# Autogenerated by git-gui Makefile' >$@ && \
+        echo >>$@ && \
+        $(foreach p,$(PRELOAD_FILES) $(ALL_LIBFILES),echo '$(subst lib/,,$p)' >>$@ &&) \
+        echo >>$@ ; \
+       fi
+
+TRACK_VARS = \
+       $(subst ','\'',SHELL_PATH='$(SHELL_PATH_SQ)') \
+       $(subst ','\'',TCL_PATH='$(TCL_PATH_SQ)') \
+       $(subst ','\'',TCLTK_PATH='$(TCLTK_PATH_SQ)') \
+       $(subst ','\'',gitexecdir='$(gitexecdir_SQ)') \
+       $(subst ','\'',gg_libdir='$(libdir_SQ)') \
+       GITGUI_MACOSXAPP=$(GITGUI_MACOSXAPP) \
+#end TRACK_VARS
+
+GIT-GUI-VARS: .FORCE-GIT-GUI-VARS
+       @VARS='$(TRACK_VARS)'; \
+       if test x"$$VARS" != x"`cat $@ 2>/dev/null`" ; then \
+               echo 1>&2 "    * new locations or Tcl/Tk interpreter"; \
+               echo 1>$@ "$$VARS"; \
+       fi
+
+ifdef GITGUI_MACOSXAPP
+all:: git-gui Git\ Gui.app
+endif
+ifdef GITGUI_WINDOWS_WRAPPER
+all:: git-gui
+endif
+all:: $(GITGUI_MAIN) lib/tclIndex $(ALL_MSGFILES)
+
+install: all
+       $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(gitexecdir_SQ)' $(INSTALL_D1)
+       $(QUIET)$(INSTALL_X0)git-gui $(INSTALL_X1) '$(DESTDIR_SQ)$(gitexecdir_SQ)'
+       $(QUIET)$(foreach p,$(GITGUI_BUILT_INS), $(INSTALL_L0)'$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' $(INSTALL_L1)'$(DESTDIR_SQ)$(gitexecdir_SQ)/git-gui' $(INSTALL_L2)'$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' $(INSTALL_L3) &&) true
+ifdef GITGUI_WINDOWS_WRAPPER
+       $(QUIET)$(INSTALL_R0)git-gui.tcl $(INSTALL_R1) '$(DESTDIR_SQ)$(gitexecdir_SQ)'
+endif
+       $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(libdir_SQ)' $(INSTALL_D1)
+       $(QUIET)$(INSTALL_R0)lib/tclIndex $(INSTALL_R1) '$(DESTDIR_SQ)$(libdir_SQ)'
+ifdef GITGUI_MACOSXAPP
+       $(QUIET)$(INSTALL_A0)'Git Gui.app' $(INSTALL_A1) '$(DESTDIR_SQ)$(libdir_SQ)'
+       $(QUIET)$(INSTALL_X0)git-gui.tcl $(INSTALL_X1) '$(DESTDIR_SQ)$(libdir_SQ)'
+endif
+       $(QUIET)$(foreach p,$(ALL_LIBFILES) $(NONTCL_LIBFILES), $(INSTALL_R0)$p $(INSTALL_R1) '$(DESTDIR_SQ)$(libdir_SQ)' &&) true
+       $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(msgsdir_SQ)' $(INSTALL_D1)
+       $(QUIET)$(foreach p,$(ALL_MSGFILES), $(INSTALL_R0)$p $(INSTALL_R1) '$(DESTDIR_SQ)$(msgsdir_SQ)' &&) true
+
+uninstall:
+       $(QUIET)$(CLEAN_DST) '$(DESTDIR_SQ)$(gitexecdir_SQ)'
+       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/git-gui $(REMOVE_F1)
+       $(QUIET)$(foreach p,$(GITGUI_BUILT_INS), $(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/$p $(REMOVE_F1) &&) true
+ifdef GITGUI_WINDOWS_WRAPPER
+       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/git-gui.tcl $(REMOVE_F1)
+endif
+       $(QUIET)$(CLEAN_DST) '$(DESTDIR_SQ)$(libdir_SQ)'
+       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)'/tclIndex $(REMOVE_F1)
+ifdef GITGUI_MACOSXAPP
+       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)/Git Gui.app' $(REMOVE_F1)
+       $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)'/git-gui.tcl $(REMOVE_F1)
+endif
+       $(QUIET)$(foreach p,$(ALL_LIBFILES) $(NONTCL_LIBFILES), $(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)'/$(notdir $p) $(REMOVE_F1) &&) true
+       $(QUIET)$(CLEAN_DST) '$(DESTDIR_SQ)$(msgsdir_SQ)'
+       $(QUIET)$(foreach p,$(ALL_MSGFILES), $(REMOVE_F0)'$(DESTDIR_SQ)$(msgsdir_SQ)'/$(notdir $p) $(REMOVE_F1) &&) true
+       $(QUIET)$(REMOVE_D0)'$(DESTDIR_SQ)$(gitexecdir_SQ)' $(REMOVE_D1)
+       $(QUIET)$(REMOVE_D0)'$(DESTDIR_SQ)$(msgsdir_SQ)' $(REMOVE_D1)
+       $(QUIET)$(REMOVE_D0)'$(DESTDIR_SQ)$(libdir_SQ)' $(REMOVE_D1)
+       $(QUIET)$(REMOVE_D0)`dirname '$(DESTDIR_SQ)$(libdir_SQ)'` $(REMOVE_D1)
+
+dist-version:
+       @mkdir -p $(TARDIR)
+       @echo $(GITGUI_VERSION) > $(TARDIR)/version
+
+clean::
+       $(RM_RF) $(GITGUI_MAIN) lib/tclIndex po/*.msg
+       $(RM_RF) GIT-VERSION-FILE GIT-GUI-VARS
+ifdef GITGUI_MACOSXAPP
+       $(RM_RF) 'Git Gui.app'* git-gui
+endif
+ifdef GITGUI_WINDOWS_WRAPPER
+       $(RM_RF) git-gui
+endif
+
+.PHONY: all install uninstall dist-version clean
+.PHONY: .FORCE-GIT-VERSION-FILE
+.PHONY: .FORCE-GIT-GUI-VARS
diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh
new file mode 100755 (executable)
index 0000000..1fca11f
--- /dev/null
@@ -0,0 +1,2841 @@
+#!/bin/sh
+# Tcl ignores the next line -*- tcl -*- \
+ if test "z$*" = zversion \
+ || test "z$*" = z--version; \
+ then \
+       echo 'git-gui version @@GITGUI_VERSION@@'; \
+       exit; \
+ fi; \
+ argv0=$0; \
+ exec wish "$argv0" -- "$@"
+
+set appvers {@@GITGUI_VERSION@@}
+set copyright [encoding convertfrom utf-8 {
+Copyright © 2006, 2007 Shawn Pearce, et. al.
+
+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}]
+
+######################################################################
+##
+## Tcl/Tk sanity check
+
+if {[catch {package require Tcl 8.4} err]
+ || [catch {package require Tk  8.4} err]
+} {
+       catch {wm withdraw .}
+       tk_messageBox \
+               -icon error \
+               -type ok \
+               -title [mc "git-gui: fatal error"] \
+               -message $err
+       exit 1
+}
+
+catch {rename send {}} ; # What an evil concept...
+
+######################################################################
+##
+## locate our library
+
+set oguilib {@@GITGUI_LIBDIR@@}
+set oguirel {@@GITGUI_RELATIVE@@}
+if {$oguirel eq {1}} {
+       set oguilib [file dirname [file dirname [file normalize $argv0]]]
+       set oguilib [file join $oguilib share git-gui lib]
+       set oguimsg [file join $oguilib msgs]
+} elseif {[string match @@* $oguirel]} {
+       set oguilib [file join [file dirname [file normalize $argv0]] lib]
+       set oguimsg [file join [file dirname [file normalize $argv0]] po]
+} else {
+       set oguimsg [file join $oguilib msgs]
+}
+unset oguirel
+
+######################################################################
+##
+## enable verbose loading?
+
+if {![catch {set _verbose $env(GITGUI_VERBOSE)}]} {
+       unset _verbose
+       rename auto_load real__auto_load
+       proc auto_load {name args} {
+               puts stderr "auto_load $name"
+               return [uplevel 1 real__auto_load $name $args]
+       }
+       rename source real__source
+       proc source {name} {
+               puts stderr "source    $name"
+               uplevel 1 real__source $name
+       }
+}
+
+######################################################################
+##
+## Internationalization (i18n) through msgcat and gettext. See
+## http://www.gnu.org/software/gettext/manual/html_node/Tcl.html
+
+package require msgcat
+
+proc _mc_trim {fmt} {
+       set cmk [string first @@ $fmt]
+       if {$cmk > 0} {
+               return [string range $fmt 0 [expr {$cmk - 1}]]
+       }
+       return $fmt
+}
+
+proc mc {en_fmt args} {
+       set fmt [_mc_trim [::msgcat::mc $en_fmt]]
+       if {[catch {set msg [eval [list format $fmt] $args]} err]} {
+               set msg [eval [list format [_mc_trim $en_fmt]] $args]
+       }
+       return $msg
+}
+
+proc strcat {args} {
+       return [join $args {}]
+}
+
+::msgcat::mcload $oguimsg
+unset oguimsg
+
+######################################################################
+##
+## read only globals
+
+set _appname {Git Gui}
+set _gitdir {}
+set _gitexec {}
+set _reponame {}
+set _iscygwin {}
+set _search_path {}
+
+proc appname {} {
+       global _appname
+       return $_appname
+}
+
+proc gitdir {args} {
+       global _gitdir
+       if {$args eq {}} {
+               return $_gitdir
+       }
+       return [eval [list file join $_gitdir] $args]
+}
+
+proc gitexec {args} {
+       global _gitexec
+       if {$_gitexec eq {}} {
+               if {[catch {set _gitexec [git --exec-path]} err]} {
+                       error "Git not installed?\n\n$err"
+               }
+               if {[is_Cygwin]} {
+                       set _gitexec [exec cygpath \
+                               --windows \
+                               --absolute \
+                               $_gitexec]
+               } else {
+                       set _gitexec [file normalize $_gitexec]
+               }
+       }
+       if {$args eq {}} {
+               return $_gitexec
+       }
+       return [eval [list file join $_gitexec] $args]
+}
+
+proc reponame {} {
+       return $::_reponame
+}
+
+proc is_MacOSX {} {
+       if {[tk windowingsystem] eq {aqua}} {
+               return 1
+       }
+       return 0
+}
+
+proc is_Windows {} {
+       if {$::tcl_platform(platform) eq {windows}} {
+               return 1
+       }
+       return 0
+}
+
+proc is_Cygwin {} {
+       global _iscygwin
+       if {$_iscygwin eq {}} {
+               if {$::tcl_platform(platform) eq {windows}} {
+                       if {[catch {set p [exec cygpath --windir]} err]} {
+                               set _iscygwin 0
+                       } else {
+                               set _iscygwin 1
+                       }
+               } else {
+                       set _iscygwin 0
+               }
+       }
+       return $_iscygwin
+}
+
+proc is_enabled {option} {
+       global enabled_options
+       if {[catch {set on $enabled_options($option)}]} {return 0}
+       return $on
+}
+
+proc enable_option {option} {
+       global enabled_options
+       set enabled_options($option) 1
+}
+
+proc disable_option {option} {
+       global enabled_options
+       set enabled_options($option) 0
+}
+
+######################################################################
+##
+## config
+
+proc is_many_config {name} {
+       switch -glob -- $name {
+       gui.recentrepo -
+       remote.*.fetch -
+       remote.*.push
+               {return 1}
+       *
+               {return 0}
+       }
+}
+
+proc is_config_true {name} {
+       global repo_config
+       if {[catch {set v $repo_config($name)}]} {
+               return 0
+       } elseif {$v eq {true} || $v eq {1} || $v eq {yes}} {
+               return 1
+       } else {
+               return 0
+       }
+}
+
+proc get_config {name} {
+       global repo_config
+       if {[catch {set v $repo_config($name)}]} {
+               return {}
+       } else {
+               return $v
+       }
+}
+
+######################################################################
+##
+## handy utils
+
+proc _git_cmd {name} {
+       global _git_cmd_path
+
+       if {[catch {set v $_git_cmd_path($name)}]} {
+               switch -- $name {
+                 version   -
+               --version   -
+               --exec-path { return [list $::_git $name] }
+               }
+
+               set p [gitexec git-$name$::_search_exe]
+               if {[file exists $p]} {
+                       set v [list $p]
+               } elseif {[is_Windows] && [file exists [gitexec git-$name]]} {
+                       # Try to determine what sort of magic will make
+                       # git-$name go and do its thing, because native
+                       # Tcl on Windows doesn't know it.
+                       #
+                       set p [gitexec git-$name]
+                       set f [open $p r]
+                       set s [gets $f]
+                       close $f
+
+                       switch -glob -- [lindex $s 0] {
+                       #!*sh     { set i sh     }
+                       #!*perl   { set i perl   }
+                       #!*python { set i python }
+                       default   { error "git-$name is not supported: $s" }
+                       }
+
+                       upvar #0 _$i interp
+                       if {![info exists interp]} {
+                               set interp [_which $i]
+                       }
+                       if {$interp eq {}} {
+                               error "git-$name requires $i (not in PATH)"
+                       }
+                       set v [concat [list $interp] [lrange $s 1 end] [list $p]]
+               } else {
+                       # Assume it is builtin to git somehow and we
+                       # aren't actually able to see a file for it.
+                       #
+                       set v [list $::_git $name]
+               }
+               set _git_cmd_path($name) $v
+       }
+       return $v
+}
+
+proc _which {what} {
+       global env _search_exe _search_path
+
+       if {$_search_path eq {}} {
+               if {[is_Cygwin] && [regexp {^(/|\.:)} $env(PATH)]} {
+                       set _search_path [split [exec cygpath \
+                               --windows \
+                               --path \
+                               --absolute \
+                               $env(PATH)] {;}]
+                       set _search_exe .exe
+               } elseif {[is_Windows]} {
+                       set gitguidir [file dirname [info script]]
+                       regsub -all ";" $gitguidir "\\;" gitguidir
+                       set env(PATH) "$gitguidir;$env(PATH)"
+                       set _search_path [split $env(PATH) {;}]
+                       set _search_exe .exe
+               } else {
+                       set _search_path [split $env(PATH) :]
+                       set _search_exe {}
+               }
+       }
+
+       foreach p $_search_path {
+               set p [file join $p $what$_search_exe]
+               if {[file exists $p]} {
+                       return [file normalize $p]
+               }
+       }
+       return {}
+}
+
+proc _lappend_nice {cmd_var} {
+       global _nice
+       upvar $cmd_var cmd
+
+       if {![info exists _nice]} {
+               set _nice [_which nice]
+       }
+       if {$_nice ne {}} {
+               lappend cmd $_nice
+       }
+}
+
+proc git {args} {
+       set opt [list exec]
+
+       while {1} {
+               switch -- [lindex $args 0] {
+               --nice {
+                       _lappend_nice opt
+               }
+
+               default {
+                       break
+               }
+
+               }
+
+               set args [lrange $args 1 end]
+       }
+
+       set cmdp [_git_cmd [lindex $args 0]]
+       set args [lrange $args 1 end]
+
+       return [eval $opt $cmdp $args]
+}
+
+proc _open_stdout_stderr {cmd} {
+       if {[catch {
+                       set fd [open $cmd r]
+               } err]} {
+               if {   [lindex $cmd end] eq {2>@1}
+                   && $err eq {can not find channel named "1"}
+                       } {
+                       # Older versions of Tcl 8.4 don't have this 2>@1 IO
+                       # redirect operator.  Fallback to |& cat for those.
+                       # The command was not actually started, so its safe
+                       # to try to start it a second time.
+                       #
+                       set fd [open [concat \
+                               [lrange $cmd 0 end-1] \
+                               [list |& cat] \
+                               ] r]
+               } else {
+                       error $err
+               }
+       }
+       fconfigure $fd -eofchar {}
+       return $fd
+}
+
+proc git_read {args} {
+       set opt [list |]
+
+       while {1} {
+               switch -- [lindex $args 0] {
+               --nice {
+                       _lappend_nice opt
+               }
+
+               --stderr {
+                       lappend args 2>@1
+               }
+
+               default {
+                       break
+               }
+
+               }
+
+               set args [lrange $args 1 end]
+       }
+
+       set cmdp [_git_cmd [lindex $args 0]]
+       set args [lrange $args 1 end]
+
+       return [_open_stdout_stderr [concat $opt $cmdp $args]]
+}
+
+proc git_write {args} {
+       set opt [list |]
+
+       while {1} {
+               switch -- [lindex $args 0] {
+               --nice {
+                       _lappend_nice opt
+               }
+
+               default {
+                       break
+               }
+
+               }
+
+               set args [lrange $args 1 end]
+       }
+
+       set cmdp [_git_cmd [lindex $args 0]]
+       set args [lrange $args 1 end]
+
+       return [open [concat $opt $cmdp $args] w]
+}
+
+proc sq {value} {
+       regsub -all ' $value "'\\''" value
+       return "'$value'"
+}
+
+proc load_current_branch {} {
+       global current_branch is_detached
+
+       set fd [open [gitdir HEAD] r]
+       if {[gets $fd ref] < 1} {
+               set ref {}
+       }
+       close $fd
+
+       set pfx {ref: refs/heads/}
+       set len [string length $pfx]
+       if {[string equal -length $len $pfx $ref]} {
+               # We're on a branch.  It might not exist.  But
+               # HEAD looks good enough to be a branch.
+               #
+               set current_branch [string range $ref $len end]
+               set is_detached 0
+       } else {
+               # Assume this is a detached head.
+               #
+               set current_branch HEAD
+               set is_detached 1
+       }
+}
+
+auto_load tk_optionMenu
+rename tk_optionMenu real__tkOptionMenu
+proc tk_optionMenu {w varName args} {
+       set m [eval real__tkOptionMenu $w $varName $args]
+       $m configure -font font_ui
+       $w configure -font font_ui
+       return $m
+}
+
+proc rmsel_tag {text} {
+       $text tag conf sel \
+               -background [$text cget -background] \
+               -foreground [$text cget -foreground] \
+               -borderwidth 0
+       $text tag conf in_sel -background lightgray
+       bind $text <Motion> break
+       return $text
+}
+
+set root_exists 0
+bind . <Visibility> {
+       bind . <Visibility> {}
+       set root_exists 1
+}
+
+if {[is_Windows]} {
+       wm iconbitmap . -default $oguilib/git-gui.ico
+}
+
+######################################################################
+##
+## config defaults
+
+set cursor_ptr arrow
+font create font_diff -family Courier -size 10
+font create font_ui
+catch {
+       label .dummy
+       eval font configure font_ui [font actual [.dummy cget -font]]
+       destroy .dummy
+}
+
+font create font_uiitalic
+font create font_uibold
+font create font_diffbold
+font create font_diffitalic
+
+foreach class {Button Checkbutton Entry Label
+               Labelframe Listbox Menu Message
+               Radiobutton Spinbox Text} {
+       option add *$class.font font_ui
+}
+unset class
+
+if {[is_Windows] || [is_MacOSX]} {
+       option add *Menu.tearOff 0
+}
+
+if {[is_MacOSX]} {
+       set M1B M1
+       set M1T Cmd
+} else {
+       set M1B Control
+       set M1T Ctrl
+}
+
+proc bind_button3 {w cmd} {
+       bind $w <Any-Button-3> $cmd
+       if {[is_MacOSX]} {
+               # Mac OS X sends Button-2 on right click through three-button mouse,
+               # or through trackpad right-clicking (two-finger touch + click).
+               bind $w <Any-Button-2> $cmd
+               bind $w <Control-Button-1> $cmd
+       }
+}
+
+proc apply_config {} {
+       global repo_config font_descs
+
+       foreach option $font_descs {
+               set name [lindex $option 0]
+               set font [lindex $option 1]
+               if {[catch {
+                       set need_weight 1
+                       foreach {cn cv} $repo_config(gui.$name) {
+                               if {$cn eq {-weight}} {
+                                       set need_weight 0
+                               }
+                               font configure $font $cn $cv
+                       }
+                       if {$need_weight} {
+                               font configure $font -weight normal
+                       }
+                       } err]} {
+                       error_popup [strcat [mc "Invalid font specified in %s:" "gui.$name"] "\n\n$err"]
+               }
+               foreach {cn cv} [font configure $font] {
+                       font configure ${font}bold $cn $cv
+                       font configure ${font}italic $cn $cv
+               }
+               font configure ${font}bold -weight bold
+               font configure ${font}italic -slant italic
+       }
+}
+
+set default_config(merge.diffstat) true
+set default_config(merge.summary) false
+set default_config(merge.verbosity) 2
+set default_config(user.name) {}
+set default_config(user.email) {}
+
+set default_config(gui.matchtrackingbranch) false
+set default_config(gui.pruneduringfetch) false
+set default_config(gui.trustmtime) false
+set default_config(gui.diffcontext) 5
+set default_config(gui.newbranchtemplate) {}
+set default_config(gui.fontui) [font configure font_ui]
+set default_config(gui.fontdiff) [font configure font_diff]
+set font_descs {
+       {fontui   font_ui   {mc "Main Font"}}
+       {fontdiff font_diff {mc "Diff/Console Font"}}
+}
+
+######################################################################
+##
+## find git
+
+set _git  [_which git]
+if {$_git eq {}} {
+       catch {wm withdraw .}
+       tk_messageBox \
+               -icon error \
+               -type ok \
+               -title [mc "git-gui: fatal error"] \
+               -message [mc "Cannot find git in PATH."]
+       exit 1
+}
+
+######################################################################
+##
+## version check
+
+if {[catch {set _git_version [git --version]} err]} {
+       catch {wm withdraw .}
+       tk_messageBox \
+               -icon error \
+               -type ok \
+               -title [mc "git-gui: fatal error"] \
+               -message "Cannot determine Git version:
+
+$err
+
+[appname] requires Git 1.5.0 or later."
+       exit 1
+}
+if {![regsub {^git version } $_git_version {} _git_version]} {
+       catch {wm withdraw .}
+       tk_messageBox \
+               -icon error \
+               -type ok \
+               -title [mc "git-gui: fatal error"] \
+               -message [strcat [mc "Cannot parse Git version string:"] "\n\n$_git_version"]
+       exit 1
+}
+
+set _real_git_version $_git_version
+regsub -- {-dirty$} $_git_version {} _git_version
+regsub {\.[0-9]+\.g[0-9a-f]+$} $_git_version {} _git_version
+regsub {\.rc[0-9]+$} $_git_version {} _git_version
+regsub {\.GIT$} $_git_version {} _git_version
+regsub {\.[a-zA-Z]+\.[0-9]+$} $_git_version {} _git_version
+
+if {![regexp {^[1-9]+(\.[0-9]+)+$} $_git_version]} {
+       catch {wm withdraw .}
+       if {[tk_messageBox \
+               -icon warning \
+               -type yesno \
+               -default no \
+               -title "[appname]: warning" \
+                -message [mc "Git version cannot be determined.
+
+%s claims it is version '%s'.
+
+%s requires at least Git 1.5.0 or later.
+
+Assume '%s' is version 1.5.0?
+" $_git $_real_git_version [appname] $_real_git_version]] eq {yes}} {
+               set _git_version 1.5.0
+       } else {
+               exit 1
+       }
+}
+unset _real_git_version
+
+proc git-version {args} {
+       global _git_version
+
+       switch [llength $args] {
+       0 {
+               return $_git_version
+       }
+
+       2 {
+               set op [lindex $args 0]
+               set vr [lindex $args 1]
+               set cm [package vcompare $_git_version $vr]
+               return [expr $cm $op 0]
+       }
+
+       4 {
+               set type [lindex $args 0]
+               set name [lindex $args 1]
+               set parm [lindex $args 2]
+               set body [lindex $args 3]
+
+               if {($type ne {proc} && $type ne {method})} {
+                       error "Invalid arguments to git-version"
+               }
+               if {[llength $body] < 2 || [lindex $body end-1] ne {default}} {
+                       error "Last arm of $type $name must be default"
+               }
+
+               foreach {op vr cb} [lrange $body 0 end-2] {
+                       if {[git-version $op $vr]} {
+                               return [uplevel [list $type $name $parm $cb]]
+                       }
+               }
+
+               return [uplevel [list $type $name $parm [lindex $body end]]]
+       }
+
+       default {
+               error "git-version >= x"
+       }
+
+       }
+}
+
+if {[git-version < 1.5]} {
+       catch {wm withdraw .}
+       tk_messageBox \
+               -icon error \
+               -type ok \
+               -title [mc "git-gui: fatal error"] \
+               -message "[appname] requires Git 1.5.0 or later.
+
+You are using [git-version]:
+
+[git --version]"
+       exit 1
+}
+
+######################################################################
+##
+## configure our library
+
+set idx [file join $oguilib tclIndex]
+if {[catch {set fd [open $idx r]} err]} {
+       catch {wm withdraw .}
+       tk_messageBox \
+               -icon error \
+               -type ok \
+               -title [mc "git-gui: fatal error"] \
+               -message $err
+       exit 1
+}
+if {[gets $fd] eq {# Autogenerated by git-gui Makefile}} {
+       set idx [list]
+       while {[gets $fd n] >= 0} {
+               if {$n ne {} && ![string match #* $n]} {
+                       lappend idx $n
+               }
+       }
+} else {
+       set idx {}
+}
+close $fd
+
+if {$idx ne {}} {
+       set loaded [list]
+       foreach p $idx {
+               if {[lsearch -exact $loaded $p] >= 0} continue
+               source [file join $oguilib $p]
+               lappend loaded $p
+       }
+       unset loaded p
+} else {
+       set auto_path [concat [list $oguilib] $auto_path]
+}
+unset -nocomplain idx fd
+
+######################################################################
+##
+## config file parsing
+
+git-version proc _parse_config {arr_name args} {
+       >= 1.5.3 {
+               upvar $arr_name arr
+               array unset arr
+               set buf {}
+               catch {
+                       set fd_rc [eval \
+                               [list git_read config] \
+                               $args \
+                               [list --null --list]]
+                       fconfigure $fd_rc -translation binary
+                       set buf [read $fd_rc]
+                       close $fd_rc
+               }
+               foreach line [split $buf "\0"] {
+                       if {[regexp {^([^\n]+)\n(.*)$} $line line name value]} {
+                               if {[is_many_config $name]} {
+                                       lappend arr($name) $value
+                               } else {
+                                       set arr($name) $value
+                               }
+                       }
+               }
+       }
+       default {
+               upvar $arr_name arr
+               array unset arr
+               catch {
+                       set fd_rc [eval [list git_read config --list] $args]
+                       while {[gets $fd_rc line] >= 0} {
+                               if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
+                                       if {[is_many_config $name]} {
+                                               lappend arr($name) $value
+                                       } else {
+                                               set arr($name) $value
+                                       }
+                               }
+                       }
+                       close $fd_rc
+               }
+       }
+}
+
+proc load_config {include_global} {
+       global repo_config global_config default_config
+
+       if {$include_global} {
+               _parse_config global_config --global
+       }
+       _parse_config repo_config
+
+       foreach name [array names default_config] {
+               if {[catch {set v $global_config($name)}]} {
+                       set global_config($name) $default_config($name)
+               }
+               if {[catch {set v $repo_config($name)}]} {
+                       set repo_config($name) $default_config($name)
+               }
+       }
+}
+
+######################################################################
+##
+## feature option selection
+
+if {[regexp {^git-(.+)$} [file tail $argv0] _junk subcommand]} {
+       unset _junk
+} else {
+       set subcommand gui
+}
+if {$subcommand eq {gui.sh}} {
+       set subcommand gui
+}
+if {$subcommand eq {gui} && [llength $argv] > 0} {
+       set subcommand [lindex $argv 0]
+       set argv [lrange $argv 1 end]
+}
+
+enable_option multicommit
+enable_option branch
+enable_option transport
+disable_option bare
+
+switch -- $subcommand {
+browser -
+blame {
+       enable_option bare
+
+       disable_option multicommit
+       disable_option branch
+       disable_option transport
+}
+citool {
+       enable_option singlecommit
+
+       disable_option multicommit
+       disable_option branch
+       disable_option transport
+}
+}
+
+######################################################################
+##
+## repository setup
+
+if {[catch {
+               set _gitdir $env(GIT_DIR)
+               set _prefix {}
+               }]
+       && [catch {
+               set _gitdir [git rev-parse --git-dir]
+               set _prefix [git rev-parse --show-prefix]
+       } err]} {
+       load_config 1
+       apply_config
+       choose_repository::pick
+}
+if {![file isdirectory $_gitdir] && [is_Cygwin]} {
+       catch {set _gitdir [exec cygpath --windows $_gitdir]}
+}
+if {![file isdirectory $_gitdir]} {
+       catch {wm withdraw .}
+       error_popup [strcat [mc "Git directory not found:"] "\n\n$_gitdir"]
+       exit 1
+}
+if {$_prefix ne {}} {
+       regsub -all {[^/]+/} $_prefix ../ cdup
+       if {[catch {cd $cdup} err]} {
+               catch {wm withdraw .}
+               error_popup [strcat [mc "Cannot move to top of working directory:"] "\n\n$err"]
+               exit 1
+       }
+       unset cdup
+} elseif {![is_enabled bare]} {
+       if {[lindex [file split $_gitdir] end] ne {.git}} {
+               catch {wm withdraw .}
+               error_popup [strcat [mc "Cannot use funny .git directory:"] "\n\n$_gitdir"]
+               exit 1
+       }
+       if {[catch {cd [file dirname $_gitdir]} err]} {
+               catch {wm withdraw .}
+               error_popup [strcat [mc "No working directory"] " [file dirname $_gitdir]:\n\n$err"]
+               exit 1
+       }
+}
+set _reponame [file split [file normalize $_gitdir]]
+if {[lindex $_reponame end] eq {.git}} {
+       set _reponame [lindex $_reponame end-1]
+} else {
+       set _reponame [lindex $_reponame end]
+}
+
+######################################################################
+##
+## global init
+
+set current_diff_path {}
+set current_diff_side {}
+set diff_actions [list]
+
+set HEAD {}
+set PARENT {}
+set MERGE_HEAD [list]
+set commit_type {}
+set empty_tree {}
+set current_branch {}
+set is_detached 0
+set current_diff_path {}
+set is_3way_diff 0
+set selected_commit_type new
+
+######################################################################
+##
+## task management
+
+set rescan_active 0
+set diff_active 0
+set last_clicked {}
+
+set disable_on_lock [list]
+set index_lock_type none
+
+proc lock_index {type} {
+       global index_lock_type disable_on_lock
+
+       if {$index_lock_type eq {none}} {
+               set index_lock_type $type
+               foreach w $disable_on_lock {
+                       uplevel #0 $w disabled
+               }
+               return 1
+       } elseif {$index_lock_type eq "begin-$type"} {
+               set index_lock_type $type
+               return 1
+       }
+       return 0
+}
+
+proc unlock_index {} {
+       global index_lock_type disable_on_lock
+
+       set index_lock_type none
+       foreach w $disable_on_lock {
+               uplevel #0 $w normal
+       }
+}
+
+######################################################################
+##
+## status
+
+proc repository_state {ctvar hdvar mhvar} {
+       global current_branch
+       upvar $ctvar ct $hdvar hd $mhvar mh
+
+       set mh [list]
+
+       load_current_branch
+       if {[catch {set hd [git rev-parse --verify HEAD]}]} {
+               set hd {}
+               set ct initial
+               return
+       }
+
+       set merge_head [gitdir MERGE_HEAD]
+       if {[file exists $merge_head]} {
+               set ct merge
+               set fd_mh [open $merge_head r]
+               while {[gets $fd_mh line] >= 0} {
+                       lappend mh $line
+               }
+               close $fd_mh
+               return
+       }
+
+       set ct normal
+}
+
+proc PARENT {} {
+       global PARENT empty_tree
+
+       set p [lindex $PARENT 0]
+       if {$p ne {}} {
+               return $p
+       }
+       if {$empty_tree eq {}} {
+               set empty_tree [git mktree << {}]
+       }
+       return $empty_tree
+}
+
+proc rescan {after {honor_trustmtime 1}} {
+       global HEAD PARENT MERGE_HEAD commit_type
+       global ui_index ui_workdir ui_comm
+       global rescan_active file_states
+       global repo_config
+
+       if {$rescan_active > 0 || ![lock_index read]} return
+
+       repository_state newType newHEAD newMERGE_HEAD
+       if {[string match amend* $commit_type]
+               && $newType eq {normal}
+               && $newHEAD eq $HEAD} {
+       } else {
+               set HEAD $newHEAD
+               set PARENT $newHEAD
+               set MERGE_HEAD $newMERGE_HEAD
+               set commit_type $newType
+       }
+
+       array unset file_states
+
+       if {!$::GITGUI_BCK_exists &&
+               (![$ui_comm edit modified]
+               || [string trim [$ui_comm get 0.0 end]] eq {})} {
+               if {[string match amend* $commit_type]} {
+               } elseif {[load_message GITGUI_MSG]} {
+               } elseif {[load_message MERGE_MSG]} {
+               } elseif {[load_message SQUASH_MSG]} {
+               }
+               $ui_comm edit reset
+               $ui_comm edit modified false
+       }
+
+       if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} {
+               rescan_stage2 {} $after
+       } else {
+               set rescan_active 1
+               ui_status [mc "Refreshing file status..."]
+               set fd_rf [git_read update-index \
+                       -q \
+                       --unmerged \
+                       --ignore-missing \
+                       --refresh \
+                       ]
+               fconfigure $fd_rf -blocking 0 -translation binary
+               fileevent $fd_rf readable \
+                       [list rescan_stage2 $fd_rf $after]
+       }
+}
+
+if {[is_Cygwin]} {
+       set is_git_info_link {}
+       set is_git_info_exclude {}
+       proc have_info_exclude {} {
+               global is_git_info_link is_git_info_exclude
+
+               if {$is_git_info_link eq {}} {
+                       set is_git_info_link [file isfile [gitdir info.lnk]]
+               }
+
+               if {$is_git_info_link} {
+                       if {$is_git_info_exclude eq {}} {
+                               if {[catch {exec test -f [gitdir info exclude]}]} {
+                                       set is_git_info_exclude 0
+                               } else {
+                                       set is_git_info_exclude 1
+                               }
+                       }
+                       return $is_git_info_exclude
+               } else {
+                       return [file readable [gitdir info exclude]]
+               }
+       }
+} else {
+       proc have_info_exclude {} {
+               return [file readable [gitdir info exclude]]
+       }
+}
+
+proc rescan_stage2 {fd after} {
+       global rescan_active buf_rdi buf_rdf buf_rlo
+
+       if {$fd ne {}} {
+               read $fd
+               if {![eof $fd]} return
+               close $fd
+       }
+
+       set ls_others [list --exclude-per-directory=.gitignore]
+       if {[have_info_exclude]} {
+               lappend ls_others "--exclude-from=[gitdir info exclude]"
+       }
+       set user_exclude [get_config core.excludesfile]
+       if {$user_exclude ne {} && [file readable $user_exclude]} {
+               lappend ls_others "--exclude-from=$user_exclude"
+       }
+
+       set buf_rdi {}
+       set buf_rdf {}
+       set buf_rlo {}
+
+       set rescan_active 3
+       ui_status [mc "Scanning for modified files ..."]
+       set fd_di [git_read diff-index --cached -z [PARENT]]
+       set fd_df [git_read diff-files -z]
+       set fd_lo [eval git_read ls-files --others -z $ls_others]
+
+       fconfigure $fd_di -blocking 0 -translation binary -encoding binary
+       fconfigure $fd_df -blocking 0 -translation binary -encoding binary
+       fconfigure $fd_lo -blocking 0 -translation binary -encoding binary
+       fileevent $fd_di readable [list read_diff_index $fd_di $after]
+       fileevent $fd_df readable [list read_diff_files $fd_df $after]
+       fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
+}
+
+proc load_message {file} {
+       global ui_comm
+
+       set f [gitdir $file]
+       if {[file isfile $f]} {
+               if {[catch {set fd [open $f r]}]} {
+                       return 0
+               }
+               fconfigure $fd -eofchar {}
+               set content [string trim [read $fd]]
+               close $fd
+               regsub -all -line {[ \r\t]+$} $content {} content
+               $ui_comm delete 0.0 end
+               $ui_comm insert end $content
+               return 1
+       }
+       return 0
+}
+
+proc read_diff_index {fd after} {
+       global buf_rdi
+
+       append buf_rdi [read $fd]
+       set c 0
+       set n [string length $buf_rdi]
+       while {$c < $n} {
+               set z1 [string first "\0" $buf_rdi $c]
+               if {$z1 == -1} break
+               incr z1
+               set z2 [string first "\0" $buf_rdi $z1]
+               if {$z2 == -1} break
+
+               incr c
+               set i [split [string range $buf_rdi $c [expr {$z1 - 2}]] { }]
+               set p [string range $buf_rdi $z1 [expr {$z2 - 1}]]
+               merge_state \
+                       [encoding convertfrom $p] \
+                       [lindex $i 4]? \
+                       [list [lindex $i 0] [lindex $i 2]] \
+                       [list]
+               set c $z2
+               incr c
+       }
+       if {$c < $n} {
+               set buf_rdi [string range $buf_rdi $c end]
+       } else {
+               set buf_rdi {}
+       }
+
+       rescan_done $fd buf_rdi $after
+}
+
+proc read_diff_files {fd after} {
+       global buf_rdf
+
+       append buf_rdf [read $fd]
+       set c 0
+       set n [string length $buf_rdf]
+       while {$c < $n} {
+               set z1 [string first "\0" $buf_rdf $c]
+               if {$z1 == -1} break
+               incr z1
+               set z2 [string first "\0" $buf_rdf $z1]
+               if {$z2 == -1} break
+
+               incr c
+               set i [split [string range $buf_rdf $c [expr {$z1 - 2}]] { }]
+               set p [string range $buf_rdf $z1 [expr {$z2 - 1}]]
+               merge_state \
+                       [encoding convertfrom $p] \
+                       ?[lindex $i 4] \
+                       [list] \
+                       [list [lindex $i 0] [lindex $i 2]]
+               set c $z2
+               incr c
+       }
+       if {$c < $n} {
+               set buf_rdf [string range $buf_rdf $c end]
+       } else {
+               set buf_rdf {}
+       }
+
+       rescan_done $fd buf_rdf $after
+}
+
+proc read_ls_others {fd after} {
+       global buf_rlo
+
+       append buf_rlo [read $fd]
+       set pck [split $buf_rlo "\0"]
+       set buf_rlo [lindex $pck end]
+       foreach p [lrange $pck 0 end-1] {
+               set p [encoding convertfrom $p]
+               if {[string index $p end] eq {/}} {
+                       set p [string range $p 0 end-1]
+               }
+               merge_state $p ?O
+       }
+       rescan_done $fd buf_rlo $after
+}
+
+proc rescan_done {fd buf after} {
+       global rescan_active current_diff_path
+       global file_states repo_config
+       upvar $buf to_clear
+
+       if {![eof $fd]} return
+       set to_clear {}
+       close $fd
+       if {[incr rescan_active -1] > 0} return
+
+       prune_selection
+       unlock_index
+       display_all_files
+       if {$current_diff_path ne {}} reshow_diff
+       uplevel #0 $after
+}
+
+proc prune_selection {} {
+       global file_states selected_paths
+
+       foreach path [array names selected_paths] {
+               if {[catch {set still_here $file_states($path)}]} {
+                       unset selected_paths($path)
+               }
+       }
+}
+
+######################################################################
+##
+## ui helpers
+
+proc mapicon {w state path} {
+       global all_icons
+
+       if {[catch {set r $all_icons($state$w)}]} {
+               puts "error: no icon for $w state={$state} $path"
+               return file_plain
+       }
+       return $r
+}
+
+proc mapdesc {state path} {
+       global all_descs
+
+       if {[catch {set r $all_descs($state)}]} {
+               puts "error: no desc for state={$state} $path"
+               return $state
+       }
+       return $r
+}
+
+proc ui_status {msg} {
+       global main_status
+       if {[info exists main_status]} {
+               $main_status show $msg
+       }
+}
+
+proc ui_ready {{test {}}} {
+       global main_status
+       if {[info exists main_status]} {
+               $main_status show [mc "Ready."] $test
+       }
+}
+
+proc escape_path {path} {
+       regsub -all {\\} $path "\\\\" path
+       regsub -all "\n" $path "\\n" path
+       return $path
+}
+
+proc short_path {path} {
+       return [escape_path [lindex [file split $path] end]]
+}
+
+set next_icon_id 0
+set null_sha1 [string repeat 0 40]
+
+proc merge_state {path new_state {head_info {}} {index_info {}}} {
+       global file_states next_icon_id null_sha1
+
+       set s0 [string index $new_state 0]
+       set s1 [string index $new_state 1]
+
+       if {[catch {set info $file_states($path)}]} {
+               set state __
+               set icon n[incr next_icon_id]
+       } else {
+               set state [lindex $info 0]
+               set icon [lindex $info 1]
+               if {$head_info eq {}}  {set head_info  [lindex $info 2]}
+               if {$index_info eq {}} {set index_info [lindex $info 3]}
+       }
+
+       if     {$s0 eq {?}} {set s0 [string index $state 0]} \
+       elseif {$s0 eq {_}} {set s0 _}
+
+       if     {$s1 eq {?}} {set s1 [string index $state 1]} \
+       elseif {$s1 eq {_}} {set s1 _}
+
+       if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} {
+               set head_info [list 0 $null_sha1]
+       } elseif {$s0 ne {_} && [string index $state 0] eq {_}
+               && $head_info eq {}} {
+               set head_info $index_info
+       }
+
+       set file_states($path) [list $s0$s1 $icon \
+               $head_info $index_info \
+               ]
+       return $state
+}
+
+proc display_file_helper {w path icon_name old_m new_m} {
+       global file_lists
+
+       if {$new_m eq {_}} {
+               set lno [lsearch -sorted -exact $file_lists($w) $path]
+               if {$lno >= 0} {
+                       set file_lists($w) [lreplace $file_lists($w) $lno $lno]
+                       incr lno
+                       $w conf -state normal
+                       $w delete $lno.0 [expr {$lno + 1}].0
+                       $w conf -state disabled
+               }
+       } elseif {$old_m eq {_} && $new_m ne {_}} {
+               lappend file_lists($w) $path
+               set file_lists($w) [lsort -unique $file_lists($w)]
+               set lno [lsearch -sorted -exact $file_lists($w) $path]
+               incr lno
+               $w conf -state normal
+               $w image create $lno.0 \
+                       -align center -padx 5 -pady 1 \
+                       -name $icon_name \
+                       -image [mapicon $w $new_m $path]
+               $w insert $lno.1 "[escape_path $path]\n"
+               $w conf -state disabled
+       } elseif {$old_m ne $new_m} {
+               $w conf -state normal
+               $w image conf $icon_name -image [mapicon $w $new_m $path]
+               $w conf -state disabled
+       }
+}
+
+proc display_file {path state} {
+       global file_states selected_paths
+       global ui_index ui_workdir
+
+       set old_m [merge_state $path $state]
+       set s $file_states($path)
+       set new_m [lindex $s 0]
+       set icon_name [lindex $s 1]
+
+       set o [string index $old_m 0]
+       set n [string index $new_m 0]
+       if {$o eq {U}} {
+               set o _
+       }
+       if {$n eq {U}} {
+               set n _
+       }
+       display_file_helper     $ui_index $path $icon_name $o $n
+
+       if {[string index $old_m 0] eq {U}} {
+               set o U
+       } else {
+               set o [string index $old_m 1]
+       }
+       if {[string index $new_m 0] eq {U}} {
+               set n U
+       } else {
+               set n [string index $new_m 1]
+       }
+       display_file_helper     $ui_workdir $path $icon_name $o $n
+
+       if {$new_m eq {__}} {
+               unset file_states($path)
+               catch {unset selected_paths($path)}
+       }
+}
+
+proc display_all_files_helper {w path icon_name m} {
+       global file_lists
+
+       lappend file_lists($w) $path
+       set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
+       $w image create end \
+               -align center -padx 5 -pady 1 \
+               -name $icon_name \
+               -image [mapicon $w $m $path]
+       $w insert end "[escape_path $path]\n"
+}
+
+proc display_all_files {} {
+       global ui_index ui_workdir
+       global file_states file_lists
+       global last_clicked
+
+       $ui_index conf -state normal
+       $ui_workdir conf -state normal
+
+       $ui_index delete 0.0 end
+       $ui_workdir delete 0.0 end
+       set last_clicked {}
+
+       set file_lists($ui_index) [list]
+       set file_lists($ui_workdir) [list]
+
+       foreach path [lsort [array names file_states]] {
+               set s $file_states($path)
+               set m [lindex $s 0]
+               set icon_name [lindex $s 1]
+
+               set s [string index $m 0]
+               if {$s ne {U} && $s ne {_}} {
+                       display_all_files_helper $ui_index $path \
+                               $icon_name $s
+               }
+
+               if {[string index $m 0] eq {U}} {
+                       set s U
+               } else {
+                       set s [string index $m 1]
+               }
+               if {$s ne {_}} {
+                       display_all_files_helper $ui_workdir $path \
+                               $icon_name $s
+               }
+       }
+
+       $ui_index conf -state disabled
+       $ui_workdir conf -state disabled
+}
+
+######################################################################
+##
+## icons
+
+set filemask {
+#define mask_width 14
+#define mask_height 15
+static unsigned char mask_bits[] = {
+   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
+   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
+   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
+}
+
+image create bitmap file_plain -background white -foreground black -data {
+#define plain_width 14
+#define plain_height 15
+static unsigned char plain_bits[] = {
+   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
+   0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
+   0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
+} -maskdata $filemask
+
+image create bitmap file_mod -background white -foreground blue -data {
+#define mod_width 14
+#define mod_height 15
+static unsigned char mod_bits[] = {
+   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
+   0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
+   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
+} -maskdata $filemask
+
+image create bitmap file_fulltick -background white -foreground "#007000" -data {
+#define file_fulltick_width 14
+#define file_fulltick_height 15
+static unsigned char file_fulltick_bits[] = {
+   0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
+   0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
+   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
+} -maskdata $filemask
+
+image create bitmap file_parttick -background white -foreground "#005050" -data {
+#define parttick_width 14
+#define parttick_height 15
+static unsigned char parttick_bits[] = {
+   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
+   0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
+   0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
+} -maskdata $filemask
+
+image create bitmap file_question -background white -foreground black -data {
+#define file_question_width 14
+#define file_question_height 15
+static unsigned char file_question_bits[] = {
+   0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
+   0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
+   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
+} -maskdata $filemask
+
+image create bitmap file_removed -background white -foreground red -data {
+#define file_removed_width 14
+#define file_removed_height 15
+static unsigned char file_removed_bits[] = {
+   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
+   0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
+   0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
+} -maskdata $filemask
+
+image create bitmap file_merge -background white -foreground blue -data {
+#define file_merge_width 14
+#define file_merge_height 15
+static unsigned char file_merge_bits[] = {
+   0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
+   0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
+   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
+} -maskdata $filemask
+
+set ui_index .vpane.files.index.list
+set ui_workdir .vpane.files.workdir.list
+
+set all_icons(_$ui_index)   file_plain
+set all_icons(A$ui_index)   file_fulltick
+set all_icons(M$ui_index)   file_fulltick
+set all_icons(D$ui_index)   file_removed
+set all_icons(U$ui_index)   file_merge
+
+set all_icons(_$ui_workdir) file_plain
+set all_icons(M$ui_workdir) file_mod
+set all_icons(D$ui_workdir) file_question
+set all_icons(U$ui_workdir) file_merge
+set all_icons(O$ui_workdir) file_plain
+
+set max_status_desc 0
+foreach i {
+               {__ {mc "Unmodified"}}
+
+               {_M {mc "Modified, not staged"}}
+               {M_ {mc "Staged for commit"}}
+               {MM {mc "Portions staged for commit"}}
+               {MD {mc "Staged for commit, missing"}}
+
+               {_O {mc "Untracked, not staged"}}
+               {A_ {mc "Staged for commit"}}
+               {AM {mc "Portions staged for commit"}}
+               {AD {mc "Staged for commit, missing"}}
+
+               {_D {mc "Missing"}}
+               {D_ {mc "Staged for removal"}}
+               {DO {mc "Staged for removal, still present"}}
+
+               {U_ {mc "Requires merge resolution"}}
+               {UU {mc "Requires merge resolution"}}
+               {UM {mc "Requires merge resolution"}}
+               {UD {mc "Requires merge resolution"}}
+       } {
+       set text [eval [lindex $i 1]]
+       if {$max_status_desc < [string length $text]} {
+               set max_status_desc [string length $text]
+       }
+       set all_descs([lindex $i 0]) $text
+}
+unset i
+
+######################################################################
+##
+## util
+
+proc scrollbar2many {list mode args} {
+       foreach w $list {eval $w $mode $args}
+}
+
+proc many2scrollbar {list mode sb top bottom} {
+       $sb set $top $bottom
+       foreach w $list {$w $mode moveto $top}
+}
+
+proc incr_font_size {font {amt 1}} {
+       set sz [font configure $font -size]
+       incr sz $amt
+       font configure $font -size $sz
+       font configure ${font}bold -size $sz
+       font configure ${font}italic -size $sz
+}
+
+######################################################################
+##
+## ui commands
+
+set starting_gitk_msg [mc "Starting gitk... please wait..."]
+
+proc do_gitk {revs} {
+       # -- Always start gitk through whatever we were loaded with.  This
+       #    lets us bypass using shell process on Windows systems.
+       #
+       set exe [file join [file dirname $::_git] gitk]
+       set cmd [list [info nameofexecutable] $exe]
+       if {! [file exists $exe]} {
+               error_popup [mc "Unable to start gitk:\n\n%s does not exist" $exe]
+       } else {
+               global env
+
+               if {[info exists env(GIT_DIR)]} {
+                       set old_GIT_DIR $env(GIT_DIR)
+               } else {
+                       set old_GIT_DIR {}
+               }
+
+               set pwd [pwd]
+               cd [file dirname [gitdir]]
+               set env(GIT_DIR) [file tail [gitdir]]
+
+               eval exec $cmd $revs &
+
+               if {$old_GIT_DIR eq {}} {
+                       unset env(GIT_DIR)
+               } else {
+                       set env(GIT_DIR) $old_GIT_DIR
+               }
+               cd $pwd
+
+               ui_status $::starting_gitk_msg
+               after 10000 {
+                       ui_ready $starting_gitk_msg
+               }
+       }
+}
+
+set is_quitting 0
+
+proc do_quit {} {
+       global ui_comm is_quitting repo_config commit_type
+       global GITGUI_BCK_exists GITGUI_BCK_i
+
+       if {$is_quitting} return
+       set is_quitting 1
+
+       if {[winfo exists $ui_comm]} {
+               # -- Stash our current commit buffer.
+               #
+               set save [gitdir GITGUI_MSG]
+               if {$GITGUI_BCK_exists && ![$ui_comm edit modified]} {
+                       file rename -force [gitdir GITGUI_BCK] $save
+                       set GITGUI_BCK_exists 0
+               } else {
+                       set msg [string trim [$ui_comm get 0.0 end]]
+                       regsub -all -line {[ \r\t]+$} $msg {} msg
+                       if {(![string match amend* $commit_type]
+                               || [$ui_comm edit modified])
+                               && $msg ne {}} {
+                               catch {
+                                       set fd [open $save w]
+                                       puts -nonewline $fd $msg
+                                       close $fd
+                               }
+                       } else {
+                               catch {file delete $save}
+                       }
+               }
+
+               # -- Remove our editor backup, its not needed.
+               #
+               after cancel $GITGUI_BCK_i
+               if {$GITGUI_BCK_exists} {
+                       catch {file delete [gitdir GITGUI_BCK]}
+               }
+
+               # -- Stash our current window geometry into this repository.
+               #
+               set cfg_geometry [list]
+               lappend cfg_geometry [wm geometry .]
+               lappend cfg_geometry [lindex [.vpane sash coord 0] 0]
+               lappend cfg_geometry [lindex [.vpane.files sash coord 0] 1]
+               if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
+                       set rc_geometry {}
+               }
+               if {$cfg_geometry ne $rc_geometry} {
+                       catch {git config gui.geometry $cfg_geometry}
+               }
+       }
+
+       destroy .
+}
+
+proc do_rescan {} {
+       rescan ui_ready
+}
+
+proc do_commit {} {
+       commit_tree
+}
+
+proc toggle_or_diff {w x y} {
+       global file_states file_lists current_diff_path ui_index ui_workdir
+       global last_clicked selected_paths
+
+       set pos [split [$w index @$x,$y] .]
+       set lno [lindex $pos 0]
+       set col [lindex $pos 1]
+       set path [lindex $file_lists($w) [expr {$lno - 1}]]
+       if {$path eq {}} {
+               set last_clicked {}
+               return
+       }
+
+       set last_clicked [list $w $lno]
+       array unset selected_paths
+       $ui_index tag remove in_sel 0.0 end
+       $ui_workdir tag remove in_sel 0.0 end
+
+       if {$col == 0} {
+               if {$current_diff_path eq $path} {
+                       set after {reshow_diff;}
+               } else {
+                       set after {}
+               }
+               if {$w eq $ui_index} {
+                       update_indexinfo \
+                               "Unstaging [short_path $path] from commit" \
+                               [list $path] \
+                               [concat $after [list ui_ready]]
+               } elseif {$w eq $ui_workdir} {
+                       update_index \
+                               "Adding [short_path $path]" \
+                               [list $path] \
+                               [concat $after [list ui_ready]]
+               }
+       } else {
+               show_diff $path $w $lno
+       }
+}
+
+proc add_one_to_selection {w x y} {
+       global file_lists last_clicked selected_paths
+
+       set lno [lindex [split [$w index @$x,$y] .] 0]
+       set path [lindex $file_lists($w) [expr {$lno - 1}]]
+       if {$path eq {}} {
+               set last_clicked {}
+               return
+       }
+
+       if {$last_clicked ne {}
+               && [lindex $last_clicked 0] ne $w} {
+               array unset selected_paths
+               [lindex $last_clicked 0] tag remove in_sel 0.0 end
+       }
+
+       set last_clicked [list $w $lno]
+       if {[catch {set in_sel $selected_paths($path)}]} {
+               set in_sel 0
+       }
+       if {$in_sel} {
+               unset selected_paths($path)
+               $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
+       } else {
+               set selected_paths($path) 1
+               $w tag add in_sel $lno.0 [expr {$lno + 1}].0
+       }
+}
+
+proc add_range_to_selection {w x y} {
+       global file_lists last_clicked selected_paths
+
+       if {[lindex $last_clicked 0] ne $w} {
+               toggle_or_diff $w $x $y
+               return
+       }
+
+       set lno [lindex [split [$w index @$x,$y] .] 0]
+       set lc [lindex $last_clicked 1]
+       if {$lc < $lno} {
+               set begin $lc
+               set end $lno
+       } else {
+               set begin $lno
+               set end $lc
+       }
+
+       foreach path [lrange $file_lists($w) \
+               [expr {$begin - 1}] \
+               [expr {$end - 1}]] {
+               set selected_paths($path) 1
+       }
+       $w tag add in_sel $begin.0 [expr {$end + 1}].0
+}
+
+######################################################################
+##
+## ui construction
+
+load_config 0
+apply_config
+set ui_comm {}
+
+# -- Menu Bar
+#
+menu .mbar -tearoff 0
+.mbar add cascade -label [mc Repository] -menu .mbar.repository
+.mbar add cascade -label [mc Edit] -menu .mbar.edit
+if {[is_enabled branch]} {
+       .mbar add cascade -label [mc Branch] -menu .mbar.branch
+}
+if {[is_enabled multicommit] || [is_enabled singlecommit]} {
+       .mbar add cascade -label [mc Commit@@noun] -menu .mbar.commit
+}
+if {[is_enabled transport]} {
+       .mbar add cascade -label [mc Merge] -menu .mbar.merge
+       .mbar add cascade -label [mc Remote] -menu .mbar.remote
+}
+. configure -menu .mbar
+
+# -- Repository Menu
+#
+menu .mbar.repository
+
+.mbar.repository add command \
+       -label [mc "Browse Current Branch's Files"] \
+       -command {browser::new $current_branch}
+set ui_browse_current [.mbar.repository index last]
+.mbar.repository add command \
+       -label [mc "Browse Branch Files..."] \
+       -command browser_open::dialog
+.mbar.repository add separator
+
+.mbar.repository add command \
+       -label [mc "Visualize Current Branch's History"] \
+       -command {do_gitk $current_branch}
+set ui_visualize_current [.mbar.repository index last]
+.mbar.repository add command \
+       -label [mc "Visualize All Branch History"] \
+       -command {do_gitk --all}
+.mbar.repository add separator
+
+proc current_branch_write {args} {
+       global current_branch
+       .mbar.repository entryconf $::ui_browse_current \
+               -label [mc "Browse %s's Files" $current_branch]
+       .mbar.repository entryconf $::ui_visualize_current \
+               -label [mc "Visualize %s's History" $current_branch]
+}
+trace add variable current_branch write current_branch_write
+
+if {[is_enabled multicommit]} {
+       .mbar.repository add command -label [mc "Database Statistics"] \
+               -command do_stats
+
+       .mbar.repository add command -label [mc "Compress Database"] \
+               -command do_gc
+
+       .mbar.repository add command -label [mc "Verify Database"] \
+               -command do_fsck_objects
+
+       .mbar.repository add separator
+
+       if {[is_Cygwin]} {
+               .mbar.repository add command \
+                       -label [mc "Create Desktop Icon"] \
+                       -command do_cygwin_shortcut
+       } elseif {[is_Windows]} {
+               .mbar.repository add command \
+                       -label [mc "Create Desktop Icon"] \
+                       -command do_windows_shortcut
+       } elseif {[is_MacOSX]} {
+               .mbar.repository add command \
+                       -label [mc "Create Desktop Icon"] \
+                       -command do_macosx_app
+       }
+}
+
+.mbar.repository add command -label [mc Quit] \
+       -command do_quit \
+       -accelerator $M1T-Q
+
+# -- Edit Menu
+#
+menu .mbar.edit
+.mbar.edit add command -label [mc Undo] \
+       -command {catch {[focus] edit undo}} \
+       -accelerator $M1T-Z
+.mbar.edit add command -label [mc Redo] \
+       -command {catch {[focus] edit redo}} \
+       -accelerator $M1T-Y
+.mbar.edit add separator
+.mbar.edit add command -label [mc Cut] \
+       -command {catch {tk_textCut [focus]}} \
+       -accelerator $M1T-X
+.mbar.edit add command -label [mc Copy] \
+       -command {catch {tk_textCopy [focus]}} \
+       -accelerator $M1T-C
+.mbar.edit add command -label [mc Paste] \
+       -command {catch {tk_textPaste [focus]; [focus] see insert}} \
+       -accelerator $M1T-V
+.mbar.edit add command -label [mc Delete] \
+       -command {catch {[focus] delete sel.first sel.last}} \
+       -accelerator Del
+.mbar.edit add separator
+.mbar.edit add command -label [mc "Select All"] \
+       -command {catch {[focus] tag add sel 0.0 end}} \
+       -accelerator $M1T-A
+
+# -- Branch Menu
+#
+if {[is_enabled branch]} {
+       menu .mbar.branch
+
+       .mbar.branch add command -label [mc "Create..."] \
+               -command branch_create::dialog \
+               -accelerator $M1T-N
+       lappend disable_on_lock [list .mbar.branch entryconf \
+               [.mbar.branch index last] -state]
+
+       .mbar.branch add command -label [mc "Checkout..."] \
+               -command branch_checkout::dialog \
+               -accelerator $M1T-O
+       lappend disable_on_lock [list .mbar.branch entryconf \
+               [.mbar.branch index last] -state]
+
+       .mbar.branch add command -label [mc "Rename..."] \
+               -command branch_rename::dialog
+       lappend disable_on_lock [list .mbar.branch entryconf \
+               [.mbar.branch index last] -state]
+
+       .mbar.branch add command -label [mc "Delete..."] \
+               -command branch_delete::dialog
+       lappend disable_on_lock [list .mbar.branch entryconf \
+               [.mbar.branch index last] -state]
+
+       .mbar.branch add command -label [mc "Reset..."] \
+               -command merge::reset_hard
+       lappend disable_on_lock [list .mbar.branch entryconf \
+               [.mbar.branch index last] -state]
+}
+
+# -- Commit Menu
+#
+if {[is_enabled multicommit] || [is_enabled singlecommit]} {
+       menu .mbar.commit
+
+       .mbar.commit add radiobutton \
+               -label [mc "New Commit"] \
+               -command do_select_commit_type \
+               -variable selected_commit_type \
+               -value new
+       lappend disable_on_lock \
+               [list .mbar.commit entryconf [.mbar.commit index last] -state]
+
+       .mbar.commit add radiobutton \
+               -label [mc "Amend Last Commit"] \
+               -command do_select_commit_type \
+               -variable selected_commit_type \
+               -value amend
+       lappend disable_on_lock \
+               [list .mbar.commit entryconf [.mbar.commit index last] -state]
+
+       .mbar.commit add separator
+
+       .mbar.commit add command -label [mc Rescan] \
+               -command do_rescan \
+               -accelerator F5
+       lappend disable_on_lock \
+               [list .mbar.commit entryconf [.mbar.commit index last] -state]
+
+       .mbar.commit add command -label [mc "Stage To Commit"] \
+               -command do_add_selection \
+               -accelerator $M1T-T
+       lappend disable_on_lock \
+               [list .mbar.commit entryconf [.mbar.commit index last] -state]
+
+       .mbar.commit add command -label [mc "Stage Changed Files To Commit"] \
+               -command do_add_all \
+               -accelerator $M1T-I
+       lappend disable_on_lock \
+               [list .mbar.commit entryconf [.mbar.commit index last] -state]
+
+       .mbar.commit add command -label [mc "Unstage From Commit"] \
+               -command do_unstage_selection
+       lappend disable_on_lock \
+               [list .mbar.commit entryconf [.mbar.commit index last] -state]
+
+       .mbar.commit add command -label [mc "Revert Changes"] \
+               -command do_revert_selection
+       lappend disable_on_lock \
+               [list .mbar.commit entryconf [.mbar.commit index last] -state]
+
+       .mbar.commit add separator
+
+       .mbar.commit add command -label [mc "Sign Off"] \
+               -command do_signoff \
+               -accelerator $M1T-S
+
+       .mbar.commit add command -label [mc Commit@@verb] \
+               -command do_commit \
+               -accelerator $M1T-Return
+       lappend disable_on_lock \
+               [list .mbar.commit entryconf [.mbar.commit index last] -state]
+}
+
+# -- Merge Menu
+#
+if {[is_enabled branch]} {
+       menu .mbar.merge
+       .mbar.merge add command -label [mc "Local Merge..."] \
+               -command merge::dialog \
+               -accelerator $M1T-M
+       lappend disable_on_lock \
+               [list .mbar.merge entryconf [.mbar.merge index last] -state]
+       .mbar.merge add command -label [mc "Abort Merge..."] \
+               -command merge::reset_hard
+       lappend disable_on_lock \
+               [list .mbar.merge entryconf [.mbar.merge index last] -state]
+}
+
+# -- Transport Menu
+#
+if {[is_enabled transport]} {
+       menu .mbar.remote
+
+       .mbar.remote add command \
+               -label [mc "Push..."] \
+               -command do_push_anywhere \
+               -accelerator $M1T-P
+       .mbar.remote add command \
+               -label [mc "Delete..."] \
+               -command remote_branch_delete::dialog
+}
+
+if {[is_MacOSX]} {
+       # -- Apple Menu (Mac OS X only)
+       #
+       .mbar add cascade -label [mc Apple] -menu .mbar.apple
+       menu .mbar.apple
+
+       .mbar.apple add command -label [mc "About %s" [appname]] \
+               -command do_about
+       .mbar.apple add separator
+       .mbar.apple add command \
+               -label [mc "Preferences..."] \
+               -command do_options \
+               -accelerator $M1T-,
+       bind . <$M1B-,> do_options
+} else {
+       # -- Edit Menu
+       #
+       .mbar.edit add separator
+       .mbar.edit add command -label [mc "Options..."] \
+               -command do_options
+}
+
+# -- Help Menu
+#
+.mbar add cascade -label [mc Help] -menu .mbar.help
+menu .mbar.help
+
+if {![is_MacOSX]} {
+       .mbar.help add command -label [mc "About %s" [appname]] \
+               -command do_about
+}
+
+set browser {}
+catch {set browser $repo_config(instaweb.browser)}
+set doc_path [file dirname [gitexec]]
+set doc_path [file join $doc_path Documentation index.html]
+
+if {[is_Cygwin]} {
+       set doc_path [exec cygpath --mixed $doc_path]
+}
+
+if {$browser eq {}} {
+       if {[is_MacOSX]} {
+               set browser open
+       } elseif {[is_Cygwin]} {
+               set program_files [file dirname [exec cygpath --windir]]
+               set program_files [file join $program_files {Program Files}]
+               set firefox [file join $program_files {Mozilla Firefox} firefox.exe]
+               set ie [file join $program_files {Internet Explorer} IEXPLORE.EXE]
+               if {[file exists $firefox]} {
+                       set browser $firefox
+               } elseif {[file exists $ie]} {
+                       set browser $ie
+               }
+               unset program_files firefox ie
+       }
+}
+
+if {[file isfile $doc_path]} {
+       set doc_url "file:$doc_path"
+} else {
+       set doc_url {http://www.kernel.org/pub/software/scm/git/docs/}
+}
+
+if {$browser ne {}} {
+       .mbar.help add command -label [mc "Online Documentation"] \
+               -command [list exec $browser $doc_url &]
+}
+unset browser doc_path doc_url
+
+# -- Standard bindings
+#
+wm protocol . WM_DELETE_WINDOW do_quit
+bind all <$M1B-Key-q> do_quit
+bind all <$M1B-Key-Q> do_quit
+bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
+bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
+
+set subcommand_args {}
+proc usage {} {
+       puts stderr "usage: $::argv0 $::subcommand $::subcommand_args"
+       exit 1
+}
+
+# -- Not a normal commit type invocation?  Do that instead!
+#
+switch -- $subcommand {
+browser -
+blame {
+       set subcommand_args {rev? path}
+       if {$argv eq {}} usage
+       set head {}
+       set path {}
+       set is_path 0
+       foreach a $argv {
+               if {$is_path || [file exists $_prefix$a]} {
+                       if {$path ne {}} usage
+                       set path $_prefix$a
+                       break
+               } elseif {$a eq {--}} {
+                       if {$path ne {}} {
+                               if {$head ne {}} usage
+                               set head $path
+                               set path {}
+                       }
+                       set is_path 1
+               } elseif {$head eq {}} {
+                       if {$head ne {}} usage
+                       set head $a
+                       set is_path 1
+               } else {
+                       usage
+               }
+       }
+       unset is_path
+
+       if {$head ne {} && $path eq {}} {
+               set path $_prefix$head
+               set head {}
+       }
+
+       if {$head eq {}} {
+               load_current_branch
+       } else {
+               if {[regexp {^[0-9a-f]{1,39}$} $head]} {
+                       if {[catch {
+                                       set head [git rev-parse --verify $head]
+                               } err]} {
+                               puts stderr $err
+                               exit 1
+                       }
+               }
+               set current_branch $head
+       }
+
+       switch -- $subcommand {
+       browser {
+               if {$head eq {}} {
+                       if {$path ne {} && [file isdirectory $path]} {
+                               set head $current_branch
+                       } else {
+                               set head $path
+                               set path {}
+                       }
+               }
+               browser::new $head $path
+       }
+       blame   {
+               if {$head eq {} && ![file exists $path]} {
+                       puts stderr [mc "fatal: cannot stat path %s: No such file or directory" $path]
+                       exit 1
+               }
+               blame::new $head $path
+       }
+       }
+       return
+}
+citool -
+gui {
+       if {[llength $argv] != 0} {
+               puts -nonewline stderr "usage: $argv0"
+               if {$subcommand ne {gui}
+                       && [file tail $argv0] ne "git-$subcommand"} {
+                       puts -nonewline stderr " $subcommand"
+               }
+               puts stderr {}
+               exit 1
+       }
+       # fall through to setup UI for commits
+}
+default {
+       puts stderr "usage: $argv0 \[{blame|browser|citool}\]"
+       exit 1
+}
+}
+
+# -- Branch Control
+#
+frame .branch \
+       -borderwidth 1 \
+       -relief sunken
+label .branch.l1 \
+       -text [mc "Current Branch:"] \
+       -anchor w \
+       -justify left
+label .branch.cb \
+       -textvariable current_branch \
+       -anchor w \
+       -justify left
+pack .branch.l1 -side left
+pack .branch.cb -side left -fill x
+pack .branch -side top -fill x
+
+# -- Main Window Layout
+#
+panedwindow .vpane -orient horizontal
+panedwindow .vpane.files -orient vertical
+.vpane add .vpane.files -sticky nsew -height 100 -width 200
+pack .vpane -anchor n -side top -fill both -expand 1
+
+# -- Index File List
+#
+frame .vpane.files.index -height 100 -width 200
+label .vpane.files.index.title -text [mc "Staged Changes (Will Commit)"] \
+       -background lightgreen
+text $ui_index -background white -borderwidth 0 \
+       -width 20 -height 10 \
+       -wrap none \
+       -cursor $cursor_ptr \
+       -xscrollcommand {.vpane.files.index.sx set} \
+       -yscrollcommand {.vpane.files.index.sy set} \
+       -state disabled
+scrollbar .vpane.files.index.sx -orient h -command [list $ui_index xview]
+scrollbar .vpane.files.index.sy -orient v -command [list $ui_index yview]
+pack .vpane.files.index.title -side top -fill x
+pack .vpane.files.index.sx -side bottom -fill x
+pack .vpane.files.index.sy -side right -fill y
+pack $ui_index -side left -fill both -expand 1
+
+# -- Working Directory File List
+#
+frame .vpane.files.workdir -height 100 -width 200
+label .vpane.files.workdir.title -text [mc "Unstaged Changes"] \
+       -background lightsalmon
+text $ui_workdir -background white -borderwidth 0 \
+       -width 20 -height 10 \
+       -wrap none \
+       -cursor $cursor_ptr \
+       -xscrollcommand {.vpane.files.workdir.sx set} \
+       -yscrollcommand {.vpane.files.workdir.sy set} \
+       -state disabled
+scrollbar .vpane.files.workdir.sx -orient h -command [list $ui_workdir xview]
+scrollbar .vpane.files.workdir.sy -orient v -command [list $ui_workdir yview]
+pack .vpane.files.workdir.title -side top -fill x
+pack .vpane.files.workdir.sx -side bottom -fill x
+pack .vpane.files.workdir.sy -side right -fill y
+pack $ui_workdir -side left -fill both -expand 1
+
+.vpane.files add .vpane.files.workdir -sticky nsew
+.vpane.files add .vpane.files.index -sticky nsew
+
+foreach i [list $ui_index $ui_workdir] {
+       rmsel_tag $i
+       $i tag conf in_diff -background [$i tag cget in_sel -background]
+}
+unset i
+
+# -- Diff and Commit Area
+#
+frame .vpane.lower -height 300 -width 400
+frame .vpane.lower.commarea
+frame .vpane.lower.diff -relief sunken -borderwidth 1
+pack .vpane.lower.diff -fill both -expand 1
+pack .vpane.lower.commarea -side bottom -fill x
+.vpane add .vpane.lower -sticky nsew
+
+# -- Commit Area Buttons
+#
+frame .vpane.lower.commarea.buttons
+label .vpane.lower.commarea.buttons.l -text {} \
+       -anchor w \
+       -justify left
+pack .vpane.lower.commarea.buttons.l -side top -fill x
+pack .vpane.lower.commarea.buttons -side left -fill y
+
+button .vpane.lower.commarea.buttons.rescan -text [mc Rescan] \
+       -command do_rescan
+pack .vpane.lower.commarea.buttons.rescan -side top -fill x
+lappend disable_on_lock \
+       {.vpane.lower.commarea.buttons.rescan conf -state}
+
+button .vpane.lower.commarea.buttons.incall -text [mc "Stage Changed"] \
+       -command do_add_all
+pack .vpane.lower.commarea.buttons.incall -side top -fill x
+lappend disable_on_lock \
+       {.vpane.lower.commarea.buttons.incall conf -state}
+
+button .vpane.lower.commarea.buttons.signoff -text [mc "Sign Off"] \
+       -command do_signoff
+pack .vpane.lower.commarea.buttons.signoff -side top -fill x
+
+button .vpane.lower.commarea.buttons.commit -text [mc Commit@@verb] \
+       -command do_commit
+pack .vpane.lower.commarea.buttons.commit -side top -fill x
+lappend disable_on_lock \
+       {.vpane.lower.commarea.buttons.commit conf -state}
+
+button .vpane.lower.commarea.buttons.push -text [mc Push] \
+       -command do_push_anywhere
+pack .vpane.lower.commarea.buttons.push -side top -fill x
+
+# -- Commit Message Buffer
+#
+frame .vpane.lower.commarea.buffer
+frame .vpane.lower.commarea.buffer.header
+set ui_comm .vpane.lower.commarea.buffer.t
+set ui_coml .vpane.lower.commarea.buffer.header.l
+radiobutton .vpane.lower.commarea.buffer.header.new \
+       -text [mc "New Commit"] \
+       -command do_select_commit_type \
+       -variable selected_commit_type \
+       -value new
+lappend disable_on_lock \
+       [list .vpane.lower.commarea.buffer.header.new conf -state]
+radiobutton .vpane.lower.commarea.buffer.header.amend \
+       -text [mc "Amend Last Commit"] \
+       -command do_select_commit_type \
+       -variable selected_commit_type \
+       -value amend
+lappend disable_on_lock \
+       [list .vpane.lower.commarea.buffer.header.amend conf -state]
+label $ui_coml \
+       -anchor w \
+       -justify left
+proc trace_commit_type {varname args} {
+       global ui_coml commit_type
+       switch -glob -- $commit_type {
+       initial       {set txt [mc "Initial Commit Message:"]}
+       amend         {set txt [mc "Amended Commit Message:"]}
+       amend-initial {set txt [mc "Amended Initial Commit Message:"]}
+       amend-merge   {set txt [mc "Amended Merge Commit Message:"]}
+       merge         {set txt [mc "Merge Commit Message:"]}
+       *             {set txt [mc "Commit Message:"]}
+       }
+       $ui_coml conf -text $txt
+}
+trace add variable commit_type write trace_commit_type
+pack $ui_coml -side left -fill x
+pack .vpane.lower.commarea.buffer.header.amend -side right
+pack .vpane.lower.commarea.buffer.header.new -side right
+
+text $ui_comm -background white -borderwidth 1 \
+       -undo true \
+       -maxundo 20 \
+       -autoseparators true \
+       -relief sunken \
+       -width 75 -height 9 -wrap none \
+       -font font_diff \
+       -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
+scrollbar .vpane.lower.commarea.buffer.sby \
+       -command [list $ui_comm yview]
+pack .vpane.lower.commarea.buffer.header -side top -fill x
+pack .vpane.lower.commarea.buffer.sby -side right -fill y
+pack $ui_comm -side left -fill y
+pack .vpane.lower.commarea.buffer -side left -fill y
+
+# -- Commit Message Buffer Context Menu
+#
+set ctxm .vpane.lower.commarea.buffer.ctxm
+menu $ctxm -tearoff 0
+$ctxm add command \
+       -label [mc Cut] \
+       -command {tk_textCut $ui_comm}
+$ctxm add command \
+       -label [mc Copy] \
+       -command {tk_textCopy $ui_comm}
+$ctxm add command \
+       -label [mc Paste] \
+       -command {tk_textPaste $ui_comm}
+$ctxm add command \
+       -label [mc Delete] \
+       -command {$ui_comm delete sel.first sel.last}
+$ctxm add separator
+$ctxm add command \
+       -label [mc "Select All"] \
+       -command {focus $ui_comm;$ui_comm tag add sel 0.0 end}
+$ctxm add command \
+       -label [mc "Copy All"] \
+       -command {
+               $ui_comm tag add sel 0.0 end
+               tk_textCopy $ui_comm
+               $ui_comm tag remove sel 0.0 end
+       }
+$ctxm add separator
+$ctxm add command \
+       -label [mc "Sign Off"] \
+       -command do_signoff
+bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
+
+# -- Diff Header
+#
+proc trace_current_diff_path {varname args} {
+       global current_diff_path diff_actions file_states
+       if {$current_diff_path eq {}} {
+               set s {}
+               set f {}
+               set p {}
+               set o disabled
+       } else {
+               set p $current_diff_path
+               set s [mapdesc [lindex $file_states($p) 0] $p]
+               set f [mc "File:"]
+               set p [escape_path $p]
+               set o normal
+       }
+
+       .vpane.lower.diff.header.status configure -text $s
+       .vpane.lower.diff.header.file configure -text $f
+       .vpane.lower.diff.header.path configure -text $p
+       foreach w $diff_actions {
+               uplevel #0 $w $o
+       }
+}
+trace add variable current_diff_path write trace_current_diff_path
+
+frame .vpane.lower.diff.header -background gold
+label .vpane.lower.diff.header.status \
+       -background gold \
+       -width $max_status_desc \
+       -anchor w \
+       -justify left
+label .vpane.lower.diff.header.file \
+       -background gold \
+       -anchor w \
+       -justify left
+label .vpane.lower.diff.header.path \
+       -background gold \
+       -anchor w \
+       -justify left
+pack .vpane.lower.diff.header.status -side left
+pack .vpane.lower.diff.header.file -side left
+pack .vpane.lower.diff.header.path -fill x
+set ctxm .vpane.lower.diff.header.ctxm
+menu $ctxm -tearoff 0
+$ctxm add command \
+       -label [mc Copy] \
+       -command {
+               clipboard clear
+               clipboard append \
+                       -format STRING \
+                       -type STRING \
+                       -- $current_diff_path
+       }
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
+bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
+
+# -- Diff Body
+#
+frame .vpane.lower.diff.body
+set ui_diff .vpane.lower.diff.body.t
+text $ui_diff -background white -borderwidth 0 \
+       -width 80 -height 15 -wrap none \
+       -font font_diff \
+       -xscrollcommand {.vpane.lower.diff.body.sbx set} \
+       -yscrollcommand {.vpane.lower.diff.body.sby set} \
+       -state disabled
+scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
+       -command [list $ui_diff xview]
+scrollbar .vpane.lower.diff.body.sby -orient vertical \
+       -command [list $ui_diff yview]
+pack .vpane.lower.diff.body.sbx -side bottom -fill x
+pack .vpane.lower.diff.body.sby -side right -fill y
+pack $ui_diff -side left -fill both -expand 1
+pack .vpane.lower.diff.header -side top -fill x
+pack .vpane.lower.diff.body -side bottom -fill both -expand 1
+
+$ui_diff tag conf d_cr -elide true
+$ui_diff tag conf d_@ -foreground blue -font font_diffbold
+$ui_diff tag conf d_+ -foreground {#00a000}
+$ui_diff tag conf d_- -foreground red
+
+$ui_diff tag conf d_++ -foreground {#00a000}
+$ui_diff tag conf d_-- -foreground red
+$ui_diff tag conf d_+s \
+       -foreground {#00a000} \
+       -background {#e2effa}
+$ui_diff tag conf d_-s \
+       -foreground red \
+       -background {#e2effa}
+$ui_diff tag conf d_s+ \
+       -foreground {#00a000} \
+       -background ivory1
+$ui_diff tag conf d_s- \
+       -foreground red \
+       -background ivory1
+
+$ui_diff tag conf d<<<<<<< \
+       -foreground orange \
+       -font font_diffbold
+$ui_diff tag conf d======= \
+       -foreground orange \
+       -font font_diffbold
+$ui_diff tag conf d>>>>>>> \
+       -foreground orange \
+       -font font_diffbold
+
+$ui_diff tag raise sel
+
+# -- Diff Body Context Menu
+#
+set ctxm .vpane.lower.diff.body.ctxm
+menu $ctxm -tearoff 0
+$ctxm add command \
+       -label [mc Refresh] \
+       -command reshow_diff
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
+$ctxm add command \
+       -label [mc Copy] \
+       -command {tk_textCopy $ui_diff}
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
+$ctxm add command \
+       -label [mc "Select All"] \
+       -command {focus $ui_diff;$ui_diff tag add sel 0.0 end}
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
+$ctxm add command \
+       -label [mc "Copy All"] \
+       -command {
+               $ui_diff tag add sel 0.0 end
+               tk_textCopy $ui_diff
+               $ui_diff tag remove sel 0.0 end
+       }
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
+$ctxm add separator
+$ctxm add command \
+       -label [mc "Apply/Reverse Hunk"] \
+       -command {apply_hunk $cursorX $cursorY}
+set ui_diff_applyhunk [$ctxm index last]
+lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state]
+$ctxm add separator
+$ctxm add command \
+       -label [mc "Decrease Font Size"] \
+       -command {incr_font_size font_diff -1}
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
+$ctxm add command \
+       -label [mc "Increase Font Size"] \
+       -command {incr_font_size font_diff 1}
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
+$ctxm add separator
+$ctxm add command \
+       -label [mc "Show Less Context"] \
+       -command {if {$repo_config(gui.diffcontext) >= 1} {
+               incr repo_config(gui.diffcontext) -1
+               reshow_diff
+       }}
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
+$ctxm add command \
+       -label [mc "Show More Context"] \
+       -command {if {$repo_config(gui.diffcontext) < 99} {
+               incr repo_config(gui.diffcontext)
+               reshow_diff
+       }}
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
+$ctxm add separator
+$ctxm add command -label [mc "Options..."] \
+       -command do_options
+proc popup_diff_menu {ctxm x y X Y} {
+       global current_diff_path file_states
+       set ::cursorX $x
+       set ::cursorY $y
+       if {$::ui_index eq $::current_diff_side} {
+               set l [mc "Unstage Hunk From Commit"]
+       } else {
+               set l [mc "Stage Hunk For Commit"]
+       }
+       if {$::is_3way_diff
+               || $current_diff_path eq {}
+               || ![info exists file_states($current_diff_path)]
+               || {_O} eq [lindex $file_states($current_diff_path) 0]} {
+               set s disabled
+       } else {
+               set s normal
+       }
+       $ctxm entryconf $::ui_diff_applyhunk -state $s -label $l
+       tk_popup $ctxm $X $Y
+}
+bind_button3 $ui_diff [list popup_diff_menu $ctxm %x %y %X %Y]
+
+# -- Status Bar
+#
+set main_status [::status_bar::new .status]
+pack .status -anchor w -side bottom -fill x
+$main_status show [mc "Initializing..."]
+
+# -- Load geometry
+#
+catch {
+set gm $repo_config(gui.geometry)
+wm geometry . [lindex $gm 0]
+.vpane sash place 0 \
+       [lindex $gm 1] \
+       [lindex [.vpane sash coord 0] 1]
+.vpane.files sash place 0 \
+       [lindex [.vpane.files sash coord 0] 0] \
+       [lindex $gm 2]
+unset gm
+}
+
+# -- Key Bindings
+#
+bind $ui_comm <$M1B-Key-Return> {do_commit;break}
+bind $ui_comm <$M1B-Key-t> {do_add_selection;break}
+bind $ui_comm <$M1B-Key-T> {do_add_selection;break}
+bind $ui_comm <$M1B-Key-i> {do_add_all;break}
+bind $ui_comm <$M1B-Key-I> {do_add_all;break}
+bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
+bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
+bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
+bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
+bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
+bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
+bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
+bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
+
+bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
+bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
+bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
+bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
+bind $ui_diff <$M1B-Key-v> {break}
+bind $ui_diff <$M1B-Key-V> {break}
+bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
+bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
+bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
+bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
+bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
+bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
+bind $ui_diff <Key-k>         {catch {%W yview scroll -1 units};break}
+bind $ui_diff <Key-j>         {catch {%W yview scroll  1 units};break}
+bind $ui_diff <Key-h>         {catch {%W xview scroll -1 units};break}
+bind $ui_diff <Key-l>         {catch {%W xview scroll  1 units};break}
+bind $ui_diff <Control-Key-b> {catch {%W yview scroll -1 pages};break}
+bind $ui_diff <Control-Key-f> {catch {%W yview scroll  1 pages};break}
+bind $ui_diff <Button-1>   {focus %W}
+
+if {[is_enabled branch]} {
+       bind . <$M1B-Key-n> branch_create::dialog
+       bind . <$M1B-Key-N> branch_create::dialog
+       bind . <$M1B-Key-o> branch_checkout::dialog
+       bind . <$M1B-Key-O> branch_checkout::dialog
+       bind . <$M1B-Key-m> merge::dialog
+       bind . <$M1B-Key-M> merge::dialog
+}
+if {[is_enabled transport]} {
+       bind . <$M1B-Key-p> do_push_anywhere
+       bind . <$M1B-Key-P> do_push_anywhere
+}
+
+bind .   <Key-F5>     do_rescan
+bind .   <$M1B-Key-r> do_rescan
+bind .   <$M1B-Key-R> do_rescan
+bind .   <$M1B-Key-s> do_signoff
+bind .   <$M1B-Key-S> do_signoff
+bind .   <$M1B-Key-t> do_add_selection
+bind .   <$M1B-Key-T> do_add_selection
+bind .   <$M1B-Key-i> do_add_all
+bind .   <$M1B-Key-I> do_add_all
+bind .   <$M1B-Key-Return> do_commit
+foreach i [list $ui_index $ui_workdir] {
+       bind $i <Button-1>       "toggle_or_diff         $i %x %y; break"
+       bind $i <$M1B-Button-1>  "add_one_to_selection   $i %x %y; break"
+       bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
+}
+unset i
+
+set file_lists($ui_index) [list]
+set file_lists($ui_workdir) [list]
+
+wm title . "[appname] ([reponame]) [file normalize [file dirname [gitdir]]]"
+focus -force $ui_comm
+
+# -- Warn the user about environmental problems.  Cygwin's Tcl
+#    does *not* pass its env array onto any processes it spawns.
+#    This means that git processes get none of our environment.
+#
+if {[is_Cygwin]} {
+       set ignored_env 0
+       set suggest_user {}
+       set msg [mc "Possible environment issues exist.
+
+The following environment variables are probably
+going to be ignored by any Git subprocess run
+by %s:
+
+" [appname]]
+       foreach name [array names env] {
+               switch -regexp -- $name {
+               {^GIT_INDEX_FILE$} -
+               {^GIT_OBJECT_DIRECTORY$} -
+               {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} -
+               {^GIT_DIFF_OPTS$} -
+               {^GIT_EXTERNAL_DIFF$} -
+               {^GIT_PAGER$} -
+               {^GIT_TRACE$} -
+               {^GIT_CONFIG$} -
+               {^GIT_CONFIG_LOCAL$} -
+               {^GIT_(AUTHOR|COMMITTER)_DATE$} {
+                       append msg " - $name\n"
+                       incr ignored_env
+               }
+               {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} {
+                       append msg " - $name\n"
+                       incr ignored_env
+                       set suggest_user $name
+               }
+               }
+       }
+       if {$ignored_env > 0} {
+               append msg [mc "
+This is due to a known issue with the
+Tcl binary distributed by Cygwin."]
+
+               if {$suggest_user ne {}} {
+                       append msg [mc "
+
+A good replacement for %s
+is placing values for the user.name and
+user.email settings into your personal
+~/.gitconfig file.
+" $suggest_user]
+               }
+               warn_popup $msg
+       }
+       unset ignored_env msg suggest_user name
+}
+
+# -- Only initialize complex UI if we are going to stay running.
+#
+if {[is_enabled transport]} {
+       load_all_remotes
+
+       set n [.mbar.remote index end]
+       populate_push_menu
+       populate_fetch_menu
+       set n [expr {[.mbar.remote index end] - $n}]
+       if {$n > 0} {
+               .mbar.remote insert $n separator
+       }
+       unset n
+}
+
+if {[winfo exists $ui_comm]} {
+       set GITGUI_BCK_exists [load_message GITGUI_BCK]
+
+       # -- If both our backup and message files exist use the
+       #    newer of the two files to initialize the buffer.
+       #
+       if {$GITGUI_BCK_exists} {
+               set m [gitdir GITGUI_MSG]
+               if {[file isfile $m]} {
+                       if {[file mtime [gitdir GITGUI_BCK]] > [file mtime $m]} {
+                               catch {file delete [gitdir GITGUI_MSG]}
+                       } else {
+                               $ui_comm delete 0.0 end
+                               $ui_comm edit reset
+                               $ui_comm edit modified false
+                               catch {file delete [gitdir GITGUI_BCK]}
+                               set GITGUI_BCK_exists 0
+                       }
+               }
+               unset m
+       }
+
+       proc backup_commit_buffer {} {
+               global ui_comm GITGUI_BCK_exists
+
+               set m [$ui_comm edit modified]
+               if {$m || $GITGUI_BCK_exists} {
+                       set msg [string trim [$ui_comm get 0.0 end]]
+                       regsub -all -line {[ \r\t]+$} $msg {} msg
+
+                       if {$msg eq {}} {
+                               if {$GITGUI_BCK_exists} {
+                                       catch {file delete [gitdir GITGUI_BCK]}
+                                       set GITGUI_BCK_exists 0
+                               }
+                       } elseif {$m} {
+                               catch {
+                                       set fd [open [gitdir GITGUI_BCK] w]
+                                       puts -nonewline $fd $msg
+                                       close $fd
+                                       set GITGUI_BCK_exists 1
+                               }
+                       }
+
+                       $ui_comm edit modified false
+               }
+
+               set ::GITGUI_BCK_i [after 2000 backup_commit_buffer]
+       }
+
+       backup_commit_buffer
+}
+
+lock_index begin-read
+if {![winfo ismapped .]} {
+       wm deiconify .
+}
+after 1 do_rescan
+if {[is_enabled multicommit]} {
+       after 1000 hint_gc
+}
diff --git a/git-gui/lib/about.tcl b/git-gui/lib/about.tcl
new file mode 100644 (file)
index 0000000..719fc54
--- /dev/null
@@ -0,0 +1,81 @@
+# git-gui about git-gui dialog
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc do_about {} {
+       global appvers copyright oguilib
+       global tcl_patchLevel tk_patchLevel
+
+       set w .about_dialog
+       toplevel $w
+       wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
+
+       pack [git_logo $w.git_logo] -side left -fill y -padx 10 -pady 10
+       label $w.header -text [mc "About %s" [appname]] \
+               -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       button $w.buttons.close -text {Close} \
+               -default active \
+               -command [list destroy $w]
+       pack $w.buttons.close -side right
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       label $w.desc \
+               -text "[mc "git-gui - a graphical user interface for Git."]\n$copyright" \
+               -padx 5 -pady 5 \
+               -justify left \
+               -anchor w \
+               -borderwidth 1 \
+               -relief solid
+       pack $w.desc -side top -fill x -padx 5 -pady 5
+
+       set v {}
+       append v "git-gui version $appvers\n"
+       append v "[git version]\n"
+       append v "\n"
+       if {$tcl_patchLevel eq $tk_patchLevel} {
+               append v "Tcl/Tk version $tcl_patchLevel"
+       } else {
+               append v "Tcl version $tcl_patchLevel"
+               append v ", Tk version $tk_patchLevel"
+       }
+
+       set d {}
+       append d "git wrapper: $::_git\n"
+       append d "git exec dir: [gitexec]\n"
+       append d "git-gui lib: $oguilib"
+
+       label $w.vers \
+               -text $v \
+               -padx 5 -pady 5 \
+               -justify left \
+               -anchor w \
+               -borderwidth 1 \
+               -relief solid
+       pack $w.vers -side top -fill x -padx 5 -pady 5
+
+       label $w.dirs \
+               -text $d \
+               -padx 5 -pady 5 \
+               -justify left \
+               -anchor w \
+               -borderwidth 1 \
+               -relief solid
+       pack $w.dirs -side top -fill x -padx 5 -pady 5
+
+       menu $w.ctxm -tearoff 0
+       $w.ctxm add command \
+               -label {Copy} \
+               -command "
+               clipboard clear
+               clipboard append -format STRING -type STRING -- \[$w.vers cget -text\]
+       "
+
+       bind $w <Visibility> "grab $w; focus $w.buttons.close"
+       bind $w <Key-Escape> "destroy $w"
+       bind $w <Key-Return> "destroy $w"
+       bind_button3 $w.vers "tk_popup $w.ctxm %X %Y; grab $w; focus $w"
+       wm title $w "About [appname]"
+       tkwait window $w
+}
diff --git a/git-gui/lib/blame.tcl b/git-gui/lib/blame.tcl
new file mode 100644 (file)
index 0000000..00ecf21
--- /dev/null
@@ -0,0 +1,980 @@
+# git-gui blame viewer
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+class blame {
+
+image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
+
+# Persistant data (survives loads)
+#
+field history {}; # viewer history: {commit path}
+field header    ; # array commit,key -> header field
+
+# Tk UI control paths
+#
+field w          ; # top window in this viewer
+field w_back     ; # our back button
+field w_path     ; # label showing the current file path
+field w_columns  ; # list of all column widgets in the viewer
+field w_line     ; # text column: all line numbers
+field w_amov     ; # text column: annotations + move tracking
+field w_asim     ; # text column: annotations (simple computation)
+field w_file     ; # text column: actual file data
+field w_cviewer  ; # pane showing commit message
+field status     ; # status mega-widget instance
+field old_height ; # last known height of $w.file_pane
+
+# Tk UI colors
+#
+variable active_color #c0edc5
+variable group_colors {
+       #d6d6d6
+       #e1e1e1
+       #ececec
+}
+
+# Switches for original location detection
+#
+variable original_options [list -C -C]
+if {[git-version >= 1.5.3]} {
+       lappend original_options -w ; # ignore indentation changes
+}
+
+# Current blame data; cleared/reset on each load
+#
+field commit               ; # input commit to blame
+field path                 ; # input filename to view in $commit
+
+field current_fd        {} ; # background process running
+field highlight_line    -1 ; # current line selected
+field highlight_column  {} ; # current commit column selected
+field highlight_commit  {} ; # sha1 of commit selected
+
+field total_lines       0  ; # total length of file
+field blame_lines       0  ; # number of lines computed
+field amov_data            ; # list of {commit origfile origline}
+field asim_data            ; # list of {commit origfile origline}
+
+field r_commit             ; # commit currently being parsed
+field r_orig_line          ; # original line number
+field r_final_line         ; # final line number
+field r_line_count         ; # lines in this region
+
+field tooltip_wm        {} ; # Current tooltip toplevel, if open
+field tooltip_t         {} ; # Text widget in $tooltip_wm
+field tooltip_timer     {} ; # Current timer event for our tooltip
+field tooltip_commit    {} ; # Commit(s) in tooltip
+
+constructor new {i_commit i_path} {
+       global cursor_ptr
+       variable active_color
+       variable group_colors
+
+       set commit $i_commit
+       set path   $i_path
+
+       make_toplevel top w
+       wm title $top [append "[appname] ([reponame]): " [mc "File Viewer"]]
+
+       frame $w.header -background gold
+       label $w.header.commit_l \
+               -text [mc "Commit:"] \
+               -background gold \
+               -anchor w \
+               -justify left
+       set w_back $w.header.commit_b
+       label $w_back \
+               -image ::blame::img_back_arrow \
+               -borderwidth 0 \
+               -relief flat \
+               -state disabled \
+               -background gold \
+               -activebackground gold
+       bind $w_back <Button-1> "
+               if {\[$w_back cget -state\] eq {normal}} {
+                       [cb _history_menu]
+               }
+               "
+       label $w.header.commit \
+               -textvariable @commit \
+               -background gold \
+               -anchor w \
+               -justify left
+       label $w.header.path_l \
+               -text [mc "File:"] \
+               -background gold \
+               -anchor w \
+               -justify left
+       set w_path $w.header.path
+       label $w_path \
+               -background gold \
+               -anchor w \
+               -justify left
+       pack $w.header.commit_l -side left
+       pack $w_back -side left
+       pack $w.header.commit -side left
+       pack $w_path -fill x -side right
+       pack $w.header.path_l -side right
+
+       panedwindow $w.file_pane -orient vertical
+       frame $w.file_pane.out
+       frame $w.file_pane.cm
+       $w.file_pane add $w.file_pane.out \
+               -sticky nsew \
+               -minsize 100 \
+               -height 100 \
+               -width 100
+       $w.file_pane add $w.file_pane.cm \
+               -sticky nsew \
+               -minsize 25 \
+               -height 25 \
+               -width 100
+
+       set w_line $w.file_pane.out.linenumber_t
+       text $w_line \
+               -takefocus 0 \
+               -highlightthickness 0 \
+               -padx 0 -pady 0 \
+               -background white -borderwidth 0 \
+               -state disabled \
+               -wrap none \
+               -height 40 \
+               -width 6 \
+               -font font_diff
+       $w_line tag conf linenumber -justify right -rmargin 5
+
+       set w_amov $w.file_pane.out.amove_t
+       text $w_amov \
+               -takefocus 0 \
+               -highlightthickness 0 \
+               -padx 0 -pady 0 \
+               -background white -borderwidth 0 \
+               -state disabled \
+               -wrap none \
+               -height 40 \
+               -width 5 \
+               -font font_diff
+       $w_amov tag conf author_abbr -justify right -rmargin 5
+       $w_amov tag conf curr_commit
+       $w_amov tag conf prior_commit -foreground blue -underline 1
+       $w_amov tag bind prior_commit \
+               <Button-1> \
+               "[cb _load_commit $w_amov @amov_data @%x,%y];break"
+
+       set w_asim $w.file_pane.out.asimple_t
+       text $w_asim \
+               -takefocus 0 \
+               -highlightthickness 0 \
+               -padx 0 -pady 0 \
+               -background white -borderwidth 0 \
+               -state disabled \
+               -wrap none \
+               -height 40 \
+               -width 4 \
+               -font font_diff
+       $w_asim tag conf author_abbr -justify right
+       $w_asim tag conf curr_commit
+       $w_asim tag conf prior_commit -foreground blue -underline 1
+       $w_asim tag bind prior_commit \
+               <Button-1> \
+               "[cb _load_commit $w_asim @asim_data @%x,%y];break"
+
+       set w_file $w.file_pane.out.file_t
+       text $w_file \
+               -takefocus 0 \
+               -highlightthickness 0 \
+               -padx 0 -pady 0 \
+               -background white -borderwidth 0 \
+               -state disabled \
+               -wrap none \
+               -height 40 \
+               -width 80 \
+               -xscrollcommand [list $w.file_pane.out.sbx set] \
+               -font font_diff
+
+       set w_columns [list $w_amov $w_asim $w_line $w_file]
+
+       scrollbar $w.file_pane.out.sbx \
+               -orient h \
+               -command [list $w_file xview]
+       scrollbar $w.file_pane.out.sby \
+               -orient v \
+               -command [list scrollbar2many $w_columns yview]
+       eval grid $w_columns $w.file_pane.out.sby -sticky nsew
+       grid conf \
+               $w.file_pane.out.sbx \
+               -column [expr {[llength $w_columns] - 1}] \
+               -sticky we
+       grid columnconfigure \
+               $w.file_pane.out \
+               [expr {[llength $w_columns] - 1}] \
+               -weight 1
+       grid rowconfigure $w.file_pane.out 0 -weight 1
+
+       set w_cviewer $w.file_pane.cm.t
+       text $w_cviewer \
+               -background white -borderwidth 0 \
+               -state disabled \
+               -wrap none \
+               -height 10 \
+               -width 80 \
+               -xscrollcommand [list $w.file_pane.cm.sbx set] \
+               -yscrollcommand [list $w.file_pane.cm.sby set] \
+               -font font_diff
+       $w_cviewer tag conf still_loading \
+               -font font_uiitalic \
+               -justify center
+       $w_cviewer tag conf header_key \
+               -tabs {3c} \
+               -background $active_color \
+               -font font_uibold
+       $w_cviewer tag conf header_val \
+               -background $active_color \
+               -font font_ui
+       $w_cviewer tag raise sel
+       scrollbar $w.file_pane.cm.sbx \
+               -orient h \
+               -command [list $w_cviewer xview]
+       scrollbar $w.file_pane.cm.sby \
+               -orient v \
+               -command [list $w_cviewer yview]
+       pack $w.file_pane.cm.sby -side right -fill y
+       pack $w.file_pane.cm.sbx -side bottom -fill x
+       pack $w_cviewer -expand 1 -fill both
+
+       set status [::status_bar::new $w.status]
+
+       menu $w.ctxm -tearoff 0
+       $w.ctxm add command \
+               -label [mc "Copy Commit"] \
+               -command [cb _copycommit]
+
+       foreach i $w_columns {
+               for {set g 0} {$g < [llength $group_colors]} {incr g} {
+                       $i tag conf color$g -background [lindex $group_colors $g]
+               }
+
+               $i conf -cursor $cursor_ptr
+               $i conf -yscrollcommand [list many2scrollbar \
+                       $w_columns yview $w.file_pane.out.sby]
+               bind $i <Button-1> "
+                       [cb _hide_tooltip]
+                       [cb _click $i @%x,%y]
+                       focus $i
+               "
+               bind $i <Any-Motion>  [cb _show_tooltip $i @%x,%y]
+               bind $i <Any-Enter>   [cb _hide_tooltip]
+               bind $i <Any-Leave>   [cb _hide_tooltip]
+               bind_button3 $i "
+                       [cb _hide_tooltip]
+                       set cursorX %x
+                       set cursorY %y
+                       set cursorW %W
+                       tk_popup $w.ctxm %X %Y
+               "
+               bind $i <Shift-Tab> "[list focus $w_cviewer];break"
+               bind $i <Tab>       "[list focus $w_cviewer];break"
+       }
+
+       foreach i [concat $w_columns $w_cviewer] {
+               bind $i <Key-Up>        {catch {%W yview scroll -1 units};break}
+               bind $i <Key-Down>      {catch {%W yview scroll  1 units};break}
+               bind $i <Key-Left>      {catch {%W xview scroll -1 units};break}
+               bind $i <Key-Right>     {catch {%W xview scroll  1 units};break}
+               bind $i <Key-k>         {catch {%W yview scroll -1 units};break}
+               bind $i <Key-j>         {catch {%W yview scroll  1 units};break}
+               bind $i <Key-h>         {catch {%W xview scroll -1 units};break}
+               bind $i <Key-l>         {catch {%W xview scroll  1 units};break}
+               bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break}
+               bind $i <Control-Key-f> {catch {%W yview scroll  1 pages};break}
+       }
+
+       bind $w_cviewer <Shift-Tab> "[list focus $w_file];break"
+       bind $w_cviewer <Tab>       "[list focus $w_file];break"
+       bind $w_cviewer <Button-1> [list focus $w_cviewer]
+       bind $w_file    <Visibility> [list focus $w_file]
+
+       grid configure $w.header -sticky ew
+       grid configure $w.file_pane -sticky nsew
+       grid configure $w.status -sticky ew
+       grid columnconfigure $top 0 -weight 1
+       grid rowconfigure $top 0 -weight 0
+       grid rowconfigure $top 1 -weight 1
+       grid rowconfigure $top 2 -weight 0
+
+       set req_w [winfo reqwidth  $top]
+       set req_h [winfo reqheight $top]
+       set scr_h [expr {[winfo screenheight $top] - 100}]
+       if {$req_w < 600} {set req_w 600}
+       if {$req_h < $scr_h} {set req_h $scr_h}
+       set g "${req_w}x${req_h}"
+       wm geometry $top $g
+       update
+
+       set old_height [winfo height $w.file_pane]
+       $w.file_pane sash place 0 \
+               [lindex [$w.file_pane sash coord 0] 0] \
+               [expr {int($old_height * 0.70)}]
+       bind $w.file_pane <Configure> \
+       "if {{$w.file_pane} eq {%W}} {[cb _resize %h]}"
+
+       _load $this {}
+}
+
+method _load {jump} {
+       variable group_colors
+
+       _hide_tooltip $this
+
+       if {$total_lines != 0 || $current_fd ne {}} {
+               if {$current_fd ne {}} {
+                       catch {close $current_fd}
+                       set current_fd {}
+               }
+
+               foreach i $w_columns {
+                       $i conf -state normal
+                       $i delete 0.0 end
+                       foreach g [$i tag names] {
+                               if {[regexp {^g[0-9a-f]{40}$} $g]} {
+                                       $i tag delete $g
+                               }
+                       }
+                       $i conf -state disabled
+               }
+
+               $w_cviewer conf -state normal
+               $w_cviewer delete 0.0 end
+               $w_cviewer conf -state disabled
+
+               set highlight_line -1
+               set highlight_column {}
+               set highlight_commit {}
+               set total_lines 0
+       }
+
+       if {$history eq {}} {
+               $w_back conf -state disabled
+       } else {
+               $w_back conf -state normal
+       }
+
+       # Index 0 is always empty.  There is never line 0 as
+       # we use only 1 based lines, as that matches both with
+       # git-blame output and with Tk's text widget.
+       #
+       set amov_data [list [list]]
+       set asim_data [list [list]]
+
+       $status show [mc "Reading %s..." "$commit:[escape_path $path]"]
+       $w_path conf -text [escape_path $path]
+       if {$commit eq {}} {
+               set fd [open $path r]
+               fconfigure $fd -eofchar {}
+       } else {
+               set fd [git_read cat-file blob "$commit:$path"]
+       }
+       fconfigure $fd -blocking 0 -translation lf -encoding binary
+       fileevent $fd readable [cb _read_file $fd $jump]
+       set current_fd $fd
+}
+
+method _history_menu {} {
+       set m $w.backmenu
+       if {[winfo exists $m]} {
+               $m delete 0 end
+       } else {
+               menu $m -tearoff 0
+       }
+
+       for {set i [expr {[llength $history] - 1}]
+               } {$i >= 0} {incr i -1} {
+               set e [lindex $history $i]
+               set c [lindex $e 0]
+               set f [lindex $e 1]
+
+               if {[regexp {^[0-9a-f]{40}$} $c]} {
+                       set t [string range $c 0 8]...
+               } elseif {$c eq {}} {
+                       set t {Working Directory}
+               } else {
+                       set t $c
+               }
+               if {![catch {set summary $header($c,summary)}]} {
+                       append t " $summary"
+                       if {[string length $t] > 70} {
+                               set t [string range $t 0 66]...
+                       }
+               }
+
+               $m add command -label $t -command [cb _goback $i]
+       }
+       set X [winfo rootx $w_back]
+       set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}]
+       tk_popup $m $X $Y
+}
+
+method _goback {i} {
+       set dat [lindex $history $i]
+       set history [lrange $history 0 [expr {$i - 1}]]
+       set commit [lindex $dat 0]
+       set path [lindex $dat 1]
+       _load $this [lrange $dat 2 5]
+}
+
+method _read_file {fd jump} {
+       if {$fd ne $current_fd} {
+               catch {close $fd}
+               return
+       }
+
+       foreach i $w_columns {$i conf -state normal}
+       while {[gets $fd line] >= 0} {
+               regsub "\r\$" $line {} line
+               incr total_lines
+               lappend amov_data {}
+               lappend asim_data {}
+
+               if {$total_lines > 1} {
+                       foreach i $w_columns {$i insert end "\n"}
+               }
+
+               $w_line insert end "$total_lines" linenumber
+               $w_file insert end "$line"
+       }
+
+       set ln_wc [expr {[string length $total_lines] + 2}]
+       if {[$w_line cget -width] < $ln_wc} {
+               $w_line conf -width $ln_wc
+       }
+
+       foreach i $w_columns {$i conf -state disabled}
+
+       if {[eof $fd]} {
+               close $fd
+
+               # If we don't force Tk to update the widgets *right now*
+               # none of our jump commands will cause a change in the UI.
+               #
+               update
+
+               if {[llength $jump] == 1} {
+                       set highlight_line [lindex $jump 0]
+                       $w_file see "$highlight_line.0"
+               } elseif {[llength $jump] == 4} {
+                       set highlight_column [lindex $jump 0]
+                       set highlight_line [lindex $jump 1]
+                       $w_file xview moveto [lindex $jump 2]
+                       $w_file yview moveto [lindex $jump 3]
+               }
+
+               _exec_blame $this $w_asim @asim_data \
+                       [list] \
+                       [mc "Loading copy/move tracking annotations..."]
+       }
+} ifdeleted { catch {close $fd} }
+
+method _exec_blame {cur_w cur_d options cur_s} {
+       lappend options --incremental
+       if {$commit eq {}} {
+               lappend options --contents $path
+       } else {
+               lappend options $commit
+       }
+       lappend options -- $path
+       set fd [eval git_read --nice blame $options]
+       fconfigure $fd -blocking 0 -translation lf -encoding binary
+       fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]
+       set current_fd $fd
+       set blame_lines 0
+
+       $status start \
+               $cur_s \
+               [mc "lines annotated"]
+}
+
+method _read_blame {fd cur_w cur_d} {
+       upvar #0 $cur_d line_data
+       variable group_colors
+       variable original_options
+
+       if {$fd ne $current_fd} {
+               catch {close $fd}
+               return
+       }
+
+       $cur_w conf -state normal
+       while {[gets $fd line] >= 0} {
+               if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
+                       cmit original_line final_line line_count]} {
+                       set r_commit     $cmit
+                       set r_orig_line  $original_line
+                       set r_final_line $final_line
+                       set r_line_count $line_count
+               } elseif {[string match {filename *} $line]} {
+                       set file [string range $line 9 end]
+                       set n    $r_line_count
+                       set lno  $r_final_line
+                       set oln  $r_orig_line
+                       set cmit $r_commit
+
+                       if {[regexp {^0{40}$} $cmit]} {
+                               set commit_abbr work
+                               set commit_type curr_commit
+                       } elseif {$cmit eq $commit} {
+                               set commit_abbr this
+                               set commit_type curr_commit
+                       } else {
+                               set commit_type prior_commit
+                               set commit_abbr [string range $cmit 0 3]
+                       }
+
+                       set author_abbr {}
+                       set a_name {}
+                       catch {set a_name $header($cmit,author)}
+                       while {$a_name ne {}} {
+                               if {$author_abbr ne {}
+                                       && [string index $a_name 0] eq {'}} {
+                                       regsub {^'[^']+'\s+} $a_name {} a_name
+                               }
+                               if {![regexp {^([[:upper:]])} $a_name _a]} break
+                               append author_abbr $_a
+                               unset _a
+                               if {![regsub \
+                                       {^[[:upper:]][^\s]*\s+} \
+                                       $a_name {} a_name ]} break
+                       }
+                       if {$author_abbr eq {}} {
+                               set author_abbr { |}
+                       } else {
+                               set author_abbr [string range $author_abbr 0 3]
+                       }
+                       unset a_name
+
+                       set first_lno $lno
+                       while {
+                          $first_lno > 1
+                       && $cmit eq [lindex $line_data [expr {$first_lno - 1}] 0]
+                       && $file eq [lindex $line_data [expr {$first_lno - 1}] 1]
+                       } {
+                               incr first_lno -1
+                       }
+
+                       set color {}
+                       if {$first_lno < $lno} {
+                               foreach g [$w_file tag names $first_lno.0] {
+                                       if {[regexp {^color[0-9]+$} $g]} {
+                                               set color $g
+                                               break
+                                       }
+                               }
+                       } else {
+                               set i [lsort [concat \
+                                       [$w_file tag names "[expr {$first_lno - 1}].0"] \
+                                       [$w_file tag names "[expr {$lno + $n}].0"] \
+                                       ]]
+                               for {set g 0} {$g < [llength $group_colors]} {incr g} {
+                                       if {[lsearch -sorted -exact $i color$g] == -1} {
+                                               set color color$g
+                                               break
+                                       }
+                               }
+                       }
+                       if {$color eq {}} {
+                               set color color0
+                       }
+
+                       while {$n > 0} {
+                               set lno_e "$lno.0 lineend + 1c"
+                               if {[lindex $line_data $lno] ne {}} {
+                                       set g [lindex $line_data $lno 0]
+                                       foreach i $w_columns {
+                                               $i tag remove g$g $lno.0 $lno_e
+                                       }
+                               }
+                               lset line_data $lno [list $cmit $file $oln]
+
+                               $cur_w delete $lno.0 "$lno.0 lineend"
+                               if {$lno == $first_lno} {
+                                       $cur_w insert $lno.0 $commit_abbr $commit_type
+                               } elseif {$lno == [expr {$first_lno + 1}]} {
+                                       $cur_w insert $lno.0 $author_abbr author_abbr
+                               } else {
+                                       $cur_w insert $lno.0 { |}
+                               }
+
+                               foreach i $w_columns {
+                                       if {$cur_w eq $w_amov} {
+                                               for {set g 0} \
+                                                       {$g < [llength $group_colors]} \
+                                                       {incr g} {
+                                                       $i tag remove color$g $lno.0 $lno_e
+                                               }
+                                               $i tag add $color $lno.0 $lno_e
+                                       }
+                                       $i tag add g$cmit $lno.0 $lno_e
+                               }
+
+                               if {$highlight_column eq $cur_w} {
+                                       if {$highlight_line == -1
+                                        && [lindex [$w_file yview] 0] == 0} {
+                                               $w_file see $lno.0
+                                               set highlight_line $lno
+                                       }
+                                       if {$highlight_line == $lno} {
+                                               _showcommit $this $cur_w $lno
+                                       }
+                               }
+
+                               incr n -1
+                               incr lno
+                               incr oln
+                               incr blame_lines
+                       }
+
+                       while {
+                          $cmit eq [lindex $line_data $lno 0]
+                       && $file eq [lindex $line_data $lno 1]
+                       } {
+                               $cur_w delete $lno.0 "$lno.0 lineend"
+
+                               if {$lno == $first_lno} {
+                                       $cur_w insert $lno.0 $commit_abbr $commit_type
+                               } elseif {$lno == [expr {$first_lno + 1}]} {
+                                       $cur_w insert $lno.0 $author_abbr author_abbr
+                               } else {
+                                       $cur_w insert $lno.0 { |}
+                               }
+
+                               if {$cur_w eq $w_amov} {
+                                       foreach i $w_columns {
+                                               for {set g 0} \
+                                                       {$g < [llength $group_colors]} \
+                                                       {incr g} {
+                                                       $i tag remove color$g $lno.0 $lno_e
+                                               }
+                                               $i tag add $color $lno.0 $lno_e
+                                       }
+                               }
+
+                               incr lno
+                       }
+
+               } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} {
+                       set header($r_commit,$key) $data
+               }
+       }
+       $cur_w conf -state disabled
+
+       if {[eof $fd]} {
+               close $fd
+               if {$cur_w eq $w_asim} {
+                       _exec_blame $this $w_amov @amov_data \
+                               $original_options \
+                               [mc "Loading original location annotations..."]
+               } else {
+                       set current_fd {}
+                       $status stop [mc "Annotation complete."]
+               }
+       } else {
+               $status update $blame_lines $total_lines
+       }
+} ifdeleted { catch {close $fd} }
+
+method _click {cur_w pos} {
+       set lno [lindex [split [$cur_w index $pos] .] 0]
+       _showcommit $this $cur_w $lno
+}
+
+method _load_commit {cur_w cur_d pos} {
+       upvar #0 $cur_d line_data
+       set lno [lindex [split [$cur_w index $pos] .] 0]
+       set dat [lindex $line_data $lno]
+       if {$dat ne {}} {
+               lappend history [list \
+                       $commit $path \
+                       $highlight_column \
+                       $highlight_line \
+                       [lindex [$w_file xview] 0] \
+                       [lindex [$w_file yview] 0] \
+                       ]
+               set commit [lindex $dat 0]
+               set path   [lindex $dat 1]
+               _load $this [list [lindex $dat 2]]
+       }
+}
+
+method _showcommit {cur_w lno} {
+       global repo_config
+       variable active_color
+
+       if {$highlight_commit ne {}} {
+               foreach i $w_columns {
+                       $i tag conf g$highlight_commit -background {}
+                       $i tag lower g$highlight_commit
+               }
+       }
+
+       if {$cur_w eq $w_asim} {
+               set dat [lindex $asim_data $lno]
+               set highlight_column $w_asim
+       } else {
+               set dat [lindex $amov_data $lno]
+               set highlight_column $w_amov
+       }
+
+       $w_cviewer conf -state normal
+       $w_cviewer delete 0.0 end
+
+       if {$dat eq {}} {
+               set cmit {}
+               $w_cviewer insert end [mc "Loading annotation..."] still_loading
+       } else {
+               set cmit [lindex $dat 0]
+               set file [lindex $dat 1]
+
+               foreach i $w_columns {
+                       $i tag conf g$cmit -background $active_color
+                       $i tag raise g$cmit
+               }
+
+               set author_name {}
+               set author_email {}
+               set author_time {}
+               catch {set author_name $header($cmit,author)}
+               catch {set author_email $header($cmit,author-mail)}
+               catch {set author_time [format_date $header($cmit,author-time)]}
+
+               set committer_name {}
+               set committer_email {}
+               set committer_time {}
+               catch {set committer_name $header($cmit,committer)}
+               catch {set committer_email $header($cmit,committer-mail)}
+               catch {set committer_time [format_date $header($cmit,committer-time)]}
+
+               if {[catch {set msg $header($cmit,message)}]} {
+                       set msg {}
+                       catch {
+                               set fd [git_read cat-file commit $cmit]
+                               fconfigure $fd -encoding binary -translation lf
+                               if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
+                                       set enc utf-8
+                               }
+                               while {[gets $fd line] > 0} {
+                                       if {[string match {encoding *} $line]} {
+                                               set enc [string tolower [string range $line 9 end]]
+                                       }
+                               }
+                               set msg [read $fd]
+                               close $fd
+
+                               set enc [tcl_encoding $enc]
+                               if {$enc ne {}} {
+                                       set msg [encoding convertfrom $enc $msg]
+                                       set author_name [encoding convertfrom $enc $author_name]
+                                       set committer_name [encoding convertfrom $enc $committer_name]
+                                       set header($cmit,author) $author_name
+                                       set header($cmit,committer) $committer_name
+                                       set header($cmit,summary) \
+                                       [encoding convertfrom $enc $header($cmit,summary)]
+                               }
+                               set msg [string trim $msg]
+                       }
+                       set header($cmit,message) $msg
+               }
+
+               $w_cviewer insert end "commit $cmit\n" header_key
+               $w_cviewer insert end [strcat [mc "Author:"] "\t"] header_key
+               $w_cviewer insert end "$author_name $author_email" header_val
+               $w_cviewer insert end "  $author_time\n" header_val
+
+               $w_cviewer insert end [strcat [mc "Committer:"] "\t"] header_key
+               $w_cviewer insert end "$committer_name $committer_email" header_val
+               $w_cviewer insert end "  $committer_time\n" header_val
+
+               if {$file ne $path} {
+                       $w_cviewer insert end [strcat [mc "Original File:"] "\t"] header_key
+                       $w_cviewer insert end "[escape_path $file]\n" header_val
+               }
+
+               $w_cviewer insert end "\n$msg"
+       }
+       $w_cviewer conf -state disabled
+
+       set highlight_line $lno
+       set highlight_commit $cmit
+
+       if {[lsearch -exact $tooltip_commit $highlight_commit] != -1} {
+               _hide_tooltip $this
+       }
+}
+
+method _copycommit {} {
+       set pos @$::cursorX,$::cursorY
+       set lno [lindex [split [$::cursorW index $pos] .] 0]
+       set dat [lindex $amov_data $lno]
+       if {$dat ne {}} {
+               clipboard clear
+               clipboard append \
+                       -format STRING \
+                       -type STRING \
+                       -- [lindex $dat 0]
+       }
+}
+
+method _show_tooltip {cur_w pos} {
+       if {$tooltip_wm ne {}} {
+               _open_tooltip $this $cur_w
+       } elseif {$tooltip_timer eq {}} {
+               set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]]
+       }
+}
+
+method _open_tooltip {cur_w} {
+       set tooltip_timer {}
+       set pos_x [winfo pointerx $cur_w]
+       set pos_y [winfo pointery $cur_w]
+       if {[winfo containing $pos_x $pos_y] ne $cur_w} {
+               _hide_tooltip $this
+               return
+       }
+
+       if {$tooltip_wm ne "$cur_w.tooltip"} {
+               _hide_tooltip $this
+
+               set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1]
+               wm overrideredirect $tooltip_wm 1
+               wm transient $tooltip_wm [winfo toplevel $cur_w]
+               set tooltip_t $tooltip_wm.label
+               text $tooltip_t \
+                       -takefocus 0 \
+                       -highlightthickness 0 \
+                       -relief flat \
+                       -borderwidth 0 \
+                       -wrap none \
+                       -background lightyellow \
+                       -foreground black
+               $tooltip_t tag conf section_header -font font_uibold
+               pack $tooltip_t
+       } else {
+               $tooltip_t conf -state normal
+               $tooltip_t delete 0.0 end
+       }
+
+       set pos @[join [list \
+               [expr {$pos_x - [winfo rootx $cur_w]}] \
+               [expr {$pos_y - [winfo rooty $cur_w]}]] ,]
+       set lno [lindex [split [$cur_w index $pos] .] 0]
+       if {$cur_w eq $w_amov} {
+               set dat [lindex $amov_data $lno]
+               set org {}
+       } else {
+               set dat [lindex $asim_data $lno]
+               set org [lindex $amov_data $lno]
+       }
+
+       if {$dat eq {}} {
+               _hide_tooltip $this
+               return
+       }
+
+       set cmit [lindex $dat 0]
+       set tooltip_commit [list $cmit]
+
+       set author_name {}
+       set summary     {}
+       set author_time {}
+       catch {set author_name $header($cmit,author)}
+       catch {set summary     $header($cmit,summary)}
+       catch {set author_time [format_date $header($cmit,author-time)]}
+
+       $tooltip_t insert end "commit $cmit\n"
+       $tooltip_t insert end "$author_name  $author_time\n"
+       $tooltip_t insert end "$summary"
+
+       if {$org ne {} && [lindex $org 0] ne $cmit} {
+               set save [$tooltip_t get 0.0 end]
+               $tooltip_t delete 0.0 end
+
+               set cmit [lindex $org 0]
+               set file [lindex $org 1]
+               lappend tooltip_commit $cmit
+
+               set author_name {}
+               set summary     {}
+               set author_time {}
+               catch {set author_name $header($cmit,author)}
+               catch {set summary     $header($cmit,summary)}
+               catch {set author_time [format_date $header($cmit,author-time)]}
+
+               $tooltip_t insert end [strcat [mc "Originally By:"] "\n"] section_header
+               $tooltip_t insert end "commit $cmit\n"
+               $tooltip_t insert end "$author_name  $author_time\n"
+               $tooltip_t insert end "$summary\n"
+
+               if {$file ne $path} {
+                       $tooltip_t insert end [strcat [mc "In File:"] " "] section_header
+                       $tooltip_t insert end "$file\n"
+               }
+
+               $tooltip_t insert end "\n"
+               $tooltip_t insert end [strcat [mc "Copied Or Moved Here By:"] "\n"] section_header
+               $tooltip_t insert end $save
+       }
+
+       $tooltip_t conf -state disabled
+       _position_tooltip $this
+}
+
+method _position_tooltip {} {
+       set max_h [lindex [split [$tooltip_t index end] .] 0]
+       set max_w 0
+       for {set i 1} {$i <= $max_h} {incr i} {
+               set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1]
+               if {$c > $max_w} {set max_w $c}
+       }
+       $tooltip_t conf -width $max_w -height $max_h
+
+       set req_w [winfo reqwidth  $tooltip_t]
+       set req_h [winfo reqheight $tooltip_t]
+       set pos_x [expr {[winfo pointerx .] +  5}]
+       set pos_y [expr {[winfo pointery .] + 10}]
+
+       set g "${req_w}x${req_h}"
+       if {$pos_x >= 0} {append g +}
+       append g $pos_x
+       if {$pos_y >= 0} {append g +}
+       append g $pos_y
+
+       wm geometry $tooltip_wm $g
+       raise $tooltip_wm
+}
+
+method _hide_tooltip {} {
+       if {$tooltip_wm ne {}} {
+               destroy $tooltip_wm
+               set tooltip_wm {}
+               set tooltip_commit {}
+       }
+       if {$tooltip_timer ne {}} {
+               after cancel $tooltip_timer
+               set tooltip_timer {}
+       }
+}
+
+method _resize {new_height} {
+       set diff [expr {$new_height - $old_height}]
+       if {$diff == 0} return
+
+       set my [expr {[winfo height $w.file_pane] - 25}]
+       set o [$w.file_pane sash coord 0]
+       set ox [lindex $o 0]
+       set oy [expr {[lindex $o 1] + $diff}]
+       if {$oy < 0}   {set oy 0}
+       if {$oy > $my} {set oy $my}
+       $w.file_pane sash place 0 $ox $oy
+
+       set old_height $new_height
+}
+
+}
diff --git a/git-gui/lib/branch.tcl b/git-gui/lib/branch.tcl
new file mode 100644 (file)
index 0000000..777eeb7
--- /dev/null
@@ -0,0 +1,38 @@
+# git-gui branch (create/delete) support
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc load_all_heads {} {
+       global some_heads_tracking
+
+       set rh refs/heads
+       set rh_len [expr {[string length $rh] + 1}]
+       set all_heads [list]
+       set fd [git_read for-each-ref --format=%(refname) $rh]
+       while {[gets $fd line] > 0} {
+               if {!$some_heads_tracking || ![is_tracking_branch $line]} {
+                       lappend all_heads [string range $line $rh_len end]
+               }
+       }
+       close $fd
+
+       return [lsort $all_heads]
+}
+
+proc load_all_tags {} {
+       set all_tags [list]
+       set fd [git_read for-each-ref \
+               --sort=-taggerdate \
+               --format=%(refname) \
+               refs/tags]
+       while {[gets $fd line] > 0} {
+               if {![regsub ^refs/tags/ $line {} name]} continue
+               lappend all_tags $name
+       }
+       close $fd
+       return $all_tags
+}
+
+proc radio_selector {varname value args} {
+       upvar #0 $varname var
+       set var $value
+}
diff --git a/git-gui/lib/branch_checkout.tcl b/git-gui/lib/branch_checkout.tcl
new file mode 100644 (file)
index 0000000..6603703
--- /dev/null
@@ -0,0 +1,89 @@
+# git-gui branch checkout support
+# Copyright (C) 2007 Shawn Pearce
+
+class branch_checkout {
+
+field w              ; # widget path
+field w_rev          ; # mega-widget to pick the initial revision
+
+field opt_fetch     1; # refetch tracking branch if used?
+field opt_detach    0; # force a detached head case?
+
+constructor dialog {} {
+       make_toplevel top w
+       wm title $top [append "[appname] ([reponame]): " [mc "Checkout Branch"]]
+       if {$top ne {.}} {
+               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
+       }
+
+       label $w.header -text [mc "Checkout Branch"] -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       button $w.buttons.create -text [mc Checkout] \
+               -default active \
+               -command [cb _checkout]
+       pack $w.buttons.create -side right
+       button $w.buttons.cancel -text [mc Cancel] \
+               -command [list destroy $w]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       set w_rev [::choose_rev::new $w.rev [mc Revision]]
+       $w_rev bind_listbox <Double-Button-1> [cb _checkout]
+       pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5
+
+       labelframe $w.options -text [mc Options]
+
+       checkbutton $w.options.fetch \
+               -text [mc "Fetch Tracking Branch"] \
+               -variable @opt_fetch
+       pack $w.options.fetch -anchor nw
+
+       checkbutton $w.options.detach \
+               -text [mc "Detach From Local Branch"] \
+               -variable @opt_detach
+       pack $w.options.detach -anchor nw
+
+       pack $w.options -anchor nw -fill x -pady 5 -padx 5
+
+       bind $w <Visibility> [cb _visible]
+       bind $w <Key-Escape> [list destroy $w]
+       bind $w <Key-Return> [cb _checkout]\;break
+       tkwait window $w
+}
+
+method _checkout {} {
+       set spec [$w_rev get_tracking_branch]
+       if {$spec ne {} && $opt_fetch} {
+               set new {}
+       } elseif {[catch {set new [$w_rev commit_or_die]}]} {
+               return
+       }
+
+       if {$opt_detach} {
+               set ref {}
+       } else {
+               set ref [$w_rev get_local_branch]
+       }
+
+       set co [::checkout_op::new [$w_rev get] $new $ref]
+       $co parent $w
+       $co enable_checkout 1
+       if {$spec ne {} && $opt_fetch} {
+               $co enable_fetch $spec
+       }
+
+       if {[$co run]} {
+               destroy $w
+       } else {
+               $w_rev focus_filter
+       }
+}
+
+method _visible {} {
+       grab $w
+       $w_rev focus_filter
+}
+
+}
diff --git a/git-gui/lib/branch_create.tcl b/git-gui/lib/branch_create.tcl
new file mode 100644 (file)
index 0000000..53dfb4c
--- /dev/null
@@ -0,0 +1,220 @@
+# git-gui branch create support
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+class branch_create {
+
+field w              ; # widget path
+field w_rev          ; # mega-widget to pick the initial revision
+field w_name         ; # new branch name widget
+
+field name         {}; # name of the branch the user has chosen
+field name_type  user; # type of branch name to use
+
+field opt_merge    ff; # type of merge to apply to existing branch
+field opt_checkout  1; # automatically checkout the new branch?
+field opt_fetch     1; # refetch tracking branch if used?
+field reset_ok      0; # did the user agree to reset?
+
+constructor dialog {} {
+       global repo_config
+
+       make_toplevel top w
+       wm title $top [append "[appname] ([reponame]): " [mc "Create Branch"]]
+       if {$top ne {.}} {
+               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
+       }
+
+       label $w.header -text [mc "Create New Branch"] -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       button $w.buttons.create -text [mc Create] \
+               -default active \
+               -command [cb _create]
+       pack $w.buttons.create -side right
+       button $w.buttons.cancel -text [mc Cancel] \
+               -command [list destroy $w]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       labelframe $w.desc -text [mc "Branch Name"]
+       radiobutton $w.desc.name_r \
+               -anchor w \
+               -text [mc "Name:"] \
+               -value user \
+               -variable @name_type
+       set w_name $w.desc.name_t
+       entry $w_name \
+               -borderwidth 1 \
+               -relief sunken \
+               -width 40 \
+               -textvariable @name \
+               -validate key \
+               -validatecommand [cb _validate %d %S]
+       grid $w.desc.name_r $w_name -sticky we -padx {0 5}
+
+       radiobutton $w.desc.match_r \
+               -anchor w \
+               -text [mc "Match Tracking Branch Name"] \
+               -value match \
+               -variable @name_type
+       grid $w.desc.match_r -sticky we -padx {0 5} -columnspan 2
+
+       grid columnconfigure $w.desc 1 -weight 1
+       pack $w.desc -anchor nw -fill x -pady 5 -padx 5
+
+       set w_rev [::choose_rev::new $w.rev [mc "Starting Revision"]]
+       pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5
+
+       labelframe $w.options -text [mc Options]
+
+       frame $w.options.merge
+       label $w.options.merge.l -text [mc "Update Existing Branch:"]
+       pack $w.options.merge.l -side left
+       radiobutton $w.options.merge.no \
+               -text [mc No] \
+               -value none \
+               -variable @opt_merge
+       pack $w.options.merge.no -side left
+       radiobutton $w.options.merge.ff \
+               -text [mc "Fast Forward Only"] \
+               -value ff \
+               -variable @opt_merge
+       pack $w.options.merge.ff -side left
+       radiobutton $w.options.merge.reset \
+               -text [mc Reset] \
+               -value reset \
+               -variable @opt_merge
+       pack $w.options.merge.reset -side left
+       pack $w.options.merge -anchor nw
+
+       checkbutton $w.options.fetch \
+               -text [mc "Fetch Tracking Branch"] \
+               -variable @opt_fetch
+       pack $w.options.fetch -anchor nw
+
+       checkbutton $w.options.checkout \
+               -text [mc "Checkout After Creation"] \
+               -variable @opt_checkout
+       pack $w.options.checkout -anchor nw
+       pack $w.options -anchor nw -fill x -pady 5 -padx 5
+
+       trace add variable @name_type write [cb _select]
+
+       set name $repo_config(gui.newbranchtemplate)
+       if {[is_config_true gui.matchtrackingbranch]} {
+               set name_type match
+       }
+
+       bind $w <Visibility> [cb _visible]
+       bind $w <Key-Escape> [list destroy $w]
+       bind $w <Key-Return> [cb _create]\;break
+       tkwait window $w
+}
+
+method _create {} {
+       global repo_config
+       global M1B
+
+       set spec [$w_rev get_tracking_branch]
+       switch -- $name_type {
+       user {
+               set newbranch $name
+       }
+       match {
+               if {$spec eq {}} {
+                       tk_messageBox \
+                               -icon error \
+                               -type ok \
+                               -title [wm title $w] \
+                               -parent $w \
+                               -message [mc "Please select a tracking branch."]
+                       return
+               }
+               if {![regsub ^refs/heads/ [lindex $spec 2] {} newbranch]} {
+                       tk_messageBox \
+                               -icon error \
+                               -type ok \
+                               -title [wm title $w] \
+                               -parent $w \
+                               -message [mc "Tracking branch %s is not a branch in the remote repository." [$w get]]
+                       return
+               }
+       }
+       }
+
+       if {$newbranch eq {}
+               || $newbranch eq $repo_config(gui.newbranchtemplate)} {
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message [mc "Please supply a branch name."]
+               focus $w_name
+               return
+       }
+
+       if {[catch {git check-ref-format "heads/$newbranch"}]} {
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message [mc "'%s' is not an acceptable branch name." $newbranch]
+               focus $w_name
+               return
+       }
+
+       if {$spec ne {} && $opt_fetch} {
+               set new {}
+       } elseif {[catch {set new [$w_rev commit_or_die]}]} {
+               return
+       }
+
+       set co [::checkout_op::new \
+               [$w_rev get] \
+               $new \
+               refs/heads/$newbranch]
+       $co parent $w
+       $co enable_create   1
+       $co enable_merge    $opt_merge
+       $co enable_checkout $opt_checkout
+       if {$spec ne {} && $opt_fetch} {
+               $co enable_fetch $spec
+       }
+
+       if {[$co run]} {
+               destroy $w
+       } else {
+               focus $w_name
+       }
+}
+
+method _validate {d S} {
+       if {$d == 1} {
+               if {[regexp {[~^:?*\[\0- ]} $S]} {
+                       return 0
+               }
+               if {[string length $S] > 0} {
+                       set name_type user
+               }
+       }
+       return 1
+}
+
+method _select {args} {
+       if {$name_type eq {match}} {
+               $w_rev pick_tracking_branch
+       }
+}
+
+method _visible {} {
+       grab $w
+       if {$name_type eq {user}} {
+               $w_name icursor end
+               focus $w_name
+       }
+}
+
+}
diff --git a/git-gui/lib/branch_delete.tcl b/git-gui/lib/branch_delete.tcl
new file mode 100644 (file)
index 0000000..86c4f73
--- /dev/null
@@ -0,0 +1,147 @@
+# git-gui branch delete support
+# Copyright (C) 2007 Shawn Pearce
+
+class branch_delete {
+
+field w               ; # widget path
+field w_heads         ; # listbox of local head names
+field w_check         ; # revision picker for merge test
+field w_delete        ; # delete button
+
+constructor dialog {} {
+       global current_branch
+
+       make_toplevel top w
+       wm title $top [append "[appname] ([reponame]): " [mc "Delete Branch"]]
+       if {$top ne {.}} {
+               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
+       }
+
+       label $w.header -text [mc "Delete Local Branch"] -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       set w_delete $w.buttons.delete
+       button $w_delete \
+               -text [mc Delete] \
+               -default active \
+               -state disabled \
+               -command [cb _delete]
+       pack $w_delete -side right
+       button $w.buttons.cancel \
+               -text [mc Cancel] \
+               -command [list destroy $w]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       labelframe $w.list -text [mc "Local Branches"]
+       set w_heads $w.list.l
+       listbox $w_heads \
+               -height 10 \
+               -width 70 \
+               -selectmode extended \
+               -exportselection false \
+               -yscrollcommand [list $w.list.sby set]
+       scrollbar $w.list.sby -command [list $w.list.l yview]
+       pack $w.list.sby -side right -fill y
+       pack $w.list.l -side left -fill both -expand 1
+       pack $w.list -fill both -expand 1 -pady 5 -padx 5
+
+       set w_check [choose_rev::new \
+               $w.check \
+               [mc "Delete Only If Merged Into"] \
+               ]
+       $w_check none [mc "Always (Do not perform merge test.)"]
+       pack $w.check -anchor nw -fill x -pady 5 -padx 5
+
+       foreach h [load_all_heads] {
+               if {$h ne $current_branch} {
+                       $w_heads insert end $h
+               }
+       }
+
+       bind $w_heads <<ListboxSelect>> [cb _select]
+       bind $w <Visibility> "
+               grab $w
+               focus $w
+       "
+       bind $w <Key-Escape> [list destroy $w]
+       bind $w <Key-Return> [cb _delete]\;break
+       tkwait window $w
+}
+
+method _select {} {
+       if {[$w_heads curselection] eq {}} {
+               $w_delete configure -state disabled
+       } else {
+               $w_delete configure -state normal
+       }
+}
+
+method _delete {} {
+       if {[catch {set check_cmt [$w_check commit_or_die]}]} {
+               return
+       }
+
+       set to_delete [list]
+       set not_merged [list]
+       foreach i [$w_heads curselection] {
+               set b [$w_heads get $i]
+               if {[catch {
+                       set o [git rev-parse --verify "refs/heads/$b"]
+               }]} continue
+               if {$check_cmt ne {}} {
+                       if {[catch {set m [git merge-base $o $check_cmt]}]} continue
+                       if {$o ne $m} {
+                               lappend not_merged $b
+                               continue
+                       }
+               }
+               lappend to_delete [list $b $o]
+       }
+       if {$not_merged ne {}} {
+               set msg "[mc "The following branches are not completely merged into %s:" [$w_check get]]
+
+ - [join $not_merged "\n - "]"
+               tk_messageBox \
+                       -icon info \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message $msg
+       }
+       if {$to_delete eq {}} return
+       if {$check_cmt eq {}} {
+               set msg [mc "Recovering deleted branches is difficult. \n\n Delete the selected branches?"]
+               if {[tk_messageBox \
+                       -icon warning \
+                       -type yesno \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message $msg] ne yes} {
+                       return
+               }
+       }
+
+       set failed {}
+       foreach i $to_delete {
+               set b [lindex $i 0]
+               set o [lindex $i 1]
+               if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
+                       append failed " - $b: $err\n"
+               }
+       }
+
+       if {$failed ne {}} {
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message [mc "Failed to delete branches:\n%s" $failed]
+       }
+
+       destroy $w
+}
+
+}
diff --git a/git-gui/lib/branch_rename.tcl b/git-gui/lib/branch_rename.tcl
new file mode 100644 (file)
index 0000000..1665388
--- /dev/null
@@ -0,0 +1,128 @@
+# git-gui branch rename support
+# Copyright (C) 2007 Shawn Pearce
+
+class branch_rename {
+
+field w
+field oldname
+field newname
+
+constructor dialog {} {
+       global current_branch
+
+       make_toplevel top w
+       wm title $top [append "[appname] ([reponame]): " [mc "Rename Branch"]]
+       if {$top ne {.}} {
+               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
+       }
+
+       set oldname $current_branch
+       set newname [get_config gui.newbranchtemplate]
+
+       label $w.header -text [mc "Rename Branch"] -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       button $w.buttons.rename -text [mc Rename] \
+               -default active \
+               -command [cb _rename]
+       pack $w.buttons.rename -side right
+       button $w.buttons.cancel -text [mc Cancel] \
+               -command [list destroy $w]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       frame $w.rename
+       label $w.rename.oldname_l -text [mc "Branch:"]
+       eval tk_optionMenu $w.rename.oldname_m @oldname [load_all_heads]
+
+       label $w.rename.newname_l -text [mc "New Name:"]
+       entry $w.rename.newname_t \
+               -borderwidth 1 \
+               -relief sunken \
+               -width 40 \
+               -textvariable @newname \
+               -validate key \
+               -validatecommand {
+                       if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0}
+                       return 1
+               }
+
+       grid $w.rename.oldname_l $w.rename.oldname_m -sticky w  -padx {0 5}
+       grid $w.rename.newname_l $w.rename.newname_t -sticky we -padx {0 5}
+       grid columnconfigure $w.rename 1 -weight 1
+       pack $w.rename -anchor nw -fill x -pady 5 -padx 5
+
+       bind $w <Key-Return> [cb _rename]
+       bind $w <Key-Escape> [list destroy $w]
+       bind $w <Visibility> "
+               grab $w
+               $w.rename.newname_t icursor end
+               focus $w.rename.newname_t
+       "
+       tkwait window $w
+}
+
+method _rename {} {
+       global current_branch
+
+       if {$oldname eq {}} {
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message [mc "Please select a branch to rename."]
+               focus $w.rename.oldname_m
+               return
+       }
+       if {$newname eq {}
+               || $newname eq [get_config gui.newbranchtemplate]} {
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message [mc "Please supply a branch name."]
+               focus $w.rename.newname_t
+               return
+       }
+       if {![catch {git show-ref --verify -- "refs/heads/$newname"}]} {
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message [mc "Branch '%s' already exists." $newname]
+               focus $w.rename.newname_t
+               return
+       }
+       if {[catch {git check-ref-format "heads/$newname"}]} {
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message [mc "'%s' is not an acceptable branch name." $newname]
+               focus $w.rename.newname_t
+               return
+       }
+
+       if {[catch {git branch -m $oldname $newname} err]} {
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message [strcat [mc "Failed to rename '%s'." $oldname] "\n\n$err"]
+               return
+       }
+
+       if {$current_branch eq $oldname} {
+               set current_branch $newname
+       }
+
+       destroy $w
+}
+
+}
diff --git a/git-gui/lib/browser.tcl b/git-gui/lib/browser.tcl
new file mode 100644 (file)
index 0000000..53d5a62
--- /dev/null
@@ -0,0 +1,310 @@
+# git-gui tree browser
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+class browser {
+
+image create photo ::browser::img_parent  -data {R0lGODlhEAAQAIUAAPwCBBxSHBxOHMTSzNzu3KzCtBRGHCSKFIzCjLzSxBQ2FAxGHDzCLCyeHBQ+FHSmfAwuFBxKLDSCNMzizISyjJzOnDSyLAw+FAQSDAQeDBxWJAwmDAQOBKzWrDymNAQaDAQODAwaDDyKTFSyXFTGTEy6TAQCBAQKDAwiFBQyHAwSFAwmHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAZ1QIBwSCwaj0hiQCBICpcDQsFgGAaIguhhi0gohIsrQEDYMhiNrRfgeAQC5fMCAolIDhD2hFI5WC4YRBkaBxsOE2l/RxsHHA4dHmkfRyAbIQ4iIyQlB5NFGCAACiakpSZEJyinTgAcKSesACorgU4mJ6uxR35BACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=}
+image create photo ::browser::img_rblob   -data {R0lGODlhEAAQAIUAAPwCBFxaXNze3Ly2rJSWjPz+/Ozq7GxqbJyanPT29HRydMzOzDQyNIyKjERCROTi3Pz69PTy7Pzy7PTu5Ozm3LyqlJyWlJSSjJSOhOzi1LyulPz27PTq3PTm1OzezLyqjIyKhJSKfOzaxPz29OzizLyidIyGdIyCdOTOpLymhOzavOTStMTCtMS+rMS6pMSynMSulLyedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAaQQIAQECgajcNkQMBkDgKEQFK4LFgLhkMBIVUKroWEYlEgMLxbBKLQUBwc52HgAQ4LBo049atWQyIPA3pEdFcQEhMUFYNVagQWFxgZGoxfYRsTHB0eH5UJCJAYICEinUoPIxIcHCQkIiIllQYEGCEhJicoKYwPmiQeKisrKLFKLCwtLi8wHyUlMYwM0tPUDH5BACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=}
+image create photo ::browser::img_xblob   -data {R0lGODlhEAAQAIYAAPwCBFRWVFxaXNza3OTi3Nze3Ly2tJyanPz+/Ozq7GxubNzSxMzOzMTGxHRybDQyNLy+vHRydHx6fKSipISChIyKjGxqbERCRCwuLLy6vGRiZExKTCQiJAwKDLSytLy2rJSSlHx+fDw6PKyqrBQWFPTu5Ozm3LyulLS2tCQmJAQCBPTq3Ozi1MSynCwqLAQGBOTazOzizOzezLyqjBweHNzSvOzaxKyurHRuZNzOtLymhDw+PIyCdOzWvOTOpLyidNzKtOTStLyifMTCtMS+rLyedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAfZgACCAAEChYeGg4oCAwQFjgYBBwGKggEECJkICQoIkwADCwwNDY2mDA4Lng8QDhESsLARExQVDhYXGBkWExIaGw8cHR4SCQQfFQ8eFgUgIQEiwiMSBMYfGB4atwEXDyQd0wQlJicPKAHoFyIpJCoeDgMrLC0YKBsX6i4kL+4OMDEyZijr5oLGNxUqUCioEcPGDAwjPNyI6MEDChQjcOSwsUDHgw07RIgI4KCkAgs8cvTw8eOBogAxQtXIASTISiEuBwUYMoRIixYnZggpUgTDywdIkWJIitRPIAAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
+image create photo ::browser::img_tree    -data {R0lGODlhEAAQAIYAAPwCBAQCBExKTBwWHMzKzOzq7ERCRExGTCwqLARqnAQ+ZHR2dKyqrNTOzHx2fCQiJMTi9NTu9HzC3AxmnAQ+XPTm7Dy67DymzITC3IzG5AxypHRydKymrMzOzOzu7BweHByy9AyGtFyy1IzG3NTu/ARupFRSVByazBR6rAyGvFyuzJTK3MTm9BR+tAxWhHS61MTi7Pz+/IymvCxulBRelAx2rHS63Pz6/PTy9PTu9Nza3ISitBRupFSixNTS1CxqnDQyNMzGzOTi5MTCxMTGxGxubGxqbLy2vLSutGRiZLy6vLSytKyurDQuNFxaXKSipDw6PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAfDgACCAAECg4eIAAMEBQYHCImDBgkKCwwNBQIBBw4Bhw8QERITFJYEFQUFnoIPFhcYoRkaFBscHR4Ggh8gIRciEiMQJBkltCa6JyUoKSkXKhIrLCQYuQAPLS4TEyUhKb0qLzDVAjEFMjMuNBMoNcw21QY3ODkFOjs82RM1PfDzFRU3fOggcM7Fj2pAgggRokOHDx9DhhAZUqQaISBGhjwMEvEIkiIHEgUAkgSJkiNLmFSMJChAEydPGBSBwvJQgAc0/QQCACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=}
+image create photo ::browser::img_symlink -data {R0lGODlhEAAQAIQAAPwCBCwqLLSytLy+vERGRFRWVDQ2NKSmpAQCBKyurMTGxISChJyanHR2dIyKjGxubHRydGRmZIyOjFxeXHx6fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAVbICACwWieY1CibCCsrBkMb0zchSEcNYskCtqBBzshFkOGQFk0IRqOxqPBODRHCMhCQKteRc9FI/KQWGOIyFYgkDC+gPR4snCcfRGKOIKIgSMQE31+f4OEYCZ+IQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
+image create photo ::browser::img_unknown -data {R0lGODlhEAAQAIUAAPwCBFxaXIyKjNTW1Nze3LS2tJyanER2RGS+VPz+/PTu5GxqbPz69BQ6BCxeLFSqRPT29HRydMzOzDQyNERmPKSypCRWHIyKhERCRDyGPKz2nESiLBxGHCyCHGxubPz6/PTy7Ozi1Ly2rKSipOzm3LyqlKSWhCRyFOzizLymhNTKtNzOvOzaxOTStPz27OzWvOTOpLSupLyedMS+rMS6pMSulLyqjLymfLyifAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAamQIAQECgajcOkYEBoDgoBQyAJOCCuiENCsWBIh9aGw9F4HCARiXciRDQoBUnlYRlcIgsMG5CxXAgMGhscBRAEBRd7AB0eBBoIgxUfICEiikSPgyMMIAokJZcBkBybJgomIaBJAZoMpyCmqkMBFCcVCrgKKAwpoSorKqchKCwtvasIFBIhLiYvLzDHsxQNMcMKLDAwMqEz3jQ1NTY3ONyrE+jp6hN+QQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
+
+field w
+field browser_commit
+field browser_path
+field browser_files  {}
+field browser_status [mc "Starting..."]
+field browser_stack  {}
+field browser_busy   1
+
+field ls_buf     {}; # Buffered record output from ls-tree
+
+constructor new {commit {path {}}} {
+       global cursor_ptr M1B
+       make_toplevel top w
+       wm title $top [append "[appname] ([reponame]): " [mc "File Browser"]]
+
+       set browser_commit $commit
+       set browser_path $browser_commit:$path
+
+       label $w.path \
+               -textvariable @browser_path \
+               -anchor w \
+               -justify left \
+               -borderwidth 1 \
+               -relief sunken \
+               -font font_uibold
+       pack $w.path -anchor w -side top -fill x
+
+       frame $w.list
+       set w_list $w.list.l
+       text $w_list -background white -borderwidth 0 \
+               -cursor $cursor_ptr \
+               -state disabled \
+               -wrap none \
+               -height 20 \
+               -width 70 \
+               -xscrollcommand [list $w.list.sbx set] \
+               -yscrollcommand [list $w.list.sby set]
+       rmsel_tag $w_list
+       scrollbar $w.list.sbx -orient h -command [list $w_list xview]
+       scrollbar $w.list.sby -orient v -command [list $w_list yview]
+       pack $w.list.sbx -side bottom -fill x
+       pack $w.list.sby -side right -fill y
+       pack $w_list -side left -fill both -expand 1
+       pack $w.list -side top -fill both -expand 1
+
+       label $w.status \
+               -textvariable @browser_status \
+               -anchor w \
+               -justify left \
+               -borderwidth 1 \
+               -relief sunken
+       pack $w.status -anchor w -side bottom -fill x
+
+       bind $w_list <Button-1>        "[cb _click 0 @%x,%y];break"
+       bind $w_list <Double-Button-1> "[cb _click 1 @%x,%y];break"
+       bind $w_list <$M1B-Up>         "[cb _parent]        ;break"
+       bind $w_list <$M1B-Left>       "[cb _parent]        ;break"
+       bind $w_list <Up>              "[cb _move -1]       ;break"
+       bind $w_list <Down>            "[cb _move  1]       ;break"
+       bind $w_list <$M1B-Right>      "[cb _enter]         ;break"
+       bind $w_list <Return>          "[cb _enter]         ;break"
+       bind $w_list <Prior>           "[cb _page -1]       ;break"
+       bind $w_list <Next>            "[cb _page  1]       ;break"
+       bind $w_list <Left>            break
+       bind $w_list <Right>           break
+
+       bind $w_list <Visibility> [list focus $w_list]
+       set w $w_list
+       if {$path ne {}} {
+               _ls $this $browser_commit:$path $path
+       } else {
+               _ls $this $browser_commit $path
+       }
+       return $this
+}
+
+method _move {dir} {
+       if {$browser_busy} return
+       set lno [lindex [split [$w index in_sel.first] .] 0]
+       incr lno $dir
+       if {[lindex $browser_files [expr {$lno - 1}]] ne {}} {
+               $w tag remove in_sel 0.0 end
+               $w tag add in_sel $lno.0 [expr {$lno + 1}].0
+               $w see $lno.0
+       }
+}
+
+method _page {dir} {
+       if {$browser_busy} return
+       $w yview scroll $dir pages
+       set lno [expr {int(
+                 [lindex [$w yview] 0]
+               * [llength $browser_files]
+               + 1)}]
+       if {[lindex $browser_files [expr {$lno - 1}]] ne {}} {
+               $w tag remove in_sel 0.0 end
+               $w tag add in_sel $lno.0 [expr {$lno + 1}].0
+               $w see $lno.0
+       }
+}
+
+method _parent {} {
+       if {$browser_busy} return
+       set info [lindex $browser_files 0]
+       if {[lindex $info 0] eq {parent}} {
+               set parent [lindex $browser_stack end-1]
+               set browser_stack [lrange $browser_stack 0 end-2]
+               if {$browser_stack eq {}} {
+                       regsub {:.*$} $browser_path {:} browser_path
+               } else {
+                       regsub {/[^/]+$} $browser_path {} browser_path
+               }
+               set browser_status [mc "Loading %s..." $browser_path]
+               _ls $this [lindex $parent 0] [lindex $parent 1]
+       }
+}
+
+method _enter {} {
+       if {$browser_busy} return
+       set lno [lindex [split [$w index in_sel.first] .] 0]
+       set info [lindex $browser_files [expr {$lno - 1}]]
+       if {$info ne {}} {
+               switch -- [lindex $info 0] {
+               parent {
+                       _parent $this
+               }
+               tree {
+                       set name [lindex $info 2]
+                       set escn [escape_path $name]
+                       set browser_status [mc "Loading %s..." $escn]
+                       append browser_path $escn
+                       _ls $this [lindex $info 1] $name
+               }
+               blob {
+                       set name [lindex $info 2]
+                       set p {}
+                       foreach n $browser_stack {
+                               append p [lindex $n 1]
+                       }
+                       append p $name
+                       blame::new $browser_commit $p
+               }
+               }
+       }
+}
+
+method _click {was_double_click pos} {
+       if {$browser_busy} return
+       set lno [lindex [split [$w index $pos] .] 0]
+       focus $w
+
+       if {[lindex $browser_files [expr {$lno - 1}]] ne {}} {
+               $w tag remove in_sel 0.0 end
+               $w tag add in_sel $lno.0 [expr {$lno + 1}].0
+               if {$was_double_click} {
+                       _enter $this
+               }
+       }
+}
+
+method _ls {tree_id {name {}}} {
+       set ls_buf {}
+       set browser_files {}
+       set browser_busy 1
+
+       $w conf -state normal
+       $w tag remove in_sel 0.0 end
+       $w delete 0.0 end
+       if {$browser_stack ne {}} {
+               $w image create end \
+                       -align center -padx 5 -pady 1 \
+                       -name icon0 \
+                       -image ::browser::img_parent
+               $w insert end [mc "\[Up To Parent\]"]
+               lappend browser_files parent
+       }
+       lappend browser_stack [list $tree_id $name]
+       $w conf -state disabled
+
+       set fd [git_read ls-tree -z $tree_id]
+       fconfigure $fd -blocking 0 -translation binary -encoding binary
+       fileevent $fd readable [cb _read $fd]
+}
+
+method _read {fd} {
+       append ls_buf [read $fd]
+       set pck [split $ls_buf "\0"]
+       set ls_buf [lindex $pck end]
+
+       set n [llength $browser_files]
+       $w conf -state normal
+       foreach p [lrange $pck 0 end-1] {
+               set tab [string first "\t" $p]
+               if {$tab == -1} continue
+
+               set info [split [string range $p 0 [expr {$tab - 1}]] { }]
+               set path [string range $p [expr {$tab + 1}] end]
+               set type   [lindex $info 1]
+               set object [lindex $info 2]
+
+               switch -- $type {
+               blob {
+                       scan [lindex $info 0] %o mode
+                       if {$mode == 0120000} {
+                               set image ::browser::img_symlink
+                       } elseif {($mode & 0100) != 0} {
+                               set image ::browser::img_xblob
+                       } else {
+                               set image ::browser::img_rblob
+                       }
+               }
+               tree {
+                       set image ::browser::img_tree
+                       append path /
+               }
+               default {
+                       set image ::browser::img_unknown
+               }
+               }
+
+               if {$n > 0} {$w insert end "\n"}
+               $w image create end \
+                       -align center -padx 5 -pady 1 \
+                       -name icon[incr n] \
+                       -image $image
+               $w insert end [escape_path $path]
+               lappend browser_files [list $type $object $path]
+       }
+       $w conf -state disabled
+
+       if {[eof $fd]} {
+               close $fd
+               set browser_status [mc "Ready."]
+               set browser_busy 0
+               set ls_buf {}
+               if {$n > 0} {
+                       $w tag add in_sel 1.0 2.0
+                       focus -force $w
+               }
+       }
+} ifdeleted {
+       catch {close $fd}
+}
+
+}
+
+class browser_open {
+
+field w              ; # widget path
+field w_rev          ; # mega-widget to pick the initial revision
+
+constructor dialog {} {
+       make_toplevel top w
+       wm title $top [append "[appname] ([reponame]): " [mc "Browse Branch Files"]]
+       if {$top ne {.}} {
+               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
+       }
+
+       label $w.header \
+               -text [mc "Browse Branch Files"] \
+               -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       button $w.buttons.browse -text [mc Browse] \
+               -default active \
+               -command [cb _open]
+       pack $w.buttons.browse -side right
+       button $w.buttons.cancel -text [mc Cancel] \
+               -command [list destroy $w]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       set w_rev [::choose_rev::new $w.rev [mc Revision]]
+       $w_rev bind_listbox <Double-Button-1> [cb _open]
+       pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5
+
+       bind $w <Visibility> [cb _visible]
+       bind $w <Key-Escape> [list destroy $w]
+       bind $w <Key-Return> [cb _open]\;break
+       tkwait window $w
+}
+
+method _open {} {
+       if {[catch {$w_rev commit_or_die} err]} {
+               return
+       }
+       set name [$w_rev get]
+       destroy $w
+       browser::new $name
+}
+
+method _visible {} {
+       grab $w
+       $w_rev focus_filter
+}
+
+}
diff --git a/git-gui/lib/checkout_op.tcl b/git-gui/lib/checkout_op.tcl
new file mode 100644 (file)
index 0000000..f243966
--- /dev/null
@@ -0,0 +1,588 @@
+# git-gui commit checkout support
+# Copyright (C) 2007 Shawn Pearce
+
+class checkout_op {
+
+field w        {}; # our window (if we have one)
+field w_cons   {}; # embedded console window object
+
+field new_expr   ; # expression the user saw/thinks this is
+field new_hash   ; # commit SHA-1 we are switching to
+field new_ref    ; # ref we are updating/creating
+
+field parent_w      .; # window that started us
+field merge_type none; # type of merge to apply to existing branch
+field merge_base   {}; # merge base if we have another ref involved
+field fetch_spec   {}; # refetch tracking branch if used?
+field checkout      1; # actually checkout the branch?
+field create        0; # create the branch if it doesn't exist?
+
+field reset_ok      0; # did the user agree to reset?
+field fetch_ok      0; # did the fetch succeed?
+
+field readtree_d   {}; # buffered output from read-tree
+field update_old   {}; # was the update-ref call deferred?
+field reflog_msg   {}; # log message for the update-ref call
+
+constructor new {expr hash {ref {}}} {
+       set new_expr $expr
+       set new_hash $hash
+       set new_ref  $ref
+
+       return $this
+}
+
+method parent {path} {
+       set parent_w [winfo toplevel $path]
+}
+
+method enable_merge {type} {
+       set merge_type $type
+}
+
+method enable_fetch {spec} {
+       set fetch_spec $spec
+}
+
+method enable_checkout {co} {
+       set checkout $co
+}
+
+method enable_create {co} {
+       set create $co
+}
+
+method run {} {
+       if {$fetch_spec ne {}} {
+               global M1B
+
+               # We were asked to refresh a single tracking branch
+               # before we get to work.  We should do that before we
+               # consider any ref updating.
+               #
+               set fetch_ok 0
+               set l_trck [lindex $fetch_spec 0]
+               set remote [lindex $fetch_spec 1]
+               set r_head [lindex $fetch_spec 2]
+               regsub ^refs/heads/ $r_head {} r_name
+
+               set cmd [list git fetch $remote]
+               if {$l_trck ne {}} {
+                       lappend cmd +$r_head:$l_trck
+               } else {
+                       lappend cmd $r_head
+               }
+
+               _toplevel $this {Refreshing Tracking Branch}
+               set w_cons [::console::embed \
+                       $w.console \
+                       [mc "Fetching %s from %s" $r_name $remote]]
+               pack $w.console -fill both -expand 1
+               $w_cons exec $cmd [cb _finish_fetch]
+
+               bind $w <$M1B-Key-w> break
+               bind $w <$M1B-Key-W> break
+               bind $w <Visibility> "
+                       [list grab $w]
+                       [list focus $w]
+               "
+               wm protocol $w WM_DELETE_WINDOW [cb _noop]
+               tkwait window $w
+
+               if {!$fetch_ok} {
+                       delete_this
+                       return 0
+               }
+       }
+
+       if {$new_ref ne {}} {
+               # If we have a ref we need to update it before we can
+               # proceed with a checkout (if one was enabled).
+               #
+               if {![_update_ref $this]} {
+                       delete_this
+                       return 0
+               }
+       }
+
+       if {$checkout} {
+               _checkout $this
+               return 1
+       }
+
+       delete_this
+       return 1
+}
+
+method _noop {} {}
+
+method _finish_fetch {ok} {
+       if {$ok} {
+               set l_trck [lindex $fetch_spec 0]
+               if {$l_trck eq {}} {
+                       set l_trck FETCH_HEAD
+               }
+               if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
+                       set ok 0
+                       $w_cons insert [mc "fatal: Cannot resolve %s" $l_trck]
+                       $w_cons insert $err
+               }
+       }
+
+       $w_cons done $ok
+       set w_cons {}
+       wm protocol $w WM_DELETE_WINDOW {}
+
+       if {$ok} {
+               destroy $w
+               set w {}
+       } else {
+               button $w.close -text [mc Close] -command [list destroy $w]
+               pack $w.close -side bottom -anchor e -padx 10 -pady 10
+       }
+
+       set fetch_ok $ok
+}
+
+method _update_ref {} {
+       global null_sha1 current_branch
+
+       set ref $new_ref
+       set new $new_hash
+
+       set is_current 0
+       set rh refs/heads/
+       set rn [string length $rh]
+       if {[string equal -length $rn $rh $ref]} {
+               set newbranch [string range $ref $rn end]
+               if {$current_branch eq $newbranch} {
+                       set is_current 1
+               }
+       } else {
+               set newbranch $ref
+       }
+
+       if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
+               # Assume it does not exist, and that is what the error was.
+               #
+               if {!$create} {
+                       _error $this [mc "Branch '%s' does not exist." $newbranch]
+                       return 0
+               }
+
+               set reflog_msg "branch: Created from $new_expr"
+               set cur $null_sha1
+       } elseif {$create && $merge_type eq {none}} {
+               # We were told to create it, but not do a merge.
+               # Bad.  Name shouldn't have existed.
+               #
+               _error $this [mc "Branch '%s' already exists." $newbranch]
+               return 0
+       } elseif {!$create && $merge_type eq {none}} {
+               # We aren't creating, it exists and we don't merge.
+               # We are probably just a simple branch switch.
+               # Use whatever value we just read.
+               #
+               set new      $cur
+               set new_hash $cur
+       } elseif {$new eq $cur} {
+               # No merge would be required, don't compute anything.
+               #
+       } else {
+               catch {set merge_base [git merge-base $new $cur]}
+               if {$merge_base eq $cur} {
+                       # The current branch is older.
+                       #
+                       set reflog_msg "merge $new_expr: Fast-forward"
+               } else {
+                       switch -- $merge_type {
+                       ff {
+                               if {$merge_base eq $new} {
+                                       # The current branch is actually newer.
+                                       #
+                                       set new $cur
+                                       set new_hash $cur
+                               } else {
+                                       _error $this [mc "Branch '%s' already exists.\n\nIt cannot fast-forward to %s.\nA merge is required." $newbranch $new_expr]
+                                       return 0
+                               }
+                       }
+                       reset {
+                               # The current branch will lose things.
+                               #
+                               if {[_confirm_reset $this $cur]} {
+                                       set reflog_msg "reset $new_expr"
+                               } else {
+                                       return 0
+                               }
+                       }
+                       default {
+                               _error $this [mc "Merge strategy '%s' not supported." $merge_type]
+                               return 0
+                       }
+                       }
+               }
+       }
+
+       if {$new ne $cur} {
+               if {$is_current} {
+                       # No so fast.  We should defer this in case
+                       # we cannot update the working directory.
+                       #
+                       set update_old $cur
+                       return 1
+               }
+
+               if {[catch {
+                               git update-ref -m $reflog_msg $ref $new $cur
+                       } err]} {
+                       _error $this [strcat [mc "Failed to update '%s'." $newbranch] "\n\n$err"]
+                       return 0
+               }
+       }
+
+       return 1
+}
+
+method _checkout {} {
+       if {[lock_index checkout_op]} {
+               after idle [cb _start_checkout]
+       } else {
+               _error $this [mc "Staging area (index) is already locked."]
+               delete_this
+       }
+}
+
+method _start_checkout {} {
+       global HEAD commit_type
+
+       # -- Our in memory state should match the repository.
+       #
+       repository_state curType curHEAD curMERGE_HEAD
+       if {[string match amend* $commit_type]
+               && $curType eq {normal}
+               && $curHEAD eq $HEAD} {
+       } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
+               info_popup [mc "Last scanned state does not match repository state.
+
+Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
+
+The rescan will be automatically started now.
+"]
+               unlock_index
+               rescan ui_ready
+               delete_this
+               return
+       }
+
+       if {$curHEAD eq $new_hash} {
+               _after_readtree $this
+       } elseif {[is_config_true gui.trustmtime]} {
+               _readtree $this
+       } else {
+               ui_status {Refreshing file status...}
+               set fd [git_read update-index \
+                       -q \
+                       --unmerged \
+                       --ignore-missing \
+                       --refresh \
+                       ]
+               fconfigure $fd -blocking 0 -translation binary
+               fileevent $fd readable [cb _refresh_wait $fd]
+       }
+}
+
+method _refresh_wait {fd} {
+       read $fd
+       if {[eof $fd]} {
+               close $fd
+               _readtree $this
+       }
+}
+
+method _name {} {
+       if {$new_ref eq {}} {
+               return [string range $new_hash 0 7]
+       }
+
+       set rh refs/heads/
+       set rn [string length $rh]
+       if {[string equal -length $rn $rh $new_ref]} {
+               return [string range $new_ref $rn end]
+       } else {
+               return $new_ref
+       }
+}
+
+method _readtree {} {
+       global HEAD
+
+       set readtree_d {}
+       $::main_status start \
+               [mc "Updating working directory to '%s'..." [_name $this]] \
+               {files checked out}
+
+       set fd [git_read --stderr read-tree \
+               -m \
+               -u \
+               -v \
+               --exclude-per-directory=.gitignore \
+               $HEAD \
+               $new_hash \
+               ]
+       fconfigure $fd -blocking 0 -translation binary
+       fileevent $fd readable [cb _readtree_wait $fd]
+}
+
+method _readtree_wait {fd} {
+       global current_branch
+
+       set buf [read $fd]
+       $::main_status update_meter $buf
+       append readtree_d $buf
+
+       fconfigure $fd -blocking 1
+       if {![eof $fd]} {
+               fconfigure $fd -blocking 0
+               return
+       }
+
+       if {[catch {close $fd}]} {
+               set err $readtree_d
+               regsub {^fatal: } $err {} err
+               $::main_status stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]]
+               warn_popup [strcat [mc "File level merge required."] "
+
+$err
+
+" [mc "Staying on branch '%s'." $current_branch]]
+               unlock_index
+               delete_this
+               return
+       }
+
+       $::main_status stop
+       _after_readtree $this
+}
+
+method _after_readtree {} {
+       global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
+       global current_branch is_detached
+       global ui_comm
+
+       set name [_name $this]
+       set log "checkout: moving"
+       if {!$is_detached} {
+               append log " from $current_branch"
+       }
+
+       # -- Move/create HEAD as a symbolic ref.  Core git does not
+       #    even check for failure here, it Just Works(tm).  If it
+       #    doesn't we are in some really ugly state that is difficult
+       #    to recover from within git-gui.
+       #
+       set rh refs/heads/
+       set rn [string length $rh]
+       if {[string equal -length $rn $rh $new_ref]} {
+               set new_branch [string range $new_ref $rn end]
+               if {$is_detached || $current_branch ne $new_branch} {
+                       append log " to $new_branch"
+                       if {[catch {
+                                       git symbolic-ref -m $log HEAD $new_ref
+                               } err]} {
+                               _fatal $this $err
+                       }
+                       set current_branch $new_branch
+                       set is_detached 0
+               }
+       } else {
+               if {!$is_detached || $new_hash ne $HEAD} {
+                       append log " to $new_expr"
+                       if {[catch {
+                                       _detach_HEAD $log $new_hash
+                               } err]} {
+                               _fatal $this $err
+                       }
+               }
+               set current_branch HEAD
+               set is_detached 1
+       }
+
+       # -- We had to defer updating the branch itself until we
+       #    knew the working directory would update.  So now we
+       #    need to finish that work.  If it fails we're in big
+       #    trouble.
+       #
+       if {$update_old ne {}} {
+               if {[catch {
+                               git update-ref \
+                                       -m $reflog_msg \
+                                       $new_ref \
+                                       $new_hash \
+                                       $update_old
+                       } err]} {
+                       _fatal $this $err
+               }
+       }
+
+       if {$is_detached} {
+               info_popup [mc "You are no longer on a local branch.
+
+If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."]
+       }
+
+       # -- Update our repository state.  If we were previously in
+       #    amend mode we need to toss the current buffer and do a
+       #    full rescan to update our file lists.  If we weren't in
+       #    amend mode our file lists are accurate and we can avoid
+       #    the rescan.
+       #
+       unlock_index
+       set selected_commit_type new
+       if {[string match amend* $commit_type]} {
+               $ui_comm delete 0.0 end
+               $ui_comm edit reset
+               $ui_comm edit modified false
+               rescan [list ui_status [mc "Checked out '%s'." $name]]
+       } else {
+               repository_state commit_type HEAD MERGE_HEAD
+               set PARENT $HEAD
+               ui_status "Checked out '$name'."
+       }
+       delete_this
+}
+
+git-version proc _detach_HEAD {log new} {
+       >= 1.5.3 {
+               git update-ref --no-deref -m $log HEAD $new
+       }
+       default {
+               set p [gitdir HEAD]
+               file delete $p
+               set fd [open $p w]
+               fconfigure $fd -translation lf -encoding utf-8
+               puts $fd $new
+               close $fd
+       }
+}
+
+method _confirm_reset {cur} {
+       set reset_ok 0
+       set name [_name $this]
+       set gitk [list do_gitk [list $cur ^$new_hash]]
+
+       _toplevel $this {Confirm Branch Reset}
+       pack [label $w.msg1 \
+               -anchor w \
+               -justify left \
+               -text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\
+               ] -anchor w
+
+       set list $w.list.l
+       frame $w.list
+       text $list \
+               -font font_diff \
+               -width 80 \
+               -height 10 \
+               -wrap none \
+               -xscrollcommand [list $w.list.sbx set] \
+               -yscrollcommand [list $w.list.sby set]
+       scrollbar $w.list.sbx -orient h -command [list $list xview]
+       scrollbar $w.list.sby -orient v -command [list $list yview]
+       pack $w.list.sbx -fill x -side bottom
+       pack $w.list.sby -fill y -side right
+       pack $list -fill both -expand 1
+       pack $w.list -fill both -expand 1 -padx 5 -pady 5
+
+       pack [label $w.msg2 \
+               -anchor w \
+               -justify left \
+               -text [mc "Recovering lost commits may not be easy."] \
+               ]
+       pack [label $w.msg3 \
+               -anchor w \
+               -justify left \
+               -text [mc "Reset '%s'?" $name] \
+               ]
+
+       frame $w.buttons
+       button $w.buttons.visualize \
+               -text [mc Visualize] \
+               -command $gitk
+       pack $w.buttons.visualize -side left
+       button $w.buttons.reset \
+               -text [mc Reset] \
+               -command "
+                       set @reset_ok 1
+                       destroy $w
+               "
+       pack $w.buttons.reset -side right
+       button $w.buttons.cancel \
+               -default active \
+               -text [mc Cancel] \
+               -command [list destroy $w]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
+       while {[gets $fd line] > 0} {
+               set abbr [string range $line 0 7]
+               set subj [string range $line 41 end]
+               $list insert end "$abbr  $subj\n"
+       }
+       close $fd
+       $list configure -state disabled
+
+       bind $w    <Key-v> $gitk
+       bind $w <Visibility> "
+               grab $w
+               focus $w.buttons.cancel
+       "
+       bind $w <Key-Return> [list destroy $w]
+       bind $w <Key-Escape> [list destroy $w]
+       tkwait window $w
+       return $reset_ok
+}
+
+method _error {msg} {
+       if {[winfo ismapped $parent_w]} {
+               set p $parent_w
+       } else {
+               set p .
+       }
+
+       tk_messageBox \
+               -icon error \
+               -type ok \
+               -title [wm title $p] \
+               -parent $p \
+               -message $msg
+}
+
+method _toplevel {title} {
+       regsub -all {::} $this {__} w
+       set w .$w
+
+       if {[winfo ismapped $parent_w]} {
+               set p $parent_w
+       } else {
+               set p .
+       }
+
+       toplevel $w
+       wm title $w $title
+       wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
+}
+
+method _fatal {err} {
+       error_popup [strcat [mc "Failed to set current branch.
+
+This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
+
+This should not have occurred.  %s will now close and give up." [appname]] "
+
+$err"]
+       exit 1
+}
+
+}
diff --git a/git-gui/lib/choose_font.tcl b/git-gui/lib/choose_font.tcl
new file mode 100644 (file)
index 0000000..0c4051b
--- /dev/null
@@ -0,0 +1,166 @@
+# git-gui font chooser
+# Copyright (C) 2007 Shawn Pearce
+
+class choose_font {
+
+field w
+field w_family    ; # UI widget of all known family names
+field w_example   ; # Example to showcase the chosen font
+
+field f_family    ; # Currently chosen family name
+field f_size      ; # Currently chosen point size
+
+field v_family    ; # Name of global variable for family
+field v_size      ; # Name of global variable for size
+
+variable all_families [list]  ; # All fonts known to Tk
+
+constructor pick {path title a_family a_size} {
+       variable all_families
+
+       set v_family $a_family
+       set v_size $a_size
+
+       upvar #0 $v_family pv_family
+       upvar #0 $v_size pv_size
+
+       set f_family $pv_family
+       set f_size $pv_size
+
+       make_toplevel top w
+       wm title $top "[appname] ([reponame]): $title"
+       wm geometry $top "+[winfo rootx $path]+[winfo rooty $path]"
+
+       label $w.header -text $title -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       button $w.buttons.select \
+               -text [mc Select] \
+               -default active \
+               -command [cb _select]
+       button $w.buttons.cancel \
+               -text [mc Cancel] \
+               -command [list destroy $w]
+       pack $w.buttons.select -side right
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       frame $w.inner
+
+       frame $w.inner.family
+       label $w.inner.family.l \
+               -text [mc "Font Family"] \
+               -anchor w
+       set w_family $w.inner.family.v
+       text $w_family \
+               -background white \
+               -borderwidth 1 \
+               -relief sunken \
+               -cursor $::cursor_ptr \
+               -wrap none \
+               -width 30 \
+               -height 10 \
+               -yscrollcommand [list $w.inner.family.sby set]
+       rmsel_tag $w_family
+       scrollbar $w.inner.family.sby -command [list $w_family yview]
+       pack $w.inner.family.l -side top -fill x
+       pack $w.inner.family.sby -side right -fill y
+       pack $w_family -fill both -expand 1
+
+       frame $w.inner.size
+       label $w.inner.size.l \
+               -text [mc "Font Size"] \
+               -anchor w
+       spinbox $w.inner.size.v \
+               -textvariable @f_size \
+               -from 2 -to 80 -increment 1 \
+               -width 3
+       bind $w.inner.size.v <FocusIn> {%W selection range 0 end}
+       pack $w.inner.size.l -fill x -side top
+       pack $w.inner.size.v -fill x -padx 2
+
+       grid configure $w.inner.family $w.inner.size -sticky nsew
+       grid rowconfigure $w.inner 0 -weight 1
+       grid columnconfigure $w.inner 0 -weight 1
+       pack $w.inner -fill both -expand 1 -padx 5 -pady 5
+
+       frame $w.example
+       label $w.example.l \
+               -text [mc "Font Example"] \
+               -anchor w
+       set w_example $w.example.t
+       text $w_example \
+               -background white \
+               -borderwidth 1 \
+               -relief sunken \
+               -height 3 \
+               -width 40
+       rmsel_tag $w_example
+       $w_example tag conf example -justify center
+       $w_example insert end [mc "This is example text.\nIf you like this text, it can be your font."] example
+       $w_example conf -state disabled
+       pack $w.example.l -fill x
+       pack $w_example -fill x
+       pack $w.example -fill x -padx 5
+
+       if {$all_families eq {}} {
+               set all_families [lsort [font families]]
+       }
+
+       $w_family tag conf pick
+       $w_family tag bind pick <Button-1> [cb _pick_family %x %y]\;break
+       foreach f $all_families {
+               set sel [list pick]
+               if {$f eq $f_family} {
+                       lappend sel in_sel
+               }
+               $w_family insert end "$f\n" $sel
+       }
+       $w_family conf -state disabled
+       _update $this
+
+       trace add variable @f_size write [cb _update]
+       bind $w <Key-Escape> [list destroy $w]
+       bind $w <Key-Return> [cb _select]\;break
+       bind $w <Visibility> "
+               grab $w
+               focus $w
+       "
+       tkwait window $w
+}
+
+method _select {} {
+       upvar #0 $v_family pv_family
+       upvar #0 $v_size pv_size
+
+       set pv_family $f_family
+       set pv_size $f_size
+
+       destroy $w
+}
+
+method _pick_family {x y} {
+       variable all_families
+
+       set i [lindex [split [$w_family index @$x,$y] .] 0]
+       set n [lindex $all_families [expr {$i - 1}]]
+       if {$n ne {}} {
+               $w_family tag remove in_sel 0.0 end
+               $w_family tag add in_sel $i.0 [expr {$i + 1}].0
+               set f_family $n
+               _update $this
+       }
+}
+
+method _update {args} {
+       variable all_families
+
+       set i [lsearch -exact $all_families $f_family]
+       if {$i < 0} return
+
+       $w_example tag conf example -font [list $f_family $f_size]
+       $w_family see [expr {$i + 1}].0
+}
+
+}
diff --git a/git-gui/lib/choose_repository.tcl b/git-gui/lib/choose_repository.tcl
new file mode 100644 (file)
index 0000000..2bac50e
--- /dev/null
@@ -0,0 +1,1044 @@
+# git-gui Git repository chooser
+# Copyright (C) 2007 Shawn Pearce
+
+class choose_repository {
+
+field top
+field w
+field w_body      ; # Widget holding the center content
+field w_next      ; # Next button
+field w_quit      ; # Quit button
+field o_cons      ; # Console object (if active)
+field w_types     ; # List of type buttons in clone
+field w_recentlist ; # Listbox containing recent repositories
+
+field done              0 ; # Finished picking the repository?
+field local_path       {} ; # Where this repository is locally
+field origin_url       {} ; # Where we are cloning from
+field origin_name  origin ; # What we shall call 'origin'
+field clone_type hardlink ; # Type of clone to construct
+field readtree_err        ; # Error output from read-tree (if any)
+field sorted_recent       ; # recent repositories (sorted)
+
+constructor pick {} {
+       global M1T M1B
+
+       make_toplevel top w
+       wm title $top [mc "Git Gui"]
+
+       if {$top eq {.}} {
+               menu $w.mbar -tearoff 0
+               $top configure -menu $w.mbar
+
+               set m_repo $w.mbar.repository
+               $w.mbar add cascade \
+                       -label [mc Repository] \
+                       -menu $m_repo
+               menu $m_repo
+
+               if {[is_MacOSX]} {
+                       $w.mbar add cascade -label [mc Apple] -menu .mbar.apple
+                       menu $w.mbar.apple
+                       $w.mbar.apple add command \
+                               -label [mc "About %s" [appname]] \
+                               -command do_about
+               } else {
+                       $w.mbar add cascade -label [mc Help] -menu $w.mbar.help
+                       menu $w.mbar.help
+                       $w.mbar.help add command \
+                               -label [mc "About %s" [appname]] \
+                               -command do_about
+               }
+
+               wm protocol $top WM_DELETE_WINDOW exit
+               bind $top <$M1B-q> exit
+               bind $top <$M1B-Q> exit
+               bind $top <Key-Escape> exit
+       } else {
+               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
+               bind $top <Key-Escape> [list destroy $top]
+               set m_repo {}
+       }
+
+       pack [git_logo $w.git_logo] -side left -fill y -padx 10 -pady 10
+
+       set w_body $w.body
+       set opts $w_body.options
+       frame $w_body
+       text $opts \
+               -cursor $::cursor_ptr \
+               -relief flat \
+               -background [$w_body cget -background] \
+               -wrap none \
+               -spacing1 5 \
+               -width 50 \
+               -height 3
+       pack $opts -anchor w -fill x
+
+       $opts tag conf link_new -foreground blue -underline 1
+       $opts tag bind link_new <1> [cb _next new]
+       $opts insert end [mc "Create New Repository"] link_new
+       $opts insert end "\n"
+       if {$m_repo ne {}} {
+               $m_repo add command \
+                       -command [cb _next new] \
+                       -accelerator $M1T-N \
+                       -label [mc "New..."]
+               bind $top <$M1B-n> [cb _next new]
+               bind $top <$M1B-N> [cb _next new]
+       }
+
+       $opts tag conf link_clone -foreground blue -underline 1
+       $opts tag bind link_clone <1> [cb _next clone]
+       $opts insert end [mc "Clone Existing Repository"] link_clone
+       $opts insert end "\n"
+       if {$m_repo ne {}} {
+               $m_repo add command \
+                       -command [cb _next clone] \
+                       -accelerator $M1T-C \
+                       -label [mc "Clone..."]
+               bind $top <$M1B-c> [cb _next clone]
+               bind $top <$M1B-C> [cb _next clone]
+       }
+
+       $opts tag conf link_open -foreground blue -underline 1
+       $opts tag bind link_open <1> [cb _next open]
+       $opts insert end [mc "Open Existing Repository"] link_open
+       $opts insert end "\n"
+       if {$m_repo ne {}} {
+               $m_repo add command \
+                       -command [cb _next open] \
+                       -accelerator $M1T-O \
+                       -label [mc "Open..."]
+               bind $top <$M1B-o> [cb _next open]
+               bind $top <$M1B-O> [cb _next open]
+       }
+
+       $opts conf -state disabled
+
+       set sorted_recent [_get_recentrepos]
+       if {[llength $sorted_recent] > 0} {
+               if {$m_repo ne {}} {
+                       $m_repo add separator
+                       $m_repo add command \
+                               -state disabled \
+                               -label [mc "Recent Repositories"]
+               }
+
+               label $w_body.space
+               label $w_body.recentlabel \
+                       -anchor w \
+                       -text [mc "Open Recent Repository:"]
+               set w_recentlist $w_body.recentlist
+               text $w_recentlist \
+                       -cursor $::cursor_ptr \
+                       -relief flat \
+                       -background [$w_body.recentlabel cget -background] \
+                       -wrap none \
+                       -width 50 \
+                       -height 10
+               $w_recentlist tag conf link \
+                       -foreground blue \
+                       -underline 1
+               set home $::env(HOME)
+               if {[is_Cygwin]} {
+                       set home [exec cygpath --windows --absolute $home]
+               }
+               set home "[file normalize $home]/"
+               set hlen [string length $home]
+               foreach p $sorted_recent {
+                       set path $p
+                       if {[string equal -length $hlen $home $p]} {
+                               set p "~/[string range $p $hlen end]"
+                       }
+                       regsub -all "\n" $p "\\n" p
+                       $w_recentlist insert end $p link
+                       $w_recentlist insert end "\n"
+
+                       if {$m_repo ne {}} {
+                               $m_repo add command \
+                                       -command [cb _open_recent_path $path] \
+                                       -label "    $p"
+                       }
+               }
+               $w_recentlist conf -state disabled
+               $w_recentlist tag bind link <1> [cb _open_recent %x,%y]
+               pack $w_body.space -anchor w -fill x
+               pack $w_body.recentlabel -anchor w -fill x
+               pack $w_recentlist -anchor w -fill x
+       }
+       pack $w_body -fill x -padx 10 -pady 10
+
+       frame $w.buttons
+       set w_next $w.buttons.next
+       set w_quit $w.buttons.quit
+       button $w_quit \
+               -text [mc "Quit"] \
+               -command exit
+       pack $w_quit -side right -padx 5
+       pack $w.buttons -side bottom -fill x -padx 10 -pady 10
+
+       if {$m_repo ne {}} {
+               $m_repo add separator
+               $m_repo add command \
+                       -label [mc Quit] \
+                       -command exit \
+                       -accelerator $M1T-Q
+       }
+
+       bind $top <Return> [cb _invoke_next]
+       bind $top <Visibility> "
+               [cb _center]
+               grab $top
+               focus $top
+               bind $top <Visibility> {}
+       "
+       wm deiconify $top
+       tkwait variable @done
+
+       if {$top eq {.}} {
+               eval destroy [winfo children $top]
+       }
+}
+
+proc _home {} {
+       if {[catch {set h $::env(HOME)}]
+               || ![file isdirectory $h]} {
+               set h .
+       }
+       return $h
+}
+
+method _center {} {
+       set nx [winfo reqwidth $top]
+       set ny [winfo reqheight $top]
+       set rx [expr {([winfo screenwidth  $top] - $nx) / 3}]
+       set ry [expr {([winfo screenheight $top] - $ny) / 3}]
+       wm geometry $top [format {+%d+%d} $rx $ry]
+}
+
+method _invoke_next {} {
+       if {[winfo exists $w_next]} {
+               uplevel #0 [$w_next cget -command]
+       }
+}
+
+proc _get_recentrepos {} {
+       set recent [list]
+       foreach p [get_config gui.recentrepo] {
+               if {[_is_git [file join $p .git]]} {
+                       lappend recent $p
+               }
+       }
+       return [lsort $recent]
+}
+
+proc _unset_recentrepo {p} {
+       regsub -all -- {([()\[\]{}\.^$+*?\\])} $p {\\\1} p
+       git config --global --unset gui.recentrepo "^$p\$"
+}
+
+proc _append_recentrepos {path} {
+       set path [file normalize $path]
+       set recent [get_config gui.recentrepo]
+
+       if {[lindex $recent end] eq $path} {
+               return
+       }
+
+       set i [lsearch $recent $path]
+       if {$i >= 0} {
+               _unset_recentrepo $path
+               set recent [lreplace $recent $i $i]
+       }
+
+       lappend recent $path
+       git config --global --add gui.recentrepo $path
+
+       while {[llength $recent] > 10} {
+               _unset_recentrepo [lindex $recent 0]
+               set recent [lrange $recent 1 end]
+       }
+}
+
+method _open_recent {xy} {
+       set id [lindex [split [$w_recentlist index @$xy] .] 0]
+       set local_path [lindex $sorted_recent [expr {$id - 1}]]
+       _do_open2 $this
+}
+
+method _open_recent_path {p} {
+       set local_path $p
+       _do_open2 $this
+}
+
+method _next {action} {
+       destroy $w_body
+       if {![winfo exists $w_next]} {
+               button $w_next -default active
+               pack $w_next -side right -padx 5 -before $w_quit
+       }
+       _do_$action $this
+}
+
+method _write_local_path {args} {
+       if {$local_path eq {}} {
+               $w_next conf -state disabled
+       } else {
+               $w_next conf -state normal
+       }
+}
+
+method _git_init {} {
+       if {[file exists $local_path]} {
+               error_popup [mc "Location %s already exists." $local_path]
+               return 0
+       }
+
+       if {[catch {file mkdir $local_path} err]} {
+               error_popup [strcat \
+                       [mc "Failed to create repository %s:" $local_path] \
+                       "\n\n$err"]
+               return 0
+       }
+
+       if {[catch {cd $local_path} err]} {
+               error_popup [strcat \
+                       [mc "Failed to create repository %s:" $local_path] \
+                       "\n\n$err"]
+               return 0
+       }
+
+       if {[catch {git init} err]} {
+               error_popup [strcat \
+                       [mc "Failed to create repository %s:" $local_path] \
+                       "\n\n$err"]
+               return 0
+       }
+
+       _append_recentrepos [pwd]
+       set ::_gitdir .git
+       set ::_prefix {}
+       return 1
+}
+
+proc _is_git {path} {
+       if {[file exists [file join $path HEAD]]
+        && [file exists [file join $path objects]]
+        && [file exists [file join $path config]]} {
+               return 1
+       }
+       if {[is_Cygwin]} {
+               if {[file exists [file join $path HEAD]]
+                && [file exists [file join $path objects.lnk]]
+                && [file exists [file join $path config.lnk]]} {
+                       return 1
+               }
+       }
+       return 0
+}
+
+proc _objdir {path} {
+       set objdir [file join $path .git objects]
+       if {[file isdirectory $objdir]} {
+               return $objdir
+       }
+
+       set objdir [file join $path objects]
+       if {[file isdirectory $objdir]} {
+               return $objdir
+       }
+
+       if {[is_Cygwin]} {
+               set objdir [file join $path .git objects.lnk]
+               if {[file isfile $objdir]} {
+                       return [win32_read_lnk $objdir]
+               }
+
+               set objdir [file join $path objects.lnk]
+               if {[file isfile $objdir]} {
+                       return [win32_read_lnk $objdir]
+               }
+       }
+
+       return {}
+}
+
+######################################################################
+##
+## Create New Repository
+
+method _do_new {} {
+       $w_next conf \
+               -state disabled \
+               -command [cb _do_new2] \
+               -text [mc "Create"]
+
+       frame $w_body
+       label $w_body.h \
+               -font font_uibold \
+               -text [mc "Create New Repository"]
+       pack $w_body.h -side top -fill x -pady 10
+       pack $w_body -fill x -padx 10
+
+       frame $w_body.where
+       label $w_body.where.l -text [mc "Directory:"]
+       entry $w_body.where.t \
+               -textvariable @local_path \
+               -font font_diff \
+               -width 50
+       button $w_body.where.b \
+               -text [mc "Browse"] \
+               -command [cb _new_local_path]
+
+       pack $w_body.where.b -side right
+       pack $w_body.where.l -side left
+       pack $w_body.where.t -fill x
+       pack $w_body.where -fill x
+
+       trace add variable @local_path write [cb _write_local_path]
+       bind $w_body.h <Destroy> [list trace remove variable @local_path write [cb _write_local_path]]
+       update
+       focus $w_body.where.t
+}
+
+method _new_local_path {} {
+       if {$local_path ne {}} {
+               set p [file dirname $local_path]
+       } else {
+               set p [_home]
+       }
+
+       set p [tk_chooseDirectory \
+               -initialdir $p \
+               -parent $top \
+               -title [mc "Git Repository"] \
+               -mustexist false]
+       if {$p eq {}} return
+
+       set p [file normalize $p]
+       if {[file isdirectory $p]} {
+               foreach i [glob \
+                       -directory $p \
+                       -tails \
+                       -nocomplain \
+                       * .*] {
+                       switch -- $i {
+                        . continue
+                       .. continue
+                       default {
+                               error_popup [mc "Directory %s already exists." $p]
+                               return
+                       }
+                       }
+               }
+               if {[catch {file delete $p} err]} {
+                       error_popup [strcat \
+                               [mc "Directory %s already exists." $p] \
+                               "\n\n$err"]
+                       return
+               }
+       } elseif {[file exists $p]} {
+               error_popup [mc "File %s already exists." $p]
+               return
+       }
+       set local_path $p
+}
+
+method _do_new2 {} {
+       if {![_git_init $this]} {
+               return
+       }
+       set done 1
+}
+
+######################################################################
+##
+## Clone Existing Repository
+
+method _do_clone {} {
+       $w_next conf \
+               -state disabled \
+               -command [cb _do_clone2] \
+               -text [mc "Clone"]
+
+       frame $w_body
+       label $w_body.h \
+               -font font_uibold \
+               -text [mc "Clone Existing Repository"]
+       pack $w_body.h -side top -fill x -pady 10
+       pack $w_body -fill x -padx 10
+
+       set args $w_body.args
+       frame $w_body.args
+       pack $args -fill both
+
+       label $args.origin_l -text [mc "URL:"]
+       entry $args.origin_t \
+               -textvariable @origin_url \
+               -font font_diff \
+               -width 50
+       button $args.origin_b \
+               -text [mc "Browse"] \
+               -command [cb _open_origin]
+       grid $args.origin_l $args.origin_t $args.origin_b -sticky ew
+
+       label $args.where_l -text [mc "Directory:"]
+       entry $args.where_t \
+               -textvariable @local_path \
+               -font font_diff \
+               -width 50
+       button $args.where_b \
+               -text [mc "Browse"] \
+               -command [cb _new_local_path]
+       grid $args.where_l $args.where_t $args.where_b -sticky ew
+
+       label $args.type_l -text [mc "Clone Type:"]
+       frame $args.type_f
+       set w_types [list]
+       lappend w_types [radiobutton $args.type_f.hardlink \
+               -state disabled \
+               -anchor w \
+               -text [mc "Standard (Fast, Semi-Redundant, Hardlinks)"] \
+               -variable @clone_type \
+               -value hardlink]
+       lappend w_types [radiobutton $args.type_f.full \
+               -state disabled \
+               -anchor w \
+               -text [mc "Full Copy (Slower, Redundant Backup)"] \
+               -variable @clone_type \
+               -value full]
+       lappend w_types [radiobutton $args.type_f.shared \
+               -state disabled \
+               -anchor w \
+               -text [mc "Shared (Fastest, Not Recommended, No Backup)"] \
+               -variable @clone_type \
+               -value shared]
+       foreach r $w_types {
+               pack $r -anchor w
+       }
+       grid $args.type_l $args.type_f -sticky new
+
+       grid columnconfigure $args 1 -weight 1
+
+       trace add variable @local_path write [cb _update_clone]
+       trace add variable @origin_url write [cb _update_clone]
+       bind $w_body.h <Destroy> "
+               [list trace remove variable @local_path write [cb _update_clone]]
+               [list trace remove variable @origin_url write [cb _update_clone]]
+       "
+       update
+       focus $args.origin_t
+}
+
+method _open_origin {} {
+       if {$origin_url ne {} && [file isdirectory $origin_url]} {
+               set p $origin_url
+       } else {
+               set p [_home]
+       }
+
+       set p [tk_chooseDirectory \
+               -initialdir $p \
+               -parent $top \
+               -title [mc "Git Repository"] \
+               -mustexist true]
+       if {$p eq {}} return
+
+       set p [file normalize $p]
+       if {![_is_git [file join $p .git]] && ![_is_git $p]} {
+               error_popup [mc "Not a Git repository: %s" [file tail $p]]
+               return
+       }
+       set origin_url $p
+}
+
+method _update_clone {args} {
+       if {$local_path ne {} && $origin_url ne {}} {
+               $w_next conf -state normal
+       } else {
+               $w_next conf -state disabled
+       }
+
+       if {$origin_url ne {} &&
+               (  [_is_git [file join $origin_url .git]]
+               || [_is_git $origin_url])} {
+               set e normal
+               if {[[lindex $w_types 0] cget -state] eq {disabled}} {
+                       set clone_type hardlink
+               }
+       } else {
+               set e disabled
+               set clone_type full
+       }
+
+       foreach r $w_types {
+               $r conf -state $e
+       }
+}
+
+method _do_clone2 {} {
+       if {[file isdirectory $origin_url]} {
+               set origin_url [file normalize $origin_url]
+       }
+
+       if {$clone_type eq {hardlink} && ![file isdirectory $origin_url]} {
+               error_popup [mc "Standard only available for local repository."]
+               return
+       }
+       if {$clone_type eq {shared} && ![file isdirectory $origin_url]} {
+               error_popup [mc "Shared only available for local repository."]
+               return
+       }
+
+       if {$clone_type eq {hardlink} || $clone_type eq {shared}} {
+               set objdir [_objdir $origin_url]
+               if {$objdir eq {}} {
+                       error_popup [mc "Not a Git repository: %s" [file tail $origin_url]]
+                       return
+               }
+       }
+
+       set giturl $origin_url
+       if {[is_Cygwin] && [file isdirectory $giturl]} {
+               set giturl [exec cygpath --unix --absolute $giturl]
+               if {$clone_type eq {shared}} {
+                       set objdir [exec cygpath --unix --absolute $objdir]
+               }
+       }
+
+       if {![_git_init $this]} return
+       set local_path [pwd]
+
+       if {[catch {
+                       git config remote.$origin_name.url $giturl
+                       git config remote.$origin_name.fetch +refs/heads/*:refs/remotes/$origin_name/*
+               } err]} {
+               error_popup [strcat [mc "Failed to configure origin"] "\n\n$err"]
+               return
+       }
+
+       destroy $w_body $w_next
+
+       switch -exact -- $clone_type {
+       hardlink {
+               set o_cons [status_bar::two_line $w_body]
+               pack $w_body -fill x -padx 10 -pady 10
+
+               $o_cons start \
+                       [mc "Counting objects"] \
+                       [mc "buckets"]
+               update
+
+               if {[file exists [file join $objdir info alternates]]} {
+                       set pwd [pwd]
+                       if {[catch {
+                               file mkdir [gitdir objects info]
+                               set f_in [open [file join $objdir info alternates] r]
+                               set f_cp [open [gitdir objects info alternates] w]
+                               fconfigure $f_in -translation binary -encoding binary
+                               fconfigure $f_cp -translation binary -encoding binary
+                               cd $objdir
+                               while {[gets $f_in line] >= 0} {
+                                       if {[is_Cygwin]} {
+                                               puts $f_cp [exec cygpath --unix --absolute $line]
+                                       } else {
+                                               puts $f_cp [file normalize $line]
+                                       }
+                               }
+                               close $f_in
+                               close $f_cp
+                               cd $pwd
+                       } err]} {
+                               catch {cd $pwd}
+                               _clone_failed $this [mc "Unable to copy objects/info/alternates: %s" $err]
+                               return
+                       }
+               }
+
+               set tolink  [list]
+               set buckets [glob \
+                       -tails \
+                       -nocomplain \
+                       -directory [file join $objdir] ??]
+               set bcnt [expr {[llength $buckets] + 2}]
+               set bcur 1
+               $o_cons update $bcur $bcnt
+               update
+
+               file mkdir [file join .git objects pack]
+               foreach i [glob -tails -nocomplain \
+                       -directory [file join $objdir pack] *] {
+                       lappend tolink [file join pack $i]
+               }
+               $o_cons update [incr bcur] $bcnt
+               update
+
+               foreach i $buckets {
+                       file mkdir [file join .git objects $i]
+                       foreach j [glob -tails -nocomplain \
+                               -directory [file join $objdir $i] *] {
+                               lappend tolink [file join $i $j]
+                       }
+                       $o_cons update [incr bcur] $bcnt
+                       update
+               }
+               $o_cons stop
+
+               if {$tolink eq {}} {
+                       info_popup [strcat \
+                               [mc "Nothing to clone from %s." $origin_url] \
+                               "\n" \
+                               [mc "The 'master' branch has not been initialized."] \
+                               ]
+                       destroy $w_body
+                       set done 1
+                       return
+               }
+
+               set i [lindex $tolink 0]
+               if {[catch {
+                               file link -hard \
+                                       [file join .git objects $i] \
+                                       [file join $objdir $i]
+                       } err]} {
+                       info_popup [mc "Hardlinks are unavailable.  Falling back to copying."]
+                       set i [_copy_files $this $objdir $tolink]
+               } else {
+                       set i [_link_files $this $objdir [lrange $tolink 1 end]]
+               }
+               if {!$i} return
+
+               destroy $w_body
+       }
+       full {
+               set o_cons [console::embed \
+                       $w_body \
+                       [mc "Cloning from %s" $origin_url]]
+               pack $w_body -fill both -expand 1 -padx 10
+               $o_cons exec \
+                       [list git fetch --no-tags -k $origin_name] \
+                       [cb _do_clone_tags]
+       }
+       shared {
+               set fd [open [gitdir objects info alternates] w]
+               fconfigure $fd -translation binary
+               puts $fd $objdir
+               close $fd
+       }
+       }
+
+       if {$clone_type eq {hardlink} || $clone_type eq {shared}} {
+               if {![_clone_refs $this]} return
+               set pwd [pwd]
+               if {[catch {
+                               cd $origin_url
+                               set HEAD [git rev-parse --verify HEAD^0]
+                       } err]} {
+                       _clone_failed $this [mc "Not a Git repository: %s" [file tail $origin_url]]
+                       return 0
+               }
+               cd $pwd
+               _do_clone_checkout $this $HEAD
+       }
+}
+
+method _copy_files {objdir tocopy} {
+       $o_cons start \
+               [mc "Copying objects"] \
+               [mc "KiB"]
+       set tot 0
+       set cmp 0
+       foreach p $tocopy {
+               incr tot [file size [file join $objdir $p]]
+       }
+       foreach p $tocopy {
+               if {[catch {
+                               set f_in [open [file join $objdir $p] r]
+                               set f_cp [open [file join .git objects $p] w]
+                               fconfigure $f_in -translation binary -encoding binary
+                               fconfigure $f_cp -translation binary -encoding binary
+
+                               while {![eof $f_in]} {
+                                       incr cmp [fcopy $f_in $f_cp -size 16384]
+                                       $o_cons update \
+                                               [expr {$cmp / 1024}] \
+                                               [expr {$tot / 1024}]
+                                       update
+                               }
+
+                               close $f_in
+                               close $f_cp
+                       } err]} {
+                       _clone_failed $this [mc "Unable to copy object: %s" $err]
+                       return 0
+               }
+       }
+       return 1
+}
+
+method _link_files {objdir tolink} {
+       set total [llength $tolink]
+       $o_cons start \
+               [mc "Linking objects"] \
+               [mc "objects"]
+       for {set i 0} {$i < $total} {} {
+               set p [lindex $tolink $i]
+               if {[catch {
+                               file link -hard \
+                                       [file join .git objects $p] \
+                                       [file join $objdir $p]
+                       } err]} {
+                       _clone_failed $this [mc "Unable to hardlink object: %s" $err]
+                       return 0
+               }
+
+               incr i
+               if {$i % 5 == 0} {
+                       $o_cons update $i $total
+                       update
+               }
+       }
+       return 1
+}
+
+method _clone_refs {} {
+       set pwd [pwd]
+       if {[catch {cd $origin_url} err]} {
+               error_popup [mc "Not a Git repository: %s" [file tail $origin_url]]
+               return 0
+       }
+       set fd_in [git_read for-each-ref \
+               --tcl \
+               {--format=list %(refname) %(objectname) %(*objectname)}]
+       cd $pwd
+
+       set fd [open [gitdir packed-refs] w]
+       fconfigure $fd -translation binary
+       puts $fd "# pack-refs with: peeled"
+       while {[gets $fd_in line] >= 0} {
+               set line [eval $line]
+               set refn [lindex $line 0]
+               set robj [lindex $line 1]
+               set tobj [lindex $line 2]
+
+               if {[regsub ^refs/heads/ $refn \
+                       "refs/remotes/$origin_name/" refn]} {
+                       puts $fd "$robj $refn"
+               } elseif {[string match refs/tags/* $refn]} {
+                       puts $fd "$robj $refn"
+                       if {$tobj ne {}} {
+                               puts $fd "^$tobj"
+                       }
+               }
+       }
+       close $fd_in
+       close $fd
+       return 1
+}
+
+method _do_clone_tags {ok} {
+       if {$ok} {
+               $o_cons exec \
+                       [list git fetch --tags -k $origin_name] \
+                       [cb _do_clone_HEAD]
+       } else {
+               $o_cons done $ok
+               _clone_failed $this [mc "Cannot fetch branches and objects.  See console output for details."]
+       }
+}
+
+method _do_clone_HEAD {ok} {
+       if {$ok} {
+               $o_cons exec \
+                       [list git fetch $origin_name HEAD] \
+                       [cb _do_clone_full_end]
+       } else {
+               $o_cons done $ok
+               _clone_failed $this [mc "Cannot fetch tags.  See console output for details."]
+       }
+}
+
+method _do_clone_full_end {ok} {
+       $o_cons done $ok
+
+       if {$ok} {
+               destroy $w_body
+
+               set HEAD {}
+               if {[file exists [gitdir FETCH_HEAD]]} {
+                       set fd [open [gitdir FETCH_HEAD] r]
+                       while {[gets $fd line] >= 0} {
+                               if {[regexp "^(.{40})\t\t" $line line HEAD]} {
+                                       break
+                               }
+                       }
+                       close $fd
+               }
+
+               catch {git pack-refs}
+               _do_clone_checkout $this $HEAD
+       } else {
+               _clone_failed $this [mc "Cannot determine HEAD.  See console output for details."]
+       }
+}
+
+method _clone_failed {{why {}}} {
+       if {[catch {file delete -force $local_path} err]} {
+               set why [strcat \
+                       $why \
+                       "\n\n" \
+                       [mc "Unable to cleanup %s" $local_path] \
+                       "\n\n" \
+                       $err]
+       }
+       if {$why ne {}} {
+               update
+               error_popup [strcat [mc "Clone failed."] "\n" $why]
+       }
+}
+
+method _do_clone_checkout {HEAD} {
+       if {$HEAD eq {}} {
+               info_popup [strcat \
+                       [mc "No default branch obtained."] \
+                       "\n" \
+                       [mc "The 'master' branch has not been initialized."] \
+                       ]
+               set done 1
+               return
+       }
+       if {[catch {
+                       git update-ref HEAD $HEAD^0
+               } err]} {
+               info_popup [strcat \
+                       [mc "Cannot resolve %s as a commit." $HEAD^0] \
+                       "\n  $err" \
+                       "\n" \
+                       [mc "The 'master' branch has not been initialized."] \
+                       ]
+               set done 1
+               return
+       }
+
+       set o_cons [status_bar::two_line $w_body]
+       pack $w_body -fill x -padx 10 -pady 10
+       $o_cons start \
+               [mc "Creating working directory"] \
+               [mc "files"]
+
+       set readtree_err {}
+       set fd [git_read --stderr read-tree \
+               -m \
+               -u \
+               -v \
+               HEAD \
+               HEAD \
+               ]
+       fconfigure $fd -blocking 0 -translation binary
+       fileevent $fd readable [cb _readtree_wait $fd]
+}
+
+method _readtree_wait {fd} {
+       set buf [read $fd]
+       $o_cons update_meter $buf
+       append readtree_err $buf
+
+       fconfigure $fd -blocking 1
+       if {![eof $fd]} {
+               fconfigure $fd -blocking 0
+               return
+       }
+
+       if {[catch {close $fd}]} {
+               set err $readtree_err
+               regsub {^fatal: } $err {} err
+               error_popup [strcat \
+                       [mc "Initial file checkout failed."] \
+                       "\n\n$err"]
+               return
+       }
+
+       set done 1
+}
+
+######################################################################
+##
+## Open Existing Repository
+
+method _do_open {} {
+       $w_next conf \
+               -state disabled \
+               -command [cb _do_open2] \
+               -text [mc "Open"]
+
+       frame $w_body
+       label $w_body.h \
+               -font font_uibold \
+               -text [mc "Open Existing Repository"]
+       pack $w_body.h -side top -fill x -pady 10
+       pack $w_body -fill x -padx 10
+
+       frame $w_body.where
+       label $w_body.where.l -text [mc "Repository:"]
+       entry $w_body.where.t \
+               -textvariable @local_path \
+               -font font_diff \
+               -width 50
+       button $w_body.where.b \
+               -text [mc "Browse"] \
+               -command [cb _open_local_path]
+
+       pack $w_body.where.b -side right
+       pack $w_body.where.l -side left
+       pack $w_body.where.t -fill x
+       pack $w_body.where -fill x
+
+       trace add variable @local_path write [cb _write_local_path]
+       bind $w_body.h <Destroy> [list trace remove variable @local_path write [cb _write_local_path]]
+       update
+       focus $w_body.where.t
+}
+
+method _open_local_path {} {
+       if {$local_path ne {}} {
+               set p $local_path
+       } else {
+               set p [_home]
+       }
+
+       set p [tk_chooseDirectory \
+               -initialdir $p \
+               -parent $top \
+               -title [mc "Git Repository"] \
+               -mustexist true]
+       if {$p eq {}} return
+
+       set p [file normalize $p]
+       if {![_is_git [file join $p .git]]} {
+               error_popup [mc "Not a Git repository: %s" [file tail $p]]
+               return
+       }
+       set local_path $p
+}
+
+method _do_open2 {} {
+       if {![_is_git [file join $local_path .git]]} {
+               error_popup [mc "Not a Git repository: %s" [file tail $local_path]]
+               return
+       }
+
+       if {[catch {cd $local_path} err]} {
+               error_popup [strcat \
+                       [mc "Failed to open repository %s:" $local_path] \
+                       "\n\n$err"]
+               return
+       }
+
+       _append_recentrepos [pwd]
+       set ::_gitdir .git
+       set ::_prefix {}
+       set done 1
+}
+
+}
diff --git a/git-gui/lib/choose_rev.tcl b/git-gui/lib/choose_rev.tcl
new file mode 100644 (file)
index 0000000..a063c5b
--- /dev/null
@@ -0,0 +1,627 @@
+# git-gui revision chooser
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+class choose_rev {
+
+image create photo ::choose_rev::img_find -data {R0lGODlhEAAQAIYAAPwCBCQmJDw+PBQSFAQCBMza3NTm5MTW1HyChOT29Ozq7MTq7Kze5Kzm7Oz6/NTy9Iza5GzGzKzS1Nzy9Nz29Kzq9HTGzHTK1Lza3AwKDLzu9JTi7HTW5GTCzITO1Mzq7Hza5FTK1ESyvHzKzKzW3DQyNDyqtDw6PIzW5HzGzAT+/Dw+RKyurNTOzMTGxMS+tJSGdATCxHRydLSqpLymnLSijBweHERCRNze3Pz69PTy9Oze1OTSxOTGrMSqlLy+vPTu5OzSvMymjNTGvNS+tMy2pMyunMSefAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAe4gACCAAECA4OIiAIEBQYHBAKJgwIICQoLDA0IkZIECQ4PCxARCwSSAxITFA8VEBYXGBmJAQYLGhUbHB0eH7KIGRIMEBAgISIjJKaIJQQLFxERIialkieUGigpKRoIBCqJKyyLBwvJAioEyoICLS4v6QQwMQQyLuqLli8zNDU2BCf1lN3AkUPHDh49fAQAAEnGD1MCCALZEaSHkIUMBQS8wWMIkSJGhBzBmFEGgRsBUqpMiSgdAD+BAAAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
+
+field w               ; # our megawidget path
+field w_list          ; # list of currently filtered specs
+field w_filter        ; # filter entry for $w_list
+
+field c_expr        {}; # current revision expression
+field filter          ; # current filter string
+field revtype     head; # type of revision chosen
+field cur_specs [list]; # list of specs for $revtype
+field spec_head       ; # list of all head specs
+field spec_trck       ; # list of all tracking branch specs
+field spec_tag        ; # list of all tag specs
+field tip_data        ; # array of tip commit info by refname
+field log_last        ; # array of reflog date by refname
+
+field tooltip_wm        {} ; # Current tooltip toplevel, if open
+field tooltip_t         {} ; # Text widget in $tooltip_wm
+field tooltip_timer     {} ; # Current timer event for our tooltip
+
+proc new {path {title {}}} {
+       return [_new $path 0 $title]
+}
+
+proc new_unmerged {path {title {}}} {
+       return [_new $path 1 $title]
+}
+
+constructor _new {path unmerged_only title} {
+       global current_branch is_detached
+
+       if {![info exists ::all_remotes]} {
+               load_all_remotes
+       }
+
+       set w $path
+
+       if {$title ne {}} {
+               labelframe $w -text $title
+       } else {
+               frame $w
+       }
+       bind $w <Destroy> [cb _delete %W]
+
+       if {$is_detached} {
+               radiobutton $w.detachedhead_r \
+                       -anchor w \
+                       -text [mc "This Detached Checkout"] \
+                       -value HEAD \
+                       -variable @revtype
+               grid $w.detachedhead_r -sticky we -padx {0 5} -columnspan 2
+       }
+
+       radiobutton $w.expr_r \
+               -text [mc "Revision Expression:"] \
+               -value expr \
+               -variable @revtype
+       entry $w.expr_t \
+               -borderwidth 1 \
+               -relief sunken \
+               -width 50 \
+               -textvariable @c_expr \
+               -validate key \
+               -validatecommand [cb _validate %d %S]
+       grid $w.expr_r $w.expr_t -sticky we -padx {0 5}
+
+       frame $w.types
+       radiobutton $w.types.head_r \
+               -text [mc "Local Branch"] \
+               -value head \
+               -variable @revtype
+       pack $w.types.head_r -side left
+       radiobutton $w.types.trck_r \
+               -text [mc "Tracking Branch"] \
+               -value trck \
+               -variable @revtype
+       pack $w.types.trck_r -side left
+       radiobutton $w.types.tag_r \
+               -text [mc "Tag"] \
+               -value tag \
+               -variable @revtype
+       pack $w.types.tag_r -side left
+       set w_filter $w.types.filter
+       entry $w_filter \
+               -borderwidth 1 \
+               -relief sunken \
+               -width 12 \
+               -textvariable @filter \
+               -validate key \
+               -validatecommand [cb _filter %P]
+       pack $w_filter -side right
+       pack [label $w.types.filter_icon \
+               -image ::choose_rev::img_find \
+               ] -side right
+       grid $w.types -sticky we -padx {0 5} -columnspan 2
+
+       frame $w.list
+       set w_list $w.list.l
+       listbox $w_list \
+               -font font_diff \
+               -width 50 \
+               -height 10 \
+               -selectmode browse \
+               -exportselection false \
+               -xscrollcommand [cb _sb_set $w.list.sbx h] \
+               -yscrollcommand [cb _sb_set $w.list.sby v]
+       pack $w_list -fill both -expand 1
+       grid $w.list -sticky nswe -padx {20 5} -columnspan 2
+       bind $w_list <Any-Motion>  [cb _show_tooltip @%x,%y]
+       bind $w_list <Any-Enter>   [cb _hide_tooltip]
+       bind $w_list <Any-Leave>   [cb _hide_tooltip]
+       bind $w_list <Destroy>     [cb _hide_tooltip]
+
+       grid columnconfigure $w 1 -weight 1
+       if {$is_detached} {
+               grid rowconfigure $w 3 -weight 1
+       } else {
+               grid rowconfigure $w 2 -weight 1
+       }
+
+       trace add variable @revtype write [cb _select]
+       bind $w_filter <Key-Return> [list focus $w_list]\;break
+       bind $w_filter <Key-Down>   [list focus $w_list]
+
+       set fmt list
+       append fmt { %(refname)}
+       append fmt { [list}
+       append fmt { %(objecttype)}
+       append fmt { %(objectname)}
+       append fmt { [concat %(taggername) %(authorname)]}
+       append fmt { [reformat_date [concat %(taggerdate) %(authordate)]]}
+       append fmt { %(subject)}
+       append fmt {] [list}
+       append fmt { %(*objecttype)}
+       append fmt { %(*objectname)}
+       append fmt { %(*authorname)}
+       append fmt { [reformat_date %(*authordate)]}
+       append fmt { %(*subject)}
+       append fmt {]}
+       set all_refn [list]
+       set fr_fd [git_read for-each-ref \
+               --tcl \
+               --sort=-taggerdate \
+               --format=$fmt \
+               refs/heads \
+               refs/remotes \
+               refs/tags \
+               ]
+       fconfigure $fr_fd -translation lf -encoding utf-8
+       while {[gets $fr_fd line] > 0} {
+               set line [eval $line]
+               if {[lindex $line 1 0] eq {tag}} {
+                       if {[lindex $line 2 0] eq {commit}} {
+                               set sha1 [lindex $line 2 1]
+                       } else {
+                               continue
+                       }
+               } elseif {[lindex $line 1 0] eq {commit}} {
+                       set sha1 [lindex $line 1 1]
+               } else {
+                       continue
+               }
+               set refn [lindex $line 0]
+               set tip_data($refn) [lrange $line 1 end]
+               lappend cmt_refn($sha1) $refn
+               lappend all_refn $refn
+       }
+       close $fr_fd
+
+       if {$unmerged_only} {
+               set fr_fd [git_read rev-list --all ^$::HEAD]
+               while {[gets $fr_fd sha1] > 0} {
+                       if {[catch {set rlst $cmt_refn($sha1)}]} continue
+                       foreach refn $rlst {
+                               set inc($refn) 1
+                       }
+               }
+               close $fr_fd
+       } else {
+               foreach refn $all_refn {
+                       set inc($refn) 1
+               }
+       }
+
+       set spec_head [list]
+       foreach name [load_all_heads] {
+               set refn refs/heads/$name
+               if {[info exists inc($refn)]} {
+                       lappend spec_head [list $name $refn]
+               }
+       }
+
+       set spec_trck [list]
+       foreach spec [all_tracking_branches] {
+               set refn [lindex $spec 0]
+               if {[info exists inc($refn)]} {
+                       regsub ^refs/(heads|remotes)/ $refn {} name
+                       lappend spec_trck [concat $name $spec]
+               }
+       }
+
+       set spec_tag [list]
+       foreach name [load_all_tags] {
+               set refn refs/tags/$name
+               if {[info exists inc($refn)]} {
+                       lappend spec_tag [list $name $refn]
+               }
+       }
+
+                 if {$is_detached}             { set revtype HEAD
+       } elseif {[llength $spec_head] > 0} { set revtype head
+       } elseif {[llength $spec_trck] > 0} { set revtype trck
+       } elseif {[llength $spec_tag ] > 0} { set revtype tag
+       } else {                              set revtype expr
+       }
+
+       if {$revtype eq {head} && $current_branch ne {}} {
+               set i 0
+               foreach spec $spec_head {
+                       if {[lindex $spec 0] eq $current_branch} {
+                               $w_list selection clear 0 end
+                               $w_list selection set $i
+                               break
+                       }
+                       incr i
+               }
+       }
+
+       return $this
+}
+
+method none {text} {
+       if {![winfo exists $w.none_r]} {
+               radiobutton $w.none_r \
+                       -anchor w \
+                       -value none \
+                       -variable @revtype
+               grid $w.none_r -sticky we -padx {0 5} -columnspan 2
+       }
+       $w.none_r configure -text $text
+}
+
+method get {} {
+       switch -- $revtype {
+       head -
+       trck -
+       tag  {
+               set i [$w_list curselection]
+               if {$i ne {}} {
+                       return [lindex $cur_specs $i 0]
+               } else {
+                       return {}
+               }
+       }
+
+       HEAD { return HEAD                     }
+       expr { return $c_expr                  }
+       none { return {}                       }
+       default { error "unknown type of revision" }
+       }
+}
+
+method pick_tracking_branch {} {
+       set revtype trck
+}
+
+method focus_filter {} {
+       if {[$w_filter cget -state] eq {normal}} {
+               focus $w_filter
+       }
+}
+
+method bind_listbox {event script}  {
+       bind $w_list $event $script
+}
+
+method get_local_branch {} {
+       if {$revtype eq {head}} {
+               return [_expr $this]
+       } else {
+               return {}
+       }
+}
+
+method get_tracking_branch {} {
+       set i [$w_list curselection]
+       if {$i eq {} || $revtype ne {trck}} {
+               return {}
+       }
+       return [lrange [lindex $cur_specs $i] 1 end]
+}
+
+method get_commit {} {
+       set e [_expr $this]
+       if {$e eq {}} {
+               return {}
+       }
+       return [git rev-parse --verify "$e^0"]
+}
+
+method commit_or_die {} {
+       if {[catch {set new [get_commit $this]} err]} {
+
+               # Cleanup the not-so-friendly error from rev-parse.
+               #
+               regsub {^fatal:\s*} $err {} err
+               if {$err eq {Needed a single revision}} {
+                       set err {}
+               }
+
+               set top [winfo toplevel $w]
+               set msg [strcat [mc "Invalid revision: %s" [get $this]] "\n\n$err"]
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $top] \
+                       -parent $top \
+                       -message $msg
+               error $msg
+       }
+       return $new
+}
+
+method _expr {} {
+       switch -- $revtype {
+       head -
+       trck -
+       tag  {
+               set i [$w_list curselection]
+               if {$i ne {}} {
+                       return [lindex $cur_specs $i 1]
+               } else {
+                       error [mc "No revision selected."]
+               }
+       }
+
+       expr {
+               if {$c_expr ne {}} {
+                       return $c_expr
+               } else {
+                       error [mc "Revision expression is empty."]
+               }
+       }
+       HEAD { return HEAD                     }
+       none { return {}                       }
+       default { error "unknown type of revision"      }
+       }
+}
+
+method _validate {d S} {
+       if {$d == 1} {
+               if {[regexp {\s} $S]} {
+                       return 0
+               }
+               if {[string length $S] > 0} {
+                       set revtype expr
+               }
+       }
+       return 1
+}
+
+method _filter {P} {
+       if {[regexp {\s} $P]} {
+               return 0
+       }
+       _rebuild $this $P
+       return 1
+}
+
+method _select {args} {
+       _rebuild $this $filter
+       focus_filter $this
+}
+
+method _rebuild {pat} {
+       set ste normal
+       switch -- $revtype {
+       head { set new $spec_head }
+       trck { set new $spec_trck }
+       tag  { set new $spec_tag  }
+       expr -
+       HEAD -
+       none {
+               set new [list]
+               set ste disabled
+       }
+       }
+
+       if {[$w_list cget -state] eq {disabled}} {
+               $w_list configure -state normal
+       }
+       $w_list delete 0 end
+
+       if {$pat ne {}} {
+               set pat *${pat}*
+       }
+       set cur_specs [list]
+       foreach spec $new {
+               set txt [lindex $spec 0]
+               if {$pat eq {} || [string match $pat $txt]} {
+                       lappend cur_specs $spec
+                       $w_list insert end $txt
+               }
+       }
+       if {$cur_specs ne {}} {
+               $w_list selection clear 0 end
+               $w_list selection set 0
+       }
+
+       if {[$w_filter cget -state] ne $ste} {
+               $w_list   configure -state $ste
+               $w_filter configure -state $ste
+       }
+}
+
+method _delete {current} {
+       if {$current eq $w} {
+               delete_this
+       }
+}
+
+method _sb_set {sb orient first last} {
+       set old_focus [focus -lastfor $w]
+
+       if {$first == 0 && $last == 1} {
+               if {[winfo exists $sb]} {
+                       destroy $sb
+                       if {$old_focus ne {}} {
+                               update
+                               focus $old_focus
+                       }
+               }
+               return
+       }
+
+       if {![winfo exists $sb]} {
+               if {$orient eq {h}} {
+                       scrollbar $sb -orient h -command [list $w_list xview]
+                       pack $sb -fill x -side bottom -before $w_list
+               } else {
+                       scrollbar $sb -orient v -command [list $w_list yview]
+                       pack $sb -fill y -side right -before $w_list
+               }
+               if {$old_focus ne {}} {
+                       update
+                       focus $old_focus
+               }
+       }
+       $sb set $first $last
+}
+
+method _show_tooltip {pos} {
+       if {$tooltip_wm ne {}} {
+               _open_tooltip $this
+       } elseif {$tooltip_timer eq {}} {
+               set tooltip_timer [after 1000 [cb _open_tooltip]]
+       }
+}
+
+method _open_tooltip {} {
+       global remote_url
+
+       set tooltip_timer {}
+       set pos_x [winfo pointerx $w_list]
+       set pos_y [winfo pointery $w_list]
+       if {[winfo containing $pos_x $pos_y] ne $w_list} {
+               _hide_tooltip $this
+               return
+       }
+
+       set pos @[join [list \
+               [expr {$pos_x - [winfo rootx $w_list]}] \
+               [expr {$pos_y - [winfo rooty $w_list]}]] ,]
+       set lno [$w_list index $pos]
+       if {$lno eq {}} {
+               _hide_tooltip $this
+               return
+       }
+
+       set spec [lindex $cur_specs $lno]
+       set refn [lindex $spec 1]
+       if {$refn eq {}} {
+               _hide_tooltip $this
+               return
+       }
+
+       if {$tooltip_wm eq {}} {
+               set tooltip_wm [toplevel $w_list.tooltip -borderwidth 1]
+               wm overrideredirect $tooltip_wm 1
+               wm transient $tooltip_wm [winfo toplevel $w_list]
+               set tooltip_t $tooltip_wm.label
+               text $tooltip_t \
+                       -takefocus 0 \
+                       -highlightthickness 0 \
+                       -relief flat \
+                       -borderwidth 0 \
+                       -wrap none \
+                       -background lightyellow \
+                       -foreground black
+               $tooltip_t tag conf section_header -font font_uibold
+               bind $tooltip_wm <Escape> [cb _hide_tooltip]
+               pack $tooltip_t
+       } else {
+               $tooltip_t conf -state normal
+               $tooltip_t delete 0.0 end
+       }
+
+       set data $tip_data($refn)
+       if {[lindex $data 0 0] eq {tag}} {
+               set tag  [lindex $data 0]
+               if {[lindex $data 1 0] eq {commit}} {
+                       set cmit [lindex $data 1]
+               } else {
+                       set cmit {}
+               }
+       } elseif {[lindex $data 0 0] eq {commit}} {
+               set tag  {}
+               set cmit [lindex $data 0]
+       }
+
+       $tooltip_t insert end [lindex $spec 0]
+       set last [_reflog_last $this [lindex $spec 1]]
+       if {$last ne {}} {
+               $tooltip_t insert end "\n"
+               $tooltip_t insert end [mc "Updated"]
+               $tooltip_t insert end " $last"
+       }
+       $tooltip_t insert end "\n"
+
+       if {$tag ne {}} {
+               $tooltip_t insert end "\n"
+               $tooltip_t insert end [mc "Tag"] section_header
+               $tooltip_t insert end "  [lindex $tag 1]\n"
+               $tooltip_t insert end [lindex $tag 2]
+               $tooltip_t insert end " ([lindex $tag 3])\n"
+               $tooltip_t insert end [lindex $tag 4]
+               $tooltip_t insert end "\n"
+       }
+
+       if {$cmit ne {}} {
+               $tooltip_t insert end "\n"
+               $tooltip_t insert end [mc "Commit@@noun"] section_header
+               $tooltip_t insert end "  [lindex $cmit 1]\n"
+               $tooltip_t insert end [lindex $cmit 2]
+               $tooltip_t insert end " ([lindex $cmit 3])\n"
+               $tooltip_t insert end [lindex $cmit 4]
+       }
+
+       if {[llength $spec] > 2} {
+               $tooltip_t insert end "\n"
+               $tooltip_t insert end [mc "Remote"] section_header
+               $tooltip_t insert end "  [lindex $spec 2]\n"
+               $tooltip_t insert end [mc "URL"]
+               $tooltip_t insert end " $remote_url([lindex $spec 2])\n"
+               $tooltip_t insert end [mc "Branch"]
+               $tooltip_t insert end " [lindex $spec 3]"
+       }
+
+       $tooltip_t conf -state disabled
+       _position_tooltip $this
+}
+
+method _reflog_last {name} {
+       if {[info exists reflog_last($name)]} {
+               return reflog_last($name)
+       }
+
+       set last {}
+       if {[catch {set last [file mtime [gitdir $name]]}]
+       && ![catch {set g [open [gitdir logs $name] r]}]} {
+               fconfigure $g -translation binary
+               while {[gets $g line] >= 0} {
+                       if {[regexp {> ([1-9][0-9]*) } $line line when]} {
+                               set last $when
+                       }
+               }
+               close $g
+       }
+
+       if {$last ne {}} {
+               set last [format_date $last]
+       }
+       set reflog_last($name) $last
+       return $last
+}
+
+method _position_tooltip {} {
+       set max_h [lindex [split [$tooltip_t index end] .] 0]
+       set max_w 0
+       for {set i 1} {$i <= $max_h} {incr i} {
+               set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1]
+               if {$c > $max_w} {set max_w $c}
+       }
+       $tooltip_t conf -width $max_w -height $max_h
+
+       set req_w [winfo reqwidth  $tooltip_t]
+       set req_h [winfo reqheight $tooltip_t]
+       set pos_x [expr {[winfo pointerx .] +  5}]
+       set pos_y [expr {[winfo pointery .] + 10}]
+
+       set g "${req_w}x${req_h}"
+       if {$pos_x >= 0} {append g +}
+       append g $pos_x
+       if {$pos_y >= 0} {append g +}
+       append g $pos_y
+
+       wm geometry $tooltip_wm $g
+       raise $tooltip_wm
+}
+
+method _hide_tooltip {} {
+       if {$tooltip_wm ne {}} {
+               destroy $tooltip_wm
+               set tooltip_wm {}
+       }
+       if {$tooltip_timer ne {}} {
+               after cancel $tooltip_timer
+               set tooltip_timer {}
+       }
+}
+
+}
diff --git a/git-gui/lib/class.tcl b/git-gui/lib/class.tcl
new file mode 100644 (file)
index 0000000..dc21411
--- /dev/null
@@ -0,0 +1,186 @@
+# git-gui simple class/object fake-alike
+# Copyright (C) 2007 Shawn Pearce
+
+proc class {class body} {
+       if {[namespace exists $class]} {
+               error "class $class already declared"
+       }
+       namespace eval $class "
+               variable __nextid     0
+               variable __sealed     0
+               variable __field_list {}
+               variable __field_array
+
+               proc cb {name args} {
+                       upvar this this
+                       concat \[list ${class}::\$name \$this\] \$args
+               }
+       "
+       namespace eval $class $body
+}
+
+proc field {name args} {
+       set class [uplevel {namespace current}]
+       variable ${class}::__sealed
+       variable ${class}::__field_array
+
+       switch [llength $args] {
+       0 { set new [list $name] }
+       1 { set new [list $name [lindex $args 0]] }
+       default { error "wrong # args: field name value?" }
+       }
+
+       if {$__sealed} {
+               error "class $class is sealed (cannot add new fields)"
+       }
+
+       if {[catch {set old $__field_array($name)}]} {
+               variable ${class}::__field_list
+               lappend __field_list $new
+               set __field_array($name) 1
+       } else {
+               error "field $name already declared"
+       }
+}
+
+proc constructor {name params body} {
+       set class [uplevel {namespace current}]
+       set ${class}::__sealed 1
+       variable ${class}::__field_list
+       set mbodyc {}
+
+       append mbodyc {set this } $class
+       append mbodyc {::__o[incr } $class {::__nextid]::__d} \;
+       append mbodyc {create_this } $class \;
+       append mbodyc {set __this [namespace qualifiers $this]} \;
+
+       if {$__field_list ne {}} {
+               append mbodyc {upvar #0}
+               foreach n $__field_list {
+                       set n [lindex $n 0]
+                       append mbodyc { ${__this}::} $n { } $n
+                       regsub -all @$n\\M $body "\${__this}::$n" body
+               }
+               append mbodyc \;
+               foreach n $__field_list {
+                       if {[llength $n] == 2} {
+                               append mbodyc \
+                               {set } [lindex $n 0] { } [list [lindex $n 1]] \;
+                       }
+               }
+       }
+       append mbodyc $body
+       namespace eval $class [list proc $name $params $mbodyc]
+}
+
+proc method {name params body {deleted {}} {del_body {}}} {
+       set class [uplevel {namespace current}]
+       set ${class}::__sealed 1
+       variable ${class}::__field_list
+       set params [linsert $params 0 this]
+       set mbodyc {}
+
+       append mbodyc {set __this [namespace qualifiers $this]} \;
+
+       switch $deleted {
+       {} {}
+       ifdeleted {
+               append mbodyc {if {![namespace exists $__this]} }
+               append mbodyc \{ $del_body \; return \} \;
+       }
+       default {
+               error "wrong # args: method name args body (ifdeleted body)?"
+       }
+       }
+
+       set decl {}
+       foreach n $__field_list {
+               set n [lindex $n 0]
+               if {[regexp -- $n\\M $body]} {
+                       if {   [regexp -all -- $n\\M $body] == 1
+                               && [regexp -all -- \\\$$n\\M $body] == 1
+                               && [regexp -all -- \\\$$n\\( $body] == 0} {
+                               regsub -all \
+                                       \\\$$n\\M $body \
+                                       "\[set \${__this}::$n\]" body
+                       } else {
+                               append decl { ${__this}::} $n { } $n
+                               regsub -all @$n\\M $body "\${__this}::$n" body
+                       }
+               }
+       }
+       if {$decl ne {}} {
+               append mbodyc {upvar #0} $decl \;
+       }
+       append mbodyc $body
+       namespace eval $class [list proc $name $params $mbodyc]
+}
+
+proc create_this {class} {
+       upvar this this
+       namespace eval [namespace qualifiers $this] [list proc \
+               [namespace tail $this] \
+               [list name args] \
+               "eval \[list ${class}::\$name $this\] \$args" \
+       ]
+}
+
+proc delete_this {{t {}}} {
+       if {$t eq {}} {
+               upvar this this
+               set t $this
+       }
+       set t [namespace qualifiers $t]
+       if {[namespace exists $t]} {namespace delete $t}
+}
+
+proc make_toplevel {t w args} {
+       upvar $t top $w pfx this this
+
+       if {[llength $args] % 2} {
+               error "make_toplevel topvar winvar {options}"
+       }
+       set autodelete 1
+       foreach {name value} $args {
+               switch -exact -- $name {
+               -autodelete {set autodelete $value}
+               default     {error "unsupported option $name"}
+               }
+       }
+
+       if {$::root_exists || [winfo ismapped .]} {
+               regsub -all {::} $this {__} w
+               set top .$w
+               set pfx $top
+               toplevel $top
+               set ::root_exists 1
+       } else {
+               set top .
+               set pfx {}
+       }
+
+       if {$autodelete} {
+               wm protocol $top WM_DELETE_WINDOW "
+                       [list delete_this $this]
+                       [list destroy $top]
+               "
+       }
+}
+
+
+## auto_mkindex support for class/constructor/method
+##
+auto_mkindex_parser::command class {name body} {
+       variable parser
+       variable contextStack
+       set contextStack [linsert $contextStack 0 $name]
+       $parser eval [list _%@namespace eval $name] $body
+       set contextStack [lrange $contextStack 1 end]
+}
+auto_mkindex_parser::command constructor {name args} {
+       variable index
+       variable scriptFile
+       append index [list set auto_index([fullname $name])] \
+               [format { [list source [file join $dir %s]]} \
+               [file split $scriptFile]] "\n"
+}
diff --git a/git-gui/lib/commit.tcl b/git-gui/lib/commit.tcl
new file mode 100644 (file)
index 0000000..b2d2d53
--- /dev/null
@@ -0,0 +1,431 @@
+# git-gui misc. commit reading/writing support
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc load_last_commit {} {
+       global HEAD PARENT MERGE_HEAD commit_type ui_comm
+       global repo_config
+
+       if {[llength $PARENT] == 0} {
+               error_popup [mc "There is nothing to amend.
+
+You are about to create the initial commit.  There is no commit before this to amend.
+"]
+               return
+       }
+
+       repository_state curType curHEAD curMERGE_HEAD
+       if {$curType eq {merge}} {
+               error_popup [mc "Cannot amend while merging.
+
+You are currently in the middle of a merge that has not been fully completed.  You cannot amend the prior commit unless you first abort the current merge activity.
+"]
+               return
+       }
+
+       set msg {}
+       set parents [list]
+       if {[catch {
+                       set fd [git_read cat-file commit $curHEAD]
+                       fconfigure $fd -encoding binary -translation lf
+                       if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
+                               set enc utf-8
+                       }
+                       while {[gets $fd line] > 0} {
+                               if {[string match {parent *} $line]} {
+                                       lappend parents [string range $line 7 end]
+                               } elseif {[string match {encoding *} $line]} {
+                                       set enc [string tolower [string range $line 9 end]]
+                               }
+                       }
+                       set msg [read $fd]
+                       close $fd
+
+                       set enc [tcl_encoding $enc]
+                       if {$enc ne {}} {
+                               set msg [encoding convertfrom $enc $msg]
+                       }
+                       set msg [string trim $msg]
+               } err]} {
+               error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
+               return
+       }
+
+       set HEAD $curHEAD
+       set PARENT $parents
+       set MERGE_HEAD [list]
+       switch -- [llength $parents] {
+       0       {set commit_type amend-initial}
+       1       {set commit_type amend}
+       default {set commit_type amend-merge}
+       }
+
+       $ui_comm delete 0.0 end
+       $ui_comm insert end $msg
+       $ui_comm edit reset
+       $ui_comm edit modified false
+       rescan ui_ready
+}
+
+set GIT_COMMITTER_IDENT {}
+
+proc committer_ident {} {
+       global GIT_COMMITTER_IDENT
+
+       if {$GIT_COMMITTER_IDENT eq {}} {
+               if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} {
+                       error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"]
+                       return {}
+               }
+               if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
+                       $me me GIT_COMMITTER_IDENT]} {
+                       error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"]
+                       return {}
+               }
+       }
+
+       return $GIT_COMMITTER_IDENT
+}
+
+proc do_signoff {} {
+       global ui_comm
+
+       set me [committer_ident]
+       if {$me eq {}} return
+
+       set sob "Signed-off-by: $me"
+       set last [$ui_comm get {end -1c linestart} {end -1c}]
+       if {$last ne $sob} {
+               $ui_comm edit separator
+               if {$last ne {}
+                       && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
+                       $ui_comm insert end "\n"
+               }
+               $ui_comm insert end "\n$sob"
+               $ui_comm edit separator
+               $ui_comm see end
+       }
+}
+
+proc create_new_commit {} {
+       global commit_type ui_comm
+
+       set commit_type normal
+       $ui_comm delete 0.0 end
+       $ui_comm edit reset
+       $ui_comm edit modified false
+       rescan ui_ready
+}
+
+proc commit_tree {} {
+       global HEAD commit_type file_states ui_comm repo_config
+       global pch_error
+
+       if {[committer_ident] eq {}} return
+       if {![lock_index update]} return
+
+       # -- Our in memory state should match the repository.
+       #
+       repository_state curType curHEAD curMERGE_HEAD
+       if {[string match amend* $commit_type]
+               && $curType eq {normal}
+               && $curHEAD eq $HEAD} {
+       } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
+               info_popup [mc "Last scanned state does not match repository state.
+
+Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
+
+The rescan will be automatically started now.
+"]
+               unlock_index
+               rescan ui_ready
+               return
+       }
+
+       # -- At least one file should differ in the index.
+       #
+       set files_ready 0
+       foreach path [array names file_states] {
+               switch -glob -- [lindex $file_states($path) 0] {
+               _? {continue}
+               A? -
+               D? -
+               M? {set files_ready 1}
+               U? {
+                       error_popup [mc "Unmerged files cannot be committed.
+
+File %s has merge conflicts.  You must resolve them and stage the file before committing.
+" [short_path $path]]
+                       unlock_index
+                       return
+               }
+               default {
+                       error_popup [mc "Unknown file state %s detected.
+
+File %s cannot be committed by this program.
+" [lindex $s 0] [short_path $path]]
+               }
+               }
+       }
+       if {!$files_ready && ![string match *merge $curType]} {
+               info_popup [mc "No changes to commit.
+
+You must stage at least 1 file before you can commit.
+"]
+               unlock_index
+               return
+       }
+
+       # -- A message is required.
+       #
+       set msg [string trim [$ui_comm get 1.0 end]]
+       regsub -all -line {[ \t\r]+$} $msg {} msg
+       if {$msg eq {}} {
+               error_popup [mc "Please supply a commit message.
+
+A good commit message has the following format:
+
+- First line: Describe in one sentence what you did.
+- Second line: Blank
+- Remaining lines: Describe why this change is good.
+"]
+               unlock_index
+               return
+       }
+
+       # -- Run the pre-commit hook.
+       #
+       set pchook [gitdir hooks pre-commit]
+
+       # On Cygwin [file executable] might lie so we need to ask
+       # the shell if the hook is executable.  Yes that's annoying.
+       #
+       if {[is_Cygwin] && [file isfile $pchook]} {
+               set pchook [list sh -c [concat \
+                       "if test -x \"$pchook\";" \
+                       "then exec \"$pchook\" 2>&1;" \
+                       "fi"]]
+       } elseif {[file executable $pchook]} {
+               set pchook [list $pchook |& cat]
+       } else {
+               commit_writetree $curHEAD $msg
+               return
+       }
+
+       ui_status {Calling pre-commit hook...}
+       set pch_error {}
+       set fd_ph [open "| $pchook" r]
+       fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
+       fileevent $fd_ph readable \
+               [list commit_prehook_wait $fd_ph $curHEAD $msg]
+}
+
+proc commit_prehook_wait {fd_ph curHEAD msg} {
+       global pch_error
+
+       append pch_error [read $fd_ph]
+       fconfigure $fd_ph -blocking 1
+       if {[eof $fd_ph]} {
+               if {[catch {close $fd_ph}]} {
+                       ui_status {Commit declined by pre-commit hook.}
+                       hook_failed_popup pre-commit $pch_error
+                       unlock_index
+               } else {
+                       commit_writetree $curHEAD $msg
+               }
+               set pch_error {}
+               return
+       }
+       fconfigure $fd_ph -blocking 0
+}
+
+proc commit_writetree {curHEAD msg} {
+       ui_status {Committing changes...}
+       set fd_wt [git_read write-tree]
+       fileevent $fd_wt readable \
+               [list commit_committree $fd_wt $curHEAD $msg]
+}
+
+proc commit_committree {fd_wt curHEAD msg} {
+       global HEAD PARENT MERGE_HEAD commit_type
+       global current_branch
+       global ui_comm selected_commit_type
+       global file_states selected_paths rescan_active
+       global repo_config
+
+       gets $fd_wt tree_id
+       if {[catch {close $fd_wt} err]} {
+               error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
+               ui_status {Commit failed.}
+               unlock_index
+               return
+       }
+
+       # -- Verify this wasn't an empty change.
+       #
+       if {$commit_type eq {normal}} {
+               set fd_ot [git_read cat-file commit $PARENT]
+               fconfigure $fd_ot -encoding binary -translation lf
+               set old_tree [gets $fd_ot]
+               close $fd_ot
+
+               if {[string equal -length 5 {tree } $old_tree]
+                       && [string length $old_tree] == 45} {
+                       set old_tree [string range $old_tree 5 end]
+               } else {
+                       error [mc "Commit %s appears to be corrupt" $PARENT]
+               }
+
+               if {$tree_id eq $old_tree} {
+                       info_popup [mc "No changes to commit.
+
+No files were modified by this commit and it was not a merge commit.
+
+A rescan will be automatically started now.
+"]
+                       unlock_index
+                       rescan {ui_status [mc "No changes to commit."]}
+                       return
+               }
+       }
+
+       # -- Build the message.
+       #
+       set msg_p [gitdir COMMIT_EDITMSG]
+       set msg_wt [open $msg_p w]
+       fconfigure $msg_wt -translation lf
+       if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
+               set enc utf-8
+       }
+       set use_enc [tcl_encoding $enc]
+       if {$use_enc ne {}} {
+               fconfigure $msg_wt -encoding $use_enc
+       } else {
+               puts stderr [mc "warning: Tcl does not support encoding '%s'." $enc]
+               fconfigure $msg_wt -encoding utf-8
+       }
+       puts -nonewline $msg_wt $msg
+       close $msg_wt
+
+       # -- Create the commit.
+       #
+       set cmd [list commit-tree $tree_id]
+       foreach p [concat $PARENT $MERGE_HEAD] {
+               lappend cmd -p $p
+       }
+       lappend cmd <$msg_p
+       if {[catch {set cmt_id [eval git $cmd]} err]} {
+               error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
+               ui_status {Commit failed.}
+               unlock_index
+               return
+       }
+
+       # -- Update the HEAD ref.
+       #
+       set reflogm commit
+       if {$commit_type ne {normal}} {
+               append reflogm " ($commit_type)"
+       }
+       set i [string first "\n" $msg]
+       if {$i >= 0} {
+               set subject [string range $msg 0 [expr {$i - 1}]]
+       } else {
+               set subject $msg
+       }
+       append reflogm {: } $subject
+       if {[catch {
+                       git update-ref -m $reflogm HEAD $cmt_id $curHEAD
+               } err]} {
+               error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
+               ui_status {Commit failed.}
+               unlock_index
+               return
+       }
+
+       # -- Cleanup after ourselves.
+       #
+       catch {file delete $msg_p}
+       catch {file delete [gitdir MERGE_HEAD]}
+       catch {file delete [gitdir MERGE_MSG]}
+       catch {file delete [gitdir SQUASH_MSG]}
+       catch {file delete [gitdir GITGUI_MSG]}
+
+       # -- Let rerere do its thing.
+       #
+       if {[get_config rerere.enabled] eq {}} {
+               set rerere [file isdirectory [gitdir rr-cache]]
+       } else {
+               set rerere [is_config_true rerere.enabled]
+       }
+       if {$rerere} {
+               catch {git rerere}
+       }
+
+       # -- Run the post-commit hook.
+       #
+       set pchook [gitdir hooks post-commit]
+       if {[is_Cygwin] && [file isfile $pchook]} {
+               set pchook [list sh -c [concat \
+                       "if test -x \"$pchook\";" \
+                       "then exec \"$pchook\";" \
+                       "fi"]]
+       } elseif {![file executable $pchook]} {
+               set pchook {}
+       }
+       if {$pchook ne {}} {
+               catch {exec $pchook &}
+       }
+
+       $ui_comm delete 0.0 end
+       $ui_comm edit reset
+       $ui_comm edit modified false
+       if {$::GITGUI_BCK_exists} {
+               catch {file delete [gitdir GITGUI_BCK]}
+               set ::GITGUI_BCK_exists 0
+       }
+
+       if {[is_enabled singlecommit]} do_quit
+
+       # -- Update in memory status
+       #
+       set selected_commit_type new
+       set commit_type normal
+       set HEAD $cmt_id
+       set PARENT $cmt_id
+       set MERGE_HEAD [list]
+
+       foreach path [array names file_states] {
+               set s $file_states($path)
+               set m [lindex $s 0]
+               switch -glob -- $m {
+               _O -
+               _M -
+               _D {continue}
+               __ -
+               A_ -
+               M_ -
+               D_ {
+                       unset file_states($path)
+                       catch {unset selected_paths($path)}
+               }
+               DO {
+                       set file_states($path) [list _O [lindex $s 1] {} {}]
+               }
+               AM -
+               AD -
+               MM -
+               MD {
+                       set file_states($path) [list \
+                               _[string index $m 1] \
+                               [lindex $s 1] \
+                               [lindex $s 3] \
+                               {}]
+               }
+               }
+       }
+
+       display_all_files
+       unlock_index
+       reshow_diff
+       ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
+}
diff --git a/git-gui/lib/console.tcl b/git-gui/lib/console.tcl
new file mode 100644 (file)
index 0000000..5597188
--- /dev/null
@@ -0,0 +1,218 @@
+# git-gui console support
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+class console {
+
+field t_short
+field t_long
+field w
+field w_t
+field console_cr
+field is_toplevel    1; # are we our own window?
+
+constructor new {short_title long_title} {
+       set t_short $short_title
+       set t_long $long_title
+       _init $this
+       return $this
+}
+
+constructor embed {path title} {
+       set t_short {}
+       set t_long $title
+       set w $path
+       set is_toplevel 0
+       _init $this
+       return $this
+}
+
+method _init {} {
+       global M1B
+
+       if {$is_toplevel} {
+               make_toplevel top w -autodelete 0
+               wm title $top "[appname] ([reponame]): $t_short"
+       } else {
+               frame $w
+       }
+
+       set console_cr 1.0
+       set w_t $w.m.t
+
+       frame $w.m
+       label $w.m.l1 \
+               -textvariable @t_long  \
+               -anchor w \
+               -justify left \
+               -font font_uibold
+       text $w_t \
+               -background white -borderwidth 1 \
+               -relief sunken \
+               -width 80 -height 10 \
+               -wrap none \
+               -font font_diff \
+               -state disabled \
+               -xscrollcommand [cb _sb_set $w.m.sbx h] \
+               -yscrollcommand [cb _sb_set $w.m.sby v]
+       label $w.m.s -text [mc "Working... please wait..."] \
+               -anchor w \
+               -justify left \
+               -font font_uibold
+       pack $w.m.l1 -side top -fill x
+       pack $w.m.s -side bottom -fill x
+       pack $w_t -side left -fill both -expand 1
+       pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
+
+       menu $w.ctxm -tearoff 0
+       $w.ctxm add command -label [mc "Copy"] \
+               -command "tk_textCopy $w_t"
+       $w.ctxm add command -label [mc "Select All"] \
+               -command "focus $w_t;$w_t tag add sel 0.0 end"
+       $w.ctxm add command -label [mc "Copy All"] \
+               -command "
+                       $w_t tag add sel 0.0 end
+                       tk_textCopy $w_t
+                       $w_t tag remove sel 0.0 end
+               "
+
+       if {$is_toplevel} {
+               button $w.ok -text [mc "Close"] \
+                       -state disabled \
+                       -command [list destroy $w]
+               pack $w.ok -side bottom -anchor e -pady 10 -padx 10
+               bind $w <Visibility> [list focus $w]
+       }
+
+       bind_button3 $w_t "tk_popup $w.ctxm %X %Y"
+       bind $w_t <$M1B-Key-a> "$w_t tag add sel 0.0 end;break"
+       bind $w_t <$M1B-Key-A> "$w_t tag add sel 0.0 end;break"
+}
+
+method exec {cmd {after {}}} {
+       if {[lindex $cmd 0] eq {git}} {
+               set fd_f [eval git_read --stderr [lrange $cmd 1 end]]
+       } else {
+               lappend cmd 2>@1
+               set fd_f [_open_stdout_stderr $cmd]
+       }
+       fconfigure $fd_f -blocking 0 -translation binary
+       fileevent $fd_f readable [cb _read $fd_f $after]
+}
+
+method _read {fd after} {
+       set buf [read $fd]
+       if {$buf ne {}} {
+               if {![winfo exists $w_t]} {_init $this}
+               $w_t conf -state normal
+               set c 0
+               set n [string length $buf]
+               while {$c < $n} {
+                       set cr [string first "\r" $buf $c]
+                       set lf [string first "\n" $buf $c]
+                       if {$cr < 0} {set cr [expr {$n + 1}]}
+                       if {$lf < 0} {set lf [expr {$n + 1}]}
+
+                       if {$lf < $cr} {
+                               $w_t insert end [string range $buf $c $lf]
+                               set console_cr [$w_t index {end -1c}]
+                               set c $lf
+                               incr c
+                       } else {
+                               $w_t delete $console_cr end
+                               $w_t insert end "\n"
+                               $w_t insert end [string range $buf $c [expr {$cr - 1}]]
+                               set c $cr
+                               incr c
+                       }
+               }
+               $w_t conf -state disabled
+               $w_t see end
+       }
+
+       fconfigure $fd -blocking 1
+       if {[eof $fd]} {
+               if {[catch {close $fd}]} {
+                       set ok 0
+               } else {
+                       set ok 1
+               }
+               if {$after ne {}} {
+                       uplevel #0 $after $ok
+               } else {
+                       done $this $ok
+               }
+               return
+       }
+       fconfigure $fd -blocking 0
+}
+
+method chain {cmdlist {ok 1}} {
+       if {$ok} {
+               if {[llength $cmdlist] == 0} {
+                       done $this $ok
+                       return
+               }
+
+               set cmd [lindex $cmdlist 0]
+               set cmdlist [lrange $cmdlist 1 end]
+
+               if {[lindex $cmd 0] eq {exec}} {
+                       exec $this \
+                               [lrange $cmd 1 end] \
+                               [cb chain $cmdlist]
+               } else {
+                       uplevel #0 $cmd [cb chain $cmdlist]
+               }
+       } else {
+               done $this $ok
+       }
+}
+
+method insert {txt} {
+       if {![winfo exists $w_t]} {_init $this}
+       $w_t conf -state normal
+       $w_t insert end "$txt\n"
+       set console_cr [$w_t index {end -1c}]
+       $w_t conf -state disabled
+}
+
+method done {ok} {
+       if {$ok} {
+               if {[winfo exists $w.m.s]} {
+                       bind $w.m.s <Destroy> [list delete_this $this]
+                       $w.m.s conf -background green -text [mc "Success"]
+                       if {$is_toplevel} {
+                               $w.ok conf -state normal
+                               focus $w.ok
+                       }
+               } else {
+                       delete_this
+               }
+       } else {
+               if {![winfo exists $w.m.s]} {
+                       _init $this
+               }
+               bind $w.m.s <Destroy> [list delete_this $this]
+               $w.m.s conf -background red -text [mc "Error: Command Failed"]
+               if {$is_toplevel} {
+                       $w.ok conf -state normal
+                       focus $w.ok
+               }
+       }
+}
+
+method _sb_set {sb orient first last} {
+       if {![winfo exists $sb]} {
+               if {$first == $last || ($first == 0 && $last == 1)} return
+               if {$orient eq {h}} {
+                       scrollbar $sb -orient h -command [list $w_t xview]
+                       pack $sb -fill x -side bottom -before $w_t
+               } else {
+                       scrollbar $sb -orient v -command [list $w_t yview]
+                       pack $sb -fill y -side right -before $w_t
+               }
+       }
+       $sb set $first $last
+}
+
+}
diff --git a/git-gui/lib/database.tcl b/git-gui/lib/database.tcl
new file mode 100644 (file)
index 0000000..d66aa3f
--- /dev/null
@@ -0,0 +1,116 @@
+# git-gui object database management support
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc do_stats {} {
+       set fd [git_read count-objects -v]
+       while {[gets $fd line] > 0} {
+               if {[regexp {^([^:]+): (\d+)$} $line _ name value]} {
+                       set stats($name) $value
+               }
+       }
+       close $fd
+
+       set packed_sz 0
+       foreach p [glob -directory [gitdir objects pack] \
+               -type f \
+               -nocomplain -- *] {
+               incr packed_sz [file size $p]
+       }
+       if {$packed_sz > 0} {
+               set stats(size-pack) [expr {$packed_sz / 1024}]
+       }
+
+       set w .stats_view
+       toplevel $w
+       wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
+
+       label $w.header -text [mc "Database Statistics"]
+       pack $w.header -side top -fill x
+
+       frame $w.buttons -border 1
+       button $w.buttons.close -text [mc Close] \
+               -default active \
+               -command [list destroy $w]
+       button $w.buttons.gc -text [mc "Compress Database"] \
+               -default normal \
+               -command "destroy $w;do_gc"
+       pack $w.buttons.close -side right
+       pack $w.buttons.gc -side left
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       frame $w.stat -borderwidth 1 -relief solid
+       foreach s {
+               {count           {mc "Number of loose objects"}}
+               {size            {mc "Disk space used by loose objects"} { KiB}}
+               {in-pack         {mc "Number of packed objects"}}
+               {packs           {mc "Number of packs"}}
+               {size-pack       {mc "Disk space used by packed objects"} { KiB}}
+               {prune-packable  {mc "Packed objects waiting for pruning"}}
+               {garbage         {mc "Garbage files"}}
+               } {
+               set name [lindex $s 0]
+               set label [eval [lindex $s 1]]
+               if {[catch {set value $stats($name)}]} continue
+               if {[llength $s] > 2} {
+                       set value "$value[lindex $s 2]"
+               }
+
+               label $w.stat.l_$name -text "$label:" -anchor w
+               label $w.stat.v_$name -text $value -anchor w
+               grid $w.stat.l_$name $w.stat.v_$name -sticky we -padx {0 5}
+       }
+       pack $w.stat -pady 10 -padx 10
+
+       bind $w <Visibility> "grab $w; focus $w.buttons.close"
+       bind $w <Key-Escape> [list destroy $w]
+       bind $w <Key-Return> [list destroy $w]
+       wm title $w [append "[appname] ([reponame]): " [mc "Database Statistics"]]
+       tkwait window $w
+}
+
+proc do_gc {} {
+       set w [console::new {gc} [mc "Compressing the object database"]]
+       console::chain $w {
+               {exec git pack-refs --prune}
+               {exec git reflog expire --all}
+               {exec git repack -a -d -l}
+               {exec git rerere gc}
+       }
+}
+
+proc do_fsck_objects {} {
+       set w [console::new {fsck-objects} \
+               [mc "Verifying the object database with fsck-objects"]]
+       set cmd [list git fsck-objects]
+       lappend cmd --full
+       lappend cmd --cache
+       lappend cmd --strict
+       console::exec $w $cmd
+}
+
+proc hint_gc {} {
+       set object_limit 8
+       if {[is_Windows]} {
+               set object_limit 1
+       }
+
+       set objects_current [llength [glob \
+               -directory [gitdir objects 42] \
+               -nocomplain \
+               -tails \
+               -- \
+               *]]
+
+       if {$objects_current >= $object_limit} {
+               set objects_current [expr {$objects_current * 256}]
+               set object_limit    [expr {$object_limit    * 256}]
+               if {[ask_popup \
+                       [mc "This repository currently has approximately %i loose objects.
+
+To maintain optimal performance it is strongly recommended that you compress the database when more than %i loose objects exist.
+
+Compress the database now?" $objects_current $object_limit]] eq yes} {
+                       do_gc
+               }
+       }
+}
diff --git a/git-gui/lib/date.tcl b/git-gui/lib/date.tcl
new file mode 100644 (file)
index 0000000..abe8299
--- /dev/null
@@ -0,0 +1,53 @@
+# git-gui date processing support
+# Copyright (C) 2007 Shawn Pearce
+
+set git_month(Jan)  1
+set git_month(Feb)  2
+set git_month(Mar)  3
+set git_month(Apr)  4
+set git_month(May)  5
+set git_month(Jun)  6
+set git_month(Jul)  7
+set git_month(Aug)  8
+set git_month(Sep)  9
+set git_month(Oct) 10
+set git_month(Nov) 11
+set git_month(Dec) 12
+
+proc parse_git_date {s} {
+       if {$s eq {}} {
+               return {}
+       }
+
+       if {![regexp \
+               {^... (...) (\d{1,2}) (\d\d):(\d\d):(\d\d) (\d{4}) ([+-]?)(\d\d)(\d\d)$} $s s \
+               month day hr mm ss yr ew tz_h tz_m]} {
+               error [mc "Invalid date from Git: %s" $s]
+       }
+
+       set s [clock scan [format {%4.4i%2.2i%2.2iT%2s%2s%2s} \
+                       $yr $::git_month($month) $day \
+                       $hr $mm $ss] \
+                       -gmt 1]
+
+       regsub ^0 $tz_h {} tz_h
+       regsub ^0 $tz_m {} tz_m
+       switch -- $ew {
+       -  {set ew +}
+       +  {set ew -}
+       {} {set ew -}
+       }
+
+       return [expr "$s $ew ($tz_h * 3600 + $tz_m * 60)"]
+}
+
+proc format_date {s} {
+       if {$s eq {}} {
+               return {}
+       }
+       return [clock format $s -format {%a %b %e %H:%M:%S %Y}]
+}
+
+proc reformat_date {s} {
+       return [format_date [parse_git_date $s]]
+}
diff --git a/git-gui/lib/diff.tcl b/git-gui/lib/diff.tcl
new file mode 100644 (file)
index 0000000..43565e4
--- /dev/null
@@ -0,0 +1,361 @@
+# git-gui diff viewer
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc clear_diff {} {
+       global ui_diff current_diff_path current_diff_header
+       global ui_index ui_workdir
+
+       $ui_diff conf -state normal
+       $ui_diff delete 0.0 end
+       $ui_diff conf -state disabled
+
+       set current_diff_path {}
+       set current_diff_header {}
+
+       $ui_index tag remove in_diff 0.0 end
+       $ui_workdir tag remove in_diff 0.0 end
+}
+
+proc reshow_diff {} {
+       global file_states file_lists
+       global current_diff_path current_diff_side
+
+       set p $current_diff_path
+       if {$p eq {}} {
+               # No diff is being shown.
+       } elseif {$current_diff_side eq {}
+               || [catch {set s $file_states($p)}]
+               || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
+               clear_diff
+       } else {
+               show_diff $p $current_diff_side
+       }
+}
+
+proc handle_empty_diff {} {
+       global current_diff_path file_states file_lists
+
+       set path $current_diff_path
+       set s $file_states($path)
+       if {[lindex $s 0] ne {_M}} return
+
+       info_popup [mc "No differences detected.
+
+%s has no changes.
+
+The modification date of this file was updated by another application, but the content within the file was not changed.
+
+A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
+
+       clear_diff
+       display_file $path __
+       rescan ui_ready 0
+}
+
+proc show_diff {path w {lno {}}} {
+       global file_states file_lists
+       global is_3way_diff diff_active repo_config
+       global ui_diff ui_index ui_workdir
+       global current_diff_path current_diff_side current_diff_header
+
+       if {$diff_active || ![lock_index read]} return
+
+       clear_diff
+       if {$lno == {}} {
+               set lno [lsearch -sorted -exact $file_lists($w) $path]
+               if {$lno >= 0} {
+                       incr lno
+               }
+       }
+       if {$lno >= 1} {
+               $w tag add in_diff $lno.0 [expr {$lno + 1}].0
+       }
+
+       set s $file_states($path)
+       set m [lindex $s 0]
+       set is_3way_diff 0
+       set diff_active 1
+       set current_diff_path $path
+       set current_diff_side $w
+       set current_diff_header {}
+       ui_status [mc "Loading diff of %s..." [escape_path $path]]
+
+       # - Git won't give us the diff, there's nothing to compare to!
+       #
+       if {$m eq {_O}} {
+               set max_sz [expr {128 * 1024}]
+               set type unknown
+               if {[catch {
+                               set type [file type $path]
+                               switch -- $type {
+                               directory {
+                                       set type submodule
+                                       set content {}
+                                       set sz 0
+                               }
+                               link {
+                                       set content [file readlink $path]
+                                       set sz [string length $content]
+                               }
+                               file {
+                                       set fd [open $path r]
+                                       fconfigure $fd -eofchar {}
+                                       set content [read $fd $max_sz]
+                                       close $fd
+                                       set sz [file size $path]
+                               }
+                               default {
+                                       error "'$type' not supported"
+                               }
+                               }
+                       } err ]} {
+                       set diff_active 0
+                       unlock_index
+                       ui_status [mc "Unable to display %s" [escape_path $path]]
+                       error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
+                       return
+               }
+               $ui_diff conf -state normal
+               if {$type eq {submodule}} {
+                       $ui_diff insert end [append \
+                               "* " \
+                               [mc "Git Repository (subproject)"] \
+                               "\n"] d_@
+               } elseif {![catch {set type [exec file $path]}]} {
+                       set n [string length $path]
+                       if {[string equal -length $n $path $type]} {
+                               set type [string range $type $n end]
+                               regsub {^:?\s*} $type {} type
+                       }
+                       $ui_diff insert end "* $type\n" d_@
+               }
+               if {[string first "\0" $content] != -1} {
+                       $ui_diff insert end \
+                               [mc "* Binary file (not showing content)."] \
+                               d_@
+               } else {
+                       if {$sz > $max_sz} {
+                               $ui_diff insert end \
+"* Untracked file is $sz bytes.
+* Showing only first $max_sz bytes.
+" d_@
+                       }
+                       $ui_diff insert end $content
+                       if {$sz > $max_sz} {
+                               $ui_diff insert end "
+* Untracked file clipped here by [appname].
+* To see the entire file, use an external editor.
+" d_@
+                       }
+               }
+               $ui_diff conf -state disabled
+               set diff_active 0
+               unlock_index
+               ui_ready
+               return
+       }
+
+       set cmd [list]
+       if {$w eq $ui_index} {
+               lappend cmd diff-index
+               lappend cmd --cached
+       } elseif {$w eq $ui_workdir} {
+               if {[string index $m 0] eq {U}} {
+                       lappend cmd diff
+               } else {
+                       lappend cmd diff-files
+               }
+       }
+
+       lappend cmd -p
+       lappend cmd --no-color
+       if {$repo_config(gui.diffcontext) >= 0} {
+               lappend cmd "-U$repo_config(gui.diffcontext)"
+       }
+       if {$w eq $ui_index} {
+               lappend cmd [PARENT]
+       }
+       lappend cmd --
+       lappend cmd $path
+
+       if {[catch {set fd [eval git_read --nice $cmd]} err]} {
+               set diff_active 0
+               unlock_index
+               ui_status [mc "Unable to display %s" [escape_path $path]]
+               error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
+               return
+       }
+
+       fconfigure $fd \
+               -blocking 0 \
+               -encoding binary \
+               -translation binary
+       fileevent $fd readable [list read_diff $fd]
+}
+
+proc read_diff {fd} {
+       global ui_diff diff_active
+       global is_3way_diff current_diff_header
+
+       $ui_diff conf -state normal
+       while {[gets $fd line] >= 0} {
+               # -- Cleanup uninteresting diff header lines.
+               #
+               if {   [string match {diff --git *}      $line]
+                       || [string match {diff --cc *}       $line]
+                       || [string match {diff --combined *} $line]
+                       || [string match {--- *}             $line]
+                       || [string match {+++ *}             $line]} {
+                       append current_diff_header $line "\n"
+                       continue
+               }
+               if {[string match {index *} $line]} continue
+               if {$line eq {deleted file mode 120000}} {
+                       set line "deleted symlink"
+               }
+
+               # -- Automatically detect if this is a 3 way diff.
+               #
+               if {[string match {@@@ *} $line]} {set is_3way_diff 1}
+
+               if {[string match {mode *} $line]
+                       || [string match {new file *} $line]
+                       || [string match {deleted file *} $line]
+                       || [string match {deleted symlink} $line]
+                       || [string match {Binary files * and * differ} $line]
+                       || $line eq {\ No newline at end of file}
+                       || [regexp {^\* Unmerged path } $line]} {
+                       set tags {}
+               } elseif {$is_3way_diff} {
+                       set op [string range $line 0 1]
+                       switch -- $op {
+                       {  } {set tags {}}
+                       {@@} {set tags d_@}
+                       { +} {set tags d_s+}
+                       { -} {set tags d_s-}
+                       {+ } {set tags d_+s}
+                       {- } {set tags d_-s}
+                       {--} {set tags d_--}
+                       {++} {
+                               if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
+                                       set line [string replace $line 0 1 {  }]
+                                       set tags d$op
+                               } else {
+                                       set tags d_++
+                               }
+                       }
+                       default {
+                               puts "error: Unhandled 3 way diff marker: {$op}"
+                               set tags {}
+                       }
+                       }
+               } else {
+                       set op [string index $line 0]
+                       switch -- $op {
+                       { } {set tags {}}
+                       {@} {set tags d_@}
+                       {-} {set tags d_-}
+                       {+} {
+                               if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
+                                       set line [string replace $line 0 0 { }]
+                                       set tags d$op
+                               } else {
+                                       set tags d_+
+                               }
+                       }
+                       default {
+                               puts "error: Unhandled 2 way diff marker: {$op}"
+                               set tags {}
+                       }
+                       }
+               }
+               $ui_diff insert end $line $tags
+               if {[string index $line end] eq "\r"} {
+                       $ui_diff tag add d_cr {end - 2c}
+               }
+               $ui_diff insert end "\n" $tags
+       }
+       $ui_diff conf -state disabled
+
+       if {[eof $fd]} {
+               close $fd
+               set diff_active 0
+               unlock_index
+               ui_ready
+
+               if {[$ui_diff index end] eq {2.0}} {
+                       handle_empty_diff
+               }
+       }
+}
+
+proc apply_hunk {x y} {
+       global current_diff_path current_diff_header current_diff_side
+       global ui_diff ui_index file_states
+
+       if {$current_diff_path eq {} || $current_diff_header eq {}} return
+       if {![lock_index apply_hunk]} return
+
+       set apply_cmd {apply --cached --whitespace=nowarn}
+       set mi [lindex $file_states($current_diff_path) 0]
+       if {$current_diff_side eq $ui_index} {
+               set failed_msg [mc "Failed to unstage selected hunk."]
+               lappend apply_cmd --reverse
+               if {[string index $mi 0] ne {M}} {
+                       unlock_index
+                       return
+               }
+       } else {
+               set failed_msg [mc "Failed to stage selected hunk."]
+               if {[string index $mi 1] ne {M}} {
+                       unlock_index
+                       return
+               }
+       }
+
+       set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
+       set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
+       if {$s_lno eq {}} {
+               unlock_index
+               return
+       }
+
+       set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
+       if {$e_lno eq {}} {
+               set e_lno end
+       }
+
+       if {[catch {
+               set p [eval git_write $apply_cmd]
+               fconfigure $p -translation binary -encoding binary
+               puts -nonewline $p $current_diff_header
+               puts -nonewline $p [$ui_diff get $s_lno $e_lno]
+               close $p} err]} {
+               error_popup [append $failed_msg "\n\n$err"]
+               unlock_index
+               return
+       }
+
+       $ui_diff conf -state normal
+       $ui_diff delete $s_lno $e_lno
+       $ui_diff conf -state disabled
+
+       if {[$ui_diff get 1.0 end] eq "\n"} {
+               set o _
+       } else {
+               set o ?
+       }
+
+       if {$current_diff_side eq $ui_index} {
+               set mi ${o}M
+       } elseif {[string index $mi 0] eq {_}} {
+               set mi M$o
+       } else {
+               set mi ?$o
+       }
+       unlock_index
+       display_file $current_diff_path $mi
+       if {$o eq {_}} {
+               clear_diff
+       }
+}
diff --git a/git-gui/lib/encoding.tcl b/git-gui/lib/encoding.tcl
new file mode 100644 (file)
index 0000000..7f06b0d
--- /dev/null
@@ -0,0 +1,276 @@
+# git-gui encoding support
+# Copyright (C) 2005 Paul Mackerras <paulus@samba.org>
+# (Copied from gitk, commit fd8ccbec4f0161)
+
+# This list of encoding names and aliases is distilled from
+# http://www.iana.org/assignments/character-sets.
+# Not all of them are supported by Tcl.
+set encoding_aliases {
+    { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII
+      ISO646-US US-ASCII us IBM367 cp367 csASCII }
+    { ISO-10646-UTF-1 csISO10646UTF1 }
+    { ISO_646.basic:1983 ref csISO646basic1983 }
+    { INVARIANT csINVARIANT }
+    { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion }
+    { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom }
+    { NATS-SEFI iso-ir-8-1 csNATSSEFI }
+    { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD }
+    { NATS-DANO iso-ir-9-1 csNATSDANO }
+    { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD }
+    { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish }
+    { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames }
+    { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 }
+    { ISO-2022-KR csISO2022KR }
+    { EUC-KR csEUCKR }
+    { ISO-2022-JP csISO2022JP }
+    { ISO-2022-JP-2 csISO2022JP2 }
+    { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7
+      csISO13JISC6220jp }
+    { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro }
+    { IT iso-ir-15 ISO646-IT csISO15Italian }
+    { PT iso-ir-16 ISO646-PT csISO16Portuguese }
+    { ES iso-ir-17 ISO646-ES csISO17Spanish }
+    { greek7-old iso-ir-18 csISO18Greek7Old }
+    { latin-greek iso-ir-19 csISO19LatinGreek }
+    { DIN_66003 iso-ir-21 de ISO646-DE csISO21German }
+    { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French }
+    { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 }
+    { ISO_5427 iso-ir-37 csISO5427Cyrillic }
+    { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 }
+    { BS_viewdata iso-ir-47 csISO47BSViewdata }
+    { INIS iso-ir-49 csISO49INIS }
+    { INIS-8 iso-ir-50 csISO50INIS8 }
+    { INIS-cyrillic iso-ir-51 csISO51INISCyrillic }
+    { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 }
+    { ISO_5428:1980 iso-ir-55 csISO5428Greek }
+    { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 }
+    { GB_2312-80 iso-ir-58 chinese csISO58GB231280 }
+    { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian
+      csISO60Norwegian1 }
+    { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 }
+    { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French }
+    { videotex-suppl iso-ir-70 csISO70VideotexSupp1 }
+    { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 }
+    { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 }
+    { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian }
+    { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 }
+    { greek7 iso-ir-88 csISO88Greek7 }
+    { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 }
+    { iso-ir-90 csISO90 }
+    { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a }
+    { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b
+      csISO92JISC62991984b }
+    { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd }
+    { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand }
+    { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add
+      csISO95JIS62291984handadd }
+    { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana }
+    { ISO_2033-1983 iso-ir-98 e13b csISO2033 }
+    { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS }
+    { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819
+      CP819 csISOLatin1 }
+    { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 }
+    { T.61-7bit iso-ir-102 csISO102T617bit }
+    { T.61-8bit T.61 iso-ir-103 csISO103T618bit }
+    { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 }
+    { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 }
+    { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic }
+    { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 }
+    { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 }
+    { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr }
+    { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708
+      arabic csISOLatinArabic }
+    { ISO_8859-6-E csISO88596E ISO-8859-6-E }
+    { ISO_8859-6-I csISO88596I ISO-8859-6-I }
+    { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118
+      greek greek8 csISOLatinGreek }
+    { T.101-G2 iso-ir-128 csISO128T101G2 }
+    { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew
+      csISOLatinHebrew }
+    { ISO_8859-8-E csISO88598E ISO-8859-8-E }
+    { ISO_8859-8-I csISO88598I ISO-8859-8-I }
+    { CSN_369103 iso-ir-139 csISO139CSN369103 }
+    { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 }
+    { ISO_6937-2-add iso-ir-142 csISOTextComm }
+    { IEC_P27-1 iso-ir-143 csISO143IECP271 }
+    { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic
+      csISOLatinCyrillic }
+    { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian }
+    { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian }
+    { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 }
+    { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT }
+    { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba }
+    { ISO_6937-2-25 iso-ir-152 csISO6937Add }
+    { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 }
+    { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp }
+    { ISO_10367-box iso-ir-155 csISO10367Box }
+    { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 }
+    { latin-lap lap iso-ir-158 csISO158Lap }
+    { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 }
+    { DS_2089 DS2089 ISO646-DK dk csISO646Danish }
+    { us-dk csUSDK }
+    { dk-us csDKUS }
+    { JIS_X0201 X0201 csHalfWidthKatakana }
+    { KSC5636 ISO646-KR csKSC5636 }
+    { ISO-10646-UCS-2 csUnicode }
+    { ISO-10646-UCS-4 csUCS4 }
+    { DEC-MCS dec csDECMCS }
+    { hp-roman8 roman8 r8 csHPRoman8 }
+    { macintosh mac csMacintosh }
+    { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl
+      csIBM037 }
+    { IBM038 EBCDIC-INT cp038 csIBM038 }
+    { IBM273 CP273 csIBM273 }
+    { IBM274 EBCDIC-BE CP274 csIBM274 }
+    { IBM275 EBCDIC-BR cp275 csIBM275 }
+    { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 }
+    { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 }
+    { IBM280 CP280 ebcdic-cp-it csIBM280 }
+    { IBM281 EBCDIC-JP-E cp281 csIBM281 }
+    { IBM284 CP284 ebcdic-cp-es csIBM284 }
+    { IBM285 CP285 ebcdic-cp-gb csIBM285 }
+    { IBM290 cp290 EBCDIC-JP-kana csIBM290 }
+    { IBM297 cp297 ebcdic-cp-fr csIBM297 }
+    { IBM420 cp420 ebcdic-cp-ar1 csIBM420 }
+    { IBM423 cp423 ebcdic-cp-gr csIBM423 }
+    { IBM424 cp424 ebcdic-cp-he csIBM424 }
+    { IBM437 cp437 437 csPC8CodePage437 }
+    { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 }
+    { IBM775 cp775 csPC775Baltic }
+    { IBM850 cp850 850 csPC850Multilingual }
+    { IBM851 cp851 851 csIBM851 }
+    { IBM852 cp852 852 csPCp852 }
+    { IBM855 cp855 855 csIBM855 }
+    { IBM857 cp857 857 csIBM857 }
+    { IBM860 cp860 860 csIBM860 }
+    { IBM861 cp861 861 cp-is csIBM861 }
+    { IBM862 cp862 862 csPC862LatinHebrew }
+    { IBM863 cp863 863 csIBM863 }
+    { IBM864 cp864 csIBM864 }
+    { IBM865 cp865 865 csIBM865 }
+    { IBM866 cp866 866 csIBM866 }
+    { IBM868 CP868 cp-ar csIBM868 }
+    { IBM869 cp869 869 cp-gr csIBM869 }
+    { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 }
+    { IBM871 CP871 ebcdic-cp-is csIBM871 }
+    { IBM880 cp880 EBCDIC-Cyrillic csIBM880 }
+    { IBM891 cp891 csIBM891 }
+    { IBM903 cp903 csIBM903 }
+    { IBM904 cp904 904 csIBBM904 }
+    { IBM905 CP905 ebcdic-cp-tr csIBM905 }
+    { IBM918 CP918 ebcdic-cp-ar2 csIBM918 }
+    { IBM1026 CP1026 csIBM1026 }
+    { EBCDIC-AT-DE csIBMEBCDICATDE }
+    { EBCDIC-AT-DE-A csEBCDICATDEA }
+    { EBCDIC-CA-FR csEBCDICCAFR }
+    { EBCDIC-DK-NO csEBCDICDKNO }
+    { EBCDIC-DK-NO-A csEBCDICDKNOA }
+    { EBCDIC-FI-SE csEBCDICFISE }
+    { EBCDIC-FI-SE-A csEBCDICFISEA }
+    { EBCDIC-FR csEBCDICFR }
+    { EBCDIC-IT csEBCDICIT }
+    { EBCDIC-PT csEBCDICPT }
+    { EBCDIC-ES csEBCDICES }
+    { EBCDIC-ES-A csEBCDICESA }
+    { EBCDIC-ES-S csEBCDICESS }
+    { EBCDIC-UK csEBCDICUK }
+    { EBCDIC-US csEBCDICUS }
+    { UNKNOWN-8BIT csUnknown8BiT }
+    { MNEMONIC csMnemonic }
+    { MNEM csMnem }
+    { VISCII csVISCII }
+    { VIQR csVIQR }
+    { KOI8-R csKOI8R }
+    { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro }
+    { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro }
+    { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro }
+    { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro }
+    { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro }
+    { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro }
+    { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro }
+    { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro }
+    { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro }
+    { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro }
+    { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro }
+    { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro }
+    { IBM1047 IBM-1047 }
+    { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian }
+    { Amiga-1251 Ami1251 Amiga1251 Ami-1251 }
+    { UNICODE-1-1 csUnicode11 }
+    { CESU-8 csCESU-8 }
+    { BOCU-1 csBOCU-1 }
+    { UNICODE-1-1-UTF-7 csUnicode11UTF7 }
+    { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic
+      l8 }
+    { ISO-8859-15 ISO_8859-15 Latin-9 }
+    { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 }
+    { GBK CP936 MS936 windows-936 }
+    { JIS_Encoding csJISEncoding }
+    { Shift_JIS MS_Kanji csShiftJIS }
+    { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese
+      EUC-JP }
+    { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese }
+    { ISO-10646-UCS-Basic csUnicodeASCII }
+    { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 }
+    { ISO-Unicode-IBM-1261 csUnicodeIBM1261 }
+    { ISO-Unicode-IBM-1268 csUnicodeIBM1268 }
+    { ISO-Unicode-IBM-1276 csUnicodeIBM1276 }
+    { ISO-Unicode-IBM-1264 csUnicodeIBM1264 }
+    { ISO-Unicode-IBM-1265 csUnicodeIBM1265 }
+    { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 }
+    { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 }
+    { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 }
+    { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 }
+    { Adobe-Standard-Encoding csAdobeStandardEncoding }
+    { Ventura-US csVenturaUS }
+    { Ventura-International csVenturaInternational }
+    { PC8-Danish-Norwegian csPC8DanishNorwegian }
+    { PC8-Turkish csPC8Turkish }
+    { IBM-Symbols csIBMSymbols }
+    { IBM-Thai csIBMThai }
+    { HP-Legal csHPLegal }
+    { HP-Pi-font csHPPiFont }
+    { HP-Math8 csHPMath8 }
+    { Adobe-Symbol-Encoding csHPPSMath }
+    { HP-DeskTop csHPDesktop }
+    { Ventura-Math csVenturaMath }
+    { Microsoft-Publishing csMicrosoftPublishing }
+    { Windows-31J csWindows31J }
+    { GB2312 csGB2312 }
+    { Big5 csBig5 }
+}
+
+proc tcl_encoding {enc} {
+    global encoding_aliases
+    set names [encoding names]
+    set lcnames [string tolower $names]
+    set enc [string tolower $enc]
+    set i [lsearch -exact $lcnames $enc]
+    if {$i < 0} {
+       # look for "isonnn" instead of "iso-nnn" or "iso_nnn"
+       if {[regsub {^iso[-_]} $enc iso encx]} {
+           set i [lsearch -exact $lcnames $encx]
+       }
+    }
+    if {$i < 0} {
+       foreach l $encoding_aliases {
+           set ll [string tolower $l]
+           if {[lsearch -exact $ll $enc] < 0} continue
+           # look through the aliases for one that tcl knows about
+           foreach e $ll {
+               set i [lsearch -exact $lcnames $e]
+               if {$i < 0} {
+                   if {[regsub {^iso[-_]} $e iso ex]} {
+                       set i [lsearch -exact $lcnames $ex]
+                   }
+               }
+               if {$i >= 0} break
+           }
+           break
+       }
+    }
+    if {$i >= 0} {
+       return [lindex $names $i]
+    }
+    return {}
+}
diff --git a/git-gui/lib/error.tcl b/git-gui/lib/error.tcl
new file mode 100644 (file)
index 0000000..13565b7
--- /dev/null
@@ -0,0 +1,104 @@
+# git-gui branch (create/delete) support
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc error_popup {msg} {
+       set title [appname]
+       if {[reponame] ne {}} {
+               append title " ([reponame])"
+       }
+       set cmd [list tk_messageBox \
+               -icon error \
+               -type ok \
+               -title [append "$title: " [mc "error"]] \
+               -message $msg]
+       if {[winfo ismapped .]} {
+               lappend cmd -parent .
+       }
+       eval $cmd
+}
+
+proc warn_popup {msg} {
+       set title [appname]
+       if {[reponame] ne {}} {
+               append title " ([reponame])"
+       }
+       set cmd [list tk_messageBox \
+               -icon warning \
+               -type ok \
+               -title [append "$title: " [mc "warning"]] \
+               -message $msg]
+       if {[winfo ismapped .]} {
+               lappend cmd -parent .
+       }
+       eval $cmd
+}
+
+proc info_popup {msg {parent .}} {
+       set title [appname]
+       if {[reponame] ne {}} {
+               append title " ([reponame])"
+       }
+       tk_messageBox \
+               -parent $parent \
+               -icon info \
+               -type ok \
+               -title $title \
+               -message $msg
+}
+
+proc ask_popup {msg} {
+       set title [appname]
+       if {[reponame] ne {}} {
+               append title " ([reponame])"
+       }
+       set cmd [list tk_messageBox \
+               -icon question \
+               -type yesno \
+               -title $title \
+               -message $msg]
+       if {[winfo ismapped .]} {
+               lappend cmd -parent .
+       }
+       eval $cmd
+}
+
+proc hook_failed_popup {hook msg} {
+       set w .hookfail
+       toplevel $w
+
+       frame $w.m
+       label $w.m.l1 -text "$hook hook failed:" \
+               -anchor w \
+               -justify left \
+               -font font_uibold
+       text $w.m.t \
+               -background white -borderwidth 1 \
+               -relief sunken \
+               -width 80 -height 10 \
+               -font font_diff \
+               -yscrollcommand [list $w.m.sby set]
+       label $w.m.l2 \
+               -text [mc "You must correct the above errors before committing."] \
+               -anchor w \
+               -justify left \
+               -font font_uibold
+       scrollbar $w.m.sby -command [list $w.m.t yview]
+       pack $w.m.l1 -side top -fill x
+       pack $w.m.l2 -side bottom -fill x
+       pack $w.m.sby -side right -fill y
+       pack $w.m.t -side left -fill both -expand 1
+       pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
+
+       $w.m.t insert 1.0 $msg
+       $w.m.t conf -state disabled
+
+       button $w.ok -text OK \
+               -width 15 \
+               -command "destroy $w"
+       pack $w.ok -side bottom -anchor e -pady 10 -padx 10
+
+       bind $w <Visibility> "grab $w; focus $w"
+       bind $w <Key-Return> "destroy $w"
+       wm title $w [append "[appname] ([reponame]): " [mc "error"]]
+       tkwait window $w
+}
diff --git a/git-gui/lib/git-gui.ico b/git-gui/lib/git-gui.ico
new file mode 100644 (file)
index 0000000..334cfa5
Binary files /dev/null and b/git-gui/lib/git-gui.ico differ
diff --git a/git-gui/lib/index.tcl b/git-gui/lib/index.tcl
new file mode 100644 (file)
index 0000000..a0b22f2
--- /dev/null
@@ -0,0 +1,435 @@
+# git-gui index (add/remove) support
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc _delete_indexlock {} {
+       if {[catch {file delete -- [gitdir index.lock]} err]} {
+               error_popup [strcat [mc "Unable to unlock the index."] "\n\n$err"]
+       }
+}
+
+proc _close_updateindex {fd after} {
+       fconfigure $fd -blocking 1
+       if {[catch {close $fd} err]} {
+               set w .indexfried
+               toplevel $w
+               wm title $w [strcat "[appname] ([reponame]): " [mc "Index Error"]]
+               wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
+               pack [label $w.msg \
+                       -justify left \
+                       -anchor w \
+                       -text [strcat \
+                               [mc "Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui."] \
+                               "\n\n$err"] \
+                       ] -anchor w
+
+               frame $w.buttons
+               button $w.buttons.continue \
+                       -text [mc "Continue"] \
+                       -command [list destroy $w]
+               pack $w.buttons.continue -side right -padx 5
+               button $w.buttons.unlock \
+                       -text [mc "Unlock Index"] \
+                       -command "destroy $w; _delete_indexlock"
+               pack $w.buttons.unlock -side right
+               pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+               wm protocol $w WM_DELETE_WINDOW update
+               bind $w.buttons.continue <Visibility> "
+                       grab $w
+                       focus $w.buttons.continue
+               "
+               tkwait window $w
+
+               $::main_status stop
+               unlock_index
+               rescan $after 0
+               return
+       }
+
+       $::main_status stop
+       unlock_index
+       uplevel #0 $after
+}
+
+proc update_indexinfo {msg pathList after} {
+       global update_index_cp
+
+       if {![lock_index update]} return
+
+       set update_index_cp 0
+       set pathList [lsort $pathList]
+       set totalCnt [llength $pathList]
+       set batch [expr {int($totalCnt * .01) + 1}]
+       if {$batch > 25} {set batch 25}
+
+       $::main_status start $msg [mc "files"]
+       set fd [git_write update-index -z --index-info]
+       fconfigure $fd \
+               -blocking 0 \
+               -buffering full \
+               -buffersize 512 \
+               -encoding binary \
+               -translation binary
+       fileevent $fd writable [list \
+               write_update_indexinfo \
+               $fd \
+               $pathList \
+               $totalCnt \
+               $batch \
+               $after \
+               ]
+}
+
+proc write_update_indexinfo {fd pathList totalCnt batch after} {
+       global update_index_cp
+       global file_states current_diff_path
+
+       if {$update_index_cp >= $totalCnt} {
+               _close_updateindex $fd $after
+               return
+       }
+
+       for {set i $batch} \
+               {$update_index_cp < $totalCnt && $i > 0} \
+               {incr i -1} {
+               set path [lindex $pathList $update_index_cp]
+               incr update_index_cp
+
+               set s $file_states($path)
+               switch -glob -- [lindex $s 0] {
+               A? {set new _O}
+               M? {set new _M}
+               D_ {set new _D}
+               D? {set new _?}
+               ?? {continue}
+               }
+               set info [lindex $s 2]
+               if {$info eq {}} continue
+
+               puts -nonewline $fd "$info\t[encoding convertto $path]\0"
+               display_file $path $new
+       }
+
+       $::main_status update $update_index_cp $totalCnt
+}
+
+proc update_index {msg pathList after} {
+       global update_index_cp
+
+       if {![lock_index update]} return
+
+       set update_index_cp 0
+       set pathList [lsort $pathList]
+       set totalCnt [llength $pathList]
+       set batch [expr {int($totalCnt * .01) + 1}]
+       if {$batch > 25} {set batch 25}
+
+       $::main_status start $msg [mc "files"]
+       set fd [git_write update-index --add --remove -z --stdin]
+       fconfigure $fd \
+               -blocking 0 \
+               -buffering full \
+               -buffersize 512 \
+               -encoding binary \
+               -translation binary
+       fileevent $fd writable [list \
+               write_update_index \
+               $fd \
+               $pathList \
+               $totalCnt \
+               $batch \
+               $after \
+               ]
+}
+
+proc write_update_index {fd pathList totalCnt batch after} {
+       global update_index_cp
+       global file_states current_diff_path
+
+       if {$update_index_cp >= $totalCnt} {
+               _close_updateindex $fd $after
+               return
+       }
+
+       for {set i $batch} \
+               {$update_index_cp < $totalCnt && $i > 0} \
+               {incr i -1} {
+               set path [lindex $pathList $update_index_cp]
+               incr update_index_cp
+
+               switch -glob -- [lindex $file_states($path) 0] {
+               AD {set new __}
+               ?D {set new D_}
+               _O -
+               AM {set new A_}
+               U? {
+                       if {[file exists $path]} {
+                               set new M_
+                       } else {
+                               set new D_
+                       }
+               }
+               ?M {set new M_}
+               ?? {continue}
+               }
+               puts -nonewline $fd "[encoding convertto $path]\0"
+               display_file $path $new
+       }
+
+       $::main_status update $update_index_cp $totalCnt
+}
+
+proc checkout_index {msg pathList after} {
+       global update_index_cp
+
+       if {![lock_index update]} return
+
+       set update_index_cp 0
+       set pathList [lsort $pathList]
+       set totalCnt [llength $pathList]
+       set batch [expr {int($totalCnt * .01) + 1}]
+       if {$batch > 25} {set batch 25}
+
+       $::main_status start $msg [mc "files"]
+       set fd [git_write checkout-index \
+               --index \
+               --quiet \
+               --force \
+               -z \
+               --stdin \
+               ]
+       fconfigure $fd \
+               -blocking 0 \
+               -buffering full \
+               -buffersize 512 \
+               -encoding binary \
+               -translation binary
+       fileevent $fd writable [list \
+               write_checkout_index \
+               $fd \
+               $pathList \
+               $totalCnt \
+               $batch \
+               $after \
+               ]
+}
+
+proc write_checkout_index {fd pathList totalCnt batch after} {
+       global update_index_cp
+       global file_states current_diff_path
+
+       if {$update_index_cp >= $totalCnt} {
+               _close_updateindex $fd $after
+               return
+       }
+
+       for {set i $batch} \
+               {$update_index_cp < $totalCnt && $i > 0} \
+               {incr i -1} {
+               set path [lindex $pathList $update_index_cp]
+               incr update_index_cp
+               switch -glob -- [lindex $file_states($path) 0] {
+               U? {continue}
+               ?M -
+               ?D {
+                       puts -nonewline $fd "[encoding convertto $path]\0"
+                       display_file $path ?_
+               }
+               }
+       }
+
+       $::main_status update $update_index_cp $totalCnt
+}
+
+proc unstage_helper {txt paths} {
+       global file_states current_diff_path
+
+       if {![lock_index begin-update]} return
+
+       set pathList [list]
+       set after {}
+       foreach path $paths {
+               switch -glob -- [lindex $file_states($path) 0] {
+               A? -
+               M? -
+               D? {
+                       lappend pathList $path
+                       if {$path eq $current_diff_path} {
+                               set after {reshow_diff;}
+                       }
+               }
+               }
+       }
+       if {$pathList eq {}} {
+               unlock_index
+       } else {
+               update_indexinfo \
+                       $txt \
+                       $pathList \
+                       [concat $after [list ui_ready]]
+       }
+}
+
+proc do_unstage_selection {} {
+       global current_diff_path selected_paths
+
+       if {[array size selected_paths] > 0} {
+               unstage_helper \
+                       {Unstaging selected files from commit} \
+                       [array names selected_paths]
+       } elseif {$current_diff_path ne {}} {
+               unstage_helper \
+                       [mc "Unstaging %s from commit" [short_path $current_diff_path]] \
+                       [list $current_diff_path]
+       }
+}
+
+proc add_helper {txt paths} {
+       global file_states current_diff_path
+
+       if {![lock_index begin-update]} return
+
+       set pathList [list]
+       set after {}
+       foreach path $paths {
+               switch -glob -- [lindex $file_states($path) 0] {
+               _O -
+               ?M -
+               ?D -
+               U? {
+                       lappend pathList $path
+                       if {$path eq $current_diff_path} {
+                               set after {reshow_diff;}
+                       }
+               }
+               }
+       }
+       if {$pathList eq {}} {
+               unlock_index
+       } else {
+               update_index \
+                       $txt \
+                       $pathList \
+                       [concat $after {ui_status {Ready to commit.}}]
+       }
+}
+
+proc do_add_selection {} {
+       global current_diff_path selected_paths
+
+       if {[array size selected_paths] > 0} {
+               add_helper \
+                       {Adding selected files} \
+                       [array names selected_paths]
+       } elseif {$current_diff_path ne {}} {
+               add_helper \
+                       [mc "Adding %s" [short_path $current_diff_path]] \
+                       [list $current_diff_path]
+       }
+}
+
+proc do_add_all {} {
+       global file_states
+
+       set paths [list]
+       foreach path [array names file_states] {
+               switch -glob -- [lindex $file_states($path) 0] {
+               U? {continue}
+               ?M -
+               ?D {lappend paths $path}
+               }
+       }
+       add_helper {Adding all changed files} $paths
+}
+
+proc revert_helper {txt paths} {
+       global file_states current_diff_path
+
+       if {![lock_index begin-update]} return
+
+       set pathList [list]
+       set after {}
+       foreach path $paths {
+               switch -glob -- [lindex $file_states($path) 0] {
+               U? {continue}
+               ?M -
+               ?D {
+                       lappend pathList $path
+                       if {$path eq $current_diff_path} {
+                               set after {reshow_diff;}
+                       }
+               }
+               }
+       }
+
+
+       # Split question between singular and plural cases, because
+       # such distinction is needed in some languages. Previously, the
+       # code used "Revert changes in" for both, but that can't work
+       # in languages where 'in' must be combined with word from
+       # rest of string (in diffrent way for both cases of course).
+       #
+       # FIXME: Unfortunately, even that isn't enough in some languages
+       # as they have quite complex plural-form rules. Unfortunately,
+       # msgcat doesn't seem to support that kind of string translation.
+       #
+       set n [llength $pathList]
+       if {$n == 0} {
+               unlock_index
+               return
+       } elseif {$n == 1} {
+               set query [mc "Revert changes in file %s?" [short_path [lindex $pathList]]]
+       } else {
+               set query [mc "Revert changes in these %i files?" $n]
+       }
+
+       set reply [tk_dialog \
+               .confirm_revert \
+               "[appname] ([reponame])" \
+               [mc "Any unstaged changes will be permanently lost by the revert."] \
+               question \
+               1 \
+               [mc "Do Nothing"] \
+               [mc "Revert Changes"] \
+               ]
+       if {$reply == 1} {
+               checkout_index \
+                       $txt \
+                       $pathList \
+                       [concat $after [list ui_ready]]
+       } else {
+               unlock_index
+       }
+}
+
+proc do_revert_selection {} {
+       global current_diff_path selected_paths
+
+       if {[array size selected_paths] > 0} {
+               revert_helper \
+                       {Reverting selected files} \
+                       [array names selected_paths]
+       } elseif {$current_diff_path ne {}} {
+               revert_helper \
+                       "Reverting [short_path $current_diff_path]" \
+                       [list $current_diff_path]
+       }
+}
+
+proc do_select_commit_type {} {
+       global commit_type selected_commit_type
+
+       if {$selected_commit_type eq {new}
+               && [string match amend* $commit_type]} {
+               create_new_commit
+       } elseif {$selected_commit_type eq {amend}
+               && ![string match amend* $commit_type]} {
+               load_last_commit
+
+               # The amend request was rejected...
+               #
+               if {![string match amend* $commit_type]} {
+                       set selected_commit_type new
+               }
+       }
+}
diff --git a/git-gui/lib/logo.tcl b/git-gui/lib/logo.tcl
new file mode 100644 (file)
index 0000000..5ff7669
--- /dev/null
@@ -0,0 +1,43 @@
+# git-gui Git Gui logo
+# Copyright (C) 2007 Shawn Pearce
+
+# Henrik Nyh's alternative Git logo, from his blog post
+# http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon
+#
+image create photo ::git_logo_data -data {
+R0lGODdhYQC8AIQbAGZmZtg4LW9vb3l5eYKCgoyMjEC/TOJpYZWVlZ+fn2/PeKmpqbKysry8vMXF
+xZ/fpc/Pz7fnvPXNytnZ2eLi4s/v0vja1+zs7Of36fX19f3z8v///////////////////ywAAAAA
+YQC8AAAF/uAmjmRpnmiqrmzrvq4hz3RtGw+s7zx5/7dcb0hUAY8zYXHJRCKVzGjPeYRKry8q0Irt
+GrVBr3gFDo/PprKNix6ra+y2902Ly7H05L2dl9n3UX04gGeCf4RFhohiiotdjY5XkJGBfYeUOpOY
+iZablXmXURgPpKWmp6ipqYIKqq6vqREjFYK1trUKs7e7vFq5IrS9wsM0vxvBxMm8xsjKzqy6z9J5
+zNPWatXX2k7Z29433d/iMuHj3+Xm2+jp1+vs0+7vz/HyyvT1xPf4wvr7y9H+pBkbBasgLFYGE8ba
+o8nTlE4OOYGKKJFOKIopGmLMAnHjDo0eWYAM+WUiSRgj/k+eSKmyBMuWI17C3CATZs2WN1XmPLmT
+ZM+QPz0G3VihqNGjSJNWwDCzqdOnUKPu0SChqtWrWLNq3cq1q9evYCVYGCEhgNmzaNOqXcu2rdu3
+cOMGOEBWrt27ePPCpSuirN6/gAO35bvBr+DDiPMSNpy4sWO2ix9Lnmw2MuXLiS1j3gxYM+fPdz2D
+Hv1WNOnTak2jXj23LuvXlV3DZq16Nujatjnjzo15N2/Kvn9LDi7cMfHimaUqX868ufPn0KPPpOCA
+AQMWCQBo3869u/fv4MNrd3DlQoMC3QlkSJFdvPv38LVDWJLBAYHwE1LE38+/+/UhGTAggHv5odDf
+gfv9/seDgPAVeAKCELqnIAwU3BefgyZEqOF3E7rAQH8YlrDhiNt1uEIG6IGoH4kjmpjCBRaqaCCL
+G7p4AgUDIhgiCTTW2AKOEe44Qo8a2khCBgNoKKQIREZopAgZxAjhkhs0CeGTG7Sn5IpW9vekAyRS
+2eWBRl6Q44ZijhlfAQlQmeKIaarpHZsMTHABCxDQGKec3JH3QpIs7snndn6yAKaeXA7aZwuABppo
+fAws0GiEhaKQJ40F3DkjfwVC8CaCAlCgAgIkJjDfCgdiOMGn/Q2w3gkZtPgqC6ma0ECECaBwa4QE
+aOpCrSYAqeMJpEKYqw7ABnsmfwQ8aCwPySqLYKUb/kwAYbPQyoiCtQcOUMKHBwrgK7LaogBuuaxC
+OkS0KEwa37EiLBufALPuwO4Jh/InwAixkknEvSe4C9+p3PY3rr3lpnDufguIcCmzRQAc7IHYLhxf
+w/8mnILA74lg8cARa4xCsZxusMCBomZccgsfv0deuh2HvLKh/sLs3hJSvieuCwUzvIHN4tGXc3ih
+vtDzmj8fSNLR8BWQdH9LH+g00OFF3d/UBx4cUcvuOc21eFRiouV+Xvvr0dDvlX21R/2uzTR89TqU
+L3+5UoBgAxtRHd5/CHpLkd13i4D2e3hHRLKMY+9Hr0Nvx/fq3Pw57cng7/m9wQVObnIyhAiQwHF8
+/tQS8nDgI2wOYeh3CAvhuIBHiDEgqvdtwudkaz3GBPKaTcKuGgqAJRMZmK6h1hnk3ncDcUvhgPFS
+o5B476ZKQcECzCN4qgmYN4lAncmzcAEEkhJp+QlfkyhAAdtbN8H67FvHQAF6b4g6v9UryqfkKkBu
+v/0prxD//kR63YnqB8AeqcdoBRxU/1zAuwRaaX4reJ4DSSRAHUhwgrgqwgUx2B94EWGDHISPBzUY
+QgSNcAn6K6F4fscDCtBOhdoRwPW6kIHDwZA7vWoDBF44Qd/tIUAEBCACbIeG4AXxfmFrQ4B4OCYE
+JBEQELChmgbAACJioj4JOCKCCLCABZ6EAg1IHwDlyLYAB1gRJhSYgHUQAD9WnQ9+CWBAA+wknTpC
+JwQAOw==
+}
+
+proc git_logo {w} {
+       label $w \
+               -borderwidth 1 \
+               -relief sunken \
+               -background white \
+               -image ::git_logo_data
+       return $w
+}
diff --git a/git-gui/lib/merge.tcl b/git-gui/lib/merge.tcl
new file mode 100644 (file)
index 0000000..63e1427
--- /dev/null
@@ -0,0 +1,274 @@
+# git-gui branch merge support
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+class merge {
+
+field w         ; # top level window
+field w_rev     ; # mega-widget to pick the revision to merge
+
+method _can_merge {} {
+       global HEAD commit_type file_states
+
+       if {[string match amend* $commit_type]} {
+               info_popup [mc "Cannot merge while amending.
+
+You must finish amending this commit before starting any type of merge.
+"]
+               return 0
+       }
+
+       if {[committer_ident] eq {}} {return 0}
+       if {![lock_index merge]} {return 0}
+
+       # -- Our in memory state should match the repository.
+       #
+       repository_state curType curHEAD curMERGE_HEAD
+       if {$commit_type ne $curType || $HEAD ne $curHEAD} {
+               info_popup [mc "Last scanned state does not match repository state.
+
+Another Git program has modified this repository since the last scan.  A rescan must be performed before a merge can be performed.
+
+The rescan will be automatically started now.
+"]
+               unlock_index
+               rescan ui_ready
+               return 0
+       }
+
+       foreach path [array names file_states] {
+               switch -glob -- [lindex $file_states($path) 0] {
+               _O {
+                       continue; # and pray it works!
+               }
+               U? {
+                       error_popup [mc "You are in the middle of a conflicted merge.
+
+File %s has merge conflicts.
+
+You must resolve them, stage the file, and commit to complete the current merge.  Only then can you begin another merge.
+" [short_path $path]]
+                       unlock_index
+                       return 0
+               }
+               ?? {
+                       error_popup [mc "You are in the middle of a change.
+
+File %s is modified.
+
+You should complete the current commit before starting a merge.  Doing so will help you abort a failed merge, should the need arise.
+" [short_path $path]]
+                       unlock_index
+                       return 0
+               }
+               }
+       }
+
+       return 1
+}
+
+method _rev {} {
+       if {[catch {$w_rev commit_or_die}]} {
+               return {}
+       }
+       return [$w_rev get]
+}
+
+method _visualize {} {
+       set rev [_rev $this]
+       if {$rev ne {}} {
+               do_gitk [list $rev --not HEAD]
+       }
+}
+
+method _start {} {
+       global HEAD current_branch remote_url
+
+       set name [_rev $this]
+       if {$name eq {}} {
+               return
+       }
+
+       set spec [$w_rev get_tracking_branch]
+       set cmit [$w_rev get_commit]
+
+       set fh [open [gitdir FETCH_HEAD] w]
+       fconfigure $fh -translation lf
+       if {$spec eq {}} {
+               set remote .
+               set branch $name
+               set stitle $branch
+       } else {
+               set remote $remote_url([lindex $spec 1])
+               if {[regexp {^[^:@]*@[^:]*:/} $remote]} {
+                       regsub {^[^:@]*@} $remote {} remote
+               }
+               set branch [lindex $spec 2]
+               set stitle [mc "%s of %s" $branch $remote]
+       }
+       regsub ^refs/heads/ $branch {} branch
+       puts $fh "$cmit\t\tbranch '$branch' of $remote"
+       close $fh
+
+       set cmd [list git]
+       lappend cmd merge
+       lappend cmd --strategy=recursive
+       lappend cmd [git fmt-merge-msg <[gitdir FETCH_HEAD]]
+       lappend cmd HEAD
+       lappend cmd $name
+
+       set msg [mc "Merging %s and %s" $current_branch $stitle]
+       ui_status "$msg..."
+       set cons [console::new [mc "Merge"] "merge $stitle"]
+       console::exec $cons $cmd [cb _finish $cons]
+
+       wm protocol $w WM_DELETE_WINDOW {}
+       destroy $w
+}
+
+method _finish {cons ok} {
+       console::done $cons $ok
+       if {$ok} {
+               set msg [mc "Merge completed successfully."]
+       } else {
+               set msg [mc "Merge failed.  Conflict resolution is required."]
+       }
+       unlock_index
+       rescan [list ui_status $msg]
+       delete_this
+}
+
+constructor dialog {} {
+       global current_branch
+       global M1B
+
+       if {![_can_merge $this]} {
+               delete_this
+               return
+       }
+
+       make_toplevel top w
+       wm title $top [append "[appname] ([reponame]): " [mc "Merge"]]
+       if {$top ne {.}} {
+               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
+       }
+
+       set _start [cb _start]
+
+       label $w.header \
+               -text [mc "Merge Into %s" $current_branch] \
+               -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       button $w.buttons.visualize \
+               -text [mc Visualize] \
+               -command [cb _visualize]
+       pack $w.buttons.visualize -side left
+       button $w.buttons.merge \
+               -text [mc Merge] \
+               -command $_start
+       pack $w.buttons.merge -side right
+       button $w.buttons.cancel \
+               -text [mc "Cancel"] \
+               -command [cb _cancel]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       set w_rev [::choose_rev::new_unmerged $w.rev [mc "Revision To Merge"]]
+       pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5
+
+       bind $w <$M1B-Key-Return> $_start
+       bind $w <Key-Return> $_start
+       bind $w <Key-Escape> [cb _cancel]
+       wm protocol $w WM_DELETE_WINDOW [cb _cancel]
+
+       bind $w.buttons.merge <Visibility> [cb _visible]
+       tkwait window $w
+}
+
+method _visible {} {
+       grab $w
+       if {[is_config_true gui.matchtrackingbranch]} {
+               $w_rev pick_tracking_branch
+       }
+       $w_rev focus_filter
+}
+
+method _cancel {} {
+       wm protocol $w WM_DELETE_WINDOW {}
+       unlock_index
+       destroy $w
+       delete_this
+}
+
+}
+
+namespace eval merge {
+
+proc reset_hard {} {
+       global HEAD commit_type file_states
+
+       if {[string match amend* $commit_type]} {
+               info_popup [mc "Cannot abort while amending.
+
+You must finish amending this commit.
+"]
+               return
+       }
+
+       if {![lock_index abort]} return
+
+       if {[string match *merge* $commit_type]} {
+               set op_question [mc "Abort merge?
+
+Aborting the current merge will cause *ALL* uncommitted changes to be lost.
+
+Continue with aborting the current merge?"]
+       } else {
+               set op_question [mc "Reset changes?
+
+Resetting the changes will cause *ALL* uncommitted changes to be lost.
+
+Continue with resetting the current changes?"]
+       }
+
+       if {[ask_popup $op_question] eq {yes}} {
+               set fd [git_read --stderr read-tree --reset -u -v HEAD]
+               fconfigure $fd -blocking 0 -translation binary
+               fileevent $fd readable [namespace code [list _reset_wait $fd]]
+               $::main_status start [mc "Aborting"] {files reset}
+       } else {
+               unlock_index
+       }
+}
+
+proc _reset_wait {fd} {
+       global ui_comm
+
+       $::main_status update_meter [read $fd]
+
+       fconfigure $fd -blocking 1
+       if {[eof $fd]} {
+               set fail [catch {close $fd} err]
+               $::main_status stop
+               unlock_index
+
+               $ui_comm delete 0.0 end
+               $ui_comm edit modified false
+
+               catch {file delete [gitdir MERGE_HEAD]}
+               catch {file delete [gitdir rr-cache MERGE_RR]}
+               catch {file delete [gitdir SQUASH_MSG]}
+               catch {file delete [gitdir MERGE_MSG]}
+               catch {file delete [gitdir GITGUI_MSG]}
+
+               if {$fail} {
+                       warn_popup "[mc "Abort failed."]\n\n$err"
+               }
+               rescan {ui_status [mc "Abort completed.  Ready."]}
+       } else {
+               fconfigure $fd -blocking 0
+       }
+}
+
+}
diff --git a/git-gui/lib/option.tcl b/git-gui/lib/option.tcl
new file mode 100644 (file)
index 0000000..f812e5e
--- /dev/null
@@ -0,0 +1,239 @@
+# git-gui options editor
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc save_config {} {
+       global default_config font_descs
+       global repo_config global_config
+       global repo_config_new global_config_new
+
+       foreach option $font_descs {
+               set name [lindex $option 0]
+               set font [lindex $option 1]
+               font configure $font \
+                       -family $global_config_new(gui.$font^^family) \
+                       -size $global_config_new(gui.$font^^size)
+               font configure ${font}bold \
+                       -family $global_config_new(gui.$font^^family) \
+                       -size $global_config_new(gui.$font^^size)
+               font configure ${font}italic \
+                       -family $global_config_new(gui.$font^^family) \
+                       -size $global_config_new(gui.$font^^size)
+               set global_config_new(gui.$name) [font configure $font]
+               unset global_config_new(gui.$font^^family)
+               unset global_config_new(gui.$font^^size)
+       }
+
+       foreach name [array names default_config] {
+               set value $global_config_new($name)
+               if {$value ne $global_config($name)} {
+                       if {$value eq $default_config($name)} {
+                               catch {git config --global --unset $name}
+                       } else {
+                               regsub -all "\[{}\]" $value {"} value
+                               git config --global $name $value
+                       }
+                       set global_config($name) $value
+                       if {$value eq $repo_config($name)} {
+                               catch {git config --unset $name}
+                               set repo_config($name) $value
+                       }
+               }
+       }
+
+       foreach name [array names default_config] {
+               set value $repo_config_new($name)
+               if {$value ne $repo_config($name)} {
+                       if {$value eq $global_config($name)} {
+                               catch {git config --unset $name}
+                       } else {
+                               regsub -all "\[{}\]" $value {"} value
+                               git config $name $value
+                       }
+                       set repo_config($name) $value
+               }
+       }
+}
+
+proc do_options {} {
+       global repo_config global_config font_descs
+       global repo_config_new global_config_new
+
+       array unset repo_config_new
+       array unset global_config_new
+       foreach name [array names repo_config] {
+               set repo_config_new($name) $repo_config($name)
+       }
+       load_config 1
+       foreach name [array names repo_config] {
+               switch -- $name {
+               gui.diffcontext {continue}
+               }
+               set repo_config_new($name) $repo_config($name)
+       }
+       foreach name [array names global_config] {
+               set global_config_new($name) $global_config($name)
+       }
+
+       set w .options_editor
+       toplevel $w
+       wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
+
+       frame $w.buttons
+       button $w.buttons.restore -text [mc "Restore Defaults"] \
+               -default normal \
+               -command do_restore_defaults
+       pack $w.buttons.restore -side left
+       button $w.buttons.save -text [mc Save] \
+               -default active \
+               -command [list do_save_config $w]
+       pack $w.buttons.save -side right
+       button $w.buttons.cancel -text [mc "Cancel"] \
+               -default normal \
+               -command [list destroy $w]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       labelframe $w.repo -text [mc "%s Repository" [reponame]]
+       labelframe $w.global -text [mc "Global (All Repositories)"]
+       pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
+       pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
+
+       set optid 0
+       foreach option {
+               {t user.name {mc "User Name"}}
+               {t user.email {mc "Email Address"}}
+
+               {b merge.summary {mc "Summarize Merge Commits"}}
+               {i-1..5 merge.verbosity {mc "Merge Verbosity"}}
+               {b merge.diffstat {mc "Show Diffstat After Merge"}}
+
+               {b gui.trustmtime  {mc "Trust File Modification Timestamps"}}
+               {b gui.pruneduringfetch {mc "Prune Tracking Branches During Fetch"}}
+               {b gui.matchtrackingbranch {mc "Match Tracking Branches"}}
+               {i-0..99 gui.diffcontext {mc "Number of Diff Context Lines"}}
+               {t gui.newbranchtemplate {mc "New Branch Name Template"}}
+               } {
+               set type [lindex $option 0]
+               set name [lindex $option 1]
+               set text [eval [lindex $option 2]]
+               incr optid
+               foreach f {repo global} {
+                       switch -glob -- $type {
+                       b {
+                               checkbutton $w.$f.$optid -text $text \
+                                       -variable ${f}_config_new($name) \
+                                       -onvalue true \
+                                       -offvalue false
+                               pack $w.$f.$optid -side top -anchor w
+                       }
+                       i-* {
+                               regexp -- {-(\d+)\.\.(\d+)$} $type _junk min max
+                               frame $w.$f.$optid
+                               label $w.$f.$optid.l -text "$text:"
+                               pack $w.$f.$optid.l -side left -anchor w -fill x
+                               spinbox $w.$f.$optid.v \
+                                       -textvariable ${f}_config_new($name) \
+                                       -from $min \
+                                       -to $max \
+                                       -increment 1 \
+                                       -width [expr {1 + [string length $max]}]
+                               bind $w.$f.$optid.v <FocusIn> {%W selection range 0 end}
+                               pack $w.$f.$optid.v -side right -anchor e -padx 5
+                               pack $w.$f.$optid -side top -anchor w -fill x
+                       }
+                       t {
+                               frame $w.$f.$optid
+                               label $w.$f.$optid.l -text "$text:"
+                               entry $w.$f.$optid.v \
+                                       -borderwidth 1 \
+                                       -relief sunken \
+                                       -width 20 \
+                                       -textvariable ${f}_config_new($name)
+                               pack $w.$f.$optid.l -side left -anchor w
+                               pack $w.$f.$optid.v -side left -anchor w \
+                                       -fill x -expand 1 \
+                                       -padx 5
+                               pack $w.$f.$optid -side top -anchor w -fill x
+                       }
+                       }
+               }
+       }
+
+       set all_fonts [lsort [font families]]
+       foreach option $font_descs {
+               set name [lindex $option 0]
+               set font [lindex $option 1]
+               set text [eval [lindex $option 2]]
+
+               set global_config_new(gui.$font^^family) \
+                       [font configure $font -family]
+               set global_config_new(gui.$font^^size) \
+                       [font configure $font -size]
+
+               frame $w.global.$name
+               label $w.global.$name.l -text "$text:"
+               button $w.global.$name.b \
+                       -text [mc "Change Font"] \
+                       -command [list \
+                               choose_font::pick \
+                               $w \
+                               [mc "Choose %s" $text] \
+                               global_config_new(gui.$font^^family) \
+                               global_config_new(gui.$font^^size) \
+                               ]
+               label $w.global.$name.f -textvariable global_config_new(gui.$font^^family)
+               label $w.global.$name.s -textvariable global_config_new(gui.$font^^size)
+               label $w.global.$name.pt -text [mc "pt."]
+               pack $w.global.$name.l -side left -anchor w
+               pack $w.global.$name.b -side right -anchor e
+               pack $w.global.$name.pt -side right -anchor w
+               pack $w.global.$name.s -side right -anchor w
+               pack $w.global.$name.f -side right -anchor w
+               pack $w.global.$name -side top -anchor w -fill x
+       }
+
+       bind $w <Visibility> "grab $w; focus $w.buttons.save"
+       bind $w <Key-Escape> "destroy $w"
+       bind $w <Key-Return> [list do_save_config $w]
+
+       if {[is_MacOSX]} {
+               set t [mc "Preferences"]
+       } else {
+               set t [mc "Options"]
+       }
+       wm title $w "[appname] ([reponame]): $t"
+       tkwait window $w
+}
+
+proc do_restore_defaults {} {
+       global font_descs default_config repo_config
+       global repo_config_new global_config_new
+
+       foreach name [array names default_config] {
+               set repo_config_new($name) $default_config($name)
+               set global_config_new($name) $default_config($name)
+       }
+
+       foreach option $font_descs {
+               set name [lindex $option 0]
+               set repo_config(gui.$name) $default_config(gui.$name)
+       }
+       apply_config
+
+       foreach option $font_descs {
+               set name [lindex $option 0]
+               set font [lindex $option 1]
+               set global_config_new(gui.$font^^family) \
+                       [font configure $font -family]
+               set global_config_new(gui.$font^^size) \
+                       [font configure $font -size]
+       }
+}
+
+proc do_save_config {w} {
+       if {[catch {save_config} err]} {
+               error_popup [strcat [mc "Failed to completely save options:"] "\n\n$err"]
+       }
+       reshow_diff
+       destroy $w
+}
diff --git a/git-gui/lib/remote.tcl b/git-gui/lib/remote.tcl
new file mode 100644 (file)
index 0000000..0e86dda
--- /dev/null
@@ -0,0 +1,222 @@
+# git-gui remote management
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+set some_heads_tracking 0;  # assume not
+
+proc is_tracking_branch {name} {
+       global tracking_branches
+       foreach spec $tracking_branches {
+               set t [lindex $spec 0]
+               if {$t eq $name || [string match $t $name]} {
+                       return 1
+               }
+       }
+       return 0
+}
+
+proc all_tracking_branches {} {
+       global tracking_branches
+
+       set all [list]
+       set pat [list]
+       set cmd [list]
+
+       foreach spec $tracking_branches {
+               set dst [lindex $spec 0]
+               if {[string range $dst end-1 end] eq {/*}} {
+                       lappend pat $spec
+                       lappend cmd [string range $dst 0 end-2]
+               } else {
+                       lappend all $spec
+               }
+       }
+
+       if {$pat ne {}} {
+               set fd [eval git_read for-each-ref --format=%(refname) $cmd]
+               while {[gets $fd n] > 0} {
+                       foreach spec $pat {
+                               set dst [string range [lindex $spec 0] 0 end-2]
+                               set len [string length $dst]
+                               if {[string equal -length $len $dst $n]} {
+                                       set src [string range [lindex $spec 2] 0 end-2]
+                                       set spec [list \
+                                               $n \
+                                               [lindex $spec 1] \
+                                               $src[string range $n $len end] \
+                                               ]
+                                       lappend all $spec
+                               }
+                       }
+               }
+               close $fd
+       }
+
+       return [lsort -index 0 -unique $all]
+}
+
+proc load_all_remotes {} {
+       global repo_config
+       global all_remotes tracking_branches some_heads_tracking
+       global remote_url
+
+       set some_heads_tracking 0
+       set all_remotes [list]
+       set trck [list]
+
+       set rh_str refs/heads/
+       set rh_len [string length $rh_str]
+       set rm_dir [gitdir remotes]
+       if {[file isdirectory $rm_dir]} {
+               set all_remotes [glob \
+                       -types f \
+                       -tails \
+                       -nocomplain \
+                       -directory $rm_dir *]
+
+               foreach name $all_remotes {
+                       catch {
+                               set fd [open [file join $rm_dir $name] r]
+                               while {[gets $fd line] >= 0} {
+                                       if {[regexp {^URL:[     ]*(.+)$} $line line url]} {
+                                               set remote_url($name) $url
+                                               continue
+                                       }
+                                       if {![regexp {^Pull:[   ]*([^:]+):(.+)$} \
+                                               $line line src dst]} continue
+                                       if {[string index $src 0] eq {+}} {
+                                               set src [string range $src 1 end]
+                                       }
+                                       if {![string equal -length 5 refs/ $src]} {
+                                               set src $rh_str$src
+                                       }
+                                       if {![string equal -length 5 refs/ $dst]} {
+                                               set dst $rh_str$dst
+                                       }
+                                       if {[string equal -length $rh_len $rh_str $dst]} {
+                                               set some_heads_tracking 1
+                                       }
+                                       lappend trck [list $dst $name $src]
+                               }
+                               close $fd
+                       }
+               }
+       }
+
+       foreach line [array names repo_config remote.*.url] {
+               if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue
+               lappend all_remotes $name
+               set remote_url($name) $repo_config(remote.$name.url)
+
+               if {[catch {set fl $repo_config(remote.$name.fetch)}]} {
+                       set fl {}
+               }
+               foreach line $fl {
+                       if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue
+                       if {[string index $src 0] eq {+}} {
+                               set src [string range $src 1 end]
+                       }
+                       if {![string equal -length 5 refs/ $src]} {
+                               set src $rh_str$src
+                       }
+                       if {![string equal -length 5 refs/ $dst]} {
+                               set dst $rh_str$dst
+                       }
+                       if {[string equal -length $rh_len $rh_str $dst]} {
+                               set some_heads_tracking 1
+                       }
+                       lappend trck [list $dst $name $src]
+               }
+       }
+
+       set tracking_branches [lsort -index 0 -unique $trck]
+       set all_remotes [lsort -unique $all_remotes]
+}
+
+proc populate_fetch_menu {} {
+       global all_remotes repo_config
+
+       set remote_m .mbar.remote
+       set fetch_m $remote_m.fetch
+       set prune_m $remote_m.prune
+
+       foreach r $all_remotes {
+               set enable 0
+               if {![catch {set a $repo_config(remote.$r.url)}]} {
+                       if {![catch {set a $repo_config(remote.$r.fetch)}]} {
+                               set enable 1
+                       }
+               } else {
+                       catch {
+                               set fd [open [gitdir remotes $r] r]
+                               while {[gets $fd n] >= 0} {
+                                       if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
+                                               set enable 1
+                                               break
+                                       }
+                               }
+                               close $fd
+                       }
+               }
+
+               if {$enable} {
+                       if {![winfo exists $fetch_m]} {
+                               menu $prune_m
+                               $remote_m insert 0 cascade \
+                                       -label [mc "Prune from"] \
+                                       -menu $prune_m
+
+                               menu $fetch_m
+                               $remote_m insert 0 cascade \
+                                       -label [mc "Fetch from"] \
+                                       -menu $fetch_m
+                       }
+
+                       $fetch_m add command \
+                               -label $r \
+                               -command [list fetch_from $r]
+                       $prune_m add command \
+                               -label $r \
+                               -command [list prune_from $r]
+               }
+       }
+}
+
+proc populate_push_menu {} {
+       global all_remotes repo_config
+
+       set remote_m .mbar.remote
+       set push_m $remote_m.push
+
+       foreach r $all_remotes {
+               set enable 0
+               if {![catch {set a $repo_config(remote.$r.url)}]} {
+                       if {![catch {set a $repo_config(remote.$r.push)}]} {
+                               set enable 1
+                       }
+               } else {
+                       catch {
+                               set fd [open [gitdir remotes $r] r]
+                               while {[gets $fd n] >= 0} {
+                                       if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
+                                               set enable 1
+                                               break
+                                       }
+                               }
+                               close $fd
+                       }
+               }
+
+               if {$enable} {
+                       if {![winfo exists $push_m]} {
+                               menu $push_m
+                               $remote_m insert 0 cascade \
+                                       -label [mc "Push to"] \
+                                       -menu $push_m
+                       }
+
+                       $push_m add command \
+                               -label $r \
+                               -command [list push_to $r]
+               }
+       }
+}
diff --git a/git-gui/lib/remote_branch_delete.tcl b/git-gui/lib/remote_branch_delete.tcl
new file mode 100644 (file)
index 0000000..c7b8148
--- /dev/null
@@ -0,0 +1,345 @@
+# git-gui remote branch deleting support
+# Copyright (C) 2007 Shawn Pearce
+
+class remote_branch_delete {
+
+field w
+field head_m
+
+field urltype   {url}
+field remote    {}
+field url       {}
+
+field checktype  {head}
+field check_head {}
+
+field status    {}
+field idle_id   {}
+field full_list {}
+field head_list {}
+field active_ls {}
+field head_cache
+field full_cache
+field cached
+
+constructor dialog {} {
+       global all_remotes M1B
+
+       make_toplevel top w
+       wm title $top [append "[appname] ([reponame]): " [mc "Delete Remote Branch"]]
+       if {$top ne {.}} {
+               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
+       }
+
+       label $w.header -text [mc "Delete Remote Branch"] -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       button $w.buttons.delete -text [mc Delete] \
+               -default active \
+               -command [cb _delete]
+       pack $w.buttons.delete -side right
+       button $w.buttons.cancel -text [mc "Cancel"] \
+               -command [list destroy $w]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       labelframe $w.dest -text [mc "From Repository"]
+       if {$all_remotes ne {}} {
+               radiobutton $w.dest.remote_r \
+                       -text [mc "Remote:"] \
+                       -value remote \
+                       -variable @urltype
+               eval tk_optionMenu $w.dest.remote_m @remote $all_remotes
+               grid $w.dest.remote_r $w.dest.remote_m -sticky w
+               if {[lsearch -sorted -exact $all_remotes origin] != -1} {
+                       set remote origin
+               } else {
+                       set remote [lindex $all_remotes 0]
+               }
+               set urltype remote
+               trace add variable @remote write [cb _write_remote]
+       } else {
+               set urltype url
+       }
+       radiobutton $w.dest.url_r \
+               -text [mc "Arbitrary URL:"] \
+               -value url \
+               -variable @urltype
+       entry $w.dest.url_t \
+               -borderwidth 1 \
+               -relief sunken \
+               -width 50 \
+               -textvariable @url \
+               -validate key \
+               -validatecommand {
+                       if {%d == 1 && [regexp {\s} %S]} {return 0}
+                       return 1
+               }
+       trace add variable @url write [cb _write_url]
+       grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5}
+       grid columnconfigure $w.dest 1 -weight 1
+       pack $w.dest -anchor nw -fill x -pady 5 -padx 5
+
+       labelframe $w.heads -text [mc "Branches"]
+       listbox $w.heads.l \
+               -height 10 \
+               -width 70 \
+               -listvariable @head_list \
+               -selectmode extended \
+               -yscrollcommand [list $w.heads.sby set]
+       scrollbar $w.heads.sby -command [list $w.heads.l yview]
+
+       frame $w.heads.footer
+       label $w.heads.footer.status \
+               -textvariable @status \
+               -anchor w \
+               -justify left
+       button $w.heads.footer.rescan \
+               -text [mc "Rescan"] \
+               -command [cb _rescan]
+       pack $w.heads.footer.status -side left -fill x
+       pack $w.heads.footer.rescan -side right
+
+       pack $w.heads.footer -side bottom -fill x
+       pack $w.heads.sby -side right -fill y
+       pack $w.heads.l -side left -fill both -expand 1
+       pack $w.heads -fill both -expand 1 -pady 5 -padx 5
+
+       labelframe $w.validate -text [mc "Delete Only If"]
+       radiobutton $w.validate.head_r \
+               -text [mc "Merged Into:"] \
+               -value head \
+               -variable @checktype
+       set head_m [tk_optionMenu $w.validate.head_m @check_head {}]
+       trace add variable @head_list write [cb _write_head_list]
+       trace add variable @check_head write [cb _write_check_head]
+       grid $w.validate.head_r $w.validate.head_m -sticky w
+       radiobutton $w.validate.always_r \
+               -text [mc "Always (Do not perform merge checks)"] \
+               -value always \
+               -variable @checktype
+       grid $w.validate.always_r -columnspan 2 -sticky w
+       grid columnconfigure $w.validate 1 -weight 1
+       pack $w.validate -anchor nw -fill x -pady 5 -padx 5
+
+       trace add variable @urltype write [cb _write_urltype]
+       _rescan $this
+
+       bind $w <Key-F5>     [cb _rescan]
+       bind $w <$M1B-Key-r> [cb _rescan]
+       bind $w <$M1B-Key-R> [cb _rescan]
+       bind $w <Key-Return> [cb _delete]
+       bind $w <Key-Escape> [list destroy $w]
+       return $w
+}
+
+method _delete {} {
+       switch $urltype {
+       remote {set uri $remote}
+       url    {set uri $url}
+       }
+
+       set cache $urltype:$uri
+       set crev {}
+       if {$checktype eq {head}} {
+               if {$check_head eq {}} {
+                       tk_messageBox \
+                               -icon error \
+                               -type ok \
+                               -title [wm title $w] \
+                               -parent $w \
+                               -message [mc "A branch is required for 'Merged Into'."]
+                       return
+               }
+               set crev $full_cache("$cache\nrefs/heads/$check_head")
+       }
+
+       set not_merged [list]
+       set need_fetch 0
+       set have_selection 0
+       set push_cmd [list git push]
+       lappend push_cmd -v
+       lappend push_cmd $uri
+
+       foreach i [$w.heads.l curselection] {
+               set ref [lindex $full_list $i]
+               if {$crev ne {}} {
+                       set obj $full_cache("$cache\n$ref")
+                       if {[catch {set m [git merge-base $obj $crev]}]} {
+                               set need_fetch 1
+                               set m {}
+                       }
+                       if {$obj ne $m} {
+                               lappend not_merged [lindex $head_list $i]
+                               continue
+                       }
+               }
+
+               lappend push_cmd :$ref
+               set have_selection 1
+       }
+
+       if {$not_merged ne {}} {
+               set msg [mc "The following branches are not completely merged into %s:
+
+ - %s" $check_head [join $not_merged "\n - "]]
+
+               if {$need_fetch} {
+                       append msg "\n\n" [mc "One or more of the merge tests failed because you have not fetched the necessary commits.  Try fetching from %s first." $uri]
+               }
+
+               tk_messageBox \
+                       -icon info \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message $msg
+               if {!$have_selection} return
+       }
+
+       if {!$have_selection} {
+               tk_messageBox \
+                       -icon error \
+                       -type ok \
+                       -title [wm title $w] \
+                       -parent $w \
+                       -message [mc "Please select one or more branches to delete."]
+               return
+       }
+
+       if {[tk_messageBox \
+               -icon warning \
+               -type yesno \
+               -title [wm title $w] \
+               -parent $w \
+               -message [mc "Recovering deleted branches is difficult.
+
+Delete the selected branches?"]] ne yes} {
+               return
+       }
+
+       destroy $w
+
+       set cons [console::new \
+               "push $uri" \
+               [mc "Deleting branches from %s" $uri]]
+       console::exec $cons $push_cmd
+}
+
+method _rescan {{force 1}} {
+       switch $urltype {
+       remote {set uri $remote}
+       url    {set uri $url}
+       }
+
+       if {$force} {
+               unset -nocomplain cached($urltype:$uri)
+       }
+
+       if {$idle_id ne {}} {
+               after cancel $idle_id
+               set idle_id {}
+       }
+
+       _load $this $urltype:$uri $uri
+}
+
+method _write_remote     {args} { set urltype remote }
+method _write_url        {args} { set urltype url    }
+method _write_check_head {args} { set checktype head }
+
+method _write_head_list {args} {
+       $head_m delete 0 end
+       foreach abr $head_list {
+               $head_m insert end radiobutton \
+                       -label $abr \
+                       -value $abr \
+                       -variable @check_head
+       }
+       if {[lsearch -exact -sorted $head_list $check_head] < 0} {
+               set check_head {}
+       }
+}
+
+method _write_urltype {args} {
+       if {$urltype eq {url}} {
+               if {$idle_id ne {}} {
+                       after cancel $idle_id
+               }
+               _load $this none: {}
+               set idle_id [after 1000 [cb _rescan 0]]
+       } else {
+               _rescan $this 0
+       }
+}
+
+method _load {cache uri} {
+       if {$active_ls ne {}} {
+               catch {close $active_ls}
+       }
+
+       if {$uri eq {}} {
+               $w.heads.l conf -state disabled
+               set head_list [list]
+               set full_list [list]
+               set status [mc "No repository selected."]
+               return
+       }
+
+       if {[catch {set x $cached($cache)}]} {
+               set status [mc "Scanning %s..." $uri]
+               $w.heads.l conf -state disabled
+               set head_list [list]
+               set full_list [list]
+               set head_cache($cache) [list]
+               set full_cache($cache) [list]
+               set active_ls [git_read ls-remote $uri]
+               fconfigure $active_ls \
+                       -blocking 0 \
+                       -translation lf \
+                       -encoding utf-8
+               fileevent $active_ls readable [cb _read $cache $active_ls]
+       } else {
+               set status {}
+               set full_list $full_cache($cache)
+               set head_list $head_cache($cache)
+               $w.heads.l conf -state normal
+       }
+}
+
+method _read {cache fd} {
+       if {$fd ne $active_ls} {
+               catch {close $fd}
+               return
+       }
+
+       while {[gets $fd line] >= 0} {
+               if {[string match {*^{}} $line]} continue
+               if {[regexp {^([0-9a-f]{40})    (.*)$} $line _junk obj ref]} {
+                       if {[regsub ^refs/heads/ $ref {} abr]} {
+                               lappend head_list $abr
+                               lappend head_cache($cache) $abr
+                               lappend full_list $ref
+                               lappend full_cache($cache) $ref
+                               set full_cache("$cache\n$ref") $obj
+                       }
+               }
+       }
+
+       if {[eof $fd]} {
+               if {[catch {close $fd} err]} {
+                       set status $err
+                       set head_list [list]
+                       set full_list [list]
+               } else {
+                       set status {}
+                       set cached($cache) 1
+                       $w.heads.l conf -state normal
+               }
+       }
+} ifdeleted {
+       catch {close $fd}
+}
+
+}
diff --git a/git-gui/lib/shortcut.tcl b/git-gui/lib/shortcut.tcl
new file mode 100644 (file)
index 0000000..38c3151
--- /dev/null
@@ -0,0 +1,139 @@
+# git-gui desktop icon creators
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc do_windows_shortcut {} {
+       set fn [tk_getSaveFile \
+               -parent . \
+               -title [append "[appname] ([reponame]): " [mc "Create Desktop Icon"]] \
+               -initialfile "Git [reponame].lnk"]
+       if {$fn != {}} {
+               if {[file extension $fn] ne {.lnk}} {
+                       set fn ${fn}.lnk
+               }
+               if {[catch {
+                               win32_create_lnk $fn [list \
+                                       [info nameofexecutable] \
+                                       [file normalize $::argv0] \
+                                       ] \
+                                       [file dirname [file normalize [gitdir]]]
+                       } err]} {
+                       error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"]
+               }
+       }
+}
+
+proc do_cygwin_shortcut {} {
+       global argv0
+
+       if {[catch {
+               set desktop [exec cygpath \
+                       --windows \
+                       --absolute \
+                       --long-name \
+                       --desktop]
+               }]} {
+                       set desktop .
+       }
+       set fn [tk_getSaveFile \
+               -parent . \
+               -title [append "[appname] ([reponame]): " [mc "Create Desktop Icon"]] \
+               -initialdir $desktop \
+               -initialfile "Git [reponame].lnk"]
+       if {$fn != {}} {
+               if {[file extension $fn] ne {.lnk}} {
+                       set fn ${fn}.lnk
+               }
+               if {[catch {
+                               set sh [exec cygpath \
+                                       --windows \
+                                       --absolute \
+                                       /bin/sh.exe]
+                               set me [exec cygpath \
+                                       --unix \
+                                       --absolute \
+                                       $argv0]
+                               win32_create_lnk $fn [list \
+                                       $sh -c \
+                                       "CHERE_INVOKING=1 source /etc/profile;[sq $me]" \
+                                       ] \
+                                       [file dirname [file normalize [gitdir]]]
+                       } err]} {
+                       error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"]
+               }
+       }
+}
+
+proc do_macosx_app {} {
+       global argv0 env
+
+       set fn [tk_getSaveFile \
+               -parent . \
+               -title [append "[appname] ([reponame]): " [mc "Create Desktop Icon"]] \
+               -initialdir [file join $env(HOME) Desktop] \
+               -initialfile "Git [reponame].app"]
+       if {$fn != {}} {
+               if {[file extension $fn] ne {.app}} {
+                       set fn ${fn}.app
+               }
+               if {[catch {
+                               set Contents [file join $fn Contents]
+                               set MacOS [file join $Contents MacOS]
+                               set exe [file join $MacOS git-gui]
+
+                               file mkdir $MacOS
+
+                               set fd [open [file join $Contents Info.plist] w]
+                               puts $fd {<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>CFBundleDevelopmentRegion</key>
+       <string>English</string>
+       <key>CFBundleExecutable</key>
+       <string>git-gui</string>
+       <key>CFBundleIdentifier</key>
+       <string>org.spearce.git-gui</string>
+       <key>CFBundleInfoDictionaryVersion</key>
+       <string>6.0</string>
+       <key>CFBundlePackageType</key>
+       <string>APPL</string>
+       <key>CFBundleSignature</key>
+       <string>????</string>
+       <key>CFBundleVersion</key>
+       <string>1.0</string>
+       <key>NSPrincipalClass</key>
+       <string>NSApplication</string>
+</dict>
+</plist>}
+                               close $fd
+
+                               set fd [open $exe w]
+                               puts $fd "#!/bin/sh"
+                               foreach name [lsort [array names env]] {
+                                       set value $env($name)
+                                       switch -- $name {
+                                       GIT_DIR { set value [file normalize [gitdir]] }
+                                       }
+
+                                       switch -glob -- $name {
+                                       SSH_* -
+                                       GIT_* {
+                                               puts $fd "if test \"z\$$name\" = z; then"
+                                               puts $fd "  export $name=[sq $value]"
+                                               puts $fd "fi &&"
+                                       }
+                                       }
+                               }
+                               puts $fd "export PATH=[sq [file dirname $::_git]]:\$PATH &&"
+                               puts $fd "cd [sq [file normalize [pwd]]] &&"
+                               puts $fd "exec \\"
+                               puts $fd " [sq [info nameofexecutable]] \\"
+                               puts $fd " [sq [file normalize $argv0]]"
+                               close $fd
+
+                               file attributes $exe -permissions u+x,g+x,o+x
+                       } err]} {
+                       error_popup [strcat [mc "Cannot write icon:"] "\n\n$err"]
+               }
+       }
+}
diff --git a/git-gui/lib/status_bar.tcl b/git-gui/lib/status_bar.tcl
new file mode 100644 (file)
index 0000000..51d4177
--- /dev/null
@@ -0,0 +1,127 @@
+# git-gui status bar mega-widget
+# Copyright (C) 2007 Shawn Pearce
+
+class status_bar {
+
+field w         ; # our own window path
+field w_l       ; # text widget we draw messages into
+field w_c       ; # canvas we draw a progress bar into
+field c_pack    ; # script to pack the canvas with
+field status  {}; # single line of text we show
+field prefix  {}; # text we format into status
+field units   {}; # unit of progress
+field meter   {}; # current core git progress meter (if active)
+
+constructor new {path} {
+       set w $path
+       set w_l $w.l
+       set w_c $w.c
+
+       frame $w \
+               -borderwidth 1 \
+               -relief sunken
+       label $w_l \
+               -textvariable @status \
+               -anchor w \
+               -justify left
+       pack $w_l -side left
+       set c_pack [cb _oneline_pack]
+
+       bind $w <Destroy> [cb _delete %W]
+       return $this
+}
+
+method _oneline_pack {} {
+       $w_c conf -width 100
+       pack $w_c -side right
+}
+
+constructor two_line {path} {
+       set w $path
+       set w_l $w.l
+       set w_c $w.c
+
+       frame $w
+       label $w_l \
+               -textvariable @status \
+               -anchor w \
+               -justify left
+       pack $w_l -anchor w -fill x
+       set c_pack [list pack $w_c -fill x]
+
+       bind $w <Destroy> [cb _delete %W]
+       return $this
+}
+
+method start {msg uds} {
+       if {[winfo exists $w_c]} {
+               $w_c coords bar 0 0 0 20
+       } else {
+               canvas $w_c \
+                       -height [expr {int([winfo reqheight $w_l] * 0.6)}] \
+                       -borderwidth 1 \
+                       -relief groove \
+                       -highlightt 0
+               $w_c create rectangle 0 0 0 20 -tags bar -fill navy
+               eval $c_pack
+       }
+
+       set status $msg
+       set prefix $msg
+       set units  $uds
+       set meter  {}
+}
+
+method update {have total} {
+       set pdone 0
+       if {$total > 0} {
+               set pdone [expr {100 * $have / $total}]
+               set cdone [expr {[winfo width $w_c] * $have / $total}]
+       }
+
+       set prec [string length [format %i $total]]
+       set status [mc "%s ... %*i of %*i %s (%3i%%)" \
+               $prefix \
+               $prec $have \
+               $prec $total \
+               $units $pdone]
+       $w_c coords bar 0 0 $cdone 20
+}
+
+method update_meter {buf} {
+       append meter $buf
+       set r [string last "\r" $meter]
+       if {$r == -1} {
+               return
+       }
+
+       set prior [string range $meter 0 $r]
+       set meter [string range $meter [expr {$r + 1}] end]
+       set p "\\((\\d+)/(\\d+)\\)"
+       if {[regexp ":\\s*\\d+% $p\(?:, done.\\s*\n|\\s*\r)\$" $prior _j a b]} {
+               update $this $a $b
+       } elseif {[regexp "$p\\s+done\r\$" $prior _j a b]} {
+               update $this $a $b
+       }
+}
+
+method stop {{msg {}}} {
+       destroy $w_c
+       if {$msg ne {}} {
+               set status $msg
+       }
+}
+
+method show {msg {test {}}} {
+       if {$test eq {} || $status eq $test} {
+               set status $msg
+       }
+}
+
+method _delete {current} {
+       if {$current eq $w} {
+               delete_this
+       }
+}
+
+}
diff --git a/git-gui/lib/transport.tcl b/git-gui/lib/transport.tcl
new file mode 100644 (file)
index 0000000..8e6a9d0
--- /dev/null
@@ -0,0 +1,184 @@
+# git-gui transport (fetch/push) support
+# Copyright (C) 2006, 2007 Shawn Pearce
+
+proc fetch_from {remote} {
+       set w [console::new \
+               [mc "fetch %s" $remote] \
+               [mc "Fetching new changes from %s" $remote]]
+       set cmds [list]
+       lappend cmds [list exec git fetch $remote]
+       if {[is_config_true gui.pruneduringfetch]} {
+               lappend cmds [list exec git remote prune $remote]
+       }
+       console::chain $w $cmds
+}
+
+proc prune_from {remote} {
+       set w [console::new \
+               [mc "remote prune %s" $remote] \
+               [mc "Pruning tracking branches deleted from %s" $remote]]
+       console::exec $w [list git remote prune $remote]
+}
+
+proc push_to {remote} {
+       set w [console::new \
+               [mc "push %s" $remote] \
+               [mc "Pushing changes to %s" $remote]]
+       set cmd [list git push]
+       lappend cmd -v
+       lappend cmd $remote
+       console::exec $w $cmd
+}
+
+proc start_push_anywhere_action {w} {
+       global push_urltype push_remote push_url push_thin push_tags
+       global push_force
+
+       set r_url {}
+       switch -- $push_urltype {
+       remote {set r_url $push_remote}
+       url {set r_url $push_url}
+       }
+       if {$r_url eq {}} return
+
+       set cmd [list git push]
+       lappend cmd -v
+       if {$push_thin} {
+               lappend cmd --thin
+       }
+       if {$push_force} {
+               lappend cmd --force
+       }
+       if {$push_tags} {
+               lappend cmd --tags
+       }
+       lappend cmd $r_url
+       set cnt 0
+       foreach i [$w.source.l curselection] {
+               set b [$w.source.l get $i]
+               lappend cmd "refs/heads/$b:refs/heads/$b"
+               incr cnt
+       }
+       if {$cnt == 0} {
+               return
+       } elseif {$cnt == 1} {
+               set unit branch
+       } else {
+               set unit branches
+       }
+
+       set cons [console::new \
+               [mc "push %s" $r_url] \
+               [mc "Pushing %s %s to %s" $cnt $unit $r_url]]
+       console::exec $cons $cmd
+       destroy $w
+}
+
+trace add variable push_remote write \
+       [list radio_selector push_urltype remote]
+
+proc do_push_anywhere {} {
+       global all_remotes current_branch
+       global push_urltype push_remote push_url push_thin push_tags
+       global push_force
+
+       set w .push_setup
+       toplevel $w
+       wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
+
+       label $w.header -text [mc "Push Branches"] -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons
+       button $w.buttons.create -text [mc Push] \
+               -default active \
+               -command [list start_push_anywhere_action $w]
+       pack $w.buttons.create -side right
+       button $w.buttons.cancel -text [mc "Cancel"] \
+               -default normal \
+               -command [list destroy $w]
+       pack $w.buttons.cancel -side right -padx 5
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       labelframe $w.source -text [mc "Source Branches"]
+       listbox $w.source.l \
+               -height 10 \
+               -width 70 \
+               -selectmode extended \
+               -yscrollcommand [list $w.source.sby set]
+       foreach h [load_all_heads] {
+               $w.source.l insert end $h
+               if {$h eq $current_branch} {
+                       $w.source.l select set end
+               }
+       }
+       scrollbar $w.source.sby -command [list $w.source.l yview]
+       pack $w.source.sby -side right -fill y
+       pack $w.source.l -side left -fill both -expand 1
+       pack $w.source -fill both -expand 1 -pady 5 -padx 5
+
+       labelframe $w.dest -text [mc "Destination Repository"]
+       if {$all_remotes ne {}} {
+               radiobutton $w.dest.remote_r \
+                       -text [mc "Remote:"] \
+                       -value remote \
+                       -variable push_urltype
+               eval tk_optionMenu $w.dest.remote_m push_remote $all_remotes
+               grid $w.dest.remote_r $w.dest.remote_m -sticky w
+               if {[lsearch -sorted -exact $all_remotes origin] != -1} {
+                       set push_remote origin
+               } else {
+                       set push_remote [lindex $all_remotes 0]
+               }
+               set push_urltype remote
+       } else {
+               set push_urltype url
+       }
+       radiobutton $w.dest.url_r \
+               -text [mc "Arbitrary URL:"] \
+               -value url \
+               -variable push_urltype
+       entry $w.dest.url_t \
+               -borderwidth 1 \
+               -relief sunken \
+               -width 50 \
+               -textvariable push_url \
+               -validate key \
+               -validatecommand {
+                       if {%d == 1 && [regexp {\s} %S]} {return 0}
+                       if {%d == 1 && [string length %S] > 0} {
+                               set push_urltype url
+                       }
+                       return 1
+               }
+       grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5}
+       grid columnconfigure $w.dest 1 -weight 1
+       pack $w.dest -anchor nw -fill x -pady 5 -padx 5
+
+       labelframe $w.options -text [mc "Transfer Options"]
+       checkbutton $w.options.force \
+               -text [mc "Force overwrite existing branch (may discard changes)"] \
+               -variable push_force
+       grid $w.options.force -columnspan 2 -sticky w
+       checkbutton $w.options.thin \
+               -text [mc "Use thin pack (for slow network connections)"] \
+               -variable push_thin
+       grid $w.options.thin -columnspan 2 -sticky w
+       checkbutton $w.options.tags \
+               -text [mc "Include tags"] \
+               -variable push_tags
+       grid $w.options.tags -columnspan 2 -sticky w
+       grid columnconfigure $w.options 1 -weight 1
+       pack $w.options -anchor nw -fill x -pady 5 -padx 5
+
+       set push_url {}
+       set push_force 0
+       set push_thin 0
+       set push_tags 0
+
+       bind $w <Visibility> "grab $w; focus $w.buttons.create"
+       bind $w <Key-Escape> "destroy $w"
+       bind $w <Key-Return> [list start_push_anywhere_action $w]
+       wm title $w [append "[appname] ([reponame]): " [mc "Push"]]
+       tkwait window $w
+}
diff --git a/git-gui/lib/win32.tcl b/git-gui/lib/win32.tcl
new file mode 100644 (file)
index 0000000..d7f93d0
--- /dev/null
@@ -0,0 +1,26 @@
+# git-gui Misc. native Windows 32 support
+# Copyright (C) 2007 Shawn Pearce
+
+proc win32_read_lnk {lnk_path} {
+       return [exec cscript.exe \
+               /E:jscript \
+               /nologo \
+               [file join $::oguilib win32_shortcut.js] \
+               $lnk_path]
+}
+
+proc win32_create_lnk {lnk_path lnk_exec lnk_dir} {
+       global oguilib
+
+       set lnk_args [lrange $lnk_exec 1 end]
+       set lnk_exec [lindex $lnk_exec 0]
+
+       eval [list exec wscript.exe \
+               /E:jscript \
+               /nologo \
+               [file join $oguilib win32_shortcut.js] \
+               $lnk_path \
+               [file join $oguilib git-gui.ico] \
+               $lnk_dir \
+               $lnk_exec] $lnk_args
+}
diff --git a/git-gui/lib/win32_shortcut.js b/git-gui/lib/win32_shortcut.js
new file mode 100644 (file)
index 0000000..117923f
--- /dev/null
@@ -0,0 +1,34 @@
+// git-gui Windows shortcut support
+// Copyright (C) 2007 Shawn Pearce
+
+var WshShell = WScript.CreateObject("WScript.Shell");
+var argv = WScript.Arguments;
+var argi = 0;
+var lnk_path = argv.item(argi++);
+var ico_path = argi < argv.length ? argv.item(argi++) : undefined;
+var dir_path = argi < argv.length ? argv.item(argi++) : undefined;
+var lnk_exec = argi < argv.length ? argv.item(argi++) : undefined;
+var lnk_args = '';
+while (argi < argv.length) {
+       var s = argv.item(argi++);
+       if (lnk_args != '')
+               lnk_args += ' ';
+       if (s.indexOf(' ') >= 0) {
+               lnk_args += '"';
+               lnk_args += s;
+               lnk_args += '"';
+       } else {
+               lnk_args += s;
+       }
+}
+
+var lnk = WshShell.CreateShortcut(lnk_path);
+if (argv.length == 1) {
+       WScript.echo(lnk.TargetPath);
+} else {
+       lnk.TargetPath = lnk_exec;
+       lnk.Arguments = lnk_args;
+       lnk.IconLocation = ico_path + ", 0";
+       lnk.WorkingDirectory = dir_path;
+       lnk.Save();
+}
diff --git a/git-gui/macosx/AppMain.tcl b/git-gui/macosx/AppMain.tcl
new file mode 100644 (file)
index 0000000..41ca08e
--- /dev/null
@@ -0,0 +1,22 @@
+set gitexecdir {@@gitexecdir@@}
+set gitguilib  {@@GITGUI_LIBDIR@@}
+set env(PATH) "$gitexecdir:$env(PATH)"
+
+if {[string first -psn [lindex $argv 0]] == 0} {
+       lset argv 0 [file join $gitexecdir git-gui]
+}
+
+if {[file tail [lindex $argv 0]] eq {gitk}} {
+       set argv0 [file join $gitexecdir gitk]
+       set AppMain_source $argv0
+} else {
+       set argv0 [file join $gitexecdir [file tail [lindex $argv 0]]]
+       set AppMain_source [file join $gitguilib git-gui.tcl]
+       if {[pwd] eq {/}} {
+               cd $env(HOME)
+       }
+}
+
+unset gitexecdir gitguilib
+set argv [lrange $argv 1 end]
+source $AppMain_source
diff --git a/git-gui/macosx/Info.plist b/git-gui/macosx/Info.plist
new file mode 100644 (file)
index 0000000..99913ec
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>CFBundleDevelopmentRegion</key>
+       <string>English</string>
+       <key>CFBundleExecutable</key>
+       <string>Wish</string>
+       <key>CFBundleGetInfoString</key>
+       <string>Git Gui @@GITGUI_VERSION@@ © 2006-2007 Shawn Pearce, et. al.</string>
+       <key>CFBundleIconFile</key>
+       <string>git-gui.icns</string>
+       <key>CFBundleIdentifier</key>
+       <string>cz.or.repo.git-gui</string>
+       <key>CFBundleInfoDictionaryVersion</key>
+       <string>6.0</string>
+       <key>CFBundleName</key>
+       <string>Git Gui</string>
+       <key>CFBundlePackageType</key>
+       <string>APPL</string>
+       <key>CFBundleShortVersionString</key>
+       <string>@@GITGUI_VERSION@@</string>
+       <key>CFBundleSignature</key>
+       <string>GITg</string>
+       <key>CFBundleVersion</key>
+       <string>@@GITGUI_VERSION@@</string>
+</dict>
+</plist>
diff --git a/git-gui/macosx/git-gui.icns b/git-gui/macosx/git-gui.icns
new file mode 100644 (file)
index 0000000..77d88a7
Binary files /dev/null and b/git-gui/macosx/git-gui.icns differ
diff --git a/git-gui/po/.gitignore b/git-gui/po/.gitignore
new file mode 100644 (file)
index 0000000..a89cf44
--- /dev/null
@@ -0,0 +1,2 @@
+*.msg
+*~
diff --git a/git-gui/po/README b/git-gui/po/README
new file mode 100644 (file)
index 0000000..9d8b736
--- /dev/null
@@ -0,0 +1,209 @@
+Localizing git-gui for your language
+====================================
+
+This short note is to help you, who reads and writes English and your
+own language, help us getting git-gui localized for more languages.  It
+does not try to be a comprehensive manual of GNU gettext, which is the
+i18n framework we use, but tries to help you get started by covering the
+basics and how it is used in this project.
+
+1. Getting started.
+
+You would first need to have a working "git".  Your distribution may
+have it as "git-core" package (do not get "GNU Interactive Tools" --
+that is a different "git").  You would also need GNU gettext toolchain
+to test the resulting translation out.  Although you can work on message
+translation files with a regular text editor, it is a good idea to have
+specialized so-called "po file editors" (e.g. emacs po-mode, KBabel,
+poedit, GTranslator --- any of them would work well).  Please install
+them.
+
+You would then need to clone the git-gui internationalization project
+repository, so that you can work on it:
+
+       $ git clone mob@repo.or.cz:/srv/git/git-gui/git-gui-i18n.git/
+       $ cd git-gui-i18n
+       $ git checkout --track -b mob origin/mob
+       $ git config remote.origin.push mob
+
+The "git checkout" command creates a 'mob' branch from upstream's
+corresponding branch and makes it your current branch.  You will be
+working on this branch.
+
+The "git config" command records in your repository configuration file
+that you would push "mob" branch to the upstream when you say "git
+push".
+
+
+2. Starting a new language.
+
+In the git-gui-i18n directory is a po/ subdirectory.  It has a
+handful files whose names end with ".po".  Is there a file that has
+messages in your language?
+
+If you do not know what your language should be named, you need to find
+it.  This currently follows ISO 639-1 two letter codes:
+
+       http://www.loc.gov/standards/iso639-2/php/code_list.php
+
+For example, if you are preparing a translation for Afrikaans, the
+language code is "af".  If there already is a translation for your
+language, you do not have to perform any step in this section, but keep
+reading, because we are covering the basics.
+
+If you did not find your language, you would need to start one yourself.
+Copy po/git-gui.pot file to po/af.po (replace "af" with the code for
+your language).  Edit the first several lines to match existing *.po
+files to make it clear this is a translation table for git-gui project,
+and you are the primary translator.  The result of your editing would
+look something like this:
+
+    # Translation of git-gui to Afrikaans
+    # Copyright (C) 2007 Shawn Pearce
+    # This file is distributed under the same license as the git-gui package.
+    # YOUR NAME <YOUR@E-MAIL.ADDRESS>, 2007.
+    #
+    #, fuzzy
+    msgid ""
+    msgstr ""
+    "Project-Id-Version: git-gui\n"
+    "Report-Msgid-Bugs-To: \n"
+    "POT-Creation-Date: 2007-07-24 22:19+0300\n"
+    "PO-Revision-Date: 2007-07-25 18:00+0900\n"
+    "Last-Translator: YOUR NAME <YOUR@E-MAIL.ADDRESS>\n"
+    "Language-Team: Afrikaans\n"
+    "MIME-Version: 1.0\n"
+    "Content-Type: text/plain; charset=UTF-8\n"
+    "Content-Transfer-Encoding: 8bit\n"
+
+You will find many pairs of a "msgid" line followed by a "msgstr" line.
+These pairs define how messages in git-gui application are translated to
+your language.  Your primarily job is to fill in the empty double quote
+pairs on msgstr lines with the translation of the strings on their
+matching msgid lines.  A few tips:
+
+ - Control characters, such as newlines, are written in backslash
+   sequence similar to string literals in the C programming language.
+   When the string given on a msgid line has such a backslash sequence,
+   you would typically want to have corresponding ones in the string on
+   your msgstr line.
+
+ - Some messages contain an optional context indicator at the end,
+   for example "@@noun" or "@@verb".  This indicator allows the
+   software to select the correct translation depending upon the use.
+   The indicator is not actually part of the message and will not
+   be shown to the end-user.
+
+   If your language does not require a different translation you
+   will still need to translate both messages.
+
+ - Often the messages being translated are format strings given to
+   "printf()"-like functions.  Make sure "%s", "%d", and "%%" in your
+   translated messages match the original.
+
+   When you have to change the order of words, you can add "<number>\$"
+   between '%' and the conversion ('s', 'd', etc.) to say "<number>-th
+   parameter to the format string is used at this point".  For example,
+   if the original message is like this:
+
+       "Length is %d, Weight is %d"
+
+   and if for whatever reason your translation needs to say weight first
+   and then length, you can say something like:
+
+       "WEIGHT IS %2\$d, LENGTH IS %1\$d"
+
+   The reason you need a backslash before dollar sign is because
+   this is a double quoted string in Tcl language, and without
+   it the letter introduces a variable interpolation, which you
+   do not want here.
+
+ - A long message can be split across multiple lines by ending the
+   string with a double quote, and starting another string on the next
+   line with another double quote.  They will be concatenated in the
+   result.  For example:
+
+   #: lib/remote_branch_delete.tcl:189
+   #, tcl-format
+   msgid ""
+   "One or more of the merge tests failed because you have not fetched the "
+   "necessary commits.  Try fetching from %s first."
+   msgstr ""
+   "HERE YOU WILL WRITE YOUR TRANSLATION OF THE ABOVE LONG "
+   "MESSAGE IN YOUR LANGUAGE."
+
+You can test your translation by running "make install", which would
+create po/af.msg file and installs the result, and then running the
+resulting git-gui under your locale:
+
+       $ make install
+       $ LANG=af git-gui
+
+There is a trick to test your translation without first installing:
+
+       $ make
+       $ LANG=af ./git-gui.sh
+
+When you are satisfied with your translation, commit your changes, and
+push it back to the 'mob' branch:
+
+       $ edit po/af.po
+       ... be sure to update Last-Translator: and
+       ... PO-Revision-Date: lines.
+       $ git add po/af.po
+       $ git commit -m 'Started Afrikaans translation.'
+       $ git push
+
+
+3. Updating your translation.
+
+There may already be a translation for your language, and you may want
+to contribute an update.  This may be because you would want to improve
+the translation of existing messages, or because the git-gui software
+itself was updated and there are new messages that need translation.
+
+In any case, make sure you are up-to-date before starting your work:
+
+       $ git pull
+
+In the former case, you will edit po/af.po (again, replace "af" with
+your language code), and after testing and updating the Last-Translator:
+and PO-Revision-Date: lines, "add/commit/push" as in the previous
+section.
+
+By comparing "POT-Creation-Date:" line in po/git-gui.pot file and
+po/af.po file, you can tell if there are new messages that need to be
+translated.  You would need the GNU gettext package to perform this
+step.
+
+       $ msgmerge -U po/af.po po/git-gui.pot
+
+[NEEDSWORK: who is responsible for updating po/git-gui.pot file by
+running xgettext?  IIRC, Christian recommended against running it
+nilly-willy because it can become a source of unnecessary merge
+conflicts.  Perhaps we should mention something like "
+
+The po/git-gui.pot file is updated by the internationalization
+coordinator from time to time.  You _could_ update it yourself, but
+translators are discouraged from doing so because we would want all
+language teams to be working off of the same version of git-gui.pot.
+
+" here?]
+
+This updates po/af.po (again, replace "af" with your language
+code) so that it contains msgid lines (i.e. the original) that
+your translation did not have before.  There are a few things to
+watch out for:
+
+ - The original text in English of an older message you already
+   translated might have been changed.  You will notice a comment line
+   that begins with "#, fuzzy" in front of such a message.  msgmerge
+   tool made its best effort to match your old translation with the
+   message from the updated software, but you may find cases that it
+   matched your old translated message to a new msgid and the pairing
+   does not make any sense -- you would need to fix them, and then
+   remove the "#, fuzzy" line from the message (your fixed translation
+   of the message will not be used before you remove the marker).
+
+ - New messages added to the software will have msgstr lines with empty
+   strings.  You would need to translate them.
diff --git a/git-gui/po/de.po b/git-gui/po/de.po
new file mode 100644 (file)
index 0000000..ac8ae0a
--- /dev/null
@@ -0,0 +1,1904 @@
+# Translation of git-gui to German.
+# Copyright (C) 2007 Shawn Pearce, et al.
+# This file is distributed under the same license as the git package.
+# Christian Stimming <stimming@tuhh.de>, 2007
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: git-gui\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-11-24 10:36+0100\n"
+"PO-Revision-Date: 2007-11-24 10:55+0100\n"
+"Last-Translator: Christian Stimming <stimming@tuhh.de>\n"
+"Language-Team: German\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
+#: git-gui.sh:733
+msgid "git-gui: fatal error"
+msgstr "git-gui: Programmfehler"
+
+#: git-gui.sh:565
+#, tcl-format
+msgid "Invalid font specified in %s:"
+msgstr "Ungültige Zeichensatz-Angabe in %s:"
+
+#: git-gui.sh:590
+msgid "Main Font"
+msgstr "Programmschriftart"
+
+#: git-gui.sh:591
+msgid "Diff/Console Font"
+msgstr "Vergleich-Schriftart"
+
+#: git-gui.sh:605
+msgid "Cannot find git in PATH."
+msgstr "Git kann im PATH nicht gefunden werden."
+
+#: git-gui.sh:632
+msgid "Cannot parse Git version string:"
+msgstr "Git Versionsangabe kann nicht erkannt werden:"
+
+#: git-gui.sh:650
+#, tcl-format
+msgid ""
+"Git version cannot be determined.\n"
+"\n"
+"%s claims it is version '%s'.\n"
+"\n"
+"%s requires at least Git 1.5.0 or later.\n"
+"\n"
+"Assume '%s' is version 1.5.0?\n"
+msgstr ""
+"Die Version von Git kann nicht bestimmt werden.\n"
+"\n"
+"»%s« behauptet, es sei Version »%s«.\n"
+"\n"
+"%s benötigt mindestens Git 1.5.0 oder höher.\n"
+"\n"
+"Soll angenommen werden, »%s« sei Version 1.5.0?\n"
+
+#: git-gui.sh:888
+msgid "Git directory not found:"
+msgstr "Git-Verzeichnis nicht gefunden:"
+
+#: git-gui.sh:895
+msgid "Cannot move to top of working directory:"
+msgstr ""
+"Es konnte nicht in das oberste Verzeichnis der Arbeitskopie gewechselt "
+"werden:"
+
+#: git-gui.sh:902
+msgid "Cannot use funny .git directory:"
+msgstr "Unerwartete Struktur des .git Verzeichnis:"
+
+#: git-gui.sh:907
+msgid "No working directory"
+msgstr "Kein Arbeitsverzeichnis"
+
+#: git-gui.sh:1054
+msgid "Refreshing file status..."
+msgstr "Dateistatus aktualisieren..."
+
+#: git-gui.sh:1119
+msgid "Scanning for modified files ..."
+msgstr "Nach geänderten Dateien suchen..."
+
+#: git-gui.sh:1294 lib/browser.tcl:245
+msgid "Ready."
+msgstr "Bereit."
+
+#: git-gui.sh:1560
+msgid "Unmodified"
+msgstr "Unverändert"
+
+#: git-gui.sh:1562
+msgid "Modified, not staged"
+msgstr "Verändert, nicht bereitgestellt"
+
+#: git-gui.sh:1563 git-gui.sh:1568
+msgid "Staged for commit"
+msgstr "Bereitgestellt zum Eintragen"
+
+#: git-gui.sh:1564 git-gui.sh:1569
+msgid "Portions staged for commit"
+msgstr "Teilweise bereitgestellt zum Eintragen"
+
+#: git-gui.sh:1565 git-gui.sh:1570
+msgid "Staged for commit, missing"
+msgstr "Bereitgestellt zum Eintragen, fehlend"
+
+#: git-gui.sh:1567
+msgid "Untracked, not staged"
+msgstr "Nicht unter Versionskontrolle, nicht bereitgestellt"
+
+#: git-gui.sh:1572
+msgid "Missing"
+msgstr "Fehlend"
+
+#: git-gui.sh:1573
+msgid "Staged for removal"
+msgstr "Bereitgestellt zum Löschen"
+
+#: git-gui.sh:1574
+msgid "Staged for removal, still present"
+msgstr "Bereitgestellt zum Löschen, trotzdem vorhanden"
+
+#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
+msgid "Requires merge resolution"
+msgstr "Konfliktauflösung nötig"
+
+#: git-gui.sh:1614
+msgid "Starting gitk... please wait..."
+msgstr "Gitk wird gestartet... bitte warten."
+
+#: git-gui.sh:1623
+#, tcl-format
+msgid ""
+"Unable to start gitk:\n"
+"\n"
+"%s does not exist"
+msgstr ""
+"Gitk kann nicht gestartet werden:\n"
+"\n"
+"%s existiert nicht"
+
+#: git-gui.sh:1823 lib/choose_repository.tcl:35
+msgid "Repository"
+msgstr "Projektarchiv"
+
+#: git-gui.sh:1824
+msgid "Edit"
+msgstr "Bearbeiten"
+
+#: git-gui.sh:1826 lib/choose_rev.tcl:560
+msgid "Branch"
+msgstr "Zweig"
+
+#: git-gui.sh:1829 lib/choose_rev.tcl:547
+msgid "Commit@@noun"
+msgstr "Version"
+
+#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
+msgid "Merge"
+msgstr "Zusammenführen"
+
+#: git-gui.sh:1833 lib/choose_rev.tcl:556
+msgid "Remote"
+msgstr "Andere Archive"
+
+#: git-gui.sh:1842
+msgid "Browse Current Branch's Files"
+msgstr "Aktuellen Zweig durchblättern"
+
+#: git-gui.sh:1846
+msgid "Browse Branch Files..."
+msgstr "Einen Zweig durchblättern..."
+
+#: git-gui.sh:1851
+msgid "Visualize Current Branch's History"
+msgstr "Aktuellen Zweig darstellen"
+
+#: git-gui.sh:1855
+msgid "Visualize All Branch History"
+msgstr "Alle Zweige darstellen"
+
+#: git-gui.sh:1862
+#, tcl-format
+msgid "Browse %s's Files"
+msgstr "Zweig »%s« durchblättern"
+
+#: git-gui.sh:1864
+#, tcl-format
+msgid "Visualize %s's History"
+msgstr "Historie von »%s« darstellen"
+
+#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
+msgid "Database Statistics"
+msgstr "Datenbankstatistik"
+
+#: git-gui.sh:1872 lib/database.tcl:34
+msgid "Compress Database"
+msgstr "Datenbank komprimieren"
+
+#: git-gui.sh:1875
+msgid "Verify Database"
+msgstr "Datenbank überprüfen"
+
+#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
+#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
+msgid "Create Desktop Icon"
+msgstr "Desktop-Icon erstellen"
+
+#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
+msgid "Quit"
+msgstr "Beenden"
+
+#: git-gui.sh:1902
+msgid "Undo"
+msgstr "Rückgängig"
+
+#: git-gui.sh:1905
+msgid "Redo"
+msgstr "Wiederholen"
+
+#: git-gui.sh:1909 git-gui.sh:2403
+msgid "Cut"
+msgstr "Ausschneiden"
+
+#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
+#: lib/console.tcl:67
+msgid "Copy"
+msgstr "Kopieren"
+
+#: git-gui.sh:1915 git-gui.sh:2409
+msgid "Paste"
+msgstr "Einfügen"
+
+#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
+#: lib/remote_branch_delete.tcl:38
+msgid "Delete"
+msgstr "Löschen"
+
+#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
+msgid "Select All"
+msgstr "Alle auswählen"
+
+#: git-gui.sh:1931
+msgid "Create..."
+msgstr "Erstellen..."
+
+#: git-gui.sh:1937
+msgid "Checkout..."
+msgstr "Umstellen..."
+
+#: git-gui.sh:1943
+msgid "Rename..."
+msgstr "Umbenennen..."
+
+#: git-gui.sh:1948 git-gui.sh:2048
+msgid "Delete..."
+msgstr "Löschen..."
+
+#: git-gui.sh:1953
+msgid "Reset..."
+msgstr "Zurücksetzen..."
+
+#: git-gui.sh:1965 git-gui.sh:2350
+msgid "New Commit"
+msgstr "Neue Version"
+
+#: git-gui.sh:1973 git-gui.sh:2357
+msgid "Amend Last Commit"
+msgstr "Letzte Version nachbessern"
+
+#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
+msgid "Rescan"
+msgstr "Neu laden"
+
+#: git-gui.sh:1988
+msgid "Stage To Commit"
+msgstr "Zum Eintragen bereitstellen"
+
+#: git-gui.sh:1994
+msgid "Stage Changed Files To Commit"
+msgstr "Geänderte Dateien zum Eintragen bereitstellen"
+
+#: git-gui.sh:2000
+msgid "Unstage From Commit"
+msgstr "Aus der Bereitstellung herausnehmen"
+
+#: git-gui.sh:2005 lib/index.tcl:393
+msgid "Revert Changes"
+msgstr "Änderungen revidieren"
+
+#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
+msgid "Sign Off"
+msgstr "Abzeichnen"
+
+#: git-gui.sh:2016 git-gui.sh:2333
+msgid "Commit@@verb"
+msgstr "Eintragen"
+
+#: git-gui.sh:2027
+msgid "Local Merge..."
+msgstr "Lokales Zusammenführen..."
+
+#: git-gui.sh:2032
+msgid "Abort Merge..."
+msgstr "Zusammenführen abbrechen..."
+
+#: git-gui.sh:2044
+msgid "Push..."
+msgstr "Versenden..."
+
+#: git-gui.sh:2055 lib/choose_repository.tcl:40
+msgid "Apple"
+msgstr "Apple"
+
+#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
+#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
+#, tcl-format
+msgid "About %s"
+msgstr "Über %s"
+
+#: git-gui.sh:2062
+msgid "Preferences..."
+msgstr "Einstellungen..."
+
+#: git-gui.sh:2070 git-gui.sh:2595
+msgid "Options..."
+msgstr "Optionen..."
+
+#: git-gui.sh:2076 lib/choose_repository.tcl:46
+msgid "Help"
+msgstr "Hilfe"
+
+#: git-gui.sh:2117
+msgid "Online Documentation"
+msgstr "Online-Dokumentation"
+
+#: git-gui.sh:2201
+#, tcl-format
+msgid "fatal: cannot stat path %s: No such file or directory"
+msgstr "Fehler: Verzeichnis »%s« kann nicht gelesen werden: Datei oder Verzeichnis nicht gefunden"
+
+#: git-gui.sh:2234
+msgid "Current Branch:"
+msgstr "Aktueller Zweig:"
+
+#: git-gui.sh:2255
+msgid "Staged Changes (Will Commit)"
+msgstr "Bereitgestellte Änderungen (zum Eintragen)"
+
+#: git-gui.sh:2274
+msgid "Unstaged Changes"
+msgstr "Nicht bereitgestellte Änderungen"
+
+#: git-gui.sh:2323
+msgid "Stage Changed"
+msgstr "Alles bereitstellen"
+
+#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
+msgid "Push"
+msgstr "Versenden"
+
+#: git-gui.sh:2369
+msgid "Initial Commit Message:"
+msgstr "Erste Versionsbeschreibung:"
+
+#: git-gui.sh:2370
+msgid "Amended Commit Message:"
+msgstr "Nachgebesserte Versionsbeschreibung:"
+
+#: git-gui.sh:2371
+msgid "Amended Initial Commit Message:"
+msgstr "Nachgebesserte erste Versionsbeschreibung:"
+
+#: git-gui.sh:2372
+msgid "Amended Merge Commit Message:"
+msgstr "Nachgebesserte Zusammenführungs-Versionsbeschreibung:"
+
+#: git-gui.sh:2373
+msgid "Merge Commit Message:"
+msgstr "Zusammenführungs-Versionsbeschreibung:"
+
+#: git-gui.sh:2374
+msgid "Commit Message:"
+msgstr "Versionsbeschreibung:"
+
+#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
+msgid "Copy All"
+msgstr "Alle kopieren"
+
+#: git-gui.sh:2443 lib/blame.tcl:104
+msgid "File:"
+msgstr "Datei:"
+
+#: git-gui.sh:2545
+msgid "Refresh"
+msgstr "Aktualisieren"
+
+#: git-gui.sh:2566
+msgid "Apply/Reverse Hunk"
+msgstr "Änderung anwenden/umkehren"
+
+#: git-gui.sh:2572
+msgid "Decrease Font Size"
+msgstr "Schriftgröße verkleinern"
+
+#: git-gui.sh:2576
+msgid "Increase Font Size"
+msgstr "Schriftgröße vergrößern"
+
+#: git-gui.sh:2581
+msgid "Show Less Context"
+msgstr "Weniger Kontext anzeigen"
+
+#: git-gui.sh:2588
+msgid "Show More Context"
+msgstr "Mehr Kontext anzeigen"
+
+#: git-gui.sh:2602
+msgid "Unstage Hunk From Commit"
+msgstr "Aus der Bereitstellung herausnehmen"
+
+#: git-gui.sh:2604
+msgid "Stage Hunk For Commit"
+msgstr "In die Bereitstellung hinzufügen"
+
+#: git-gui.sh:2623
+msgid "Initializing..."
+msgstr "Initialisieren..."
+
+#: git-gui.sh:2718
+#, tcl-format
+msgid ""
+"Possible environment issues exist.\n"
+"\n"
+"The following environment variables are probably\n"
+"going to be ignored by any Git subprocess run\n"
+"by %s:\n"
+"\n"
+msgstr ""
+"Möglicherweise gibt es Probleme mit manchen Umgebungsvariablen.\n"
+"\n"
+"Die folgenden Umgebungsvariablen können vermutlich nicht \n"
+"von %s an Git weitergegeben werden:\n"
+"\n"
+
+#: git-gui.sh:2748
+msgid ""
+"\n"
+"This is due to a known issue with the\n"
+"Tcl binary distributed by Cygwin."
+msgstr ""
+"\n"
+"Dies ist ein bekanntes Problem der Tcl-Version, die\n"
+"in Cygwin mitgeliefert wird."
+
+#: git-gui.sh:2753
+#, tcl-format
+msgid ""
+"\n"
+"\n"
+"A good replacement for %s\n"
+"is placing values for the user.name and\n"
+"user.email settings into your personal\n"
+"~/.gitconfig file.\n"
+msgstr ""
+"\n"
+"\n"
+"Um den Namen »%s« zu ändern, sollten Sie die \n"
+"gewünschten Werte für die Einstellung user.name und \n"
+"user.email in Ihre Datei ~/.gitconfig einfügen.\n"
+
+#: lib/about.tcl:25
+msgid "git-gui - a graphical user interface for Git."
+msgstr "git-gui - eine grafische Oberfläche für Git."
+
+#: lib/blame.tcl:77
+msgid "File Viewer"
+msgstr "Datei-Browser"
+
+#: lib/blame.tcl:81
+msgid "Commit:"
+msgstr "Version:"
+
+#: lib/blame.tcl:249
+msgid "Copy Commit"
+msgstr "Version kopieren"
+
+#: lib/blame.tcl:369
+#, tcl-format
+msgid "Reading %s..."
+msgstr "%s lesen..."
+
+#: lib/blame.tcl:473
+msgid "Loading copy/move tracking annotations..."
+msgstr "Annotierungen für Kopieren/Verschieben werden geladen..."
+
+#: lib/blame.tcl:493
+msgid "lines annotated"
+msgstr "Zeilen annotiert"
+
+#: lib/blame.tcl:674
+msgid "Loading original location annotations..."
+msgstr "Annotierungen für ursprünglichen Ort werden geladen..."
+
+#: lib/blame.tcl:677
+msgid "Annotation complete."
+msgstr "Annotierung vollständig."
+
+#: lib/blame.tcl:731
+msgid "Loading annotation..."
+msgstr "Annotierung laden..."
+
+#: lib/blame.tcl:787
+msgid "Author:"
+msgstr "Autor:"
+
+#: lib/blame.tcl:791
+msgid "Committer:"
+msgstr "Eintragender:"
+
+#: lib/blame.tcl:796
+msgid "Original File:"
+msgstr "Ursprüngliche Datei:"
+
+#: lib/blame.tcl:910
+msgid "Originally By:"
+msgstr "Ursprünglich von:"
+
+#: lib/blame.tcl:916
+msgid "In File:"
+msgstr "In Datei:"
+
+#: lib/blame.tcl:921
+msgid "Copied Or Moved Here By:"
+msgstr "Kopiert oder verschoben durch:"
+
+#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
+msgid "Checkout Branch"
+msgstr "Zweig umstellen"
+
+#: lib/branch_checkout.tcl:23
+msgid "Checkout"
+msgstr "Umstellen"
+
+#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
+#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
+#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
+#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
+msgid "Cancel"
+msgstr "Abbrechen"
+
+#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
+msgid "Revision"
+msgstr "Version"
+
+#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
+msgid "Options"
+msgstr "Optionen"
+
+#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
+msgid "Fetch Tracking Branch"
+msgstr "Übernahmezweig anfordern"
+
+#: lib/branch_checkout.tcl:44
+msgid "Detach From Local Branch"
+msgstr "Verbindung zu lokalem Zweig lösen"
+
+#: lib/branch_create.tcl:22
+msgid "Create Branch"
+msgstr "Zweig erstellen"
+
+#: lib/branch_create.tcl:27
+msgid "Create New Branch"
+msgstr "Neuen Zweig erstellen"
+
+#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
+msgid "Create"
+msgstr "Erstellen"
+
+#: lib/branch_create.tcl:40
+msgid "Branch Name"
+msgstr "Zweigname"
+
+#: lib/branch_create.tcl:43
+msgid "Name:"
+msgstr "Name:"
+
+#: lib/branch_create.tcl:58
+msgid "Match Tracking Branch Name"
+msgstr "Passend zu Übernahmezweig-Name"
+
+#: lib/branch_create.tcl:66
+msgid "Starting Revision"
+msgstr "Anfangsversion"
+
+#: lib/branch_create.tcl:72
+msgid "Update Existing Branch:"
+msgstr "Existierenden Zweig aktualisieren:"
+
+#: lib/branch_create.tcl:75
+msgid "No"
+msgstr "Nein"
+
+#: lib/branch_create.tcl:80
+msgid "Fast Forward Only"
+msgstr "Nur Schnellzusammenführung"
+
+#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
+msgid "Reset"
+msgstr "Zurücksetzen"
+
+#: lib/branch_create.tcl:97
+msgid "Checkout After Creation"
+msgstr "Arbeitskopie umstellen nach Erstellen"
+
+#: lib/branch_create.tcl:131
+msgid "Please select a tracking branch."
+msgstr "Bitte wählen Sie einen Übernahmezweig."
+
+#: lib/branch_create.tcl:140
+#, tcl-format
+msgid "Tracking branch %s is not a branch in the remote repository."
+msgstr "Übernahmezweig »%s« ist kein Zweig im anderen Projektarchiv."
+
+#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
+msgid "Please supply a branch name."
+msgstr "Bitte geben Sie einen Zweignamen an."
+
+#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
+#, tcl-format
+msgid "'%s' is not an acceptable branch name."
+msgstr "»%s« ist kein zulässiger Zweigname."
+
+#: lib/branch_delete.tcl:15
+msgid "Delete Branch"
+msgstr "Zweig löschen"
+
+#: lib/branch_delete.tcl:20
+msgid "Delete Local Branch"
+msgstr "Lokalen Zweig löschen"
+
+#: lib/branch_delete.tcl:37
+msgid "Local Branches"
+msgstr "Lokale Zweige"
+
+#: lib/branch_delete.tcl:52
+msgid "Delete Only If Merged Into"
+msgstr "Nur löschen, wenn darin zusammengeführt"
+
+#: lib/branch_delete.tcl:54
+msgid "Always (Do not perform merge test.)"
+msgstr "Immer (ohne Zusammenführungstest)"
+
+#: lib/branch_delete.tcl:103
+#, tcl-format
+msgid "The following branches are not completely merged into %s:"
+msgstr "Folgende Zweige sind noch nicht mit »%s« zusammengeführt:"
+
+#: lib/branch_delete.tcl:115
+msgid ""
+"Recovering deleted branches is difficult. \n"
+"\n"
+" Delete the selected branches?"
+msgstr ""
+"Gelöschte Zweige können nur mit größerem Aufwand wiederhergestellt werden.\n"
+"\n"
+"Gewählte Zweige jetzt löschen?"
+
+#: lib/branch_delete.tcl:141
+#, tcl-format
+msgid ""
+"Failed to delete branches:\n"
+"%s"
+msgstr ""
+"Fehler beim Löschen der Zweige:\n"
+"%s"
+
+#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
+msgid "Rename Branch"
+msgstr "Zweig umbenennen"
+
+#: lib/branch_rename.tcl:26
+msgid "Rename"
+msgstr "Umbenennen"
+
+#: lib/branch_rename.tcl:36
+msgid "Branch:"
+msgstr "Zweig:"
+
+#: lib/branch_rename.tcl:39
+msgid "New Name:"
+msgstr "Neuer Name:"
+
+#: lib/branch_rename.tcl:75
+msgid "Please select a branch to rename."
+msgstr "Bitte wählen Sie einen Zweig zum umbenennen."
+
+#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
+#, tcl-format
+msgid "Branch '%s' already exists."
+msgstr "Zweig »%s« existiert bereits."
+
+#: lib/branch_rename.tcl:117
+#, tcl-format
+msgid "Failed to rename '%s'."
+msgstr "Fehler beim Umbenennen von »%s«."
+
+#: lib/browser.tcl:17
+msgid "Starting..."
+msgstr "Starten..."
+
+#: lib/browser.tcl:26
+msgid "File Browser"
+msgstr "Datei-Browser"
+
+#: lib/browser.tcl:125 lib/browser.tcl:142
+#, tcl-format
+msgid "Loading %s..."
+msgstr "%s laden..."
+
+#: lib/browser.tcl:186
+msgid "[Up To Parent]"
+msgstr "[Nach oben]"
+
+#: lib/browser.tcl:266 lib/browser.tcl:272
+msgid "Browse Branch Files"
+msgstr "Dateien des Zweigs durchblättern"
+
+#: lib/browser.tcl:277 lib/choose_repository.tcl:391
+#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
+#: lib/choose_repository.tcl:989
+msgid "Browse"
+msgstr "Blättern"
+
+#: lib/checkout_op.tcl:79
+#, tcl-format
+msgid "Fetching %s from %s"
+msgstr "Änderungen »%s« von »%s« anfordern"
+
+#: lib/checkout_op.tcl:127
+#, tcl-format
+msgid "fatal: Cannot resolve %s"
+msgstr "Fehler: »%s« kann nicht als Zweig oder Version erkannt werden"
+
+#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
+msgid "Close"
+msgstr "Schließen"
+
+#: lib/checkout_op.tcl:169
+#, tcl-format
+msgid "Branch '%s' does not exist."
+msgstr "Zweig »%s« existiert nicht."
+
+#: lib/checkout_op.tcl:206
+#, tcl-format
+msgid ""
+"Branch '%s' already exists.\n"
+"\n"
+"It cannot fast-forward to %s.\n"
+"A merge is required."
+msgstr ""
+"Zweig »%s« existiert bereits.\n"
+"\n"
+"Zweig kann nicht mit »%s« schnellzusammengeführt werden. Reguläres "
+"Zusammenführen ist notwendig."
+
+#: lib/checkout_op.tcl:220
+#, tcl-format
+msgid "Merge strategy '%s' not supported."
+msgstr "Zusammenführungsmethode »%s« nicht unterstützt."
+
+#: lib/checkout_op.tcl:239
+#, tcl-format
+msgid "Failed to update '%s'."
+msgstr "Aktualisieren von »%s« fehlgeschlagen."
+
+#: lib/checkout_op.tcl:251
+msgid "Staging area (index) is already locked."
+msgstr "Bereitstellung (»index«) ist zur Bearbeitung gesperrt (»locked«)."
+
+#: lib/checkout_op.tcl:266
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before the current branch can be changed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"Der letzte geladene Status stimmt nicht mehr mit dem Projektarchiv überein.\n"
+"\n"
+"Ein anderes Git-Programm hat das Projektarchiv seit dem letzten Laden "
+"geändert.  Vor dem Wechseln des lokalen Zweigs muss neu geladen werden.\n"
+"\n"
+"Es wird gleich neu geladen.\n"
+
+#: lib/checkout_op.tcl:322
+#, tcl-format
+msgid "Updating working directory to '%s'..."
+msgstr "Arbeitskopie umstellen auf »%s«..."
+
+#: lib/checkout_op.tcl:353
+#, tcl-format
+msgid "Aborted checkout of '%s' (file level merging is required)."
+msgstr ""
+"Zweig umstellen von »%s« abgebrochen (Zusammenführen der Dateien ist "
+"notwendig)."
+
+#: lib/checkout_op.tcl:354
+msgid "File level merge required."
+msgstr "Zusammenführen der Dateien ist notwendig."
+
+#: lib/checkout_op.tcl:358
+#, tcl-format
+msgid "Staying on branch '%s'."
+msgstr "Es wird auf Zweig »%s« verblieben."
+
+#: lib/checkout_op.tcl:429
+msgid ""
+"You are no longer on a local branch.\n"
+"\n"
+"If you wanted to be on a branch, create one now starting from 'This Detached "
+"Checkout'."
+msgstr ""
+"Die Arbeitskopie ist nicht auf einem lokalen Zweig.\n"
+"\n"
+"Wenn Sie auf einem Zweig arbeiten möchten, erstellen Sie bitte jetzt einen "
+"Zweig mit der Auswahl »Abgetrennte Arbeitskopie-Version«."
+
+#: lib/checkout_op.tcl:446
+#, tcl-format
+msgid "Checked out '%s'."
+msgstr "Umgestellt auf »%s«."
+
+#: lib/checkout_op.tcl:478
+#, tcl-format
+msgid "Resetting '%s' to '%s' will lose the following commits:"
+msgstr "Zurücksetzen von »%s« nach »%s« wird folgende Versionen verwerfen:"
+
+#: lib/checkout_op.tcl:500
+msgid "Recovering lost commits may not be easy."
+msgstr ""
+"Verworfene Versionen können nur mit größerem Aufwand wiederhergestellt "
+"werden."
+
+#: lib/checkout_op.tcl:505
+#, tcl-format
+msgid "Reset '%s'?"
+msgstr "»%s« zurücksetzen?"
+
+#: lib/checkout_op.tcl:510 lib/merge.tcl:164
+msgid "Visualize"
+msgstr "Darstellen"
+
+#: lib/checkout_op.tcl:578
+#, tcl-format
+msgid ""
+"Failed to set current branch.\n"
+"\n"
+"This working directory is only partially switched.  We successfully updated "
+"your files, but failed to update an internal Git file.\n"
+"\n"
+"This should not have occurred.  %s will now close and give up."
+msgstr ""
+"Lokaler Zweig kann nicht gesetzt werden.\n"
+"\n"
+"Diese Arbeitskopie ist nur teilweise umgestellt. Die Dateien sind korrekt "
+"aktualisiert, aber einige interne Git-Dateien konnten nicht geändert "
+"werden.\n"
+"\n"
+"Dies ist ein interner Programmfehler von %s. Programm wird jetzt abgebrochen."
+
+#: lib/choose_font.tcl:39
+msgid "Select"
+msgstr "Auswählen"
+
+#: lib/choose_font.tcl:53
+msgid "Font Family"
+msgstr "Schriftfamilie"
+
+#: lib/choose_font.tcl:73
+msgid "Font Size"
+msgstr "Schriftgröße"
+
+#: lib/choose_font.tcl:90
+msgid "Font Example"
+msgstr "Schriftbeispiel"
+
+#: lib/choose_font.tcl:101
+msgid ""
+"This is example text.\n"
+"If you like this text, it can be your font."
+msgstr ""
+"Dies ist ein Beispieltext.\n"
+"Wenn Ihnen dieser Text gefällt, sollten Sie diese Schriftart wählen."
+
+#: lib/choose_repository.tcl:27
+msgid "Git Gui"
+msgstr "Git Gui"
+
+#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
+msgid "Create New Repository"
+msgstr "Neues Projektarchiv"
+
+#: lib/choose_repository.tcl:86
+msgid "New..."
+msgstr "Neu..."
+
+#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
+msgid "Clone Existing Repository"
+msgstr "Projektarchiv kopieren"
+
+#: lib/choose_repository.tcl:99
+msgid "Clone..."
+msgstr "Kopieren..."
+
+#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
+msgid "Open Existing Repository"
+msgstr "Projektarchiv öffnen"
+
+#: lib/choose_repository.tcl:112
+msgid "Open..."
+msgstr "Öffnen..."
+
+#: lib/choose_repository.tcl:125
+msgid "Recent Repositories"
+msgstr "Zuletzt benutzte Projektarchive"
+
+#: lib/choose_repository.tcl:131
+msgid "Open Recent Repository:"
+msgstr "Zuletzt benutztes Projektarchiv öffnen:"
+
+#: lib/choose_repository.tcl:294
+#, tcl-format
+msgid "Location %s already exists."
+msgstr "Projektarchiv »%s« existiert bereits."
+
+#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
+#: lib/choose_repository.tcl:314
+#, tcl-format
+msgid "Failed to create repository %s:"
+msgstr "Projektarchiv »%s« konnte nicht erstellt werden:"
+
+#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
+msgid "Directory:"
+msgstr "Verzeichnis:"
+
+#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
+#: lib/choose_repository.tcl:1013
+msgid "Git Repository"
+msgstr "Git Projektarchiv"
+
+#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
+#, tcl-format
+msgid "Directory %s already exists."
+msgstr "Verzeichnis »%s« existiert bereits."
+
+#: lib/choose_repository.tcl:442
+#, tcl-format
+msgid "File %s already exists."
+msgstr "Datei »%s« existiert bereits."
+
+#: lib/choose_repository.tcl:463
+msgid "Clone"
+msgstr "Kopieren"
+
+#: lib/choose_repository.tcl:476
+msgid "URL:"
+msgstr "URL:"
+
+#: lib/choose_repository.tcl:496
+msgid "Clone Type:"
+msgstr "Art der Kopie:"
+
+#: lib/choose_repository.tcl:502
+msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
+msgstr "Standard (schnell, teilweise redundant, Hardlinks)"
+
+#: lib/choose_repository.tcl:508
+msgid "Full Copy (Slower, Redundant Backup)"
+msgstr "Alles kopieren (langsamer, volle Redundanz)"
+
+#: lib/choose_repository.tcl:514
+msgid "Shared (Fastest, Not Recommended, No Backup)"
+msgstr "Verknüpft (schnell, nicht empfohlen, kein Backup)"
+
+#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
+#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
+#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
+#, tcl-format
+msgid "Not a Git repository: %s"
+msgstr "Kein Git-Projektarchiv in »%s« gefunden."
+
+#: lib/choose_repository.tcl:586
+msgid "Standard only available for local repository."
+msgstr "Standard ist nur für lokale Projektarchive verfügbar."
+
+#: lib/choose_repository.tcl:590
+msgid "Shared only available for local repository."
+msgstr "Verknüpft ist nur für lokale Projektarchive verfügbar."
+
+#: lib/choose_repository.tcl:617
+msgid "Failed to configure origin"
+msgstr "Der Ursprungsort konnte nicht eingerichtet werden"
+
+#: lib/choose_repository.tcl:629
+msgid "Counting objects"
+msgstr "Objekte werden gezählt"
+
+#: lib/choose_repository.tcl:630
+msgid "buckets"
+msgstr "Buckets"
+
+#: lib/choose_repository.tcl:654
+#, tcl-format
+msgid "Unable to copy objects/info/alternates: %s"
+msgstr "Kopien von Objekten/Info/Alternates konnten nicht erstellt werden: %s"
+
+#: lib/choose_repository.tcl:690
+#, tcl-format
+msgid "Nothing to clone from %s."
+msgstr "Von »%s« konnte nichts kopiert werden."
+
+#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
+#: lib/choose_repository.tcl:918
+msgid "The 'master' branch has not been initialized."
+msgstr "Der »master«-Zweig wurde noch nicht initialisiert."
+
+#: lib/choose_repository.tcl:705
+msgid "Hardlinks are unavailable.  Falling back to copying."
+msgstr "Hardlinks nicht verfügbar. Stattdessen wird kopiert."
+
+#: lib/choose_repository.tcl:717
+#, tcl-format
+msgid "Cloning from %s"
+msgstr "Kopieren von »%s«"
+
+#: lib/choose_repository.tcl:748
+msgid "Copying objects"
+msgstr "Objektdatenbank kopieren"
+
+#: lib/choose_repository.tcl:749
+msgid "KiB"
+msgstr "KB"
+
+#: lib/choose_repository.tcl:773
+#, tcl-format
+msgid "Unable to copy object: %s"
+msgstr "Objekt kann nicht kopiert werden: %s"
+
+#: lib/choose_repository.tcl:783
+msgid "Linking objects"
+msgstr "Objekte verlinken"
+
+#: lib/choose_repository.tcl:784
+msgid "objects"
+msgstr "Objekte"
+
+#: lib/choose_repository.tcl:792
+#, tcl-format
+msgid "Unable to hardlink object: %s"
+msgstr "Für Objekt konnte kein Hardlink erstellt werden: %s"
+
+#: lib/choose_repository.tcl:847
+msgid "Cannot fetch branches and objects.  See console output for details."
+msgstr "Zweige und Objekte konnten nicht angefordert werden.  Kontrollieren Sie die Ausgaben auf der Konsole für weitere Angaben."
+
+#: lib/choose_repository.tcl:858
+msgid "Cannot fetch tags.  See console output for details."
+msgstr "Markierungen konnten nicht angefordert werden.  Kontrollieren Sie die Ausgaben auf der Konsole für weitere Angaben."
+
+#: lib/choose_repository.tcl:882
+msgid "Cannot determine HEAD.  See console output for details."
+msgstr "Die Zweigspitze (HEAD) konnte nicht gefunden werden.  Kontrollieren Sie die Ausgaben auf der Konsole für weitere Angaben."
+
+#: lib/choose_repository.tcl:891
+#, tcl-format
+msgid "Unable to cleanup %s"
+msgstr "Verzeichnis »%s« kann nicht aufgeräumt werden."
+
+#: lib/choose_repository.tcl:897
+msgid "Clone failed."
+msgstr "Kopieren fehlgeschlagen."
+
+#: lib/choose_repository.tcl:904
+msgid "No default branch obtained."
+msgstr "Kein voreingestellter Zweig gefunden."
+
+#: lib/choose_repository.tcl:915
+#, tcl-format
+msgid "Cannot resolve %s as a commit."
+msgstr "»%s« wurde nicht als Version gefunden."
+
+#: lib/choose_repository.tcl:927
+msgid "Creating working directory"
+msgstr "Arbeitskopie erstellen"
+
+#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
+#: lib/index.tcl:193
+msgid "files"
+msgstr "Dateien"
+
+#: lib/choose_repository.tcl:957
+msgid "Initial file checkout failed."
+msgstr "Erstellen der Arbeitskopie fehlgeschlagen."
+
+#: lib/choose_repository.tcl:973
+msgid "Open"
+msgstr "Öffnen"
+
+#: lib/choose_repository.tcl:983
+msgid "Repository:"
+msgstr "Projektarchiv:"
+
+#: lib/choose_repository.tcl:1033
+#, tcl-format
+msgid "Failed to open repository %s:"
+msgstr "Projektarchiv »%s« konnte nicht geöffnet werden."
+
+#: lib/choose_rev.tcl:53
+msgid "This Detached Checkout"
+msgstr "Abgetrennte Arbeitskopie-Version"
+
+#: lib/choose_rev.tcl:60
+msgid "Revision Expression:"
+msgstr "Version Regexp-Ausdruck:"
+
+#: lib/choose_rev.tcl:74
+msgid "Local Branch"
+msgstr "Lokaler Zweig"
+
+#: lib/choose_rev.tcl:79
+msgid "Tracking Branch"
+msgstr "Übernahmezweig"
+
+#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
+msgid "Tag"
+msgstr "Markierung"
+
+#: lib/choose_rev.tcl:317
+#, tcl-format
+msgid "Invalid revision: %s"
+msgstr "Ungültige Version: %s"
+
+#: lib/choose_rev.tcl:338
+msgid "No revision selected."
+msgstr "Keine Version ausgewählt."
+
+#: lib/choose_rev.tcl:346
+msgid "Revision expression is empty."
+msgstr "Versions-Ausdruck ist leer."
+
+#: lib/choose_rev.tcl:530
+msgid "Updated"
+msgstr "Aktualisiert"
+
+#: lib/choose_rev.tcl:558
+msgid "URL"
+msgstr "URL"
+
+#: lib/commit.tcl:9
+msgid ""
+"There is nothing to amend.\n"
+"\n"
+"You are about to create the initial commit.  There is no commit before this "
+"to amend.\n"
+msgstr ""
+"Keine Version zur Nachbesserung vorhanden.\n"
+"\n"
+"Sie sind dabei, die erste Version zu übertragen. Es gibt keine existierende "
+"Version, die Sie nachbessern könnten.\n"
+
+#: lib/commit.tcl:18
+msgid ""
+"Cannot amend while merging.\n"
+"\n"
+"You are currently in the middle of a merge that has not been fully "
+"completed.  You cannot amend the prior commit unless you first abort the "
+"current merge activity.\n"
+msgstr ""
+"Nachbesserung währen Zusammenführung nicht möglich.\n"
+"\n"
+"Sie haben das Zusammenführen von Versionen angefangen, aber noch nicht "
+"beendet. Sie können keine vorige Übertragung nachbessern, solange eine "
+"unfertige Zusammenführung existiert. Dazu müssen Sie die Zusammenführung "
+"beenden oder abbrechen.\n"
+
+#: lib/commit.tcl:49
+msgid "Error loading commit data for amend:"
+msgstr "Fehler beim Laden der Versionsdaten für Nachbessern:"
+
+#: lib/commit.tcl:76
+msgid "Unable to obtain your identity:"
+msgstr "Benutzername konnte nicht bestimmt werden:"
+
+#: lib/commit.tcl:81
+msgid "Invalid GIT_COMMITTER_IDENT:"
+msgstr "Ungültiger Wert von GIT_COMMITTER_INDENT:"
+
+#: lib/commit.tcl:133
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before another commit can be created.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"Der letzte geladene Status stimmt nicht mehr mit dem Projektarchiv überein.\n"
+"\n"
+"Ein anderes Git-Programm hat das Projektarchiv seit dem letzten Laden "
+"geändert.  Vor dem Eintragen einer neuen Version muss neu geladen werden.\n"
+"\n"
+"Es wird gleich neu geladen.\n"
+
+#: lib/commit.tcl:154
+#, tcl-format
+msgid ""
+"Unmerged files cannot be committed.\n"
+"\n"
+"File %s has merge conflicts.  You must resolve them and stage the file "
+"before committing.\n"
+msgstr ""
+"Nicht zusammengeführte Dateien können nicht eingetragen werden.\n"
+"\n"
+"Die Datei »%s« hat noch nicht aufgelöste Zusammenführungs-Konflikte. Sie "
+"müssen diese Konflikte auflösen, bevor Sie eintragen können.\n"
+
+#: lib/commit.tcl:162
+#, tcl-format
+msgid ""
+"Unknown file state %s detected.\n"
+"\n"
+"File %s cannot be committed by this program.\n"
+msgstr ""
+"Unbekannter Dateizustand »%s«.\n"
+"\n"
+"Datei »%s« kann nicht eingetragen werden.\n"
+
+#: lib/commit.tcl:170
+msgid ""
+"No changes to commit.\n"
+"\n"
+"You must stage at least 1 file before you can commit.\n"
+msgstr ""
+"Keine Änderungen vorhanden, die eingetragen werden könnten.\n"
+"\n"
+"Sie müssen mindestens eine Datei bereitstellen, bevor Sie eintragen können.\n"
+
+#: lib/commit.tcl:183
+msgid ""
+"Please supply a commit message.\n"
+"\n"
+"A good commit message has the following format:\n"
+"\n"
+"- First line: Describe in one sentence what you did.\n"
+"- Second line: Blank\n"
+"- Remaining lines: Describe why this change is good.\n"
+msgstr ""
+"Bitte geben Sie eine Versionsbeschreibung ein.\n"
+"\n"
+"Eine gute Versionsbeschreibung enthält folgende Abschnitte:\n"
+"\n"
+"- Erste Zeile: Eine Zusammenfassung, was man gemacht hat.\n"
+"\n"
+"- Zweite Zeile: Leerzeile\n"
+"\n"
+"- Rest: Eine ausführliche Beschreibung, warum diese Änderung hilfreich ist.\n"
+
+#: lib/commit.tcl:257
+msgid "write-tree failed:"
+msgstr "write-tree fehlgeschlagen:"
+
+#: lib/commit.tcl:275
+#, tcl-format
+msgid "Commit %s appears to be corrupt"
+msgstr "Version »%s« scheint beschädigt zu sein"
+
+#: lib/commit.tcl:279
+msgid ""
+"No changes to commit.\n"
+"\n"
+"No files were modified by this commit and it was not a merge commit.\n"
+"\n"
+"A rescan will be automatically started now.\n"
+msgstr ""
+"Keine Änderungen einzutragen.\n"
+"\n"
+"Es gibt keine geänderte Datei bei dieser Version und es wurde auch nichts "
+"zusammengeführt.\n"
+"\n"
+"Das Arbeitsverzeichnis wird daher jetzt neu geladen.\n"
+
+#: lib/commit.tcl:286
+msgid "No changes to commit."
+msgstr "Keine Änderungen, die eingetragen werden können."
+
+#: lib/commit.tcl:303
+#, tcl-format
+msgid "warning: Tcl does not support encoding '%s'."
+msgstr "Warning: Tcl/Tk unterstützt die Zeichencodierung »%s« nicht."
+
+#: lib/commit.tcl:317
+msgid "commit-tree failed:"
+msgstr "commit-tree fehlgeschlagen:"
+
+#: lib/commit.tcl:339
+msgid "update-ref failed:"
+msgstr "update-ref fehlgeschlagen:"
+
+#: lib/commit.tcl:430
+#, tcl-format
+msgid "Created commit %s: %s"
+msgstr "Version %s übertragen: %s"
+
+#: lib/console.tcl:57
+msgid "Working... please wait..."
+msgstr "Verarbeitung. Bitte warten..."
+
+#: lib/console.tcl:183
+msgid "Success"
+msgstr "Erfolgreich"
+
+#: lib/console.tcl:196
+msgid "Error: Command Failed"
+msgstr "Fehler: Kommando fehlgeschlagen"
+
+#: lib/database.tcl:43
+msgid "Number of loose objects"
+msgstr "Anzahl unverknüpfter Objekte"
+
+#: lib/database.tcl:44
+msgid "Disk space used by loose objects"
+msgstr "Festplattenplatz von unverknüpften Objekten"
+
+#: lib/database.tcl:45
+msgid "Number of packed objects"
+msgstr "Anzahl komprimierter Objekte"
+
+#: lib/database.tcl:46
+msgid "Number of packs"
+msgstr "Anzahl Komprimierungseinheiten"
+
+#: lib/database.tcl:47
+msgid "Disk space used by packed objects"
+msgstr "Festplattenplatz von komprimierten Objekten"
+
+#: lib/database.tcl:48
+msgid "Packed objects waiting for pruning"
+msgstr "Komprimierte Objekte, die zum Entfernen vorgesehen sind"
+
+#: lib/database.tcl:49
+msgid "Garbage files"
+msgstr "Dateien im Mülleimer"
+
+#: lib/database.tcl:72
+msgid "Compressing the object database"
+msgstr "Objektdatenbank komprimieren"
+
+#: lib/database.tcl:83
+msgid "Verifying the object database with fsck-objects"
+msgstr "Die Objektdatenbank durch »fsck-objects« überprüfen lassen"
+
+#: lib/database.tcl:108
+#, tcl-format
+msgid ""
+"This repository currently has approximately %i loose objects.\n"
+"\n"
+"To maintain optimal performance it is strongly recommended that you compress "
+"the database when more than %i loose objects exist.\n"
+"\n"
+"Compress the database now?"
+msgstr ""
+"Dieses Projektarchiv enthält ungefähr %i nicht verknüpfte Objekte.\n"
+"\n"
+"Für eine optimale Performance wird empfohlen, die Datenbank des "
+"Projektarchivs zu komprimieren, sobald mehr als %i nicht verknüpfte Objekte "
+"vorliegen.\n"
+"\n"
+"Soll die Datenbank jetzt komprimiert werden?"
+
+#: lib/date.tcl:25
+#, tcl-format
+msgid "Invalid date from Git: %s"
+msgstr "Ungültiges Datum von Git: %s"
+
+#: lib/diff.tcl:42
+#, tcl-format
+msgid ""
+"No differences detected.\n"
+"\n"
+"%s has no changes.\n"
+"\n"
+"The modification date of this file was updated by another application, but "
+"the content within the file was not changed.\n"
+"\n"
+"A rescan will be automatically started to find other files which may have "
+"the same state."
+msgstr ""
+"Keine Änderungen feststellbar.\n"
+"\n"
+"»%s« enthält keine Änderungen. Zwar wurde das Änderungsdatum dieser Datei "
+"von einem anderen Programm modifiziert, aber der Inhalt der Datei ist "
+"unverändert.\n"
+"\n"
+"Das Arbeitsverzeichnis wird jetzt neu geladen, um diese Änderung bei allen "
+"Dateien zu prüfen."
+
+#: lib/diff.tcl:81
+#, tcl-format
+msgid "Loading diff of %s..."
+msgstr "Vergleich von »%s« laden..."
+
+#: lib/diff.tcl:114 lib/diff.tcl:184
+#, tcl-format
+msgid "Unable to display %s"
+msgstr "Datei »%s« kann nicht angezeigt werden"
+
+#: lib/diff.tcl:115
+msgid "Error loading file:"
+msgstr "Fehler beim Laden der Datei:"
+
+#: lib/diff.tcl:122
+msgid "Git Repository (subproject)"
+msgstr "Git-Projektarchiv (Unterprojekt)"
+
+#: lib/diff.tcl:134
+msgid "* Binary file (not showing content)."
+msgstr "* Binärdatei (Inhalt wird nicht angezeigt)"
+
+#: lib/diff.tcl:185
+msgid "Error loading diff:"
+msgstr "Fehler beim Laden des Vergleichs:"
+
+#: lib/diff.tcl:302
+msgid "Failed to unstage selected hunk."
+msgstr "Fehler beim Herausnehmen der gewählten Dateien aus der Bereitstellung."
+
+#: lib/diff.tcl:309
+msgid "Failed to stage selected hunk."
+msgstr "Fehler beim Bereitstellen der gewählten Dateien."
+
+#: lib/error.tcl:12 lib/error.tcl:102
+msgid "error"
+msgstr "Fehler"
+
+#: lib/error.tcl:28
+msgid "warning"
+msgstr "Warnung"
+
+#: lib/error.tcl:81
+msgid "You must correct the above errors before committing."
+msgstr ""
+"Sie müssen die obigen Fehler zuerst beheben, bevor Sie eintragen können."
+
+#: lib/index.tcl:6
+msgid "Unable to unlock the index."
+msgstr "Bereitstellung kann nicht wieder freigegeben werden."
+
+#: lib/index.tcl:15
+msgid "Index Error"
+msgstr "Fehler in Bereitstellung"
+
+#: lib/index.tcl:21
+msgid ""
+"Updating the Git index failed.  A rescan will be automatically started to "
+"resynchronize git-gui."
+msgstr "Das Aktualisieren der Git-Bereitstellung ist fehlgeschlagen. Eine allgemeine Git-Aktualisierung wird jetzt gestartet, um git-gui wieder mit git zu synchronisieren."
+
+#: lib/index.tcl:27
+msgid "Continue"
+msgstr "Fortsetzen"
+
+#: lib/index.tcl:31
+msgid "Unlock Index"
+msgstr "Bereitstellung freigeben"
+
+#: lib/index.tcl:282
+#, tcl-format
+msgid "Unstaging %s from commit"
+msgstr "Datei »%s« aus der Bereitstellung herausnehmen"
+
+#: lib/index.tcl:326
+#, tcl-format
+msgid "Adding %s"
+msgstr "»%s« hinzufügen..."
+
+#: lib/index.tcl:381
+#, tcl-format
+msgid "Revert changes in file %s?"
+msgstr "Änderungen in Datei »%s« revidieren?"
+
+#: lib/index.tcl:383
+#, tcl-format
+msgid "Revert changes in these %i files?"
+msgstr "Änderungen in den gewählten %i Dateien revidieren?"
+
+#: lib/index.tcl:389
+msgid "Any unstaged changes will be permanently lost by the revert."
+msgstr ""
+"Alle nicht bereitgestellten Änderungen werden beim Revidieren verloren gehen."
+
+#: lib/index.tcl:392
+msgid "Do Nothing"
+msgstr "Nichts tun"
+
+#: lib/merge.tcl:13
+msgid ""
+"Cannot merge while amending.\n"
+"\n"
+"You must finish amending this commit before starting any type of merge.\n"
+msgstr ""
+"Zusammenführen kann nicht gleichzeitig mit Nachbessern durchgeführt werden.\n"
+"\n"
+"Sie müssen zuerst die Nachbesserungs-Version abschließen, bevor Sie "
+"zusammenführen können.\n"
+
+#: lib/merge.tcl:27
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before a merge can be performed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"Der letzte geladene Status stimmt nicht mehr mit dem Projektarchiv überein.\n"
+"\n"
+"Ein anderes Git-Programm hat das Projektarchiv seit dem letzten Laden "
+"geändert.  Vor einem Zusammenführen muss neu geladen werden.\n"
+"\n"
+"Es wird gleich neu geladen.\n"
+
+#: lib/merge.tcl:44
+#, tcl-format
+msgid ""
+"You are in the middle of a conflicted merge.\n"
+"\n"
+"File %s has merge conflicts.\n"
+"\n"
+"You must resolve them, stage the file, and commit to complete the current "
+"merge.  Only then can you begin another merge.\n"
+msgstr ""
+"Zusammenführung mit Konflikten.\n"
+"\n"
+"Die Datei »%s« enthält Konflikte beim Zusammenführen. Sie müssen diese "
+"Konflikte per Hand auflösen. Anschließend müssen Sie die Datei wieder "
+"bereitstellen und eintragen, um die Zusammenführung abzuschließen. Erst "
+"danach kann eine neue Zusammenführung begonnen werden.\n"
+
+#: lib/merge.tcl:54
+#, tcl-format
+msgid ""
+"You are in the middle of a change.\n"
+"\n"
+"File %s is modified.\n"
+"\n"
+"You should complete the current commit before starting a merge.  Doing so "
+"will help you abort a failed merge, should the need arise.\n"
+msgstr ""
+"Es liegen Änderungen vor.\n"
+"\n"
+"Die Datei »%s« wurde geändert.  Sie sollten zuerst die bereitgestellte "
+"Version abschließen, bevor Sie eine Zusammenführung beginnen.  Mit dieser "
+"Reihenfolge können Sie mögliche Konflikte beim Zusammenführen wesentlich "
+"einfacher beheben oder abbrechen.\n"
+
+#: lib/merge.tcl:106
+#, tcl-format
+msgid "%s of %s"
+msgstr "%s von %s"
+
+#: lib/merge.tcl:119
+#, tcl-format
+msgid "Merging %s and %s"
+msgstr "Zusammenführen von %s und %s"
+
+#: lib/merge.tcl:131
+msgid "Merge completed successfully."
+msgstr "Zusammenführen erfolgreich abgeschlossen."
+
+#: lib/merge.tcl:133
+msgid "Merge failed.  Conflict resolution is required."
+msgstr "Zusammenführen fehlgeschlagen. Konfliktauflösung ist notwendig."
+
+#: lib/merge.tcl:158
+#, tcl-format
+msgid "Merge Into %s"
+msgstr "Zusammenführen in %s"
+
+#: lib/merge.tcl:177
+msgid "Revision To Merge"
+msgstr "Zusammenzuführende Version"
+
+#: lib/merge.tcl:212
+msgid ""
+"Cannot abort while amending.\n"
+"\n"
+"You must finish amending this commit.\n"
+msgstr ""
+"Abbruch der Nachbesserung ist nicht möglich.\n"
+"\n"
+"Sie müssen die Nachbesserung der Version abschließen.\n"
+
+#: lib/merge.tcl:222
+msgid ""
+"Abort merge?\n"
+"\n"
+"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with aborting the current merge?"
+msgstr ""
+"Zusammenführen abbrechen?\n"
+"\n"
+"Wenn Sie abbrechen, gehen alle noch nicht eingetragenen Änderungen "
+"verloren.\n"
+"\n"
+"Zusammenführen jetzt abbrechen?"
+
+#: lib/merge.tcl:228
+msgid ""
+"Reset changes?\n"
+"\n"
+"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with resetting the current changes?"
+msgstr ""
+"Änderungen zurücksetzen?\n"
+"\n"
+"Wenn Sie zurücksetzen, gehen alle noch nicht eingetragenen Änderungen "
+"verloren.\n"
+"\n"
+"Änderungen jetzt zurücksetzen?"
+
+#: lib/merge.tcl:239
+msgid "Aborting"
+msgstr "Abbruch"
+
+#: lib/merge.tcl:266
+msgid "Abort failed."
+msgstr "Abbruch fehlgeschlagen."
+
+#: lib/merge.tcl:268
+msgid "Abort completed.  Ready."
+msgstr "Abbruch durchgeführt. Bereit."
+
+#: lib/option.tcl:82
+msgid "Restore Defaults"
+msgstr "Voreinstellungen wiederherstellen"
+
+#: lib/option.tcl:86
+msgid "Save"
+msgstr "Speichern"
+
+#: lib/option.tcl:96
+#, tcl-format
+msgid "%s Repository"
+msgstr "Projektarchiv %s"
+
+#: lib/option.tcl:97
+msgid "Global (All Repositories)"
+msgstr "Global (Alle Projektarchive)"
+
+#: lib/option.tcl:103
+msgid "User Name"
+msgstr "Benutzername"
+
+#: lib/option.tcl:104
+msgid "Email Address"
+msgstr "E-Mail-Adresse"
+
+#: lib/option.tcl:106
+msgid "Summarize Merge Commits"
+msgstr "Zusammenführungs-Versionen zusammenfassen"
+
+#: lib/option.tcl:107
+msgid "Merge Verbosity"
+msgstr "Ausführlichkeit der Zusammenführen-Meldungen"
+
+#: lib/option.tcl:108
+msgid "Show Diffstat After Merge"
+msgstr "Vergleichsstatistik nach Zusammenführen anzeigen"
+
+#: lib/option.tcl:110
+msgid "Trust File Modification Timestamps"
+msgstr "Auf Dateiänderungsdatum verlassen"
+
+#: lib/option.tcl:111
+msgid "Prune Tracking Branches During Fetch"
+msgstr "Übernahmezweige entfernen während Anforderung"
+
+#: lib/option.tcl:112
+msgid "Match Tracking Branches"
+msgstr "Passend zu Übernahmezweig"
+
+#: lib/option.tcl:113
+msgid "Number of Diff Context Lines"
+msgstr "Anzahl der Kontextzeilen beim Vergleich"
+
+#: lib/option.tcl:114
+msgid "New Branch Name Template"
+msgstr "Namensvorschlag für neue Zweige"
+
+#: lib/option.tcl:176
+msgid "Change Font"
+msgstr "Schriftart ändern"
+
+#: lib/option.tcl:180
+#, tcl-format
+msgid "Choose %s"
+msgstr "%s wählen"
+
+#: lib/option.tcl:186
+msgid "pt."
+msgstr "pt."
+
+#: lib/option.tcl:200
+msgid "Preferences"
+msgstr "Einstellungen"
+
+#: lib/option.tcl:235
+msgid "Failed to completely save options:"
+msgstr "Optionen konnten nicht gespeichert werden:"
+
+#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
+msgid "Delete Remote Branch"
+msgstr "Zweig im anderen Projektarchiv löschen"
+
+#: lib/remote_branch_delete.tcl:47
+msgid "From Repository"
+msgstr "Von Projektarchiv"
+
+#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
+msgid "Remote:"
+msgstr "Anderes Archiv:"
+
+#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
+msgid "Arbitrary URL:"
+msgstr "Kommunikation mit URL:"
+
+#: lib/remote_branch_delete.tcl:84
+msgid "Branches"
+msgstr "Zweige"
+
+#: lib/remote_branch_delete.tcl:109
+msgid "Delete Only If"
+msgstr "Löschen, falls"
+
+#: lib/remote_branch_delete.tcl:111
+msgid "Merged Into:"
+msgstr "Zusammenführen mit:"
+
+#: lib/remote_branch_delete.tcl:119
+msgid "Always (Do not perform merge checks)"
+msgstr "Immer (Keine Zusammenführungsprüfung)"
+
+#: lib/remote_branch_delete.tcl:152
+msgid "A branch is required for 'Merged Into'."
+msgstr "Für »Zusammenführen mit« muss ein Zweig angegeben werden."
+
+#: lib/remote_branch_delete.tcl:184
+#, tcl-format
+msgid ""
+"The following branches are not completely merged into %s:\n"
+"\n"
+" - %s"
+msgstr ""
+"Folgende Zweige sind noch nicht mit »%s« zusammengeführt:\n"
+"\n"
+" - %s"
+
+#: lib/remote_branch_delete.tcl:189
+#, tcl-format
+msgid ""
+"One or more of the merge tests failed because you have not fetched the "
+"necessary commits.  Try fetching from %s first."
+msgstr ""
+"Ein oder mehrere Zusammenführungen sind fehlgeschlagen, da Sie nicht die "
+"notwendigen Versionen vorher angefordert haben.  Sie sollten versuchen, "
+"zuerst von »%s« anzufordern."
+
+#: lib/remote_branch_delete.tcl:207
+msgid "Please select one or more branches to delete."
+msgstr "Bitte wählen Sie mindestens einen Zweig, der gelöscht werden soll."
+
+#: lib/remote_branch_delete.tcl:216
+msgid ""
+"Recovering deleted branches is difficult.\n"
+"\n"
+"Delete the selected branches?"
+msgstr ""
+"Das Wiederherstellen von gelöschten Zweigen ist nur mit größerem Aufwand "
+"möglich.\n"
+"\n"
+"Sollen die ausgewählten Zweige gelöscht werden?"
+
+#: lib/remote_branch_delete.tcl:226
+#, tcl-format
+msgid "Deleting branches from %s"
+msgstr "Zweige auf »%s« werden gelöscht"
+
+#: lib/remote_branch_delete.tcl:286
+msgid "No repository selected."
+msgstr "Kein Projektarchiv ausgewählt."
+
+#: lib/remote_branch_delete.tcl:291
+#, tcl-format
+msgid "Scanning %s..."
+msgstr "»%s« laden..."
+
+#: lib/remote.tcl:165
+msgid "Prune from"
+msgstr "Entfernen von"
+
+#: lib/remote.tcl:170
+msgid "Fetch from"
+msgstr "Anfordern von"
+
+#: lib/remote.tcl:213
+msgid "Push to"
+msgstr "Versenden nach"
+
+#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
+msgid "Cannot write shortcut:"
+msgstr "Fehler beim Schreiben der Verknüpfung:"
+
+#: lib/shortcut.tcl:136
+msgid "Cannot write icon:"
+msgstr "Fehler beim Erstellen des Icons:"
+
+#: lib/status_bar.tcl:83
+#, tcl-format
+msgid "%s ... %*i of %*i %s (%3i%%)"
+msgstr "%s ... %*i von %*i %s (%3i%%)"
+
+#: lib/transport.tcl:6
+#, tcl-format
+msgid "fetch %s"
+msgstr "»%s« anfordern"
+
+#: lib/transport.tcl:7
+#, tcl-format
+msgid "Fetching new changes from %s"
+msgstr "Neue Änderungen von »%s« holen"
+
+#: lib/transport.tcl:18
+#, tcl-format
+msgid "remote prune %s"
+msgstr "Entfernen von »%s« im anderen Archiv"
+
+#: lib/transport.tcl:19
+#, tcl-format
+msgid "Pruning tracking branches deleted from %s"
+msgstr "Übernahmezweige entfernen, die in »%s« gelöscht wurden"
+
+#: lib/transport.tcl:25 lib/transport.tcl:71
+#, tcl-format
+msgid "push %s"
+msgstr "»%s« versenden..."
+
+#: lib/transport.tcl:26
+#, tcl-format
+msgid "Pushing changes to %s"
+msgstr "Änderungen nach »%s« versenden"
+
+#: lib/transport.tcl:72
+#, tcl-format
+msgid "Pushing %s %s to %s"
+msgstr "%s %s nach %s versenden"
+
+#: lib/transport.tcl:89
+msgid "Push Branches"
+msgstr "Zweige versenden"
+
+#: lib/transport.tcl:103
+msgid "Source Branches"
+msgstr "Herkunftszweige"
+
+#: lib/transport.tcl:120
+msgid "Destination Repository"
+msgstr "Ziel-Projektarchiv"
+
+#: lib/transport.tcl:158
+msgid "Transfer Options"
+msgstr "Netzwerk-Einstellungen"
+
+#: lib/transport.tcl:160
+msgid "Force overwrite existing branch (may discard changes)"
+msgstr ""
+"Überschreiben von existierenden Zweigen erzwingen (könnte Änderungen löschen)"
+
+#: lib/transport.tcl:164
+msgid "Use thin pack (for slow network connections)"
+msgstr "Kompaktes Datenformat benutzen (für langsame Netzverbindungen)"
+
+#: lib/transport.tcl:168
+msgid "Include tags"
+msgstr "Mit Markierungen übertragen"
diff --git a/git-gui/po/git-gui.pot b/git-gui/po/git-gui.pot
new file mode 100644 (file)
index 0000000..dfa48ae
--- /dev/null
@@ -0,0 +1,1742 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-11-24 10:36+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
+#: git-gui.sh:733
+msgid "git-gui: fatal error"
+msgstr ""
+
+#: git-gui.sh:565
+#, tcl-format
+msgid "Invalid font specified in %s:"
+msgstr ""
+
+#: git-gui.sh:590
+msgid "Main Font"
+msgstr ""
+
+#: git-gui.sh:591
+msgid "Diff/Console Font"
+msgstr ""
+
+#: git-gui.sh:605
+msgid "Cannot find git in PATH."
+msgstr ""
+
+#: git-gui.sh:632
+msgid "Cannot parse Git version string:"
+msgstr ""
+
+#: git-gui.sh:650
+#, tcl-format
+msgid ""
+"Git version cannot be determined.\n"
+"\n"
+"%s claims it is version '%s'.\n"
+"\n"
+"%s requires at least Git 1.5.0 or later.\n"
+"\n"
+"Assume '%s' is version 1.5.0?\n"
+msgstr ""
+
+#: git-gui.sh:888
+msgid "Git directory not found:"
+msgstr ""
+
+#: git-gui.sh:895
+msgid "Cannot move to top of working directory:"
+msgstr ""
+
+#: git-gui.sh:902
+msgid "Cannot use funny .git directory:"
+msgstr ""
+
+#: git-gui.sh:907
+msgid "No working directory"
+msgstr ""
+
+#: git-gui.sh:1054
+msgid "Refreshing file status..."
+msgstr ""
+
+#: git-gui.sh:1119
+msgid "Scanning for modified files ..."
+msgstr ""
+
+#: git-gui.sh:1294 lib/browser.tcl:245
+msgid "Ready."
+msgstr ""
+
+#: git-gui.sh:1560
+msgid "Unmodified"
+msgstr ""
+
+#: git-gui.sh:1562
+msgid "Modified, not staged"
+msgstr ""
+
+#: git-gui.sh:1563 git-gui.sh:1568
+msgid "Staged for commit"
+msgstr ""
+
+#: git-gui.sh:1564 git-gui.sh:1569
+msgid "Portions staged for commit"
+msgstr ""
+
+#: git-gui.sh:1565 git-gui.sh:1570
+msgid "Staged for commit, missing"
+msgstr ""
+
+#: git-gui.sh:1567
+msgid "Untracked, not staged"
+msgstr ""
+
+#: git-gui.sh:1572
+msgid "Missing"
+msgstr ""
+
+#: git-gui.sh:1573
+msgid "Staged for removal"
+msgstr ""
+
+#: git-gui.sh:1574
+msgid "Staged for removal, still present"
+msgstr ""
+
+#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
+msgid "Requires merge resolution"
+msgstr ""
+
+#: git-gui.sh:1614
+msgid "Starting gitk... please wait..."
+msgstr ""
+
+#: git-gui.sh:1623
+#, tcl-format
+msgid ""
+"Unable to start gitk:\n"
+"\n"
+"%s does not exist"
+msgstr ""
+
+#: git-gui.sh:1823 lib/choose_repository.tcl:35
+msgid "Repository"
+msgstr ""
+
+#: git-gui.sh:1824
+msgid "Edit"
+msgstr ""
+
+#: git-gui.sh:1826 lib/choose_rev.tcl:560
+msgid "Branch"
+msgstr ""
+
+#: git-gui.sh:1829 lib/choose_rev.tcl:547
+msgid "Commit@@noun"
+msgstr ""
+
+#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
+msgid "Merge"
+msgstr ""
+
+#: git-gui.sh:1833 lib/choose_rev.tcl:556
+msgid "Remote"
+msgstr ""
+
+#: git-gui.sh:1842
+msgid "Browse Current Branch's Files"
+msgstr ""
+
+#: git-gui.sh:1846
+msgid "Browse Branch Files..."
+msgstr ""
+
+#: git-gui.sh:1851
+msgid "Visualize Current Branch's History"
+msgstr ""
+
+#: git-gui.sh:1855
+msgid "Visualize All Branch History"
+msgstr ""
+
+#: git-gui.sh:1862
+#, tcl-format
+msgid "Browse %s's Files"
+msgstr ""
+
+#: git-gui.sh:1864
+#, tcl-format
+msgid "Visualize %s's History"
+msgstr ""
+
+#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
+msgid "Database Statistics"
+msgstr ""
+
+#: git-gui.sh:1872 lib/database.tcl:34
+msgid "Compress Database"
+msgstr ""
+
+#: git-gui.sh:1875
+msgid "Verify Database"
+msgstr ""
+
+#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
+#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
+msgid "Create Desktop Icon"
+msgstr ""
+
+#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
+msgid "Quit"
+msgstr ""
+
+#: git-gui.sh:1902
+msgid "Undo"
+msgstr ""
+
+#: git-gui.sh:1905
+msgid "Redo"
+msgstr ""
+
+#: git-gui.sh:1909 git-gui.sh:2403
+msgid "Cut"
+msgstr ""
+
+#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
+#: lib/console.tcl:67
+msgid "Copy"
+msgstr ""
+
+#: git-gui.sh:1915 git-gui.sh:2409
+msgid "Paste"
+msgstr ""
+
+#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
+#: lib/remote_branch_delete.tcl:38
+msgid "Delete"
+msgstr ""
+
+#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
+msgid "Select All"
+msgstr ""
+
+#: git-gui.sh:1931
+msgid "Create..."
+msgstr ""
+
+#: git-gui.sh:1937
+msgid "Checkout..."
+msgstr ""
+
+#: git-gui.sh:1943
+msgid "Rename..."
+msgstr ""
+
+#: git-gui.sh:1948 git-gui.sh:2048
+msgid "Delete..."
+msgstr ""
+
+#: git-gui.sh:1953
+msgid "Reset..."
+msgstr ""
+
+#: git-gui.sh:1965 git-gui.sh:2350
+msgid "New Commit"
+msgstr ""
+
+#: git-gui.sh:1973 git-gui.sh:2357
+msgid "Amend Last Commit"
+msgstr ""
+
+#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
+msgid "Rescan"
+msgstr ""
+
+#: git-gui.sh:1988
+msgid "Stage To Commit"
+msgstr ""
+
+#: git-gui.sh:1994
+msgid "Stage Changed Files To Commit"
+msgstr ""
+
+#: git-gui.sh:2000
+msgid "Unstage From Commit"
+msgstr ""
+
+#: git-gui.sh:2005 lib/index.tcl:393
+msgid "Revert Changes"
+msgstr ""
+
+#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
+msgid "Sign Off"
+msgstr ""
+
+#: git-gui.sh:2016 git-gui.sh:2333
+msgid "Commit@@verb"
+msgstr ""
+
+#: git-gui.sh:2027
+msgid "Local Merge..."
+msgstr ""
+
+#: git-gui.sh:2032
+msgid "Abort Merge..."
+msgstr ""
+
+#: git-gui.sh:2044
+msgid "Push..."
+msgstr ""
+
+#: git-gui.sh:2055 lib/choose_repository.tcl:40
+msgid "Apple"
+msgstr ""
+
+#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
+#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
+#, tcl-format
+msgid "About %s"
+msgstr ""
+
+#: git-gui.sh:2062
+msgid "Preferences..."
+msgstr ""
+
+#: git-gui.sh:2070 git-gui.sh:2595
+msgid "Options..."
+msgstr ""
+
+#: git-gui.sh:2076 lib/choose_repository.tcl:46
+msgid "Help"
+msgstr ""
+
+#: git-gui.sh:2117
+msgid "Online Documentation"
+msgstr ""
+
+#: git-gui.sh:2201
+#, tcl-format
+msgid "fatal: cannot stat path %s: No such file or directory"
+msgstr ""
+
+#: git-gui.sh:2234
+msgid "Current Branch:"
+msgstr ""
+
+#: git-gui.sh:2255
+msgid "Staged Changes (Will Commit)"
+msgstr ""
+
+#: git-gui.sh:2274
+msgid "Unstaged Changes"
+msgstr ""
+
+#: git-gui.sh:2323
+msgid "Stage Changed"
+msgstr ""
+
+#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
+msgid "Push"
+msgstr ""
+
+#: git-gui.sh:2369
+msgid "Initial Commit Message:"
+msgstr ""
+
+#: git-gui.sh:2370
+msgid "Amended Commit Message:"
+msgstr ""
+
+#: git-gui.sh:2371
+msgid "Amended Initial Commit Message:"
+msgstr ""
+
+#: git-gui.sh:2372
+msgid "Amended Merge Commit Message:"
+msgstr ""
+
+#: git-gui.sh:2373
+msgid "Merge Commit Message:"
+msgstr ""
+
+#: git-gui.sh:2374
+msgid "Commit Message:"
+msgstr ""
+
+#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
+msgid "Copy All"
+msgstr ""
+
+#: git-gui.sh:2443 lib/blame.tcl:104
+msgid "File:"
+msgstr ""
+
+#: git-gui.sh:2545
+msgid "Refresh"
+msgstr ""
+
+#: git-gui.sh:2566
+msgid "Apply/Reverse Hunk"
+msgstr ""
+
+#: git-gui.sh:2572
+msgid "Decrease Font Size"
+msgstr ""
+
+#: git-gui.sh:2576
+msgid "Increase Font Size"
+msgstr ""
+
+#: git-gui.sh:2581
+msgid "Show Less Context"
+msgstr ""
+
+#: git-gui.sh:2588
+msgid "Show More Context"
+msgstr ""
+
+#: git-gui.sh:2602
+msgid "Unstage Hunk From Commit"
+msgstr ""
+
+#: git-gui.sh:2604
+msgid "Stage Hunk For Commit"
+msgstr ""
+
+#: git-gui.sh:2623
+msgid "Initializing..."
+msgstr ""
+
+#: git-gui.sh:2718
+#, tcl-format
+msgid ""
+"Possible environment issues exist.\n"
+"\n"
+"The following environment variables are probably\n"
+"going to be ignored by any Git subprocess run\n"
+"by %s:\n"
+"\n"
+msgstr ""
+
+#: git-gui.sh:2748
+msgid ""
+"\n"
+"This is due to a known issue with the\n"
+"Tcl binary distributed by Cygwin."
+msgstr ""
+
+#: git-gui.sh:2753
+#, tcl-format
+msgid ""
+"\n"
+"\n"
+"A good replacement for %s\n"
+"is placing values for the user.name and\n"
+"user.email settings into your personal\n"
+"~/.gitconfig file.\n"
+msgstr ""
+
+#: lib/about.tcl:25
+msgid "git-gui - a graphical user interface for Git."
+msgstr ""
+
+#: lib/blame.tcl:77
+msgid "File Viewer"
+msgstr ""
+
+#: lib/blame.tcl:81
+msgid "Commit:"
+msgstr ""
+
+#: lib/blame.tcl:249
+msgid "Copy Commit"
+msgstr ""
+
+#: lib/blame.tcl:369
+#, tcl-format
+msgid "Reading %s..."
+msgstr ""
+
+#: lib/blame.tcl:473
+msgid "Loading copy/move tracking annotations..."
+msgstr ""
+
+#: lib/blame.tcl:493
+msgid "lines annotated"
+msgstr ""
+
+#: lib/blame.tcl:674
+msgid "Loading original location annotations..."
+msgstr ""
+
+#: lib/blame.tcl:677
+msgid "Annotation complete."
+msgstr ""
+
+#: lib/blame.tcl:731
+msgid "Loading annotation..."
+msgstr ""
+
+#: lib/blame.tcl:787
+msgid "Author:"
+msgstr ""
+
+#: lib/blame.tcl:791
+msgid "Committer:"
+msgstr ""
+
+#: lib/blame.tcl:796
+msgid "Original File:"
+msgstr ""
+
+#: lib/blame.tcl:910
+msgid "Originally By:"
+msgstr ""
+
+#: lib/blame.tcl:916
+msgid "In File:"
+msgstr ""
+
+#: lib/blame.tcl:921
+msgid "Copied Or Moved Here By:"
+msgstr ""
+
+#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
+msgid "Checkout Branch"
+msgstr ""
+
+#: lib/branch_checkout.tcl:23
+msgid "Checkout"
+msgstr ""
+
+#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
+#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
+#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
+#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
+msgid "Cancel"
+msgstr ""
+
+#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
+msgid "Revision"
+msgstr ""
+
+#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
+msgid "Options"
+msgstr ""
+
+#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
+msgid "Fetch Tracking Branch"
+msgstr ""
+
+#: lib/branch_checkout.tcl:44
+msgid "Detach From Local Branch"
+msgstr ""
+
+#: lib/branch_create.tcl:22
+msgid "Create Branch"
+msgstr ""
+
+#: lib/branch_create.tcl:27
+msgid "Create New Branch"
+msgstr ""
+
+#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
+msgid "Create"
+msgstr ""
+
+#: lib/branch_create.tcl:40
+msgid "Branch Name"
+msgstr ""
+
+#: lib/branch_create.tcl:43
+msgid "Name:"
+msgstr ""
+
+#: lib/branch_create.tcl:58
+msgid "Match Tracking Branch Name"
+msgstr ""
+
+#: lib/branch_create.tcl:66
+msgid "Starting Revision"
+msgstr ""
+
+#: lib/branch_create.tcl:72
+msgid "Update Existing Branch:"
+msgstr ""
+
+#: lib/branch_create.tcl:75
+msgid "No"
+msgstr ""
+
+#: lib/branch_create.tcl:80
+msgid "Fast Forward Only"
+msgstr ""
+
+#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
+msgid "Reset"
+msgstr ""
+
+#: lib/branch_create.tcl:97
+msgid "Checkout After Creation"
+msgstr ""
+
+#: lib/branch_create.tcl:131
+msgid "Please select a tracking branch."
+msgstr ""
+
+#: lib/branch_create.tcl:140
+#, tcl-format
+msgid "Tracking branch %s is not a branch in the remote repository."
+msgstr ""
+
+#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
+msgid "Please supply a branch name."
+msgstr ""
+
+#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
+#, tcl-format
+msgid "'%s' is not an acceptable branch name."
+msgstr ""
+
+#: lib/branch_delete.tcl:15
+msgid "Delete Branch"
+msgstr ""
+
+#: lib/branch_delete.tcl:20
+msgid "Delete Local Branch"
+msgstr ""
+
+#: lib/branch_delete.tcl:37
+msgid "Local Branches"
+msgstr ""
+
+#: lib/branch_delete.tcl:52
+msgid "Delete Only If Merged Into"
+msgstr ""
+
+#: lib/branch_delete.tcl:54
+msgid "Always (Do not perform merge test.)"
+msgstr ""
+
+#: lib/branch_delete.tcl:103
+#, tcl-format
+msgid "The following branches are not completely merged into %s:"
+msgstr ""
+
+#: lib/branch_delete.tcl:115
+msgid ""
+"Recovering deleted branches is difficult. \n"
+"\n"
+" Delete the selected branches?"
+msgstr ""
+
+#: lib/branch_delete.tcl:141
+#, tcl-format
+msgid ""
+"Failed to delete branches:\n"
+"%s"
+msgstr ""
+
+#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
+msgid "Rename Branch"
+msgstr ""
+
+#: lib/branch_rename.tcl:26
+msgid "Rename"
+msgstr ""
+
+#: lib/branch_rename.tcl:36
+msgid "Branch:"
+msgstr ""
+
+#: lib/branch_rename.tcl:39
+msgid "New Name:"
+msgstr ""
+
+#: lib/branch_rename.tcl:75
+msgid "Please select a branch to rename."
+msgstr ""
+
+#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
+#, tcl-format
+msgid "Branch '%s' already exists."
+msgstr ""
+
+#: lib/branch_rename.tcl:117
+#, tcl-format
+msgid "Failed to rename '%s'."
+msgstr ""
+
+#: lib/browser.tcl:17
+msgid "Starting..."
+msgstr ""
+
+#: lib/browser.tcl:26
+msgid "File Browser"
+msgstr ""
+
+#: lib/browser.tcl:125 lib/browser.tcl:142
+#, tcl-format
+msgid "Loading %s..."
+msgstr ""
+
+#: lib/browser.tcl:186
+msgid "[Up To Parent]"
+msgstr ""
+
+#: lib/browser.tcl:266 lib/browser.tcl:272
+msgid "Browse Branch Files"
+msgstr ""
+
+#: lib/browser.tcl:277 lib/choose_repository.tcl:391
+#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
+#: lib/choose_repository.tcl:989
+msgid "Browse"
+msgstr ""
+
+#: lib/checkout_op.tcl:79
+#, tcl-format
+msgid "Fetching %s from %s"
+msgstr ""
+
+#: lib/checkout_op.tcl:127
+#, tcl-format
+msgid "fatal: Cannot resolve %s"
+msgstr ""
+
+#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
+msgid "Close"
+msgstr ""
+
+#: lib/checkout_op.tcl:169
+#, tcl-format
+msgid "Branch '%s' does not exist."
+msgstr ""
+
+#: lib/checkout_op.tcl:206
+#, tcl-format
+msgid ""
+"Branch '%s' already exists.\n"
+"\n"
+"It cannot fast-forward to %s.\n"
+"A merge is required."
+msgstr ""
+
+#: lib/checkout_op.tcl:220
+#, tcl-format
+msgid "Merge strategy '%s' not supported."
+msgstr ""
+
+#: lib/checkout_op.tcl:239
+#, tcl-format
+msgid "Failed to update '%s'."
+msgstr ""
+
+#: lib/checkout_op.tcl:251
+msgid "Staging area (index) is already locked."
+msgstr ""
+
+#: lib/checkout_op.tcl:266
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before the current branch can be changed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+
+#: lib/checkout_op.tcl:322
+#, tcl-format
+msgid "Updating working directory to '%s'..."
+msgstr ""
+
+#: lib/checkout_op.tcl:353
+#, tcl-format
+msgid "Aborted checkout of '%s' (file level merging is required)."
+msgstr ""
+
+#: lib/checkout_op.tcl:354
+msgid "File level merge required."
+msgstr ""
+
+#: lib/checkout_op.tcl:358
+#, tcl-format
+msgid "Staying on branch '%s'."
+msgstr ""
+
+#: lib/checkout_op.tcl:429
+msgid ""
+"You are no longer on a local branch.\n"
+"\n"
+"If you wanted to be on a branch, create one now starting from 'This Detached "
+"Checkout'."
+msgstr ""
+
+#: lib/checkout_op.tcl:446
+#, tcl-format
+msgid "Checked out '%s'."
+msgstr ""
+
+#: lib/checkout_op.tcl:478
+#, tcl-format
+msgid "Resetting '%s' to '%s' will lose the following commits:"
+msgstr ""
+
+#: lib/checkout_op.tcl:500
+msgid "Recovering lost commits may not be easy."
+msgstr ""
+
+#: lib/checkout_op.tcl:505
+#, tcl-format
+msgid "Reset '%s'?"
+msgstr ""
+
+#: lib/checkout_op.tcl:510 lib/merge.tcl:164
+msgid "Visualize"
+msgstr ""
+
+#: lib/checkout_op.tcl:578
+#, tcl-format
+msgid ""
+"Failed to set current branch.\n"
+"\n"
+"This working directory is only partially switched.  We successfully updated "
+"your files, but failed to update an internal Git file.\n"
+"\n"
+"This should not have occurred.  %s will now close and give up."
+msgstr ""
+
+#: lib/choose_font.tcl:39
+msgid "Select"
+msgstr ""
+
+#: lib/choose_font.tcl:53
+msgid "Font Family"
+msgstr ""
+
+#: lib/choose_font.tcl:73
+msgid "Font Size"
+msgstr ""
+
+#: lib/choose_font.tcl:90
+msgid "Font Example"
+msgstr ""
+
+#: lib/choose_font.tcl:101
+msgid ""
+"This is example text.\n"
+"If you like this text, it can be your font."
+msgstr ""
+
+#: lib/choose_repository.tcl:27
+msgid "Git Gui"
+msgstr ""
+
+#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
+msgid "Create New Repository"
+msgstr ""
+
+#: lib/choose_repository.tcl:86
+msgid "New..."
+msgstr ""
+
+#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
+msgid "Clone Existing Repository"
+msgstr ""
+
+#: lib/choose_repository.tcl:99
+msgid "Clone..."
+msgstr ""
+
+#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
+msgid "Open Existing Repository"
+msgstr ""
+
+#: lib/choose_repository.tcl:112
+msgid "Open..."
+msgstr ""
+
+#: lib/choose_repository.tcl:125
+msgid "Recent Repositories"
+msgstr ""
+
+#: lib/choose_repository.tcl:131
+msgid "Open Recent Repository:"
+msgstr ""
+
+#: lib/choose_repository.tcl:294
+#, tcl-format
+msgid "Location %s already exists."
+msgstr ""
+
+#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
+#: lib/choose_repository.tcl:314
+#, tcl-format
+msgid "Failed to create repository %s:"
+msgstr ""
+
+#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
+msgid "Directory:"
+msgstr ""
+
+#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
+#: lib/choose_repository.tcl:1013
+msgid "Git Repository"
+msgstr ""
+
+#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
+#, tcl-format
+msgid "Directory %s already exists."
+msgstr ""
+
+#: lib/choose_repository.tcl:442
+#, tcl-format
+msgid "File %s already exists."
+msgstr ""
+
+#: lib/choose_repository.tcl:463
+msgid "Clone"
+msgstr ""
+
+#: lib/choose_repository.tcl:476
+msgid "URL:"
+msgstr ""
+
+#: lib/choose_repository.tcl:496
+msgid "Clone Type:"
+msgstr ""
+
+#: lib/choose_repository.tcl:502
+msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
+msgstr ""
+
+#: lib/choose_repository.tcl:508
+msgid "Full Copy (Slower, Redundant Backup)"
+msgstr ""
+
+#: lib/choose_repository.tcl:514
+msgid "Shared (Fastest, Not Recommended, No Backup)"
+msgstr ""
+
+#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
+#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
+#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
+#, tcl-format
+msgid "Not a Git repository: %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:586
+msgid "Standard only available for local repository."
+msgstr ""
+
+#: lib/choose_repository.tcl:590
+msgid "Shared only available for local repository."
+msgstr ""
+
+#: lib/choose_repository.tcl:617
+msgid "Failed to configure origin"
+msgstr ""
+
+#: lib/choose_repository.tcl:629
+msgid "Counting objects"
+msgstr ""
+
+#: lib/choose_repository.tcl:630
+msgid "buckets"
+msgstr ""
+
+#: lib/choose_repository.tcl:654
+#, tcl-format
+msgid "Unable to copy objects/info/alternates: %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:690
+#, tcl-format
+msgid "Nothing to clone from %s."
+msgstr ""
+
+#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
+#: lib/choose_repository.tcl:918
+msgid "The 'master' branch has not been initialized."
+msgstr ""
+
+#: lib/choose_repository.tcl:705
+msgid "Hardlinks are unavailable.  Falling back to copying."
+msgstr ""
+
+#: lib/choose_repository.tcl:717
+#, tcl-format
+msgid "Cloning from %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:748
+msgid "Copying objects"
+msgstr ""
+
+#: lib/choose_repository.tcl:749
+msgid "KiB"
+msgstr ""
+
+#: lib/choose_repository.tcl:773
+#, tcl-format
+msgid "Unable to copy object: %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:783
+msgid "Linking objects"
+msgstr ""
+
+#: lib/choose_repository.tcl:784
+msgid "objects"
+msgstr ""
+
+#: lib/choose_repository.tcl:792
+#, tcl-format
+msgid "Unable to hardlink object: %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:847
+msgid "Cannot fetch branches and objects.  See console output for details."
+msgstr ""
+
+#: lib/choose_repository.tcl:858
+msgid "Cannot fetch tags.  See console output for details."
+msgstr ""
+
+#: lib/choose_repository.tcl:882
+msgid "Cannot determine HEAD.  See console output for details."
+msgstr ""
+
+#: lib/choose_repository.tcl:891
+#, tcl-format
+msgid "Unable to cleanup %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:897
+msgid "Clone failed."
+msgstr ""
+
+#: lib/choose_repository.tcl:904
+msgid "No default branch obtained."
+msgstr ""
+
+#: lib/choose_repository.tcl:915
+#, tcl-format
+msgid "Cannot resolve %s as a commit."
+msgstr ""
+
+#: lib/choose_repository.tcl:927
+msgid "Creating working directory"
+msgstr ""
+
+#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
+#: lib/index.tcl:193
+msgid "files"
+msgstr ""
+
+#: lib/choose_repository.tcl:957
+msgid "Initial file checkout failed."
+msgstr ""
+
+#: lib/choose_repository.tcl:973
+msgid "Open"
+msgstr ""
+
+#: lib/choose_repository.tcl:983
+msgid "Repository:"
+msgstr ""
+
+#: lib/choose_repository.tcl:1033
+#, tcl-format
+msgid "Failed to open repository %s:"
+msgstr ""
+
+#: lib/choose_rev.tcl:53
+msgid "This Detached Checkout"
+msgstr ""
+
+#: lib/choose_rev.tcl:60
+msgid "Revision Expression:"
+msgstr ""
+
+#: lib/choose_rev.tcl:74
+msgid "Local Branch"
+msgstr ""
+
+#: lib/choose_rev.tcl:79
+msgid "Tracking Branch"
+msgstr ""
+
+#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
+msgid "Tag"
+msgstr ""
+
+#: lib/choose_rev.tcl:317
+#, tcl-format
+msgid "Invalid revision: %s"
+msgstr ""
+
+#: lib/choose_rev.tcl:338
+msgid "No revision selected."
+msgstr ""
+
+#: lib/choose_rev.tcl:346
+msgid "Revision expression is empty."
+msgstr ""
+
+#: lib/choose_rev.tcl:530
+msgid "Updated"
+msgstr ""
+
+#: lib/choose_rev.tcl:558
+msgid "URL"
+msgstr ""
+
+#: lib/commit.tcl:9
+msgid ""
+"There is nothing to amend.\n"
+"\n"
+"You are about to create the initial commit.  There is no commit before this "
+"to amend.\n"
+msgstr ""
+
+#: lib/commit.tcl:18
+msgid ""
+"Cannot amend while merging.\n"
+"\n"
+"You are currently in the middle of a merge that has not been fully "
+"completed.  You cannot amend the prior commit unless you first abort the "
+"current merge activity.\n"
+msgstr ""
+
+#: lib/commit.tcl:49
+msgid "Error loading commit data for amend:"
+msgstr ""
+
+#: lib/commit.tcl:76
+msgid "Unable to obtain your identity:"
+msgstr ""
+
+#: lib/commit.tcl:81
+msgid "Invalid GIT_COMMITTER_IDENT:"
+msgstr ""
+
+#: lib/commit.tcl:133
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before another commit can be created.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+
+#: lib/commit.tcl:154
+#, tcl-format
+msgid ""
+"Unmerged files cannot be committed.\n"
+"\n"
+"File %s has merge conflicts.  You must resolve them and stage the file "
+"before committing.\n"
+msgstr ""
+
+#: lib/commit.tcl:162
+#, tcl-format
+msgid ""
+"Unknown file state %s detected.\n"
+"\n"
+"File %s cannot be committed by this program.\n"
+msgstr ""
+
+#: lib/commit.tcl:170
+msgid ""
+"No changes to commit.\n"
+"\n"
+"You must stage at least 1 file before you can commit.\n"
+msgstr ""
+
+#: lib/commit.tcl:183
+msgid ""
+"Please supply a commit message.\n"
+"\n"
+"A good commit message has the following format:\n"
+"\n"
+"- First line: Describe in one sentence what you did.\n"
+"- Second line: Blank\n"
+"- Remaining lines: Describe why this change is good.\n"
+msgstr ""
+
+#: lib/commit.tcl:257
+msgid "write-tree failed:"
+msgstr ""
+
+#: lib/commit.tcl:275
+#, tcl-format
+msgid "Commit %s appears to be corrupt"
+msgstr ""
+
+#: lib/commit.tcl:279
+msgid ""
+"No changes to commit.\n"
+"\n"
+"No files were modified by this commit and it was not a merge commit.\n"
+"\n"
+"A rescan will be automatically started now.\n"
+msgstr ""
+
+#: lib/commit.tcl:286
+msgid "No changes to commit."
+msgstr ""
+
+#: lib/commit.tcl:303
+#, tcl-format
+msgid "warning: Tcl does not support encoding '%s'."
+msgstr ""
+
+#: lib/commit.tcl:317
+msgid "commit-tree failed:"
+msgstr ""
+
+#: lib/commit.tcl:339
+msgid "update-ref failed:"
+msgstr ""
+
+#: lib/commit.tcl:430
+#, tcl-format
+msgid "Created commit %s: %s"
+msgstr ""
+
+#: lib/console.tcl:57
+msgid "Working... please wait..."
+msgstr ""
+
+#: lib/console.tcl:183
+msgid "Success"
+msgstr ""
+
+#: lib/console.tcl:196
+msgid "Error: Command Failed"
+msgstr ""
+
+#: lib/database.tcl:43
+msgid "Number of loose objects"
+msgstr ""
+
+#: lib/database.tcl:44
+msgid "Disk space used by loose objects"
+msgstr ""
+
+#: lib/database.tcl:45
+msgid "Number of packed objects"
+msgstr ""
+
+#: lib/database.tcl:46
+msgid "Number of packs"
+msgstr ""
+
+#: lib/database.tcl:47
+msgid "Disk space used by packed objects"
+msgstr ""
+
+#: lib/database.tcl:48
+msgid "Packed objects waiting for pruning"
+msgstr ""
+
+#: lib/database.tcl:49
+msgid "Garbage files"
+msgstr ""
+
+#: lib/database.tcl:72
+msgid "Compressing the object database"
+msgstr ""
+
+#: lib/database.tcl:83
+msgid "Verifying the object database with fsck-objects"
+msgstr ""
+
+#: lib/database.tcl:108
+#, tcl-format
+msgid ""
+"This repository currently has approximately %i loose objects.\n"
+"\n"
+"To maintain optimal performance it is strongly recommended that you compress "
+"the database when more than %i loose objects exist.\n"
+"\n"
+"Compress the database now?"
+msgstr ""
+
+#: lib/date.tcl:25
+#, tcl-format
+msgid "Invalid date from Git: %s"
+msgstr ""
+
+#: lib/diff.tcl:42
+#, tcl-format
+msgid ""
+"No differences detected.\n"
+"\n"
+"%s has no changes.\n"
+"\n"
+"The modification date of this file was updated by another application, but "
+"the content within the file was not changed.\n"
+"\n"
+"A rescan will be automatically started to find other files which may have "
+"the same state."
+msgstr ""
+
+#: lib/diff.tcl:81
+#, tcl-format
+msgid "Loading diff of %s..."
+msgstr ""
+
+#: lib/diff.tcl:114 lib/diff.tcl:184
+#, tcl-format
+msgid "Unable to display %s"
+msgstr ""
+
+#: lib/diff.tcl:115
+msgid "Error loading file:"
+msgstr ""
+
+#: lib/diff.tcl:122
+msgid "Git Repository (subproject)"
+msgstr ""
+
+#: lib/diff.tcl:134
+msgid "* Binary file (not showing content)."
+msgstr ""
+
+#: lib/diff.tcl:185
+msgid "Error loading diff:"
+msgstr ""
+
+#: lib/diff.tcl:302
+msgid "Failed to unstage selected hunk."
+msgstr ""
+
+#: lib/diff.tcl:309
+msgid "Failed to stage selected hunk."
+msgstr ""
+
+#: lib/error.tcl:12 lib/error.tcl:102
+msgid "error"
+msgstr ""
+
+#: lib/error.tcl:28
+msgid "warning"
+msgstr ""
+
+#: lib/error.tcl:81
+msgid "You must correct the above errors before committing."
+msgstr ""
+
+#: lib/index.tcl:6
+msgid "Unable to unlock the index."
+msgstr ""
+
+#: lib/index.tcl:15
+msgid "Index Error"
+msgstr ""
+
+#: lib/index.tcl:21
+msgid ""
+"Updating the Git index failed.  A rescan will be automatically started to "
+"resynchronize git-gui."
+msgstr ""
+
+#: lib/index.tcl:27
+msgid "Continue"
+msgstr ""
+
+#: lib/index.tcl:31
+msgid "Unlock Index"
+msgstr ""
+
+#: lib/index.tcl:282
+#, tcl-format
+msgid "Unstaging %s from commit"
+msgstr ""
+
+#: lib/index.tcl:326
+#, tcl-format
+msgid "Adding %s"
+msgstr ""
+
+#: lib/index.tcl:381
+#, tcl-format
+msgid "Revert changes in file %s?"
+msgstr ""
+
+#: lib/index.tcl:383
+#, tcl-format
+msgid "Revert changes in these %i files?"
+msgstr ""
+
+#: lib/index.tcl:389
+msgid "Any unstaged changes will be permanently lost by the revert."
+msgstr ""
+
+#: lib/index.tcl:392
+msgid "Do Nothing"
+msgstr ""
+
+#: lib/merge.tcl:13
+msgid ""
+"Cannot merge while amending.\n"
+"\n"
+"You must finish amending this commit before starting any type of merge.\n"
+msgstr ""
+
+#: lib/merge.tcl:27
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before a merge can be performed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+
+#: lib/merge.tcl:44
+#, tcl-format
+msgid ""
+"You are in the middle of a conflicted merge.\n"
+"\n"
+"File %s has merge conflicts.\n"
+"\n"
+"You must resolve them, stage the file, and commit to complete the current "
+"merge.  Only then can you begin another merge.\n"
+msgstr ""
+
+#: lib/merge.tcl:54
+#, tcl-format
+msgid ""
+"You are in the middle of a change.\n"
+"\n"
+"File %s is modified.\n"
+"\n"
+"You should complete the current commit before starting a merge.  Doing so "
+"will help you abort a failed merge, should the need arise.\n"
+msgstr ""
+
+#: lib/merge.tcl:106
+#, tcl-format
+msgid "%s of %s"
+msgstr ""
+
+#: lib/merge.tcl:119
+#, tcl-format
+msgid "Merging %s and %s"
+msgstr ""
+
+#: lib/merge.tcl:131
+msgid "Merge completed successfully."
+msgstr ""
+
+#: lib/merge.tcl:133
+msgid "Merge failed.  Conflict resolution is required."
+msgstr ""
+
+#: lib/merge.tcl:158
+#, tcl-format
+msgid "Merge Into %s"
+msgstr ""
+
+#: lib/merge.tcl:177
+msgid "Revision To Merge"
+msgstr ""
+
+#: lib/merge.tcl:212
+msgid ""
+"Cannot abort while amending.\n"
+"\n"
+"You must finish amending this commit.\n"
+msgstr ""
+
+#: lib/merge.tcl:222
+msgid ""
+"Abort merge?\n"
+"\n"
+"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with aborting the current merge?"
+msgstr ""
+
+#: lib/merge.tcl:228
+msgid ""
+"Reset changes?\n"
+"\n"
+"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with resetting the current changes?"
+msgstr ""
+
+#: lib/merge.tcl:239
+msgid "Aborting"
+msgstr ""
+
+#: lib/merge.tcl:266
+msgid "Abort failed."
+msgstr ""
+
+#: lib/merge.tcl:268
+msgid "Abort completed.  Ready."
+msgstr ""
+
+#: lib/option.tcl:82
+msgid "Restore Defaults"
+msgstr ""
+
+#: lib/option.tcl:86
+msgid "Save"
+msgstr ""
+
+#: lib/option.tcl:96
+#, tcl-format
+msgid "%s Repository"
+msgstr ""
+
+#: lib/option.tcl:97
+msgid "Global (All Repositories)"
+msgstr ""
+
+#: lib/option.tcl:103
+msgid "User Name"
+msgstr ""
+
+#: lib/option.tcl:104
+msgid "Email Address"
+msgstr ""
+
+#: lib/option.tcl:106
+msgid "Summarize Merge Commits"
+msgstr ""
+
+#: lib/option.tcl:107
+msgid "Merge Verbosity"
+msgstr ""
+
+#: lib/option.tcl:108
+msgid "Show Diffstat After Merge"
+msgstr ""
+
+#: lib/option.tcl:110
+msgid "Trust File Modification Timestamps"
+msgstr ""
+
+#: lib/option.tcl:111
+msgid "Prune Tracking Branches During Fetch"
+msgstr ""
+
+#: lib/option.tcl:112
+msgid "Match Tracking Branches"
+msgstr ""
+
+#: lib/option.tcl:113
+msgid "Number of Diff Context Lines"
+msgstr ""
+
+#: lib/option.tcl:114
+msgid "New Branch Name Template"
+msgstr ""
+
+#: lib/option.tcl:176
+msgid "Change Font"
+msgstr ""
+
+#: lib/option.tcl:180
+#, tcl-format
+msgid "Choose %s"
+msgstr ""
+
+#: lib/option.tcl:186
+msgid "pt."
+msgstr ""
+
+#: lib/option.tcl:200
+msgid "Preferences"
+msgstr ""
+
+#: lib/option.tcl:235
+msgid "Failed to completely save options:"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
+msgid "Delete Remote Branch"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:47
+msgid "From Repository"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
+msgid "Remote:"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
+msgid "Arbitrary URL:"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:84
+msgid "Branches"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:109
+msgid "Delete Only If"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:111
+msgid "Merged Into:"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:119
+msgid "Always (Do not perform merge checks)"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:152
+msgid "A branch is required for 'Merged Into'."
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:184
+#, tcl-format
+msgid ""
+"The following branches are not completely merged into %s:\n"
+"\n"
+" - %s"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:189
+#, tcl-format
+msgid ""
+"One or more of the merge tests failed because you have not fetched the "
+"necessary commits.  Try fetching from %s first."
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:207
+msgid "Please select one or more branches to delete."
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:216
+msgid ""
+"Recovering deleted branches is difficult.\n"
+"\n"
+"Delete the selected branches?"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:226
+#, tcl-format
+msgid "Deleting branches from %s"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:286
+msgid "No repository selected."
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:291
+#, tcl-format
+msgid "Scanning %s..."
+msgstr ""
+
+#: lib/remote.tcl:165
+msgid "Prune from"
+msgstr ""
+
+#: lib/remote.tcl:170
+msgid "Fetch from"
+msgstr ""
+
+#: lib/remote.tcl:213
+msgid "Push to"
+msgstr ""
+
+#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
+msgid "Cannot write shortcut:"
+msgstr ""
+
+#: lib/shortcut.tcl:136
+msgid "Cannot write icon:"
+msgstr ""
+
+#: lib/status_bar.tcl:83
+#, tcl-format
+msgid "%s ... %*i of %*i %s (%3i%%)"
+msgstr ""
+
+#: lib/transport.tcl:6
+#, tcl-format
+msgid "fetch %s"
+msgstr ""
+
+#: lib/transport.tcl:7
+#, tcl-format
+msgid "Fetching new changes from %s"
+msgstr ""
+
+#: lib/transport.tcl:18
+#, tcl-format
+msgid "remote prune %s"
+msgstr ""
+
+#: lib/transport.tcl:19
+#, tcl-format
+msgid "Pruning tracking branches deleted from %s"
+msgstr ""
+
+#: lib/transport.tcl:25 lib/transport.tcl:71
+#, tcl-format
+msgid "push %s"
+msgstr ""
+
+#: lib/transport.tcl:26
+#, tcl-format
+msgid "Pushing changes to %s"
+msgstr ""
+
+#: lib/transport.tcl:72
+#, tcl-format
+msgid "Pushing %s %s to %s"
+msgstr ""
+
+#: lib/transport.tcl:89
+msgid "Push Branches"
+msgstr ""
+
+#: lib/transport.tcl:103
+msgid "Source Branches"
+msgstr ""
+
+#: lib/transport.tcl:120
+msgid "Destination Repository"
+msgstr ""
+
+#: lib/transport.tcl:158
+msgid "Transfer Options"
+msgstr ""
+
+#: lib/transport.tcl:160
+msgid "Force overwrite existing branch (may discard changes)"
+msgstr ""
+
+#: lib/transport.tcl:164
+msgid "Use thin pack (for slow network connections)"
+msgstr ""
+
+#: lib/transport.tcl:168
+msgid "Include tags"
+msgstr ""
diff --git a/git-gui/po/glossary/Makefile b/git-gui/po/glossary/Makefile
new file mode 100644 (file)
index 0000000..749aa2e
--- /dev/null
@@ -0,0 +1,9 @@
+PO_TEMPLATE = git-gui-glossary.pot
+
+ALL_POFILES = $(wildcard *.po)
+
+$(PO_TEMPLATE): $(subst .pot,.txt,$(PO_TEMPLATE))
+       ./txt-to-pot.sh $< > $@
+
+update-po:: git-gui-glossary.pot
+       $(foreach p, $(ALL_POFILES), echo Updating $p ; msgmerge -U $p $(PO_TEMPLATE) ; )
diff --git a/git-gui/po/glossary/de.po b/git-gui/po/glossary/de.po
new file mode 100644 (file)
index 0000000..c94786c
--- /dev/null
@@ -0,0 +1,185 @@
+# Translation of git-gui glossary to German
+# Copyright (C) 2007 Shawn Pearce, et al.
+# This file is distributed under the same license as the git package.
+# Christian Stimming <stimming@tuhh.de>, 2007
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: git-gui glossary\n"
+"POT-Creation-Date: 2007-10-19 21:43+0200\n"
+"PO-Revision-Date: 2007-10-20 15:24+0200\n"
+"Last-Translator: Christian Stimming <stimming@tuhh.de>\n"
+"Language-Team: German \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
+msgid ""
+"English Term (Dear translator: This file will never be visible to the user!)"
+msgstr ""
+"Deutsche Übersetzung.\n"
+"Andere deutsche SCM:\n"
+"  http://tortoisesvn.net/docs/release/TortoiseSVN_de/index.html und http://"
+"tortoisesvn.tigris.org/svn/tortoisesvn/trunk/Languages/Tortoise_de.po "
+"(username=guest, password empty, gut),\n"
+"  http://msdn.microsoft.com/de-de/library/ms181038(vs.80).aspx (MS Visual "
+"Source Safe, kommerziell),\n"
+"  http://cvsbook.red-bean.com/translations/german/Kap_06.html "
+"(mittelmäßig),\n"
+"  http://tortoisecvs.cvs.sourceforge.net/tortoisecvs/po/TortoiseCVS/de_DE.po?"
+"view=markup (mittelmäßig),\n"
+"  http://rapidsvn.tigris.org/svn/rapidsvn/trunk/src/locale/de/rapidsvn.po "
+"(username=guest, password empty, schlecht)"
+
+#. ""
+msgid "amend"
+msgstr "nachbessern (ergänzen)"
+
+#. ""
+msgid "annotate"
+msgstr "annotieren"
+
+#. "A 'branch' is an active line of development."
+msgid "branch [noun]"
+msgstr "Zweig"
+
+#. ""
+msgid "branch [verb]"
+msgstr "verzweigen"
+
+#. ""
+msgid "checkout [noun]"
+msgstr ""
+"Arbeitskopie (Erstellung einer Arbeitskopie; Auscheck? Ausspielung? Abruf? "
+"Source Safe: Auscheckvorgang)"
+
+#. "The action of updating the working tree to a revision which was stored in the object database."
+msgid "checkout [verb]"
+msgstr ""
+"Arbeitskopie erstellen; Zweig umstellen [checkout a branch] (auschecken? "
+"ausspielen? abrufen? Source Safe: auschecken)"
+
+#. ""
+msgid "clone [verb]"
+msgstr "kopieren"
+
+#. "A single point in the git history."
+msgid "commit [noun]"
+msgstr ""
+"Version; Eintragung; Änderung (Buchung?, Eintragung?, Übertragung?, "
+"Sendung?, Übergabe?, Einspielung?, Ablagevorgang?)"
+
+#. "The action of storing a new snapshot of the project's state in the git history."
+msgid "commit [verb]"
+msgstr ""
+"eintragen (TortoiseSVN: übertragen; Source Safe: einchecken; senden?, "
+"übergeben?, einspielen?, einpflegen?, ablegen?)"
+
+#. ""
+msgid "diff [noun]"
+msgstr "Vergleich (Source Safe: Unterschiede)"
+
+#. ""
+msgid "diff [verb]"
+msgstr "vergleichen"
+
+#. "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
+msgid "fast forward merge"
+msgstr "Schnellzusammenführung"
+
+#. "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
+msgid "fetch"
+msgstr "anfordern (holen?)"
+
+#. "A collection of files. The index is a stored version of your working tree."
+msgid "index (in git-gui: staging area)"
+msgstr "Bereitstellung"
+
+#. "A successful merge results in the creation of a new commit representing the result of the merge."
+msgid "merge [noun]"
+msgstr "Zusammenführung"
+
+#. "To bring the contents of another branch into the current branch."
+msgid "merge [verb]"
+msgstr "zusammenführen"
+
+#. ""
+msgid "message"
+msgstr "Beschreibung (Meldung?, Nachricht?; Source Safe: Kommentar)"
+
+#. "Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in 'remotes/<name>'."
+msgid "prune"
+msgstr "entfernen"
+
+#. "Pulling a branch means to fetch it and merge it."
+msgid "pull"
+msgstr "übernehmen (ziehen?)"
+
+#. "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
+msgid "push"
+msgstr "versenden (ausliefern? hochladen? verschicken? schieben?)"
+
+#. ""
+msgid "redo"
+msgstr "wiederholen"
+
+#. "An other repository ('remote'). One might have a set of remotes whose branches one tracks."
+msgid "remote"
+msgstr "Andere Archive (Gegenseite?, Entfernte?, Server?)"
+
+#. "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
+msgid "repository"
+msgstr "Projektarchiv"
+
+#. ""
+msgid "reset"
+msgstr "zurücksetzen (zurückkehren?)"
+
+#. ""
+msgid "revert"
+msgstr "revidieren"
+
+#. "A particular state of files and directories which was stored in the object database."
+msgid "revision"
+msgstr "Version (TortoiseSVN: Revision; Source Safe: Version)"
+
+#. ""
+msgid "sign off"
+msgstr "abzeichnen (gegenzeichnen?, freizeichnen?, absegnen?)"
+
+#. ""
+msgid "staging area"
+msgstr "Bereitstellung"
+
+#. ""
+msgid "status"
+msgstr "Status"
+
+#. "A ref pointing to a tag or commit object"
+msgid "tag [noun]"
+msgstr "Markierung"
+
+#. ""
+msgid "tag [verb]"
+msgstr "markieren"
+
+#. "A regular git branch that is used to follow changes from another repository."
+msgid "tracking branch"
+msgstr "Übernahmezweig"
+
+#. ""
+msgid "undo"
+msgstr "rückgängig"
+
+#. ""
+msgid "update"
+msgstr "aktualisieren"
+
+#. ""
+msgid "verify"
+msgstr "überprüfen"
+
+#. "The tree of actual checked out files."
+msgid "working copy, working tree"
+msgstr "Arbeitskopie"
diff --git a/git-gui/po/glossary/git-gui-glossary.pot b/git-gui/po/glossary/git-gui-glossary.pot
new file mode 100644 (file)
index 0000000..48af803
--- /dev/null
@@ -0,0 +1,164 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR Free Software Foundation, Inc.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2007-10-19 21:43+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: ENCODING\n"
+
+#. "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
+msgid "English Term (Dear translator: This file will never be visible to the user!)"
+msgstr ""
+
+#. ""
+msgid "amend"
+msgstr ""
+
+#. ""
+msgid "annotate"
+msgstr ""
+
+#. "A 'branch' is an active line of development."
+msgid "branch [noun]"
+msgstr ""
+
+#. ""
+msgid "branch [verb]"
+msgstr ""
+
+#. ""
+msgid "checkout [noun]"
+msgstr ""
+
+#. "The action of updating the working tree to a revision which was stored in the object database."
+msgid "checkout [verb]"
+msgstr ""
+
+#. ""
+msgid "clone [verb]"
+msgstr ""
+
+#. "A single point in the git history."
+msgid "commit [noun]"
+msgstr ""
+
+#. "The action of storing a new snapshot of the project's state in the git history."
+msgid "commit [verb]"
+msgstr ""
+
+#. ""
+msgid "diff [noun]"
+msgstr ""
+
+#. ""
+msgid "diff [verb]"
+msgstr ""
+
+#. "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
+msgid "fast forward merge"
+msgstr ""
+
+#. "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
+msgid "fetch"
+msgstr ""
+
+#. "A collection of files. The index is a stored version of your working tree."
+msgid "index (in git-gui: staging area)"
+msgstr ""
+
+#. "A successful merge results in the creation of a new commit representing the result of the merge."
+msgid "merge [noun]"
+msgstr ""
+
+#. "To bring the contents of another branch into the current branch."
+msgid "merge [verb]"
+msgstr ""
+
+#. ""
+msgid "message"
+msgstr ""
+
+#. "Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in 'remotes/<name>'."
+msgid "prune"
+msgstr ""
+
+#. "Pulling a branch means to fetch it and merge it."
+msgid "pull"
+msgstr ""
+
+#. "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
+msgid "push"
+msgstr ""
+
+#. ""
+msgid "redo"
+msgstr ""
+
+#. "An other repository ('remote'). One might have a set of remotes whose branches one tracks."
+msgid "remote"
+msgstr ""
+
+#. "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
+msgid "repository"
+msgstr ""
+
+#. ""
+msgid "reset"
+msgstr ""
+
+#. ""
+msgid "revert"
+msgstr ""
+
+#. "A particular state of files and directories which was stored in the object database."
+msgid "revision"
+msgstr ""
+
+#. ""
+msgid "sign off"
+msgstr ""
+
+#. ""
+msgid "staging area"
+msgstr ""
+
+#. ""
+msgid "status"
+msgstr ""
+
+#. "A ref pointing to a tag or commit object"
+msgid "tag [noun]"
+msgstr ""
+
+#. ""
+msgid "tag [verb]"
+msgstr ""
+
+#. "A regular git branch that is used to follow changes from another repository."
+msgid "tracking branch"
+msgstr ""
+
+#. ""
+msgid "undo"
+msgstr ""
+
+#. ""
+msgid "update"
+msgstr ""
+
+#. ""
+msgid "verify"
+msgstr ""
+
+#. "The tree of actual checked out files."
+msgid "working copy, working tree"
+msgstr ""
+
diff --git a/git-gui/po/glossary/git-gui-glossary.txt b/git-gui/po/glossary/git-gui-glossary.txt
new file mode 100644 (file)
index 0000000..500d0a0
--- /dev/null
@@ -0,0 +1,37 @@
+"English Term (Dear translator: This file will never be visible to the user!)" "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
+"amend"        ""
+"annotate"     ""
+"branch [noun]"        "A 'branch' is an active line of development."
+"branch [verb]"        ""
+"checkout [noun]"      ""
+"checkout [verb]"      "The action of updating the working tree to a revision which was stored in the object database."
+"clone [verb]" ""
+"commit [noun]"        "A single point in the git history."
+"commit [verb]"        "The action of storing a new snapshot of the project's state in the git history."
+"diff [noun]"  ""
+"diff [verb]"  ""
+"fast forward merge"   "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
+"fetch"        "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
+"index (in git-gui: staging area)"     "A collection of files. The index is a stored version of your working tree."
+"merge [noun]" "A successful merge results in the creation of a new commit representing the result of the merge."
+"merge [verb]" "To bring the contents of another branch into the current branch."
+"message"      ""
+"prune"        "Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in 'remotes/<name>'."
+"pull" "Pulling a branch means to fetch it and merge it."
+"push" "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
+"redo" ""
+"remote"       "An other repository ('remote'). One might have a set of remotes whose branches one tracks."
+"repository"   "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
+"reset"        ""
+"revert"       ""
+"revision"     "A particular state of files and directories which was stored in the object database."
+"sign off"     ""
+"staging area" ""
+"status"       ""
+"tag [noun]"   "A ref pointing to a tag or commit object"
+"tag [verb]"   ""
+"tracking branch"      "A regular git branch that is used to follow changes from another repository."
+"undo" ""
+"update"       ""
+"verify"       ""
+"working copy, working tree"   "The tree of actual checked out files."
diff --git a/git-gui/po/glossary/it.po b/git-gui/po/glossary/it.po
new file mode 100644 (file)
index 0000000..bb46b48
--- /dev/null
@@ -0,0 +1,184 @@
+# Translation of git-gui glossary to Italian
+# Copyright (C) 2007 Shawn Pearce, et al.
+# This file is distributed under the same license as the git package.
+# Christian Stimming <stimming@tuhh.de>, 2007
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: git-gui glossary\n"
+"POT-Creation-Date: 2007-10-19 21:43+0200\n"
+"PO-Revision-Date: 2007-10-10 15:24+0200\n"
+"Last-Translator: Michele Ballabio <barra_cuda@katamail.com>\n"
+"Language-Team: Italian \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
+msgid ""
+"English Term (Dear translator: This file will never be visible to the user!)"
+msgstr ""
+"Traduzione italiana.\n"
+"Altri SCM in italiano:\n"
+"  http://tortoisesvn.tigris.org/svn/tortoisesvn/trunk/Languages/Tortoise_it."
+"po (username=guest, password empty),\n"
+"  http://tortoisecvs.cvs.sourceforge.net/tortoisecvs/po/TortoiseCVS/it_IT.po?"
+"view=markup ,\n"
+"  http://rapidsvn.tigris.org/svn/rapidsvn/trunk/src/locale/it_IT/rapidsvn.po "
+"(username=guest, password empty)"
+
+#. ""
+msgid "amend"
+msgstr "correggere, correzione"
+
+#. ""
+msgid "annotate"
+msgstr "annotare, annotazione"
+
+#. "A 'branch' is an active line of development."
+msgid "branch [noun]"
+msgstr "ramo, diramazione, ramificazione"
+
+#. ""
+msgid "branch [verb]"
+msgstr "creare ramo, ramificare, diramare"
+
+#. ""
+msgid "checkout [noun]"
+msgstr "attivazione, checkout, revisione attiva, prelievo (TortoiseCVS)?"
+
+#. "The action of updating the working tree to a revision which was stored in the object database."
+msgid "checkout [verb]"
+msgstr ""
+"attivare, effettuare un checkout, attivare revisione, prelevare "
+"(TortoiseCVS), ritirare (TSVN)?"
+
+#. ""
+msgid "clone [verb]"
+msgstr "clonare"
+
+#. "A single point in the git history."
+msgid "commit [noun]"
+msgstr "revisione, commit, deposito (TortoiseCVS), invio (TSVN)?"
+
+#. "The action of storing a new snapshot of the project's state in the git history."
+msgid "commit [verb]"
+msgstr ""
+"creare una nuova revisione, archiviare, effettuare un commit, depositare "
+"(nel server), fare un deposito (TortoiseCVS), inviare (TSVN)?"
+
+#. ""
+msgid "diff [noun]"
+msgstr "differenza, confronto, comparazione, raffronto"
+
+#. ""
+msgid "diff [verb]"
+msgstr "confronta, mostra le differenze"
+
+#. "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
+msgid "fast forward merge"
+msgstr "fusione in 'fast-forward', fusione in avanti veloce"
+
+#. "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
+msgid "fetch"
+msgstr "recuperare, prelevare, prendere da, recuperare (TSVN)"
+
+#. "A collection of files. The index is a stored version of your working tree."
+msgid "index (in git-gui: staging area)"
+msgstr "indice"
+
+#. "A successful merge results in the creation of a new commit representing the result of the merge."
+msgid "merge [noun]"
+msgstr "fusione, unione"
+
+#. "To bring the contents of another branch into the current branch."
+msgid "merge [verb]"
+msgstr "effettuare la fusione, unire, fondere, eseguire la fusione"
+
+#. ""
+msgid "message"
+msgstr "messaggio, commento"
+
+#. "Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in 'remotes/<name>'."
+msgid "prune"
+msgstr "potatura"
+
+#. "Pulling a branch means to fetch it and merge it."
+msgid "pull"
+msgstr ""
+"prendi (recupera) e fondi (unisci)? (in pratica una traduzione di fetch + "
+"merge)"
+
+#. "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
+msgid "push"
+msgstr "propaga"
+
+#. ""
+msgid "redo"
+msgstr "ripeti, rifai"
+
+#. "An other repository ('remote'). One might have a set of remotes whose branches one tracks."
+msgid "remote"
+msgstr "remoto"
+
+#. "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
+msgid "repository"
+msgstr "archivio, repository, database? deposito (rapidsvn)?"
+
+#. ""
+msgid "reset"
+msgstr "ripristinare, annullare, azzerare, ripristinare"
+
+#. ""
+msgid "revert"
+msgstr ""
+"annullare, inverti (rapidsvn), ritorna allo stato precedente, annulla le "
+"modifiche della revisione"
+
+#. "A particular state of files and directories which was stored in the object database."
+msgid "revision"
+msgstr "revisione (TortoiseSVN)"
+
+#. ""
+msgid "sign off"
+msgstr "sign off, firma"
+
+#. ""
+msgid "staging area"
+msgstr ""
+"area di preparazione, zona di preparazione, modifiche in preparazione? "
+"modifiche in allestimento?"
+
+#. ""
+msgid "status"
+msgstr "stato"
+
+#. "A ref pointing to a tag or commit object"
+msgid "tag [noun]"
+msgstr "etichetta, etichettatura (TortoiseCVS)"
+
+#. ""
+msgid "tag [verb]"
+msgstr "etichettare"
+
+#. "A regular git branch that is used to follow changes from another repository."
+msgid "tracking branch"
+msgstr ""
+"duplicato locale di ramo remoto, ramo in 'tracking', ramo inseguitore? ramo "
+"di {inseguimento,allineamento,rilevamento,puntamento}?"
+
+#. ""
+msgid "undo"
+msgstr "annulla"
+
+#. ""
+msgid "update"
+msgstr "aggiornamento, aggiornare"
+
+#. ""
+msgid "verify"
+msgstr "verifica, verificare"
+
+#. "The tree of actual checked out files."
+msgid "working copy, working tree"
+msgstr "directory di lavoro, copia di lavoro"
diff --git a/git-gui/po/glossary/txt-to-pot.sh b/git-gui/po/glossary/txt-to-pot.sh
new file mode 100755 (executable)
index 0000000..49bf7c5
--- /dev/null
@@ -0,0 +1,48 @@
+#!/bin/sh
+# This is a very, _very_, simple script to convert a tab-separated
+# .txt file into a .pot/.po.
+# Its not clever but it took me 2 minutes to write :)
+# Michael Twomey <michael.twomey@ireland.sun.com>
+# 23 March 2001
+# with slight GnuCash modifications by Christian Stimming <stimming@tuhh.de>
+# 19 Aug 2001, 23 Jul 2007
+
+#check args
+if [ $# -eq 0 ]
+then
+       cat <<!
+Usage: `basename $0` git-gui-glossary.txt > git-gui-glossary.pot
+!
+       exit 1;
+fi
+
+GLOSSARY_CSV="$1";
+
+if [ ! -f "$GLOSSARY_CSV" ]
+then
+       echo "Can't find $GLOSSARY_CSV.";
+       exit 1;
+fi
+
+cat <<!
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR Free Software Foundation, Inc.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: `date +'%Y-%m-%d %H:%M%z'`\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: ENCODING\n"
+
+!
+
+#Yes this is the most simple awk script you've ever seen :)
+awk -F'\t' '{if ($2 != "") print "#. "$2; print "msgid "$1; print "msgstr \"\"\n"}' \
+$GLOSSARY_CSV
diff --git a/git-gui/po/glossary/zh_cn.po b/git-gui/po/glossary/zh_cn.po
new file mode 100644 (file)
index 0000000..158835b
--- /dev/null
@@ -0,0 +1,170 @@
+# Translation of git-gui glossary to Simplified Chinese
+# Copyright (C) 2007 Shawn Pearce, et al.
+# This file is distributed under the same license as the git package.
+# Xudong Guan <xudong.guan@gmail.com> and the zh-kernel.org mailing list, 2007
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: git-gui glossary\n"
+"PO-Revision-Date: 2007-07-23 22:07+0200\n"
+"Last-Translator: Xudong Guan <xudong.guan@gmail.com>\n"
+"Language-Team: Simplified Chinese \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
+msgid ""
+"English Term (Dear translator: This file will never be visible to the user!)"
+msgstr "注:这个文件是为了帮助翻译人员统一名词术语。最终用户不会关心这个文件。"
+
+#. ""
+#. amend指用户修改最近一次commit的操作,修订?修改?修正?
+#. [WANG Cong]: 根据我的了解,这个词似乎翻译成“修订”多一些。“修正”也可以,“修改”再次之。
+#. [ZHANG Le]: 修订,感觉一般指对一些大型出版物的大规模升级,比如修订新华字典
+#              修正,其实每次amend的结果也不一定就是最后结果,说不定还需要修改。所以不
+#              如就叫修改
+msgid "amend"
+msgstr "修订"
+
+#. ""
+#. git annotate 文件名:用来标注文件的每一行在什么时候被谁最后修改。
+#. [WANG Cong]: "标记"一般是mark。;)
+#. [ZHANG Le]: 标注,或者干脆用原意:注解,或注释
+msgid "annotate"
+msgstr "标注"
+
+#. "A 'branch' is an active line of development."
+msgid "branch [noun]"
+msgstr "分支"
+
+#. ""
+msgid "branch [verb]"
+msgstr "建立分支"
+
+#. ""
+#. [WANG Cong]: 网上有人翻译成“检出”,我感觉更好一些,毕竟把check的意思翻译出来了。
+#. [ZHNAG Le]: 提取吧,提取分支/版本
+#. [rae l]: 签出。subversion软件中的大多词汇已有翻译,既然git与subversion同是SCM管理,可以参考同类软件的翻译也不错。
+msgid "checkout [noun]"
+msgstr "签出"
+
+#. "The action of updating the working tree to a revision which was stored in the object database."
+msgid "checkout [verb]"
+msgstr "签出"
+
+#. "A single point in the git history."
+msgid "commit [noun]"
+msgstr "提交"
+
+#. "The action of storing a new snapshot of the project's state in the git history."
+msgid "commit [verb]"
+msgstr "提交"
+
+#. ""
+#. 差异?差别?
+#. [ZHANG Le]: 个人感觉差别更加中性一些
+msgid "diff [noun]"
+msgstr "差别"
+
+#. ""
+msgid "diff [verb]"
+msgstr "比较"
+
+#. "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
+msgid "fast forward merge"
+msgstr "快进式合并"
+
+#. "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
+#. 获取?取得?下载?更新?注意和update的区分
+msgid "fetch"
+msgstr "获取"
+
+#. "A collection of files. The index is a stored version of your working tree."
+#. index是working tree和repository之间的缓存
+msgid "index (in git-gui: staging area)"
+msgstr "工作缓存?"
+
+#. "A successful merge results in the creation of a new commit representing the result of the merge."
+msgid "merge [noun]"
+msgstr "合并"
+
+#. "To bring the contents of another branch into the current branch."
+msgid "merge [verb]"
+msgstr "合并"
+
+#. ""
+#. message是指commit中的文字信息
+msgid "message"
+msgstr "描述"
+
+#. "Pulling a branch means to fetch it and merge it."
+msgid "pull"
+msgstr "获取+合并"
+
+#. "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
+msgid "push"
+msgstr "推入"
+
+#. ""
+msgid "redo"
+msgstr "重做"
+
+#. "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
+msgid "repository"
+msgstr "仓库"
+
+#. ""
+msgid "reset"
+msgstr "重置"
+
+#. ""
+msgid "revert"
+msgstr "恢复"
+
+#. "A particular state of files and directories which was stored in the object database."
+msgid "revision"
+msgstr "版本"
+
+#. ""
+msgid "sign off"
+msgstr "签名"
+
+#. ""
+#. 似乎是git-gui里面显示的本次提交的文件清单区域
+msgid "staging area"
+msgstr "提交暂存区"
+
+#. ""
+msgid "status"
+msgstr "状态"
+
+#. "A ref pointing to a tag or commit object"
+msgid "tag [noun]"
+msgstr "标签"
+
+#. ""
+msgid "tag [verb]"
+msgstr "添加标签"
+
+#. "A regular git branch that is used to follow changes from another repository."
+msgid "tracking branch"
+msgstr "跟踪分支"
+
+#. ""
+msgid "undo"
+msgstr "撤销"
+
+#. ""
+msgid "update"
+msgstr "更新。注意和fetch的区分"
+
+#. ""
+msgid "verify"
+msgstr "验证"
+
+#. "The tree of actual checked out files."
+#. "工作副本?工作区域?工作目录"
+#. [LI Yang]: 当前副本, 当前源码树?
+msgid "working copy, working tree"
+msgstr "工作副本,工作源码树"
diff --git a/git-gui/po/hu.po b/git-gui/po/hu.po
new file mode 100644 (file)
index 0000000..627c05e
--- /dev/null
@@ -0,0 +1,1937 @@
+# Hungarian translations for git-gui-i package.
+# Copyright (C) 2007 THE git-gui-i'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the git-gui-i package.
+# Miklos Vajna <vmiklos@frugalware.org>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: git-gui-i 18n\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-11-24 10:36+0100\n"
+"PO-Revision-Date: 2007-12-04 01:15+0100\n"
+"Last-Translator: Miklos Vajna <vmiklos@frugalware.org>\n"
+"Language-Team: Hungarian\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
+#: git-gui.sh:733
+msgid "git-gui: fatal error"
+msgstr "git-gui: végzetes hiba"
+
+#: git-gui.sh:565
+#, tcl-format
+msgid "Invalid font specified in %s:"
+msgstr "Érvénytelen font lett megadva itt: %s:"
+
+#: git-gui.sh:590
+msgid "Main Font"
+msgstr "Fő betűtípus"
+
+#: git-gui.sh:591
+msgid "Diff/Console Font"
+msgstr "Diff/konzol betűtípus"
+
+#: git-gui.sh:605
+msgid "Cannot find git in PATH."
+msgstr "A git nem található a PATH-ban."
+
+#: git-gui.sh:632
+msgid "Cannot parse Git version string:"
+msgstr "Nem értelmezhető a Git verzió sztring:"
+
+#: git-gui.sh:650
+#, tcl-format
+msgid ""
+"Git version cannot be determined.\n"
+"\n"
+"%s claims it is version '%s'.\n"
+"\n"
+"%s requires at least Git 1.5.0 or later.\n"
+"\n"
+"Assume '%s' is version 1.5.0?\n"
+msgstr ""
+"Nem állípítható meg a Git verziója.\n"
+"\n"
+"A(z) %s szerint a verzió '%s'.\n"
+"\n"
+"A(z) %s a Git 1.5.0 vagy későbbi verzióját igényli.\n"
+"\n"
+"Feltételezhetjük, hogy a(z) '%s' verziója legalább 1.5.0?\n"
+
+#: git-gui.sh:888
+msgid "Git directory not found:"
+msgstr "A Git könyvtár nem található:"
+
+#: git-gui.sh:895
+msgid "Cannot move to top of working directory:"
+msgstr "Nem lehet a munkakönyvtár tetejére lépni:"
+
+#: git-gui.sh:902
+msgid "Cannot use funny .git directory:"
+msgstr "Nem használható vicces .git könyvtár:"
+
+#: git-gui.sh:907
+msgid "No working directory"
+msgstr "Nincs munkakönyvtár"
+
+#: git-gui.sh:1054
+msgid "Refreshing file status..."
+msgstr "A fájlok státuszának frissítése..."
+
+#: git-gui.sh:1119
+msgid "Scanning for modified files ..."
+msgstr "Módosított fájlok keresése ..."
+
+#: git-gui.sh:1294 lib/browser.tcl:245
+msgid "Ready."
+msgstr "Kész."
+
+#: git-gui.sh:1560
+msgid "Unmodified"
+msgstr "Nem módosított"
+
+#: git-gui.sh:1562
+msgid "Modified, not staged"
+msgstr "Módosított, de nem kiválasztott"
+
+#: git-gui.sh:1563 git-gui.sh:1568
+msgid "Staged for commit"
+msgstr "Kiválasztva commitolásra"
+
+#: git-gui.sh:1564 git-gui.sh:1569
+msgid "Portions staged for commit"
+msgstr "Részek kiválasztva commitolásra"
+
+#: git-gui.sh:1565 git-gui.sh:1570
+msgid "Staged for commit, missing"
+msgstr "Kiválasztva commitolásra, hiányzó"
+
+#: git-gui.sh:1567
+msgid "Untracked, not staged"
+msgstr "Nem követett, nem kiválasztott"
+
+#: git-gui.sh:1572
+msgid "Missing"
+msgstr "Hiányzó"
+
+#: git-gui.sh:1573
+msgid "Staged for removal"
+msgstr "Kiválasztva eltávolításra"
+
+#: git-gui.sh:1574
+msgid "Staged for removal, still present"
+msgstr "Kiválasztva eltávolításra, jelenleg is elérhető"
+
+#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
+msgid "Requires merge resolution"
+msgstr "Merge feloldás szükséges"
+
+#: git-gui.sh:1614
+msgid "Starting gitk... please wait..."
+msgstr "A gitk indítása... várjunk..."
+
+#: git-gui.sh:1623
+#, tcl-format
+msgid ""
+"Unable to start gitk:\n"
+"\n"
+"%s does not exist"
+msgstr ""
+"A gitk indítása sikertelen:\n"
+"\n"
+"A(z) %s nem létezik"
+
+#: git-gui.sh:1823 lib/choose_repository.tcl:35
+msgid "Repository"
+msgstr "Repó"
+
+#: git-gui.sh:1824
+msgid "Edit"
+msgstr "Szerkesztés"
+
+#: git-gui.sh:1826 lib/choose_rev.tcl:560
+msgid "Branch"
+msgstr "Branch"
+
+#: git-gui.sh:1829 lib/choose_rev.tcl:547
+msgid "Commit@@noun"
+msgstr "Commit@@főnév"
+
+#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
+msgid "Merge"
+msgstr "Merge"
+
+#: git-gui.sh:1833 lib/choose_rev.tcl:556
+msgid "Remote"
+msgstr "Távoli"
+
+#: git-gui.sh:1842
+msgid "Browse Current Branch's Files"
+msgstr "A jelenlegi branch fájljainak böngészése"
+
+#: git-gui.sh:1846
+msgid "Browse Branch Files..."
+msgstr "A branch fájljainak böngészése..."
+
+#: git-gui.sh:1851
+msgid "Visualize Current Branch's History"
+msgstr "A jelenlegi branch történetének vizualizálása"
+
+#: git-gui.sh:1855
+msgid "Visualize All Branch History"
+msgstr "Az összes branch történetének vizualizálása"
+
+#: git-gui.sh:1862
+#, tcl-format
+msgid "Browse %s's Files"
+msgstr "A(z) %s branch fájljainak böngészése"
+
+#: git-gui.sh:1864
+#, tcl-format
+msgid "Visualize %s's History"
+msgstr "A(z) %s branch történetének vizualizálása"
+
+#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
+msgid "Database Statistics"
+msgstr "Adatbázis statisztikák"
+
+#: git-gui.sh:1872 lib/database.tcl:34
+msgid "Compress Database"
+msgstr "Adatbázis tömörítése"
+
+#: git-gui.sh:1875
+msgid "Verify Database"
+msgstr "Adatbázis ellenőrzése"
+
+#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
+#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
+msgid "Create Desktop Icon"
+msgstr "Asztal ikon létrehozása"
+
+#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
+msgid "Quit"
+msgstr "Kilépés"
+
+#: git-gui.sh:1902
+msgid "Undo"
+msgstr "Visszavonás"
+
+#: git-gui.sh:1905
+msgid "Redo"
+msgstr "Mégis"
+
+#: git-gui.sh:1909 git-gui.sh:2403
+msgid "Cut"
+msgstr "Kivágás"
+
+#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
+#: lib/console.tcl:67
+msgid "Copy"
+msgstr "Másolás"
+
+#: git-gui.sh:1915 git-gui.sh:2409
+msgid "Paste"
+msgstr "Beillesztés"
+
+#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
+#: lib/remote_branch_delete.tcl:38
+msgid "Delete"
+msgstr "Törlés"
+
+#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
+msgid "Select All"
+msgstr "Mindent kiválaszt"
+
+#: git-gui.sh:1931
+msgid "Create..."
+msgstr "Létrehozás..."
+
+#: git-gui.sh:1937
+msgid "Checkout..."
+msgstr "Checkout..."
+
+#: git-gui.sh:1943
+msgid "Rename..."
+msgstr "Átnevezés..."
+
+#: git-gui.sh:1948 git-gui.sh:2048
+msgid "Delete..."
+msgstr "Törlés..."
+
+#: git-gui.sh:1953
+msgid "Reset..."
+msgstr "Visszaállítás..."
+
+#: git-gui.sh:1965 git-gui.sh:2350
+msgid "New Commit"
+msgstr "Új commit"
+
+#: git-gui.sh:1973 git-gui.sh:2357
+msgid "Amend Last Commit"
+msgstr "Utolsó commit javítása"
+
+#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
+msgid "Rescan"
+msgstr "Keresés újra"
+
+#: git-gui.sh:1988
+msgid "Stage To Commit"
+msgstr "Kiválasztás commitolásra"
+
+#: git-gui.sh:1994
+msgid "Stage Changed Files To Commit"
+msgstr "Módosított fájlok kiválasztása commitolásra"
+
+#: git-gui.sh:2000
+msgid "Unstage From Commit"
+msgstr "Commitba való kiválasztás visszavonása"
+
+#: git-gui.sh:2005 lib/index.tcl:393
+msgid "Revert Changes"
+msgstr "Változtatások visszaállítása"
+
+#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
+msgid "Sign Off"
+msgstr "Aláír"
+
+#: git-gui.sh:2016 git-gui.sh:2333
+msgid "Commit@@verb"
+msgstr "Commit@@ige"
+
+#: git-gui.sh:2027
+msgid "Local Merge..."
+msgstr "Helyi merge..."
+
+#: git-gui.sh:2032
+msgid "Abort Merge..."
+msgstr "Merge megszakítása..."
+
+#: git-gui.sh:2044
+msgid "Push..."
+msgstr "Push..."
+
+#: git-gui.sh:2055 lib/choose_repository.tcl:40
+msgid "Apple"
+msgstr "Apple"
+
+#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
+#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
+#, tcl-format
+msgid "About %s"
+msgstr "Névjegy: %s"
+
+#: git-gui.sh:2062
+msgid "Preferences..."
+msgstr "Beállítások..."
+
+#: git-gui.sh:2070 git-gui.sh:2595
+msgid "Options..."
+msgstr "Opciók..."
+
+#: git-gui.sh:2076 lib/choose_repository.tcl:46
+msgid "Help"
+msgstr "Segítség"
+
+#: git-gui.sh:2117
+msgid "Online Documentation"
+msgstr "Online dokumentáció"
+
+#: git-gui.sh:2201
+#, tcl-format
+msgid "fatal: cannot stat path %s: No such file or directory"
+msgstr "végzetes hiba: nem érhető el a(z) %s útvonal: Nincs ilyen fájl vagy könyvtár"
+
+#: git-gui.sh:2234
+msgid "Current Branch:"
+msgstr "Jelenlegi branch:"
+
+#: git-gui.sh:2255
+msgid "Staged Changes (Will Commit)"
+msgstr "Kiválasztott változtatások (commitolva lesz)"
+
+#: git-gui.sh:2274
+msgid "Unstaged Changes"
+msgstr "Kiválasztatlan változtatások"
+
+#: git-gui.sh:2323
+msgid "Stage Changed"
+msgstr "Változtatások kiválasztása"
+
+#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
+msgid "Push"
+msgstr "Push"
+
+#: git-gui.sh:2369
+msgid "Initial Commit Message:"
+msgstr "Kezdeti commit üzenet:"
+
+#: git-gui.sh:2370
+msgid "Amended Commit Message:"
+msgstr "Javító commit üzenet:"
+
+#: git-gui.sh:2371
+msgid "Amended Initial Commit Message:"
+msgstr "Kezdeti javító commit üzenet:"
+
+#: git-gui.sh:2372
+msgid "Amended Merge Commit Message:"
+msgstr "Javító merge commit üzenet:"
+
+#: git-gui.sh:2373
+msgid "Merge Commit Message:"
+msgstr "Merge commit üzenet:"
+
+#: git-gui.sh:2374
+msgid "Commit Message:"
+msgstr "Commit üzenet:"
+
+#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
+msgid "Copy All"
+msgstr "Összes másolása"
+
+#: git-gui.sh:2443 lib/blame.tcl:104
+msgid "File:"
+msgstr "Fájl:"
+
+#: git-gui.sh:2545
+msgid "Refresh"
+msgstr "Frissítés"
+
+#: git-gui.sh:2566
+msgid "Apply/Reverse Hunk"
+msgstr "Hunk alkalmazása/visszaállítása"
+
+#: git-gui.sh:2572
+msgid "Decrease Font Size"
+msgstr "Font méret csökkentése"
+
+#: git-gui.sh:2576
+msgid "Increase Font Size"
+msgstr "Fönt méret növelése"
+
+#: git-gui.sh:2581
+msgid "Show Less Context"
+msgstr "Kevesebb környezet mutatása"
+
+#: git-gui.sh:2588
+msgid "Show More Context"
+msgstr "Több környezet mutatása"
+
+#: git-gui.sh:2602
+msgid "Unstage Hunk From Commit"
+msgstr "Hunk törlése commitból"
+
+#: git-gui.sh:2604
+msgid "Stage Hunk For Commit"
+msgstr "Hunk kiválasztása commitba"
+
+#: git-gui.sh:2623
+msgid "Initializing..."
+msgstr "Inicializálás..."
+
+#: git-gui.sh:2718
+#, tcl-format
+msgid ""
+"Possible environment issues exist.\n"
+"\n"
+"The following environment variables are probably\n"
+"going to be ignored by any Git subprocess run\n"
+"by %s:\n"
+"\n"
+msgstr ""
+"Lehetséges, hogy környezeti problémák vannak.\n"
+"\n"
+"A következő környezeti változók valószínűleg\n"
+"figyelmen kívül lesznek hagyva a(z) %s által\n"
+"indított folyamatok által:\n"
+"\n"
+
+#: git-gui.sh:2748
+msgid ""
+"\n"
+"This is due to a known issue with the\n"
+"Tcl binary distributed by Cygwin."
+msgstr ""
+"\n"
+"Ez a Cygwin által terjesztett Tcl binárisban\n"
+"lévő ismert hiba miatt van."
+
+#: git-gui.sh:2753
+#, tcl-format
+msgid ""
+"\n"
+"\n"
+"A good replacement for %s\n"
+"is placing values for the user.name and\n"
+"user.email settings into your personal\n"
+"~/.gitconfig file.\n"
+msgstr ""
+"\n"
+"\n"
+"Egy jó helyettesítés a(z) %s számára\n"
+"a user.name és user.email beállítások\n"
+"elhelyezése a személyes\n"
+"~/.gitconfig fájlba.\n"
+
+#: lib/about.tcl:25
+msgid "git-gui - a graphical user interface for Git."
+msgstr "git-gui - egy grafikus felület a Githez."
+
+#: lib/blame.tcl:77
+msgid "File Viewer"
+msgstr "Fájl néző"
+
+#: lib/blame.tcl:81
+msgid "Commit:"
+msgstr "Commit:"
+
+#: lib/blame.tcl:249
+msgid "Copy Commit"
+msgstr "Commit másolása"
+
+#: lib/blame.tcl:369
+#, tcl-format
+msgid "Reading %s..."
+msgstr "A(z) %s olvasása..."
+
+#: lib/blame.tcl:473
+msgid "Loading copy/move tracking annotations..."
+msgstr "A másolást/átnevezést követő annotációk betöltése..."
+
+#: lib/blame.tcl:493
+msgid "lines annotated"
+msgstr "sor annotálva"
+
+#: lib/blame.tcl:674
+msgid "Loading original location annotations..."
+msgstr "Az eredeti hely annotációk betöltése..."
+
+#: lib/blame.tcl:677
+msgid "Annotation complete."
+msgstr "Az annotáció kész."
+
+#: lib/blame.tcl:731
+msgid "Loading annotation..."
+msgstr "Az annotáció betöltése..."
+
+#: lib/blame.tcl:787
+msgid "Author:"
+msgstr "Szerző:"
+
+#: lib/blame.tcl:791
+msgid "Committer:"
+msgstr "Commiter:"
+
+#: lib/blame.tcl:796
+msgid "Original File:"
+msgstr "Eredeti fájl:"
+
+#: lib/blame.tcl:910
+msgid "Originally By:"
+msgstr "Eredeti szerző:"
+
+#: lib/blame.tcl:916
+msgid "In File:"
+msgstr "Ebben a fájlban:"
+
+#: lib/blame.tcl:921
+msgid "Copied Or Moved Here By:"
+msgstr "Ide másolta vagy helyezte:"
+
+#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
+msgid "Checkout Branch"
+msgstr "Branch checkoutolása"
+
+#: lib/branch_checkout.tcl:23
+msgid "Checkout"
+msgstr "Checkout"
+
+#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
+#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
+#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
+#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
+msgid "Cancel"
+msgstr "Mégsem"
+
+#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
+msgid "Revision"
+msgstr "Revízió"
+
+#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
+msgid "Options"
+msgstr "Opciók"
+
+#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
+msgid "Fetch Tracking Branch"
+msgstr "Követő branch letöltése"
+
+#: lib/branch_checkout.tcl:44
+msgid "Detach From Local Branch"
+msgstr "Helyi branch leválasztása"
+
+#: lib/branch_create.tcl:22
+msgid "Create Branch"
+msgstr "Branch létrehozása"
+
+#: lib/branch_create.tcl:27
+msgid "Create New Branch"
+msgstr "Új branch létrehozása"
+
+#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
+msgid "Create"
+msgstr "Létrehozás"
+
+#: lib/branch_create.tcl:40
+msgid "Branch Name"
+msgstr "Branch neve"
+
+#: lib/branch_create.tcl:43
+msgid "Name:"
+msgstr "Név:"
+
+#: lib/branch_create.tcl:58
+msgid "Match Tracking Branch Name"
+msgstr "Egyeztetendő követési branch név"
+
+#: lib/branch_create.tcl:66
+msgid "Starting Revision"
+msgstr "A következő revíziótól"
+
+#: lib/branch_create.tcl:72
+msgid "Update Existing Branch:"
+msgstr "Létező branch frissítése"
+
+#: lib/branch_create.tcl:75
+msgid "No"
+msgstr "Nem"
+
+#: lib/branch_create.tcl:80
+msgid "Fast Forward Only"
+msgstr "Csak fast forward"
+
+#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
+msgid "Reset"
+msgstr "Visszaállítás"
+
+#: lib/branch_create.tcl:97
+msgid "Checkout After Creation"
+msgstr "Checkout létrehozás után"
+
+#: lib/branch_create.tcl:131
+msgid "Please select a tracking branch."
+msgstr "Válasszunk ki egy követő branchet."
+
+#: lib/branch_create.tcl:140
+#, tcl-format
+msgid "Tracking branch %s is not a branch in the remote repository."
+msgstr "A(z) %s követő branch nem branch a távoli repóban."
+
+#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
+msgid "Please supply a branch name."
+msgstr "Adjunk megy egy branch nevet."
+
+#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
+#, tcl-format
+msgid "'%s' is not an acceptable branch name."
+msgstr "A(z) '%s' nem egy elfogadható branch név."
+
+#: lib/branch_delete.tcl:15
+msgid "Delete Branch"
+msgstr "Branch törlése"
+
+#: lib/branch_delete.tcl:20
+msgid "Delete Local Branch"
+msgstr "Helyi branch törlése"
+
+#: lib/branch_delete.tcl:37
+msgid "Local Branches"
+msgstr "Helyi branchek"
+
+#: lib/branch_delete.tcl:52
+msgid "Delete Only If Merged Into"
+msgstr "Csak már merge-ölt törlése"
+
+#: lib/branch_delete.tcl:54
+msgid "Always (Do not perform merge test.)"
+msgstr "Mindig (Ne legyen merge teszt.)"
+
+#: lib/branch_delete.tcl:103
+#, tcl-format
+msgid "The following branches are not completely merged into %s:"
+msgstr "A következő branchek nem teljesen lettek merge-ölve ebbe: %s:"
+
+#: lib/branch_delete.tcl:115
+msgid ""
+"Recovering deleted branches is difficult. \n"
+"\n"
+" Delete the selected branches?"
+msgstr ""
+"A törölt branchek visszaállítása bonyolult. \n"
+"\n"
+" Biztosan törli a kiválasztott brancheket?"
+
+#: lib/branch_delete.tcl:141
+#, tcl-format
+msgid ""
+"Failed to delete branches:\n"
+"%s"
+msgstr ""
+"Nem sikerült törölni a következő brancheket:\n"
+"%s"
+
+#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
+msgid "Rename Branch"
+msgstr "Branch átnevezése"
+
+#: lib/branch_rename.tcl:26
+msgid "Rename"
+msgstr "Átnevezés"
+
+#: lib/branch_rename.tcl:36
+msgid "Branch:"
+msgstr "Branch:"
+
+#: lib/branch_rename.tcl:39
+msgid "New Name:"
+msgstr "Új név:"
+
+#: lib/branch_rename.tcl:75
+msgid "Please select a branch to rename."
+msgstr "Válasszunk ki egy átnevezendő branchet."
+
+#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
+#, tcl-format
+msgid "Branch '%s' already exists."
+msgstr "A(z) '%s' branch már létezik."
+
+#: lib/branch_rename.tcl:117
+#, tcl-format
+msgid "Failed to rename '%s'."
+msgstr "Nem sikerült átnevezni: '%s'."
+
+#: lib/browser.tcl:17
+msgid "Starting..."
+msgstr "Indítás..."
+
+#: lib/browser.tcl:26
+msgid "File Browser"
+msgstr "Fájl böngésző"
+
+#: lib/browser.tcl:125 lib/browser.tcl:142
+#, tcl-format
+msgid "Loading %s..."
+msgstr "A(z) %s betöltése..."
+
+#: lib/browser.tcl:186
+msgid "[Up To Parent]"
+msgstr "[Fel a szülőhöz]"
+
+#: lib/browser.tcl:266 lib/browser.tcl:272
+msgid "Browse Branch Files"
+msgstr "A branch fájljainak böngészése"
+
+#: lib/browser.tcl:277 lib/choose_repository.tcl:391
+#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
+#: lib/choose_repository.tcl:989
+msgid "Browse"
+msgstr "Böngészés"
+
+#: lib/checkout_op.tcl:79
+#, tcl-format
+msgid "Fetching %s from %s"
+msgstr "A(z) %s letöltése innen: %s"
+
+#: lib/checkout_op.tcl:127
+#, tcl-format
+msgid "fatal: Cannot resolve %s"
+msgstr "végzetes: Nem lehet feloldani a következőt: %s"
+
+#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
+msgid "Close"
+msgstr "Bezárás"
+
+#: lib/checkout_op.tcl:169
+#, tcl-format
+msgid "Branch '%s' does not exist."
+msgstr "A(z) '%s' branch nem létezik."
+
+#: lib/checkout_op.tcl:206
+#, tcl-format
+msgid ""
+"Branch '%s' already exists.\n"
+"\n"
+"It cannot fast-forward to %s.\n"
+"A merge is required."
+msgstr ""
+"A(z) '%s' branch már létezik.\n"
+"\n"
+"Nem lehet fast-forwardolni a következőhöz: %s.\n"
+"Egy merge szükséges."
+
+#: lib/checkout_op.tcl:220
+#, tcl-format
+msgid "Merge strategy '%s' not supported."
+msgstr "A(z) '%s' merge strategy nem támogatott."
+
+#: lib/checkout_op.tcl:239
+#, tcl-format
+msgid "Failed to update '%s'."
+msgstr "Nem sikerült frissíteni a következőt: '%s'."
+
+#: lib/checkout_op.tcl:251
+msgid "Staging area (index) is already locked."
+msgstr "A kiválasztási terület (index) már zárolva van."
+
+#: lib/checkout_op.tcl:266
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before the current branch can be changed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"Az utolsó keresési állapot nem egyezik meg a repó állpotával.\n"
+"\n"
+"Egy másik Git program módosította ezt a repót az utolsó keresés óta. Egy "
+"újrakeresés mindenképpen szükséges mielőtt a jelenlegi branchet módosítani "
+"lehetne.\n"
+"\n"
+"Az újrakeresés most automatikusan el fog indulni.\n"
+
+#: lib/checkout_op.tcl:322
+#, tcl-format
+msgid "Updating working directory to '%s'..."
+msgstr "A munkkönyvtár frissiítése a következőre: '%s'..."
+
+#: lib/checkout_op.tcl:353
+#, tcl-format
+msgid "Aborted checkout of '%s' (file level merging is required)."
+msgstr "A(z) '%s' checkoutja megszakítva (fájlszintű merge-ölés szükséges)."
+
+#: lib/checkout_op.tcl:354
+msgid "File level merge required."
+msgstr "Fájlszintű merge-ölés szükséges."
+
+#: lib/checkout_op.tcl:358
+#, tcl-format
+msgid "Staying on branch '%s'."
+msgstr "Jelenleg a(z) '%s' branchen."
+
+#: lib/checkout_op.tcl:429
+msgid ""
+"You are no longer on a local branch.\n"
+"\n"
+"If you wanted to be on a branch, create one now starting from 'This Detached "
+"Checkout'."
+msgstr ""
+"Már nem egy helyi branchen vagyunk.\n"
+"\n"
+"Ha egy branchen szeretnénk lenni, hozzunk létre egyet az 'Ez a leválasztott "
+"checkout'-ból."
+
+#: lib/checkout_op.tcl:446
+#, tcl-format
+msgid "Checked out '%s'."
+msgstr "'%s' kifejtve."
+
+#: lib/checkout_op.tcl:478
+#, tcl-format
+msgid "Resetting '%s' to '%s' will lose the following commits:"
+msgstr ""
+"A(z) '%s' -> '%s' visszaállítás a következő commitok elvesztését jelenti:"
+
+#: lib/checkout_op.tcl:500
+msgid "Recovering lost commits may not be easy."
+msgstr "Az elveszett commitok helyreállítása nem biztos, hogy egyszerű."
+
+#: lib/checkout_op.tcl:505
+#, tcl-format
+msgid "Reset '%s'?"
+msgstr "Visszaállítjuk a következőt: '%s'?"
+
+#: lib/checkout_op.tcl:510 lib/merge.tcl:164
+msgid "Visualize"
+msgstr "Vizualizálás"
+
+#: lib/checkout_op.tcl:578
+#, tcl-format
+msgid ""
+"Failed to set current branch.\n"
+"\n"
+"This working directory is only partially switched.  We successfully updated "
+"your files, but failed to update an internal Git file.\n"
+"\n"
+"This should not have occurred.  %s will now close and give up."
+msgstr ""
+"Nem sikerült beállítani a jelenlegi branchet.\n"
+"\n"
+"A munkakönyvtár csak részben váltott át.  A fájlok sikeresen frissítve "
+"lettek, de nem sikerült frissíteni egy belső Git fájlt.\n"
+"\n"
+"Ennek nem szabad megtörténnie.  A(z) %s most kilép és feladja."
+
+#: lib/choose_font.tcl:39
+msgid "Select"
+msgstr "Kiválaszt"
+
+#: lib/choose_font.tcl:53
+msgid "Font Family"
+msgstr "Font család"
+
+#: lib/choose_font.tcl:73
+msgid "Font Size"
+msgstr "Font méret"
+
+#: lib/choose_font.tcl:90
+msgid "Font Example"
+msgstr "Font példa"
+
+#: lib/choose_font.tcl:101
+msgid ""
+"This is example text.\n"
+"If you like this text, it can be your font."
+msgstr ""
+"Ez egy példa szöveg.\n"
+"Ha ez megfelel, ez lehet a betűtípus."
+
+#: lib/choose_repository.tcl:27
+msgid "Git Gui"
+msgstr "Git Gui"
+
+#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
+msgid "Create New Repository"
+msgstr "Új repó létrehozása"
+
+#: lib/choose_repository.tcl:86
+msgid "New..."
+msgstr "Új..."
+
+#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
+msgid "Clone Existing Repository"
+msgstr "Létező repó másolása"
+
+#: lib/choose_repository.tcl:99
+msgid "Clone..."
+msgstr "Másolás..."
+
+#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
+msgid "Open Existing Repository"
+msgstr "Létező könyvtár megnyitása"
+
+#: lib/choose_repository.tcl:112
+msgid "Open..."
+msgstr "Meggyitás..."
+
+#: lib/choose_repository.tcl:125
+msgid "Recent Repositories"
+msgstr "Legutóbbi repók"
+
+#: lib/choose_repository.tcl:131
+msgid "Open Recent Repository:"
+msgstr "Legutóbbi repók megnyitása:"
+
+#: lib/choose_repository.tcl:294
+#, tcl-format
+msgid "Location %s already exists."
+msgstr "A(z) '%s' hely már létezik."
+
+#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
+#: lib/choose_repository.tcl:314
+#, tcl-format
+msgid "Failed to create repository %s:"
+msgstr "Nem sikerült letrehozni a(z) %s repót:"
+
+#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
+msgid "Directory:"
+msgstr "Könyvtár:"
+
+#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
+#: lib/choose_repository.tcl:1013
+msgid "Git Repository"
+msgstr "Git repó"
+
+#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
+#, tcl-format
+msgid "Directory %s already exists."
+msgstr "A(z) '%s' könyvtár már létezik."
+
+#: lib/choose_repository.tcl:442
+#, tcl-format
+msgid "File %s already exists."
+msgstr "A(z) '%s' fájl már létezik."
+
+#: lib/choose_repository.tcl:463
+msgid "Clone"
+msgstr "Bezárás"
+
+#: lib/choose_repository.tcl:476
+msgid "URL:"
+msgstr "URL:"
+
+#: lib/choose_repository.tcl:496
+msgid "Clone Type:"
+msgstr "Másolás típusa:"
+
+#: lib/choose_repository.tcl:502
+msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
+msgstr "Általános (Gyors, félig-redundáns, hardlinkek)"
+
+#: lib/choose_repository.tcl:508
+msgid "Full Copy (Slower, Redundant Backup)"
+msgstr "Teljes másolás (Lassabb, redundáns biztonsági mentés)"
+
+#: lib/choose_repository.tcl:514
+msgid "Shared (Fastest, Not Recommended, No Backup)"
+msgstr "Megosztott (Leggyorsabb, nem ajánlott, nincs mentés)"
+
+#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
+#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
+#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
+#, tcl-format
+msgid "Not a Git repository: %s"
+msgstr "Nem Git repó: %s"
+
+#: lib/choose_repository.tcl:586
+msgid "Standard only available for local repository."
+msgstr "A standard csak helyi repókra érhető el."
+
+#: lib/choose_repository.tcl:590
+msgid "Shared only available for local repository."
+msgstr "A megosztott csak helyi repókra érhető el."
+
+#: lib/choose_repository.tcl:617
+msgid "Failed to configure origin"
+msgstr "Nem sikerült beállítani az origint"
+
+#: lib/choose_repository.tcl:629
+msgid "Counting objects"
+msgstr "Objektumok számolása"
+
+#: lib/choose_repository.tcl:630
+msgid "buckets"
+msgstr "vödrök"
+
+#: lib/choose_repository.tcl:654
+#, tcl-format
+msgid "Unable to copy objects/info/alternates: %s"
+msgstr "Nem sikerült másolni az objects/info/alternates-t: %s"
+
+#: lib/choose_repository.tcl:690
+#, tcl-format
+msgid "Nothing to clone from %s."
+msgstr "Semmi másolni való nincs innen: %s"
+
+#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
+#: lib/choose_repository.tcl:918
+msgid "The 'master' branch has not been initialized."
+msgstr "A 'master' branch nincs inicializálva."
+
+#: lib/choose_repository.tcl:705
+msgid "Hardlinks are unavailable.  Falling back to copying."
+msgstr "Nem érhetőek el hardlinkek.  Másolás használata."
+
+#: lib/choose_repository.tcl:717
+#, tcl-format
+msgid "Cloning from %s"
+msgstr "Másolás innen: %s"
+
+#: lib/choose_repository.tcl:748
+msgid "Copying objects"
+msgstr "Objektumok másolása"
+
+#: lib/choose_repository.tcl:749
+msgid "KiB"
+msgstr "KiB"
+
+#: lib/choose_repository.tcl:773
+#, tcl-format
+msgid "Unable to copy object: %s"
+msgstr "Nem sikerült másolni az objektumot: %s"
+
+#: lib/choose_repository.tcl:783
+msgid "Linking objects"
+msgstr "Objektumok összefűzése"
+
+#: lib/choose_repository.tcl:784
+msgid "objects"
+msgstr "objektum"
+
+#: lib/choose_repository.tcl:792
+#, tcl-format
+msgid "Unable to hardlink object: %s"
+msgstr "Nem sikerült hardlinkelni az objektumot: %s"
+
+#: lib/choose_repository.tcl:847
+msgid "Cannot fetch branches and objects.  See console output for details."
+msgstr "Nem sikerült letölteni a branch-eket és az objektumokat.  Bővebben a konzolos kimenetben."
+
+#: lib/choose_repository.tcl:858
+msgid "Cannot fetch tags.  See console output for details."
+msgstr "Nem sikerült letölteni a tageket.  Bővebben a konzolos kimenetben."
+
+#: lib/choose_repository.tcl:882
+msgid "Cannot determine HEAD.  See console output for details."
+msgstr "Nem sikerült megállapítani a HEAD-et.  Bővebben a konzolos kimenetben."
+
+#: lib/choose_repository.tcl:891
+#, tcl-format
+msgid "Unable to cleanup %s"
+msgstr "Nem sikerült tiszítani: %s."
+
+#: lib/choose_repository.tcl:897
+msgid "Clone failed."
+msgstr "A másolás nem sikerült."
+
+#: lib/choose_repository.tcl:904
+msgid "No default branch obtained."
+msgstr "Nincs alapértelmezett branch."
+
+#: lib/choose_repository.tcl:915
+#, tcl-format
+msgid "Cannot resolve %s as a commit."
+msgstr "Nem sikerült felöldani a(z) %s objektumot commitként."
+
+#: lib/choose_repository.tcl:927
+msgid "Creating working directory"
+msgstr "Munkakönyvtár létrehozása"
+
+#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
+#: lib/index.tcl:193
+msgid "files"
+msgstr "fájl"
+
+#: lib/choose_repository.tcl:957
+msgid "Initial file checkout failed."
+msgstr "A kezdeti fájl-kibontás sikertelen."
+
+#: lib/choose_repository.tcl:973
+msgid "Open"
+msgstr "Megnyitás"
+
+#: lib/choose_repository.tcl:983
+msgid "Repository:"
+msgstr "Repó:"
+
+#: lib/choose_repository.tcl:1033
+#, tcl-format
+msgid "Failed to open repository %s:"
+msgstr "Nem sikerült megnyitni a(z) %s repót:"
+
+#: lib/choose_rev.tcl:53
+msgid "This Detached Checkout"
+msgstr "Ez a leválasztott checkout"
+
+#: lib/choose_rev.tcl:60
+msgid "Revision Expression:"
+msgstr "Revízió kifejezés:"
+
+#: lib/choose_rev.tcl:74
+msgid "Local Branch"
+msgstr "Helyi branch"
+
+#: lib/choose_rev.tcl:79
+msgid "Tracking Branch"
+msgstr "Követő branch"
+
+#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
+msgid "Tag"
+msgstr "Tag"
+
+#: lib/choose_rev.tcl:317
+#, tcl-format
+msgid "Invalid revision: %s"
+msgstr "Érvénytelen revízió: %s"
+
+#: lib/choose_rev.tcl:338
+msgid "No revision selected."
+msgstr "Nincs kiválasztva revízió."
+
+#: lib/choose_rev.tcl:346
+msgid "Revision expression is empty."
+msgstr "A revízió kifejezés üres."
+
+#: lib/choose_rev.tcl:530
+msgid "Updated"
+msgstr "Frissítve"
+
+#: lib/choose_rev.tcl:558
+msgid "URL"
+msgstr "URL"
+
+#: lib/commit.tcl:9
+msgid ""
+"There is nothing to amend.\n"
+"\n"
+"You are about to create the initial commit.  There is no commit before this "
+"to amend.\n"
+msgstr ""
+"Nincs semmi javítanivaló.\n"
+"\n"
+"Az első commit létrehozása előtt nincs semmilyen commit amit javitani "
+"lehetne.\n"
+
+#: lib/commit.tcl:18
+msgid ""
+"Cannot amend while merging.\n"
+"\n"
+"You are currently in the middle of a merge that has not been fully "
+"completed.  You cannot amend the prior commit unless you first abort the "
+"current merge activity.\n"
+msgstr ""
+"Nem lehet javítani merge alatt.\n"
+"\n"
+"A jelenlegi merge még nem teljesen fejeződött be. Csak akkor javíthat egy "
+"előbbi commitot, hogyha megszakítja a jelenlegi merge folyamatot.\n"
+
+#: lib/commit.tcl:49
+msgid "Error loading commit data for amend:"
+msgstr "Hiba a javítandó commit adat betöltése közben:"
+
+#: lib/commit.tcl:76
+msgid "Unable to obtain your identity:"
+msgstr "Nem sikerült megállapítani az azonosítót:"
+
+#: lib/commit.tcl:81
+msgid "Invalid GIT_COMMITTER_IDENT:"
+msgstr "Érvénytelen GIT_COMMITTER_IDENT:"
+
+#: lib/commit.tcl:133
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before another commit can be created.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"Az utolsó keresési állapot nem egyezik meg a repó állapotával.\n"
+"\n"
+"Egy másik Git program módosította ezt a repót az utolsó keresés óta. Egy "
+"újrakeresés mindenképpen szükséges mielőtt a jelenlegi branchet módosítani "
+"lehetne.\n"
+"\n"
+"Az újrakeresés most automatikusan el fog indulni.\n"
+
+#: lib/commit.tcl:154
+#, tcl-format
+msgid ""
+"Unmerged files cannot be committed.\n"
+"\n"
+"File %s has merge conflicts.  You must resolve them and stage the file "
+"before committing.\n"
+msgstr ""
+"Nem commitolhatunk fájlokat merge előtt.\n"
+"\n"
+"A(z) %s fájlban ütközések vannak. Egyszer azokat ki kell javítani, majd "
+"hozzá ki kell választani a fájlt mielőtt commitolni lehetne.\n"
+
+#: lib/commit.tcl:162
+#, tcl-format
+msgid ""
+"Unknown file state %s detected.\n"
+"\n"
+"File %s cannot be committed by this program.\n"
+msgstr ""
+"Ismeretlen fájl típus %s érzékelve.\n"
+"\n"
+"A(z) %s fájlt nem tudja ez a program commitolni.\n"
+
+#: lib/commit.tcl:170
+msgid ""
+"No changes to commit.\n"
+"\n"
+"You must stage at least 1 file before you can commit.\n"
+msgstr ""
+"Nincs commitolandó változtatás.\n"
+"\n"
+"Legalább egy fájl ki kell választani, hogy commitolni lehessen.\n"
+
+#: lib/commit.tcl:183
+msgid ""
+"Please supply a commit message.\n"
+"\n"
+"A good commit message has the following format:\n"
+"\n"
+"- First line: Describe in one sentence what you did.\n"
+"- Second line: Blank\n"
+"- Remaining lines: Describe why this change is good.\n"
+msgstr ""
+"Adjunk megy egy commit üzenetet.\n"
+"\n"
+"Egy jó commit üzenetnek a következő a formátuma:\n"
+"\n"
+"- Első sor: Egy mondatban leírja, hogy mit csináltunk.\n"
+"- Második sor: Üres\n"
+"- A többi sor: Leírja, hogy miért jó ez a változtatás.\n"
+
+#: lib/commit.tcl:257
+msgid "write-tree failed:"
+msgstr "a write-tree sikertelen:"
+
+#: lib/commit.tcl:275
+#, tcl-format
+msgid "Commit %s appears to be corrupt"
+msgstr "A(z) %s commit sérültnek tűnik"
+
+#: lib/commit.tcl:279
+msgid ""
+"No changes to commit.\n"
+"\n"
+"No files were modified by this commit and it was not a merge commit.\n"
+"\n"
+"A rescan will be automatically started now.\n"
+msgstr ""
+"Nincs commitolandó változtatás.\n"
+"\n"
+"Egyetlen fájlt se módosított ez a commit és merge commit se volt.\n"
+"\n"
+"Az újrakeresés most automatikusan el fog indulni.\n"
+
+#: lib/commit.tcl:286
+msgid "No changes to commit."
+msgstr "Nincs commitolandó változtatás."
+
+#: lib/commit.tcl:303
+#, tcl-format
+msgid "warning: Tcl does not support encoding '%s'."
+msgstr "figyelmeztetés: a Tcl nem támogatja a(z) '%s' kódolást."
+
+#: lib/commit.tcl:317
+msgid "commit-tree failed:"
+msgstr "a commit-tree sikertelen:"
+
+#: lib/commit.tcl:339
+msgid "update-ref failed:"
+msgstr "az update-ref sikertelen:"
+
+#: lib/commit.tcl:430
+#, tcl-format
+msgid "Created commit %s: %s"
+msgstr "Létrejött a %s commit: %s"
+
+#: lib/console.tcl:57
+msgid "Working... please wait..."
+msgstr "Munka folyamatban.. Várjunk..."
+
+#: lib/console.tcl:183
+msgid "Success"
+msgstr "Siker"
+
+#: lib/console.tcl:196
+msgid "Error: Command Failed"
+msgstr "Hiba: a parancs sikertelen"
+
+#: lib/database.tcl:43
+msgid "Number of loose objects"
+msgstr "Elvesztett objektumok száma"
+
+#: lib/database.tcl:44
+msgid "Disk space used by loose objects"
+msgstr "Elveszett objektumok által elfoglalt lemezterület"
+
+#: lib/database.tcl:45
+msgid "Number of packed objects"
+msgstr "Csomagolt objektumok számra"
+
+#: lib/database.tcl:46
+msgid "Number of packs"
+msgstr "Csomagok száma"
+
+#: lib/database.tcl:47
+msgid "Disk space used by packed objects"
+msgstr "A csomagolt objektumok által használt lemezterület"
+
+#: lib/database.tcl:48
+msgid "Packed objects waiting for pruning"
+msgstr "Eltávolításra váró csomagolt objektumok számra"
+
+#: lib/database.tcl:49
+msgid "Garbage files"
+msgstr "Hulladék fájlok"
+
+#: lib/database.tcl:72
+msgid "Compressing the object database"
+msgstr "Az objektum adatbázis tömörítése"
+
+#: lib/database.tcl:83
+msgid "Verifying the object database with fsck-objects"
+msgstr "Az objektum adatbázis ellenőrzése az fsck-objects használatával"
+
+#: lib/database.tcl:108
+#, tcl-format
+msgid ""
+"This repository currently has approximately %i loose objects.\n"
+"\n"
+"To maintain optimal performance it is strongly recommended that you compress "
+"the database when more than %i loose objects exist.\n"
+"\n"
+"Compress the database now?"
+msgstr ""
+"Ennek a repónak jelenleg %i különálló objektuma van.\n"
+"\n"
+"Az optimális teljesítményhez erősen ajánlott az adatbázis tömörítése, ha "
+"több mint %i objektum létezik.\n"
+"\n"
+"Lehet most tömöríteni az adatbázist?"
+
+#: lib/date.tcl:25
+#, tcl-format
+msgid "Invalid date from Git: %s"
+msgstr "Érvénytelen dátum a Git-től: %s"
+
+#: lib/diff.tcl:42
+#, tcl-format
+msgid ""
+"No differences detected.\n"
+"\n"
+"%s has no changes.\n"
+"\n"
+"The modification date of this file was updated by another application, but "
+"the content within the file was not changed.\n"
+"\n"
+"A rescan will be automatically started to find other files which may have "
+"the same state."
+msgstr ""
+"Nincsenek változások.\n"
+"\n"
+"A(z) %s módosítatlan.\n"
+"\n"
+"A fájl módosítási dátumát frissítette egy másik alkalmazás, de a fájl "
+"tartalma változatlan.\n"
+"\n"
+"Egy újrakeresés fog indulni a hasonló állapotú fájlok megtalálása érdekében."
+
+#: lib/diff.tcl:81
+#, tcl-format
+msgid "Loading diff of %s..."
+msgstr "A(z) %s diff-jének betöltése..."
+
+#: lib/diff.tcl:114 lib/diff.tcl:184
+#, tcl-format
+msgid "Unable to display %s"
+msgstr "Nem lehet megjeleníteni a következőt: %s"
+
+#: lib/diff.tcl:115
+msgid "Error loading file:"
+msgstr "Hiba a fájl betöltése közben:"
+
+#: lib/diff.tcl:122
+msgid "Git Repository (subproject)"
+msgstr "Git repó (alprojekt)"
+
+#: lib/diff.tcl:134
+msgid "* Binary file (not showing content)."
+msgstr "* Bináris fájl (tartalom elrejtése)."
+
+#: lib/diff.tcl:185
+msgid "Error loading diff:"
+msgstr "Hiba a diff betöltése közben:"
+
+#: lib/diff.tcl:302
+msgid "Failed to unstage selected hunk."
+msgstr "Nem visszavonni a hunk kiválasztását."
+
+#: lib/diff.tcl:309
+msgid "Failed to stage selected hunk."
+msgstr "Nem sikerült kiválasztani a hunkot."
+
+#: lib/error.tcl:12 lib/error.tcl:102
+msgid "error"
+msgstr "hiba"
+
+#: lib/error.tcl:28
+msgid "warning"
+msgstr "figyelmeztetés"
+
+#: lib/error.tcl:81
+msgid "You must correct the above errors before committing."
+msgstr "Ki kell javítanunk a fenti hibákat commit előtt."
+
+#: lib/index.tcl:6
+msgid "Unable to unlock the index."
+msgstr "Nem sikerült az index zárolásának feloldása."
+
+#: lib/index.tcl:15
+msgid "Index Error"
+msgstr "Index hiba"
+
+#: lib/index.tcl:21
+msgid ""
+"Updating the Git index failed.  A rescan will be automatically started to "
+"resynchronize git-gui."
+msgstr ""
+"A Git index frissítése sikertelen volt.  Egy újraolvasás automatikusan elindult, hogy "
+"a git-gui újra szinkonban legyen."
+
+#: lib/index.tcl:27
+msgid "Continue"
+msgstr "Folytatás"
+
+#: lib/index.tcl:31
+msgid "Unlock Index"
+msgstr "Index zárolásának feloldása"
+
+#: lib/index.tcl:282
+#, tcl-format
+msgid "Unstaging %s from commit"
+msgstr "A(z) %s commitba való kiválasztásának visszavonása"
+
+#: lib/index.tcl:326
+#, tcl-format
+msgid "Adding %s"
+msgstr "A(z) %s hozzáadása..."
+
+#: lib/index.tcl:381
+#, tcl-format
+msgid "Revert changes in file %s?"
+msgstr "Visszaállítja a változtatásokat a(z) %s fájlban?"
+
+#: lib/index.tcl:383
+#, tcl-format
+msgid "Revert changes in these %i files?"
+msgstr "Visszaállítja a változtatásokat ebben e %i fájlban?"
+
+#: lib/index.tcl:389
+msgid "Any unstaged changes will be permanently lost by the revert."
+msgstr ""
+"Minden nem kiválasztott változtatás el fog veszni ezáltal a visszaállítás "
+"által."
+
+#: lib/index.tcl:392
+msgid "Do Nothing"
+msgstr "Ne csináljunk semmit"
+
+#: lib/merge.tcl:13
+msgid ""
+"Cannot merge while amending.\n"
+"\n"
+"You must finish amending this commit before starting any type of merge.\n"
+msgstr ""
+"Javítás közben nem lehetséges a merge.\n"
+"\n"
+"Egyszer be kell fejezni ennek a commitnak a javítását, majd kezdődhet egy "
+"merge.\n"
+
+#: lib/merge.tcl:27
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before a merge can be performed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"Az utolsó keresési állapot nem egyezik meg a repó állapotával.\n"
+"\n"
+"Egy másik Git program módosította ezt a repót az utolsó keresés óta. Egy "
+"újrakeresés mindenképpen szükséges mielőtt a jelenlegi branchet módosítani "
+"lehetne.\n"
+"\n"
+"Az újrakeresés most automatikusan el fog indulni.\n"
+
+#: lib/merge.tcl:44
+#, tcl-format
+msgid ""
+"You are in the middle of a conflicted merge.\n"
+"\n"
+"File %s has merge conflicts.\n"
+"\n"
+"You must resolve them, stage the file, and commit to complete the current "
+"merge.  Only then can you begin another merge.\n"
+msgstr ""
+"Jelenleg egy ütközés feloldása közben vagyunk.\n"
+"\n"
+"A(z) %s fájlban ütközések vannak.\n"
+"\n"
+"Fel kell oldanunk őket, kiválasztani a fájlt, és commitolni hogy befejezzük "
+"a jelenlegi merge-t. Csak ezután kezdhetünk el egy újabbat.\n"
+
+#: lib/merge.tcl:54
+#, tcl-format
+msgid ""
+"You are in the middle of a change.\n"
+"\n"
+"File %s is modified.\n"
+"\n"
+"You should complete the current commit before starting a merge.  Doing so "
+"will help you abort a failed merge, should the need arise.\n"
+msgstr ""
+"Jelenleg egy változtatás közben vagyunk.\n"
+"\n"
+"A(z) %s fájl megváltozott.\n"
+"\n"
+"Először be kell fejeznünk a jelenlegi commitot, hogy elkezdhessünk egy merge-"
+"t. Ez segíteni fog, hogy félbeszakíthassunk egy merge-t.\n"
+
+#: lib/merge.tcl:106
+#, tcl-format
+msgid "%s of %s"
+msgstr "%s / %s"
+
+#: lib/merge.tcl:119
+#, tcl-format
+msgid "Merging %s and %s"
+msgstr "A(z) %s és a(z) %s merge-ölése"
+
+#: lib/merge.tcl:131
+msgid "Merge completed successfully."
+msgstr "A merge sikeresen befejeződött."
+
+#: lib/merge.tcl:133
+msgid "Merge failed.  Conflict resolution is required."
+msgstr "A merge sikertelen. Fel kell oldanunk az ütközéseket."
+
+#: lib/merge.tcl:158
+#, tcl-format
+msgid "Merge Into %s"
+msgstr "Merge-ölés a következőbe: %s"
+
+#: lib/merge.tcl:177
+msgid "Revision To Merge"
+msgstr "Merge-ölni szándékozott revízió"
+
+#: lib/merge.tcl:212
+msgid ""
+"Cannot abort while amending.\n"
+"\n"
+"You must finish amending this commit.\n"
+msgstr ""
+"A commit javítás közben megszakítva.\n"
+"\n"
+"Be kell fejeznünk ennek a commitnak a javítását.\n"
+
+#: lib/merge.tcl:222
+msgid ""
+"Abort merge?\n"
+"\n"
+"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with aborting the current merge?"
+msgstr ""
+"Megszakítjuk a merge-t?\n"
+"\n"
+"A jelenlegi merge megszakítása *MINDEN* nem commitolt változtatás "
+"elvesztését jelenti.\n"
+"\n"
+"Folytatjuk a jelenlegi merge megszakítását?"
+
+#: lib/merge.tcl:228
+msgid ""
+"Reset changes?\n"
+"\n"
+"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with resetting the current changes?"
+msgstr ""
+"Visszavonjuk a módosításokat?\n"
+"\n"
+"A módosítások visszavonása *MINDEN* nem commitolt változtatás elvesztését "
+"jelenti.\n"
+"\n"
+"Folytatjuk a jelenlegi módosítások visszavonását?"
+
+#: lib/merge.tcl:239
+msgid "Aborting"
+msgstr "Félbeszakítás"
+
+#: lib/merge.tcl:266
+msgid "Abort failed."
+msgstr "A félbeszakítás nem sikerült."
+
+#: lib/merge.tcl:268
+msgid "Abort completed.  Ready."
+msgstr "A megkeszakítás befejeződött. Kész."
+
+#: lib/option.tcl:82
+msgid "Restore Defaults"
+msgstr "Alapértelmezés visszaállítása"
+
+#: lib/option.tcl:86
+msgid "Save"
+msgstr "Mentés"
+
+#: lib/option.tcl:96
+#, tcl-format
+msgid "%s Repository"
+msgstr "%s Repó"
+
+#: lib/option.tcl:97
+msgid "Global (All Repositories)"
+msgstr "Globális (minden repó)"
+
+#: lib/option.tcl:103
+msgid "User Name"
+msgstr "Felhasználónév"
+
+#: lib/option.tcl:104
+msgid "Email Address"
+msgstr "Email cím"
+
+#: lib/option.tcl:106
+msgid "Summarize Merge Commits"
+msgstr "A merge commitok összegzése"
+
+#: lib/option.tcl:107
+msgid "Merge Verbosity"
+msgstr "Merge beszédesség"
+
+#: lib/option.tcl:108
+msgid "Show Diffstat After Merge"
+msgstr "Diffstat mutatása merge után"
+
+#: lib/option.tcl:110
+msgid "Trust File Modification Timestamps"
+msgstr "A fájl módosítási dátumok megbízhatóak"
+
+#: lib/option.tcl:111
+msgid "Prune Tracking Branches During Fetch"
+msgstr "A követő branchek eltávolítása letöltés alatt"
+
+#: lib/option.tcl:112
+msgid "Match Tracking Branches"
+msgstr "A követő branchek egyeztetése"
+
+#: lib/option.tcl:113
+msgid "Number of Diff Context Lines"
+msgstr "A diff környezeti sorok száma"
+
+#: lib/option.tcl:114
+msgid "New Branch Name Template"
+msgstr "Új branch név sablon"
+
+#: lib/option.tcl:176
+msgid "Change Font"
+msgstr "Betűtípus megváltoztatása"
+
+#: lib/option.tcl:180
+#, tcl-format
+msgid "Choose %s"
+msgstr "%s választása"
+
+#: lib/option.tcl:186
+msgid "pt."
+msgstr "pt."
+
+#: lib/option.tcl:200
+msgid "Preferences"
+msgstr "Beállítások"
+
+#: lib/option.tcl:235
+msgid "Failed to completely save options:"
+msgstr "Nem sikerült teljesen elmenteni a beállításokat:"
+
+#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
+msgid "Delete Remote Branch"
+msgstr "Távoli branch törlése"
+
+#: lib/remote_branch_delete.tcl:47
+msgid "From Repository"
+msgstr "Forrás repó"
+
+#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
+msgid "Remote:"
+msgstr "Távoli:"
+
+#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
+msgid "Arbitrary URL:"
+msgstr "Tetszőleges URL:"
+
+#: lib/remote_branch_delete.tcl:84
+msgid "Branches"
+msgstr "Branchek"
+
+#: lib/remote_branch_delete.tcl:109
+msgid "Delete Only If"
+msgstr "Törlés csak akkor ha"
+
+#: lib/remote_branch_delete.tcl:111
+msgid "Merged Into:"
+msgstr "Merge-ölt a következőbe:"
+
+#: lib/remote_branch_delete.tcl:119
+msgid "Always (Do not perform merge checks)"
+msgstr "Mindig (Ne végezzen merge vizsgálatokat)"
+
+#: lib/remote_branch_delete.tcl:152
+msgid "A branch is required for 'Merged Into'."
+msgstr "Egy branch szükséges a 'Merge-ölt a következőbe'-hez."
+
+#: lib/remote_branch_delete.tcl:184
+#, tcl-format
+msgid ""
+"The following branches are not completely merged into %s:\n"
+"\n"
+" - %s"
+msgstr ""
+"A következő branchek nem teljesen lettek merge-ölve ebbe: %s:"
+"\n"
+" - %s"
+
+#: lib/remote_branch_delete.tcl:189
+#, tcl-format
+msgid ""
+"One or more of the merge tests failed because you have not fetched the "
+"necessary commits.  Try fetching from %s first."
+msgstr ""
+"Egy vagy több merge teszt hibát jelzett, mivel nem töltöttük le a megfelelő "
+"commitokat. Próbáljunk meg letölteni a következőből: %s először."
+
+#: lib/remote_branch_delete.tcl:207
+msgid "Please select one or more branches to delete."
+msgstr "Válasszunk ki egy vagy több branchet törlésre."
+
+#: lib/remote_branch_delete.tcl:216
+msgid ""
+"Recovering deleted branches is difficult.\n"
+"\n"
+"Delete the selected branches?"
+msgstr ""
+"A törölt branchek visszaállítása nehéz.\n"
+"\n"
+"Töröljük a kiválasztott brancheket?"
+
+#: lib/remote_branch_delete.tcl:226
+#, tcl-format
+msgid "Deleting branches from %s"
+msgstr "Brancek törlése innen: %s"
+
+#: lib/remote_branch_delete.tcl:286
+msgid "No repository selected."
+msgstr "Nincs kiválasztott repó."
+
+#: lib/remote_branch_delete.tcl:291
+#, tcl-format
+msgid "Scanning %s..."
+msgstr "Keresés itt: %s..."
+
+#: lib/remote.tcl:165
+msgid "Prune from"
+msgstr "Törlés innen"
+
+# tcl-format
+#: lib/remote.tcl:170
+msgid "Fetch from"
+msgstr "Letöltés innen"
+
+#: lib/remote.tcl:213
+msgid "Push to"
+msgstr "Push ide"
+
+#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
+msgid "Cannot write shortcut:"
+msgstr "Nem sikerült írni a gyorsbillentyűt:"
+
+#: lib/shortcut.tcl:136
+msgid "Cannot write icon:"
+msgstr "Nem sikerült írni az ikont:"
+
+#: lib/status_bar.tcl:83
+#, tcl-format
+msgid "%s ... %*i of %*i %s (%3i%%)"
+msgstr "%s ... %*i / %*i %s (%3i%%)"
+
+#: lib/transport.tcl:6
+#, tcl-format
+msgid "fetch %s"
+msgstr "a(z) %s letöltése"
+
+#: lib/transport.tcl:7
+#, tcl-format
+msgid "Fetching new changes from %s"
+msgstr "Új változások letöltése innen: %s"
+
+#: lib/transport.tcl:18
+#, tcl-format
+msgid "remote prune %s"
+msgstr "a(z) %s távoli törlése"
+
+#: lib/transport.tcl:19
+#, tcl-format
+msgid "Pruning tracking branches deleted from %s"
+msgstr "A %s repóból törölt követő branchek törlése"
+
+#: lib/transport.tcl:25 lib/transport.tcl:71
+#, tcl-format
+msgid "push %s"
+msgstr "%s push-olása"
+
+#: lib/transport.tcl:26
+#, tcl-format
+msgid "Pushing changes to %s"
+msgstr "Változások pusholása ide: %s"
+
+#: lib/transport.tcl:72
+#, tcl-format
+msgid "Pushing %s %s to %s"
+msgstr "Pusholás: %s %s, ide: %s"
+
+#: lib/transport.tcl:89
+msgid "Push Branches"
+msgstr "Branchek pusholása"
+
+#: lib/transport.tcl:103
+msgid "Source Branches"
+msgstr "Forrás branchek"
+
+#: lib/transport.tcl:120
+msgid "Destination Repository"
+msgstr "Cél repó"
+
+#: lib/transport.tcl:158
+msgid "Transfer Options"
+msgstr "Átviteli opciók"
+
+#: lib/transport.tcl:160
+msgid "Force overwrite existing branch (may discard changes)"
+msgstr "Létező branch felülírásának erőltetése (lehet, hogy el fog dobni változtatásokat)"
+
+#: lib/transport.tcl:164
+msgid "Use thin pack (for slow network connections)"
+msgstr "Vékony csomagok használata (lassú hálózati kapcsolatok számára)"
+
+#: lib/transport.tcl:168
+msgid "Include tags"
+msgstr "Tageket is"
+
+#~ msgid "Cannot find the git directory:"
+#~ msgstr "Nem található a git könyvtár:"
+
+#~ msgid "Unstaged Changes (Will Not Be Committed)"
+#~ msgstr "Nem kiválasztott változtatások (nem lesz commitolva)"
+
+#~ msgid "Push to %s..."
+#~ msgstr "Pusholás ide: %s..."
+
+#~ msgid "Add To Commit"
+#~ msgstr "Hozzáadás a commithoz"
+
+#~ msgid "Add Existing To Commit"
+#~ msgstr "Hozzáadás létező commithoz"
+
+#~ msgid "Running miga..."
+#~ msgstr "A miga futtatása..."
+
+#~ msgid "Add Existing"
+#~ msgstr "Létező hozzáadása"
+
+#~ msgid ""
+#~ "Abort commit?\n"
+#~ "\n"
+#~ "Aborting the current commit will cause *ALL* uncommitted changes to be "
+#~ "lost.\n"
+#~ "\n"
+#~ "Continue with aborting the current commit?"
+#~ msgstr ""
+#~ "Megszakítjuk a commitot?\n"
+#~ "\n"
+#~ "A jelenlegi commit megszakítása *MINDEN* nem commitolt változtatás "
+#~ "elvesztését jelenti.\n"
+#~ "\n"
+#~ "Folytatjuk a jelenlegi commit megszakítását?"
+
+#~ msgid "Aborting... please wait..."
+#~ msgstr "Megszakítás... várjunk..."
diff --git a/git-gui/po/it.po b/git-gui/po/it.po
new file mode 100644 (file)
index 0000000..33a8399
--- /dev/null
@@ -0,0 +1,1913 @@
+# Translation of git-gui to Italian
+# Copyright (C) 2007 Shawn Pearce
+# This file is distributed under the same license as the git-gui package.
+# Paolo Ciarrocchi <paolo.ciarrocchi@gmail.com>, 2007
+# Michele Ballabio <barra_cuda@katamail.com>, 2007.
+# 
+# 
+msgid ""
+msgstr ""
+"Project-Id-Version: git-gui\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-11-09 11:18+0100\n"
+"PO-Revision-Date: 2007-11-01 21:05+0100\n"
+"Last-Translator: Michele Ballabio <barra_cuda@katamail.com>\n"
+"Language-Team: Italian <tp@lists.linux.it>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
+#: git-gui.sh:733
+msgid "git-gui: fatal error"
+msgstr "git-gui: errore grave"
+
+#: git-gui.sh:565
+#, tcl-format
+msgid "Invalid font specified in %s:"
+msgstr "Caratteri non validi specificati in %s:"
+
+#: git-gui.sh:590
+msgid "Main Font"
+msgstr "Caratteri principali"
+
+#: git-gui.sh:591
+msgid "Diff/Console Font"
+msgstr "Caratteri per confronti e terminale"
+
+#: git-gui.sh:605
+msgid "Cannot find git in PATH."
+msgstr "Impossibile trovare git nel PATH"
+
+#: git-gui.sh:632
+msgid "Cannot parse Git version string:"
+msgstr "Impossibile determinare la versione di Git:"
+
+#: git-gui.sh:650
+#, tcl-format
+msgid ""
+"Git version cannot be determined.\n"
+"\n"
+"%s claims it is version '%s'.\n"
+"\n"
+"%s requires at least Git 1.5.0 or later.\n"
+"\n"
+"Assume '%s' is version 1.5.0?\n"
+msgstr ""
+"La versione di Git non può essere determinata.\n"
+"\n"
+"%s riporta che la versione è '%s'.\n"
+"\n"
+"%s richiede almeno Git 1.5.0 o superiore.\n"
+"\n"
+"Assumere che '%s' sia alla versione 1.5.0?\n"
+
+#: git-gui.sh:888
+msgid "Git directory not found:"
+msgstr "Non trovo la directory di git: "
+
+#: git-gui.sh:895
+msgid "Cannot move to top of working directory:"
+msgstr "Impossibile spostarsi sulla directory principale del progetto:"
+
+#: git-gui.sh:902
+msgid "Cannot use funny .git directory:"
+msgstr "Impossibile usare una .git directory strana:"
+
+#: git-gui.sh:907
+msgid "No working directory"
+msgstr "Nessuna directory di lavoro"
+
+#: git-gui.sh:1054
+msgid "Refreshing file status..."
+msgstr "Controllo dello stato dei file in corso..."
+
+#: git-gui.sh:1119
+msgid "Scanning for modified files ..."
+msgstr "Ricerca di file modificati in corso..."
+
+#: git-gui.sh:1294 lib/browser.tcl:245
+msgid "Ready."
+msgstr "Pronto."
+
+#: git-gui.sh:1560
+msgid "Unmodified"
+msgstr "Non modificato"
+
+#: git-gui.sh:1562
+msgid "Modified, not staged"
+msgstr "Modificato, non preparato per una nuova revisione"
+
+#: git-gui.sh:1563 git-gui.sh:1568
+msgid "Staged for commit"
+msgstr "Preparato per una nuova revisione"
+
+#: git-gui.sh:1564 git-gui.sh:1569
+msgid "Portions staged for commit"
+msgstr "Parti preparate per una nuova revisione"
+
+#: git-gui.sh:1565 git-gui.sh:1570
+msgid "Staged for commit, missing"
+msgstr "Preparato per una nuova revisione, mancante"
+
+#: git-gui.sh:1567
+msgid "Untracked, not staged"
+msgstr "Non tracciato, non preparato per una nuova revisione"
+
+#: git-gui.sh:1572
+msgid "Missing"
+msgstr "Mancante"
+
+#: git-gui.sh:1573
+msgid "Staged for removal"
+msgstr "Preparato per la rimozione"
+
+#: git-gui.sh:1574
+msgid "Staged for removal, still present"
+msgstr "Preparato alla rimozione, ancora presente"
+
+#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
+msgid "Requires merge resolution"
+msgstr "Richiede risoluzione dei conflitti"
+
+#: git-gui.sh:1614
+msgid "Starting gitk... please wait..."
+msgstr "Avvio di gitk... attendere..."
+
+#: git-gui.sh:1623
+#, tcl-format
+msgid ""
+"Unable to start gitk:\n"
+"\n"
+"%s does not exist"
+msgstr ""
+"Impossibile avviare gitk:\n"
+"\n"
+"%s non esiste"
+
+#: git-gui.sh:1823 lib/choose_repository.tcl:35
+msgid "Repository"
+msgstr "Archivio"
+
+#: git-gui.sh:1824
+msgid "Edit"
+msgstr "Modifica"
+
+#: git-gui.sh:1826 lib/choose_rev.tcl:560
+msgid "Branch"
+msgstr "Ramo"
+
+#: git-gui.sh:1829 lib/choose_rev.tcl:547
+msgid "Commit@@noun"
+msgstr "Revisione"
+
+#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
+msgid "Merge"
+msgstr "Fusione (Merge)"
+
+#: git-gui.sh:1833 lib/choose_rev.tcl:556
+msgid "Remote"
+msgstr "Remoto"
+
+#: git-gui.sh:1842
+msgid "Browse Current Branch's Files"
+msgstr "Esplora i file del ramo attuale"
+
+#: git-gui.sh:1846
+msgid "Browse Branch Files..."
+msgstr "Esplora i file del ramo..."
+
+#: git-gui.sh:1851
+msgid "Visualize Current Branch's History"
+msgstr "Visualizza la cronologia del ramo attuale"
+
+#: git-gui.sh:1855
+msgid "Visualize All Branch History"
+msgstr "Visualizza la cronologia di tutti i rami"
+
+#: git-gui.sh:1862
+#, tcl-format
+msgid "Browse %s's Files"
+msgstr "Esplora i file di %s"
+
+#: git-gui.sh:1864
+#, tcl-format
+msgid "Visualize %s's History"
+msgstr "Visualizza la cronologia di %s"
+
+#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
+msgid "Database Statistics"
+msgstr "Statistiche dell'archivio"
+
+#: git-gui.sh:1872 lib/database.tcl:34
+msgid "Compress Database"
+msgstr "Comprimi l'archivio"
+
+#: git-gui.sh:1875
+msgid "Verify Database"
+msgstr "Verifica l'archivio"
+
+#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
+#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
+msgid "Create Desktop Icon"
+msgstr "Crea icona desktop"
+
+#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
+msgid "Quit"
+msgstr "Esci"
+
+#: git-gui.sh:1902
+msgid "Undo"
+msgstr "Annulla"
+
+#: git-gui.sh:1905
+msgid "Redo"
+msgstr "Ripeti"
+
+#: git-gui.sh:1909 git-gui.sh:2403
+msgid "Cut"
+msgstr "Taglia"
+
+#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
+#: lib/console.tcl:67
+msgid "Copy"
+msgstr "Copia"
+
+#: git-gui.sh:1915 git-gui.sh:2409
+msgid "Paste"
+msgstr "Incolla"
+
+#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
+#: lib/remote_branch_delete.tcl:38
+msgid "Delete"
+msgstr "Elimina"
+
+#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
+msgid "Select All"
+msgstr "Seleziona tutto"
+
+#: git-gui.sh:1931
+msgid "Create..."
+msgstr "Crea..."
+
+#: git-gui.sh:1937
+msgid "Checkout..."
+msgstr "Attiva..."
+
+#: git-gui.sh:1943
+msgid "Rename..."
+msgstr "Rinomina"
+
+#: git-gui.sh:1948 git-gui.sh:2048
+msgid "Delete..."
+msgstr "Elimina..."
+
+#: git-gui.sh:1953
+msgid "Reset..."
+msgstr "Ripristina..."
+
+#: git-gui.sh:1965 git-gui.sh:2350
+msgid "New Commit"
+msgstr "Nuova revisione"
+
+#: git-gui.sh:1973 git-gui.sh:2357
+msgid "Amend Last Commit"
+msgstr "Correggi l'ultima revisione"
+
+#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
+msgid "Rescan"
+msgstr "Analizza nuovamente"
+
+#: git-gui.sh:1988
+msgid "Stage To Commit"
+msgstr "Prepara per una nuova revisione"
+
+#: git-gui.sh:1994
+msgid "Stage Changed Files To Commit"
+msgstr "Prepara i file modificati per una nuova revisione"
+
+#: git-gui.sh:2000
+msgid "Unstage From Commit"
+msgstr "Annulla preparazione"
+
+#: git-gui.sh:2005 lib/index.tcl:393
+msgid "Revert Changes"
+msgstr "Annulla modifiche"
+
+#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
+msgid "Sign Off"
+msgstr "Sign Off"
+
+#: git-gui.sh:2016 git-gui.sh:2333
+msgid "Commit@@verb"
+msgstr "Nuova revisione"
+
+#: git-gui.sh:2027
+msgid "Local Merge..."
+msgstr "Fusione locale..."
+
+#: git-gui.sh:2032
+msgid "Abort Merge..."
+msgstr "Interrompi fusione..."
+
+#: git-gui.sh:2044
+msgid "Push..."
+msgstr "Propaga..."
+
+#: git-gui.sh:2055 lib/choose_repository.tcl:40
+msgid "Apple"
+msgstr "Apple"
+
+#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
+#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
+#, tcl-format
+msgid "About %s"
+msgstr "Informazioni su %s"
+
+#: git-gui.sh:2062
+msgid "Preferences..."
+msgstr "Preferenze..."
+
+#: git-gui.sh:2070 git-gui.sh:2595
+msgid "Options..."
+msgstr "Opzioni..."
+
+#: git-gui.sh:2076 lib/choose_repository.tcl:46
+msgid "Help"
+msgstr "Aiuto"
+
+#: git-gui.sh:2117
+msgid "Online Documentation"
+msgstr "Documentazione sul web"
+
+#: git-gui.sh:2201
+#, tcl-format
+msgid "fatal: cannot stat path %s: No such file or directory"
+msgstr ""
+"errore grave: impossibile effettuare lo stat del path %s: file o directory "
+"non trovata"
+
+#: git-gui.sh:2234
+msgid "Current Branch:"
+msgstr "Ramo attuale:"
+
+#: git-gui.sh:2255
+msgid "Staged Changes (Will Commit)"
+msgstr "Modifiche preparate (saranno nella nuova revisione)"
+
+#: git-gui.sh:2274
+msgid "Unstaged Changes"
+msgstr "Modifiche non preparate"
+
+#: git-gui.sh:2323
+msgid "Stage Changed"
+msgstr "Prepara modificati"
+
+#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
+msgid "Push"
+msgstr "Propaga (Push)"
+
+#: git-gui.sh:2369
+msgid "Initial Commit Message:"
+msgstr "Messaggio di revisione iniziale:"
+
+#: git-gui.sh:2370
+msgid "Amended Commit Message:"
+msgstr "Messaggio di revisione corretto:"
+
+#: git-gui.sh:2371
+msgid "Amended Initial Commit Message:"
+msgstr "Messaggio iniziale di revisione corretto:"
+
+#: git-gui.sh:2372
+msgid "Amended Merge Commit Message:"
+msgstr "Messaggio di fusione corretto:"
+
+#: git-gui.sh:2373
+msgid "Merge Commit Message:"
+msgstr "Messaggio di fusione:"
+
+#: git-gui.sh:2374
+msgid "Commit Message:"
+msgstr "Messaggio di revisione:"
+
+#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
+msgid "Copy All"
+msgstr "Copia tutto"
+
+#: git-gui.sh:2443 lib/blame.tcl:104
+msgid "File:"
+msgstr "File:"
+
+#: git-gui.sh:2545
+msgid "Refresh"
+msgstr "Rinfresca"
+
+#: git-gui.sh:2566
+msgid "Apply/Reverse Hunk"
+msgstr "Applica/Inverti sezione"
+
+#: git-gui.sh:2572
+msgid "Decrease Font Size"
+msgstr "Diminuisci dimensione caratteri"
+
+#: git-gui.sh:2576
+msgid "Increase Font Size"
+msgstr "Aumenta dimensione caratteri"
+
+#: git-gui.sh:2581
+msgid "Show Less Context"
+msgstr "Mostra meno contesto"
+
+#: git-gui.sh:2588
+msgid "Show More Context"
+msgstr "Mostra più contesto"
+
+#: git-gui.sh:2602
+msgid "Unstage Hunk From Commit"
+msgstr "Sezione non preparata per una nuova revisione"
+
+#: git-gui.sh:2604
+msgid "Stage Hunk For Commit"
+msgstr "Prepara sezione per una nuova revisione"
+
+#: git-gui.sh:2623
+msgid "Initializing..."
+msgstr "Inizializzazione..."
+
+#: git-gui.sh:2718
+#, tcl-format
+msgid ""
+"Possible environment issues exist.\n"
+"\n"
+"The following environment variables are probably\n"
+"going to be ignored by any Git subprocess run\n"
+"by %s:\n"
+"\n"
+msgstr ""
+"Possibili problemi con le variabili d'ambiente.\n"
+"\n"
+"Le seguenti variabili d'ambiente saranno probabilmente\n"
+"ignorate da tutti i sottoprocessi di Git avviati\n"
+"da %s:\n"
+"\n"
+
+#: git-gui.sh:2748
+msgid ""
+"\n"
+"This is due to a known issue with the\n"
+"Tcl binary distributed by Cygwin."
+msgstr ""
+"\n"
+"Ciò è dovuto a un problema conosciuto\n"
+"causato dall'eseguibile Tcl distribuito da Cygwin."
+
+#: git-gui.sh:2753
+#, tcl-format
+msgid ""
+"\n"
+"\n"
+"A good replacement for %s\n"
+"is placing values for the user.name and\n"
+"user.email settings into your personal\n"
+"~/.gitconfig file.\n"
+msgstr ""
+"\n"
+"\n"
+"Una buona alternativa a %s\n"
+"consiste nell'assegnare valori alle variabili di configurazione\n"
+"user.name e user.email nel tuo file ~/.gitconfig personale.\n"
+
+#: lib/about.tcl:25
+msgid "git-gui - a graphical user interface for Git."
+msgstr "git-gui - un'interfaccia grafica per Git."
+
+#: lib/blame.tcl:77
+msgid "File Viewer"
+msgstr "Mostra file"
+
+#: lib/blame.tcl:81
+msgid "Commit:"
+msgstr "Revisione:"
+
+#: lib/blame.tcl:249
+msgid "Copy Commit"
+msgstr "Copia revisione"
+
+#: lib/blame.tcl:369
+#, tcl-format
+msgid "Reading %s..."
+msgstr "Lettura di %s..."
+
+#: lib/blame.tcl:473
+msgid "Loading copy/move tracking annotations..."
+msgstr "Caricamento annotazioni per copie/spostamenti..."
+
+#: lib/blame.tcl:493
+msgid "lines annotated"
+msgstr "linee annotate"
+
+#: lib/blame.tcl:674
+msgid "Loading original location annotations..."
+msgstr "Caricamento annotazioni per posizione originaria..."
+
+#: lib/blame.tcl:677
+msgid "Annotation complete."
+msgstr "Annotazione completata."
+
+#: lib/blame.tcl:731
+msgid "Loading annotation..."
+msgstr "Caricamento annotazioni..."
+
+#: lib/blame.tcl:787
+msgid "Author:"
+msgstr "Autore:"
+
+#: lib/blame.tcl:791
+msgid "Committer:"
+msgstr "Revisione creata da:"
+
+#: lib/blame.tcl:796
+msgid "Original File:"
+msgstr "File originario:"
+
+#: lib/blame.tcl:910
+msgid "Originally By:"
+msgstr "In origine da:"
+
+#: lib/blame.tcl:916
+msgid "In File:"
+msgstr "Nel file:"
+
+#: lib/blame.tcl:921
+msgid "Copied Or Moved Here By:"
+msgstr "Copiato o spostato qui da:"
+
+#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
+msgid "Checkout Branch"
+msgstr "Attiva ramo"
+
+#: lib/branch_checkout.tcl:23
+msgid "Checkout"
+msgstr "Attiva"
+
+#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
+#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
+#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
+#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
+msgid "Cancel"
+msgstr "Annulla"
+
+#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
+msgid "Revision"
+msgstr "Revisione"
+
+#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
+msgid "Options"
+msgstr "Opzioni"
+
+#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
+msgid "Fetch Tracking Branch"
+msgstr "Recupera duplicato locale di ramo remoto"
+
+#: lib/branch_checkout.tcl:44
+msgid "Detach From Local Branch"
+msgstr "Stacca da ramo locale"
+
+#: lib/branch_create.tcl:22
+msgid "Create Branch"
+msgstr "Crea ramo"
+
+#: lib/branch_create.tcl:27
+msgid "Create New Branch"
+msgstr "Crea nuovo ramo"
+
+#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
+msgid "Create"
+msgstr "Crea"
+
+#: lib/branch_create.tcl:40
+msgid "Branch Name"
+msgstr "Nome del ramo"
+
+#: lib/branch_create.tcl:43
+msgid "Name:"
+msgstr "Nome:"
+
+#: lib/branch_create.tcl:58
+msgid "Match Tracking Branch Name"
+msgstr "Appaia nome del duplicato locale di ramo remoto"
+
+#: lib/branch_create.tcl:66
+msgid "Starting Revision"
+msgstr "Revisione iniziale"
+
+#: lib/branch_create.tcl:72
+msgid "Update Existing Branch:"
+msgstr "Aggiorna ramo esistente:"
+
+#: lib/branch_create.tcl:75
+msgid "No"
+msgstr "No"
+
+#: lib/branch_create.tcl:80
+msgid "Fast Forward Only"
+msgstr "Solo fast forward"
+
+#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
+msgid "Reset"
+msgstr "Ripristina"
+
+#: lib/branch_create.tcl:97
+msgid "Checkout After Creation"
+msgstr "Attiva dopo la creazione"
+
+#: lib/branch_create.tcl:131
+msgid "Please select a tracking branch."
+msgstr "Scegliere un duplicato locale di ramo remoto"
+
+#: lib/branch_create.tcl:140
+#, tcl-format
+msgid "Tracking branch %s is not a branch in the remote repository."
+msgstr ""
+"Il duplicato locale del ramo remoto %s non è un ramo nell'archivio remoto."
+
+#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
+msgid "Please supply a branch name."
+msgstr "Inserire un nome per il ramo."
+
+#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
+#, tcl-format
+msgid "'%s' is not an acceptable branch name."
+msgstr "'%s' non è utilizzabile come nome di ramo."
+
+#: lib/branch_delete.tcl:15
+msgid "Delete Branch"
+msgstr "Elimina ramo"
+
+#: lib/branch_delete.tcl:20
+msgid "Delete Local Branch"
+msgstr "Elimina ramo locale"
+
+#: lib/branch_delete.tcl:37
+msgid "Local Branches"
+msgstr "Rami locali"
+
+#: lib/branch_delete.tcl:52
+msgid "Delete Only If Merged Into"
+msgstr "Cancella solo se fuso con un altro ramo"
+
+#: lib/branch_delete.tcl:54
+msgid "Always (Do not perform merge test.)"
+msgstr "Sempre (Non effettuare verifiche di fusione)."
+
+#: lib/branch_delete.tcl:103
+#, tcl-format
+msgid "The following branches are not completely merged into %s:"
+msgstr "I rami seguenti non sono stati fusi completamente in %s:"
+
+#: lib/branch_delete.tcl:115
+msgid ""
+"Recovering deleted branches is difficult. \n"
+"\n"
+" Delete the selected branches?"
+msgstr ""
+"Ricomporre rami cancellati può essere complicato. \n"
+"\n"
+" Eliminare i rami selezionati?"
+
+#: lib/branch_delete.tcl:141
+#, tcl-format
+msgid ""
+"Failed to delete branches:\n"
+"%s"
+msgstr ""
+"Impossibile cancellare i rami:\n"
+"%s"
+
+#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
+msgid "Rename Branch"
+msgstr "Rinomina ramo"
+
+#: lib/branch_rename.tcl:26
+msgid "Rename"
+msgstr "Rinomina"
+
+#: lib/branch_rename.tcl:36
+msgid "Branch:"
+msgstr "Ramo:"
+
+#: lib/branch_rename.tcl:39
+msgid "New Name:"
+msgstr "Nuovo Nome:"
+
+#: lib/branch_rename.tcl:75
+msgid "Please select a branch to rename."
+msgstr "Scegliere un ramo da rinominare."
+
+#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
+#, tcl-format
+msgid "Branch '%s' already exists."
+msgstr "Il ramo '%s' esiste già."
+
+#: lib/branch_rename.tcl:117
+#, tcl-format
+msgid "Failed to rename '%s'."
+msgstr "Impossibile rinominare '%s'."
+
+#: lib/browser.tcl:17
+msgid "Starting..."
+msgstr "Avvio in corso..."
+
+#: lib/browser.tcl:26
+msgid "File Browser"
+msgstr "File browser"
+
+#: lib/browser.tcl:125 lib/browser.tcl:142
+#, tcl-format
+msgid "Loading %s..."
+msgstr "Caricamento %s..."
+
+#: lib/browser.tcl:186
+msgid "[Up To Parent]"
+msgstr "[Directory superiore]"
+
+#: lib/browser.tcl:266 lib/browser.tcl:272
+msgid "Browse Branch Files"
+msgstr "Esplora i file del ramo"
+
+#: lib/browser.tcl:277 lib/choose_repository.tcl:391
+#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
+#: lib/choose_repository.tcl:989
+msgid "Browse"
+msgstr "Sfoglia"
+
+#: lib/checkout_op.tcl:79
+#, tcl-format
+msgid "Fetching %s from %s"
+msgstr "Recupero %s da %s"
+
+#: lib/checkout_op.tcl:127
+#, tcl-format
+msgid "fatal: Cannot resolve %s"
+msgstr "errore grave: impossibile risolvere %s"
+
+#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
+msgid "Close"
+msgstr "Chiudi"
+
+#: lib/checkout_op.tcl:169
+#, tcl-format
+msgid "Branch '%s' does not exist."
+msgstr "Il ramo '%s' non esiste."
+
+#: lib/checkout_op.tcl:206
+#, tcl-format
+msgid ""
+"Branch '%s' already exists.\n"
+"\n"
+"It cannot fast-forward to %s.\n"
+"A merge is required."
+msgstr ""
+"Il ramo '%s' esiste già.\n"
+"\n"
+"Non può effettuare un 'fast-forward' a %s.\n"
+"E' necessaria una fusione."
+
+#: lib/checkout_op.tcl:220
+#, tcl-format
+msgid "Merge strategy '%s' not supported."
+msgstr "La strategia di fusione '%s' non è supportata."
+
+#: lib/checkout_op.tcl:239
+#, tcl-format
+msgid "Failed to update '%s'."
+msgstr "Impossibile aggiornare '%s'."
+
+#: lib/checkout_op.tcl:251
+msgid "Staging area (index) is already locked."
+msgstr ""
+"L'area di preparazione per una nuova revisione (indice) è già bloccata."
+
+#: lib/checkout_op.tcl:266
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before the current branch can be changed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"L'ultimo stato analizzato non corrisponde allo stato dell'archivio.\n"
+"\n"
+"Un altro programma Git ha modificato questo archivio dall'ultima analisi. "
+"Bisogna effettuare una nuova analisi prima di poter cambiare il ramo "
+"attuale.\n"
+"\n"
+"La nuova analisi comincerà ora.\n"
+
+#: lib/checkout_op.tcl:322
+#, tcl-format
+msgid "Updating working directory to '%s'..."
+msgstr "Aggiornamento della directory di lavoro a '%s' in corso..."
+
+#: lib/checkout_op.tcl:353
+#, tcl-format
+msgid "Aborted checkout of '%s' (file level merging is required)."
+msgstr "Attivazione di '%s' fallita (richiesta una fusione a livello file)."
+
+#: lib/checkout_op.tcl:354
+msgid "File level merge required."
+msgstr "E' richiesta una fusione a livello file."
+
+#: lib/checkout_op.tcl:358
+#, tcl-format
+msgid "Staying on branch '%s'."
+msgstr "Si rimarrà sul ramo '%s'."
+
+#: lib/checkout_op.tcl:429
+msgid ""
+"You are no longer on a local branch.\n"
+"\n"
+"If you wanted to be on a branch, create one now starting from 'This Detached "
+"Checkout'."
+msgstr ""
+"Non si è più su un ramo locale\n"
+"\n"
+"Se si vuole rimanere su un ramo, crearne uno ora a partire da 'Questa "
+"revisione attiva staccata'."
+
+#: lib/checkout_op.tcl:446
+#, tcl-format
+msgid "Checked out '%s'."
+msgstr "Attivazione di '%s' completata."
+
+#: lib/checkout_op.tcl:478
+#, tcl-format
+msgid "Resetting '%s' to '%s' will lose the following commits:"
+msgstr ""
+"Ripristinare '%s' a '%s' comporterà la perdita delle seguenti revisioni:"
+
+#: lib/checkout_op.tcl:500
+msgid "Recovering lost commits may not be easy."
+msgstr "Ricomporre le revisioni perdute potrebbe non essere semplice."
+
+#: lib/checkout_op.tcl:505
+#, tcl-format
+msgid "Reset '%s'?"
+msgstr "Ripristinare '%s'?"
+
+#: lib/checkout_op.tcl:510 lib/merge.tcl:164
+msgid "Visualize"
+msgstr "Visualizza"
+
+#: lib/checkout_op.tcl:578
+#, tcl-format
+msgid ""
+"Failed to set current branch.\n"
+"\n"
+"This working directory is only partially switched.  We successfully updated "
+"your files, but failed to update an internal Git file.\n"
+"\n"
+"This should not have occurred.  %s will now close and give up."
+msgstr ""
+"Impossibile preparare il ramo attuale.\n"
+"\n"
+"Questa directory di lavoro è stata convertita solo parzialmente. I file sono "
+"stati aggiornati correttamente, ma l'aggiornamento di un file di Git ha "
+"prodotto degli errori.\n"
+"\n"
+"Questo non sarebbe dovuto succedere.  %s ora terminerà senza altre azioni."
+
+#: lib/choose_font.tcl:39
+msgid "Select"
+msgstr "Seleziona"
+
+#: lib/choose_font.tcl:53
+msgid "Font Family"
+msgstr "Famiglia di caratteri"
+
+#: lib/choose_font.tcl:73
+msgid "Font Size"
+msgstr "Dimensione caratteri"
+
+#: lib/choose_font.tcl:90
+msgid "Font Example"
+msgstr "Esempio caratteri"
+
+#: lib/choose_font.tcl:101
+msgid ""
+"This is example text.\n"
+"If you like this text, it can be your font."
+msgstr ""
+"Questo è un testo d'esempio.\n"
+"Se ti piace questo testo, può essere il carattere giusto."
+
+#: lib/choose_repository.tcl:27
+msgid "Git Gui"
+msgstr "Git Gui"
+
+#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
+msgid "Create New Repository"
+msgstr "Crea nuovo archivio"
+
+#: lib/choose_repository.tcl:86
+msgid "New..."
+msgstr "Nuovo..."
+
+#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
+msgid "Clone Existing Repository"
+msgstr "Clona archivio esistente"
+
+#: lib/choose_repository.tcl:99
+msgid "Clone..."
+msgstr "Clona..."
+
+#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
+msgid "Open Existing Repository"
+msgstr "Apri archivio esistente"
+
+#: lib/choose_repository.tcl:112
+msgid "Open..."
+msgstr "Apri..."
+
+#: lib/choose_repository.tcl:125
+msgid "Recent Repositories"
+msgstr "Archivi recenti"
+
+#: lib/choose_repository.tcl:131
+msgid "Open Recent Repository:"
+msgstr "Apri archivio recente:"
+
+#: lib/choose_repository.tcl:294
+#, tcl-format
+msgid "Location %s already exists."
+msgstr "La posizione %s esiste già."
+
+#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
+#: lib/choose_repository.tcl:314
+#, tcl-format
+msgid "Failed to create repository %s:"
+msgstr "Impossibile creare l'archivio %s:"
+
+#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
+msgid "Directory:"
+msgstr "Directory:"
+
+#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
+#: lib/choose_repository.tcl:1013
+msgid "Git Repository"
+msgstr "Archivio Git"
+
+#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
+#, tcl-format
+msgid "Directory %s already exists."
+msgstr "La directory %s esiste già."
+
+#: lib/choose_repository.tcl:442
+#, tcl-format
+msgid "File %s already exists."
+msgstr "Il file %s esiste già."
+
+#: lib/choose_repository.tcl:463
+msgid "Clone"
+msgstr "Clona"
+
+#: lib/choose_repository.tcl:476
+msgid "URL:"
+msgstr "URL:"
+
+#: lib/choose_repository.tcl:496
+msgid "Clone Type:"
+msgstr "Tipo di clone:"
+
+#: lib/choose_repository.tcl:502
+msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
+msgstr "Standard (veloce, semi-ridondante, con hardlink)"
+
+#: lib/choose_repository.tcl:508
+msgid "Full Copy (Slower, Redundant Backup)"
+msgstr "Copia completa (più lento, backup ridondante)"
+
+#: lib/choose_repository.tcl:514
+msgid "Shared (Fastest, Not Recommended, No Backup)"
+msgstr "Shared (il più veloce, non raccomandato, nessun backup)"
+
+#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
+#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
+#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
+#, tcl-format
+msgid "Not a Git repository: %s"
+msgstr "%s non è un archivio Git."
+
+#: lib/choose_repository.tcl:586
+msgid "Standard only available for local repository."
+msgstr "Standard è disponibile solo per archivi locali."
+
+#: lib/choose_repository.tcl:590
+msgid "Shared only available for local repository."
+msgstr "Shared è disponibile solo per archivi locali."
+
+#: lib/choose_repository.tcl:617
+msgid "Failed to configure origin"
+msgstr "Impossibile configurare origin"
+
+#: lib/choose_repository.tcl:629
+msgid "Counting objects"
+msgstr "Calcolo oggetti"
+
+#: lib/choose_repository.tcl:630
+msgid "buckets"
+msgstr ""
+
+#: lib/choose_repository.tcl:654
+#, tcl-format
+msgid "Unable to copy objects/info/alternates: %s"
+msgstr "Impossibile copiare oggetti/info/alternate: %s"
+
+#: lib/choose_repository.tcl:690
+#, tcl-format
+msgid "Nothing to clone from %s."
+msgstr "Niente da clonare da %s."
+
+#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
+#: lib/choose_repository.tcl:918
+msgid "The 'master' branch has not been initialized."
+msgstr "Il ramo 'master' non è stato inizializzato."
+
+#: lib/choose_repository.tcl:705
+msgid "Hardlinks are unavailable.  Falling back to copying."
+msgstr "Impossibile utilizzare gli hardlink. Si ricorrerà alla copia."
+
+#: lib/choose_repository.tcl:717
+#, tcl-format
+msgid "Cloning from %s"
+msgstr "Clonazione da %s"
+
+#: lib/choose_repository.tcl:748
+msgid "Copying objects"
+msgstr "Copia degli oggetti"
+
+#: lib/choose_repository.tcl:749
+msgid "KiB"
+msgstr "KiB"
+
+#: lib/choose_repository.tcl:773
+#, tcl-format
+msgid "Unable to copy object: %s"
+msgstr "Impossibile copiare oggetto: %s"
+
+#: lib/choose_repository.tcl:783
+msgid "Linking objects"
+msgstr "Collegamento oggetti"
+
+#: lib/choose_repository.tcl:784
+msgid "objects"
+msgstr "oggetti"
+
+#: lib/choose_repository.tcl:792
+#, tcl-format
+msgid "Unable to hardlink object: %s"
+msgstr "Hardlink impossibile sull'oggetto: %s"
+
+#: lib/choose_repository.tcl:847
+msgid "Cannot fetch branches and objects.  See console output for details."
+msgstr ""
+"Impossibile recuperare rami e oggetti. Controllare i dettagli forniti dalla "
+"console."
+
+#: lib/choose_repository.tcl:858
+msgid "Cannot fetch tags.  See console output for details."
+msgstr ""
+"Impossibile recuperare le etichette. Controllare i dettagli forniti dalla "
+"console."
+
+#: lib/choose_repository.tcl:882
+msgid "Cannot determine HEAD.  See console output for details."
+msgstr ""
+"Impossibile determinare HEAD. Controllare i dettagli forniti dalla console."
+
+#: lib/choose_repository.tcl:891
+#, tcl-format
+msgid "Unable to cleanup %s"
+msgstr "Impossibile ripulire %s"
+
+#: lib/choose_repository.tcl:897
+msgid "Clone failed."
+msgstr "Clonazione non riuscita."
+
+#: lib/choose_repository.tcl:904
+msgid "No default branch obtained."
+msgstr "Non è stato trovato un ramo predefinito."
+
+#: lib/choose_repository.tcl:915
+#, tcl-format
+msgid "Cannot resolve %s as a commit."
+msgstr "Impossibile risolvere %s come una revisione."
+
+#: lib/choose_repository.tcl:927
+msgid "Creating working directory"
+msgstr "Creazione directory di lavoro"
+
+#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
+#: lib/index.tcl:193
+msgid "files"
+msgstr "file"
+
+#: lib/choose_repository.tcl:957
+msgid "Initial file checkout failed."
+msgstr "Attivazione iniziale non riuscita."
+
+#: lib/choose_repository.tcl:973
+msgid "Open"
+msgstr "Apri"
+
+#: lib/choose_repository.tcl:983
+msgid "Repository:"
+msgstr "Archivio:"
+
+#: lib/choose_repository.tcl:1033
+#, tcl-format
+msgid "Failed to open repository %s:"
+msgstr "Impossibile accedere all'archivio %s:"
+
+#: lib/choose_rev.tcl:53
+msgid "This Detached Checkout"
+msgstr "Questa revisione attiva staccata"
+
+#: lib/choose_rev.tcl:60
+msgid "Revision Expression:"
+msgstr "Espressione di revisione:"
+
+#: lib/choose_rev.tcl:74
+msgid "Local Branch"
+msgstr "Ramo locale"
+
+#: lib/choose_rev.tcl:79
+msgid "Tracking Branch"
+msgstr "Duplicato locale di ramo remoto"
+
+#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
+msgid "Tag"
+msgstr "Etichetta"
+
+#: lib/choose_rev.tcl:317
+#, tcl-format
+msgid "Invalid revision: %s"
+msgstr "Revisione non valida: %s"
+
+#: lib/choose_rev.tcl:338
+msgid "No revision selected."
+msgstr "Nessuna revisione selezionata."
+
+#: lib/choose_rev.tcl:346
+msgid "Revision expression is empty."
+msgstr "L'espressione di revisione è vuota."
+
+#: lib/choose_rev.tcl:530
+msgid "Updated"
+msgstr "Aggiornato"
+
+#: lib/choose_rev.tcl:558
+msgid "URL"
+msgstr "URL"
+
+#: lib/commit.tcl:9
+msgid ""
+"There is nothing to amend.\n"
+"\n"
+"You are about to create the initial commit.  There is no commit before this "
+"to amend.\n"
+msgstr ""
+"Non c'è niente da correggere.\n"
+"\n"
+"Stai per creare la revisione iniziale. Non esiste una revisione precedente "
+"da correggere.\n"
+
+#: lib/commit.tcl:18
+msgid ""
+"Cannot amend while merging.\n"
+"\n"
+"You are currently in the middle of a merge that has not been fully "
+"completed.  You cannot amend the prior commit unless you first abort the "
+"current merge activity.\n"
+msgstr ""
+"Non è possibile effettuare una correzione durante una fusione.\n"
+"\n"
+"In questo momento si sta effettuando una fusione che non è stata del tutto "
+"completata. Non puoi correggere la revisione precedente a meno che prima tu "
+"non interrompa l'operazione di fusione in corso.\n"
+
+#: lib/commit.tcl:49
+msgid "Error loading commit data for amend:"
+msgstr "Errore durante il caricamento dei dati della revisione da correggere:"
+
+#: lib/commit.tcl:76
+msgid "Unable to obtain your identity:"
+msgstr "Impossibile ottenere la tua identità:"
+
+#: lib/commit.tcl:81
+msgid "Invalid GIT_COMMITTER_IDENT:"
+msgstr "GIT_COMMITTER_IDENT non valida:"
+
+#: lib/commit.tcl:133
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before another commit can be created.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"L'ultimo stato analizzato non corrisponde allo stato dell'archivio.\n"
+"\n"
+"Un altro programma Git ha modificato questo archivio dall'ultima analisi. "
+"Bisogna effettuare una nuova analisi prima di poter creare una nuova "
+"revisione.\n"
+"\n"
+"La nuova analisi comincerà ora.\n"
+
+#: lib/commit.tcl:154
+#, tcl-format
+msgid ""
+"Unmerged files cannot be committed.\n"
+"\n"
+"File %s has merge conflicts.  You must resolve them and stage the file "
+"before committing.\n"
+msgstr ""
+"Non è possibile creare una revisione con file non sottoposti a fusione.\n"
+"\n"
+"Il file %s presenta dei conflitti. Devi risolverli e preparare il file per "
+"creare una nuova revisione prima di effettuare questa azione.\n"
+
+#: lib/commit.tcl:162
+#, tcl-format
+msgid ""
+"Unknown file state %s detected.\n"
+"\n"
+"File %s cannot be committed by this program.\n"
+msgstr ""
+"Stato di file %s sconosciuto.\n"
+"\n"
+"Questo programma non può creare una revisione contenente il file %s.\n"
+
+#: lib/commit.tcl:170
+msgid ""
+"No changes to commit.\n"
+"\n"
+"You must stage at least 1 file before you can commit.\n"
+msgstr ""
+"Nessuna modifica per la nuova revisione.\n"
+"\n"
+"Devi preparare per una nuova revisione almeno 1 file prima di effettuare "
+"questa operazione.\n"
+
+#: lib/commit.tcl:183
+msgid ""
+"Please supply a commit message.\n"
+"\n"
+"A good commit message has the following format:\n"
+"\n"
+"- First line: Describe in one sentance what you did.\n"
+"- Second line: Blank\n"
+"- Remaining lines: Describe why this change is good.\n"
+msgstr ""
+"Bisogna fornire un messaggio di revisione.\n"
+"\n"
+"Un buon messaggio di revisione ha il seguente formato:\n"
+"\n"
+"- Prima linea: descrivi in una frase ciò che hai fatto.\n"
+"- Seconda linea: vuota.\n"
+"- Terza linea: spiega a cosa serve la tua modifica.\n"
+
+#: lib/commit.tcl:257
+msgid "write-tree failed:"
+msgstr "write-tree non riuscito:"
+
+#: lib/commit.tcl:275
+#, tcl-format
+msgid "Commit %s appears to be corrupt"
+msgstr "La revisione %s sembra essere danneggiata"
+
+#: lib/commit.tcl:279
+msgid ""
+"No changes to commit.\n"
+"\n"
+"No files were modified by this commit and it was not a merge commit.\n"
+"\n"
+"A rescan will be automatically started now.\n"
+msgstr ""
+"Nessuna modifica per la nuova revisione.\n"
+"\n"
+"Questa revisione non modifica alcun file e non effettua alcuna fusione.\n"
+"\n"
+"Si procederà subito ad una nuova analisi.\n"
+
+#: lib/commit.tcl:286
+msgid "No changes to commit."
+msgstr "Nessuna modifica per la nuova revisione."
+
+#: lib/commit.tcl:303
+#, tcl-format
+msgid "warning: Tcl does not support encoding '%s'."
+msgstr "attenzione: Tcl non supporta la codifica '%s'."
+
+#: lib/commit.tcl:317
+msgid "commit-tree failed:"
+msgstr "commit-tree non riuscito:"
+
+#: lib/commit.tcl:339
+msgid "update-ref failed:"
+msgstr "update-ref non riuscito:"
+
+#: lib/commit.tcl:430
+#, tcl-format
+msgid "Created commit %s: %s"
+msgstr "Creata revisione %s: %s"
+
+#: lib/console.tcl:57
+msgid "Working... please wait..."
+msgstr "Elaborazione in corso... attendere..."
+
+#: lib/console.tcl:183
+msgid "Success"
+msgstr "Successo"
+
+#: lib/console.tcl:196
+msgid "Error: Command Failed"
+msgstr "Errore: comando non riuscito"
+
+#: lib/database.tcl:43
+msgid "Number of loose objects"
+msgstr "Numero di oggetti slegati"
+
+#: lib/database.tcl:44
+msgid "Disk space used by loose objects"
+msgstr "Spazio su disco utilizzato da oggetti slegati"
+
+#: lib/database.tcl:45
+msgid "Number of packed objects"
+msgstr "Numero di oggetti impacchettati"
+
+#: lib/database.tcl:46
+msgid "Number of packs"
+msgstr "Numero di pacchetti"
+
+#: lib/database.tcl:47
+msgid "Disk space used by packed objects"
+msgstr "Spazio su disco utilizzato da oggetti impacchettati"
+
+#: lib/database.tcl:48
+msgid "Packed objects waiting for pruning"
+msgstr "Oggetti impacchettati che attendono la potatura"
+
+#: lib/database.tcl:49
+msgid "Garbage files"
+msgstr "File inutili"
+
+#: lib/database.tcl:72
+msgid "Compressing the object database"
+msgstr "Compressione dell'archivio in corso"
+
+#: lib/database.tcl:83
+msgid "Verifying the object database with fsck-objects"
+msgstr "Verifica dell'archivio con fsck-objects in corso"
+
+#: lib/database.tcl:108
+#, tcl-format
+msgid ""
+"This repository currently has approximately %i loose objects.\n"
+"\n"
+"To maintain optimal performance it is strongly recommended that you compress "
+"the database when more than %i loose objects exist.\n"
+"\n"
+"Compress the database now?"
+msgstr ""
+"Questo archivio attualmente ha circa %i oggetti slegati.\n"
+"\n"
+"Per mantenere buone prestazioni si raccomanda di comprimere l'archivio "
+"quando sono presenti più di %i oggetti slegati.\n"
+"\n"
+"Comprimere l'archivio ora?"
+
+#: lib/date.tcl:25
+#, tcl-format
+msgid "Invalid date from Git: %s"
+msgstr "Git ha restituito una data non valida: %s"
+
+#: lib/diff.tcl:42
+#, tcl-format
+msgid ""
+"No differences detected.\n"
+"\n"
+"%s has no changes.\n"
+"\n"
+"The modification date of this file was updated by another application, but "
+"the content within the file was not changed.\n"
+"\n"
+"A rescan will be automatically started to find other files which may have "
+"the same state."
+msgstr ""
+"Non sono state trovate differenze.\n"
+"\n"
+"%s non ha modifiche.\n"
+"\n"
+"La data di modifica di questo file è stata cambiata da un'altra "
+"applicazione, ma il contenuto del file è rimasto invariato.\n"
+"\n"
+"Si procederà automaticamente ad una nuova analisi per trovare altri file che "
+"potrebbero avere lo stesso stato."
+
+#: lib/diff.tcl:81
+#, tcl-format
+msgid "Loading diff of %s..."
+msgstr "Caricamento delle differenze di %s..."
+
+#: lib/diff.tcl:114 lib/diff.tcl:184
+#, tcl-format
+msgid "Unable to display %s"
+msgstr "Impossibile visualizzare %s"
+
+#: lib/diff.tcl:115
+msgid "Error loading file:"
+msgstr "Errore nel caricamento del file:"
+
+#: lib/diff.tcl:122
+msgid "Git Repository (subproject)"
+msgstr "Archivio Git (sottoprogetto)"
+
+#: lib/diff.tcl:134
+msgid "* Binary file (not showing content)."
+msgstr "* File binario (il contenuto non sarà mostrato)."
+
+#: lib/diff.tcl:185
+msgid "Error loading diff:"
+msgstr "Errore nel caricamento delle differenze:"
+
+#: lib/diff.tcl:302
+msgid "Failed to unstage selected hunk."
+msgstr "Impossibile rimuovere la sezione scelta dalla nuova revisione."
+
+#: lib/diff.tcl:309
+msgid "Failed to stage selected hunk."
+msgstr "Impossibile preparare la sezione scelta per una nuova revisione."
+
+#: lib/error.tcl:12 lib/error.tcl:102
+msgid "error"
+msgstr "errore"
+
+#: lib/error.tcl:28
+msgid "warning"
+msgstr "attenzione"
+
+#: lib/error.tcl:81
+msgid "You must correct the above errors before committing."
+msgstr ""
+"Bisogna correggere gli errori suddetti prima di creare una nuova revisione."
+
+#: lib/index.tcl:6
+msgid "Unable to unlock the index."
+msgstr "Impossibile sbloccare l'accesso all'indice"
+
+#: lib/index.tcl:15
+msgid "Index Error"
+msgstr "Errore nell'indice"
+
+#: lib/index.tcl:21
+msgid ""
+"Updating the Git index failed.  A rescan will be automatically started to "
+"resynchronize git-gui."
+msgstr ""
+"Impossibile aggiornare l'indice. Ora sarà avviata una nuova analisi che "
+"aggiornerà git-gui."
+
+#: lib/index.tcl:27
+msgid "Continue"
+msgstr "Continua"
+
+#: lib/index.tcl:31
+msgid "Unlock Index"
+msgstr "Sblocca l'accesso all'indice"
+
+#: lib/index.tcl:282
+#, tcl-format
+msgid "Unstaging %s from commit"
+msgstr "%s non farà parte della prossima revisione"
+
+#: lib/index.tcl:326
+#, tcl-format
+msgid "Adding %s"
+msgstr "Aggiunta di %s in corso"
+
+#: lib/index.tcl:381
+#, tcl-format
+msgid "Revert changes in file %s?"
+msgstr "Annullare le modifiche nel file %s?"
+
+#: lib/index.tcl:383
+#, tcl-format
+msgid "Revert changes in these %i files?"
+msgstr "Annullare le modifiche in questi %i file?"
+
+#: lib/index.tcl:389
+msgid "Any unstaged changes will be permanently lost by the revert."
+msgstr ""
+"Tutte le modifiche non preparate per una nuova revisione saranno perse per "
+"sempre."
+
+#: lib/index.tcl:392
+msgid "Do Nothing"
+msgstr "Non fare niente"
+
+#: lib/merge.tcl:13
+msgid ""
+"Cannot merge while amending.\n"
+"\n"
+"You must finish amending this commit before starting any type of merge.\n"
+msgstr ""
+"Non posso effettuare fusioni durante una correzione.\n"
+"\n"
+"Bisogna finire di correggere questa revisione prima di iniziare una "
+"qualunque fusione.\n"
+
+#: lib/merge.tcl:27
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before a merge can be performed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"L'ultimo stato analizzato non corrisponde allo stato dell'archivio.\n"
+"\n"
+"Un altro programma Git ha modificato questo archivio dall'ultima analisi."
+"Bisogna effettuare una nuova analisi prima di poter effettuare una fusione.\n"
+"\n"
+"La nuova analisi comincerà ora.\n"
+
+#: lib/merge.tcl:44
+#, tcl-format
+msgid ""
+"You are in the middle of a conflicted merge.\n"
+"\n"
+"File %s has merge conflicts.\n"
+"\n"
+"You must resolve them, stage the file, and commit to complete the current "
+"merge.  Only then can you begin another merge.\n"
+msgstr ""
+"Sei nel mezzo di una fusione con conflitti.\n"
+"\n"
+"Il file %s ha dei conflitti.\n"
+"\n"
+"Bisogna risolvere i conflitti, preparare il file per una nuova revisione ed "
+"infine crearla per completare la fusione attuale. Solo a questo punto potrai "
+"iniziare un'altra fusione.\n"
+
+#: lib/merge.tcl:54
+#, tcl-format
+msgid ""
+"You are in the middle of a change.\n"
+"\n"
+"File %s is modified.\n"
+"\n"
+"You should complete the current commit before starting a merge.  Doing so "
+"will help you abort a failed merge, should the need arise.\n"
+msgstr ""
+"Sei nel mezzo di una modifica.\n"
+"\n"
+"Il file %s è stato modificato.\n"
+"\n"
+"Bisogna completare la creazione della revisione attuale prima di iniziare "
+"una fusione. In questo modo sarà più facile interrompere una fusione non "
+"riuscita, nel caso ce ne fosse bisogno.\n"
+
+#: lib/merge.tcl:106
+#, tcl-format
+msgid "%s of %s"
+msgstr "%s di %s"
+
+#: lib/merge.tcl:119
+#, tcl-format
+msgid "Merging %s and %s"
+msgstr "Fusione di %s e %s in corso"
+
+#: lib/merge.tcl:131
+msgid "Merge completed successfully."
+msgstr "Fusione completata con successo."
+
+#: lib/merge.tcl:133
+msgid "Merge failed.  Conflict resolution is required."
+msgstr "Fusione non riuscita. Bisogna risolvere i conflitti."
+
+#: lib/merge.tcl:158
+#, tcl-format
+msgid "Merge Into %s"
+msgstr "Fusione in %s"
+
+#: lib/merge.tcl:177
+msgid "Revision To Merge"
+msgstr "Revisione da fondere"
+
+#: lib/merge.tcl:212
+msgid ""
+"Cannot abort while amending.\n"
+"\n"
+"You must finish amending this commit.\n"
+msgstr ""
+"Interruzione impossibile durante una correzione.\n"
+"\n"
+"Bisogna finire di correggere questa revisione.\n"
+
+#: lib/merge.tcl:222
+msgid ""
+"Abort merge?\n"
+"\n"
+"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with aborting the current merge?"
+msgstr ""
+"Interrompere fusione?\n"
+"\n"
+"L'interruzione della fusione attuale causerà la perdita di *TUTTE* le "
+"modifiche non ancora presenti nell'archivio.\n"
+"\n"
+"Continuare con l'interruzione della fusione attuale?"
+
+#: lib/merge.tcl:228
+msgid ""
+"Reset changes?\n"
+"\n"
+"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with resetting the current changes?"
+msgstr ""
+"Ripristinare la revisione corrente e annullare le modifiche?\n"
+"\n"
+"L'annullamento delle modifiche causerà la perdita di *TUTTE* le modifiche "
+"non ancora presenti nell'archivio.\n"
+"\n"
+"Continuare con l'annullamento delle modifiche attuali?"
+
+#: lib/merge.tcl:239
+msgid "Aborting"
+msgstr "Interruzione in corso"
+
+#: lib/merge.tcl:266
+msgid "Abort failed."
+msgstr "Interruzione non riuscita."
+
+#: lib/merge.tcl:268
+msgid "Abort completed.  Ready."
+msgstr "Interruzione completata. Pronto."
+
+#: lib/option.tcl:82
+msgid "Restore Defaults"
+msgstr "Ripristina valori predefiniti"
+
+#: lib/option.tcl:86
+msgid "Save"
+msgstr "Salva"
+
+#: lib/option.tcl:96
+#, tcl-format
+msgid "%s Repository"
+msgstr "Archivio di %s"
+
+#: lib/option.tcl:97
+msgid "Global (All Repositories)"
+msgstr "Tutti gli archivi"
+
+#: lib/option.tcl:103
+msgid "User Name"
+msgstr "Nome utente"
+
+#: lib/option.tcl:104
+msgid "Email Address"
+msgstr "Indirizzo Email"
+
+#: lib/option.tcl:106
+msgid "Summarize Merge Commits"
+msgstr "Riepilogo nelle revisioni di fusione"
+
+#: lib/option.tcl:107
+msgid "Merge Verbosity"
+msgstr "Prolissità della fusione"
+
+#: lib/option.tcl:108
+msgid "Show Diffstat After Merge"
+msgstr "Mostra statistiche delle differenze dopo la fusione"
+
+#: lib/option.tcl:110
+msgid "Trust File Modification Timestamps"
+msgstr "Fidati delle date di modifica dei file"
+
+#: lib/option.tcl:111
+msgid "Prune Tracking Branches During Fetch"
+msgstr ""
+"Effettua potatura dei duplicati locali di rami remoti durante il recupero"
+
+#: lib/option.tcl:112
+msgid "Match Tracking Branches"
+msgstr "Appaia duplicati locali di rami remoti"
+
+#: lib/option.tcl:113
+msgid "Number of Diff Context Lines"
+msgstr "Numero di linee di contesto nelle differenze"
+
+#: lib/option.tcl:114
+msgid "New Branch Name Template"
+msgstr "Modello per il nome di un nuovo ramo"
+
+#: lib/option.tcl:176
+msgid "Change Font"
+msgstr "Cambia caratteri"
+
+#: lib/option.tcl:180
+#, tcl-format
+msgid "Choose %s"
+msgstr "Scegli %s"
+
+#: lib/option.tcl:186
+msgid "pt."
+msgstr "pt."
+
+#: lib/option.tcl:200
+msgid "Preferences"
+msgstr "Preferenze"
+
+#: lib/option.tcl:235
+msgid "Failed to completely save options:"
+msgstr "Impossibile salvare completamente le opzioni:"
+
+#: lib/remote.tcl:165
+msgid "Prune from"
+msgstr "Effettua potatura da"
+
+#: lib/remote.tcl:170
+msgid "Fetch from"
+msgstr "Recupera da"
+
+#: lib/remote.tcl:213
+msgid "Push to"
+msgstr "Propaga verso"
+
+#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
+msgid "Delete Remote Branch"
+msgstr "Cancella ramo remoto"
+
+#: lib/remote_branch_delete.tcl:47
+msgid "From Repository"
+msgstr "Da archivio"
+
+#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
+msgid "Remote:"
+msgstr "Remoto:"
+
+#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
+msgid "Arbitrary URL:"
+msgstr "URL specifico:"
+
+#: lib/remote_branch_delete.tcl:84
+msgid "Branches"
+msgstr "Rami"
+
+#: lib/remote_branch_delete.tcl:109
+msgid "Delete Only If"
+msgstr "Elimina solo se"
+
+#: lib/remote_branch_delete.tcl:111
+msgid "Merged Into:"
+msgstr "Fuso in:"
+
+#: lib/remote_branch_delete.tcl:119
+msgid "Always (Do not perform merge checks)"
+msgstr "Sempre (non verificare le fusioni)"
+
+#: lib/remote_branch_delete.tcl:152
+msgid "A branch is required for 'Merged Into'."
+msgstr "Si richiede un ramo per 'Fuso in'."
+
+#: lib/remote_branch_delete.tcl:184
+#, tcl-format
+msgid ""
+"The following branches are not completely merged into %s:\n"
+"\n"
+" - %s"
+msgstr ""
+"I rami seguenti non sono stati fusi completamente in %s:\n"
+"\n"
+" - %s"
+
+#: lib/remote_branch_delete.tcl:189
+#, tcl-format
+msgid ""
+"One or more of the merge tests failed because you have not fetched the "
+"necessary commits.  Try fetching from %s first."
+msgstr ""
+"Impossibile verificare una o più fusioni: mancano le revisioni necessarie. "
+"Prova prima a recuperarle da %s."
+
+#: lib/remote_branch_delete.tcl:207
+msgid "Please select one or more branches to delete."
+msgstr "Scegliere uno o più rami da cancellare."
+
+#: lib/remote_branch_delete.tcl:216
+msgid ""
+"Recovering deleted branches is difficult.\n"
+"\n"
+"Delete the selected branches?"
+msgstr ""
+"Ricomporre rami cancellati è difficile.\n"
+"\n"
+"Cancellare i rami selezionati?"
+
+#: lib/remote_branch_delete.tcl:226
+#, tcl-format
+msgid "Deleting branches from %s"
+msgstr "Cancellazione rami da %s"
+
+#: lib/remote_branch_delete.tcl:286
+msgid "No repository selected."
+msgstr "Nessun archivio selezionato."
+
+#: lib/remote_branch_delete.tcl:291
+#, tcl-format
+msgid "Scanning %s..."
+msgstr "Analisi in corso %s..."
+
+#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
+msgid "Cannot write shortcut:"
+msgstr "Impossibile scrivere shortcut:"
+
+#: lib/shortcut.tcl:136
+msgid "Cannot write icon:"
+msgstr "Impossibile scrivere icona:"
+
+#: lib/status_bar.tcl:83
+#, tcl-format
+msgid "%s ... %*i of %*i %s (%3i%%)"
+msgstr "%1$s ... %6$s: %2$*i di %4$*i (%7$3i%%)"
+
+#: lib/transport.tcl:6
+#, tcl-format
+msgid "fetch %s"
+msgstr "recupera da %s"
+
+#: lib/transport.tcl:7
+#, tcl-format
+msgid "Fetching new changes from %s"
+msgstr "Recupero nuove modifiche da %s"
+
+#: lib/transport.tcl:18
+#, tcl-format
+msgid "remote prune %s"
+msgstr "potatura remota di %s"
+
+#: lib/transport.tcl:19
+#, tcl-format
+msgid "Pruning tracking branches deleted from %s"
+msgstr "Effettua potatura dei duplicati locali di rami remoti cancellati da %s"
+
+#: lib/transport.tcl:25 lib/transport.tcl:71
+#, tcl-format
+msgid "push %s"
+msgstr "propaga verso %s"
+
+#: lib/transport.tcl:26
+#, tcl-format
+msgid "Pushing changes to %s"
+msgstr "Propagazione modifiche a %s"
+
+#: lib/transport.tcl:72
+#, tcl-format
+msgid "Pushing %s %s to %s"
+msgstr "Propagazione %s %s a %s"
+
+#: lib/transport.tcl:89
+msgid "Push Branches"
+msgstr "Propaga rami"
+
+#: lib/transport.tcl:103
+msgid "Source Branches"
+msgstr "Rami di origine"
+
+#: lib/transport.tcl:120
+msgid "Destination Repository"
+msgstr "Archivio di destinazione"
+
+#: lib/transport.tcl:158
+msgid "Transfer Options"
+msgstr "Opzioni di trasferimento"
+
+#: lib/transport.tcl:160
+msgid "Force overwrite existing branch (may discard changes)"
+msgstr "Sovrascrivi ramo esistente (alcune modifiche potrebbero essere perse)"
+
+#: lib/transport.tcl:164
+msgid "Use thin pack (for slow network connections)"
+msgstr "Utilizza 'thin pack' (per connessioni lente)"
+
+#: lib/transport.tcl:168
+msgid "Include tags"
+msgstr "Includi etichette"
diff --git a/git-gui/po/ja.po b/git-gui/po/ja.po
new file mode 100644 (file)
index 0000000..e2cf5bd
--- /dev/null
@@ -0,0 +1,1883 @@
+# Translation of git-gui to Japanese
+# Copyright (C) 2007 Shawn Pearce
+# This file is distributed under the same license as the git-gui package.
+# しらいし ななこ <nanako3@bluebottle.com>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: git-gui\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-11-24 10:36+0100\n"
+"PO-Revision-Date: 2007-12-05 06:12+0900\n"
+"Last-Translator: しらいし ななこ <nanako3@bluebottle.com>\n"
+"Language-Team: Japanese\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
+#: git-gui.sh:733
+msgid "git-gui: fatal error"
+msgstr "git-gui: 致命的なエラー"
+
+#: git-gui.sh:565
+#, tcl-format
+msgid "Invalid font specified in %s:"
+msgstr "%s に無効なフォントが指定されています:"
+
+#: git-gui.sh:590
+msgid "Main Font"
+msgstr "主フォント"
+
+#: git-gui.sh:591
+msgid "Diff/Console Font"
+msgstr "diff/コンソール・フォント"
+
+#: git-gui.sh:605
+msgid "Cannot find git in PATH."
+msgstr "PATH 中に git が見つかりません"
+
+#: git-gui.sh:632
+msgid "Cannot parse Git version string:"
+msgstr "Git バージョン名が理解できません:"
+
+#: git-gui.sh:650
+#, tcl-format
+msgid ""
+"Git version cannot be determined.\n"
+"\n"
+"%s claims it is version '%s'.\n"
+"\n"
+"%s requires at least Git 1.5.0 or later.\n"
+"\n"
+"Assume '%s' is version 1.5.0?\n"
+msgstr ""
+"Git のバージョンが確認できません。\n"
+"\n"
+"%s はバージョン '%s' とのことです。\n"
+"\n"
+"%s は最低でも 1.5.0 かそれ以降の Git が必要です\n"
+"\n"
+"'%s' はバージョン 1.5.0 と思って良いですか?\n"
+
+#: git-gui.sh:888
+msgid "Git directory not found:"
+msgstr "Git ディレクトリが見つかりません:"
+
+#: git-gui.sh:895
+msgid "Cannot move to top of working directory:"
+msgstr "作業ディレクトリの最上位に移動できません"
+
+#: git-gui.sh:902
+msgid "Cannot use funny .git directory:"
+msgstr "変な .git ディレクトリは使えません"
+
+#: git-gui.sh:907
+msgid "No working directory"
+msgstr "作業ディレクトリがありません"
+
+#: git-gui.sh:1054
+msgid "Refreshing file status..."
+msgstr "ファイル状態を更新しています…"
+
+#: git-gui.sh:1119
+msgid "Scanning for modified files ..."
+msgstr "変更されたファイルをスキャンしています…"
+
+#: git-gui.sh:1294 lib/browser.tcl:245
+msgid "Ready."
+msgstr "準備完了"
+
+#: git-gui.sh:1560
+msgid "Unmodified"
+msgstr "変更無し"
+
+#: git-gui.sh:1562
+msgid "Modified, not staged"
+msgstr "変更あり、コミット未予定"
+
+#: git-gui.sh:1563 git-gui.sh:1568
+msgid "Staged for commit"
+msgstr "コミット予定済"
+
+#: git-gui.sh:1564 git-gui.sh:1569
+msgid "Portions staged for commit"
+msgstr "部分的にコミット予定済"
+
+#: git-gui.sh:1565 git-gui.sh:1570
+msgid "Staged for commit, missing"
+msgstr "コミット予定済、ファイル無し"
+
+#: git-gui.sh:1567
+msgid "Untracked, not staged"
+msgstr "管理外、コミット未予定"
+
+#: git-gui.sh:1572
+msgid "Missing"
+msgstr "ファイル無し"
+
+#: git-gui.sh:1573
+msgid "Staged for removal"
+msgstr "削除予定済"
+
+#: git-gui.sh:1574
+msgid "Staged for removal, still present"
+msgstr "削除予定済、ファイル未削除"
+
+#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
+msgid "Requires merge resolution"
+msgstr "要マージ解決"
+
+#: git-gui.sh:1614
+msgid "Starting gitk... please wait..."
+msgstr "gitk を起動中…お待ち下さい…"
+
+#: git-gui.sh:1623
+#, tcl-format
+msgid ""
+"Unable to start gitk:\n"
+"\n"
+"%s does not exist"
+msgstr ""
+"gitk を起動できません:\n"
+"\n"
+"%s がありません"
+
+#: git-gui.sh:1823 lib/choose_repository.tcl:35
+msgid "Repository"
+msgstr "リポジトリ"
+
+#: git-gui.sh:1824
+msgid "Edit"
+msgstr "編集"
+
+#: git-gui.sh:1826 lib/choose_rev.tcl:560
+msgid "Branch"
+msgstr "ブランチ"
+
+#: git-gui.sh:1829 lib/choose_rev.tcl:547
+msgid "Commit@@noun"
+msgstr "コミット"
+
+#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
+msgid "Merge"
+msgstr "マージ"
+
+#: git-gui.sh:1833 lib/choose_rev.tcl:556
+msgid "Remote"
+msgstr "リモート"
+
+#: git-gui.sh:1842
+msgid "Browse Current Branch's Files"
+msgstr "現在のブランチのファイルを見る"
+
+#: git-gui.sh:1846
+msgid "Browse Branch Files..."
+msgstr "ブランチのファイルを見る…"
+
+#: git-gui.sh:1851
+msgid "Visualize Current Branch's History"
+msgstr "現在のブランチの履歴を見る"
+
+#: git-gui.sh:1855
+msgid "Visualize All Branch History"
+msgstr "全てのブランチの履歴を見る"
+
+#: git-gui.sh:1862
+#, tcl-format
+msgid "Browse %s's Files"
+msgstr "ブランチ %s のファイルを見る"
+
+#: git-gui.sh:1864
+#, tcl-format
+msgid "Visualize %s's History"
+msgstr "ブランチ %s の履歴を見る"
+
+#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
+msgid "Database Statistics"
+msgstr "データベース統計"
+
+#: git-gui.sh:1872 lib/database.tcl:34
+msgid "Compress Database"
+msgstr "データベース圧縮"
+
+#: git-gui.sh:1875
+msgid "Verify Database"
+msgstr "データベース検証"
+
+#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
+#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
+msgid "Create Desktop Icon"
+msgstr "デスクトップ・アイコンを作る"
+
+#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
+msgid "Quit"
+msgstr "終了"
+
+#: git-gui.sh:1902
+msgid "Undo"
+msgstr "元に戻す"
+
+#: git-gui.sh:1905
+msgid "Redo"
+msgstr "やり直し"
+
+#: git-gui.sh:1909 git-gui.sh:2403
+msgid "Cut"
+msgstr "切り取り"
+
+#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
+#: lib/console.tcl:67
+msgid "Copy"
+msgstr "コピー"
+
+#: git-gui.sh:1915 git-gui.sh:2409
+msgid "Paste"
+msgstr "貼り付け"
+
+#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
+#: lib/remote_branch_delete.tcl:38
+msgid "Delete"
+msgstr "削除"
+
+#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
+msgid "Select All"
+msgstr "全て選択"
+
+#: git-gui.sh:1931
+msgid "Create..."
+msgstr "作成…"
+
+#: git-gui.sh:1937
+msgid "Checkout..."
+msgstr "チェックアウト"
+
+#: git-gui.sh:1943
+msgid "Rename..."
+msgstr "名前変更…"
+
+#: git-gui.sh:1948 git-gui.sh:2048
+msgid "Delete..."
+msgstr "削除…"
+
+#: git-gui.sh:1953
+msgid "Reset..."
+msgstr "リセット…"
+
+#: git-gui.sh:1965 git-gui.sh:2350
+msgid "New Commit"
+msgstr "新規コミット"
+
+#: git-gui.sh:1973 git-gui.sh:2357
+msgid "Amend Last Commit"
+msgstr "最新コミットを訂正"
+
+#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
+msgid "Rescan"
+msgstr "再スキャン"
+
+#: git-gui.sh:1988
+msgid "Stage To Commit"
+msgstr "コミット予定する"
+
+#: git-gui.sh:1994
+msgid "Stage Changed Files To Commit"
+msgstr "変更されたファイルをコミット予定"
+
+#: git-gui.sh:2000
+msgid "Unstage From Commit"
+msgstr "コミットから降ろす"
+
+#: git-gui.sh:2005 lib/index.tcl:393
+msgid "Revert Changes"
+msgstr "変更を元に戻す"
+
+#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
+msgid "Sign Off"
+msgstr "署名"
+
+#: git-gui.sh:2016 git-gui.sh:2333
+msgid "Commit@@verb"
+msgstr "コミット"
+
+#: git-gui.sh:2027
+msgid "Local Merge..."
+msgstr "ローカル・マージ…"
+
+#: git-gui.sh:2032
+msgid "Abort Merge..."
+msgstr "マージ中止…"
+
+#: git-gui.sh:2044
+msgid "Push..."
+msgstr "プッシュ…"
+
+#: git-gui.sh:2055 lib/choose_repository.tcl:40
+msgid "Apple"
+msgstr "りんご"
+
+#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
+#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
+#, tcl-format
+msgid "About %s"
+msgstr "%s について"
+
+#: git-gui.sh:2062
+msgid "Preferences..."
+msgstr "設定…"
+
+#: git-gui.sh:2070 git-gui.sh:2595
+msgid "Options..."
+msgstr "オプション…"
+
+#: git-gui.sh:2076 lib/choose_repository.tcl:46
+msgid "Help"
+msgstr "ヘルプ"
+
+#: git-gui.sh:2117
+msgid "Online Documentation"
+msgstr "オンライン・ドキュメント"
+
+#: git-gui.sh:2201
+#, tcl-format
+msgid "fatal: cannot stat path %s: No such file or directory"
+msgstr ""
+"致命的: パス %s が stat できません。そのようなファイルやディレクトリはありま"
+"せん"
+
+#: git-gui.sh:2234
+msgid "Current Branch:"
+msgstr "現在のブランチ"
+
+#: git-gui.sh:2255
+msgid "Staged Changes (Will Commit)"
+msgstr "ステージングされた(コミット予定済の)変更"
+
+#: git-gui.sh:2274
+msgid "Unstaged Changes"
+msgstr "コミット予定に入っていない変更"
+
+#: git-gui.sh:2323
+msgid "Stage Changed"
+msgstr "変更をコミット予定に入れる"
+
+#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
+msgid "Push"
+msgstr "プッシュ"
+
+#: git-gui.sh:2369
+msgid "Initial Commit Message:"
+msgstr "最初のコミットメッセージ:"
+
+#: git-gui.sh:2370
+msgid "Amended Commit Message:"
+msgstr "訂正したコミットメッセージ:"
+
+#: git-gui.sh:2371
+msgid "Amended Initial Commit Message:"
+msgstr "訂正した最初のコミットメッセージ:"
+
+#: git-gui.sh:2372
+msgid "Amended Merge Commit Message:"
+msgstr "訂正したマージコミットメッセージ:"
+
+#: git-gui.sh:2373
+msgid "Merge Commit Message:"
+msgstr "マージコミットメッセージ:"
+
+#: git-gui.sh:2374
+msgid "Commit Message:"
+msgstr "コミットメッセージ:"
+
+#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
+msgid "Copy All"
+msgstr "全てコピー"
+
+#: git-gui.sh:2443 lib/blame.tcl:104
+msgid "File:"
+msgstr "ファイル:"
+
+#: git-gui.sh:2545
+msgid "Refresh"
+msgstr "再読み込み"
+
+#: git-gui.sh:2566
+msgid "Apply/Reverse Hunk"
+msgstr "パッチを適用/取り消す"
+
+#: git-gui.sh:2572
+msgid "Decrease Font Size"
+msgstr "フォントを小さく"
+
+#: git-gui.sh:2576
+msgid "Increase Font Size"
+msgstr "フォントを大きく"
+
+#: git-gui.sh:2581
+msgid "Show Less Context"
+msgstr "文脈を少なく"
+
+#: git-gui.sh:2588
+msgid "Show More Context"
+msgstr "文脈を多く"
+
+#: git-gui.sh:2602
+msgid "Unstage Hunk From Commit"
+msgstr "パッチをコミット予定から外す"
+
+#: git-gui.sh:2604
+msgid "Stage Hunk For Commit"
+msgstr "パッチをコミット予定に加える"
+
+#: git-gui.sh:2623
+msgid "Initializing..."
+msgstr "初期化しています…"
+
+#: git-gui.sh:2718
+#, tcl-format
+msgid ""
+"Possible environment issues exist.\n"
+"\n"
+"The following environment variables are probably\n"
+"going to be ignored by any Git subprocess run\n"
+"by %s:\n"
+"\n"
+msgstr ""
+"環境に問題がある可能性があります\n"
+"\n"
+"以下の環境変数は %s が起動する Git サブプロセスによって無視されるでしょう:\n"
+"\n"
+
+#: git-gui.sh:2748
+msgid ""
+"\n"
+"This is due to a known issue with the\n"
+"Tcl binary distributed by Cygwin."
+msgstr ""
+"\n"
+"これは Cygwin で配布されている Tcl バイナリに\n"
+"関しての既知の問題によります"
+
+#: git-gui.sh:2753
+#, tcl-format
+msgid ""
+"\n"
+"\n"
+"A good replacement for %s\n"
+"is placing values for the user.name and\n"
+"user.email settings into your personal\n"
+"~/.gitconfig file.\n"
+msgstr ""
+"\n"
+"\n"
+"個人的な ~/.gitconfig ファイル内で user.name と user.email の値を設定\n"
+"するのが、%s の良い代用となります\n"
+
+#: lib/about.tcl:25
+msgid "git-gui - a graphical user interface for Git."
+msgstr "Git のグラフィカルUI git-gui"
+
+#: lib/blame.tcl:77
+msgid "File Viewer"
+msgstr "ファイルピューワ"
+
+#: lib/blame.tcl:81
+msgid "Commit:"
+msgstr "コミット:"
+
+#: lib/blame.tcl:249
+msgid "Copy Commit"
+msgstr "コミットをコピー"
+
+#: lib/blame.tcl:369
+#, tcl-format
+msgid "Reading %s..."
+msgstr "%s を読んでいます…"
+
+#: lib/blame.tcl:473
+msgid "Loading copy/move tracking annotations..."
+msgstr "コピー・移動追跡データを読んでいます…"
+
+#: lib/blame.tcl:493
+msgid "lines annotated"
+msgstr "行を注釈しました"
+
+#: lib/blame.tcl:674
+msgid "Loading original location annotations..."
+msgstr "元位置行の注釈データを読んでいます…"
+
+#: lib/blame.tcl:677
+msgid "Annotation complete."
+msgstr "注釈完了しました"
+
+#: lib/blame.tcl:731
+msgid "Loading annotation..."
+msgstr "注釈を読み込んでいます…"
+
+#: lib/blame.tcl:787
+msgid "Author:"
+msgstr "作者:"
+
+#: lib/blame.tcl:791
+msgid "Committer:"
+msgstr "コミット者:"
+
+#: lib/blame.tcl:796
+msgid "Original File:"
+msgstr "元ファイル"
+
+#: lib/blame.tcl:910
+msgid "Originally By:"
+msgstr "原作者:"
+
+#: lib/blame.tcl:916
+msgid "In File:"
+msgstr "ファイル:"
+
+#: lib/blame.tcl:921
+msgid "Copied Or Moved Here By:"
+msgstr "複写・移動者:"
+
+#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
+msgid "Checkout Branch"
+msgstr "ブランチをチェックアウト"
+
+#: lib/branch_checkout.tcl:23
+msgid "Checkout"
+msgstr "チェックアウト"
+
+#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
+#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
+#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
+#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
+msgid "Cancel"
+msgstr "中止"
+
+#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
+msgid "Revision"
+msgstr "リビジョン"
+
+#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
+msgid "Options"
+msgstr "オプション"
+
+#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
+msgid "Fetch Tracking Branch"
+msgstr "トラッキング・ブランチをフェッチ"
+
+#: lib/branch_checkout.tcl:44
+msgid "Detach From Local Branch"
+msgstr "ローカル・ブランチから削除"
+
+#: lib/branch_create.tcl:22
+msgid "Create Branch"
+msgstr "ブランチを作成"
+
+#: lib/branch_create.tcl:27
+msgid "Create New Branch"
+msgstr "ブランチを新規作成"
+
+#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
+msgid "Create"
+msgstr "作成"
+
+#: lib/branch_create.tcl:40
+msgid "Branch Name"
+msgstr "ブランチ名"
+
+#: lib/branch_create.tcl:43
+msgid "Name:"
+msgstr "名前:"
+
+#: lib/branch_create.tcl:58
+msgid "Match Tracking Branch Name"
+msgstr "トラッキング・ブランチ名を合わせる"
+
+#: lib/branch_create.tcl:66
+msgid "Starting Revision"
+msgstr "初期リビジョン"
+
+#: lib/branch_create.tcl:72
+msgid "Update Existing Branch:"
+msgstr "既存のブランチを更新:"
+
+#: lib/branch_create.tcl:75
+msgid "No"
+msgstr "いいえ"
+
+#: lib/branch_create.tcl:80
+msgid "Fast Forward Only"
+msgstr "早送りのみ"
+
+#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
+msgid "Reset"
+msgstr "リセット"
+
+#: lib/branch_create.tcl:97
+msgid "Checkout After Creation"
+msgstr "作成してすぐチェックアウト"
+
+#: lib/branch_create.tcl:131
+msgid "Please select a tracking branch."
+msgstr "トラッキング・ブランチを選択して下さい。"
+
+#: lib/branch_create.tcl:140
+#, tcl-format
+msgid "Tracking branch %s is not a branch in the remote repository."
+msgstr "トラッキング・ブランチ %s は遠隔リポジトリのブランチではありません。"
+
+#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
+msgid "Please supply a branch name."
+msgstr "ブランチ名を指定して下さい。"
+
+#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
+#, tcl-format
+msgid "'%s' is not an acceptable branch name."
+msgstr "'%s' はブランチ名に使えません。"
+
+#: lib/branch_delete.tcl:15
+msgid "Delete Branch"
+msgstr "ブランチ削除"
+
+#: lib/branch_delete.tcl:20
+msgid "Delete Local Branch"
+msgstr "ローカル・ブランチを削除"
+
+#: lib/branch_delete.tcl:37
+msgid "Local Branches"
+msgstr "ローカル・ブランチ"
+
+#: lib/branch_delete.tcl:52
+msgid "Delete Only If Merged Into"
+msgstr "マージ済みの時のみ削除"
+
+#: lib/branch_delete.tcl:54
+msgid "Always (Do not perform merge test.)"
+msgstr "無条件(マージテストしない)"
+
+#: lib/branch_delete.tcl:103
+#, tcl-format
+msgid "The following branches are not completely merged into %s:"
+msgstr "以下のブランチは %s に完全にマージされていません:"
+
+#: lib/branch_delete.tcl:115
+msgid ""
+"Recovering deleted branches is difficult. \n"
+"\n"
+" Delete the selected branches?"
+msgstr ""
+"ブランチを削除すると元に戻すのは困難です。 \n"
+"\n"
+" 選択したブランチを削除しますか?"
+
+#: lib/branch_delete.tcl:141
+#, tcl-format
+msgid ""
+"Failed to delete branches:\n"
+"%s"
+msgstr ""
+"以下のブランチを削除できません:\n"
+"%s"
+
+#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
+msgid "Rename Branch"
+msgstr "ブランチの名前変更"
+
+#: lib/branch_rename.tcl:26
+msgid "Rename"
+msgstr "名前変更"
+
+#: lib/branch_rename.tcl:36
+msgid "Branch:"
+msgstr "ブランチ:"
+
+#: lib/branch_rename.tcl:39
+msgid "New Name:"
+msgstr "新しい名前:"
+
+#: lib/branch_rename.tcl:75
+msgid "Please select a branch to rename."
+msgstr "名前を変更するブランチを選んで下さい。"
+
+#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
+#, tcl-format
+msgid "Branch '%s' already exists."
+msgstr "'%s'というブランチは既に存在します。"
+
+#: lib/branch_rename.tcl:117
+#, tcl-format
+msgid "Failed to rename '%s'."
+msgstr "'%s'の名前変更に失敗しました。"
+
+#: lib/browser.tcl:17
+msgid "Starting..."
+msgstr "起動中…"
+
+#: lib/browser.tcl:26
+msgid "File Browser"
+msgstr "ファイル・ブラウザ"
+
+#: lib/browser.tcl:125 lib/browser.tcl:142
+#, tcl-format
+msgid "Loading %s..."
+msgstr "%s をロード中…"
+
+#: lib/browser.tcl:186
+msgid "[Up To Parent]"
+msgstr "[上位フォルダへ]"
+
+#: lib/browser.tcl:266 lib/browser.tcl:272
+msgid "Browse Branch Files"
+msgstr "現在のブランチのファイルを見る"
+
+#: lib/browser.tcl:277 lib/choose_repository.tcl:391
+#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
+#: lib/choose_repository.tcl:989
+msgid "Browse"
+msgstr "ブラウズ"
+
+#: lib/checkout_op.tcl:79
+#, tcl-format
+msgid "Fetching %s from %s"
+msgstr "%s から %s をフェッチしています"
+
+#: lib/checkout_op.tcl:127
+#, tcl-format
+msgid "fatal: Cannot resolve %s"
+msgstr "致命的エラー: %s を解決できません"
+
+#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
+msgid "Close"
+msgstr "閉じる"
+
+#: lib/checkout_op.tcl:169
+#, tcl-format
+msgid "Branch '%s' does not exist."
+msgstr "ブランチ'%s'は存在しません。"
+
+#: lib/checkout_op.tcl:206
+#, tcl-format
+msgid ""
+"Branch '%s' already exists.\n"
+"\n"
+"It cannot fast-forward to %s.\n"
+"A merge is required."
+msgstr ""
+"ブランチ '%s' は既に存在します。\n"
+"\n"
+"%s に早送りできません。\n"
+"マージが必要です。"
+
+#: lib/checkout_op.tcl:220
+#, tcl-format
+msgid "Merge strategy '%s' not supported."
+msgstr "'%s' マージ戦略はサポートされていません。"
+
+#: lib/checkout_op.tcl:239
+#, tcl-format
+msgid "Failed to update '%s'."
+msgstr "'%s' の更新に失敗しました。"
+
+#: lib/checkout_op.tcl:251
+msgid "Staging area (index) is already locked."
+msgstr "インデックスは既にロックされています。"
+
+#: lib/checkout_op.tcl:266
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before the current branch can be changed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"最後にスキャンした状態はリポジトリの状態と合致しません。\n"
+"\n"
+"最後にスキャンして以後、別の Git プログラムがリポジトリを変更しています。現在"
+"のブランチを変更する前に、再スキャンが必要です。\n"
+"\n"
+"自動的に再スキャンを開始します。\n"
+
+#: lib/checkout_op.tcl:322
+#, tcl-format
+msgid "Updating working directory to '%s'..."
+msgstr "作業ディレクトリを '%s' に更新しています…"
+
+#: lib/checkout_op.tcl:353
+#, tcl-format
+msgid "Aborted checkout of '%s' (file level merging is required)."
+msgstr "'%s' のチェックアウトを中止しました(ファイル毎のマージが必要です)。"
+
+#: lib/checkout_op.tcl:354
+msgid "File level merge required."
+msgstr "ファイル毎のマージが必要です。"
+
+#: lib/checkout_op.tcl:358
+#, tcl-format
+msgid "Staying on branch '%s'."
+msgstr "ブランチ '%s' に滞まります。"
+
+#: lib/checkout_op.tcl:429
+msgid ""
+"You are no longer on a local branch.\n"
+"\n"
+"If you wanted to be on a branch, create one now starting from 'This Detached "
+"Checkout'."
+msgstr ""
+"ローカル・ブランチから離れます。\n"
+"\n"
+"ブランチ上に滞まりたいときは、この「分離されたチェックアウト」から新規ブラン"
+"チを開始してください。"
+
+#: lib/checkout_op.tcl:446
+#, tcl-format
+msgid "Checked out '%s'."
+msgstr "'%s' をチェックアウトしました"
+
+#: lib/checkout_op.tcl:478
+#, tcl-format
+msgid "Resetting '%s' to '%s' will lose the following commits:"
+msgstr "'%s' を '%s' にリセットすると、以下のコミットが失なわれます:"
+
+#: lib/checkout_op.tcl:500
+msgid "Recovering lost commits may not be easy."
+msgstr "失なわれたコミットを回復するのは簡単ではありません。"
+
+#: lib/checkout_op.tcl:505
+#, tcl-format
+msgid "Reset '%s'?"
+msgstr "'%s' をリセットしますか?"
+
+#: lib/checkout_op.tcl:510 lib/merge.tcl:164
+msgid "Visualize"
+msgstr "可視化"
+
+#: lib/checkout_op.tcl:578
+#, tcl-format
+msgid ""
+"Failed to set current branch.\n"
+"\n"
+"This working directory is only partially switched.  We successfully updated "
+"your files, but failed to update an internal Git file.\n"
+"\n"
+"This should not have occurred.  %s will now close and give up."
+msgstr ""
+"現在のブランチを設定できません。\n"
+"\n"
+"作業ディレクトリは部分的にしか切り替わっていません。ファイルの更新には成功し"
+"ましたが、 Git の内部データを更新できませんでした。\n"
+"起こるはずのないエラーです。あきらめて %s を終了します。"
+
+#: lib/choose_font.tcl:39
+msgid "Select"
+msgstr "選択"
+
+#: lib/choose_font.tcl:53
+msgid "Font Family"
+msgstr "フォント・ファミリー"
+
+#: lib/choose_font.tcl:73
+msgid "Font Size"
+msgstr "フォントの大きさ"
+
+#: lib/choose_font.tcl:90
+msgid "Font Example"
+msgstr "フォント・サンプル"
+
+#: lib/choose_font.tcl:101
+msgid ""
+"This is example text.\n"
+"If you like this text, it can be your font."
+msgstr ""
+"これはサンプル文です。\n"
+"このフォントが気に入ればお使いになれます。"
+
+#: lib/choose_repository.tcl:27
+msgid "Git Gui"
+msgstr "Git GUI"
+
+#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
+msgid "Create New Repository"
+msgstr "新しいリポジトリを作る"
+
+#: lib/choose_repository.tcl:86
+msgid "New..."
+msgstr "新規…"
+
+#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
+msgid "Clone Existing Repository"
+msgstr "既存リポジトリを複製する"
+
+#: lib/choose_repository.tcl:99
+msgid "Clone..."
+msgstr "複製…"
+
+#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
+msgid "Open Existing Repository"
+msgstr "既存リポジトリを開く"
+
+#: lib/choose_repository.tcl:112
+msgid "Open..."
+msgstr "開く…"
+
+#: lib/choose_repository.tcl:125
+msgid "Recent Repositories"
+msgstr "最近使ったリポジトリ"
+
+#: lib/choose_repository.tcl:131
+msgid "Open Recent Repository:"
+msgstr "最近使ったリポジトリを開く"
+
+#: lib/choose_repository.tcl:294
+#, tcl-format
+msgid "Location %s already exists."
+msgstr "'%s' は既に存在します。"
+
+#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
+#: lib/choose_repository.tcl:314
+#, tcl-format
+msgid "Failed to create repository %s:"
+msgstr "リポジトリ %s を作製できません:"
+
+#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
+msgid "Directory:"
+msgstr "ディレクトリ:"
+
+#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
+#: lib/choose_repository.tcl:1013
+msgid "Git Repository"
+msgstr "GIT リポジトリ"
+
+#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
+#, tcl-format
+msgid "Directory %s already exists."
+msgstr "ディレクトリ '%s' は既に存在します。"
+
+#: lib/choose_repository.tcl:442
+#, tcl-format
+msgid "File %s already exists."
+msgstr "ファイル '%s' は既に存在します。"
+
+#: lib/choose_repository.tcl:463
+msgid "Clone"
+msgstr "複製"
+
+#: lib/choose_repository.tcl:476
+msgid "URL:"
+msgstr "URL:"
+
+#: lib/choose_repository.tcl:496
+msgid "Clone Type:"
+msgstr "複製方式:"
+
+#: lib/choose_repository.tcl:502
+msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
+msgstr "標準(高速・中冗長度・ハードリンク)"
+
+#: lib/choose_repository.tcl:508
+msgid "Full Copy (Slower, Redundant Backup)"
+msgstr "全複写(低速・冗長バックアップ)"
+
+#: lib/choose_repository.tcl:514
+msgid "Shared (Fastest, Not Recommended, No Backup)"
+msgstr "共有(最高速・非推奨・バックアップ無し)"
+
+#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
+#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
+#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
+#, tcl-format
+msgid "Not a Git repository: %s"
+msgstr "Git リポジトリではありません: %s"
+
+#: lib/choose_repository.tcl:586
+msgid "Standard only available for local repository."
+msgstr "標準方式は同一計算機上のリポジトリにのみ使えます。"
+
+#: lib/choose_repository.tcl:590
+msgid "Shared only available for local repository."
+msgstr "共有方式は同一計算機上のリポジトリにのみ使えます。"
+
+#: lib/choose_repository.tcl:617
+msgid "Failed to configure origin"
+msgstr "origin を設定できませんでした"
+
+#: lib/choose_repository.tcl:629
+msgid "Counting objects"
+msgstr "オブジェクトを数えています"
+
+#: lib/choose_repository.tcl:630
+msgid "buckets"
+msgstr "バケツ"
+
+#: lib/choose_repository.tcl:654
+#, tcl-format
+msgid "Unable to copy objects/info/alternates: %s"
+msgstr "objects/info/alternates を複写できません: %s"
+
+#: lib/choose_repository.tcl:690
+#, tcl-format
+msgid "Nothing to clone from %s."
+msgstr "%s から複製する内容はありません"
+
+#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
+#: lib/choose_repository.tcl:918
+msgid "The 'master' branch has not been initialized."
+msgstr "'master' ブランチが初期化されていません"
+
+#: lib/choose_repository.tcl:705
+msgid "Hardlinks are unavailable.  Falling back to copying."
+msgstr "ハードリンクが作れないので、コピーします"
+
+#: lib/choose_repository.tcl:717
+#, tcl-format
+msgid "Cloning from %s"
+msgstr "%s から複製しています"
+
+#: lib/choose_repository.tcl:748
+msgid "Copying objects"
+msgstr "オブジェクトを複写しています"
+
+#: lib/choose_repository.tcl:749
+msgid "KiB"
+msgstr "KiB"
+
+#: lib/choose_repository.tcl:773
+#, tcl-format
+msgid "Unable to copy object: %s"
+msgstr "オブジェクトを複写できません: %s"
+
+#: lib/choose_repository.tcl:783
+msgid "Linking objects"
+msgstr "オブジェクトを連結しています"
+
+#: lib/choose_repository.tcl:784
+msgid "objects"
+msgstr "オブジェクト"
+
+#: lib/choose_repository.tcl:792
+#, tcl-format
+msgid "Unable to hardlink object: %s"
+msgstr "オブジェクトをハードリンクできません: %s"
+
+#: lib/choose_repository.tcl:847
+msgid "Cannot fetch branches and objects.  See console output for details."
+msgstr "ブランチやオブジェクトを取得できません。コンソール出力を見て下さい"
+
+#: lib/choose_repository.tcl:858
+msgid "Cannot fetch tags.  See console output for details."
+msgstr "タグを取得できません。コンソール出力を見て下さい"
+
+#: lib/choose_repository.tcl:882
+msgid "Cannot determine HEAD.  See console output for details."
+msgstr "HEAD を確定できません。コンソール出力を見て下さい"
+
+#: lib/choose_repository.tcl:891
+#, tcl-format
+msgid "Unable to cleanup %s"
+msgstr "%s を掃除できません"
+
+#: lib/choose_repository.tcl:897
+msgid "Clone failed."
+msgstr "複写に失敗しました。"
+
+#: lib/choose_repository.tcl:904
+msgid "No default branch obtained."
+msgstr "デフォールト・ブランチが取得されませんでした"
+
+#: lib/choose_repository.tcl:915
+#, tcl-format
+msgid "Cannot resolve %s as a commit."
+msgstr "%s をコミットとして解釈できません"
+
+#: lib/choose_repository.tcl:927
+msgid "Creating working directory"
+msgstr "作業ディレクトリを作成しています"
+
+#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
+#: lib/index.tcl:193
+msgid "files"
+msgstr "ファイル"
+
+#: lib/choose_repository.tcl:957
+msgid "Initial file checkout failed."
+msgstr "初期チェックアウトに失敗しました"
+
+#: lib/choose_repository.tcl:973
+msgid "Open"
+msgstr "開く"
+
+#: lib/choose_repository.tcl:983
+msgid "Repository:"
+msgstr "リポジトリ:"
+
+#: lib/choose_repository.tcl:1033
+#, tcl-format
+msgid "Failed to open repository %s:"
+msgstr "リポジトリ %s を開けません:"
+
+#: lib/choose_rev.tcl:53
+msgid "This Detached Checkout"
+msgstr "分離されたチェックアウト"
+
+#: lib/choose_rev.tcl:60
+msgid "Revision Expression:"
+msgstr "リビジョン式:"
+
+#: lib/choose_rev.tcl:74
+msgid "Local Branch"
+msgstr "ローカル・ブランチ"
+
+#: lib/choose_rev.tcl:79
+msgid "Tracking Branch"
+msgstr "トラッキング・ブランチ"
+
+#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
+msgid "Tag"
+msgstr "タグ"
+
+#: lib/choose_rev.tcl:317
+#, tcl-format
+msgid "Invalid revision: %s"
+msgstr "無効なリビジョン: %s"
+
+#: lib/choose_rev.tcl:338
+msgid "No revision selected."
+msgstr "リビジョンが未選択です。"
+
+#: lib/choose_rev.tcl:346
+msgid "Revision expression is empty."
+msgstr "リビジョン式が空です。"
+
+#: lib/choose_rev.tcl:530
+msgid "Updated"
+msgstr "更新しました"
+
+#: lib/choose_rev.tcl:558
+msgid "URL"
+msgstr "URL"
+
+#: lib/commit.tcl:9
+msgid ""
+"There is nothing to amend.\n"
+"\n"
+"You are about to create the initial commit.  There is no commit before this "
+"to amend.\n"
+msgstr ""
+"訂正するコミットがそもそもありません。\n"
+"\n"
+"これから作るのは最初のコミットです。その前にはまだ訂正するようなコミットはあ"
+"りません。\n"
+
+#: lib/commit.tcl:18
+msgid ""
+"Cannot amend while merging.\n"
+"\n"
+"You are currently in the middle of a merge that has not been fully "
+"completed.  You cannot amend the prior commit unless you first abort the "
+"current merge activity.\n"
+msgstr ""
+"マージ中にコミットの訂正はできません。\n"
+"\n"
+"現在はまだマージの途中です。先にこのマージを中止しないと、前のコミットの訂正"
+"はできません\n"
+
+#: lib/commit.tcl:49
+msgid "Error loading commit data for amend:"
+msgstr "訂正するコミットのデータを読めません:"
+
+#: lib/commit.tcl:76
+msgid "Unable to obtain your identity:"
+msgstr "ユーザの正体を確認できません:"
+
+#: lib/commit.tcl:81
+msgid "Invalid GIT_COMMITTER_IDENT:"
+msgstr "GIT_COMMITTER_IDENT が無効です:"
+
+#: lib/commit.tcl:133
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before another commit can be created.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"最後にスキャンした状態はリポジトリの状態と合致しません。\n"
+"\n"
+"最後にスキャンして以後、別の Git プログラムがリポジトリを変更しています。新し"
+"くコミットする前に、再スキャンが必要です。\n"
+"\n"
+"自動的に再スキャンを開始します。\n"
+
+#: lib/commit.tcl:154
+#, tcl-format
+msgid ""
+"Unmerged files cannot be committed.\n"
+"\n"
+"File %s has merge conflicts.  You must resolve them and stage the file "
+"before committing.\n"
+msgstr ""
+"マージしていないファイルはコミットできません。\n"
+"\n"
+"ファイル %s にはマージ衝突が残っています。まず解決してコミット予定に加える必"
+"要があります。\n"
+
+#: lib/commit.tcl:162
+#, tcl-format
+msgid ""
+"Unknown file state %s detected.\n"
+"\n"
+"File %s cannot be committed by this program.\n"
+msgstr ""
+"不明なファイル状態 %s です。\n"
+"\n"
+"ファイル %s は本プログラムではコミットできません。\n"
+
+#: lib/commit.tcl:170
+msgid ""
+"No changes to commit.\n"
+"\n"
+"You must stage at least 1 file before you can commit.\n"
+msgstr ""
+"コミットする変更がありません。\n"
+"\n"
+"最低一つの変更をコミット予定に加えてからコミットして下さい。\n"
+
+#: lib/commit.tcl:183
+msgid ""
+"Please supply a commit message.\n"
+"\n"
+"A good commit message has the following format:\n"
+"\n"
+"- First line: Describe in one sentence what you did.\n"
+"- Second line: Blank\n"
+"- Remaining lines: Describe why this change is good.\n"
+msgstr ""
+"コミット・メッセージを入力して下さい。\n"
+"\n"
+"正しいコミット・メッセージは:\n"
+"\n"
+"- 第1行: 何をしたか、を1行で要約。\n"
+"- 第2行: 空白\n"
+"- 残りの行: なぜ、この変更が良い変更か、の説明。\n"
+
+#: lib/commit.tcl:257
+msgid "write-tree failed:"
+msgstr "write-tree が失敗しました:"
+
+#: lib/commit.tcl:275
+#, tcl-format
+msgid "Commit %s appears to be corrupt"
+msgstr "コミット %s は壊れています"
+
+#: lib/commit.tcl:279
+msgid ""
+"No changes to commit.\n"
+"\n"
+"No files were modified by this commit and it was not a merge commit.\n"
+"\n"
+"A rescan will be automatically started now.\n"
+msgstr ""
+"コミットする変更がありません。\n"
+"\n"
+"マージでなく、また、一つも変更点がありません。\n"
+"\n"
+"自動的に再スキャンを開始します。\n"
+
+#: lib/commit.tcl:286
+msgid "No changes to commit."
+msgstr "コミットする変更がありません。"
+
+#: lib/commit.tcl:303
+#, tcl-format
+msgid "warning: Tcl does not support encoding '%s'."
+msgstr "警告: Tcl はエンコーディング '%s' をサポートしていません"
+
+#: lib/commit.tcl:317
+msgid "commit-tree failed:"
+msgstr "commit-tree が失敗しました:"
+
+#: lib/commit.tcl:339
+msgid "update-ref failed:"
+msgstr "update-ref が失敗しました:"
+
+#: lib/commit.tcl:430
+#, tcl-format
+msgid "Created commit %s: %s"
+msgstr "コミット %s を作成しました: %s"
+
+#: lib/console.tcl:57
+msgid "Working... please wait..."
+msgstr "実行中…お待ち下さい…"
+
+#: lib/console.tcl:183
+msgid "Success"
+msgstr "成功"
+
+#: lib/console.tcl:196
+msgid "Error: Command Failed"
+msgstr "エラー: コマンドが失敗しました"
+
+#: lib/database.tcl:43
+msgid "Number of loose objects"
+msgstr "ばらばらなオブジェクトの数"
+
+#: lib/database.tcl:44
+msgid "Disk space used by loose objects"
+msgstr "ばらばらなオブジェクトの使用するディスク量"
+
+#: lib/database.tcl:45
+msgid "Number of packed objects"
+msgstr "パックされたオブジェクトの数"
+
+#: lib/database.tcl:46
+msgid "Number of packs"
+msgstr "パックの数"
+
+#: lib/database.tcl:47
+msgid "Disk space used by packed objects"
+msgstr "パックされたオブジェクトの使用するディスク量"
+
+#: lib/database.tcl:48
+msgid "Packed objects waiting for pruning"
+msgstr "パックに存在するので捨てて良いオブジェクトの数"
+
+#: lib/database.tcl:49
+msgid "Garbage files"
+msgstr "ゴミファイル"
+
+#: lib/database.tcl:72
+msgid "Compressing the object database"
+msgstr "データベース圧縮"
+
+#: lib/database.tcl:83
+msgid "Verifying the object database with fsck-objects"
+msgstr "fsck-objects でオブジェクト・データベースを検証しています"
+
+#: lib/database.tcl:108
+#, tcl-format
+msgid ""
+"This repository currently has approximately %i loose objects.\n"
+"\n"
+"To maintain optimal performance it is strongly recommended that you compress "
+"the database when more than %i loose objects exist.\n"
+"\n"
+"Compress the database now?"
+msgstr ""
+"このリポジトリにはおおよそ %i 個の個別オブジェクトがあります\n"
+"\n"
+"最適な性能を保つために、%i 個以上の個別オブジェクトを作る毎にデータベースを圧"
+"縮することを推奨します\n"
+"\n"
+"データベースを圧縮しますか?"
+
+#: lib/date.tcl:25
+#, tcl-format
+msgid "Invalid date from Git: %s"
+msgstr "Git から出た無効な日付: %s"
+
+#: lib/diff.tcl:42
+#, tcl-format
+msgid ""
+"No differences detected.\n"
+"\n"
+"%s has no changes.\n"
+"\n"
+"The modification date of this file was updated by another application, but "
+"the content within the file was not changed.\n"
+"\n"
+"A rescan will be automatically started to find other files which may have "
+"the same state."
+msgstr ""
+"変更がありません。\n"
+"\n"
+"%s には変更がありません。\n"
+"\n"
+"このファイルの変更時刻は他のアプリケーションによって更新されていますがファイ"
+"ル内容には変更がありません。\n"
+"\n"
+"同様な状態のファイルを探すために、自動的に再スキャンを開始します。"
+
+#: lib/diff.tcl:81
+#, tcl-format
+msgid "Loading diff of %s..."
+msgstr "%s の変更点をロード中…"
+
+#: lib/diff.tcl:114 lib/diff.tcl:184
+#, tcl-format
+msgid "Unable to display %s"
+msgstr "%s を表示できません"
+
+#: lib/diff.tcl:115
+msgid "Error loading file:"
+msgstr "ファイルを読む際のエラーです:"
+
+#: lib/diff.tcl:122
+msgid "Git Repository (subproject)"
+msgstr "Git リポジトリ(サブプロジェクト)"
+
+#: lib/diff.tcl:134
+msgid "* Binary file (not showing content)."
+msgstr "* バイナリファイル(内容は表示しません)"
+
+#: lib/diff.tcl:185
+msgid "Error loading diff:"
+msgstr "diff を読む際のエラーです:"
+
+#: lib/diff.tcl:302
+msgid "Failed to unstage selected hunk."
+msgstr "選択されたパッチをコミット予定から外せません。"
+
+#: lib/diff.tcl:309
+msgid "Failed to stage selected hunk."
+msgstr "選択されたパッチをコミット予定に加えられません。"
+
+#: lib/error.tcl:12 lib/error.tcl:102
+msgid "error"
+msgstr "エラー"
+
+#: lib/error.tcl:28
+msgid "warning"
+msgstr "警告"
+
+#: lib/error.tcl:81
+msgid "You must correct the above errors before committing."
+msgstr "コミットする前に、以上のエラーを修正して下さい"
+
+#: lib/index.tcl:6
+msgid "Unable to unlock the index."
+msgstr "インデックスをロックできません"
+
+#: lib/index.tcl:15
+msgid "Index Error"
+msgstr "索引エラー"
+
+#: lib/index.tcl:21
+msgid ""
+"Updating the Git index failed.  A rescan will be automatically started to "
+"resynchronize git-gui."
+msgstr "GIT インデックスの更新が失敗しました。git-gui と同期をとるために再スキャンします。"
+
+#: lib/index.tcl:27
+msgid "Continue"
+msgstr "続行"
+
+#: lib/index.tcl:31
+msgid "Unlock Index"
+msgstr "インデックスのロック解除"
+
+#: lib/index.tcl:282
+#, tcl-format
+msgid "Unstaging %s from commit"
+msgstr "コミットから '%s' を降ろす"
+
+#: lib/index.tcl:326
+#, tcl-format
+msgid "Adding %s"
+msgstr "コミットに %s を加えています"
+
+#: lib/index.tcl:381
+#, tcl-format
+msgid "Revert changes in file %s?"
+msgstr "ファイル %s にした変更を元に戻しますか?"
+
+#: lib/index.tcl:383
+#, tcl-format
+msgid "Revert changes in these %i files?"
+msgstr "これら %i 個のファイルにした変更を元に戻しますか?"
+
+#: lib/index.tcl:389
+msgid "Any unstaged changes will be permanently lost by the revert."
+msgstr "変更を元に戻すとコミット予定していない変更は全て失われます。"
+
+#: lib/index.tcl:392
+msgid "Do Nothing"
+msgstr "何もしない"
+
+#: lib/merge.tcl:13
+msgid ""
+"Cannot merge while amending.\n"
+"\n"
+"You must finish amending this commit before starting any type of merge.\n"
+msgstr ""
+"訂正中にはマージできません。\n"
+"\n"
+"訂正処理を完了するまでは新たにマージを開始できません。\n"
+
+#: lib/merge.tcl:27
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before a merge can be performed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"最後にスキャンした状態はリポジトリの状態と合致しません。\n"
+"\n"
+"最後にスキャンして以後、別の Git プログラムがリポジトリを変更しています。マー"
+"ジを開始する前に、再スキャンが必要です。\n"
+"\n"
+"自動的に再スキャンを開始します。\n"
+
+#: lib/merge.tcl:44
+#, tcl-format
+msgid ""
+"You are in the middle of a conflicted merge.\n"
+"\n"
+"File %s has merge conflicts.\n"
+"\n"
+"You must resolve them, stage the file, and commit to complete the current "
+"merge.  Only then can you begin another merge.\n"
+msgstr ""
+"衝突のあったマージの途中です。\n"
+"\n"
+"ファイル %s にはマージ中の衝突が残っています。\n"
+"\n"
+"このファイルの衝突を解決し、コミット予定に加えて、コミットすることでマージを"
+"完了します。そうやって始めて、新たなマージを開始できるようになります。\n"
+
+#: lib/merge.tcl:54
+#, tcl-format
+msgid ""
+"You are in the middle of a change.\n"
+"\n"
+"File %s is modified.\n"
+"\n"
+"You should complete the current commit before starting a merge.  Doing so "
+"will help you abort a failed merge, should the need arise.\n"
+msgstr ""
+"変更の途中です。\n"
+"\n"
+"ファイル %s は変更中です。\n"
+"\n"
+"現在のコミットを完了してからマージを開始して下さい。そうする方がマージに失敗"
+"したときの回復が楽です。\n"
+
+#: lib/merge.tcl:106
+#, tcl-format
+msgid "%s of %s"
+msgstr "%s の %s ブランチ"
+
+#: lib/merge.tcl:119
+#, tcl-format
+msgid "Merging %s and %s"
+msgstr "%s と %s をマージします"
+
+#: lib/merge.tcl:131
+msgid "Merge completed successfully."
+msgstr "マージが完了しました"
+
+#: lib/merge.tcl:133
+msgid "Merge failed.  Conflict resolution is required."
+msgstr "マージが失敗しました。衝突の解決が必要です。"
+
+#: lib/merge.tcl:158
+#, tcl-format
+msgid "Merge Into %s"
+msgstr "%s にマージ"
+
+#: lib/merge.tcl:177
+msgid "Revision To Merge"
+msgstr "マージするリビジョン"
+
+#: lib/merge.tcl:212
+msgid ""
+"Cannot abort while amending.\n"
+"\n"
+"You must finish amending this commit.\n"
+msgstr ""
+"訂正中には中止できません。\n"
+"\n"
+"まず今のコミット訂正を完了させて下さい。\n"
+
+#: lib/merge.tcl:222
+msgid ""
+"Abort merge?\n"
+"\n"
+"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with aborting the current merge?"
+msgstr ""
+"マージを中断しますか?\n"
+"\n"
+"現在のマージを中断すると、コミットしていない全ての変更が失われます。\n"
+"\n"
+"マージを中断してよろしいですか?"
+
+#: lib/merge.tcl:228
+msgid ""
+"Reset changes?\n"
+"\n"
+"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with resetting the current changes?"
+msgstr ""
+"変更点をリセットしますか?\n"
+"\n"
+"変更点をリセットすると、コミットしていない全ての変更が失われます。\n"
+"\n"
+"リセットしてよろしいですか?"
+
+#: lib/merge.tcl:239
+msgid "Aborting"
+msgstr "中断しています"
+
+#: lib/merge.tcl:266
+msgid "Abort failed."
+msgstr "中断に失敗しました。"
+
+#: lib/merge.tcl:268
+msgid "Abort completed.  Ready."
+msgstr "中断完了。"
+
+#: lib/option.tcl:82
+msgid "Restore Defaults"
+msgstr "既定値に戻す"
+
+#: lib/option.tcl:86
+msgid "Save"
+msgstr "保存"
+
+#: lib/option.tcl:96
+#, tcl-format
+msgid "%s Repository"
+msgstr "%s リポジトリ"
+
+#: lib/option.tcl:97
+msgid "Global (All Repositories)"
+msgstr "大域(全てのリポジトリ)"
+
+#: lib/option.tcl:103
+msgid "User Name"
+msgstr "ユーザ名"
+
+#: lib/option.tcl:104
+msgid "Email Address"
+msgstr "電子メールアドレス"
+
+#: lib/option.tcl:106
+msgid "Summarize Merge Commits"
+msgstr "マージコミットの要約"
+
+#: lib/option.tcl:107
+msgid "Merge Verbosity"
+msgstr "マージの冗長度"
+
+#: lib/option.tcl:108
+msgid "Show Diffstat After Merge"
+msgstr "マージ後に diffstat を表示"
+
+#: lib/option.tcl:110
+msgid "Trust File Modification Timestamps"
+msgstr "ファイル変更時刻を信頼する"
+
+#: lib/option.tcl:111
+msgid "Prune Tracking Branches During Fetch"
+msgstr "フェッチ中にトラッキングブランチを刈る"
+
+#: lib/option.tcl:112
+msgid "Match Tracking Branches"
+msgstr "トラッキングブランチを合わせる"
+
+#: lib/option.tcl:113
+msgid "Number of Diff Context Lines"
+msgstr "diff の文脈行数"
+
+#: lib/option.tcl:114
+msgid "New Branch Name Template"
+msgstr "新しいブランチ名のテンプレート"
+
+#: lib/option.tcl:176
+msgid "Change Font"
+msgstr "フォントを変更"
+
+#: lib/option.tcl:180
+#, tcl-format
+msgid "Choose %s"
+msgstr "%s を選択"
+
+#: lib/option.tcl:186
+msgid "pt."
+msgstr "ポイント"
+
+#: lib/option.tcl:200
+msgid "Preferences"
+msgstr "設定"
+
+#: lib/option.tcl:235
+msgid "Failed to completely save options:"
+msgstr "完全にオプションを保存できません:"
+
+#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
+msgid "Delete Remote Branch"
+msgstr "リモート・ブランチを削除"
+
+#: lib/remote_branch_delete.tcl:47
+msgid "From Repository"
+msgstr "元のリポジトリ"
+
+#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
+msgid "Remote:"
+msgstr "リモート:"
+
+#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
+msgid "Arbitrary URL:"
+msgstr "任意の URL:"
+
+#: lib/remote_branch_delete.tcl:84
+msgid "Branches"
+msgstr "ブランチ"
+
+#: lib/remote_branch_delete.tcl:109
+msgid "Delete Only If"
+msgstr "条件付で削除"
+
+#: lib/remote_branch_delete.tcl:111
+msgid "Merged Into:"
+msgstr "マージ先:"
+
+#: lib/remote_branch_delete.tcl:119
+msgid "Always (Do not perform merge checks)"
+msgstr "無条件(マージ検査をしない)"
+
+#: lib/remote_branch_delete.tcl:152
+msgid "A branch is required for 'Merged Into'."
+msgstr "'マージ先' にはブランチが必要です。"
+
+#: lib/remote_branch_delete.tcl:184
+#, tcl-format
+msgid ""
+"The following branches are not completely merged into %s:\n"
+"\n"
+" - %s"
+msgstr ""
+"以下のブランチは %s に完全にマージされていません:\n"
+"\n"
+" - %s"
+
+#: lib/remote_branch_delete.tcl:189
+#, tcl-format
+msgid ""
+"One or more of the merge tests failed because you have not fetched the "
+"necessary commits.  Try fetching from %s first."
+msgstr ""
+"必要なコミットが不足しているために、マージ検査が失敗しました。まず %s から"
+"フェッチして下さい。"
+
+#: lib/remote_branch_delete.tcl:207
+msgid "Please select one or more branches to delete."
+msgstr "削除するブランチを選択して下さい。"
+
+#: lib/remote_branch_delete.tcl:216
+msgid ""
+"Recovering deleted branches is difficult.\n"
+"\n"
+"Delete the selected branches?"
+msgstr ""
+"削除したブランチを回復するのは困難です。\n"
+"\n"
+"選択したブランチを削除して良いですか?"
+
+#: lib/remote_branch_delete.tcl:226
+#, tcl-format
+msgid "Deleting branches from %s"
+msgstr "%s からブランチを削除しています。"
+
+#: lib/remote_branch_delete.tcl:286
+msgid "No repository selected."
+msgstr "リポジトリが選択されていません。"
+
+#: lib/remote_branch_delete.tcl:291
+#, tcl-format
+msgid "Scanning %s..."
+msgstr "%s をスキャンしています…"
+
+#: lib/remote.tcl:165
+msgid "Prune from"
+msgstr "から刈込む…"
+
+#: lib/remote.tcl:170
+msgid "Fetch from"
+msgstr "取得元"
+
+#: lib/remote.tcl:213
+msgid "Push to"
+msgstr "プッシュ先"
+
+#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
+msgid "Cannot write shortcut:"
+msgstr "ショートカットが書けません:"
+
+#: lib/shortcut.tcl:136
+msgid "Cannot write icon:"
+msgstr "アイコンが書けません:"
+
+#: lib/status_bar.tcl:83
+#, tcl-format
+msgid "%s ... %*i of %*i %s (%3i%%)"
+msgstr "%1$s ... %4$*i %6$s 中の %2$*i (%7$3i%%)"
+
+#: lib/transport.tcl:6
+#, tcl-format
+msgid "fetch %s"
+msgstr "%s を取得"
+
+#: lib/transport.tcl:7
+#, tcl-format
+msgid "Fetching new changes from %s"
+msgstr "%s から新しい変更をフェッチしています"
+
+#: lib/transport.tcl:18
+#, tcl-format
+msgid "remote prune %s"
+msgstr "遠隔刈込 %s"
+
+#: lib/transport.tcl:19
+#, tcl-format
+msgid "Pruning tracking branches deleted from %s"
+msgstr "%s から削除されたトラッキング・ブランチを刈っています"
+
+#: lib/transport.tcl:25 lib/transport.tcl:71
+#, tcl-format
+msgid "push %s"
+msgstr "%s をプッシュ"
+
+#: lib/transport.tcl:26
+#, tcl-format
+msgid "Pushing changes to %s"
+msgstr "%s へ変更をプッシュしています"
+
+#: lib/transport.tcl:72
+#, tcl-format
+msgid "Pushing %s %s to %s"
+msgstr "%3$s へ %1$s %2$s をプッシュしています"
+
+#: lib/transport.tcl:89
+msgid "Push Branches"
+msgstr "ブランチをプッシュ"
+
+#: lib/transport.tcl:103
+msgid "Source Branches"
+msgstr "元のブランチ"
+
+#: lib/transport.tcl:120
+msgid "Destination Repository"
+msgstr "送り先リポジトリ"
+
+#: lib/transport.tcl:158
+msgid "Transfer Options"
+msgstr "通信オプション"
+
+#: lib/transport.tcl:160
+msgid "Force overwrite existing branch (may discard changes)"
+msgstr "既存ブランチを上書き(変更を破棄する可能性があります)"
+
+#: lib/transport.tcl:164
+msgid "Use thin pack (for slow network connections)"
+msgstr "Thin Pack を使う(遅いネットワーク接続)"
+
+#: lib/transport.tcl:168
+msgid "Include tags"
+msgstr "タグを含める"
diff --git a/git-gui/po/po2msg.sh b/git-gui/po/po2msg.sh
new file mode 100644 (file)
index 0000000..c63248e
--- /dev/null
@@ -0,0 +1,133 @@
+#!/bin/sh
+# Tcl ignores the next line -*- tcl -*- \
+exec tclsh "$0" -- "$@"
+
+# This is a really stupid program, which serves as an alternative to
+# msgfmt.  It _only_ translates to Tcl mode, does _not_ validate the
+# input, and does _not_ output any statistics.
+
+proc u2a {s} {
+       set res ""
+       foreach i [split $s ""] {
+               scan $i %c c
+               if {$c<128} {
+                       # escape '[', '\' and ']'
+                       if {$c == 0x5b || $c == 0x5d} {
+                               append res "\\"
+                       }
+                       append res $i
+               } else {
+                       append res \\u[format %04.4x $c]
+               }
+       }
+       return $res
+}
+
+set output_directory "."
+set lang "dummy"
+set files [list]
+set show_statistics 0
+
+# parse options
+for {set i 0} {$i < $argc} {incr i} {
+       set arg [lindex $argv $i]
+       if {$arg == "--statistics"} {
+               incr show_statistics
+               continue
+       }
+       if {$arg == "--tcl"} {
+               # we know
+               continue
+       }
+       if {$arg == "-l"} {
+               incr i
+               set lang [lindex $argv $i]
+               continue
+       }
+       if {$arg == "-d"} {
+               incr i
+               set tmp [lindex $argv $i]
+               regsub "\[^/\]$" $tmp "&/" output_directory
+               continue
+       }
+       lappend files $arg
+}
+
+proc flush_msg {} {
+       global msgid msgstr mode lang out fuzzy
+       global translated_count fuzzy_count not_translated_count
+
+       if {![info exists msgid] || $mode == ""} {
+               return
+       }
+       set mode ""
+       if {$fuzzy == 1} {
+               incr fuzzy_count
+               set fuzzy 0
+               return
+       }
+
+       if {$msgid == ""} {
+               set prefix "set ::msgcat::header"
+       } else {
+               if {$msgstr == ""} {
+                       incr not_translated_count
+                       return
+               }
+               set prefix "::msgcat::mcset $lang \"[u2a $msgid]\""
+               incr translated_count
+       }
+
+       puts $out "$prefix \"[u2a $msgstr]\""
+}
+
+set fuzzy 0
+set translated_count 0
+set fuzzy_count 0
+set not_translated_count 0
+foreach file $files {
+       regsub "^.*/\(\[^/\]*\)\.po$" $file "$output_directory\\1.msg" outfile
+       set in [open $file "r"]
+       fconfigure $in -encoding utf-8
+       set out [open $outfile "w"]
+
+       set mode ""
+       while {[gets $in line] >= 0} {
+               if {[regexp "^#" $line]} {
+                       if {[regexp ", fuzzy" $line]} {
+                               set fuzzy 1
+                       } else {
+                               flush_msg
+                       }
+                       continue
+               } elseif {[regexp "^msgid \"(.*)\"$" $line dummy match]} {
+                       flush_msg
+                       set msgid $match
+                       set mode "msgid"
+               } elseif {[regexp "^msgstr \"(.*)\"$" $line dummy match]} {
+                       set msgstr $match
+                       set mode "msgstr"
+               } elseif {$line == ""} {
+                       flush_msg
+               } elseif {[regexp "^\"(.*)\"$" $line dummy match]} {
+                       if {$mode == "msgid"} {
+                               append msgid $match
+                       } elseif {$mode == "msgstr"} {
+                               append msgstr $match
+                       } else {
+                               puts stderr "I do not know what to do: $match"
+                       }
+               } else {
+                       puts stderr "Cannot handle $line"
+               }
+       }
+       flush_msg
+       close $in
+       close $out
+}
+
+if {$show_statistics} {
+       puts [concat "$translated_count translated messages, " \
+               "$fuzzy_count fuzzy ones, " \
+               "$not_translated_count untranslated ones."]
+}
diff --git a/git-gui/po/ru.po b/git-gui/po/ru.po
new file mode 100644 (file)
index 0000000..6727a83
--- /dev/null
@@ -0,0 +1,1893 @@
+# Translation of git-gui to russian
+# Copyright (C) 2007 Shawn Pearce
+# This file is distributed under the same license as the git-gui package.
+# Irina Riesen <irina.riesen@gmail.com>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: git-gui\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-10-31 21:23+0100\n"
+"PO-Revision-Date: 2007-10-22 22:30-0200\n"
+"Last-Translator: Alex Riesen <raa.lkml@gmail.com>\n"
+"Language-Team: Russian Translation <git@vger.kernel.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: git-gui.sh:41 git-gui.sh:597 git-gui.sh:611 git-gui.sh:624 git-gui.sh:707
+#: git-gui.sh:726
+msgid "git-gui: fatal error"
+msgstr "git-gui: критическая ошибка"
+
+#: git-gui.sh:558
+#, tcl-format
+msgid "Invalid font specified in %s:"
+msgstr "В %s установлен неверный шрифт:"
+
+#: git-gui.sh:583
+msgid "Main Font"
+msgstr "Шрифт интерфейса"
+
+#: git-gui.sh:584
+msgid "Diff/Console Font"
+msgstr "Шрифт консоли и изменений (diff)"
+
+#: git-gui.sh:598
+msgid "Cannot find git in PATH."
+msgstr "git не найден в PATH."
+
+#: git-gui.sh:625
+msgid "Cannot parse Git version string:"
+msgstr "Невозможно распознать строку версии Git: "
+
+#: git-gui.sh:643
+#, tcl-format
+msgid ""
+"Git version cannot be determined.\n"
+"\n"
+"%s claims it is version '%s'.\n"
+"\n"
+"%s requires at least Git 1.5.0 or later.\n"
+"\n"
+"Assume '%s' is version 1.5.0?\n"
+msgstr ""
+"Невозможно определить версию Git\n"
+"%s указывает на версию '%s'.\n"
+"\n"
+"для %s требуется версия Git, начиная с 1.5.0\n"
+"\n"
+"Принять '%s' как версию 1.5.0?\n"
+
+#: git-gui.sh:881
+msgid "Git directory not found:"
+msgstr "Каталог Git не найден:"
+
+#: git-gui.sh:888
+msgid "Cannot move to top of working directory:"
+msgstr "Невозможно перейти к корню рабочего каталога репозитория: "
+
+#: git-gui.sh:895
+msgid "Cannot use funny .git directory:"
+msgstr "Каталог.git испорчен: "
+
+#: git-gui.sh:900
+msgid "No working directory"
+msgstr "Отсутствует рабочий каталог"
+
+#: git-gui.sh:1047
+msgid "Refreshing file status..."
+msgstr "Обновление информации о состоянии файлов..."
+
+#: git-gui.sh:1112
+msgid "Scanning for modified files ..."
+msgstr "Поиск измененных файлов..."
+
+#: git-gui.sh:1287 lib/browser.tcl:245
+msgid "Ready."
+msgstr "Готово."
+
+#: git-gui.sh:1553
+msgid "Unmodified"
+msgstr "Не изменено"
+
+#: git-gui.sh:1555
+msgid "Modified, not staged"
+msgstr "Изменено, не подготовлено"
+
+#: git-gui.sh:1556 git-gui.sh:1561
+msgid "Staged for commit"
+msgstr "Подготовлено для сохранения"
+
+#: git-gui.sh:1557 git-gui.sh:1562
+msgid "Portions staged for commit"
+msgstr "Части, подготовленные для сохранения"
+
+#: git-gui.sh:1558 git-gui.sh:1563
+msgid "Staged for commit, missing"
+msgstr "Подготовлено для сохранения, отсутствует"
+
+#: git-gui.sh:1560
+msgid "Untracked, not staged"
+msgstr "Не отслеживается, не подготовлено"
+
+#: git-gui.sh:1565
+msgid "Missing"
+msgstr "Отсутствует"
+
+#: git-gui.sh:1566
+msgid "Staged for removal"
+msgstr "Подготовлено для удаления"
+
+#: git-gui.sh:1567
+msgid "Staged for removal, still present"
+msgstr "Подготовлено для удаления, еще не удалено"
+
+#: git-gui.sh:1569 git-gui.sh:1570 git-gui.sh:1571 git-gui.sh:1572
+msgid "Requires merge resolution"
+msgstr "Требуется разрешение конфликта при объединении"
+
+#: git-gui.sh:1607
+msgid "Starting gitk... please wait..."
+msgstr "Запускается gitk... пожалуйста, ждите..."
+
+#: git-gui.sh:1616
+#, tcl-format
+msgid ""
+"Unable to start gitk:\n"
+"\n"
+"%s does not exist"
+msgstr ""
+"Не удалось запустить gitk:\n"
+"\n"
+"%s не существует"
+
+#: git-gui.sh:1816 lib/choose_repository.tcl:35
+msgid "Repository"
+msgstr "Репозиторий"
+
+#: git-gui.sh:1817
+msgid "Edit"
+msgstr "Редактировать"
+
+#: git-gui.sh:1819 lib/choose_rev.tcl:560
+msgid "Branch"
+msgstr "Ветвь"
+
+#: git-gui.sh:1822 lib/choose_rev.tcl:547
+msgid "Commit@@noun"
+msgstr "Состояние"
+
+#: git-gui.sh:1825 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
+msgid "Merge"
+msgstr "Объединить"
+
+#: git-gui.sh:1826 lib/choose_rev.tcl:556
+msgid "Remote"
+msgstr "Внешние репозитории"
+
+#: git-gui.sh:1835
+msgid "Browse Current Branch's Files"
+msgstr "Просмотреть файлы текущей ветви"
+
+#: git-gui.sh:1839
+msgid "Browse Branch Files..."
+msgstr "Показать файлы ветви..."
+
+#: git-gui.sh:1844
+msgid "Visualize Current Branch's History"
+msgstr "История текущей ветви наглядно"
+
+#: git-gui.sh:1848
+msgid "Visualize All Branch History"
+msgstr "История всех ветвей наглядно"
+
+#: git-gui.sh:1855
+#, tcl-format
+msgid "Browse %s's Files"
+msgstr "Показать файлы ветви %s"
+
+#: git-gui.sh:1857
+#, tcl-format
+msgid "Visualize %s's History"
+msgstr "История ветви %s наглядно"
+
+#: git-gui.sh:1862 lib/database.tcl:27 lib/database.tcl:67
+msgid "Database Statistics"
+msgstr "Статистика базы данных"
+
+#: git-gui.sh:1865 lib/database.tcl:34
+msgid "Compress Database"
+msgstr "Сжать базу данных"
+
+#: git-gui.sh:1868
+msgid "Verify Database"
+msgstr "Проверить базу данных"
+
+#: git-gui.sh:1875 git-gui.sh:1879 git-gui.sh:1883 lib/shortcut.tcl:7
+#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
+msgid "Create Desktop Icon"
+msgstr "Создать ярлык на рабочем столе"
+
+#: git-gui.sh:1888 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
+msgid "Quit"
+msgstr "Выход"
+
+#: git-gui.sh:1895
+msgid "Undo"
+msgstr "Отменить"
+
+#: git-gui.sh:1898
+msgid "Redo"
+msgstr "Повторить"
+
+#: git-gui.sh:1902 git-gui.sh:2395
+msgid "Cut"
+msgstr "Вырезать"
+
+#: git-gui.sh:1905 git-gui.sh:2398 git-gui.sh:2469 git-gui.sh:2541
+#: lib/console.tcl:67
+msgid "Copy"
+msgstr "Копировать"
+
+#: git-gui.sh:1908 git-gui.sh:2401
+msgid "Paste"
+msgstr "Вставить"
+
+#: git-gui.sh:1911 git-gui.sh:2404 lib/branch_delete.tcl:26
+#: lib/remote_branch_delete.tcl:38
+msgid "Delete"
+msgstr "Удалить"
+
+#: git-gui.sh:1915 git-gui.sh:2408 git-gui.sh:2545 lib/console.tcl:69
+msgid "Select All"
+msgstr "Выделить все"
+
+#: git-gui.sh:1924
+msgid "Create..."
+msgstr "Создать..."
+
+#: git-gui.sh:1930
+msgid "Checkout..."
+msgstr "Перейти..."
+
+#: git-gui.sh:1936
+msgid "Rename..."
+msgstr "Переименовать..."
+
+#: git-gui.sh:1941 git-gui.sh:2040
+msgid "Delete..."
+msgstr "Удалить..."
+
+#: git-gui.sh:1946
+msgid "Reset..."
+msgstr "Сбросить..."
+
+#: git-gui.sh:1958 git-gui.sh:2342
+msgid "New Commit"
+msgstr "Новое состояние"
+
+#: git-gui.sh:1966 git-gui.sh:2349
+msgid "Amend Last Commit"
+msgstr "Исправить последнее состояние"
+
+#: git-gui.sh:1975 git-gui.sh:2309 lib/remote_branch_delete.tcl:99
+msgid "Rescan"
+msgstr "Перечитать"
+
+#: git-gui.sh:1981
+msgid "Stage To Commit"
+msgstr "Подготовить для сохранения"
+
+#: git-gui.sh:1986
+msgid "Stage Changed Files To Commit"
+msgstr "Подготовить измененные файлы для сохранения"
+
+#: git-gui.sh:1992
+msgid "Unstage From Commit"
+msgstr "Убрать из подготовленного"
+
+#: git-gui.sh:1997 lib/index.tcl:393
+msgid "Revert Changes"
+msgstr "Отменить изменения"
+
+#: git-gui.sh:2004 git-gui.sh:2321 git-gui.sh:2419
+msgid "Sign Off"
+msgstr "Подписать"
+
+#: git-gui.sh:2008 git-gui.sh:2325
+msgid "Commit@@verb"
+msgstr "Сохранить"
+
+#: git-gui.sh:2019
+msgid "Local Merge..."
+msgstr "Локальное объединение..."
+
+#: git-gui.sh:2024
+msgid "Abort Merge..."
+msgstr "Прервать объединение..."
+
+#: git-gui.sh:2036
+msgid "Push..."
+msgstr "Отправить..."
+
+#: git-gui.sh:2047 lib/choose_repository.tcl:40
+msgid "Apple"
+msgstr ""
+
+#: git-gui.sh:2050 git-gui.sh:2072 lib/about.tcl:13
+#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
+#, tcl-format
+msgid "About %s"
+msgstr "О %s"
+
+#: git-gui.sh:2054
+msgid "Preferences..."
+msgstr "Настройки..."
+
+#: git-gui.sh:2062 git-gui.sh:2587
+msgid "Options..."
+msgstr "Настройки..."
+
+#: git-gui.sh:2068 lib/choose_repository.tcl:46
+msgid "Help"
+msgstr "Помощь"
+
+#: git-gui.sh:2109
+msgid "Online Documentation"
+msgstr "Документация в интернете"
+
+#: git-gui.sh:2193
+#, tcl-format
+msgid "fatal: cannot stat path %s: No such file or directory"
+msgstr "критическая ошибка: %s: нет такого файла или каталога"
+
+#: git-gui.sh:2226
+msgid "Current Branch:"
+msgstr "Текущая ветвь:"
+
+#: git-gui.sh:2247
+msgid "Staged Changes (Will Commit)"
+msgstr "Подготовлено (будет сохранено)"
+
+#: git-gui.sh:2266
+msgid "Unstaged Changes"
+msgstr "Изменено (не будет сохранено)"
+
+#: git-gui.sh:2315
+msgid "Stage Changed"
+msgstr "Подготовить все"
+
+#: git-gui.sh:2331 lib/transport.tcl:93 lib/transport.tcl:182
+msgid "Push"
+msgstr "Отправить"
+
+#: git-gui.sh:2361
+msgid "Initial Commit Message:"
+msgstr "Комментарий к первому состоянию:"
+
+#: git-gui.sh:2362
+msgid "Amended Commit Message:"
+msgstr "Комментарий к исправленному состоянию:"
+
+#: git-gui.sh:2363
+msgid "Amended Initial Commit Message:"
+msgstr "Комментарий к исправленному первоначальному состоянию:"
+
+#: git-gui.sh:2364
+msgid "Amended Merge Commit Message:"
+msgstr "Комментарий к исправленному объединению:"
+
+#: git-gui.sh:2365
+msgid "Merge Commit Message:"
+msgstr "Комментарий к объединению:"
+
+#: git-gui.sh:2366
+msgid "Commit Message:"
+msgstr "Комментарий к состоянию:"
+
+#: git-gui.sh:2411 git-gui.sh:2549 lib/console.tcl:71
+msgid "Copy All"
+msgstr "Копировать все"
+
+#: git-gui.sh:2435 lib/blame.tcl:104
+msgid "File:"
+msgstr "Файл:"
+
+#: git-gui.sh:2537
+msgid "Refresh"
+msgstr "Обновить"
+
+#: git-gui.sh:2558
+msgid "Apply/Reverse Hunk"
+msgstr "Применить/Убрать изменение"
+
+#: git-gui.sh:2564
+msgid "Decrease Font Size"
+msgstr "Уменьшить размер шрифта"
+
+#: git-gui.sh:2568
+msgid "Increase Font Size"
+msgstr "Увеличить размер шрифта"
+
+#: git-gui.sh:2573
+msgid "Show Less Context"
+msgstr "Меньше контекста"
+
+#: git-gui.sh:2580
+msgid "Show More Context"
+msgstr "Больше контекста"
+
+#: git-gui.sh:2594
+msgid "Unstage Hunk From Commit"
+msgstr "Не сохранять часть"
+
+#: git-gui.sh:2596
+msgid "Stage Hunk For Commit"
+msgstr "Подготовить часть для сохранения"
+
+#: git-gui.sh:2615
+msgid "Initializing..."
+msgstr "Инициализация..."
+
+#: git-gui.sh:2706
+#, tcl-format
+msgid ""
+"Possible environment issues exist.\n"
+"\n"
+"The following environment variables are probably\n"
+"going to be ignored by any Git subprocess run\n"
+"by %s:\n"
+"\n"
+msgstr ""
+"Возможны ошибки в переменных окружения.\n"
+"\n"
+"Переменные окружения, которые возможно\n"
+"будут проигнорированы командами Git,\n"
+"запущенными из %s\n"
+"\n"
+
+#: git-gui.sh:2736
+msgid ""
+"\n"
+"This is due to a known issue with the\n"
+"Tcl binary distributed by Cygwin."
+msgstr ""
+"\n"
+"Это известная проблема с Tcl,\n"
+"распространяемым Cygwin."
+
+#: git-gui.sh:2741
+#, tcl-format
+msgid ""
+"\n"
+"\n"
+"A good replacement for %s\n"
+"is placing values for the user.name and\n"
+"user.email settings into your personal\n"
+"~/.gitconfig file.\n"
+msgstr ""
+"\n"
+"\n"
+"Вместо использования %s можно\n"
+"сохранить значения user.name и\n"
+"user.email в Вашем персональном\n"
+"файле ~/.gitconfig.\n"
+
+#: lib/about.tcl:25
+msgid "git-gui - a graphical user interface for Git."
+msgstr "git-gui - графический пользовательский интерфейс к Git."
+
+#: lib/blame.tcl:77
+msgid "File Viewer"
+msgstr "Просмотр файла"
+
+#: lib/blame.tcl:81
+msgid "Commit:"
+msgstr "Сохраненное состояние:"
+
+#: lib/blame.tcl:249
+msgid "Copy Commit"
+msgstr "Скопировать SHA-1"
+
+#: lib/blame.tcl:369
+#, tcl-format
+msgid "Reading %s..."
+msgstr "Чтение %s..."
+
+#: lib/blame.tcl:473
+msgid "Loading copy/move tracking annotations..."
+msgstr "Загрузка аннотации копирований/переименований..."
+
+#: lib/blame.tcl:493
+msgid "lines annotated"
+msgstr "строк прокомментировано"
+
+#: lib/blame.tcl:674
+msgid "Loading original location annotations..."
+msgstr "Загрузка аннотаций первоначального положения объекта..."
+
+#: lib/blame.tcl:677
+msgid "Annotation complete."
+msgstr "Аннотация завершена."
+
+#: lib/blame.tcl:731
+msgid "Loading annotation..."
+msgstr "Загрузка аннотации..."
+
+#: lib/blame.tcl:787
+msgid "Author:"
+msgstr "Автор:"
+
+#: lib/blame.tcl:791
+msgid "Committer:"
+msgstr "Сохранил:"
+
+#: lib/blame.tcl:796
+msgid "Original File:"
+msgstr "Исходный файл:"
+
+#: lib/blame.tcl:910
+msgid "Originally By:"
+msgstr "Источник:"
+
+#: lib/blame.tcl:916
+msgid "In File:"
+msgstr "Файл:"
+
+#: lib/blame.tcl:921
+msgid "Copied Or Moved Here By:"
+msgstr "Скопировано/перемещено в:"
+
+#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
+msgid "Checkout Branch"
+msgstr "Перейти на ветвь"
+
+#: lib/branch_checkout.tcl:23
+msgid "Checkout"
+msgstr "Перейти"
+
+#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
+#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
+#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
+#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
+msgid "Cancel"
+msgstr "Отменить"
+
+#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
+msgid "Revision"
+msgstr "Версия"
+
+#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
+msgid "Options"
+msgstr "Настройки"
+
+#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
+msgid "Fetch Tracking Branch"
+msgstr "Получить изменения из внешней ветви"
+
+#: lib/branch_checkout.tcl:44
+msgid "Detach From Local Branch"
+msgstr "Отсоединить от локальной ветви"
+
+#: lib/branch_create.tcl:22
+msgid "Create Branch"
+msgstr "Создание ветви"
+
+#: lib/branch_create.tcl:27
+msgid "Create New Branch"
+msgstr "Создать новую ветвь"
+
+#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
+msgid "Create"
+msgstr "Создать"
+
+#: lib/branch_create.tcl:40
+msgid "Branch Name"
+msgstr "Название ветви"
+
+#: lib/branch_create.tcl:43
+msgid "Name:"
+msgstr "Название:"
+
+#: lib/branch_create.tcl:58
+msgid "Match Tracking Branch Name"
+msgstr "Взять из имен ветвей слежения"
+
+#: lib/branch_create.tcl:66
+msgid "Starting Revision"
+msgstr "Начальная версия"
+
+#: lib/branch_create.tcl:72
+msgid "Update Existing Branch:"
+msgstr "Обновить имеющуюся ветвь:"
+
+#: lib/branch_create.tcl:75
+msgid "No"
+msgstr "Нет"
+
+#: lib/branch_create.tcl:80
+msgid "Fast Forward Only"
+msgstr "Только Fast Forward"
+
+#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
+msgid "Reset"
+msgstr "Сброс"
+
+#: lib/branch_create.tcl:97
+msgid "Checkout After Creation"
+msgstr "После создания сделать текущей"
+
+#: lib/branch_create.tcl:131
+msgid "Please select a tracking branch."
+msgstr "Укажите ветвь слежения."
+
+#: lib/branch_create.tcl:140
+#, tcl-format
+msgid "Tracking branch %s is not a branch in the remote repository."
+msgstr "Ветвь слежения %s не является ветвью во внешнем репозитории."
+
+#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
+msgid "Please supply a branch name."
+msgstr "Укажите название ветви."
+
+#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
+#, tcl-format
+msgid "'%s' is not an acceptable branch name."
+msgstr "Недопустимое название ветви '%s'."
+
+#: lib/branch_delete.tcl:15
+msgid "Delete Branch"
+msgstr "Удаление ветви"
+
+#: lib/branch_delete.tcl:20
+msgid "Delete Local Branch"
+msgstr "Удалить локальную ветвь"
+
+#: lib/branch_delete.tcl:37
+msgid "Local Branches"
+msgstr "Локальные ветви"
+
+#: lib/branch_delete.tcl:52
+msgid "Delete Only If Merged Into"
+msgstr "Удалить только в случае, если было объединение с"
+
+#: lib/branch_delete.tcl:54
+msgid "Always (Do not perform merge test.)"
+msgstr "Всегда (не выполнять проверку на объединение)"
+
+#: lib/branch_delete.tcl:103
+#, tcl-format
+msgid "The following branches are not completely merged into %s:"
+msgstr "Следующие ветви объединены с %s не полностью:"
+
+#: lib/branch_delete.tcl:115
+msgid ""
+"Recovering deleted branches is difficult. \n"
+"\n"
+" Delete the selected branches?"
+msgstr ""
+"Восстанавливать удаленные ветви сложно. \n"
+"\n"
+" Удалить выбранные ветви?"
+
+#: lib/branch_delete.tcl:141
+#, tcl-format
+msgid ""
+"Failed to delete branches:\n"
+"%s"
+msgstr ""
+"Не удалось удалить ветви:\n"
+"%s"
+
+#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
+msgid "Rename Branch"
+msgstr "Переименование ветви"
+
+#: lib/branch_rename.tcl:26
+msgid "Rename"
+msgstr "Переименовать"
+
+#: lib/branch_rename.tcl:36
+msgid "Branch:"
+msgstr "Ветвь:"
+
+#: lib/branch_rename.tcl:39
+msgid "New Name:"
+msgstr "Новое название:"
+
+#: lib/branch_rename.tcl:75
+msgid "Please select a branch to rename."
+msgstr "Укажите ветвь для переименования."
+
+#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
+#, tcl-format
+msgid "Branch '%s' already exists."
+msgstr "Ветвь '%s' уже существует."
+
+#: lib/branch_rename.tcl:117
+#, tcl-format
+msgid "Failed to rename '%s'."
+msgstr "Не удалось переименовать '%s'. "
+
+#: lib/browser.tcl:17
+msgid "Starting..."
+msgstr "Запуск..."
+
+#: lib/browser.tcl:26
+msgid "File Browser"
+msgstr "Просмотр списка файлов"
+
+#: lib/browser.tcl:125 lib/browser.tcl:142
+#, tcl-format
+msgid "Loading %s..."
+msgstr "Загрузка %s..."
+
+#: lib/browser.tcl:186
+msgid "[Up To Parent]"
+msgstr "[На уровень выше]"
+
+#: lib/browser.tcl:266 lib/browser.tcl:272
+msgid "Browse Branch Files"
+msgstr "Показать файлы ветви"
+
+#: lib/browser.tcl:277 lib/choose_repository.tcl:391
+#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
+#: lib/choose_repository.tcl:989
+msgid "Browse"
+msgstr "Показать"
+
+#: lib/checkout_op.tcl:79
+#, tcl-format
+msgid "Fetching %s from %s"
+msgstr "Получение %s из %s "
+
+#: lib/checkout_op.tcl:127
+#, tcl-format
+msgid "fatal: Cannot resolve %s"
+msgstr "критическая ошибка: невозможно разрешить %s"
+
+#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
+msgid "Close"
+msgstr "Закрыть"
+
+#: lib/checkout_op.tcl:169
+#, tcl-format
+msgid "Branch '%s' does not exist."
+msgstr "Ветвь '%s' не существует "
+
+#: lib/checkout_op.tcl:206
+#, tcl-format
+msgid ""
+"Branch '%s' already exists.\n"
+"\n"
+"It cannot fast-forward to %s.\n"
+"A merge is required."
+msgstr ""
+"Ветвь '%s' уже существует.\n"
+"\n"
+"Она не может быть прокручена(fast-forward) к %s.\n"
+"Требуется объединение."
+
+#: lib/checkout_op.tcl:220
+#, tcl-format
+msgid "Merge strategy '%s' not supported."
+msgstr "Стратегия объединения '%s' не поддерживается."
+
+#: lib/checkout_op.tcl:239
+#, tcl-format
+msgid "Failed to update '%s'."
+msgstr "Не удалось обновить '%s'."
+
+#: lib/checkout_op.tcl:251
+msgid "Staging area (index) is already locked."
+msgstr "Рабочая область заблокирована другим процессом."
+
+#: lib/checkout_op.tcl:266
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before the current branch can be changed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"Последнее прочитанное состояние репозитория не соответствует текущему.\n"
+"\n"
+"С момента последней проверки репозиторий был изменен другой программой Git. "
+"Необходимо перечитать репозиторий, прежде чем изменять текущую ветвь.\n"
+"\n"
+"Это будет сделано сейчас автоматически.\n"
+
+#: lib/checkout_op.tcl:322
+#, tcl-format
+msgid "Updating working directory to '%s'..."
+msgstr "Обновление рабочего каталога из '%s'..."
+
+#: lib/checkout_op.tcl:353
+#, tcl-format
+msgid "Aborted checkout of '%s' (file level merging is required)."
+msgstr "Прерван переход на '%s' (требуется объединение на уровне файлов)"
+
+#: lib/checkout_op.tcl:354
+msgid "File level merge required."
+msgstr "Требуется объединение на уровне файлов."
+
+#: lib/checkout_op.tcl:358
+#, tcl-format
+msgid "Staying on branch '%s'."
+msgstr "Ветвь '%s' остается текущей."
+
+#: lib/checkout_op.tcl:429
+msgid ""
+"You are no longer on a local branch.\n"
+"\n"
+"If you wanted to be on a branch, create one now starting from 'This Detached "
+"Checkout'."
+msgstr ""
+"Вы находитесь не в локальной ветви.\n"
+"\n"
+"Если вы хотите снова вернуться к какой-нибудь ветви, создайте ее сейчас, "
+"начиная с 'Текущего отсоединенного состояния'."
+
+#: lib/checkout_op.tcl:446
+#, tcl-format
+msgid "Checked out '%s'."
+msgstr "Ветвь '%s' сделана текущей."
+
+#: lib/checkout_op.tcl:478
+#, tcl-format
+msgid "Resetting '%s' to '%s' will lose the following commits:"
+msgstr "Сброс '%s' в '%s' приведет к потере следующих сохраненных состояний: "
+
+#: lib/checkout_op.tcl:500
+msgid "Recovering lost commits may not be easy."
+msgstr "Восстановить потерянные сохраненные состояния будет сложно."
+
+#: lib/checkout_op.tcl:505
+#, tcl-format
+msgid "Reset '%s'?"
+msgstr "Сбросить '%s'?"
+
+#: lib/checkout_op.tcl:510 lib/merge.tcl:164
+msgid "Visualize"
+msgstr "Наглядно"
+
+#: lib/checkout_op.tcl:578
+#, tcl-format
+msgid ""
+"Failed to set current branch.\n"
+"\n"
+"This working directory is only partially switched.  We successfully updated "
+"your files, but failed to update an internal Git file.\n"
+"\n"
+"This should not have occurred.  %s will now close and give up."
+msgstr ""
+"Не удалось установить текущую ветвь.\n"
+"\n"
+"Ваш рабочий каталог обновлен только частично. Были обновлены все файлы кроме "
+"служебных файлов Git. \n"
+"\n"
+"Этого не должно было произойти. %s завершается."
+
+#: lib/choose_font.tcl:39
+msgid "Select"
+msgstr "Выбрать"
+
+#: lib/choose_font.tcl:53
+msgid "Font Family"
+msgstr "Шрифт"
+
+#: lib/choose_font.tcl:73
+msgid "Font Size"
+msgstr "Размер шрифта"
+
+#: lib/choose_font.tcl:90
+msgid "Font Example"
+msgstr "Пример текста"
+
+#: lib/choose_font.tcl:101
+msgid ""
+"This is example text.\n"
+"If you like this text, it can be your font."
+msgstr ""
+"Это пример текста.\n"
+"Если Вам нравится этот текст, это может быть Ваш шрифт."
+
+#: lib/choose_repository.tcl:27
+msgid "Git Gui"
+msgstr ""
+
+#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
+msgid "Create New Repository"
+msgstr "Создать новый репозиторий"
+
+#: lib/choose_repository.tcl:86
+msgid "New..."
+msgstr "Новый..."
+
+#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
+msgid "Clone Existing Repository"
+msgstr "Склонировать существующий репозиторий"
+
+#: lib/choose_repository.tcl:99
+msgid "Clone..."
+msgstr "Склонировать..."
+
+#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
+msgid "Open Existing Repository"
+msgstr "Выбрать существующий репозиторий"
+
+#: lib/choose_repository.tcl:112
+msgid "Open..."
+msgstr "Открыть..."
+
+#: lib/choose_repository.tcl:125
+msgid "Recent Repositories"
+msgstr "Недавние репозитории"
+
+#: lib/choose_repository.tcl:131
+msgid "Open Recent Repository:"
+msgstr "Открыть последний репозиторий"
+
+#: lib/choose_repository.tcl:294
+#, tcl-format
+msgid "Location %s already exists."
+msgstr "Путь '%s' уже существует."
+
+#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
+#: lib/choose_repository.tcl:314
+#, tcl-format
+msgid "Failed to create repository %s:"
+msgstr "Не удалось создать репозиторий %s:"
+
+#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
+msgid "Directory:"
+msgstr "Каталог:"
+
+#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
+#: lib/choose_repository.tcl:1013
+msgid "Git Repository"
+msgstr "Репозиторий"
+
+#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
+#, tcl-format
+msgid "Directory %s already exists."
+msgstr "Каталог '%s' уже существует."
+
+#: lib/choose_repository.tcl:442
+#, tcl-format
+msgid "File %s already exists."
+msgstr "Файл '%s' уже существует."
+
+#: lib/choose_repository.tcl:463
+msgid "Clone"
+msgstr "Склонировать"
+
+#: lib/choose_repository.tcl:476
+msgid "URL:"
+msgstr "Ссылка:"
+
+#: lib/choose_repository.tcl:496
+msgid "Clone Type:"
+msgstr "Тип клона:"
+
+#: lib/choose_repository.tcl:502
+msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
+msgstr "Стандартный (Быстрый, полуизбыточный, \"жесткие\" ссылки)"
+
+#: lib/choose_repository.tcl:508
+msgid "Full Copy (Slower, Redundant Backup)"
+msgstr "Полная копия (Медленный, создает резервную копию)"
+
+#: lib/choose_repository.tcl:514
+msgid "Shared (Fastest, Not Recommended, No Backup)"
+msgstr "Общий (Самый быстрый, не рекомендуется, без резервной копии)"
+
+#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
+#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
+#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
+#, tcl-format
+msgid "Not a Git repository: %s"
+msgstr "Каталог не является репозиторием: %s"
+
+#: lib/choose_repository.tcl:586
+msgid "Standard only available for local repository."
+msgstr "Стандартный клон возможен только для локального репозитория."
+
+#: lib/choose_repository.tcl:590
+msgid "Shared only available for local repository."
+msgstr "Общий клон возможен только для локального репозитория."
+
+#: lib/choose_repository.tcl:617
+msgid "Failed to configure origin"
+msgstr "Не могу сконфигурировать исходный репозиторий."
+
+#: lib/choose_repository.tcl:629
+msgid "Counting objects"
+msgstr "Считаю объекты"
+
+#: lib/choose_repository.tcl:630
+msgid "buckets"
+msgstr ""
+
+#: lib/choose_repository.tcl:654
+#, tcl-format
+msgid "Unable to copy objects/info/alternates: %s"
+msgstr "Не могу скопировать objects/info/alternates: %s"
+
+#: lib/choose_repository.tcl:690
+#, tcl-format
+msgid "Nothing to clone from %s."
+msgstr "Нечего клонировать с %s."
+
+#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
+#: lib/choose_repository.tcl:918
+msgid "The 'master' branch has not been initialized."
+msgstr "Не инициализирована ветвь 'master'."
+
+#: lib/choose_repository.tcl:705
+msgid "Hardlinks are unavailable.  Falling back to copying."
+msgstr "\"Жесткие ссылки\" не доступны. Буду использовать копирование."
+
+#: lib/choose_repository.tcl:717
+#, tcl-format
+msgid "Cloning from %s"
+msgstr "Клонирование %s"
+
+#: lib/choose_repository.tcl:748
+msgid "Copying objects"
+msgstr "Копирование objects"
+
+#: lib/choose_repository.tcl:749
+msgid "KiB"
+msgstr "КБ"
+
+#: lib/choose_repository.tcl:773
+#, tcl-format
+msgid "Unable to copy object: %s"
+msgstr "Не могу скопировать объект: %s"
+
+#: lib/choose_repository.tcl:783
+msgid "Linking objects"
+msgstr "Создание ссылок на objects"
+
+#: lib/choose_repository.tcl:784
+msgid "objects"
+msgstr "объекты"
+
+#: lib/choose_repository.tcl:792
+#, tcl-format
+msgid "Unable to hardlink object: %s"
+msgstr "Не могу \"жестко связать\" объект: %s"
+
+#: lib/choose_repository.tcl:847
+msgid "Cannot fetch branches and objects.  See console output for details."
+msgstr ""
+"Не могу получить ветви и объекты. Дополнительная информация на консоли."
+
+#: lib/choose_repository.tcl:858
+msgid "Cannot fetch tags.  See console output for details."
+msgstr "Не могу получить метки. Дополнительная информация на консоли."
+
+#: lib/choose_repository.tcl:882
+msgid "Cannot determine HEAD.  See console output for details."
+msgstr "Не могу определить HEAD. Дополнительная информация на консоли."
+
+#: lib/choose_repository.tcl:891
+#, tcl-format
+msgid "Unable to cleanup %s"
+msgstr "Не могу очистить %s"
+
+#: lib/choose_repository.tcl:897
+msgid "Clone failed."
+msgstr "Клонирование не удалось."
+
+#: lib/choose_repository.tcl:904
+msgid "No default branch obtained."
+msgstr "Не было получено ветви по умолчанию."
+
+#: lib/choose_repository.tcl:915
+#, tcl-format
+msgid "Cannot resolve %s as a commit."
+msgstr "Не могу распознать %s как состояние."
+
+#: lib/choose_repository.tcl:927
+msgid "Creating working directory"
+msgstr "Создаю рабочий каталог"
+
+#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
+#: lib/index.tcl:193
+msgid "files"
+msgstr "файлов"
+
+#: lib/choose_repository.tcl:957
+msgid "Initial file checkout failed."
+msgstr "Не удалось получить начальное состояние файлов репозитория."
+
+#: lib/choose_repository.tcl:973
+msgid "Open"
+msgstr "Открыть"
+
+#: lib/choose_repository.tcl:983
+msgid "Repository:"
+msgstr "Репозиторий:"
+
+#: lib/choose_repository.tcl:1033
+#, tcl-format
+msgid "Failed to open repository %s:"
+msgstr "Не удалось открыть репозиторий %s:"
+
+#: lib/choose_rev.tcl:53
+msgid "This Detached Checkout"
+msgstr "Текущее отсоединенное состояние"
+
+#: lib/choose_rev.tcl:60
+msgid "Revision Expression:"
+msgstr "Выражение для определения версии:"
+
+#: lib/choose_rev.tcl:74
+msgid "Local Branch"
+msgstr "Локальная ветвь:"
+
+#: lib/choose_rev.tcl:79
+msgid "Tracking Branch"
+msgstr "Ветвь слежения"
+
+#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
+msgid "Tag"
+msgstr "Таг"
+
+#: lib/choose_rev.tcl:317
+#, tcl-format
+msgid "Invalid revision: %s"
+msgstr "Неверная версия: %s"
+
+#: lib/choose_rev.tcl:338
+msgid "No revision selected."
+msgstr "Версия не указана."
+
+#: lib/choose_rev.tcl:346
+msgid "Revision expression is empty."
+msgstr "Пустое выражение для определения версии."
+
+#: lib/choose_rev.tcl:530
+msgid "Updated"
+msgstr "Обновлено"
+
+#: lib/choose_rev.tcl:558
+msgid "URL"
+msgstr "Ссылка"
+
+#: lib/commit.tcl:9
+msgid ""
+"There is nothing to amend.\n"
+"\n"
+"You are about to create the initial commit.  There is no commit before this "
+"to amend.\n"
+msgstr ""
+"Отсутствует состояние для исправления.\n"
+"\n"
+"Вы создаете первое состояние в репозитории, здесь еще нечего исправлять.\n"
+
+#: lib/commit.tcl:18
+msgid ""
+"Cannot amend while merging.\n"
+"\n"
+"You are currently in the middle of a merge that has not been fully "
+"completed.  You cannot amend the prior commit unless you first abort the "
+"current merge activity.\n"
+msgstr ""
+"Невозможно исправить состояние во время объединения.\n"
+"\n"
+"Текущее объединение не завершено. Невозможно исправить предыдущее "
+"сохраненное состояние не прерывая текущее объединение.\n"
+
+#: lib/commit.tcl:49
+msgid "Error loading commit data for amend:"
+msgstr "Ошибка при загрузке данных для исправления сохраненного состояния:"
+
+#: lib/commit.tcl:76
+msgid "Unable to obtain your identity:"
+msgstr "Невозможно получить информацию об авторстве:"
+
+#: lib/commit.tcl:81
+msgid "Invalid GIT_COMMITTER_IDENT:"
+msgstr "Неверный GIT_COMMITTER_IDENT:"
+
+#: lib/commit.tcl:133
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before another commit can be created.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"Последнее прочитанное состояние репозитория не соответствует текущему.\n"
+"\n"
+"С момента последней проверки репозиторий был изменен другой программой Git. "
+"Необходимо перечитать репозиторий, прежде чем изменять текущую ветвь. \n"
+"\n"
+"Это будет сделано сейчас автоматически.\n"
+
+#: lib/commit.tcl:154
+#, tcl-format
+msgid ""
+"Unmerged files cannot be committed.\n"
+"\n"
+"File %s has merge conflicts.  You must resolve them and stage the file "
+"before committing.\n"
+msgstr ""
+"Нельзя сохранить необъединенные файлы.\n"
+"\n"
+"Для файла %s возник конфликт объединения. Разрешите конфликт и добавьте к "
+"подготовленным файлам перед сохранением.\n"
+
+#: lib/commit.tcl:162
+#, tcl-format
+msgid ""
+"Unknown file state %s detected.\n"
+"\n"
+"File %s cannot be committed by this program.\n"
+msgstr ""
+"Обнаружено неизвестное состояние файла %s.\n"
+"\n"
+"Файл %s не может быть сохранен данной программой.\n"
+
+#: lib/commit.tcl:170
+msgid ""
+"No changes to commit.\n"
+"\n"
+"You must stage at least 1 file before you can commit.\n"
+msgstr ""
+"Отсутствуют изменения для сохранения.\n"
+"\n"
+"Подготовьте хотя бы один файл до создания сохраненного состояния.\n"
+
+#: lib/commit.tcl:183
+msgid ""
+"Please supply a commit message.\n"
+"\n"
+"A good commit message has the following format:\n"
+"\n"
+"- First line: Describe in one sentance what you did.\n"
+"- Second line: Blank\n"
+"- Remaining lines: Describe why this change is good.\n"
+msgstr ""
+"Напишите комментарий к сохраненному состоянию.\n"
+"\n"
+"Рекомендуется следующий формат комментария:\n"
+"\n"
+"- первая строка: краткое описание сделанных изменений.\n"
+"- вторая строка пустая\n"
+"- оставшиеся строки: опишите, что дают ваши изменения.\n"
+
+#: lib/commit.tcl:257
+msgid "write-tree failed:"
+msgstr "Программа write-tree завершилась с ошибкой:"
+
+#: lib/commit.tcl:275
+#, tcl-format
+msgid "Commit %s appears to be corrupt"
+msgstr "Состояние %s выглядит поврежденным"
+
+#: lib/commit.tcl:279
+msgid ""
+"No changes to commit.\n"
+"\n"
+"No files were modified by this commit and it was not a merge commit.\n"
+"\n"
+"A rescan will be automatically started now.\n"
+msgstr ""
+"Отсутствуют изменения для сохранения.\n"
+"\n"
+"Ни один файл не был изменен и не было объединения.\n"
+"\n"
+"Сейчас автоматически запустится перечитывание репозитория.\n"
+
+#: lib/commit.tcl:286
+msgid "No changes to commit."
+msgstr "Отуствуют измения для сохранения."
+
+#: lib/commit.tcl:303
+#, tcl-format
+msgid "warning: Tcl does not support encoding '%s'."
+msgstr "предупреждение: Tcl не поддерживает кодировку '%s'."
+
+#: lib/commit.tcl:317
+msgid "commit-tree failed:"
+msgstr "Программа commit-tree завершилась с ошибкой:"
+
+#: lib/commit.tcl:339
+msgid "update-ref failed:"
+msgstr "Программа update-ref завершилась с ошибкой:"
+
+#: lib/commit.tcl:430
+#, tcl-format
+msgid "Created commit %s: %s"
+msgstr "Создано состояние %s: %s "
+
+#: lib/console.tcl:57
+msgid "Working... please wait..."
+msgstr "В процессе... пожалуйста, ждите..."
+
+#: lib/console.tcl:183
+msgid "Success"
+msgstr "Процесс успешно завершен"
+
+#: lib/console.tcl:196
+msgid "Error: Command Failed"
+msgstr "Ошибка: не удалось выполнить команду"
+
+#: lib/database.tcl:43
+msgid "Number of loose objects"
+msgstr "Количество несвязанных объектов"
+
+#: lib/database.tcl:44
+msgid "Disk space used by loose objects"
+msgstr "Объем дискового пространства, занятый несвязанными объектами"
+
+#: lib/database.tcl:45
+msgid "Number of packed objects"
+msgstr "Количество упакованных объектов"
+
+#: lib/database.tcl:46
+msgid "Number of packs"
+msgstr "Количество pack-файлов"
+
+#: lib/database.tcl:47
+msgid "Disk space used by packed objects"
+msgstr "Объем дискового пространства, занятый упакованными объектами"
+
+#: lib/database.tcl:48
+msgid "Packed objects waiting for pruning"
+msgstr "Несвязанные объекты, которые можно удалить"
+
+#: lib/database.tcl:49
+msgid "Garbage files"
+msgstr "Мусор"
+
+#: lib/database.tcl:72
+msgid "Compressing the object database"
+msgstr "Сжатие базы объектов"
+
+#: lib/database.tcl:83
+msgid "Verifying the object database with fsck-objects"
+msgstr "Проверка базы объектов при помощи fsck"
+
+#: lib/database.tcl:108
+#, tcl-format
+msgid ""
+"This repository currently has approximately %i loose objects.\n"
+"\n"
+"To maintain optimal performance it is strongly recommended that you compress "
+"the database when more than %i loose objects exist.\n"
+"\n"
+"Compress the database now?"
+msgstr ""
+"Этот репозиторий сейчас содержит примерно %i свободных объектов\n"
+"\n"
+"Для лучшей производительности рекомендуется сжать базу данных, когда есть "
+"более %i несвязанных объектов.\n"
+"\n"
+"Сжать базу данных сейчас?"
+
+#: lib/date.tcl:25
+#, tcl-format
+msgid "Invalid date from Git: %s"
+msgstr "Неправильная дата в репозитории: %s"
+
+#: lib/diff.tcl:42
+#, tcl-format
+msgid ""
+"No differences detected.\n"
+"\n"
+"%s has no changes.\n"
+"\n"
+"The modification date of this file was updated by another application, but "
+"the content within the file was not changed.\n"
+"\n"
+"A rescan will be automatically started to find other files which may have "
+"the same state."
+msgstr ""
+"Изменений не обнаружено.\n"
+"\n"
+"в %s отутствуют изменения.\n"
+"\n"
+"Дата изменения файла была обновлена другой программой, но содержимое файла "
+"осталось прежним.\n"
+"\n"
+"Сейчас будет запущено перечитывание репозитория, чтобы найти подобные файлы."
+
+#: lib/diff.tcl:81
+#, tcl-format
+msgid "Loading diff of %s..."
+msgstr "Загрузка изменений в %s..."
+
+#: lib/diff.tcl:114 lib/diff.tcl:184
+#, tcl-format
+msgid "Unable to display %s"
+msgstr "Не могу показать %s"
+
+#: lib/diff.tcl:115
+msgid "Error loading file:"
+msgstr "Ошибка загрузки файла:"
+
+#: lib/diff.tcl:122
+msgid "Git Repository (subproject)"
+msgstr "Репозиторий Git (подпроект)"
+
+#: lib/diff.tcl:134
+msgid "* Binary file (not showing content)."
+msgstr "* Двоичный файл (содержимое не показано)"
+
+#: lib/diff.tcl:185
+msgid "Error loading diff:"
+msgstr "Ошибка загрузки diff:"
+
+#: lib/diff.tcl:302
+msgid "Failed to unstage selected hunk."
+msgstr "Не удалось исключить выбранную часть."
+
+#: lib/diff.tcl:309
+msgid "Failed to stage selected hunk."
+msgstr "Не удалось подготовить к сохранению выбранную часть."
+
+#: lib/error.tcl:12 lib/error.tcl:102
+msgid "error"
+msgstr "ошибка"
+
+#: lib/error.tcl:28
+msgid "warning"
+msgstr "предупреждение"
+
+#: lib/error.tcl:81
+msgid "You must correct the above errors before committing."
+msgstr "Прежде чем сохранить, исправьте вышеуказанные ошибки."
+
+#: lib/index.tcl:6
+msgid "Unable to unlock the index."
+msgstr "Не удалось разблокировать индекс"
+
+#: lib/index.tcl:15
+msgid "Index Error"
+msgstr "Ошибка индекса"
+
+#: lib/index.tcl:21
+msgid ""
+"Updating the Git index failed.  A rescan will be automatically started to "
+"resynchronize git-gui."
+msgstr ""
+"Не удалось обновить индекс Git. Состояние репозитория будет"
+"перечитано автоматически."
+
+#: lib/index.tcl:27
+msgid "Continue"
+msgstr "Продолжить"
+
+#: lib/index.tcl:31
+msgid "Unlock Index"
+msgstr "Разблокировать индекс"
+
+#: lib/index.tcl:282
+#, tcl-format
+msgid "Unstaging %s from commit"
+msgstr "Удаление %s из подготовленного"
+
+#: lib/index.tcl:326
+#, tcl-format
+msgid "Adding %s"
+msgstr "Добавление %s..."
+
+#: lib/index.tcl:381
+#, tcl-format
+msgid "Revert changes in file %s?"
+msgstr "Отменить изменения в файле %s?"
+
+#: lib/index.tcl:383
+#, tcl-format
+msgid "Revert changes in these %i files?"
+msgstr "Отменить изменения в %i файле(-ах)?"
+
+#: lib/index.tcl:389
+msgid "Any unstaged changes will be permanently lost by the revert."
+msgstr ""
+"Любые изменения, не подготовленные к сохранению, будут потеряны при данной "
+"операции."
+
+#: lib/index.tcl:392
+msgid "Do Nothing"
+msgstr "Ничего не делать"
+
+#: lib/merge.tcl:13
+msgid ""
+"Cannot merge while amending.\n"
+"\n"
+"You must finish amending this commit before starting any type of merge.\n"
+msgstr ""
+"Невозможно выполнить объединение во время исправления.\n"
+"\n"
+"Завершите исправление данного состояния перед выполнением операции "
+"объединения.\n"
+
+#: lib/merge.tcl:27
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before a merge can be performed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+"Последнее прочитанное состояние репозитория не соответствует текущему.\n"
+"\n"
+"С момента последней проверки репозиторий был изменен другой программой Git. "
+"Необходимо перечитать репозиторий, прежде чем изменять текущую ветвь.\n"
+"\n"
+"Это будет сделано сейчас автоматически.\n"
+
+#: lib/merge.tcl:44
+#, tcl-format
+msgid ""
+"You are in the middle of a conflicted merge.\n"
+"\n"
+"File %s has merge conflicts.\n"
+"\n"
+"You must resolve them, stage the file, and commit to complete the current "
+"merge.  Only then can you begin another merge.\n"
+msgstr ""
+"Предыдущее объединение не завершено из-за конфликта.\n"
+"\n"
+"Для файла %s возник конфликт объединения.\n"
+"\n"
+"Разрешите конфликт, подготовьте файл и сохраните. Только после этого можно "
+"начать следующее объединение.\n"
+
+#: lib/merge.tcl:54
+#, tcl-format
+msgid ""
+"You are in the middle of a change.\n"
+"\n"
+"File %s is modified.\n"
+"\n"
+"You should complete the current commit before starting a merge.  Doing so "
+"will help you abort a failed merge, should the need arise.\n"
+msgstr ""
+"Изменения не сохранены.\n"
+"\n"
+"Файл %s изменен.\n"
+"\n"
+"Подготовьте и сохраните измения перед началом объединения. В случае "
+"необходимости это позволит прервать операцию объединения.\n"
+
+#: lib/merge.tcl:106
+#, tcl-format
+msgid "%s of %s"
+msgstr "%s из %s"
+
+#: lib/merge.tcl:119
+#, tcl-format
+msgid "Merging %s and %s"
+msgstr "Объединение %s и %s"
+
+#: lib/merge.tcl:131
+msgid "Merge completed successfully."
+msgstr "Объединение успешно завершено."
+
+#: lib/merge.tcl:133
+msgid "Merge failed.  Conflict resolution is required."
+msgstr "Не удалось завершить объединение. Требуется разрешение конфликта."
+
+#: lib/merge.tcl:158
+#, tcl-format
+msgid "Merge Into %s"
+msgstr "Объединить с %s"
+
+#: lib/merge.tcl:177
+msgid "Revision To Merge"
+msgstr "Версия для объединения"
+
+#: lib/merge.tcl:212
+msgid ""
+"Cannot abort while amending.\n"
+"\n"
+"You must finish amending this commit.\n"
+msgstr ""
+"Невозможно прервать исправление.\n"
+"\n"
+"Завершите текущее исправление сохраненного состояния.\n"
+
+#: lib/merge.tcl:222
+msgid ""
+"Abort merge?\n"
+"\n"
+"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with aborting the current merge?"
+msgstr ""
+"Прервать объединение?\n"
+"\n"
+"Прерывание объединения приведет к потере *ВСЕХ* несохраненных изменений.\n"
+"\n"
+"Продолжить?"
+
+#: lib/merge.tcl:228
+msgid ""
+"Reset changes?\n"
+"\n"
+"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with resetting the current changes?"
+msgstr ""
+"Прервать объединение?\n"
+"\n"
+"Прерывание объединения приведет к потере *ВСЕХ* несохраненных изменений.\n"
+"\n"
+"Продолжить?"
+
+#: lib/merge.tcl:239
+msgid "Aborting"
+msgstr "Прерываю"
+
+#: lib/merge.tcl:266
+msgid "Abort failed."
+msgstr "Прервать не удалось."
+
+#: lib/merge.tcl:268
+msgid "Abort completed.  Ready."
+msgstr "Прервано."
+
+#: lib/option.tcl:82
+msgid "Restore Defaults"
+msgstr "Восстановить настройки по умолчанию"
+
+#: lib/option.tcl:86
+msgid "Save"
+msgstr "Сохранить"
+
+#: lib/option.tcl:96
+#, tcl-format
+msgid "%s Repository"
+msgstr "для репозитория %s"
+
+#: lib/option.tcl:97
+msgid "Global (All Repositories)"
+msgstr "Общие (для всех репозиториев)"
+
+#: lib/option.tcl:103
+msgid "User Name"
+msgstr "Имя пользователя"
+
+#: lib/option.tcl:104
+msgid "Email Address"
+msgstr "Адес электронной почты"
+
+#: lib/option.tcl:106
+msgid "Summarize Merge Commits"
+msgstr "Суммарный комментарий при объединении"
+
+#: lib/option.tcl:107
+msgid "Merge Verbosity"
+msgstr "Уровень детальности сообщений при объединении"
+
+#: lib/option.tcl:108
+msgid "Show Diffstat After Merge"
+msgstr "Показать отчет об изменениях после объединения"
+
+#: lib/option.tcl:110
+msgid "Trust File Modification Timestamps"
+msgstr "Доверять времени модификации файла"
+
+#: lib/option.tcl:111
+msgid "Prune Tracking Branches During Fetch"
+msgstr "Чистка ветвей слежения при получении изменений"
+
+#: lib/option.tcl:112
+msgid "Match Tracking Branches"
+msgstr "Имя новой ветви взять из имен ветвей слежения"
+
+#: lib/option.tcl:113
+msgid "Number of Diff Context Lines"
+msgstr "Число строк в контексте diff"
+
+#: lib/option.tcl:114
+msgid "New Branch Name Template"
+msgstr "Шаблон для имени новой ветви"
+
+#: lib/option.tcl:176
+msgid "Change Font"
+msgstr "Изменить шрифт"
+
+#: lib/option.tcl:180
+#, tcl-format
+msgid "Choose %s"
+msgstr "Выберите %s"
+
+# carbon copy
+#: lib/option.tcl:186
+msgid "pt."
+msgstr ""
+
+#: lib/option.tcl:200
+msgid "Preferences"
+msgstr "Настройки"
+
+#: lib/option.tcl:235
+msgid "Failed to completely save options:"
+msgstr "Не удалось полностью сохранить настройки:"
+
+#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
+msgid "Delete Remote Branch"
+msgstr "Удалить внешнюю ветвь"
+
+#: lib/remote_branch_delete.tcl:47
+msgid "From Repository"
+msgstr "Из репозитория"
+
+#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
+msgid "Remote:"
+msgstr "внешний:"
+
+#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
+msgid "Arbitrary URL:"
+msgstr "по указанному URL:"
+
+#: lib/remote_branch_delete.tcl:84
+msgid "Branches"
+msgstr "Ветви"
+
+#: lib/remote_branch_delete.tcl:109
+msgid "Delete Only If"
+msgstr "Удалить только в случае, если"
+
+#: lib/remote_branch_delete.tcl:111
+msgid "Merged Into:"
+msgstr "Объединено с:"
+
+#: lib/remote_branch_delete.tcl:119
+msgid "Always (Do not perform merge checks)"
+msgstr "Всегда (не выполнять проверку объединений)"
+
+#: lib/remote_branch_delete.tcl:152
+msgid "A branch is required for 'Merged Into'."
+msgstr "Для опции 'Объединено с' требуется указать ветвь."
+
+#: lib/remote_branch_delete.tcl:184
+#, tcl-format
+msgid ""
+"The following branches are not completely merged into %s:\n"
+"\n"
+" - %s"
+msgstr ""
+"Следующие ветви объединены с %s не полностью:\n"
+" - %s"
+
+#: lib/remote_branch_delete.tcl:189
+#, tcl-format
+msgid ""
+"One or more of the merge tests failed because you have not fetched the "
+"necessary commits.  Try fetching from %s first."
+msgstr ""
+"Один или несколько тестов на объединение не прошли, потому что Вы не "
+"получили необходимые состояния. Попытайтесь получить их из %s."
+
+#: lib/remote_branch_delete.tcl:207
+msgid "Please select one or more branches to delete."
+msgstr "Укажите одну или несколько ветвей для удаления."
+
+#: lib/remote_branch_delete.tcl:216
+msgid ""
+"Recovering deleted branches is difficult.\n"
+"\n"
+"Delete the selected branches?"
+msgstr ""
+"Восстановить удаленные ветви сложно.\n"
+"\n"
+"Продолжить?"
+
+#: lib/remote_branch_delete.tcl:226
+#, tcl-format
+msgid "Deleting branches from %s"
+msgstr "Удаление ветвей из %s"
+
+#: lib/remote_branch_delete.tcl:286
+msgid "No repository selected."
+msgstr "Не указан репозиторий."
+
+#: lib/remote_branch_delete.tcl:291
+#, tcl-format
+msgid "Scanning %s..."
+msgstr "Перечитывание %s... "
+
+#: lib/remote.tcl:165
+msgid "Prune from"
+msgstr "Чистка"
+
+#: lib/remote.tcl:170
+msgid "Fetch from"
+msgstr "Получение из"
+
+#: lib/remote.tcl:213
+msgid "Push to"
+msgstr "Отправить"
+
+#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
+msgid "Cannot write shortcut:"
+msgstr "Невозможно записать ссылку:"
+
+#: lib/shortcut.tcl:136
+msgid "Cannot write icon:"
+msgstr "Невозможно записать значок:"
+
+#: lib/status_bar.tcl:83
+#, tcl-format
+msgid "%s ... %*i of %*i %s (%3i%%)"
+msgstr "%s ... %*i из %*i %s (%3i%%)"
+
+#: lib/transport.tcl:6
+#, tcl-format
+msgid "fetch %s"
+msgstr "получение %s"
+
+#: lib/transport.tcl:7
+#, tcl-format
+msgid "Fetching new changes from %s"
+msgstr "Получение изменений из %s "
+
+# carbon copy
+#: lib/transport.tcl:18
+#, tcl-format
+msgid "remote prune %s"
+msgstr "чистка внешнего %s"
+
+#: lib/transport.tcl:19
+#, tcl-format
+msgid "Pruning tracking branches deleted from %s"
+msgstr "Чистка ветвей слежения, удаленных из %s"
+
+#: lib/transport.tcl:25 lib/transport.tcl:71
+#, tcl-format
+msgid "push %s"
+msgstr "отправить %s"
+
+#: lib/transport.tcl:26
+#, tcl-format
+msgid "Pushing changes to %s"
+msgstr "Отправка изменений в %s "
+
+#: lib/transport.tcl:72
+#, tcl-format
+msgid "Pushing %s %s to %s"
+msgstr "Отправка %s %s в %s"
+
+#: lib/transport.tcl:89
+msgid "Push Branches"
+msgstr "Отправить изменения в ветвях"
+
+#: lib/transport.tcl:103
+msgid "Source Branches"
+msgstr "Исходные ветви"
+
+#: lib/transport.tcl:120
+msgid "Destination Repository"
+msgstr "Репозиторий назначения"
+
+#: lib/transport.tcl:158
+msgid "Transfer Options"
+msgstr "Настройки отправки"
+
+#: lib/transport.tcl:160
+msgid "Force overwrite existing branch (may discard changes)"
+msgstr "Намеренно переписать существующую ветвь (возможна потеря изменений)"
+
+#: lib/transport.tcl:164
+msgid "Use thin pack (for slow network connections)"
+msgstr "Использовать thin pack (для медленных сетевых подключений)"
+
+#: lib/transport.tcl:168
+msgid "Include tags"
+msgstr "Передать таги"
+
+#~ msgid "Next >"
+#~ msgstr "Дальше >"
diff --git a/git-gui/po/zh_cn.po b/git-gui/po/zh_cn.po
new file mode 100644 (file)
index 0000000..621c947
--- /dev/null
@@ -0,0 +1,1769 @@
+# Translation of git-gui to Chinese
+# Copyright (C) 2007 Shawn Pearce
+# This file is distributed under the same license as the git-gui package.
+# Xudong Guan <xudong.guan@gmail.com>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: git-gui\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-10-10 04:04-0400\n"
+"PO-Revision-Date: 2007-07-21 01:23-0700\n"
+"Last-Translator: Xudong Guan <xudong.guan@gmail.com>\n"
+"Language-Team: Chinese\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: git-gui.sh:41 git-gui.sh:634 git-gui.sh:648 git-gui.sh:661 git-gui.sh:744
+#: git-gui.sh:763
+msgid "git-gui: fatal error"
+msgstr ""
+
+#: git-gui.sh:595
+#, tcl-format
+msgid "Invalid font specified in %s:"
+msgstr ""
+
+#: git-gui.sh:620
+msgid "Main Font"
+msgstr ""
+
+#: git-gui.sh:621
+msgid "Diff/Console Font"
+msgstr ""
+
+#: git-gui.sh:635
+msgid "Cannot find git in PATH."
+msgstr ""
+
+#: git-gui.sh:662
+msgid "Cannot parse Git version string:"
+msgstr ""
+
+#: git-gui.sh:680
+#, tcl-format
+msgid ""
+"Git version cannot be determined.\n"
+"\n"
+"%s claims it is version '%s'.\n"
+"\n"
+"%s requires at least Git 1.5.0 or later.\n"
+"\n"
+"Assume '%s' is version 1.5.0?\n"
+msgstr ""
+
+#: git-gui.sh:853
+msgid "Git directory not found:"
+msgstr ""
+
+#: git-gui.sh:860
+msgid "Cannot move to top of working directory:"
+msgstr ""
+
+#: git-gui.sh:867
+msgid "Cannot use funny .git directory:"
+msgstr ""
+
+#: git-gui.sh:872
+msgid "No working directory"
+msgstr ""
+
+#: git-gui.sh:1019
+msgid "Refreshing file status..."
+msgstr ""
+
+#: git-gui.sh:1084
+msgid "Scanning for modified files ..."
+msgstr ""
+
+#: git-gui.sh:1259 lib/browser.tcl:245
+#, fuzzy
+msgid "Ready."
+msgstr "重做"
+
+#: git-gui.sh:1525
+msgid "Unmodified"
+msgstr ""
+
+#: git-gui.sh:1527
+msgid "Modified, not staged"
+msgstr ""
+
+#: git-gui.sh:1528 git-gui.sh:1533
+#, fuzzy
+msgid "Staged for commit"
+msgstr "从本次提交移除"
+
+#: git-gui.sh:1529 git-gui.sh:1534
+#, fuzzy
+msgid "Portions staged for commit"
+msgstr "从本次提交移除"
+
+#: git-gui.sh:1530 git-gui.sh:1535
+msgid "Staged for commit, missing"
+msgstr ""
+
+#: git-gui.sh:1532
+msgid "Untracked, not staged"
+msgstr ""
+
+#: git-gui.sh:1537
+msgid "Missing"
+msgstr ""
+
+#: git-gui.sh:1538
+msgid "Staged for removal"
+msgstr ""
+
+#: git-gui.sh:1539
+msgid "Staged for removal, still present"
+msgstr ""
+
+#: git-gui.sh:1541 git-gui.sh:1542 git-gui.sh:1543 git-gui.sh:1544
+msgid "Requires merge resolution"
+msgstr ""
+
+#: git-gui.sh:1579
+msgid "Starting gitk... please wait..."
+msgstr ""
+
+#: git-gui.sh:1588
+#, tcl-format
+msgid ""
+"Unable to start gitk:\n"
+"\n"
+"%s does not exist"
+msgstr ""
+
+#: git-gui.sh:1788 lib/choose_repository.tcl:32
+msgid "Repository"
+msgstr "版本树"
+
+#: git-gui.sh:1789
+msgid "Edit"
+msgstr "编辑"
+
+#: git-gui.sh:1791 lib/choose_rev.tcl:560
+msgid "Branch"
+msgstr "分支"
+
+#: git-gui.sh:1794 lib/choose_rev.tcl:547
+#, fuzzy
+msgid "Commit@@noun"
+msgstr "提交"
+
+#: git-gui.sh:1797 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
+msgid "Merge"
+msgstr "合并"
+
+#: git-gui.sh:1798 lib/choose_rev.tcl:556
+#, fuzzy
+msgid "Remote"
+msgstr "改名..."
+
+#: git-gui.sh:1807
+msgid "Browse Current Branch's Files"
+msgstr "浏览当前分支文件"
+
+#: git-gui.sh:1811
+#, fuzzy
+msgid "Browse Branch Files..."
+msgstr "浏览当前分支文件"
+
+#: git-gui.sh:1816
+msgid "Visualize Current Branch's History"
+msgstr "调用gitk显示当前分支"
+
+#: git-gui.sh:1820
+msgid "Visualize All Branch History"
+msgstr "调用gitk显示所有分支"
+
+#: git-gui.sh:1827
+#, fuzzy, tcl-format
+msgid "Browse %s's Files"
+msgstr "浏览当前分支文件"
+
+#: git-gui.sh:1829
+#, fuzzy, tcl-format
+msgid "Visualize %s's History"
+msgstr "调用gitk显示所有分支"
+
+#: git-gui.sh:1834 lib/database.tcl:27 lib/database.tcl:67
+msgid "Database Statistics"
+msgstr "数据库统计数据"
+
+#: git-gui.sh:1837 lib/database.tcl:34
+msgid "Compress Database"
+msgstr "压缩数据库"
+
+#: git-gui.sh:1840
+msgid "Verify Database"
+msgstr "验证数据库"
+
+#: git-gui.sh:1847 git-gui.sh:1851 git-gui.sh:1855 lib/shortcut.tcl:9
+#: lib/shortcut.tcl:45 lib/shortcut.tcl:84
+msgid "Create Desktop Icon"
+msgstr "创建桌面图标"
+
+#: git-gui.sh:1860 lib/choose_repository.tcl:36 lib/choose_repository.tcl:95
+msgid "Quit"
+msgstr "退出"
+
+#: git-gui.sh:1867
+msgid "Undo"
+msgstr "撤销"
+
+#: git-gui.sh:1870
+msgid "Redo"
+msgstr "重做"
+
+#: git-gui.sh:1874 git-gui.sh:2366
+msgid "Cut"
+msgstr "剪切"
+
+#: git-gui.sh:1877 git-gui.sh:2369 git-gui.sh:2440 git-gui.sh:2512
+#: lib/console.tcl:67
+msgid "Copy"
+msgstr "复制"
+
+#: git-gui.sh:1880 git-gui.sh:2372
+msgid "Paste"
+msgstr "粘贴"
+
+#: git-gui.sh:1883 git-gui.sh:2375 lib/branch_delete.tcl:26
+#: lib/remote_branch_delete.tcl:38
+msgid "Delete"
+msgstr "删除"
+
+#: git-gui.sh:1887 git-gui.sh:2379 git-gui.sh:2516 lib/console.tcl:69
+msgid "Select All"
+msgstr "全选"
+
+#: git-gui.sh:1896
+msgid "Create..."
+msgstr "新建..."
+
+#: git-gui.sh:1902
+msgid "Checkout..."
+msgstr "切换..."
+
+#: git-gui.sh:1908
+msgid "Rename..."
+msgstr "改名..."
+
+#: git-gui.sh:1913 git-gui.sh:2012
+msgid "Delete..."
+msgstr "删除..."
+
+#: git-gui.sh:1918
+msgid "Reset..."
+msgstr "重置所有修动..."
+
+#: git-gui.sh:1930 git-gui.sh:2313
+msgid "New Commit"
+msgstr "新提交"
+
+#: git-gui.sh:1938 git-gui.sh:2320
+msgid "Amend Last Commit"
+msgstr "修订上次提交"
+
+#: git-gui.sh:1947 git-gui.sh:2280 lib/remote_branch_delete.tcl:99
+msgid "Rescan"
+msgstr "重新扫描"
+
+#: git-gui.sh:1953
+#, fuzzy
+msgid "Stage To Commit"
+msgstr "从本次提交移除"
+
+#: git-gui.sh:1958
+#, fuzzy
+msgid "Stage Changed Files To Commit"
+msgstr "将被提交的修改"
+
+#: git-gui.sh:1964
+msgid "Unstage From Commit"
+msgstr "从本次提交移除"
+
+#: git-gui.sh:1969 lib/index.tcl:352
+msgid "Revert Changes"
+msgstr "恢复修改"
+
+#: git-gui.sh:1976 git-gui.sh:2292 git-gui.sh:2390
+msgid "Sign Off"
+msgstr "签名"
+
+#: git-gui.sh:1980 git-gui.sh:2296
+#, fuzzy
+msgid "Commit@@verb"
+msgstr "提交"
+
+#: git-gui.sh:1991
+msgid "Local Merge..."
+msgstr "本地合并..."
+
+#: git-gui.sh:1996
+msgid "Abort Merge..."
+msgstr "取消合并..."
+
+#: git-gui.sh:2008
+msgid "Push..."
+msgstr "上传..."
+
+#: git-gui.sh:2019 lib/choose_repository.tcl:41
+msgid "Apple"
+msgstr "苹果"
+
+#: git-gui.sh:2022 git-gui.sh:2044 lib/about.tcl:13
+#: lib/choose_repository.tcl:44 lib/choose_repository.tcl:50
+#, tcl-format
+msgid "About %s"
+msgstr "关于%s"
+
+#: git-gui.sh:2026
+msgid "Preferences..."
+msgstr ""
+
+#: git-gui.sh:2034 git-gui.sh:2558
+msgid "Options..."
+msgstr "选项..."
+
+#: git-gui.sh:2040 lib/choose_repository.tcl:47
+msgid "Help"
+msgstr "帮助"
+
+#: git-gui.sh:2081
+msgid "Online Documentation"
+msgstr "在线文档"
+
+#: git-gui.sh:2165
+#, tcl-format
+msgid "fatal: cannot stat path %s: No such file or directory"
+msgstr ""
+
+#: git-gui.sh:2198
+msgid "Current Branch:"
+msgstr "当前分支:"
+
+#: git-gui.sh:2219
+#, fuzzy
+msgid "Staged Changes (Will Commit)"
+msgstr "将被提交的修改"
+
+#: git-gui.sh:2239
+msgid "Unstaged Changes"
+msgstr ""
+
+#: git-gui.sh:2286
+msgid "Stage Changed"
+msgstr ""
+
+#: git-gui.sh:2302 lib/transport.tcl:93 lib/transport.tcl:182
+msgid "Push"
+msgstr "上传"
+
+#: git-gui.sh:2332
+msgid "Initial Commit Message:"
+msgstr "初始提交描述:"
+
+#: git-gui.sh:2333
+msgid "Amended Commit Message:"
+msgstr "修订提交描述:"
+
+#: git-gui.sh:2334
+msgid "Amended Initial Commit Message:"
+msgstr "修订初始提交描述:"
+
+#: git-gui.sh:2335
+msgid "Amended Merge Commit Message:"
+msgstr "修订合并提交描述:"
+
+#: git-gui.sh:2336
+msgid "Merge Commit Message:"
+msgstr "合并提交描述:"
+
+#: git-gui.sh:2337
+msgid "Commit Message:"
+msgstr "提交描述:"
+
+#: git-gui.sh:2382 git-gui.sh:2520 lib/console.tcl:71
+msgid "Copy All"
+msgstr "全部复制"
+
+#: git-gui.sh:2406 lib/blame.tcl:104
+msgid "File:"
+msgstr ""
+
+#: git-gui.sh:2508
+msgid "Refresh"
+msgstr "刷新"
+
+#: git-gui.sh:2529
+msgid "Apply/Reverse Hunk"
+msgstr "应用/撤消此修改块"
+
+#: git-gui.sh:2535
+msgid "Decrease Font Size"
+msgstr "缩小字体"
+
+#: git-gui.sh:2539
+msgid "Increase Font Size"
+msgstr "放大字体"
+
+#: git-gui.sh:2544
+msgid "Show Less Context"
+msgstr "显示更多diff上下文"
+
+#: git-gui.sh:2551
+msgid "Show More Context"
+msgstr "显示更少diff上下文"
+
+#: git-gui.sh:2565
+#, fuzzy
+msgid "Unstage Hunk From Commit"
+msgstr "从本次提交移除"
+
+#: git-gui.sh:2567
+#, fuzzy
+msgid "Stage Hunk For Commit"
+msgstr "从本次提交移除"
+
+#: git-gui.sh:2586
+msgid "Initializing..."
+msgstr ""
+
+#: git-gui.sh:2677
+#, tcl-format
+msgid ""
+"Possible environment issues exist.\n"
+"\n"
+"The following environment variables are probably\n"
+"going to be ignored by any Git subprocess run\n"
+"by %s:\n"
+"\n"
+msgstr ""
+
+#: git-gui.sh:2707
+msgid ""
+"\n"
+"This is due to a known issue with the\n"
+"Tcl binary distributed by Cygwin."
+msgstr ""
+
+#: git-gui.sh:2712
+#, tcl-format
+msgid ""
+"\n"
+"\n"
+"A good replacement for %s\n"
+"is placing values for the user.name and\n"
+"user.email settings into your personal\n"
+"~/.gitconfig file.\n"
+msgstr ""
+
+#: lib/about.tcl:25
+msgid "git-gui - a graphical user interface for Git."
+msgstr ""
+
+#: lib/blame.tcl:77
+msgid "File Viewer"
+msgstr ""
+
+#: lib/blame.tcl:81
+#, fuzzy
+msgid "Commit:"
+msgstr "提交"
+
+#: lib/blame.tcl:249
+#, fuzzy
+msgid "Copy Commit"
+msgstr "提交"
+
+#: lib/blame.tcl:369
+#, tcl-format
+msgid "Reading %s..."
+msgstr ""
+
+#: lib/blame.tcl:473
+msgid "Loading copy/move tracking annotations..."
+msgstr ""
+
+#: lib/blame.tcl:493
+msgid "lines annotated"
+msgstr ""
+
+#: lib/blame.tcl:674
+msgid "Loading original location annotations..."
+msgstr ""
+
+#: lib/blame.tcl:677
+msgid "Annotation complete."
+msgstr ""
+
+#: lib/blame.tcl:731
+msgid "Loading annotation..."
+msgstr ""
+
+#: lib/blame.tcl:787
+msgid "Author:"
+msgstr ""
+
+#: lib/blame.tcl:791
+#, fuzzy
+msgid "Committer:"
+msgstr "提交"
+
+#: lib/blame.tcl:796
+msgid "Original File:"
+msgstr ""
+
+#: lib/blame.tcl:910
+msgid "Originally By:"
+msgstr ""
+
+#: lib/blame.tcl:916
+msgid "In File:"
+msgstr ""
+
+#: lib/blame.tcl:921
+msgid "Copied Or Moved Here By:"
+msgstr ""
+
+#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
+#, fuzzy
+msgid "Checkout Branch"
+msgstr "当前分支:"
+
+#: lib/branch_checkout.tcl:23
+#, fuzzy
+msgid "Checkout"
+msgstr "切换..."
+
+#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
+#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
+#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
+#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
+msgid "Cancel"
+msgstr ""
+
+#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
+msgid "Revision"
+msgstr ""
+
+#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
+#, fuzzy
+msgid "Options"
+msgstr "选项..."
+
+#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
+msgid "Fetch Tracking Branch"
+msgstr ""
+
+#: lib/branch_checkout.tcl:44
+msgid "Detach From Local Branch"
+msgstr ""
+
+#: lib/branch_create.tcl:22
+#, fuzzy
+msgid "Create Branch"
+msgstr "当前分支:"
+
+#: lib/branch_create.tcl:27
+#, fuzzy
+msgid "Create New Branch"
+msgstr "当前分支:"
+
+#: lib/branch_create.tcl:31 lib/choose_repository.tcl:199
+#, fuzzy
+msgid "Create"
+msgstr "新建..."
+
+#: lib/branch_create.tcl:40
+#, fuzzy
+msgid "Branch Name"
+msgstr "分支"
+
+#: lib/branch_create.tcl:43
+msgid "Name:"
+msgstr ""
+
+#: lib/branch_create.tcl:58
+msgid "Match Tracking Branch Name"
+msgstr ""
+
+#: lib/branch_create.tcl:66
+msgid "Starting Revision"
+msgstr ""
+
+#: lib/branch_create.tcl:72
+msgid "Update Existing Branch:"
+msgstr ""
+
+#: lib/branch_create.tcl:75
+msgid "No"
+msgstr ""
+
+#: lib/branch_create.tcl:80
+msgid "Fast Forward Only"
+msgstr ""
+
+#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
+#, fuzzy
+msgid "Reset"
+msgstr "重置所有修动..."
+
+#: lib/branch_create.tcl:97
+msgid "Checkout After Creation"
+msgstr ""
+
+#: lib/branch_create.tcl:131
+msgid "Please select a tracking branch."
+msgstr ""
+
+#: lib/branch_create.tcl:140
+#, tcl-format
+msgid "Tracking branch %s is not a branch in the remote repository."
+msgstr ""
+
+#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
+msgid "Please supply a branch name."
+msgstr ""
+
+#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
+#, tcl-format
+msgid "'%s' is not an acceptable branch name."
+msgstr ""
+
+#: lib/branch_delete.tcl:15
+#, fuzzy
+msgid "Delete Branch"
+msgstr "当前分支:"
+
+#: lib/branch_delete.tcl:20
+msgid "Delete Local Branch"
+msgstr ""
+
+#: lib/branch_delete.tcl:37
+#, fuzzy
+msgid "Local Branches"
+msgstr "分支"
+
+#: lib/branch_delete.tcl:52
+msgid "Delete Only If Merged Into"
+msgstr ""
+
+#: lib/branch_delete.tcl:54
+msgid "Always (Do not perform merge test.)"
+msgstr ""
+
+#: lib/branch_delete.tcl:103
+#, tcl-format
+msgid "The following branches are not completely merged into %s:"
+msgstr ""
+
+#: lib/branch_delete.tcl:115
+msgid ""
+"Recovering deleted branches is difficult. \n"
+"\n"
+" Delete the selected branches?"
+msgstr ""
+
+#: lib/branch_delete.tcl:141
+#, tcl-format
+msgid ""
+"Failed to delete branches:\n"
+"%s"
+msgstr ""
+
+#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
+#, fuzzy
+msgid "Rename Branch"
+msgstr "当前分支:"
+
+#: lib/branch_rename.tcl:26
+#, fuzzy
+msgid "Rename"
+msgstr "改名..."
+
+#: lib/branch_rename.tcl:36
+#, fuzzy
+msgid "Branch:"
+msgstr "分支"
+
+#: lib/branch_rename.tcl:39
+msgid "New Name:"
+msgstr ""
+
+#: lib/branch_rename.tcl:75
+msgid "Please select a branch to rename."
+msgstr ""
+
+#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
+#, tcl-format
+msgid "Branch '%s' already exists."
+msgstr ""
+
+#: lib/branch_rename.tcl:117
+#, tcl-format
+msgid "Failed to rename '%s'."
+msgstr ""
+
+#: lib/browser.tcl:17
+msgid "Starting..."
+msgstr ""
+
+#: lib/browser.tcl:26
+msgid "File Browser"
+msgstr ""
+
+#: lib/browser.tcl:125 lib/browser.tcl:142
+#, tcl-format
+msgid "Loading %s..."
+msgstr ""
+
+#: lib/browser.tcl:186
+msgid "[Up To Parent]"
+msgstr ""
+
+#: lib/browser.tcl:266 lib/browser.tcl:272
+#, fuzzy
+msgid "Browse Branch Files"
+msgstr "浏览当前分支文件"
+
+#: lib/browser.tcl:277 lib/choose_repository.tcl:215
+#: lib/choose_repository.tcl:305 lib/choose_repository.tcl:315
+#: lib/choose_repository.tcl:811
+msgid "Browse"
+msgstr ""
+
+#: lib/checkout_op.tcl:79
+#, tcl-format
+msgid "Fetching %s from %s"
+msgstr ""
+
+#: lib/checkout_op.tcl:127
+#, tcl-format
+msgid "fatal: Cannot resolve %s"
+msgstr ""
+
+#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
+msgid "Close"
+msgstr ""
+
+#: lib/checkout_op.tcl:169
+#, tcl-format
+msgid "Branch '%s' does not exist."
+msgstr ""
+
+#: lib/checkout_op.tcl:206
+#, tcl-format
+msgid ""
+"Branch '%s' already exists.\n"
+"\n"
+"It cannot fast-forward to %s.\n"
+"A merge is required."
+msgstr ""
+
+#: lib/checkout_op.tcl:220
+#, tcl-format
+msgid "Merge strategy '%s' not supported."
+msgstr ""
+
+#: lib/checkout_op.tcl:239
+#, tcl-format
+msgid "Failed to update '%s'."
+msgstr ""
+
+#: lib/checkout_op.tcl:251
+msgid "Staging area (index) is already locked."
+msgstr ""
+
+#: lib/checkout_op.tcl:266
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before the current branch can be changed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+
+#: lib/checkout_op.tcl:322
+#, tcl-format
+msgid "Updating working directory to '%s'..."
+msgstr ""
+
+#: lib/checkout_op.tcl:353
+#, tcl-format
+msgid "Aborted checkout of '%s' (file level merging is required)."
+msgstr ""
+
+#: lib/checkout_op.tcl:354
+msgid "File level merge required."
+msgstr ""
+
+#: lib/checkout_op.tcl:358
+#, tcl-format
+msgid "Staying on branch '%s'."
+msgstr ""
+
+#: lib/checkout_op.tcl:429
+msgid ""
+"You are no longer on a local branch.\n"
+"\n"
+"If you wanted to be on a branch, create one now starting from 'This Detached "
+"Checkout'."
+msgstr ""
+
+#: lib/checkout_op.tcl:446
+#, fuzzy, tcl-format
+msgid "Checked out '%s'."
+msgstr "切换..."
+
+#: lib/checkout_op.tcl:478
+#, tcl-format
+msgid "Resetting '%s' to '%s' will lose the following commits:"
+msgstr ""
+
+#: lib/checkout_op.tcl:500
+msgid "Recovering lost commits may not be easy."
+msgstr ""
+
+#: lib/checkout_op.tcl:505
+#, tcl-format
+msgid "Reset '%s'?"
+msgstr ""
+
+#: lib/checkout_op.tcl:510 lib/merge.tcl:164
+msgid "Visualize"
+msgstr ""
+
+#: lib/checkout_op.tcl:578
+#, tcl-format
+msgid ""
+"Failed to set current branch.\n"
+"\n"
+"This working directory is only partially switched.  We successfully updated "
+"your files, but failed to update an internal Git file.\n"
+"\n"
+"This should not have occurred.  %s will now close and give up."
+msgstr ""
+
+#: lib/choose_font.tcl:39
+#, fuzzy
+msgid "Select"
+msgstr "全选"
+
+#: lib/choose_font.tcl:53
+msgid "Font Family"
+msgstr ""
+
+#: lib/choose_font.tcl:73
+#, fuzzy
+msgid "Font Size"
+msgstr "缩小字体"
+
+#: lib/choose_font.tcl:90
+msgid "Font Example"
+msgstr ""
+
+#: lib/choose_font.tcl:101
+msgid ""
+"This is example text.\n"
+"If you like this text, it can be your font."
+msgstr ""
+
+#: lib/choose_repository.tcl:25
+msgid "Git Gui"
+msgstr ""
+
+#: lib/choose_repository.tcl:69 lib/choose_repository.tcl:204
+#, fuzzy
+msgid "Create New Repository"
+msgstr "版本树"
+
+#: lib/choose_repository.tcl:74 lib/choose_repository.tcl:291
+#, fuzzy
+msgid "Clone Existing Repository"
+msgstr "版本树"
+
+#: lib/choose_repository.tcl:79 lib/choose_repository.tcl:800
+#, fuzzy
+msgid "Open Existing Repository"
+msgstr "版本树"
+
+#: lib/choose_repository.tcl:91
+msgid "Next >"
+msgstr ""
+
+#: lib/choose_repository.tcl:152
+#, tcl-format
+msgid "Location %s already exists."
+msgstr ""
+
+#: lib/choose_repository.tcl:158 lib/choose_repository.tcl:165
+#: lib/choose_repository.tcl:172
+#, tcl-format
+msgid "Failed to create repository %s:"
+msgstr ""
+
+#: lib/choose_repository.tcl:209 lib/choose_repository.tcl:309
+msgid "Directory:"
+msgstr ""
+
+#: lib/choose_repository.tcl:238 lib/choose_repository.tcl:363
+#: lib/choose_repository.tcl:834
+#, fuzzy
+msgid "Git Repository"
+msgstr "版本树"
+
+#: lib/choose_repository.tcl:253 lib/choose_repository.tcl:260
+#, tcl-format
+msgid "Directory %s already exists."
+msgstr ""
+
+#: lib/choose_repository.tcl:265
+#, tcl-format
+msgid "File %s already exists."
+msgstr ""
+
+#: lib/choose_repository.tcl:286
+msgid "Clone"
+msgstr ""
+
+#: lib/choose_repository.tcl:299
+msgid "URL:"
+msgstr ""
+
+#: lib/choose_repository.tcl:319
+msgid "Clone Type:"
+msgstr ""
+
+#: lib/choose_repository.tcl:325
+msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
+msgstr ""
+
+#: lib/choose_repository.tcl:331
+msgid "Full Copy (Slower, Redundant Backup)"
+msgstr ""
+
+#: lib/choose_repository.tcl:337
+msgid "Shared (Fastest, Not Recommended, No Backup)"
+msgstr ""
+
+#: lib/choose_repository.tcl:369 lib/choose_repository.tcl:418
+#: lib/choose_repository.tcl:560 lib/choose_repository.tcl:630
+#: lib/choose_repository.tcl:840 lib/choose_repository.tcl:848
+#, tcl-format
+msgid "Not a Git repository: %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:405
+msgid "Standard only available for local repository."
+msgstr ""
+
+#: lib/choose_repository.tcl:409
+msgid "Shared only available for local repository."
+msgstr ""
+
+#: lib/choose_repository.tcl:439
+msgid "Failed to configure origin"
+msgstr ""
+
+#: lib/choose_repository.tcl:451
+msgid "Counting objects"
+msgstr ""
+
+#: lib/choose_repository.tcl:452
+msgid "buckets"
+msgstr ""
+
+#: lib/choose_repository.tcl:476
+#, tcl-format
+msgid "Unable to copy objects/info/alternates: %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:512
+#, tcl-format
+msgid "Nothing to clone from %s."
+msgstr ""
+
+#: lib/choose_repository.tcl:514 lib/choose_repository.tcl:728
+#: lib/choose_repository.tcl:740
+msgid "The 'master' branch has not been initialized."
+msgstr ""
+
+#: lib/choose_repository.tcl:527
+msgid "Hardlinks are unavailable.  Falling back to copying."
+msgstr ""
+
+#: lib/choose_repository.tcl:539
+#, tcl-format
+msgid "Cloning from %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:570
+#, fuzzy
+msgid "Copying objects"
+msgstr "压缩数据库"
+
+#: lib/choose_repository.tcl:571
+msgid "KiB"
+msgstr ""
+
+#: lib/choose_repository.tcl:595
+#, tcl-format
+msgid "Unable to copy object: %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:605
+msgid "Linking objects"
+msgstr ""
+
+#: lib/choose_repository.tcl:606
+msgid "objects"
+msgstr ""
+
+#: lib/choose_repository.tcl:614
+#, tcl-format
+msgid "Unable to hardlink object: %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:669
+msgid "Cannot fetch branches and objects.  See console output for details."
+msgstr ""
+
+#: lib/choose_repository.tcl:680
+msgid "Cannot fetch tags.  See console output for details."
+msgstr ""
+
+#: lib/choose_repository.tcl:704
+msgid "Cannot determine HEAD.  See console output for details."
+msgstr ""
+
+#: lib/choose_repository.tcl:713
+#, tcl-format
+msgid "Unable to cleanup %s"
+msgstr ""
+
+#: lib/choose_repository.tcl:719
+msgid "Clone failed."
+msgstr ""
+
+#: lib/choose_repository.tcl:726
+msgid "No default branch obtained."
+msgstr ""
+
+#: lib/choose_repository.tcl:737
+#, tcl-format
+msgid "Cannot resolve %s as a commit."
+msgstr ""
+
+#: lib/choose_repository.tcl:749
+msgid "Creating working directory"
+msgstr ""
+
+#: lib/choose_repository.tcl:750 lib/index.tcl:15 lib/index.tcl:80
+#: lib/index.tcl:149
+msgid "files"
+msgstr ""
+
+#: lib/choose_repository.tcl:779
+msgid "Initial file checkout failed."
+msgstr ""
+
+#: lib/choose_repository.tcl:795
+msgid "Open"
+msgstr ""
+
+#: lib/choose_repository.tcl:805
+#, fuzzy
+msgid "Repository:"
+msgstr "版本树"
+
+#: lib/choose_repository.tcl:854
+#, tcl-format
+msgid "Failed to open repository %s:"
+msgstr ""
+
+#: lib/choose_rev.tcl:53
+msgid "This Detached Checkout"
+msgstr ""
+
+#: lib/choose_rev.tcl:60
+msgid "Revision Expression:"
+msgstr ""
+
+#: lib/choose_rev.tcl:74
+#, fuzzy
+msgid "Local Branch"
+msgstr "分支"
+
+#: lib/choose_rev.tcl:79
+#, fuzzy
+msgid "Tracking Branch"
+msgstr "当前分支:"
+
+#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
+msgid "Tag"
+msgstr ""
+
+#: lib/choose_rev.tcl:317
+#, tcl-format
+msgid "Invalid revision: %s"
+msgstr ""
+
+#: lib/choose_rev.tcl:338
+msgid "No revision selected."
+msgstr ""
+
+#: lib/choose_rev.tcl:346
+msgid "Revision expression is empty."
+msgstr ""
+
+#: lib/choose_rev.tcl:530
+msgid "Updated"
+msgstr ""
+
+#: lib/choose_rev.tcl:558
+msgid "URL"
+msgstr ""
+
+#: lib/commit.tcl:9
+msgid ""
+"There is nothing to amend.\n"
+"\n"
+"You are about to create the initial commit.  There is no commit before this "
+"to amend.\n"
+msgstr ""
+
+#: lib/commit.tcl:18
+msgid ""
+"Cannot amend while merging.\n"
+"\n"
+"You are currently in the middle of a merge that has not been fully "
+"completed.  You cannot amend the prior commit unless you first abort the "
+"current merge activity.\n"
+msgstr ""
+
+#: lib/commit.tcl:49
+msgid "Error loading commit data for amend:"
+msgstr ""
+
+#: lib/commit.tcl:76
+msgid "Unable to obtain your identity:"
+msgstr ""
+
+#: lib/commit.tcl:81
+msgid "Invalid GIT_COMMITTER_IDENT:"
+msgstr ""
+
+#: lib/commit.tcl:133
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before another commit can be created.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+
+#: lib/commit.tcl:154
+#, tcl-format
+msgid ""
+"Unmerged files cannot be committed.\n"
+"\n"
+"File %s has merge conflicts.  You must resolve them and stage the file "
+"before committing.\n"
+msgstr ""
+
+#: lib/commit.tcl:162
+#, tcl-format
+msgid ""
+"Unknown file state %s detected.\n"
+"\n"
+"File %s cannot be committed by this program.\n"
+msgstr ""
+
+#: lib/commit.tcl:170
+msgid ""
+"No changes to commit.\n"
+"\n"
+"You must stage at least 1 file before you can commit.\n"
+msgstr ""
+
+#: lib/commit.tcl:183
+msgid ""
+"Please supply a commit message.\n"
+"\n"
+"A good commit message has the following format:\n"
+"\n"
+"- First line: Describe in one sentance what you did.\n"
+"- Second line: Blank\n"
+"- Remaining lines: Describe why this change is good.\n"
+msgstr ""
+
+#: lib/commit.tcl:257
+msgid "write-tree failed:"
+msgstr ""
+
+#: lib/commit.tcl:275
+#, tcl-format
+msgid "Commit %s appears to be corrupt"
+msgstr ""
+
+#: lib/commit.tcl:279
+msgid ""
+"No changes to commit.\n"
+"\n"
+"No files were modified by this commit and it was not a merge commit.\n"
+"\n"
+"A rescan will be automatically started now.\n"
+msgstr ""
+
+#: lib/commit.tcl:286
+msgid "No changes to commit."
+msgstr ""
+
+#: lib/commit.tcl:303
+#, tcl-format
+msgid "warning: Tcl does not support encoding '%s'."
+msgstr ""
+
+#: lib/commit.tcl:317
+msgid "commit-tree failed:"
+msgstr ""
+
+#: lib/commit.tcl:339
+msgid "update-ref failed:"
+msgstr ""
+
+#: lib/commit.tcl:430
+#, tcl-format
+msgid "Created commit %s: %s"
+msgstr ""
+
+#: lib/console.tcl:57
+msgid "Working... please wait..."
+msgstr ""
+
+#: lib/console.tcl:183
+msgid "Success"
+msgstr ""
+
+#: lib/console.tcl:196
+msgid "Error: Command Failed"
+msgstr ""
+
+#: lib/database.tcl:43
+msgid "Number of loose objects"
+msgstr ""
+
+#: lib/database.tcl:44
+msgid "Disk space used by loose objects"
+msgstr ""
+
+#: lib/database.tcl:45
+msgid "Number of packed objects"
+msgstr ""
+
+#: lib/database.tcl:46
+msgid "Number of packs"
+msgstr ""
+
+#: lib/database.tcl:47
+msgid "Disk space used by packed objects"
+msgstr ""
+
+#: lib/database.tcl:48
+msgid "Packed objects waiting for pruning"
+msgstr ""
+
+#: lib/database.tcl:49
+msgid "Garbage files"
+msgstr ""
+
+#: lib/database.tcl:72
+#, fuzzy
+msgid "Compressing the object database"
+msgstr "压缩数据库"
+
+#: lib/database.tcl:83
+msgid "Verifying the object database with fsck-objects"
+msgstr ""
+
+#: lib/database.tcl:108
+#, tcl-format
+msgid ""
+"This repository currently has approximately %i loose objects.\n"
+"\n"
+"To maintain optimal performance it is strongly recommended that you compress "
+"the database when more than %i loose objects exist.\n"
+"\n"
+"Compress the database now?"
+msgstr ""
+
+#: lib/date.tcl:25
+#, tcl-format
+msgid "Invalid date from Git: %s"
+msgstr ""
+
+#: lib/diff.tcl:42
+#, tcl-format
+msgid ""
+"No differences detected.\n"
+"\n"
+"%s has no changes.\n"
+"\n"
+"The modification date of this file was updated by another application, but "
+"the content within the file was not changed.\n"
+"\n"
+"A rescan will be automatically started to find other files which may have "
+"the same state."
+msgstr ""
+
+#: lib/diff.tcl:81
+#, tcl-format
+msgid "Loading diff of %s..."
+msgstr ""
+
+#: lib/diff.tcl:114 lib/diff.tcl:184
+#, tcl-format
+msgid "Unable to display %s"
+msgstr ""
+
+#: lib/diff.tcl:115
+msgid "Error loading file:"
+msgstr ""
+
+#: lib/diff.tcl:122
+msgid "Git Repository (subproject)"
+msgstr ""
+
+#: lib/diff.tcl:134
+msgid "* Binary file (not showing content)."
+msgstr ""
+
+#: lib/diff.tcl:185
+msgid "Error loading diff:"
+msgstr ""
+
+#: lib/diff.tcl:302
+msgid "Failed to unstage selected hunk."
+msgstr ""
+
+#: lib/diff.tcl:309
+msgid "Failed to stage selected hunk."
+msgstr ""
+
+#: lib/error.tcl:12 lib/error.tcl:102
+msgid "error"
+msgstr ""
+
+#: lib/error.tcl:28
+msgid "warning"
+msgstr ""
+
+#: lib/error.tcl:81
+msgid "You must correct the above errors before committing."
+msgstr ""
+
+#: lib/index.tcl:241
+#, fuzzy, tcl-format
+msgid "Unstaging %s from commit"
+msgstr "从本次提交移除"
+
+#: lib/index.tcl:285
+#, tcl-format
+msgid "Adding %s"
+msgstr ""
+
+#: lib/index.tcl:340
+#, fuzzy, tcl-format
+msgid "Revert changes in file %s?"
+msgstr "恢复修改"
+
+#: lib/index.tcl:342
+#, tcl-format
+msgid "Revert changes in these %i files?"
+msgstr ""
+
+#: lib/index.tcl:348
+msgid "Any unstaged changes will be permanently lost by the revert."
+msgstr ""
+
+#: lib/index.tcl:351
+msgid "Do Nothing"
+msgstr ""
+
+#: lib/merge.tcl:13
+msgid ""
+"Cannot merge while amending.\n"
+"\n"
+"You must finish amending this commit before starting any type of merge.\n"
+msgstr ""
+
+#: lib/merge.tcl:27
+msgid ""
+"Last scanned state does not match repository state.\n"
+"\n"
+"Another Git program has modified this repository since the last scan.  A "
+"rescan must be performed before a merge can be performed.\n"
+"\n"
+"The rescan will be automatically started now.\n"
+msgstr ""
+
+#: lib/merge.tcl:44
+#, tcl-format
+msgid ""
+"You are in the middle of a conflicted merge.\n"
+"\n"
+"File %s has merge conflicts.\n"
+"\n"
+"You must resolve them, stage the file, and commit to complete the current "
+"merge.  Only then can you begin another merge.\n"
+msgstr ""
+
+#: lib/merge.tcl:54
+#, tcl-format
+msgid ""
+"You are in the middle of a change.\n"
+"\n"
+"File %s is modified.\n"
+"\n"
+"You should complete the current commit before starting a merge.  Doing so "
+"will help you abort a failed merge, should the need arise.\n"
+msgstr ""
+
+#: lib/merge.tcl:106
+#, tcl-format
+msgid "%s of %s"
+msgstr ""
+
+#: lib/merge.tcl:119
+#, tcl-format
+msgid "Merging %s and %s"
+msgstr ""
+
+#: lib/merge.tcl:131
+msgid "Merge completed successfully."
+msgstr ""
+
+#: lib/merge.tcl:133
+msgid "Merge failed.  Conflict resolution is required."
+msgstr ""
+
+#: lib/merge.tcl:158
+#, tcl-format
+msgid "Merge Into %s"
+msgstr ""
+
+#: lib/merge.tcl:177
+msgid "Revision To Merge"
+msgstr ""
+
+#: lib/merge.tcl:212
+msgid ""
+"Cannot abort while amending.\n"
+"\n"
+"You must finish amending this commit.\n"
+msgstr ""
+
+#: lib/merge.tcl:222
+msgid ""
+"Abort merge?\n"
+"\n"
+"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with aborting the current merge?"
+msgstr ""
+
+#: lib/merge.tcl:228
+msgid ""
+"Reset changes?\n"
+"\n"
+"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
+"\n"
+"Continue with resetting the current changes?"
+msgstr ""
+
+#: lib/merge.tcl:239
+msgid "Aborting"
+msgstr ""
+
+#: lib/merge.tcl:266
+msgid "Abort failed."
+msgstr ""
+
+#: lib/merge.tcl:268
+msgid "Abort completed.  Ready."
+msgstr ""
+
+#: lib/option.tcl:82
+msgid "Restore Defaults"
+msgstr ""
+
+#: lib/option.tcl:86
+msgid "Save"
+msgstr ""
+
+#: lib/option.tcl:96
+#, fuzzy, tcl-format
+msgid "%s Repository"
+msgstr "版本树"
+
+#: lib/option.tcl:97
+msgid "Global (All Repositories)"
+msgstr ""
+
+#: lib/option.tcl:103
+msgid "User Name"
+msgstr ""
+
+#: lib/option.tcl:104
+msgid "Email Address"
+msgstr ""
+
+#: lib/option.tcl:106
+#, fuzzy
+msgid "Summarize Merge Commits"
+msgstr "修订合并提交描述:"
+
+#: lib/option.tcl:107
+msgid "Merge Verbosity"
+msgstr ""
+
+#: lib/option.tcl:108
+msgid "Show Diffstat After Merge"
+msgstr ""
+
+#: lib/option.tcl:110
+msgid "Trust File Modification Timestamps"
+msgstr ""
+
+#: lib/option.tcl:111
+msgid "Prune Tracking Branches During Fetch"
+msgstr ""
+
+#: lib/option.tcl:112
+msgid "Match Tracking Branches"
+msgstr ""
+
+#: lib/option.tcl:113
+msgid "Number of Diff Context Lines"
+msgstr ""
+
+#: lib/option.tcl:114
+msgid "New Branch Name Template"
+msgstr ""
+
+#: lib/option.tcl:176
+msgid "Change Font"
+msgstr ""
+
+#: lib/option.tcl:180
+#, tcl-format
+msgid "Choose %s"
+msgstr ""
+
+#: lib/option.tcl:186
+msgid "pt."
+msgstr ""
+
+#: lib/option.tcl:200
+msgid "Preferences"
+msgstr ""
+
+#: lib/option.tcl:235
+msgid "Failed to completely save options:"
+msgstr ""
+
+#: lib/remote.tcl:165
+msgid "Prune from"
+msgstr ""
+
+#: lib/remote.tcl:170
+#, fuzzy
+msgid "Fetch from"
+msgstr "导入"
+
+#: lib/remote.tcl:213
+#, fuzzy
+msgid "Push to"
+msgstr "上传"
+
+#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
+msgid "Delete Remote Branch"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:47
+#, fuzzy
+msgid "From Repository"
+msgstr "版本树"
+
+#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
+msgid "Remote:"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
+msgid "Arbitrary URL:"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:84
+#, fuzzy
+msgid "Branches"
+msgstr "分支"
+
+#: lib/remote_branch_delete.tcl:109
+#, fuzzy
+msgid "Delete Only If"
+msgstr "删除"
+
+#: lib/remote_branch_delete.tcl:111
+msgid "Merged Into:"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:119
+msgid "Always (Do not perform merge checks)"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:152
+msgid "A branch is required for 'Merged Into'."
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:184
+#, tcl-format
+msgid ""
+"The following branches are not completely merged into %s:\n"
+"\n"
+" - %s"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:189
+#, tcl-format
+msgid ""
+"One or more of the merge tests failed because you have not fetched the "
+"necessary commits.  Try fetching from %s first."
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:207
+msgid "Please select one or more branches to delete."
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:216
+msgid ""
+"Recovering deleted branches is difficult.\n"
+"\n"
+"Delete the selected branches?"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:226
+#, tcl-format
+msgid "Deleting branches from %s"
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:286
+msgid "No repository selected."
+msgstr ""
+
+#: lib/remote_branch_delete.tcl:291
+#, tcl-format
+msgid "Scanning %s..."
+msgstr ""
+
+#: lib/shortcut.tcl:26 lib/shortcut.tcl:74
+msgid "Cannot write script:"
+msgstr ""
+
+#: lib/shortcut.tcl:149
+msgid "Cannot write icon:"
+msgstr ""
+
+#: lib/status_bar.tcl:83
+#, tcl-format
+msgid "%s ... %*i of %*i %s (%3i%%)"
+msgstr ""
+
+#: lib/transport.tcl:6
+#, fuzzy, tcl-format
+msgid "fetch %s"
+msgstr "导入"
+
+#: lib/transport.tcl:7
+#, tcl-format
+msgid "Fetching new changes from %s"
+msgstr ""
+
+#: lib/transport.tcl:18
+#, tcl-format
+msgid "remote prune %s"
+msgstr ""
+
+#: lib/transport.tcl:19
+#, tcl-format
+msgid "Pruning tracking branches deleted from %s"
+msgstr ""
+
+#: lib/transport.tcl:25 lib/transport.tcl:71
+#, tcl-format
+msgid "push %s"
+msgstr ""
+
+#: lib/transport.tcl:26
+#, tcl-format
+msgid "Pushing changes to %s"
+msgstr ""
+
+#: lib/transport.tcl:72
+#, tcl-format
+msgid "Pushing %s %s to %s"
+msgstr ""
+
+#: lib/transport.tcl:89
+#, fuzzy
+msgid "Push Branches"
+msgstr "分支"
+
+#: lib/transport.tcl:103
+#, fuzzy
+msgid "Source Branches"
+msgstr "当前分支:"
+
+#: lib/transport.tcl:120
+#, fuzzy
+msgid "Destination Repository"
+msgstr "版本树"
+
+#: lib/transport.tcl:158
+msgid "Transfer Options"
+msgstr ""
+
+#: lib/transport.tcl:160
+msgid "Force overwrite existing branch (may discard changes)"
+msgstr ""
+
+#: lib/transport.tcl:164
+msgid "Use thin pack (for slow network connections)"
+msgstr ""
+
+#: lib/transport.tcl:168
+msgid "Include tags"
+msgstr ""
+
+#~ msgid "Add To Commit"
+#~ msgstr "添加到本次提交"
+
+#~ msgid "Add Existing To Commit"
+#~ msgstr "添加默认修改文件"
+
+#~ msgid "Unstaged Changes (Will Not Be Committed)"
+#~ msgstr "不被提交的修改"
+
+#~ msgid "Add Existing"
+#~ msgstr "添加默认修改文件"
+
+#, fuzzy
+#~ msgid "Push to %s..."
+#~ msgstr "上传..."
diff --git a/git-gui/windows/git-gui.sh b/git-gui/windows/git-gui.sh
new file mode 100644 (file)
index 0000000..98f32c0
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/sh
+# Tcl ignores the next line -*- tcl -*- \
+exec wish "$0" -- "$@"
+
+if { $argc >=2 && [lindex $argv 0] == "--working-dir" } {
+       cd [lindex $argv 1]
+       set argv [lrange $argv 2 end]
+       incr argc -2
+}
+
+set gitguidir [file dirname [info script]]
+regsub -all ";" $gitguidir "\\;" gitguidir
+set env(PATH) "$gitguidir;$env(PATH)"
+unset gitguidir
+
+source [file join [file dirname [info script]] git-gui.tcl]
diff --git a/git-instaweb.sh b/git-instaweb.sh
new file mode 100755 (executable)
index 0000000..8503ae4
--- /dev/null
@@ -0,0 +1,275 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+#
+
+OPTIONS_KEEPDASHDASH=
+OPTIONS_SPEC="\
+git-instaweb [options] (--start | --stop | --restart)
+--
+l,local        only bind on 127.0.0.1
+p,port=        the port to bind to
+d,httpd=       the command to launch
+b,browser=     the browser to launch
+m,module-path= the module path (only needed for apache2)
+ Action
+stop           stop the web server
+start          start the web server
+restart        restart the web server
+"
+
+. git-sh-setup
+
+fqgitdir="$GIT_DIR"
+local="`git config --bool --get instaweb.local`"
+httpd="`git config --get instaweb.httpd`"
+browser="`git config --get instaweb.browser`"
+port=`git config --get instaweb.port`
+module_path="`git config --get instaweb.modulepath`"
+
+conf="$GIT_DIR/gitweb/httpd.conf"
+
+# Defaults:
+
+# if installed, it doesn't need further configuration (module_path)
+test -z "$httpd" && httpd='lighttpd -f'
+
+# probably the most popular browser among gitweb users
+test -z "$browser" && browser='firefox'
+
+# any untaken local port will do...
+test -z "$port" && port=1234
+
+start_httpd () {
+       httpd_only="`echo $httpd | cut -f1 -d' '`"
+       if case "$httpd_only" in /*) : ;; *) which $httpd_only >/dev/null;; esac
+       then
+               $httpd "$fqgitdir/gitweb/httpd.conf"
+       else
+               # many httpds are installed in /usr/sbin or /usr/local/sbin
+               # these days and those are not in most users $PATHs
+               # in addition, we may have generated a server script
+               # in $fqgitdir/gitweb.
+               for i in /usr/local/sbin /usr/sbin "$fqgitdir/gitweb"
+               do
+                       if test -x "$i/$httpd_only"
+                       then
+                               # don't quote $httpd, there can be
+                               # arguments to it (-f)
+                               $i/$httpd "$fqgitdir/gitweb/httpd.conf"
+                               return
+                       fi
+               done
+               echo "$httpd_only not found. Install $httpd_only or use" \
+                    "--httpd to specify another http daemon."
+               exit 1
+       fi
+       if test $? != 0; then
+               echo "Could not execute http daemon $httpd."
+               exit 1
+       fi
+}
+
+stop_httpd () {
+       test -f "$fqgitdir/pid" && kill `cat "$fqgitdir/pid"`
+}
+
+while test $# != 0
+do
+       case "$1" in
+       --stop|stop)
+               stop_httpd
+               exit 0
+               ;;
+       --start|start)
+               start_httpd
+               exit 0
+               ;;
+       --restart|restart)
+               stop_httpd
+               start_httpd
+               exit 0
+               ;;
+       -l|--local)
+               local=true
+               ;;
+       -d|--httpd)
+               shift
+               httpd="$1"
+               ;;
+       -b|--browser)
+               shift
+               browser="$1"
+               ;;
+       -p|--port)
+               shift
+               port="$1"
+               ;;
+       -m|--module-path)
+               shift
+               module_path="$1"
+               ;;
+       --)
+               ;;
+       *)
+               usage
+               ;;
+       esac
+       shift
+done
+
+mkdir -p "$GIT_DIR/gitweb/tmp"
+GIT_EXEC_PATH="`git --exec-path`"
+GIT_DIR="$fqgitdir"
+export GIT_EXEC_PATH GIT_DIR
+
+
+webrick_conf () {
+       # generate a standalone server script in $fqgitdir/gitweb.
+       cat >"$fqgitdir/gitweb/$httpd.rb" <<EOF
+require 'webrick'
+require 'yaml'
+options = YAML::load_file(ARGV[0])
+options[:StartCallback] = proc do
+  File.open(options[:PidFile],"w") do |f|
+    f.puts Process.pid
+  end
+end
+options[:ServerType] = WEBrick::Daemon
+server = WEBrick::HTTPServer.new(options)
+['INT', 'TERM'].each do |signal|
+  trap(signal) {server.shutdown}
+end
+server.start
+EOF
+       # generate a shell script to invoke the above ruby script,
+       # which assumes _ruby_ is in the user's $PATH. that's _one_
+       # portable way to run ruby, which could be installed anywhere,
+       # really.
+       cat >"$fqgitdir/gitweb/$httpd" <<EOF
+#!/bin/sh
+exec ruby "$fqgitdir/gitweb/$httpd.rb" \$*
+EOF
+       chmod +x "$fqgitdir/gitweb/$httpd"
+
+       cat >"$conf" <<EOF
+:Port: $port
+:DocumentRoot: "$fqgitdir/gitweb"
+:DirectoryIndex: ["gitweb.cgi"]
+:PidFile: "$fqgitdir/pid"
+EOF
+       test "$local" = true && echo ':BindAddress: "127.0.0.1"' >> "$conf"
+}
+
+lighttpd_conf () {
+       cat > "$conf" <<EOF
+server.document-root = "$fqgitdir/gitweb"
+server.port = $port
+server.modules = ( "mod_cgi" )
+server.indexfiles = ( "gitweb.cgi" )
+server.pid-file = "$fqgitdir/pid"
+cgi.assign = ( ".cgi" => "" )
+mimetype.assign = ( ".css" => "text/css" )
+EOF
+       test x"$local" = xtrue && echo 'server.bind = "127.0.0.1"' >> "$conf"
+}
+
+apache2_conf () {
+       test -z "$module_path" && module_path=/usr/lib/apache2/modules
+       mkdir -p "$GIT_DIR/gitweb/logs"
+       bind=
+       test x"$local" = xtrue && bind='127.0.0.1:'
+       echo 'text/css css' > $fqgitdir/mime.types
+       cat > "$conf" <<EOF
+ServerName "git-instaweb"
+ServerRoot "$fqgitdir/gitweb"
+DocumentRoot "$fqgitdir/gitweb"
+PidFile "$fqgitdir/pid"
+Listen $bind$port
+EOF
+
+       for mod in mime dir; do
+               if test -e $module_path/mod_${mod}.so; then
+                       echo "LoadModule ${mod}_module " \
+                            "$module_path/mod_${mod}.so" >> "$conf"
+               fi
+       done
+       cat >> "$conf" <<EOF
+TypesConfig $fqgitdir/mime.types
+DirectoryIndex gitweb.cgi
+EOF
+
+       # check to see if Dennis Stosberg's mod_perl compatibility patch
+       # (<20060621130708.Gcbc6e5c@leonov.stosberg.net>) has been applied
+       if test -f "$module_path/mod_perl.so" && grep '^our $gitbin' \
+                               "$GIT_DIR/gitweb/gitweb.cgi" >/dev/null
+       then
+               # favor mod_perl if available
+               cat >> "$conf" <<EOF
+LoadModule perl_module $module_path/mod_perl.so
+PerlPassEnv GIT_DIR
+PerlPassEnv GIT_EXEC_DIR
+<Location /gitweb.cgi>
+       SetHandler perl-script
+       PerlResponseHandler ModPerl::Registry
+       PerlOptions +ParseHeaders
+       Options +ExecCGI
+</Location>
+EOF
+       else
+               # plain-old CGI
+               list_mods=`echo "$httpd" | sed "s/-f$/-l/"`
+               $list_mods | grep 'mod_cgi\.c' >/dev/null 2>&1 || \
+               echo "LoadModule cgi_module $module_path/mod_cgi.so" >> "$conf"
+               cat >> "$conf" <<EOF
+AddHandler cgi-script .cgi
+<Location /gitweb.cgi>
+       Options +ExecCGI
+</Location>
+EOF
+       fi
+}
+
+script='
+s#^\(my\|our\) $projectroot =.*#\1 $projectroot = "'$(dirname "$fqgitdir")'";#
+s#\(my\|our\) $gitbin =.*#\1 $gitbin = "'$GIT_EXEC_PATH'";#
+s#\(my\|our\) $projects_list =.*#\1 $projects_list = $projectroot;#
+s#\(my\|our\) $git_temp =.*#\1 $git_temp = "'$fqgitdir/gitweb/tmp'";#'
+
+gitweb_cgi () {
+       cat > "$1.tmp" <<\EOFGITWEB
+@@GITWEB_CGI@@
+EOFGITWEB
+       sed "$script" "$1.tmp"  > "$1"
+       chmod +x "$1"
+       rm -f "$1.tmp"
+}
+
+gitweb_css () {
+       cat > "$1" <<\EOFGITWEB
+@@GITWEB_CSS@@
+EOFGITWEB
+}
+
+gitweb_cgi "$GIT_DIR/gitweb/gitweb.cgi"
+gitweb_css "$GIT_DIR/gitweb/gitweb.css"
+
+case "$httpd" in
+*lighttpd*)
+       lighttpd_conf
+       ;;
+*apache2*)
+       apache2_conf
+       ;;
+webrick)
+       webrick_conf
+       ;;
+*)
+       echo "Unknown httpd specified: $httpd"
+       exit 1
+       ;;
+esac
+
+start_httpd
+url=http://127.0.0.1:$port
+"$browser" $url || echo $url
diff --git a/git-lost-found.sh b/git-lost-found.sh
new file mode 100755 (executable)
index 0000000..9cedaf8
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+USAGE=''
+SUBDIRECTORY_OK='Yes'
+OPTIONS_SPEC=
+. git-sh-setup
+
+echo "WARNING: '$0' is deprecated in favor of 'git fsck --lost-found'" >&2
+
+if [ "$#" != "0" ]
+then
+    usage
+fi
+
+laf="$GIT_DIR/lost-found"
+rm -fr "$laf" && mkdir -p "$laf/commit" "$laf/other" || exit
+
+git fsck --full --no-reflogs |
+while read dangling type sha1
+do
+       case "$dangling" in
+       dangling)
+               if git rev-parse --verify "$sha1^0" >/dev/null 2>/dev/null
+               then
+                       dir="$laf/commit"
+                       git show-branch "$sha1"
+               else
+                       dir="$laf/other"
+               fi
+               echo "$sha1" >"$dir/$sha1"
+               ;;
+       esac
+done
diff --git a/git-merge-octopus.sh b/git-merge-octopus.sh
new file mode 100755 (executable)
index 0000000..645e114
--- /dev/null
@@ -0,0 +1,114 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+# Resolve two or more trees.
+#
+
+LF='
+'
+
+die () {
+    echo >&2 "$*"
+    exit 1
+}
+
+# The first parameters up to -- are merge bases; the rest are heads.
+bases= head= remotes= sep_seen=
+for arg
+do
+       case ",$sep_seen,$head,$arg," in
+       *,--,)
+               sep_seen=yes
+               ;;
+       ,yes,,*)
+               head=$arg
+               ;;
+       ,yes,*)
+               remotes="$remotes$arg "
+               ;;
+       *)
+               bases="$bases$arg "
+               ;;
+       esac
+done
+
+# Reject if this is not an Octopus -- resolve should be used instead.
+case "$remotes" in
+?*' '?*)
+       ;;
+*)
+       exit 2 ;;
+esac
+
+# MRC is the current "merge reference commit"
+# MRT is the current "merge result tree"
+
+MRC=$head MSG= PARENT="-p $head"
+MRT=$(git write-tree)
+CNT=1 ;# counting our head
+NON_FF_MERGE=0
+OCTOPUS_FAILURE=0
+for SHA1 in $remotes
+do
+       case "$OCTOPUS_FAILURE" in
+       1)
+               # We allow only last one to have a hand-resolvable
+               # conflicts.  Last round failed and we still had
+               # a head to merge.
+               echo "Automated merge did not work."
+               echo "Should not be doing an Octopus."
+               exit 2
+       esac
+
+       common=$(git merge-base --all $MRC $SHA1) ||
+               die "Unable to find common commit with $SHA1"
+
+       case "$LF$common$LF" in
+       *"$LF$SHA1$LF"*)
+               echo "Already up-to-date with $SHA1"
+               continue
+               ;;
+       esac
+
+       CNT=`expr $CNT + 1`
+       PARENT="$PARENT -p $SHA1"
+
+       if test "$common,$NON_FF_MERGE" = "$MRC,0"
+       then
+               # The first head being merged was a fast-forward.
+               # Advance MRC to the head being merged, and use that
+               # tree as the intermediate result of the merge.
+               # We still need to count this as part of the parent set.
+
+               echo "Fast forwarding to: $SHA1"
+               git read-tree -u -m $head $SHA1 || exit
+               MRC=$SHA1 MRT=$(git write-tree)
+               continue
+       fi
+
+       NON_FF_MERGE=1
+
+       echo "Trying simple merge with $SHA1"
+       git read-tree -u -m --aggressive  $common $MRT $SHA1 || exit 2
+       next=$(git write-tree 2>/dev/null)
+       if test $? -ne 0
+       then
+               echo "Simple merge did not work, trying automatic merge."
+               git-merge-index -o git-merge-one-file -a ||
+               OCTOPUS_FAILURE=1
+               next=$(git write-tree 2>/dev/null)
+       fi
+
+       # We have merged the other branch successfully.  Ideally
+       # we could implement OR'ed heads in merge-base, and keep
+       # a list of commits we have merged so far in MRC to feed
+       # them to merge-base, but we approximate it by keep using
+       # the current MRC.  We used to update it to $common, which
+       # was incorrectly doing AND'ed merge-base here, which was
+       # unneeded.
+
+       MRT=$next
+done
+
+exit "$OCTOPUS_FAILURE"
diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh
new file mode 100755 (executable)
index 0000000..1e7727d
--- /dev/null
@@ -0,0 +1,136 @@
+#!/bin/sh
+#
+# Copyright (c) Linus Torvalds, 2005
+#
+# This is the git per-file merge script, called with
+#
+#   $1 - original file SHA1 (or empty)
+#   $2 - file in branch1 SHA1 (or empty)
+#   $3 - file in branch2 SHA1 (or empty)
+#   $4 - pathname in repository
+#   $5 - original file mode (or empty)
+#   $6 - file in branch1 mode (or empty)
+#   $7 - file in branch2 mode (or empty)
+#
+# Handle some trivial cases.. The _really_ trivial cases have
+# been handled already by git read-tree, but that one doesn't
+# do any merges that might change the tree layout.
+
+case "${1:-.}${2:-.}${3:-.}" in
+#
+# Deleted in both or deleted in one and unchanged in the other
+#
+"$1.." | "$1.$1" | "$1$1.")
+       if [ "$2" ]; then
+               echo "Removing $4"
+       else
+               # read-tree checked that index matches HEAD already,
+               # so we know we do not have this path tracked.
+               # there may be an unrelated working tree file here,
+               # which we should just leave unmolested.  Make sure
+               # we do not have it in the index, though.
+               exec git update-index --remove -- "$4"
+       fi
+       if test -f "$4"; then
+               rm -f -- "$4" &&
+               rmdir -p "$(expr "z$4" : 'z\(.*\)/')" 2>/dev/null || :
+       fi &&
+               exec git update-index --remove -- "$4"
+       ;;
+
+#
+# Added in one.
+#
+".$2.")
+       # the other side did not add and we added so there is nothing
+       # to be done, except making the path merged.
+       exec git update-index --add --cacheinfo "$6" "$2" "$4"
+       ;;
+"..$3")
+       echo "Adding $4"
+       test -f "$4" || {
+               echo "ERROR: untracked $4 is overwritten by the merge."
+               exit 1
+       }
+       git update-index --add --cacheinfo "$7" "$3" "$4" &&
+               exec git checkout-index -u -f -- "$4"
+       ;;
+
+#
+# Added in both, identically (check for same permissions).
+#
+".$3$2")
+       if [ "$6" != "$7" ]; then
+               echo "ERROR: File $4 added identically in both branches,"
+               echo "ERROR: but permissions conflict $6->$7."
+               exit 1
+       fi
+       echo "Adding $4"
+       git update-index --add --cacheinfo "$6" "$2" "$4" &&
+               exec git checkout-index -u -f -- "$4"
+       ;;
+
+#
+# Modified in both, but differently.
+#
+"$1$2$3" | ".$2$3")
+
+       case ",$6,$7," in
+       *,120000,*)
+               echo "ERROR: $4: Not merging symbolic link changes."
+               exit 1
+               ;;
+       esac
+
+       src2=`git-unpack-file $3`
+       case "$1" in
+       '')
+               echo "Added $4 in both, but differently."
+               # This extracts OUR file in $orig, and uses git apply to
+               # remove lines that are unique to ours.
+               orig=`git-unpack-file $2`
+               sz0=`wc -c <"$orig"`
+               diff -u -La/$orig -Lb/$orig $orig $src2 | git apply --no-add
+               sz1=`wc -c <"$orig"`
+
+               # If we do not have enough common material, it is not
+               # worth trying two-file merge using common subsections.
+               expr "$sz0" \< "$sz1" \* 2 >/dev/null || : >$orig
+               ;;
+       *)
+               echo "Auto-merging $4"
+               orig=`git-unpack-file $1`
+               ;;
+       esac
+
+       # Be careful for funny filename such as "-L" in "$4", which
+       # would confuse "merge" greatly.
+       src1=`git-unpack-file $2`
+       git merge-file "$src1" "$orig" "$src2"
+       ret=$?
+
+       # Create the working tree file, using "our tree" version from the
+       # index, and then store the result of the merge.
+       git checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4"
+       rm -f -- "$orig" "$src1" "$src2"
+
+       if [ "$6" != "$7" ]; then
+               echo "ERROR: Permissions conflict: $5->$6,$7."
+               ret=1
+       fi
+       if [ "$1" = '' ]; then
+               ret=1
+       fi
+
+       if [ $ret -ne 0 ]; then
+               echo "ERROR: Merge conflict in $4"
+               exit 1
+       fi
+       exec git update-index -- "$4"
+       ;;
+
+*)
+       echo "ERROR: $4: Not handling case $1 -> $2 -> $3"
+       ;;
+esac
+exit 1
diff --git a/git-merge-resolve.sh b/git-merge-resolve.sh
new file mode 100755 (executable)
index 0000000..93bcfc2
--- /dev/null
@@ -0,0 +1,54 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Linus Torvalds
+# Copyright (c) 2005 Junio C Hamano
+#
+# Resolve two trees, using enhanced multi-base read-tree.
+
+# The first parameters up to -- are merge bases; the rest are heads.
+bases= head= remotes= sep_seen=
+for arg
+do
+       case ",$sep_seen,$head,$arg," in
+       *,--,)
+               sep_seen=yes
+               ;;
+       ,yes,,*)
+               head=$arg
+               ;;
+       ,yes,*)
+               remotes="$remotes$arg "
+               ;;
+       *)
+               bases="$bases$arg "
+               ;;
+       esac
+done
+
+# Give up if we are given two or more remotes -- not handling octopus.
+case "$remotes" in
+?*' '?*)
+       exit 2 ;;
+esac
+
+# Give up if this is a baseless merge.
+if test '' = "$bases"
+then
+       exit 2
+fi
+
+git update-index --refresh 2>/dev/null
+git read-tree -u -m --aggressive $bases $head $remotes || exit 2
+echo "Trying simple merge."
+if result_tree=$(git write-tree  2>/dev/null)
+then
+       exit 0
+else
+       echo "Simple merge failed, trying Automatic merge."
+       if git-merge-index -o git-merge-one-file -a
+       then
+               exit 0
+       else
+               exit 1
+       fi
+fi
diff --git a/git-merge-stupid.sh b/git-merge-stupid.sh
new file mode 100755 (executable)
index 0000000..f612d47
--- /dev/null
@@ -0,0 +1,80 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Linus Torvalds
+#
+# Resolve two trees, 'stupid merge'.
+
+# The first parameters up to -- are merge bases; the rest are heads.
+bases= head= remotes= sep_seen=
+for arg
+do
+       case ",$sep_seen,$head,$arg," in
+       *,--,)
+               sep_seen=yes
+               ;;
+       ,yes,,*)
+               head=$arg
+               ;;
+       ,yes,*)
+               remotes="$remotes$arg "
+               ;;
+       *)
+               bases="$bases$arg "
+               ;;
+       esac
+done
+
+# Give up if we are given two or more remotes -- not handling octopus.
+case "$remotes" in
+?*' '?*)
+       exit 2 ;;
+esac
+
+# Find an optimum merge base if there are more than one candidates.
+case "$bases" in
+?*' '?*)
+       echo "Trying to find the optimum merge base."
+       G=.tmp-index$$
+       best=
+       best_cnt=-1
+       for c in $bases
+       do
+               rm -f $G
+               GIT_INDEX_FILE=$G git read-tree -m $c $head $remotes \
+                        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"
+       ;;
+*)
+       common="$bases"
+       ;;
+esac
+
+git update-index --refresh 2>/dev/null
+git read-tree -u -m $common $head $remotes || exit 2
+echo "Trying simple merge."
+if result_tree=$(git write-tree  2>/dev/null)
+then
+       exit 0
+else
+       echo "Simple merge failed, trying Automatic merge."
+       if git-merge-index -o git-merge-one-file -a
+       then
+               exit 0
+       else
+               exit 1
+       fi
+fi
diff --git a/git-merge.sh b/git-merge.sh
new file mode 100755 (executable)
index 0000000..1c123a3
--- /dev/null
@@ -0,0 +1,544 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+OPTIONS_KEEPDASHDASH=
+OPTIONS_SPEC="\
+git-merge [options] <remote>...
+git-merge [options] <msg> HEAD <remote>
+--
+summary              show a diffstat at the end of the merge
+n,no-summary         don't show a diffstat at the end of the merge
+squash               create a single commit instead of doing a merge
+commit               perform a commit if the merge sucesses (default)
+ff                   allow fast forward (default)
+s,strategy=          merge strategy to use
+m,message=           message to be used for the merge commit (if any)
+"
+
+SUBDIRECTORY_OK=Yes
+. git-sh-setup
+require_work_tree
+cd_to_toplevel
+
+test -z "$(git ls-files -u)" ||
+       die "You are in the middle of a conflicted merge."
+
+LF='
+'
+
+all_strategies='recur recursive octopus resolve stupid ours subtree'
+default_twohead_strategies='recursive'
+default_octopus_strategies='octopus'
+no_fast_forward_strategies='subtree ours'
+no_trivial_strategies='recursive recur subtree ours'
+use_strategies=
+
+allow_fast_forward=t
+allow_trivial_merge=t
+
+dropsave() {
+       rm -f -- "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/MERGE_MSG" \
+                "$GIT_DIR/MERGE_STASH" || exit 1
+}
+
+savestate() {
+       # Stash away any local modifications.
+       git stash create >"$GIT_DIR/MERGE_STASH"
+}
+
+restorestate() {
+        if test -f "$GIT_DIR/MERGE_STASH"
+       then
+               git reset --hard $head >/dev/null
+               git stash apply $(cat "$GIT_DIR/MERGE_STASH")
+               git update-index --refresh >/dev/null
+       fi
+}
+
+finish_up_to_date () {
+       case "$squash" in
+       t)
+               echo "$1 (nothing to squash)" ;;
+       '')
+               echo "$1" ;;
+       esac
+       dropsave
+}
+
+squash_message () {
+       echo Squashed commit of the following:
+       echo
+       git log --no-merges ^"$head" $remoteheads
+}
+
+finish () {
+       if test '' = "$2"
+       then
+               rlogm="$GIT_REFLOG_ACTION"
+       else
+               echo "$2"
+               rlogm="$GIT_REFLOG_ACTION: $2"
+       fi
+       case "$squash" in
+       t)
+               echo "Squash commit -- not updating HEAD"
+               squash_message >"$GIT_DIR/SQUASH_MSG"
+               ;;
+       '')
+               case "$merge_msg" in
+               '')
+                       echo "No merge message -- not updating HEAD"
+                       ;;
+               *)
+                       git update-ref -m "$rlogm" HEAD "$1" "$head" || exit 1
+                       git gc --auto
+                       ;;
+               esac
+               ;;
+       esac
+       case "$1" in
+       '')
+               ;;
+       ?*)
+               if test "$show_diffstat" = t
+               then
+                       # We want color (if set), but no pager
+                       GIT_PAGER='' git diff --stat --summary -M "$head" "$1"
+               fi
+               ;;
+       esac
+
+       # Run a post-merge hook
+        if test -x "$GIT_DIR"/hooks/post-merge
+        then
+           case "$squash" in
+           t)
+                "$GIT_DIR"/hooks/post-merge 1
+               ;;
+           '')
+                "$GIT_DIR"/hooks/post-merge 0
+               ;;
+           esac
+        fi
+}
+
+merge_name () {
+       remote="$1"
+       rh=$(git rev-parse --verify "$remote^0" 2>/dev/null) || return
+       bh=$(git show-ref -s --verify "refs/heads/$remote" 2>/dev/null)
+       if test "$rh" = "$bh"
+       then
+               echo "$rh               branch '$remote' of ."
+       elif truname=$(expr "$remote" : '\(.*\)~[1-9][0-9]*$') &&
+               git show-ref -q --verify "refs/heads/$truname" 2>/dev/null
+       then
+               echo "$rh               branch '$truname' (early part) of ."
+       elif test "$remote" = "FETCH_HEAD" -a -r "$GIT_DIR/FETCH_HEAD"
+       then
+               sed -e 's/      not-for-merge   /               /' -e 1q \
+                       "$GIT_DIR/FETCH_HEAD"
+       else
+               echo "$rh               commit '$remote'"
+       fi
+}
+
+parse_config () {
+       while test $# != 0; do
+               case "$1" in
+               -n|--no-summary)
+                       show_diffstat=false ;;
+               --summary)
+                       show_diffstat=t ;;
+               --squash)
+                       allow_fast_forward=t squash=t no_commit=t ;;
+               --no-squash)
+                       allow_fast_forward=t squash= no_commit= ;;
+               --commit)
+                       allow_fast_forward=t squash= no_commit= ;;
+               --no-commit)
+                       allow_fast_forward=t squash= no_commit=t ;;
+               --ff)
+                       allow_fast_forward=t squash= no_commit= ;;
+               --no-ff)
+                       allow_fast_forward=false squash= no_commit= ;;
+               -s|--strategy)
+                       shift
+                       case " $all_strategies " in
+                       *" $1 "*)
+                               use_strategies="$use_strategies$1 " ;;
+                       *)
+                               die "available strategies are: $all_strategies" ;;
+                       esac
+                       ;;
+               -m|--message)
+                       shift
+                       merge_msg="$1"
+                       have_message=t
+                       ;;
+               --)
+                       shift
+                       break ;;
+               *)      usage ;;
+               esac
+               shift
+       done
+       args_left=$#
+}
+
+test $# != 0 || usage
+
+have_message=
+
+if branch=$(git-symbolic-ref -q HEAD)
+then
+       mergeopts=$(git config "branch.${branch#refs/heads/}.mergeoptions")
+       if test -n "$mergeopts"
+       then
+               parse_config $mergeopts --
+       fi
+fi
+
+parse_config "$@"
+while test $args_left -lt $#; do shift; done
+
+if test -z "$show_diffstat"; then
+    test "$(git config --bool merge.diffstat)" = false && show_diffstat=false
+    test -z "$show_diffstat" && show_diffstat=t
+fi
+
+# This could be traditional "merge <msg> HEAD <commit>..."  and the
+# way we can tell it is to see if the second token is HEAD, but some
+# people might have misused the interface and used a committish that
+# is the same as HEAD there instead.  Traditional format never would
+# have "-m" so it is an additional safety measure to check for it.
+
+if test -z "$have_message" &&
+       second_token=$(git rev-parse --verify "$2^0" 2>/dev/null) &&
+       head_commit=$(git rev-parse --verify "HEAD" 2>/dev/null) &&
+       test "$second_token" = "$head_commit"
+then
+       merge_msg="$1"
+       shift
+       head_arg="$1"
+       shift
+elif ! git rev-parse --verify HEAD >/dev/null 2>&1
+then
+       # If the merged head is a valid one there is no reason to
+       # forbid "git merge" into a branch yet to be born.  We do
+       # the same for "git pull".
+       if test 1 -ne $#
+       then
+               echo >&2 "Can merge only exactly one commit into empty head"
+               exit 1
+       fi
+
+       rh=$(git rev-parse --verify "$1^0") ||
+               die "$1 - not something we can merge"
+
+       git update-ref -m "initial pull" HEAD "$rh" "" &&
+       git read-tree --reset -u HEAD
+       exit
+
+else
+       # We are invoked directly as the first-class UI.
+       head_arg=HEAD
+
+       # All the rest are the commits being merged; prepare
+       # the standard merge summary message to be appended to
+       # the given message.  If remote is invalid we will die
+       # later in the common codepath so we discard the error
+       # in this loop.
+       merge_name=$(for remote
+               do
+                       merge_name "$remote"
+               done | git fmt-merge-msg
+       )
+       merge_msg="${merge_msg:+$merge_msg$LF$LF}$merge_name"
+fi
+head=$(git rev-parse --verify "$head_arg"^0) || usage
+
+# All the rest are remote heads
+test "$#" = 0 && usage ;# we need at least one remote head.
+set_reflog_action "merge $*"
+
+remoteheads=
+for remote
+do
+       remotehead=$(git rev-parse --verify "$remote"^0 2>/dev/null) ||
+           die "$remote - not something we can merge"
+       remoteheads="${remoteheads}$remotehead "
+       eval GITHEAD_$remotehead='"$remote"'
+       export GITHEAD_$remotehead
+done
+set x $remoteheads ; shift
+
+case "$use_strategies" in
+'')
+       case "$#" in
+       1)
+               var="`git config --get pull.twohead`"
+               if test -n "$var"
+               then
+                       use_strategies="$var"
+               else
+                       use_strategies="$default_twohead_strategies"
+               fi ;;
+       *)
+               var="`git config --get pull.octopus`"
+               if test -n "$var"
+               then
+                       use_strategies="$var"
+               else
+                       use_strategies="$default_octopus_strategies"
+               fi ;;
+       esac
+       ;;
+esac
+
+for s in $use_strategies
+do
+       for ss in $no_fast_forward_strategies
+       do
+               case " $s " in
+               *" $ss "*)
+                       allow_fast_forward=f
+                       break
+                       ;;
+               esac
+       done
+       for ss in $no_trivial_strategies
+       do
+               case " $s " in
+               *" $ss "*)
+                       allow_trivial_merge=f
+                       break
+                       ;;
+               esac
+       done
+done
+
+case "$#" in
+1)
+       common=$(git merge-base --all $head "$@")
+       ;;
+*)
+       common=$(git show-branch --merge-base $head "$@")
+       ;;
+esac
+echo "$head" >"$GIT_DIR/ORIG_HEAD"
+
+case "$allow_fast_forward,$#,$common,$no_commit" in
+?,*,'',*)
+       # No common ancestors found. We need a real merge.
+       ;;
+?,1,"$1",*)
+       # If head can reach all the merge then we are up to date.
+       # but first the most common case of merging one remote.
+       finish_up_to_date "Already up-to-date."
+       exit 0
+       ;;
+t,1,"$head",*)
+       # Again the most common case of merging one remote.
+       echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $1)"
+       git update-index --refresh 2>/dev/null
+       msg="Fast forward"
+       if test -n "$have_message"
+       then
+               msg="$msg (no commit created; -m option ignored)"
+       fi
+       new_head=$(git rev-parse --verify "$1^0") &&
+       git read-tree -v -m -u --exclude-per-directory=.gitignore $head "$new_head" &&
+       finish "$new_head" "$msg" || exit
+       dropsave
+       exit 0
+       ;;
+?,1,?*"$LF"?*,*)
+       # We are not doing octopus and not fast forward.  Need a
+       # real merge.
+       ;;
+?,1,*,)
+       # We are not doing octopus, not fast forward, and have only
+       # one common.
+       git update-index --refresh 2>/dev/null
+       case "$allow_trivial_merge" in
+       t)
+               # See if it is really trivial.
+               git var GIT_COMMITTER_IDENT >/dev/null || exit
+               echo "Trying really trivial in-index merge..."
+               if git read-tree --trivial -m -u -v $common $head "$1" &&
+                  result_tree=$(git write-tree)
+               then
+                       echo "Wonderful."
+                       result_commit=$(
+                               printf '%s\n' "$merge_msg" |
+                               git commit-tree $result_tree -p HEAD -p "$1"
+                       ) || exit
+                       finish "$result_commit" "In-index merge"
+                       dropsave
+                       exit 0
+               fi
+               echo "Nope."
+       esac
+       ;;
+*)
+       # An octopus.  If we can reach all the remote we are up to date.
+       up_to_date=t
+       for remote
+       do
+               common_one=$(git merge-base --all $head $remote)
+               if test "$common_one" != "$remote"
+               then
+                       up_to_date=f
+                       break
+               fi
+       done
+       if test "$up_to_date" = t
+       then
+               finish_up_to_date "Already up-to-date. Yeeah!"
+               exit 0
+       fi
+       ;;
+esac
+
+# We are going to make a new commit.
+git var GIT_COMMITTER_IDENT >/dev/null || exit
+
+# At this point, we need a real merge.  No matter what strategy
+# we use, it would operate on the index, possibly affecting the
+# working tree, and when resolved cleanly, have the desired tree
+# in the index -- this means that the index must be in sync with
+# the $head commit.  The strategies are responsible to ensure this.
+
+case "$use_strategies" in
+?*' '?*)
+    # Stash away the local changes so that we can try more than one.
+    savestate
+    single_strategy=no
+    ;;
+*)
+    rm -f "$GIT_DIR/MERGE_STASH"
+    single_strategy=yes
+    ;;
+esac
+
+result_tree= best_cnt=-1 best_strategy= wt_strategy=
+merge_was_ok=
+for strategy in $use_strategies
+do
+    test "$wt_strategy" = '' || {
+       echo "Rewinding the tree to pristine..."
+       restorestate
+    }
+    case "$single_strategy" in
+    no)
+       echo "Trying merge strategy $strategy..."
+       ;;
+    esac
+
+    # Remember which strategy left the state in the working tree
+    wt_strategy=$strategy
+
+    git-merge-$strategy $common -- "$head_arg" "$@"
+    exit=$?
+    if test "$no_commit" = t && test "$exit" = 0
+    then
+        merge_was_ok=t
+       exit=1 ;# pretend it left conflicts.
+    fi
+
+    test "$exit" = 0 || {
+
+       # The backend exits with 1 when conflicts are left to be resolved,
+       # with 2 when it does not handle the given merge at all.
+
+       if test "$exit" -eq 1
+       then
+           cnt=`{
+               git diff-files --name-only
+               git ls-files --unmerged
+           } | wc -l`
+           if test $best_cnt -le 0 -o $cnt -le $best_cnt
+           then
+               best_strategy=$strategy
+               best_cnt=$cnt
+           fi
+       fi
+       continue
+    }
+
+    # Automerge succeeded.
+    result_tree=$(git write-tree) && break
+done
+
+# If we have a resulting tree, that means the strategy module
+# auto resolved the merge cleanly.
+if test '' != "$result_tree"
+then
+    if test "$allow_fast_forward" = "t"
+    then
+        parents=$(git show-branch --independent "$head" "$@")
+    else
+        parents=$(git rev-parse "$head" "$@")
+    fi
+    parents=$(echo "$parents" | sed -e 's/^/-p /')
+    result_commit=$(printf '%s\n' "$merge_msg" | git commit-tree $result_tree $parents) || exit
+    finish "$result_commit" "Merge made by $wt_strategy."
+    dropsave
+    exit 0
+fi
+
+# Pick the result from the best strategy and have the user fix it up.
+case "$best_strategy" in
+'')
+       restorestate
+       case "$use_strategies" in
+       ?*' '?*)
+               echo >&2 "No merge strategy handled the merge."
+               ;;
+       *)
+               echo >&2 "Merge with strategy $use_strategies failed."
+               ;;
+       esac
+       exit 2
+       ;;
+"$wt_strategy")
+       # We already have its result in the working tree.
+       ;;
+*)
+       echo "Rewinding the tree to pristine..."
+       restorestate
+       echo "Using the $best_strategy to prepare resolving by hand."
+       git-merge-$best_strategy $common -- "$head_arg" "$@"
+       ;;
+esac
+
+if test "$squash" = t
+then
+       finish
+else
+       for remote
+       do
+               echo $remote
+       done >"$GIT_DIR/MERGE_HEAD"
+       printf '%s\n' "$merge_msg" >"$GIT_DIR/MERGE_MSG"
+fi
+
+if test "$merge_was_ok" = t
+then
+       echo >&2 \
+       "Automatic merge went well; stopped before committing as requested"
+       exit 0
+else
+       {
+           echo '
+Conflicts:
+'
+               git ls-files --unmerged |
+               sed -e 's/^[^   ]*      /       /' |
+               uniq
+       } >>"$GIT_DIR/MERGE_MSG"
+       git rerere
+       die "Automatic merge failed; fix conflicts and then commit the result."
+fi
diff --git a/git-mergetool.sh b/git-mergetool.sh
new file mode 100755 (executable)
index 0000000..5587c5e
--- /dev/null
@@ -0,0 +1,408 @@
+#!/bin/sh
+#
+# This program resolves merge conflicts in git
+#
+# Copyright (c) 2006 Theodore Y. Ts'o
+#
+# This file is licensed under the GPL v2, or a later version
+# at the discretion of Junio C Hamano.
+#
+
+USAGE='[--tool=tool] [file to merge] ...'
+SUBDIRECTORY_OK=Yes
+OPTIONS_SPEC=
+. git-sh-setup
+require_work_tree
+prefix=$(git rev-parse --show-prefix)
+
+# Returns true if the mode reflects a symlink
+is_symlink () {
+    test "$1" = 120000
+}
+
+local_present () {
+    test -n "$local_mode"
+}
+
+remote_present () {
+    test -n "$remote_mode"
+}
+
+base_present () {
+    test -n "$base_mode"
+}
+
+cleanup_temp_files () {
+    if test "$1" = --save-backup ; then
+       mv -- "$BACKUP" "$path.orig"
+       rm -f -- "$LOCAL" "$REMOTE" "$BASE"
+    else
+       rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
+    fi
+}
+
+describe_file () {
+    mode="$1"
+    branch="$2"
+    file="$3"
+
+    printf "  {%s}: " "$branch"
+    if test -z "$mode"; then
+       echo "deleted"
+    elif is_symlink "$mode" ; then
+       echo "a symbolic link -> '$(cat "$file")'"
+    else
+       if base_present; then
+           echo "modified"
+       else
+           echo "created"
+       fi
+    fi
+}
+
+
+resolve_symlink_merge () {
+    while true; do
+       printf "Use (l)ocal or (r)emote, or (a)bort? "
+       read ans
+       case "$ans" in
+           [lL]*)
+               git checkout-index -f --stage=2 -- "$path"
+               git add -- "$path"
+               cleanup_temp_files --save-backup
+               return
+               ;;
+           [rR]*)
+               git checkout-index -f --stage=3 -- "$path"
+               git add -- "$path"
+               cleanup_temp_files --save-backup
+               return
+               ;;
+           [aA]*)
+               exit 1
+               ;;
+           esac
+       done
+}
+
+resolve_deleted_merge () {
+    while true; do
+       if base_present; then
+           printf "Use (m)odified or (d)eleted file, or (a)bort? "
+       else
+           printf "Use (c)reated or (d)eleted file, or (a)bort? "
+       fi
+       read ans
+       case "$ans" in
+           [mMcC]*)
+               git add -- "$path"
+               cleanup_temp_files --save-backup
+               return
+               ;;
+           [dD]*)
+               git rm -- "$path" > /dev/null
+               cleanup_temp_files
+               return
+               ;;
+           [aA]*)
+               exit 1
+               ;;
+           esac
+       done
+}
+
+check_unchanged () {
+    if test "$path" -nt "$BACKUP" ; then
+       status=0;
+    else
+       while true; do
+           echo "$path seems unchanged."
+           printf "Was the merge successful? [y/n] "
+           read answer < /dev/tty
+           case "$answer" in
+               y*|Y*) status=0; break ;;
+               n*|N*) status=1; break ;;
+           esac
+       done
+    fi
+}
+
+save_backup () {
+    if test "$status" -eq 0; then
+       mv -- "$BACKUP" "$path.orig"
+    fi
+}
+
+remove_backup () {
+    if test "$status" -eq 0; then
+       rm "$BACKUP"
+    fi
+}
+
+merge_file () {
+    path="$1"
+
+    f=`git ls-files -u -- "$path"`
+    if test -z "$f" ; then
+       if test ! -f "$path" ; then
+           echo "$path: file not found"
+       else
+           echo "$path: file does not need merging"
+       fi
+       exit 1
+    fi
+
+    BACKUP="$path.BACKUP.$$"
+    LOCAL="$path.LOCAL.$$"
+    REMOTE="$path.REMOTE.$$"
+    BASE="$path.BASE.$$"
+
+    mv -- "$path" "$BACKUP"
+    cp -- "$BACKUP" "$path"
+
+    base_mode=`git ls-files -u -- "$path" | awk '{if ($3==1) print $1;}'`
+    local_mode=`git ls-files -u -- "$path" | awk '{if ($3==2) print $1;}'`
+    remote_mode=`git ls-files -u -- "$path" | awk '{if ($3==3) print $1;}'`
+
+    base_present   && git cat-file blob ":1:$prefix$path" >"$BASE" 2>/dev/null
+    local_present  && git cat-file blob ":2:$prefix$path" >"$LOCAL" 2>/dev/null
+    remote_present && git cat-file blob ":3:$prefix$path" >"$REMOTE" 2>/dev/null
+
+    if test -z "$local_mode" -o -z "$remote_mode"; then
+       echo "Deleted merge conflict for '$path':"
+       describe_file "$local_mode" "local" "$LOCAL"
+       describe_file "$remote_mode" "remote" "$REMOTE"
+       resolve_deleted_merge
+       return
+    fi
+
+    if is_symlink "$local_mode" || is_symlink "$remote_mode"; then
+       echo "Symbolic link merge conflict for '$path':"
+       describe_file "$local_mode" "local" "$LOCAL"
+       describe_file "$remote_mode" "remote" "$REMOTE"
+       resolve_symlink_merge
+       return
+    fi
+
+    echo "Normal merge conflict for '$path':"
+    describe_file "$local_mode" "local" "$LOCAL"
+    describe_file "$remote_mode" "remote" "$REMOTE"
+    printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
+    read ans
+
+    case "$merge_tool" in
+       kdiff3)
+           if base_present ; then
+               ("$merge_tool_path" --auto --L1 "$path (Base)" --L2 "$path (Local)" --L3 "$path (Remote)" \
+                   -o "$path" -- "$BASE" "$LOCAL" "$REMOTE" > /dev/null 2>&1)
+           else
+               ("$merge_tool_path" --auto --L1 "$path (Local)" --L2 "$path (Remote)" \
+                   -o "$path" -- "$LOCAL" "$REMOTE" > /dev/null 2>&1)
+           fi
+           status=$?
+           remove_backup
+           ;;
+       tkdiff)
+           if base_present ; then
+               "$merge_tool_path" -a "$BASE" -o "$path" -- "$LOCAL" "$REMOTE"
+           else
+               "$merge_tool_path" -o "$path" -- "$LOCAL" "$REMOTE"
+           fi
+           status=$?
+           save_backup
+           ;;
+       meld|vimdiff)
+           touch "$BACKUP"
+           "$merge_tool_path" -- "$LOCAL" "$path" "$REMOTE"
+           check_unchanged
+           save_backup
+           ;;
+       gvimdiff)
+               touch "$BACKUP"
+               "$merge_tool_path" -f -- "$LOCAL" "$path" "$REMOTE"
+               check_unchanged
+               save_backup
+               ;;
+       xxdiff)
+           touch "$BACKUP"
+           if base_present ; then
+               "$merge_tool_path" -X --show-merged-pane \
+                   -R 'Accel.SaveAsMerged: "Ctrl-S"' \
+                   -R 'Accel.Search: "Ctrl+F"' \
+                   -R 'Accel.SearchForward: "Ctrl-G"' \
+                   --merged-file "$path" -- "$LOCAL" "$BASE" "$REMOTE"
+           else
+               "$merge_tool_path" -X --show-merged-pane \
+                   -R 'Accel.SaveAsMerged: "Ctrl-S"' \
+                   -R 'Accel.Search: "Ctrl+F"' \
+                   -R 'Accel.SearchForward: "Ctrl-G"' \
+                   --merged-file "$path" -- "$LOCAL" "$REMOTE"
+           fi
+           check_unchanged
+           save_backup
+           ;;
+       opendiff)
+           touch "$BACKUP"
+           if base_present; then
+               "$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$path" | cat
+           else
+               "$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$path" | cat
+           fi
+           check_unchanged
+           save_backup
+           ;;
+       ecmerge)
+           touch "$BACKUP"
+           if base_present; then
+               "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --mode=merge3 --to="$path"
+           else
+               "$merge_tool_path" "$LOCAL" "$REMOTE" --mode=merge2 --to="$path"
+           fi
+           check_unchanged
+           save_backup
+           ;;
+       emerge)
+           if base_present ; then
+               "$merge_tool_path" -f emerge-files-with-ancestor-command "$LOCAL" "$REMOTE" "$BASE" "$(basename "$path")"
+           else
+               "$merge_tool_path" -f emerge-files-command "$LOCAL" "$REMOTE" "$(basename "$path")"
+           fi
+           status=$?
+           save_backup
+           ;;
+    esac
+    if test "$status" -ne 0; then
+       echo "merge of $path failed" 1>&2
+       mv -- "$BACKUP" "$path"
+       exit 1
+    fi
+    git add -- "$path"
+    cleanup_temp_files
+}
+
+while test $# != 0
+do
+    case "$1" in
+       -t|--tool*)
+           case "$#,$1" in
+               *,*=*)
+                   merge_tool=`expr "z$1" : 'z-[^=]*=\(.*\)'`
+                   ;;
+               1,*)
+                   usage ;;
+               *)
+                   merge_tool="$2"
+                   shift ;;
+           esac
+           ;;
+       --)
+           break
+           ;;
+       -*)
+           usage
+           ;;
+       *)
+           break
+           ;;
+    esac
+    shift
+done
+
+valid_tool() {
+       case "$1" in
+               kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
+                       ;; # happy
+               *)
+                       return 1
+                       ;;
+       esac
+}
+
+init_merge_tool_path() {
+       merge_tool_path=`git config mergetool.$1.path`
+       if test -z "$merge_tool_path" ; then
+               case "$1" in
+                       emerge)
+                               merge_tool_path=emacs
+                               ;;
+                       *)
+                               merge_tool_path=$1
+                               ;;
+               esac
+       fi
+}
+
+
+if test -z "$merge_tool"; then
+    merge_tool=`git config merge.tool`
+    if test -n "$merge_tool" && ! valid_tool "$merge_tool"; then
+           echo >&2 "git config option merge.tool set to unknown tool: $merge_tool"
+           echo >&2 "Resetting to default..."
+           unset merge_tool
+    fi
+fi
+
+if test -z "$merge_tool" ; then
+    if test -n "$DISPLAY"; then
+        merge_tool_candidates="kdiff3 tkdiff xxdiff meld gvimdiff"
+        if test -n "$GNOME_DESKTOP_SESSION_ID" ; then
+            merge_tool_candidates="meld $merge_tool_candidates"
+        fi
+        if test "$KDE_FULL_SESSION" = "true"; then
+            merge_tool_candidates="kdiff3 $merge_tool_candidates"
+        fi
+    fi
+    if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
+        merge_tool_candidates="$merge_tool_candidates emerge"
+    fi
+    if echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
+        merge_tool_candidates="$merge_tool_candidates vimdiff"
+    fi
+    merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
+    echo "merge tool candidates: $merge_tool_candidates"
+    for i in $merge_tool_candidates; do
+        init_merge_tool_path $i
+        if type "$merge_tool_path" > /dev/null 2>&1; then
+            merge_tool=$i
+            break
+        fi
+    done
+    if test -z "$merge_tool" ; then
+       echo "No known merge resolution program available."
+       exit 1
+    fi
+else
+    if ! valid_tool "$merge_tool"; then
+        echo >&2 "Unknown merge_tool $merge_tool"
+        exit 1
+    fi
+
+    init_merge_tool_path "$merge_tool"
+
+    if ! type "$merge_tool_path" > /dev/null 2>&1; then
+        echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
+        exit 1
+    fi
+fi
+
+
+if test $# -eq 0 ; then
+       files=`git ls-files -u | sed -e 's/^[^  ]*      //' | sort -u`
+       if test -z "$files" ; then
+               echo "No files need merging"
+               exit 0
+       fi
+       echo Merging the files: $files
+       git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u | while read i
+       do
+               printf "\n"
+               merge_file "$i" < /dev/tty > /dev/tty
+       done
+else
+       while test $# -gt 0; do
+               printf "\n"
+               merge_file "$1"
+               shift
+       done
+fi
+exit 0
diff --git a/git-parse-remote.sh b/git-parse-remote.sh
new file mode 100755 (executable)
index 0000000..695a409
--- /dev/null
@@ -0,0 +1,264 @@
+#!/bin/sh
+
+# git-ls-remote could be called from outside a git managed repository;
+# this would fail in that case and would issue an error message.
+GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) || :;
+
+get_data_source () {
+       case "$1" in
+       */*)
+               echo ''
+               ;;
+       .)
+               echo self
+               ;;
+       *)
+               if test "$(git config --get "remote.$1.url")"
+               then
+                       echo config
+               elif test -f "$GIT_DIR/remotes/$1"
+               then
+                       echo remotes
+               elif test -f "$GIT_DIR/branches/$1"
+               then
+                       echo branches
+               else
+                       echo ''
+               fi ;;
+       esac
+}
+
+get_remote_url () {
+       data_source=$(get_data_source "$1")
+       case "$data_source" in
+       '')
+               echo "$1"
+               ;;
+       self)
+               echo "$1"
+               ;;
+       config)
+               git config --get "remote.$1.url"
+               ;;
+       remotes)
+               sed -ne '/^URL: */{
+                       s///p
+                       q
+               }' "$GIT_DIR/remotes/$1"
+               ;;
+       branches)
+               sed -e 's/#.*//' "$GIT_DIR/branches/$1"
+               ;;
+       *)
+               die "internal error: get-remote-url $1" ;;
+       esac
+}
+
+get_default_remote () {
+       curr_branch=$(git symbolic-ref -q HEAD | sed -e 's|^refs/heads/||')
+       origin=$(git config --get "branch.$curr_branch.remote")
+       echo ${origin:-origin}
+}
+
+get_remote_default_refs_for_push () {
+       data_source=$(get_data_source "$1")
+       case "$data_source" in
+       '' | branches | self)
+               ;; # no default push mapping, just send matching refs.
+       config)
+               git config --get-all "remote.$1.push" ;;
+       remotes)
+               sed -ne '/^Push: */{
+                       s///p
+               }' "$GIT_DIR/remotes/$1" ;;
+       *)
+               die "internal error: get-remote-default-ref-for-push $1" ;;
+       esac
+}
+
+# Called from canon_refs_list_for_fetch -d "$remote", which
+# is called from get_remote_default_refs_for_fetch to grok
+# refspecs that are retrieved from the configuration, but not
+# from get_remote_refs_for_fetch when it deals with refspecs
+# supplied on the command line.  $ls_remote_result has the list
+# of refs available at remote.
+#
+# The first token returned is either "explicit" or "glob"; this
+# is to help prevent randomly "globbed" ref from being chosen as
+# a merge candidate
+expand_refs_wildcard () {
+       echo "$ls_remote_result" |
+       git fetch--tool expand-refs-wildcard "-" "$@"
+}
+
+# Subroutine to canonicalize remote:local notation.
+canon_refs_list_for_fetch () {
+       # If called from get_remote_default_refs_for_fetch
+       # leave the branches in branch.${curr_branch}.merge alone,
+       # or the first one otherwise; add prefix . to the rest
+       # to prevent the secondary branches to be merged by default.
+       merge_branches=
+       curr_branch=
+       if test "$1" = "-d"
+       then
+               shift ; remote="$1" ; shift
+               set $(expand_refs_wildcard "$remote" "$@")
+               is_explicit="$1"
+               shift
+               if test "$remote" = "$(get_default_remote)"
+               then
+                       curr_branch=$(git symbolic-ref -q HEAD | \
+                           sed -e 's|^refs/heads/||')
+                       merge_branches=$(git config \
+                           --get-all "branch.${curr_branch}.merge")
+               fi
+               if test -z "$merge_branches" && test $is_explicit != explicit
+               then
+                       merge_branches=..this.will.never.match.any.ref..
+               fi
+       fi
+       for ref
+       do
+               force=
+               case "$ref" in
+               +*)
+                       ref=$(expr "z$ref" : 'z+\(.*\)')
+                       force=+
+                       ;;
+               esac
+               expr "z$ref" : 'z.*:' >/dev/null || ref="${ref}:"
+               remote=$(expr "z$ref" : 'z\([^:]*\):')
+               local=$(expr "z$ref" : 'z[^:]*:\(.*\)')
+               dot_prefix=.
+               if test -z "$merge_branches"
+               then
+                       merge_branches=$remote
+                       dot_prefix=
+               else
+                       for merge_branch in $merge_branches
+                       do
+                           [ "$remote" = "$merge_branch" ] &&
+                           dot_prefix= && break
+                       done
+               fi
+               case "$remote" in
+               '' | HEAD ) remote=HEAD ;;
+               refs/*) ;;
+               heads/* | tags/* | remotes/* ) remote="refs/$remote" ;;
+               *) remote="refs/heads/$remote" ;;
+               esac
+               case "$local" in
+               '') local= ;;
+               refs/*) ;;
+               heads/* | tags/* | remotes/* ) local="refs/$local" ;;
+               *) local="refs/heads/$local" ;;
+               esac
+
+               if local_ref_name=$(expr "z$local" : 'zrefs/\(.*\)')
+               then
+                  git check-ref-format "$local_ref_name" ||
+                  die "* refusing to create funny ref '$local_ref_name' locally"
+               fi
+               echo "${dot_prefix}${force}${remote}:${local}"
+       done
+}
+
+# Returns list of src: (no store), or src:dst (store)
+get_remote_default_refs_for_fetch () {
+       data_source=$(get_data_source "$1")
+       case "$data_source" in
+       '')
+               echo "HEAD:" ;;
+       self)
+               canon_refs_list_for_fetch -d "$1" \
+                       $(git for-each-ref --format='%(refname):')
+               ;;
+       config)
+               canon_refs_list_for_fetch -d "$1" \
+                       $(git config --get-all "remote.$1.fetch") ;;
+       branches)
+               remote_branch=$(sed -ne '/#/s/.*#//p' "$GIT_DIR/branches/$1")
+               case "$remote_branch" in '') remote_branch=master ;; esac
+               echo "refs/heads/${remote_branch}:refs/heads/$1"
+               ;;
+       remotes)
+               canon_refs_list_for_fetch -d "$1" $(sed -ne '/^Pull: */{
+                                               s///p
+                                       }' "$GIT_DIR/remotes/$1")
+               ;;
+       *)
+               die "internal error: get-remote-default-ref-for-fetch $1" ;;
+       esac
+}
+
+get_remote_refs_for_push () {
+       case "$#" in
+       0) die "internal error: get-remote-refs-for-push." ;;
+       1) get_remote_default_refs_for_push "$@" ;;
+       *) shift; echo "$@" ;;
+       esac
+}
+
+get_remote_refs_for_fetch () {
+       case "$#" in
+       0)
+           die "internal error: get-remote-refs-for-fetch." ;;
+       1)
+           get_remote_default_refs_for_fetch "$@" ;;
+       *)
+           shift
+           tag_just_seen=
+           for ref
+           do
+               if test "$tag_just_seen"
+               then
+                   echo "refs/tags/${ref}:refs/tags/${ref}"
+                   tag_just_seen=
+                   continue
+               else
+                   case "$ref" in
+                   tag)
+                       tag_just_seen=yes
+                       continue
+                       ;;
+                   esac
+               fi
+               canon_refs_list_for_fetch "$ref"
+           done
+           ;;
+       esac
+}
+
+resolve_alternates () {
+       # original URL (xxx.git)
+       top_=`expr "z$1" : 'z\([^:]*:/*[^/]*\)/'`
+       while read path
+       do
+               case "$path" in
+               \#* | '')
+                       continue ;;
+               /*)
+                       echo "$top_$path/" ;;
+               ../*)
+                       # relative -- ugly but seems to work.
+                       echo "$1/objects/$path/" ;;
+               *)
+                       # exit code may not be caught by the reader.
+                       echo "bad alternate: $path"
+                       exit 1 ;;
+               esac
+       done
+}
+
+get_uploadpack () {
+       data_source=$(get_data_source "$1")
+       case "$data_source" in
+       config)
+               uplp=$(git config --get "remote.$1.uploadpack")
+               echo ${uplp:-git-upload-pack}
+               ;;
+       *)
+               echo "git-upload-pack"
+               ;;
+       esac
+}
diff --git a/git-pull.sh b/git-pull.sh
new file mode 100755 (executable)
index 0000000..698e82b
--- /dev/null
@@ -0,0 +1,155 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+# Fetch one or more remote refs and merge it/them into the current HEAD.
+
+USAGE='[-n | --no-summary] [--[no-]commit] [--[no-]squash] [--[no-]ff] [-s strategy]... [<fetch-options>] <repo> <head>...'
+LONG_USAGE='Fetch one or more remote refs and merge it/them into the current HEAD.'
+SUBDIRECTORY_OK=Yes
+OPTIONS_SPEC=
+. git-sh-setup
+set_reflog_action "pull $*"
+require_work_tree
+cd_to_toplevel
+
+test -z "$(git ls-files -u)" ||
+       die "You are in the middle of a conflicted merge."
+
+strategy_args= no_summary= no_commit= squash= no_ff=
+curr_branch=$(git symbolic-ref -q HEAD)
+curr_branch_short=$(echo "$curr_branch" | sed "s|refs/heads/||")
+rebase=$(git config --bool branch.$curr_branch_short.rebase)
+while :
+do
+       case "$1" in
+       -n|--n|--no|--no-|--no-s|--no-su|--no-sum|--no-summ|\
+               --no-summa|--no-summar|--no-summary)
+               no_summary=-n ;;
+       --summary)
+               no_summary=$1
+               ;;
+       --no-c|--no-co|--no-com|--no-comm|--no-commi|--no-commit)
+               no_commit=--no-commit ;;
+       --c|--co|--com|--comm|--commi|--commit)
+               no_commit=--commit ;;
+       --sq|--squ|--squa|--squas|--squash)
+               squash=--squash ;;
+       --no-sq|--no-squ|--no-squa|--no-squas|--no-squash)
+               squash=--no-squash ;;
+       --ff)
+               no_ff=--ff ;;
+       --no-ff)
+               no_ff=--no-ff ;;
+       -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\
+               --strateg=*|--strategy=*|\
+       -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy)
+               case "$#,$1" in
+               *,*=*)
+                       strategy=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;;
+               1,*)
+                       usage ;;
+               *)
+                       strategy="$2"
+                       shift ;;
+               esac
+               strategy_args="${strategy_args}-s $strategy "
+               ;;
+       -r|--r|--re|--reb|--reba|--rebas|--rebase)
+               rebase=true
+               ;;
+       --no-r|--no-re|--no-reb|--no-reba|--no-rebas|--no-rebase)
+               rebase=false
+               ;;
+       -h|--h|--he|--hel|--help)
+               usage
+               ;;
+       *)
+               # Pass thru anything that may be meant for fetch.
+               break
+               ;;
+       esac
+       shift
+done
+
+orig_head=$(git rev-parse --verify HEAD 2>/dev/null)
+git-fetch --update-head-ok "$@" || exit 1
+
+curr_head=$(git rev-parse --verify HEAD 2>/dev/null)
+if test "$curr_head" != "$orig_head"
+then
+       # The fetch involved updating the current branch.
+
+       # The working tree and the index file is still based on the
+       # $orig_head commit, but we are merging into $curr_head.
+       # First update the working tree to match $curr_head.
+
+       echo >&2 "Warning: fetch updated the current branch head."
+       echo >&2 "Warning: fast forwarding your working tree from"
+       echo >&2 "Warning: commit $orig_head."
+       git update-index --refresh 2>/dev/null
+       git read-tree -u -m "$orig_head" "$curr_head" ||
+               die 'Cannot fast-forward your working tree.
+After making sure that you saved anything precious from
+$ git diff '$orig_head'
+output, run
+$ git reset --hard
+to recover.'
+
+fi
+
+merge_head=$(sed -e '/ not-for-merge   /d' \
+       -e 's/  .*//' "$GIT_DIR"/FETCH_HEAD | \
+       tr '\012' ' ')
+
+case "$merge_head" in
+'')
+       case $? in
+         0) ;;
+         1) echo >&2 "You are not currently on a branch; you must explicitly"
+            echo >&2 "specify which branch you wish to merge:"
+            echo >&2 "  git pull <remote> <branch>"
+            exit 1;;
+         *) exit $?;;
+       esac
+       curr_branch=${curr_branch#refs/heads/}
+
+       echo >&2 "You asked me to pull without telling me which branch you"
+       echo >&2 "want to merge with, and 'branch.${curr_branch}.merge' in"
+       echo >&2 "your configuration file does not tell me either.  Please"
+       echo >&2 "name which branch you want to merge on the command line and"
+       echo >&2 "try again (e.g. 'git pull <repository> <refspec>')."
+       echo >&2 "See git-pull(1) for details on the refspec."
+       echo >&2
+       echo >&2 "If you often merge with the same branch, you may want to"
+       echo >&2 "configure the following variables in your configuration"
+       echo >&2 "file:"
+       echo >&2
+       echo >&2 "    branch.${curr_branch}.remote = <nickname>"
+       echo >&2 "    branch.${curr_branch}.merge = <remote-ref>"
+       echo >&2 "    remote.<nickname>.url = <url>"
+       echo >&2 "    remote.<nickname>.fetch = <refspec>"
+       echo >&2
+       echo >&2 "See git-config(1) for details."
+       exit 1
+       ;;
+?*' '?*)
+       if test -z "$orig_head"
+       then
+               echo >&2 "Cannot merge multiple branches into empty head"
+               exit 1
+       fi
+       ;;
+esac
+
+if test -z "$orig_head"
+then
+       git update-ref -m "initial pull" HEAD $merge_head "" &&
+       git read-tree --reset -u HEAD || exit 1
+       exit
+fi
+
+merge_name=$(git fmt-merge-msg <"$GIT_DIR/FETCH_HEAD") || exit
+test true = "$rebase" && exec git-rebase $merge_head
+exec git-merge $no_summary $no_commit $squash $no_ff $strategy_args \
+       "$merge_name" HEAD $merge_head
diff --git a/git-quiltimport.sh b/git-quiltimport.sh
new file mode 100755 (executable)
index 0000000..233e5ea
--- /dev/null
@@ -0,0 +1,122 @@
+#!/bin/sh
+OPTIONS_KEEPDASHDASH=
+OPTIONS_SPEC="\
+git-quiltimport [options]
+--
+n,dry-run     dry run
+author=       author name and email address for patches without any
+patches=      path to the quilt series and patches
+"
+SUBDIRECTORY_ON=Yes
+. git-sh-setup
+
+dry_run=""
+quilt_author=""
+while test $# != 0
+do
+       case "$1" in
+       --author)
+               shift
+               quilt_author="$1"
+               ;;
+       -n|--dry-run)
+               dry_run=1
+               ;;
+       --patches)
+               shift
+               QUILT_PATCHES="$1"
+               ;;
+       --)
+               shift
+               break;;
+       *)
+               usage
+               ;;
+       esac
+       shift
+done
+
+# Quilt Author
+if [ -n "$quilt_author" ] ; then
+       quilt_author_name=$(expr "z$quilt_author" : 'z\(.*[^ ]\) *<.*') &&
+       quilt_author_email=$(expr "z$quilt_author" : '.*<\([^>]*\)') &&
+       test '' != "$quilt_author_name" &&
+       test '' != "$quilt_author_email" ||
+       die "malformed --author parameter"
+fi
+
+# Quilt patch directory
+: ${QUILT_PATCHES:=patches}
+if ! [ -d "$QUILT_PATCHES" ] ; then
+       echo "The \"$QUILT_PATCHES\" directory does not exist."
+       exit 1
+fi
+
+# Temporary directories
+tmp_dir=.dotest
+tmp_msg="$tmp_dir/msg"
+tmp_patch="$tmp_dir/patch"
+tmp_info="$tmp_dir/info"
+
+
+# Find the intial commit
+commit=$(git rev-parse HEAD)
+
+mkdir $tmp_dir || exit 2
+for patch_name in $(grep -v '^#' < "$QUILT_PATCHES/series" ); do
+       if ! [ -f "$QUILT_PATCHES/$patch_name" ] ; then
+               echo "$patch_name doesn't exist. Skipping."
+               continue
+       fi
+       echo $patch_name
+       git mailinfo "$tmp_msg" "$tmp_patch" \
+               <"$QUILT_PATCHES/$patch_name" >"$tmp_info" || exit 3
+       test -s "$tmp_patch" || {
+               echo "Patch is empty.  Was it split wrong?"
+               exit 1
+       }
+
+       # Parse the author information
+       GIT_AUTHOR_NAME=$(sed -ne 's/Author: //p' "$tmp_info")
+       GIT_AUTHOR_EMAIL=$(sed -ne 's/Email: //p' "$tmp_info")
+       export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+       while test -z "$GIT_AUTHOR_EMAIL" && test -z "$GIT_AUTHOR_NAME" ; do
+               if [ -n "$quilt_author" ] ; then
+                       GIT_AUTHOR_NAME="$quilt_author_name";
+                       GIT_AUTHOR_EMAIL="$quilt_author_email";
+               elif [ -n "$dry_run" ]; then
+                       echo "No author found in $patch_name" >&2;
+                       GIT_AUTHOR_NAME="dry-run-not-found";
+                       GIT_AUTHOR_EMAIL="dry-run-not-found";
+               else
+                       echo "No author found in $patch_name" >&2;
+                       echo "---"
+                       cat $tmp_msg
+                       printf "Author: ";
+                       read patch_author
+
+                       echo "$patch_author"
+
+                       patch_author_name=$(expr "z$patch_author" : 'z\(.*[^ ]\) *<.*') &&
+                       patch_author_email=$(expr "z$patch_author" : '.*<\([^>]*\)') &&
+                       test '' != "$patch_author_name" &&
+                       test '' != "$patch_author_email" &&
+                       GIT_AUTHOR_NAME="$patch_author_name" &&
+                       GIT_AUTHOR_EMAIL="$patch_author_email"
+               fi
+       done
+       GIT_AUTHOR_DATE=$(sed -ne 's/Date: //p' "$tmp_info")
+       SUBJECT=$(sed -ne 's/Subject: //p' "$tmp_info")
+       export GIT_AUTHOR_DATE SUBJECT
+       if [ -z "$SUBJECT" ] ; then
+               SUBJECT=$(echo $patch_name | sed -e 's/.patch$//')
+       fi
+
+       if [ -z "$dry_run" ] ; then
+               git apply --index -C1 "$tmp_patch" &&
+               tree=$(git write-tree) &&
+               commit=$( (echo "$SUBJECT"; echo; cat "$tmp_msg") | git commit-tree $tree -p $commit) &&
+               git update-ref -m "quiltimport: $patch_name" HEAD $commit || exit 4
+       fi
+done
+rm -rf $tmp_dir || exit 5
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
new file mode 100755 (executable)
index 0000000..f83e00f
--- /dev/null
@@ -0,0 +1,528 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Johannes E. Schindelin
+
+# SHORT DESCRIPTION
+#
+# This script makes it easy to fix up commits in the middle of a series,
+# and rearrange commits.
+#
+# The original idea comes from Eric W. Biederman, in
+# http://article.gmane.org/gmane.comp.version-control.git/22407
+
+USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose]
+       [--onto <branch>] <upstream> [<branch>])'
+
+OPTIONS_SPEC=
+. git-sh-setup
+require_work_tree
+
+DOTEST="$GIT_DIR/.dotest-merge"
+TODO="$DOTEST"/git-rebase-todo
+DONE="$DOTEST"/done
+MSG="$DOTEST"/message
+SQUASH_MSG="$DOTEST"/message-squash
+REWRITTEN="$DOTEST"/rewritten
+PRESERVE_MERGES=
+STRATEGY=
+VERBOSE=
+test -d "$REWRITTEN" && PRESERVE_MERGES=t
+test -f "$DOTEST"/strategy && STRATEGY="$(cat "$DOTEST"/strategy)"
+test -f "$DOTEST"/verbose && VERBOSE=t
+
+GIT_CHERRY_PICK_HELP="  After resolving the conflicts,
+mark the corrected paths with 'git add <paths>', and
+run 'git rebase --continue'"
+export GIT_CHERRY_PICK_HELP
+
+warn () {
+       echo "$*" >&2
+}
+
+output () {
+       case "$VERBOSE" in
+       '')
+               output=$("$@" 2>&1 )
+               status=$?
+               test $status != 0 && printf "%s\n" "$output"
+               return $status
+               ;;
+       *)
+               "$@"
+               ;;
+       esac
+}
+
+require_clean_work_tree () {
+       # test if working tree is dirty
+       git rev-parse --verify HEAD > /dev/null &&
+       git update-index --refresh &&
+       git diff-files --quiet &&
+       git diff-index --cached --quiet HEAD -- ||
+       die "Working tree is dirty"
+}
+
+ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION"
+
+comment_for_reflog () {
+       case "$ORIG_REFLOG_ACTION" in
+       ''|rebase*)
+               GIT_REFLOG_ACTION="rebase -i ($1)"
+               export GIT_REFLOG_ACTION
+               ;;
+       esac
+}
+
+mark_action_done () {
+       sed -e 1q < "$TODO" >> "$DONE"
+       sed -e 1d < "$TODO" >> "$TODO".new
+       mv -f "$TODO".new "$TODO"
+       count=$(($(grep -ve '^$' -e '^#' < "$DONE" | wc -l)))
+       total=$(($count+$(grep -ve '^$' -e '^#' < "$TODO" | wc -l)))
+       printf "Rebasing (%d/%d)\r" $count $total
+       test -z "$VERBOSE" || echo
+}
+
+make_patch () {
+       parent_sha1=$(git rev-parse --verify "$1"^) ||
+               die "Cannot get patch for $1^"
+       git diff-tree -p "$parent_sha1".."$1" > "$DOTEST"/patch
+       test -f "$DOTEST"/message ||
+               git cat-file commit "$1" | sed "1,/^$/d" > "$DOTEST"/message
+       test -f "$DOTEST"/author-script ||
+               get_author_ident_from_commit "$1" > "$DOTEST"/author-script
+}
+
+die_with_patch () {
+       make_patch "$1"
+       git rerere
+       die "$2"
+}
+
+die_abort () {
+       rm -rf "$DOTEST"
+       die "$1"
+}
+
+has_action () {
+       grep -vqe '^$' -e '^#' "$1"
+}
+
+pick_one () {
+       no_ff=
+       case "$1" in -n) sha1=$2; no_ff=t ;; *) sha1=$1 ;; esac
+       output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
+       test -d "$REWRITTEN" &&
+               pick_one_preserving_merges "$@" && return
+       parent_sha1=$(git rev-parse --verify $sha1^) ||
+               die "Could not get the parent of $sha1"
+       current_sha1=$(git rev-parse --verify HEAD)
+       if test "$no_ff$current_sha1" = "$parent_sha1"; then
+               output git reset --hard $sha1
+               test "a$1" = a-n && output git reset --soft $current_sha1
+               sha1=$(git rev-parse --short $sha1)
+               output warn Fast forward to $sha1
+       else
+               output git cherry-pick "$@"
+       fi
+}
+
+pick_one_preserving_merges () {
+       case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
+       sha1=$(git rev-parse $sha1)
+
+       if test -f "$DOTEST"/current-commit
+       then
+               current_commit=$(cat "$DOTEST"/current-commit) &&
+               git rev-parse HEAD > "$REWRITTEN"/$current_commit &&
+               rm "$DOTEST"/current-commit ||
+               die "Cannot write current commit's replacement sha1"
+       fi
+
+       # rewrite parents; if none were rewritten, we can fast-forward.
+       fast_forward=t
+       preserve=t
+       new_parents=
+       for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -f2-)
+       do
+               if test -f "$REWRITTEN"/$p
+               then
+                       preserve=f
+                       new_p=$(cat "$REWRITTEN"/$p)
+                       test $p != $new_p && fast_forward=f
+                       case "$new_parents" in
+                       *$new_p*)
+                               ;; # do nothing; that parent is already there
+                       *)
+                               new_parents="$new_parents $new_p"
+                               ;;
+                       esac
+               fi
+       done
+       case $fast_forward in
+       t)
+               output warn "Fast forward to $sha1"
+               test $preserve = f || echo $sha1 > "$REWRITTEN"/$sha1
+               ;;
+       f)
+               test "a$1" = a-n && die "Refusing to squash a merge: $sha1"
+
+               first_parent=$(expr "$new_parents" : ' \([^ ]*\)')
+               # detach HEAD to current parent
+               output git checkout $first_parent 2> /dev/null ||
+                       die "Cannot move HEAD to $first_parent"
+
+               echo $sha1 > "$DOTEST"/current-commit
+               case "$new_parents" in
+               ' '*' '*)
+                       # redo merge
+                       author_script=$(get_author_ident_from_commit $sha1)
+                       eval "$author_script"
+                       msg="$(git cat-file commit $sha1 | sed -e '1,/^$/d')"
+                       # No point in merging the first parent, that's HEAD
+                       new_parents=${new_parents# $first_parent}
+                       if ! GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
+                               GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
+                               GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
+                               output git merge $STRATEGY -m "$msg" \
+                                       $new_parents
+                       then
+                               git rerere
+                               printf "%s\n" "$msg" > "$GIT_DIR"/MERGE_MSG
+                               die Error redoing merge $sha1
+                       fi
+                       ;;
+               *)
+                       output git cherry-pick "$@" ||
+                               die_with_patch $sha1 "Could not pick $sha1"
+                       ;;
+               esac
+               ;;
+       esac
+}
+
+nth_string () {
+       case "$1" in
+       *1[0-9]|*[04-9]) echo "$1"th;;
+       *1) echo "$1"st;;
+       *2) echo "$1"nd;;
+       *3) echo "$1"rd;;
+       esac
+}
+
+make_squash_message () {
+       if test -f "$SQUASH_MSG"; then
+               COUNT=$(($(sed -n "s/^# This is [^0-9]*\([1-9][0-9]*\).*/\1/p" \
+                       < "$SQUASH_MSG" | tail -n 1)+1))
+               echo "# This is a combination of $COUNT commits."
+               sed -n "2,\$p" < "$SQUASH_MSG"
+       else
+               COUNT=2
+               echo "# This is a combination of two commits."
+               echo "# The first commit's message is:"
+               echo
+               git cat-file commit HEAD | sed -e '1,/^$/d'
+               echo
+       fi
+       echo "# This is the $(nth_string $COUNT) commit message:"
+       echo
+       git cat-file commit $1 | sed -e '1,/^$/d'
+}
+
+peek_next_command () {
+       sed -n "1s/ .*$//p" < "$TODO"
+}
+
+do_next () {
+       rm -f "$DOTEST"/message "$DOTEST"/author-script \
+               "$DOTEST"/amend || exit
+       read command sha1 rest < "$TODO"
+       case "$command" in
+       '#'*|'')
+               mark_action_done
+               ;;
+       pick|p)
+               comment_for_reflog pick
+
+               mark_action_done
+               pick_one $sha1 ||
+                       die_with_patch $sha1 "Could not apply $sha1... $rest"
+               ;;
+       edit|e)
+               comment_for_reflog edit
+
+               mark_action_done
+               pick_one $sha1 ||
+                       die_with_patch $sha1 "Could not apply $sha1... $rest"
+               make_patch $sha1
+               : > "$DOTEST"/amend
+               warn
+               warn "You can amend the commit now, with"
+               warn
+               warn "  git commit --amend"
+               warn
+               exit 0
+               ;;
+       squash|s)
+               comment_for_reflog squash
+
+               has_action "$DONE" ||
+                       die "Cannot 'squash' without a previous commit"
+
+               mark_action_done
+               make_squash_message $sha1 > "$MSG"
+               case "$(peek_next_command)" in
+               squash|s)
+                       EDIT_COMMIT=
+                       USE_OUTPUT=output
+                       cp "$MSG" "$SQUASH_MSG"
+                       ;;
+               *)
+                       EDIT_COMMIT=-e
+                       USE_OUTPUT=
+                       rm -f "$SQUASH_MSG" || exit
+                       ;;
+               esac
+
+               failed=f
+               author_script=$(get_author_ident_from_commit HEAD)
+               output git reset --soft HEAD^
+               pick_one -n $sha1 || failed=t
+               echo "$author_script" > "$DOTEST"/author-script
+               case $failed in
+               f)
+                       # This is like --amend, but with a different message
+                       eval "$author_script"
+                       GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
+                       GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
+                       GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
+                       $USE_OUTPUT git commit -F "$MSG" $EDIT_COMMIT
+                       ;;
+               t)
+                       cp "$MSG" "$GIT_DIR"/MERGE_MSG
+                       warn
+                       warn "Could not apply $sha1... $rest"
+                       die_with_patch $sha1 ""
+                       ;;
+               esac
+               ;;
+       *)
+               warn "Unknown command: $command $sha1 $rest"
+               die_with_patch $sha1 "Please fix this in the file $TODO."
+               ;;
+       esac
+       test -s "$TODO" && return
+
+       comment_for_reflog finish &&
+       HEADNAME=$(cat "$DOTEST"/head-name) &&
+       OLDHEAD=$(cat "$DOTEST"/head) &&
+       SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) &&
+       if test -d "$REWRITTEN"
+       then
+               test -f "$DOTEST"/current-commit &&
+                       current_commit=$(cat "$DOTEST"/current-commit) &&
+                       git rev-parse HEAD > "$REWRITTEN"/$current_commit
+               NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD)
+       else
+               NEWHEAD=$(git rev-parse HEAD)
+       fi &&
+       case $HEADNAME in
+       refs/*)
+               message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" &&
+               git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD &&
+               git symbolic-ref HEAD $HEADNAME
+               ;;
+       esac && {
+               test ! -f "$DOTEST"/verbose ||
+                       git diff-tree --stat $(cat "$DOTEST"/head)..HEAD
+       } &&
+       rm -rf "$DOTEST" &&
+       git gc --auto &&
+       warn "Successfully rebased and updated $HEADNAME."
+
+       exit
+}
+
+do_rest () {
+       while :
+       do
+               do_next
+       done
+}
+
+while test $# != 0
+do
+       case "$1" in
+       --continue)
+               comment_for_reflog continue
+
+               test -d "$DOTEST" || die "No interactive rebase running"
+
+               # commit if necessary
+               git rev-parse --verify HEAD > /dev/null &&
+               git update-index --refresh &&
+               git diff-files --quiet &&
+               ! git diff-index --cached --quiet HEAD -- &&
+               . "$DOTEST"/author-script && {
+                       test ! -f "$DOTEST"/amend || git reset --soft HEAD^
+               } &&
+               export GIT_AUTHOR_NAME GIT_AUTHOR_NAME GIT_AUTHOR_DATE &&
+               git commit -F "$DOTEST"/message -e
+
+               require_clean_work_tree
+               do_rest
+               ;;
+       --abort)
+               comment_for_reflog abort
+
+               git rerere clear
+               test -d "$DOTEST" || die "No interactive rebase running"
+
+               HEADNAME=$(cat "$DOTEST"/head-name)
+               HEAD=$(cat "$DOTEST"/head)
+               case $HEADNAME in
+               refs/*)
+                       git symbolic-ref HEAD $HEADNAME
+                       ;;
+               esac &&
+               output git reset --hard $HEAD &&
+               rm -rf "$DOTEST"
+               exit
+               ;;
+       --skip)
+               comment_for_reflog skip
+
+               git rerere clear
+               test -d "$DOTEST" || die "No interactive rebase running"
+
+               output git reset --hard && do_rest
+               ;;
+       -s|--strategy)
+               case "$#,$1" in
+               *,*=*)
+                       STRATEGY="-s "$(expr "z$1" : 'z-[^=]*=\(.*\)') ;;
+               1,*)
+                       usage ;;
+               *)
+                       STRATEGY="-s $2"
+                       shift ;;
+               esac
+               ;;
+       --merge)
+               # we use merge anyway
+               ;;
+       -C*)
+               die "Interactive rebase uses merge, so $1 does not make sense"
+               ;;
+       -v|--verbose)
+               VERBOSE=t
+               ;;
+       -p|--preserve-merges)
+               PRESERVE_MERGES=t
+               ;;
+       -i|--interactive)
+               # yeah, we know
+               ;;
+       ''|-h)
+               usage
+               ;;
+       *)
+               test -d "$DOTEST" &&
+                       die "Interactive rebase already started"
+
+               git var GIT_COMMITTER_IDENT >/dev/null ||
+                       die "You need to set your committer info first"
+
+               comment_for_reflog start
+
+               ONTO=
+               case "$1" in
+               --onto)
+                       ONTO=$(git rev-parse --verify "$2") ||
+                               die "Does not point to a valid commit: $2"
+                       shift; shift
+                       ;;
+               esac
+
+               require_clean_work_tree
+
+               if test ! -z "$2"
+               then
+                       output git show-ref --verify --quiet "refs/heads/$2" ||
+                               die "Invalid branchname: $2"
+                       output git checkout "$2" ||
+                               die "Could not checkout $2"
+               fi
+
+               HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?"
+               UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base"
+
+               mkdir "$DOTEST" || die "Could not create temporary $DOTEST"
+
+               test -z "$ONTO" && ONTO=$UPSTREAM
+
+               : > "$DOTEST"/interactive || die "Could not mark as interactive"
+               git symbolic-ref HEAD > "$DOTEST"/head-name 2> /dev/null ||
+                       echo "detached HEAD" > "$DOTEST"/head-name
+
+               echo $HEAD > "$DOTEST"/head
+               echo $UPSTREAM > "$DOTEST"/upstream
+               echo $ONTO > "$DOTEST"/onto
+               test -z "$STRATEGY" || echo "$STRATEGY" > "$DOTEST"/strategy
+               test t = "$VERBOSE" && : > "$DOTEST"/verbose
+               if test t = "$PRESERVE_MERGES"
+               then
+                       # $REWRITTEN contains files for each commit that is
+                       # reachable by at least one merge base of $HEAD and
+                       # $UPSTREAM. They are not necessarily rewritten, but
+                       # their children might be.
+                       # This ensures that commits on merged, but otherwise
+                       # unrelated side branches are left alone. (Think "X"
+                       # in the man page's example.)
+                       mkdir "$REWRITTEN" &&
+                       for c in $(git merge-base --all $HEAD $UPSTREAM)
+                       do
+                               echo $ONTO > "$REWRITTEN"/$c ||
+                                       die "Could not init rewritten commits"
+                       done
+                       MERGES_OPTION=
+               else
+                       MERGES_OPTION=--no-merges
+               fi
+
+               SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
+               SHORTHEAD=$(git rev-parse --short $HEAD)
+               SHORTONTO=$(git rev-parse --short $ONTO)
+               git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
+                       --abbrev=7 --reverse --left-right --cherry-pick \
+                       $UPSTREAM...$HEAD | \
+                       sed -n "s/^>/pick /p" > "$TODO"
+               cat >> "$TODO" << EOF
+
+# Rebase $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO
+#
+# Commands:
+#  pick = use commit
+#  edit = use commit, but stop for amending
+#  squash = use commit, but meld into previous commit
+#
+# If you remove a line here THAT COMMIT WILL BE LOST.
+# However, if you remove everything, the rebase will be aborted.
+#
+EOF
+
+               has_action "$TODO" ||
+                       die_abort "Nothing to do"
+
+               cp "$TODO" "$TODO".backup
+               git_editor "$TODO" ||
+                       die "Could not execute editor"
+
+               has_action "$TODO" ||
+                       die_abort "Nothing to do"
+
+               output git checkout $ONTO && do_rest
+               ;;
+       esac
+       shift
+done
diff --git a/git-rebase.sh b/git-rebase.sh
new file mode 100755 (executable)
index 0000000..bdcea0e
--- /dev/null
@@ -0,0 +1,420 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano.
+#
+
+USAGE='[--interactive | -i] [-v] [--onto <newbase>] <upstream> [<branch>]'
+LONG_USAGE='git-rebase replaces <branch> with a new branch of the
+same name.  When the --onto option is provided the new branch starts
+out with a HEAD equal to <newbase>, otherwise it is equal to <upstream>
+It then attempts to create a new commit for each commit from the original
+<branch> that does not exist in the <upstream> branch.
+
+It is possible that a merge failure will prevent this process from being
+completely automatic.  You will have to resolve any such merge failure
+and run git rebase --continue.  Another option is to bypass the commit
+that caused the merge failure with git rebase --skip.  To restore the
+original <branch> and remove the .dotest working files, use the command
+git rebase --abort instead.
+
+Note that if <branch> is not specified on the command line, the
+currently checked out branch is used.  You must be in the top
+directory of your project to start (or continue) a rebase.
+
+Example:       git-rebase master~1 topic
+
+        A---B---C topic                   A'\''--B'\''--C'\'' topic
+       /                   -->           /
+  D---E---F---G master          D---E---F---G master
+'
+
+SUBDIRECTORY_OK=Yes
+OPTIONS_SPEC=
+. git-sh-setup
+set_reflog_action rebase
+require_work_tree
+cd_to_toplevel
+
+RESOLVEMSG="
+When you have resolved this problem run \"git rebase --continue\".
+If you would prefer to skip this patch, instead run \"git rebase --skip\".
+To restore the original branch and stop rebasing run \"git rebase --abort\".
+"
+unset newbase
+strategy=recursive
+do_merge=
+dotest=$GIT_DIR/.dotest-merge
+prec=4
+verbose=
+git_am_opt=
+
+continue_merge () {
+       test -n "$prev_head" || die "prev_head must be defined"
+       test -d "$dotest" || die "$dotest directory does not exist"
+
+       unmerged=$(git ls-files -u)
+       if test -n "$unmerged"
+       then
+               echo "You still have unmerged paths in your index"
+               echo "did you forget to use git add?"
+               die "$RESOLVEMSG"
+       fi
+
+       cmt=`cat "$dotest/current"`
+       if ! git diff-index --quiet HEAD --
+       then
+               if ! git-commit -C "$cmt"
+               then
+                       echo "Commit failed, please do not call \"git commit\""
+                       echo "directly, but instead do one of the following: "
+                       die "$RESOLVEMSG"
+               fi
+               printf "Committed: %0${prec}d " $msgnum
+       else
+               printf "Already applied: %0${prec}d " $msgnum
+       fi
+       git rev-list --pretty=oneline -1 "$cmt" | sed -e 's/^[^ ]* //'
+
+       prev_head=`git rev-parse HEAD^0`
+       # save the resulting commit so we can read-tree on it later
+       echo "$prev_head" > "$dotest/prev_head"
+
+       # onto the next patch:
+       msgnum=$(($msgnum + 1))
+       echo "$msgnum" >"$dotest/msgnum"
+}
+
+call_merge () {
+       cmt="$(cat "$dotest/cmt.$1")"
+       echo "$cmt" > "$dotest/current"
+       hd=$(git rev-parse --verify HEAD)
+       cmt_name=$(git symbolic-ref HEAD 2> /dev/null || echo HEAD)
+       msgnum=$(cat "$dotest/msgnum")
+       end=$(cat "$dotest/end")
+       eval GITHEAD_$cmt='"${cmt_name##refs/heads/}~$(($end - $msgnum))"'
+       eval GITHEAD_$hd='$(cat "$dotest/onto_name")'
+       export GITHEAD_$cmt GITHEAD_$hd
+       git-merge-$strategy "$cmt^" -- "$hd" "$cmt"
+       rv=$?
+       case "$rv" in
+       0)
+               unset GITHEAD_$cmt GITHEAD_$hd
+               return
+               ;;
+       1)
+               git rerere
+               die "$RESOLVEMSG"
+               ;;
+       2)
+               echo "Strategy: $rv $strategy failed, try another" 1>&2
+               die "$RESOLVEMSG"
+               ;;
+       *)
+               die "Unknown exit code ($rv) from command:" \
+                       "git-merge-$strategy $cmt^ -- HEAD $cmt"
+               ;;
+       esac
+}
+
+move_to_original_branch () {
+       test -z "$head_name" &&
+               head_name="$(cat "$dotest"/head-name)" &&
+               onto="$(cat "$dotest"/onto)" &&
+               orig_head="$(cat "$dotest"/orig-head)"
+       case "$head_name" in
+       refs/*)
+               message="rebase finished: $head_name onto $onto"
+               git update-ref -m "$message" \
+                       $head_name $(git rev-parse HEAD) $orig_head &&
+               git symbolic-ref HEAD $head_name ||
+               die "Could not move back to $head_name"
+               ;;
+       esac
+}
+
+finish_rb_merge () {
+       move_to_original_branch
+       rm -r "$dotest"
+       echo "All done."
+}
+
+is_interactive () {
+       test -f "$dotest"/interactive ||
+       while :; do case $#,"$1" in 0,|*,-i|*,--interactive) break ;; esac
+               shift
+       done && test -n "$1"
+}
+
+is_interactive "$@" && exec git-rebase--interactive "$@"
+
+while test $# != 0
+do
+       case "$1" in
+       --continue)
+               git diff-files --quiet || {
+                       echo "You must edit all merge conflicts and then"
+                       echo "mark them as resolved using git add"
+                       exit 1
+               }
+               if test -d "$dotest"
+               then
+                       prev_head=$(cat "$dotest/prev_head")
+                       end=$(cat "$dotest/end")
+                       msgnum=$(cat "$dotest/msgnum")
+                       onto=$(cat "$dotest/onto")
+                       continue_merge
+                       while test "$msgnum" -le "$end"
+                       do
+                               call_merge "$msgnum"
+                               continue_merge
+                       done
+                       finish_rb_merge
+                       exit
+               fi
+               head_name=$(cat .dotest/head-name) &&
+               onto=$(cat .dotest/onto) &&
+               orig_head=$(cat .dotest/orig-head) &&
+               git am --resolved --3way --resolvemsg="$RESOLVEMSG" &&
+               move_to_original_branch
+               exit
+               ;;
+       --skip)
+               git reset --hard HEAD || exit $?
+               if test -d "$dotest"
+               then
+                       git rerere clear
+                       prev_head=$(cat "$dotest/prev_head")
+                       end=$(cat "$dotest/end")
+                       msgnum=$(cat "$dotest/msgnum")
+                       msgnum=$(($msgnum + 1))
+                       onto=$(cat "$dotest/onto")
+                       while test "$msgnum" -le "$end"
+                       do
+                               call_merge "$msgnum"
+                               continue_merge
+                       done
+                       finish_rb_merge
+                       exit
+               fi
+               head_name=$(cat .dotest/head-name) &&
+               onto=$(cat .dotest/onto) &&
+               orig_head=$(cat .dotest/orig-head) &&
+               git am -3 --skip --resolvemsg="$RESOLVEMSG" &&
+               move_to_original_branch
+               exit
+               ;;
+       --abort)
+               git rerere clear
+               if test -d "$dotest"
+               then
+                       move_to_original_branch
+                       rm -r "$dotest"
+               elif test -d .dotest
+               then
+                       dotest=.dotest
+                       move_to_original_branch
+                       rm -r .dotest
+               else
+                       die "No rebase in progress?"
+               fi
+               git reset --hard ORIG_HEAD
+               exit
+               ;;
+       --onto)
+               test 2 -le "$#" || usage
+               newbase="$2"
+               shift
+               ;;
+       -M|-m|--m|--me|--mer|--merg|--merge)
+               do_merge=t
+               ;;
+       -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\
+               --strateg=*|--strategy=*|\
+       -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy)
+               case "$#,$1" in
+               *,*=*)
+                       strategy=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;;
+               1,*)
+                       usage ;;
+               *)
+                       strategy="$2"
+                       shift ;;
+               esac
+               do_merge=t
+               ;;
+       -v|--verbose)
+               verbose=t
+               ;;
+       --whitespace=*)
+               git_am_opt="$git_am_opt $1"
+               ;;
+       -C*)
+               git_am_opt="$git_am_opt $1"
+               ;;
+       -*)
+               usage
+               ;;
+       *)
+               break
+               ;;
+       esac
+       shift
+done
+
+# Make sure we do not have .dotest
+if test -z "$do_merge"
+then
+       if mkdir .dotest
+       then
+               rmdir .dotest
+       else
+               echo >&2 '
+It seems that I cannot create a .dotest directory, and I wonder if you
+are in the middle of patch application or another rebase.  If that is not
+the case, please rm -fr .dotest and run me again.  I am stopping in case
+you still have something valuable there.'
+               exit 1
+       fi
+else
+       if test -d "$dotest"
+       then
+               die "previous dotest directory $dotest still exists." \
+                       'try git-rebase < --continue | --abort >'
+       fi
+fi
+
+# The tree must be really really clean.
+git update-index --refresh || exit
+diff=$(git diff-index --cached --name-status -r HEAD --)
+case "$diff" in
+?*)    echo "cannot rebase: your index is not up-to-date"
+       echo "$diff"
+       exit 1
+       ;;
+esac
+
+# The upstream head must be given.  Make sure it is valid.
+upstream_name="$1"
+upstream=`git rev-parse --verify "${upstream_name}^0"` ||
+    die "invalid upstream $upstream_name"
+
+# Make sure the branch to rebase onto is valid.
+onto_name=${newbase-"$upstream_name"}
+onto=$(git rev-parse --verify "${onto_name}^0") || exit
+
+# If a hook exists, give it a chance to interrupt
+if test -x "$GIT_DIR/hooks/pre-rebase"
+then
+       "$GIT_DIR/hooks/pre-rebase" ${1+"$@"} || {
+               echo >&2 "The pre-rebase hook refused to rebase."
+               exit 1
+       }
+fi
+
+# If the branch to rebase is given, first switch to it.
+case "$#" in
+2)
+       branch_name="$2"
+       git-checkout "$2" || usage
+       ;;
+*)
+       if branch_name=`git symbolic-ref -q HEAD`
+       then
+               branch_name=`expr "z$branch_name" : 'zrefs/heads/\(.*\)'`
+       else
+               branch_name=HEAD ;# detached
+       fi
+       ;;
+esac
+branch=$(git rev-parse --verify "${branch_name}^0") || exit
+
+# Now we are rebasing commits $upstream..$branch on top of $onto
+
+# Check if we are already based on $onto with linear history,
+# but this should be done only when upstream and onto are the same.
+mb=$(git merge-base "$onto" "$branch")
+if test "$upstream" = "$onto" && test "$mb" = "$onto" &&
+       # linear history?
+       ! git rev-list --parents "$onto".."$branch" | grep " .* " > /dev/null
+then
+       echo >&2 "Current branch $branch_name is up to date."
+       exit 0
+fi
+
+if test -n "$verbose"
+then
+       echo "Changes from $mb to $onto:"
+       # We want color (if set), but no pager
+       GIT_PAGER='' git diff --stat --summary "$mb" "$onto"
+fi
+
+# move to a detached HEAD
+orig_head=$(git rev-parse HEAD^0)
+head_name=$(git symbolic-ref HEAD 2> /dev/null)
+case "$head_name" in
+'')
+       head_name="detached HEAD"
+       ;;
+*)
+       git checkout "$orig_head" > /dev/null 2>&1 ||
+               die "could not detach HEAD"
+       ;;
+esac
+
+# Rewind the head to "$onto"; this saves our current head in ORIG_HEAD.
+echo "First, rewinding head to replay your work on top of it..."
+git-reset --hard "$onto"
+
+# If the $onto is a proper descendant of the tip of the branch, then
+# we just fast forwarded.
+if test "$mb" = "$branch"
+then
+       echo >&2 "Fast-forwarded $branch_name to $onto_name."
+       move_to_original_branch
+       exit 0
+fi
+
+if test -z "$do_merge"
+then
+       git format-patch -k --stdout --full-index --ignore-if-in-upstream "$upstream"..ORIG_HEAD |
+       git am $git_am_opt --binary -3 -k --resolvemsg="$RESOLVEMSG" &&
+       move_to_original_branch
+       ret=$?
+       test 0 != $ret -a -d .dotest &&
+               echo $head_name > .dotest/head-name &&
+               echo $onto > .dotest/onto &&
+               echo $orig_head > .dotest/orig-head
+       exit $ret
+fi
+
+# start doing a rebase with git-merge
+# this is rename-aware if the recursive (default) strategy is used
+
+mkdir -p "$dotest"
+echo "$onto" > "$dotest/onto"
+echo "$onto_name" > "$dotest/onto_name"
+prev_head=$orig_head
+echo "$prev_head" > "$dotest/prev_head"
+echo "$orig_head" > "$dotest/orig-head"
+echo "$head_name" > "$dotest/head-name"
+
+msgnum=0
+for cmt in `git rev-list --reverse --no-merges "$upstream"..ORIG_HEAD`
+do
+       msgnum=$(($msgnum + 1))
+       echo "$cmt" > "$dotest/cmt.$msgnum"
+done
+
+echo 1 >"$dotest/msgnum"
+echo $msgnum >"$dotest/end"
+
+end=$msgnum
+msgnum=1
+
+while test "$msgnum" -le "$end"
+do
+       call_merge "$msgnum"
+       continue_merge
+done
+
+finish_rb_merge
diff --git a/git-relink.perl b/git-relink.perl
new file mode 100755 (executable)
index 0000000..f6b4f6a
--- /dev/null
@@ -0,0 +1,173 @@
+#!/usr/bin/env perl
+# Copyright 2005, Ryan Anderson <ryan@michonline.com>
+# Distribution permitted under the GPL v2, as distributed
+# by the Free Software Foundation.
+# Later versions of the GPL at the discretion of Linus Torvalds
+#
+# Scan two git object-trees, and hardlink any common objects between them.
+
+use 5.006;
+use strict;
+use warnings;
+use Getopt::Long;
+
+sub get_canonical_form($);
+sub do_scan_directory($$$);
+sub compare_two_files($$);
+sub usage();
+sub link_two_files($$);
+
+# stats
+my $total_linked = 0;
+my $total_already = 0;
+my ($linked,$already);
+
+my $fail_on_different_sizes = 0;
+my $help = 0;
+GetOptions("safe" => \$fail_on_different_sizes,
+          "help" => \$help);
+
+usage() if $help;
+
+my (@dirs) = @ARGV;
+
+usage() if (!defined $dirs[0] || !defined $dirs[1]);
+
+$_ = get_canonical_form($_) foreach (@dirs);
+
+my $master_dir = pop @dirs;
+
+opendir(D,$master_dir . "objects/")
+       or die "Failed to open $master_dir/objects/ : $!";
+
+my @hashdirs = grep !/^\.{1,2}$/, readdir(D);
+
+foreach my $repo (@dirs) {
+       $linked = 0;
+       $already = 0;
+       printf("Searching '%s' and '%s' for common objects and hardlinking them...\n",
+               $master_dir,$repo);
+
+       foreach my $hashdir (@hashdirs) {
+               do_scan_directory($master_dir, $hashdir, $repo);
+       }
+
+       printf("Linked %d files, %d were already linked.\n",$linked, $already);
+
+       $total_linked += $linked;
+       $total_already += $already;
+}
+
+printf("Totals: Linked %d files, %d were already linked.\n",
+       $total_linked, $total_already);
+
+
+sub do_scan_directory($$$) {
+       my ($srcdir, $subdir, $dstdir) = @_;
+
+       my $sfulldir = sprintf("%sobjects/%s/",$srcdir,$subdir);
+       my $dfulldir = sprintf("%sobjects/%s/",$dstdir,$subdir);
+
+       opendir(S,$sfulldir)
+               or die "Failed to opendir $sfulldir: $!";
+
+       foreach my $file (grep(!/\.{1,2}$/, readdir(S))) {
+               my $sfilename = $sfulldir . $file;
+               my $dfilename = $dfulldir . $file;
+
+               compare_two_files($sfilename,$dfilename);
+
+       }
+       closedir(S);
+}
+
+sub compare_two_files($$) {
+       my ($sfilename, $dfilename) = @_;
+
+       # Perl's stat returns relevant information as follows:
+       # 0 = dev number
+       # 1 = inode number
+       # 7 = size
+       my @sstatinfo = stat($sfilename);
+       my @dstatinfo = stat($dfilename);
+
+       if (@sstatinfo == 0 && @dstatinfo == 0) {
+               die sprintf("Stat of both %s and %s failed: %s\n",$sfilename, $dfilename, $!);
+
+       } elsif (@dstatinfo == 0) {
+               return;
+       }
+
+       if ( ($sstatinfo[0] == $dstatinfo[0]) &&
+            ($sstatinfo[1] != $dstatinfo[1])) {
+               if ($sstatinfo[7] == $dstatinfo[7]) {
+                       link_two_files($sfilename, $dfilename);
+
+               } else {
+                       my $err = sprintf("ERROR: File sizes are not the same, cannot relink %s to %s.\n",
+                               $sfilename, $dfilename);
+                       if ($fail_on_different_sizes) {
+                               die $err;
+                       } else {
+                               warn $err;
+                       }
+               }
+
+       } elsif ( ($sstatinfo[0] == $dstatinfo[0]) &&
+            ($sstatinfo[1] == $dstatinfo[1])) {
+               $already++;
+       }
+}
+
+sub get_canonical_form($) {
+       my $dir = shift;
+       my $original = $dir;
+
+       die "$dir is not a directory." unless -d $dir;
+
+       $dir .= "/" unless $dir =~ m#/$#;
+       $dir .= ".git/" unless $dir =~ m#\.git/$#;
+
+       die "$original does not have a .git/ subdirectory.\n" unless -d $dir;
+
+       return $dir;
+}
+
+sub link_two_files($$) {
+       my ($sfilename, $dfilename) = @_;
+       my $tmpdname = sprintf("%s.old",$dfilename);
+       rename($dfilename,$tmpdname)
+               or die sprintf("Failure renaming %s to %s: %s",
+                       $dfilename, $tmpdname, $!);
+
+       if (! link($sfilename,$dfilename)) {
+               my $failtxt = "";
+               unless (rename($tmpdname,$dfilename)) {
+                       $failtxt = sprintf(
+                               "Git Repository containing %s is probably corrupted, " .
+                               "please copy '%s' to '%s' to fix.\n",
+                               $tmpdname, $dfilename);
+               }
+
+               die sprintf("Failed to link %s to %s: %s\n%s" .
+                       $sfilename, $dfilename,
+                       $!, $dfilename, $failtxt);
+       }
+
+       unlink($tmpdname)
+               or die sprintf("Unlink of %s failed: %s\n",
+                       $dfilename, $!);
+
+       $linked++;
+}
+
+
+sub usage() {
+       print("Usage: $0 [--safe] <dir> [<dir> ...] <master_dir> \n");
+       print("All directories should contain a .git/objects/ subdirectory.\n");
+       print("Options\n");
+       print("\t--safe\t" .
+               "Stops if two objects with the same hash exist but " .
+               "have different sizes.  Default is to warn and continue.\n");
+       exit(1);
+}
diff --git a/git-remote.perl b/git-remote.perl
new file mode 100755 (executable)
index 0000000..d13e4c1
--- /dev/null
@@ -0,0 +1,475 @@
+#!/usr/bin/perl -w
+
+use Git;
+my $git = Git->repository();
+
+sub add_remote_config {
+       my ($hash, $name, $what, $value) = @_;
+       if ($what eq 'url') {
+               if (exists $hash->{$name}{'URL'}) {
+                       print STDERR "Warning: more than one remote.$name.url\n";
+               }
+               $hash->{$name}{'URL'} = $value;
+       }
+       elsif ($what eq 'fetch') {
+               $hash->{$name}{'FETCH'} ||= [];
+               push @{$hash->{$name}{'FETCH'}}, $value;
+       }
+       elsif ($what eq 'push') {
+               $hash->{$name}{'PUSH'} ||= [];
+               push @{$hash->{$name}{'PUSH'}}, $value;
+       }
+       if (!exists $hash->{$name}{'SOURCE'}) {
+               $hash->{$name}{'SOURCE'} = 'config';
+       }
+}
+
+sub add_remote_remotes {
+       my ($hash, $file, $name) = @_;
+
+       if (exists $hash->{$name}) {
+               $hash->{$name}{'WARNING'} = 'ignored due to config';
+               return;
+       }
+
+       my $fh;
+       if (!open($fh, '<', $file)) {
+               print STDERR "Warning: cannot open $file\n";
+               return;
+       }
+       my $it = { 'SOURCE' => 'remotes' };
+       $hash->{$name} = $it;
+       while (<$fh>) {
+               chomp;
+               if (/^URL:\s*(.*)$/) {
+                       # Having more than one is Ok -- it is used for push.
+                       if (! exists $it->{'URL'}) {
+                               $it->{'URL'} = $1;
+                       }
+               }
+               elsif (/^Push:\s*(.*)$/) {
+                       $it->{'PUSH'} ||= [];
+                       push @{$it->{'PUSH'}}, $1;
+               }
+               elsif (/^Pull:\s*(.*)$/) {
+                       $it->{'FETCH'} ||= [];
+                       push @{$it->{'FETCH'}}, $1;
+               }
+               elsif (/^\#/) {
+                       ; # ignore
+               }
+               else {
+                       print STDERR "Warning: funny line in $file: $_\n";
+               }
+       }
+       close($fh);
+}
+
+sub list_remote {
+       my ($git) = @_;
+       my %seen = ();
+       my @remotes = eval {
+               $git->command(qw(config --get-regexp), '^remote\.');
+       };
+       for (@remotes) {
+               if (/^remote\.(\S+?)\.([^.\s]+)\s+(.*)$/) {
+                       add_remote_config(\%seen, $1, $2, $3);
+               }
+       }
+
+       my $dir = $git->repo_path() . "/remotes";
+       if (opendir(my $dh, $dir)) {
+               local $_;
+               while ($_ = readdir($dh)) {
+                       chomp;
+                       next if (! -f "$dir/$_" || ! -r _);
+                       add_remote_remotes(\%seen, "$dir/$_", $_);
+               }
+       }
+
+       return \%seen;
+}
+
+sub add_branch_config {
+       my ($hash, $name, $what, $value) = @_;
+       if ($what eq 'remote') {
+               if (exists $hash->{$name}{'REMOTE'}) {
+                       print STDERR "Warning: more than one branch.$name.remote\n";
+               }
+               $hash->{$name}{'REMOTE'} = $value;
+       }
+       elsif ($what eq 'merge') {
+               $hash->{$name}{'MERGE'} ||= [];
+               push @{$hash->{$name}{'MERGE'}}, $value;
+       }
+}
+
+sub list_branch {
+       my ($git) = @_;
+       my %seen = ();
+       my @branches = eval {
+               $git->command(qw(config --get-regexp), '^branch\.');
+       };
+       for (@branches) {
+               if (/^branch\.([^.]*)\.(\S*)\s+(.*)$/) {
+                       add_branch_config(\%seen, $1, $2, $3);
+               }
+       }
+
+       return \%seen;
+}
+
+my $remote = list_remote($git);
+my $branch = list_branch($git);
+
+sub update_ls_remote {
+       my ($harder, $info) = @_;
+
+       return if (($harder == 0) ||
+                  (($harder == 1) && exists $info->{'LS_REMOTE'}));
+
+       my @ref = map {
+               s|^[0-9a-f]{40}\s+refs/heads/||;
+               $_;
+       } $git->command(qw(ls-remote --heads), $info->{'URL'});
+       $info->{'LS_REMOTE'} = \@ref;
+}
+
+sub list_wildcard_mapping {
+       my ($forced, $ours, $ls) = @_;
+       my %refs;
+       for (@$ls) {
+               $refs{$_} = 01; # bit #0 to say "they have"
+       }
+       for ($git->command('for-each-ref', "refs/remotes/$ours")) {
+               chomp;
+               next unless (s|^[0-9a-f]{40}\s[a-z]+\srefs/remotes/$ours/||);
+               next if ($_ eq 'HEAD');
+               $refs{$_} ||= 0;
+               $refs{$_} |= 02; # bit #1 to say "we have"
+       }
+       my (@new, @stale, @tracked);
+       for (sort keys %refs) {
+               my $have = $refs{$_};
+               if ($have == 1) {
+                       push @new, $_;
+               }
+               elsif ($have == 2) {
+                       push @stale, $_;
+               }
+               elsif ($have == 3) {
+                       push @tracked, $_;
+               }
+       }
+       return \@new, \@stale, \@tracked;
+}
+
+sub list_mapping {
+       my ($name, $info) = @_;
+       my $fetch = $info->{'FETCH'};
+       my $ls = $info->{'LS_REMOTE'};
+       my (@new, @stale, @tracked);
+
+       for (@$fetch) {
+               next unless (/(\+)?([^:]+):(.*)/);
+               my ($forced, $theirs, $ours) = ($1, $2, $3);
+               if ($theirs eq 'refs/heads/*' &&
+                   $ours =~ /^refs\/remotes\/(.*)\/\*$/) {
+                       # wildcard mapping
+                       my ($w_new, $w_stale, $w_tracked)
+                               = list_wildcard_mapping($forced, $1, $ls);
+                       push @new, @$w_new;
+                       push @stale, @$w_stale;
+                       push @tracked, @$w_tracked;
+               }
+               elsif ($theirs =~ /\*/ || $ours =~ /\*/) {
+                       print STDERR "Warning: unrecognized mapping in remotes.$name.fetch: $_\n";
+               }
+               elsif ($theirs =~ s|^refs/heads/||) {
+                       if (!grep { $_ eq $theirs } @$ls) {
+                               push @stale, $theirs;
+                       }
+                       elsif ($ours ne '') {
+                               push @tracked, $theirs;
+                       }
+               }
+       }
+       return \@new, \@stale, \@tracked;
+}
+
+sub show_mapping {
+       my ($name, $info) = @_;
+       my ($new, $stale, $tracked) = list_mapping($name, $info);
+       if (@$new) {
+               print "  New remote branches (next fetch will store in remotes/$name)\n";
+               print "    @$new\n";
+       }
+       if (@$stale) {
+               print "  Stale tracking branches in remotes/$name (use 'git remote prune')\n";
+               print "    @$stale\n";
+       }
+       if (@$tracked) {
+               print "  Tracked remote branches\n";
+               print "    @$tracked\n";
+       }
+}
+
+sub prune_remote {
+       my ($name, $ls_remote) = @_;
+       if (!exists $remote->{$name}) {
+               print STDERR "No such remote $name\n";
+               return 1;
+       }
+       my $info = $remote->{$name};
+       update_ls_remote($ls_remote, $info);
+
+       my ($new, $stale, $tracked) = list_mapping($name, $info);
+       my $prefix = "refs/remotes/$name";
+       foreach my $to_prune (@$stale) {
+               my @v = $git->command(qw(rev-parse --verify), "$prefix/$to_prune");
+               $git->command(qw(update-ref -d), "$prefix/$to_prune", $v[0]);
+       }
+       return 0;
+}
+
+sub show_remote {
+       my ($name, $ls_remote) = @_;
+       if (!exists $remote->{$name}) {
+               print STDERR "No such remote $name\n";
+               return 1;
+       }
+       my $info = $remote->{$name};
+       update_ls_remote($ls_remote, $info);
+
+       print "* remote $name\n";
+       print "  URL: $info->{'URL'}\n";
+       for my $branchname (sort keys %$branch) {
+               next unless (defined $branch->{$branchname}{'REMOTE'} &&
+                            $branch->{$branchname}{'REMOTE'} eq $name);
+               my @merged = map {
+                       s|^refs/heads/||;
+                       $_;
+               } split(' ',"@{$branch->{$branchname}{'MERGE'}}");
+               next unless (@merged);
+               print "  Remote branch(es) merged with 'git pull' while on branch $branchname\n";
+               print "    @merged\n";
+       }
+       if ($info->{'LS_REMOTE'}) {
+               show_mapping($name, $info);
+       }
+       if ($info->{'PUSH'}) {
+               my @pushed = map {
+                       s|^refs/heads/||;
+                       s|^\+refs/heads/|+|;
+                       s|:refs/heads/|:|;
+                       $_;
+               } @{$info->{'PUSH'}};
+               print "  Local branch(es) pushed with 'git push'\n";
+               print "    @pushed\n";
+       }
+       return 0;
+}
+
+sub add_remote {
+       my ($name, $url, $opts) = @_;
+       if (exists $remote->{$name}) {
+               print STDERR "remote $name already exists.\n";
+               exit(1);
+       }
+       $git->command('config', "remote.$name.url", $url);
+       my $track = $opts->{'track'} || ["*"];
+
+       for (@$track) {
+               $git->command('config', '--add', "remote.$name.fetch",
+                               $opts->{'mirror'} ?
+                               "+refs/$_:refs/$_" :
+                               "+refs/heads/$_:refs/remotes/$name/$_");
+       }
+       if ($opts->{'fetch'}) {
+               $git->command('fetch', $name);
+       }
+       if (exists $opts->{'master'}) {
+               $git->command('symbolic-ref', "refs/remotes/$name/HEAD",
+                             "refs/remotes/$name/$opts->{'master'}");
+       }
+}
+
+sub update_remote {
+       my ($name) = @_;
+
+        my $conf = $git->config("remotes." . $name);
+       if (defined($conf)) {
+               @remotes = split(' ', $conf);
+       } elsif ($name eq 'default') {
+               undef @remotes;
+               for (sort keys %$remote) {
+                       my $do_fetch = $git->config_bool("remote." . $_ .
+                                                   ".skipDefaultUpdate");
+                       unless ($do_fetch) {
+                               push @remotes, $_;
+                       }
+               }
+       } else {
+               print STDERR "Remote group $name does not exists.\n";
+               exit(1);
+       }
+       for (@remotes) {
+               print "Updating $_\n";
+               $git->command('fetch', "$_");
+       }
+}
+
+sub rm_remote {
+       my ($name) = @_;
+       if (!exists $remote->{$name}) {
+               print STDERR "No such remote $name\n";
+               return 1;
+       }
+
+       $git->command('config', '--remove-section', "remote.$name");
+
+       eval {
+           my @trackers = $git->command('config', '--get-regexp',
+                       'branch.*.remote', $name);
+               for (@trackers) {
+                       /^branch\.(.*)?\.remote/;
+                       $git->config('--unset', "branch.$1.remote");
+                       $git->config('--unset', "branch.$1.merge");
+               }
+       };
+
+       my @refs = $git->command('for-each-ref',
+               '--format=%(refname) %(objectname)', "refs/remotes/$name");
+       for (@refs) {
+               ($ref, $object) = split;
+               $git->command(qw(update-ref -d), $ref, $object);
+       }
+       return 0;
+}
+
+sub add_usage {
+       print STDERR "Usage: git remote add [-f] [-t track]* [-m master] <name> <url>\n";
+       exit(1);
+}
+
+local $VERBOSE = 0;
+@ARGV = grep {
+       if ($_ eq '-v' or $_ eq '--verbose') {
+               $VERBOSE=1;
+               0
+       } else {
+               1
+       }
+} @ARGV;
+
+if (!@ARGV) {
+       for (sort keys %$remote) {
+               print "$_";
+               print "\t$remote->{$_}->{URL}" if $VERBOSE;
+               print "\n";
+       }
+}
+elsif ($ARGV[0] eq 'show') {
+       my $ls_remote = 1;
+       my $i;
+       for ($i = 1; $i < @ARGV; $i++) {
+               if ($ARGV[$i] eq '-n') {
+                       $ls_remote = 0;
+               }
+               else {
+                       last;
+               }
+       }
+       if ($i >= @ARGV) {
+               print STDERR "Usage: git remote show <remote>\n";
+               exit(1);
+       }
+       my $status = 0;
+       for (; $i < @ARGV; $i++) {
+               $status |= show_remote($ARGV[$i], $ls_remote);
+       }
+       exit($status);
+}
+elsif ($ARGV[0] eq 'update') {
+       if (@ARGV <= 1) {
+               update_remote("default");
+               exit(1);
+       }
+       for ($i = 1; $i < @ARGV; $i++) {
+               update_remote($ARGV[$i]);
+       }
+}
+elsif ($ARGV[0] eq 'prune') {
+       my $ls_remote = 1;
+       my $i;
+       for ($i = 1; $i < @ARGV; $i++) {
+               if ($ARGV[$i] eq '-n') {
+                       $ls_remote = 0;
+               }
+               else {
+                       last;
+               }
+       }
+       if ($i >= @ARGV) {
+               print STDERR "Usage: git remote prune <remote>\n";
+               exit(1);
+       }
+       my $status = 0;
+       for (; $i < @ARGV; $i++) {
+               $status |= prune_remote($ARGV[$i], $ls_remote);
+       }
+        exit($status);
+}
+elsif ($ARGV[0] eq 'add') {
+       my %opts = ();
+       while (1 < @ARGV && $ARGV[1] =~ /^-/) {
+               my $opt = $ARGV[1];
+               shift @ARGV;
+               if ($opt eq '-f' || $opt eq '--fetch') {
+                       $opts{'fetch'} = 1;
+                       next;
+               }
+               if ($opt eq '-t' || $opt eq '--track') {
+                       if (@ARGV < 1) {
+                               add_usage();
+                       }
+                       $opts{'track'} ||= [];
+                       push @{$opts{'track'}}, $ARGV[1];
+                       shift @ARGV;
+                       next;
+               }
+               if ($opt eq '-m' || $opt eq '--master') {
+                       if ((@ARGV < 1) || exists $opts{'master'}) {
+                               add_usage();
+                       }
+                       $opts{'master'} = $ARGV[1];
+                       shift @ARGV;
+                       next;
+               }
+               if ($opt eq '--mirror') {
+                       $opts{'mirror'} = 1;
+                       next;
+               }
+               add_usage();
+       }
+       if (@ARGV != 3) {
+               add_usage();
+       }
+       add_remote($ARGV[1], $ARGV[2], \%opts);
+}
+elsif ($ARGV[0] eq 'rm') {
+       if (@ARGV <= 1) {
+               print STDERR "Usage: git remote rm <remote>\n";
+               exit(1);
+       }
+       exit(rm_remote($ARGV[1]));
+}
+else {
+       print STDERR "Usage: git remote\n";
+       print STDERR "       git remote add <name> <url>\n";
+       print STDERR "       git remote rm <name>\n";
+       print STDERR "       git remote show <name>\n";
+       print STDERR "       git remote prune <name>\n";
+       print STDERR "       git remote update [group]\n";
+       exit(1);
+}
diff --git a/git-repack.sh b/git-repack.sh
new file mode 100755 (executable)
index 0000000..e18eb3f
--- /dev/null
@@ -0,0 +1,144 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Linus Torvalds
+#
+
+OPTIONS_KEEPDASHDASH=
+OPTIONS_SPEC="\
+git-repack [options]
+--
+a               pack everything in a single pack
+A               same as -a, and keep unreachable objects too
+d               remove redundant packs, and run git-prune-packed
+f               pass --no-reuse-delta to git-pack-objects
+q,quiet         be quiet
+l               pass --local to git-pack-objects
+ Packing constraints
+window=         size of the window used for delta compression
+window-memory=  same as the above, but limit memory size instead of entries count
+depth=          limits the maximum delta depth
+max-pack-size=  maximum size of each packfile
+"
+SUBDIRECTORY_OK='Yes'
+. git-sh-setup
+
+no_update_info= all_into_one= remove_redundant= keep_unreachable=
+local= quiet= no_reuse= extra=
+while test $# != 0
+do
+       case "$1" in
+       -n)     no_update_info=t ;;
+       -a)     all_into_one=t ;;
+       -A)     all_into_one=t
+               keep_unreachable=--keep-unreachable ;;
+       -d)     remove_redundant=t ;;
+       -q)     quiet=-q ;;
+       -f)     no_reuse=--no-reuse-object ;;
+       -l)     local=--local ;;
+       --max-pack-size|--window|--window-memory|--depth)
+               extra="$extra $1=$2"; shift ;;
+       --) shift; break;;
+       *)      usage ;;
+       esac
+       shift
+done
+
+# Later we will default repack.UseDeltaBaseOffset to true
+default_dbo=false
+
+case "`git config --bool repack.usedeltabaseoffset ||
+       echo $default_dbo`" in
+true)
+       extra="$extra --delta-base-offset" ;;
+esac
+
+PACKDIR="$GIT_OBJECT_DIRECTORY/pack"
+PACKTMP="$GIT_OBJECT_DIRECTORY/.tmp-$$-pack"
+rm -f "$PACKTMP"-*
+trap 'rm -f "$PACKTMP"-*' 0 1 2 3 15
+
+# There will be more repacking strategies to come...
+case ",$all_into_one," in
+,,)
+       args='--unpacked --incremental'
+       ;;
+,t,)
+       if [ -d "$PACKDIR" ]; then
+               for e in `cd "$PACKDIR" && find . -type f -name '*.pack' \
+                       | sed -e 's/^\.\///' -e 's/\.pack$//'`
+               do
+                       if [ -e "$PACKDIR/$e.keep" ]; then
+                               : keep
+                       else
+                               args="$args --unpacked=$e.pack"
+                               existing="$existing $e"
+                       fi
+               done
+       fi
+       if test -z "$args"
+       then
+               args='--unpacked --incremental'
+       elif test -n "$keep_unreachable"
+       then
+               args="$args $keep_unreachable"
+       fi
+       ;;
+esac
+
+args="$args $local $quiet $no_reuse$extra"
+names=$(git pack-objects --non-empty --all --reflog $args </dev/null "$PACKTMP") ||
+       exit 1
+if [ -z "$names" ]; then
+       if test -z "$quiet"; then
+               echo Nothing new to pack.
+       fi
+fi
+for name in $names ; do
+       fullbases="$fullbases pack-$name"
+       chmod a-w "$PACKTMP-$name.pack"
+       chmod a-w "$PACKTMP-$name.idx"
+       mkdir -p "$PACKDIR" || exit
+
+       for sfx in pack idx
+       do
+               if test -f "$PACKDIR/pack-$name.$sfx"
+               then
+                       mv -f "$PACKDIR/pack-$name.$sfx" \
+                               "$PACKDIR/old-pack-$name.$sfx"
+               fi
+       done &&
+       mv -f "$PACKTMP-$name.pack" "$PACKDIR/pack-$name.pack" &&
+       mv -f "$PACKTMP-$name.idx"  "$PACKDIR/pack-$name.idx" &&
+       test -f "$PACKDIR/pack-$name.pack" &&
+       test -f "$PACKDIR/pack-$name.idx" || {
+               echo >&2 "Couldn't replace the existing pack with updated one."
+               echo >&2 "The original set of packs have been saved as"
+               echo >&2 "old-pack-$name.{pack,idx} in $PACKDIR."
+               exit 1
+       }
+       rm -f "$PACKDIR/old-pack-$name.pack" "$PACKDIR/old-pack-$name.idx"
+done
+
+if test "$remove_redundant" = t
+then
+       # We know $existing are all redundant.
+       if [ -n "$existing" ]
+       then
+               sync
+               ( cd "$PACKDIR" &&
+                 for e in $existing
+                 do
+                       case " $fullbases " in
+                       *" $e "*) ;;
+                       *)      rm -f "$e.pack" "$e.idx" "$e.keep" ;;
+                       esac
+                 done
+               )
+       fi
+       git prune-packed $quiet
+fi
+
+case "$no_update_info" in
+t) : ;;
+*) git-update-server-info ;;
+esac
diff --git a/git-request-pull.sh b/git-request-pull.sh
new file mode 100755 (executable)
index 0000000..068f5e0
--- /dev/null
@@ -0,0 +1,57 @@
+#!/bin/sh -e
+# Copyright 2005, Ryan Anderson <ryan@michonline.com>
+#
+# This file is licensed under the GPL v2, or a later version
+# at the discretion of Linus Torvalds.
+
+USAGE='<commit> <url> [<head>]'
+LONG_USAGE='Summarizes the changes since <commit> to the standard output,
+and includes <url> in the message generated.'
+SUBDIRECTORY_OK='Yes'
+OPTIONS_SPEC=
+. git-sh-setup
+. git-parse-remote
+
+base=$1
+url=$2
+head=${3-HEAD}
+
+[ "$base" ] || usage
+[ "$url" ] || usage
+
+baserev=`git rev-parse --verify "$base"^0` &&
+headrev=`git rev-parse --verify "$head"^0` || exit
+
+merge_base=`git merge-base $baserev $headrev` ||
+die "fatal: No commits in common between $base and $head"
+
+url=$(get_remote_url "$url")
+branch=$(git peek-remote "$url" \
+       | sed -n -e "/^$headrev refs.heads./{
+               s/^.*   refs.heads.//
+               p
+               q
+       }")
+if [ -z "$branch" ]; then
+       echo "warn: No branch of $url is at:" >&2
+       git log --max-count=1 --pretty='format:warn:   %h: %s' $headrev >&2
+       echo "warn: Are you sure you pushed $head there?" >&2
+       echo >&2
+       echo >&2
+       branch=..BRANCH.NOT.VERIFIED..
+       status=1
+fi
+
+PAGER=
+export PAGER
+echo "The following changes since commit $baserev:"
+git shortlog --max-count=1 $baserev | sed -e 's/^\(.\)/  \1/'
+
+echo "are available in the git repository at:"
+echo
+echo "  $url $branch"
+echo
+
+git shortlog ^$baserev $headrev
+git diff -M --stat --summary $merge_base $headrev
+exit $status
diff --git a/git-send-email.perl b/git-send-email.perl
new file mode 100755 (executable)
index 0000000..76baa8e
--- /dev/null
@@ -0,0 +1,834 @@
+#!/usr/bin/perl -w
+#
+# Copyright 2002,2005 Greg Kroah-Hartman <greg@kroah.com>
+# Copyright 2005 Ryan Anderson <ryan@michonline.com>
+#
+# GPL v2 (See COPYING)
+#
+# Ported to support git "mbox" format files by Ryan Anderson <ryan@michonline.com>
+#
+# Sends a collection of emails to the given email addresses, disturbingly fast.
+#
+# Supports two formats:
+# 1. mbox format files (ignoring most headers and MIME formatting - this is designed for sending patches)
+# 2. The original format support by Greg's script:
+#    first line of the message is who to CC,
+#    and second line is the subject of the message.
+#
+
+use strict;
+use warnings;
+use Term::ReadLine;
+use Getopt::Long;
+use Data::Dumper;
+use Term::ANSIColor;
+use Git;
+
+$SIG{INT} = sub { print color("reset"), "\n"; exit };
+
+package FakeTerm;
+sub new {
+       my ($class, $reason) = @_;
+       return bless \$reason, shift;
+}
+sub readline {
+       my $self = shift;
+       die "Cannot use readline on FakeTerm: $$self";
+}
+package main;
+
+
+sub usage {
+       print <<EOT;
+git-send-email [options] <file | directory>...
+Options:
+   --from         Specify the "From:" line of the email to be sent.
+
+   --to           Specify the primary "To:" line of the email.
+
+   --cc           Specify an initial "Cc:" list for the entire series
+                  of emails.
+
+   --cc-cmd       Specify a command to execute per file which adds
+                  per file specific cc address entries
+
+   --bcc          Specify a list of email addresses that should be Bcc:
+                 on all the emails.
+
+   --compose      Use \$GIT_EDITOR, core.editor, \$EDITOR, or \$VISUAL to edit
+                 an introductory message for the patch series.
+
+   --subject      Specify the initial "Subject:" line.
+                  Only necessary if --compose is also set.  If --compose
+                 is not set, this will be prompted for.
+
+   --in-reply-to  Specify the first "In-Reply-To:" header line.
+                  Only used if --compose is also set.  If --compose is not
+                 set, this will be prompted for.
+
+   --chain-reply-to If set, the replies will all be to the previous
+                  email sent, rather than to the first email sent.
+                  Defaults to on.
+
+   --signed-off-cc Automatically add email addresses that appear in
+                 Signed-off-by: or Cc: lines to the cc: list. Defaults to on.
+
+   --identity     The configuration identity, a subsection to prioritise over
+                  the default section.
+
+   --smtp-server  If set, specifies the outgoing SMTP server to use.
+                  Defaults to localhost.  Port number can be specified here with
+                  hostname:port format or by using --smtp-server-port option.
+
+   --smtp-server-port Specify a port on the outgoing SMTP server to connect to.
+
+   --smtp-user    The username for SMTP-AUTH.
+
+   --smtp-pass    The password for SMTP-AUTH.
+
+   --smtp-ssl     If set, connects to the SMTP server using SSL.
+
+   --suppress-from Suppress sending emails to yourself. Defaults to off.
+
+   --thread       Specify that the "In-Reply-To:" header should be set on all
+                  emails. Defaults to on.
+
+   --quiet       Make git-send-email less verbose.  One line per email
+                  should be all that is output.
+
+   --dry-run     Do everything except actually send the emails.
+
+   --envelope-sender   Specify the envelope sender used to send the emails.
+
+EOT
+       exit(1);
+}
+
+# most mail servers generate the Date: header, but not all...
+sub format_2822_time {
+       my ($time) = @_;
+       my @localtm = localtime($time);
+       my @gmttm = gmtime($time);
+       my $localmin = $localtm[1] + $localtm[2] * 60;
+       my $gmtmin = $gmttm[1] + $gmttm[2] * 60;
+       if ($localtm[0] != $gmttm[0]) {
+               die "local zone differs from GMT by a non-minute interval\n";
+       }
+       if ((($gmttm[6] + 1) % 7) == $localtm[6]) {
+               $localmin += 1440;
+       } elsif ((($gmttm[6] - 1) % 7) == $localtm[6]) {
+               $localmin -= 1440;
+       } elsif ($gmttm[6] != $localtm[6]) {
+               die "local time offset greater than or equal to 24 hours\n";
+       }
+       my $offset = $localmin - $gmtmin;
+       my $offhour = $offset / 60;
+       my $offmin = abs($offset % 60);
+       if (abs($offhour) >= 24) {
+               die ("local time offset greater than or equal to 24 hours\n");
+       }
+
+       return sprintf("%s, %2d %s %d %02d:%02d:%02d %s%02d%02d",
+                      qw(Sun Mon Tue Wed Thu Fri Sat)[$localtm[6]],
+                      $localtm[3],
+                      qw(Jan Feb Mar Apr May Jun
+                         Jul Aug Sep Oct Nov Dec)[$localtm[4]],
+                      $localtm[5]+1900,
+                      $localtm[2],
+                      $localtm[1],
+                      $localtm[0],
+                      ($offset >= 0) ? '+' : '-',
+                      abs($offhour),
+                      $offmin,
+                      );
+}
+
+my $have_email_valid = eval { require Email::Valid; 1 };
+my $smtp;
+my $auth;
+
+sub unique_email_list(@);
+sub cleanup_compose_files();
+
+# Constants (essentially)
+my $compose_filename = ".msg.$$";
+
+# Variables we fill in automatically, or via prompting:
+my (@to,@cc,@initial_cc,@bcclist,@xh,
+       $initial_reply_to,$initial_subject,@files,$author,$sender,$compose,$time);
+
+my $envelope_sender;
+
+# Example reply to:
+#$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>';
+
+my $repo = Git->repository();
+my $term = eval {
+       new Term::ReadLine 'git-send-email';
+};
+if ($@) {
+       $term = new FakeTerm "$@: going non-interactive";
+}
+
+# Behavior modification variables
+my ($quiet, $dry_run) = (0, 0);
+
+# Variables with corresponding config settings
+my ($thread, $chain_reply_to, $suppress_from, $signed_off_cc, $cc_cmd);
+my ($smtp_server, $smtp_server_port, $smtp_authuser, $smtp_authpass, $smtp_ssl);
+my ($identity, $aliasfiletype, @alias_files, @smtp_host_parts);
+
+my %config_bool_settings = (
+    "thread" => [\$thread, 1],
+    "chainreplyto" => [\$chain_reply_to, 1],
+    "suppressfrom" => [\$suppress_from, 0],
+    "signedoffcc" => [\$signed_off_cc, 1],
+    "smtpssl" => [\$smtp_ssl, 0],
+);
+
+my %config_settings = (
+    "smtpserver" => \$smtp_server,
+    "smtpserverport" => \$smtp_server_port,
+    "smtpuser" => \$smtp_authuser,
+    "smtppass" => \$smtp_authpass,
+    "to" => \@to,
+    "cccmd" => \$cc_cmd,
+    "aliasfiletype" => \$aliasfiletype,
+    "bcc" => \@bcclist,
+    "aliasesfile" => \@alias_files,
+);
+
+# Begin by accumulating all the variables (defined above), that we will end up
+# needing, first, from the command line:
+
+my $rc = GetOptions("sender|from=s" => \$sender,
+                    "in-reply-to=s" => \$initial_reply_to,
+                   "subject=s" => \$initial_subject,
+                   "to=s" => \@to,
+                   "cc=s" => \@initial_cc,
+                   "bcc=s" => \@bcclist,
+                   "chain-reply-to!" => \$chain_reply_to,
+                   "smtp-server=s" => \$smtp_server,
+                   "smtp-server-port=s" => \$smtp_server_port,
+                   "smtp-user=s" => \$smtp_authuser,
+                   "smtp-pass=s" => \$smtp_authpass,
+                   "smtp-ssl!" => \$smtp_ssl,
+                   "identity=s" => \$identity,
+                   "compose" => \$compose,
+                   "quiet" => \$quiet,
+                   "cc-cmd=s" => \$cc_cmd,
+                   "suppress-from!" => \$suppress_from,
+                   "signed-off-cc|signed-off-by-cc!" => \$signed_off_cc,
+                   "dry-run" => \$dry_run,
+                   "envelope-sender=s" => \$envelope_sender,
+                   "thread!" => \$thread,
+        );
+
+unless ($rc) {
+    usage();
+}
+
+# Now, let's fill any that aren't set in with defaults:
+
+sub read_config {
+       my ($prefix) = @_;
+
+       foreach my $setting (keys %config_bool_settings) {
+               my $target = $config_bool_settings{$setting}->[0];
+               $$target = $repo->config_bool("$prefix.$setting") unless (defined $$target);
+       }
+
+       foreach my $setting (keys %config_settings) {
+               my $target = $config_settings{$setting};
+               if (ref($target) eq "ARRAY") {
+                       unless (@$target) {
+                               my @values = $repo->config("$prefix.$setting");
+                               @$target = @values if (@values && defined $values[0]);
+                       }
+               }
+               else {
+                       $$target = $repo->config("$prefix.$setting") unless (defined $$target);
+               }
+       }
+}
+
+# read configuration from [sendemail "$identity"], fall back on [sendemail]
+$identity = $repo->config("sendemail.identity") unless (defined $identity);
+read_config("sendemail.$identity") if (defined $identity);
+read_config("sendemail");
+
+# fall back on builtin bool defaults
+foreach my $setting (values %config_bool_settings) {
+       ${$setting->[0]} = $setting->[1] unless (defined (${$setting->[0]}));
+}
+
+my ($repoauthor) = $repo->ident_person('author');
+my ($repocommitter) = $repo->ident_person('committer');
+
+# Verify the user input
+
+foreach my $entry (@to) {
+       die "Comma in --to entry: $entry'\n" unless $entry !~ m/,/;
+}
+
+foreach my $entry (@initial_cc) {
+       die "Comma in --cc entry: $entry'\n" unless $entry !~ m/,/;
+}
+
+foreach my $entry (@bcclist) {
+       die "Comma in --bcclist entry: $entry'\n" unless $entry !~ m/,/;
+}
+
+my %aliases;
+my %parse_alias = (
+       # multiline formats can be supported in the future
+       mutt => sub { my $fh = shift; while (<$fh>) {
+               if (/^\s*alias\s+(\S+)\s+(.*)$/) {
+                       my ($alias, $addr) = ($1, $2);
+                       $addr =~ s/#.*$//; # mutt allows # comments
+                        # commas delimit multiple addresses
+                       $aliases{$alias} = [ split(/\s*,\s*/, $addr) ];
+               }}},
+       mailrc => sub { my $fh = shift; while (<$fh>) {
+               if (/^alias\s+(\S+)\s+(.*)$/) {
+                       # spaces delimit multiple addresses
+                       $aliases{$1} = [ split(/\s+/, $2) ];
+               }}},
+       pine => sub { my $fh = shift; while (<$fh>) {
+               if (/^(\S+)\t.*\t(.*)$/) {
+                       $aliases{$1} = [ split(/\s*,\s*/, $2) ];
+               }}},
+       gnus => sub { my $fh = shift; while (<$fh>) {
+               if (/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) {
+                       $aliases{$1} = [ $2 ];
+               }}}
+);
+
+if (@alias_files and $aliasfiletype and defined $parse_alias{$aliasfiletype}) {
+       foreach my $file (@alias_files) {
+               open my $fh, '<', $file or die "opening $file: $!\n";
+               $parse_alias{$aliasfiletype}->($fh);
+               close $fh;
+       }
+}
+
+($sender) = expand_aliases($sender) if defined $sender;
+
+my $prompting = 0;
+if (!defined $sender) {
+       $sender = $repoauthor || $repocommitter;
+       do {
+               $_ = $term->readline("Who should the emails appear to be from? [$sender] ");
+       } while (!defined $_);
+
+       $sender = $_ if ($_);
+       print "Emails will be sent from: ", $sender, "\n";
+       $prompting++;
+}
+
+if (!@to) {
+       do {
+               $_ = $term->readline("Who should the emails be sent to? ",
+                               "");
+       } while (!defined $_);
+       my $to = $_;
+       push @to, split /,/, $to;
+       $prompting++;
+}
+
+sub expand_aliases {
+       my @cur = @_;
+       my @last;
+       do {
+               @last = @cur;
+               @cur = map { $aliases{$_} ? @{$aliases{$_}} : $_ } @last;
+       } while (join(',',@cur) ne join(',',@last));
+       return @cur;
+}
+
+@to = expand_aliases(@to);
+@to = (map { sanitize_address($_) } @to);
+@initial_cc = expand_aliases(@initial_cc);
+@bcclist = expand_aliases(@bcclist);
+
+if (!defined $initial_subject && $compose) {
+       do {
+               $_ = $term->readline("What subject should the initial email start with? ",
+                       $initial_subject);
+       } while (!defined $_);
+       $initial_subject = $_;
+       $prompting++;
+}
+
+if ($thread && !defined $initial_reply_to && $prompting) {
+       do {
+               $_= $term->readline("Message-ID to be used as In-Reply-To for the first email? ",
+                       $initial_reply_to);
+       } while (!defined $_);
+
+       $initial_reply_to = $_;
+       $initial_reply_to =~ s/^\s+<?/</;
+       $initial_reply_to =~ s/>?\s+$/>/;
+}
+
+if (!defined $smtp_server) {
+       foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {
+               if (-x $_) {
+                       $smtp_server = $_;
+                       last;
+               }
+       }
+       $smtp_server ||= 'localhost'; # could be 127.0.0.1, too... *shrug*
+}
+
+if ($compose) {
+       # Note that this does not need to be secure, but we will make a small
+       # effort to have it be unique
+       open(C,">",$compose_filename)
+               or die "Failed to open for writing $compose_filename: $!";
+       print C "From $sender # This line is ignored.\n";
+       printf C "Subject: %s\n\n", $initial_subject;
+       printf C <<EOT;
+GIT: Please enter your email below.
+GIT: Lines beginning in "GIT: " will be removed.
+GIT: Consider including an overall diffstat or table of contents
+GIT: for the patch you are writing.
+
+EOT
+       close(C);
+
+       my $editor = $ENV{GIT_EDITOR} || $repo->config("core.editor") || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
+       system($editor, $compose_filename);
+
+       open(C2,">",$compose_filename . ".final")
+               or die "Failed to open $compose_filename.final : " . $!;
+
+       open(C,"<",$compose_filename)
+               or die "Failed to open $compose_filename : " . $!;
+
+       while(<C>) {
+               next if m/^GIT: /;
+               print C2 $_;
+       }
+       close(C);
+       close(C2);
+
+       do {
+               $_ = $term->readline("Send this email? (y|n) ");
+       } while (!defined $_);
+
+       if (uc substr($_,0,1) ne 'Y') {
+               cleanup_compose_files();
+               exit(0);
+       }
+
+       @files = ($compose_filename . ".final");
+}
+
+
+# Now that all the defaults are set, process the rest of the command line
+# arguments and collect up the files that need to be processed.
+for my $f (@ARGV) {
+       if (-d $f) {
+               opendir(DH,$f)
+                       or die "Failed to opendir $f: $!";
+
+               push @files, grep { -f $_ } map { +$f . "/" . $_ }
+                               sort readdir(DH);
+
+       } elsif (-f $f) {
+               push @files, $f;
+
+       } else {
+               print STDERR "Skipping $f - not found.\n";
+       }
+}
+
+if (@files) {
+       unless ($quiet) {
+               print $_,"\n" for (@files);
+       }
+} else {
+       print STDERR "\nNo patch files specified!\n\n";
+       usage();
+}
+
+# Variables we set as part of the loop over files
+our ($message_id, %mail, $subject, $reply_to, $references, $message);
+
+sub extract_valid_address {
+       my $address = shift;
+       my $local_part_regexp = '[^<>"\s@]+';
+       my $domain_regexp = '[^.<>"\s@]+(?:\.[^.<>"\s@]+)+';
+
+       # check for a local address:
+       return $address if ($address =~ /^($local_part_regexp)$/);
+
+       $address =~ s/^\s*<(.*)>\s*$/$1/;
+       if ($have_email_valid) {
+               return scalar Email::Valid->address($address);
+       } else {
+               # less robust/correct than the monster regexp in Email::Valid,
+               # but still does a 99% job, and one less dependency
+               $address =~ /($local_part_regexp\@$domain_regexp)/;
+               return $1;
+       }
+}
+
+# Usually don't need to change anything below here.
+
+# we make a "fake" message id by taking the current number
+# of seconds since the beginning of Unix time and tacking on
+# a random number to the end, in case we are called quicker than
+# 1 second since the last time we were called.
+
+# We'll setup a template for the message id, using the "from" address:
+
+my ($message_id_stamp, $message_id_serial);
+sub make_message_id
+{
+       my $uniq;
+       if (!defined $message_id_stamp) {
+               $message_id_stamp = sprintf("%s-%s", time, $$);
+               $message_id_serial = 0;
+       }
+       $message_id_serial++;
+       $uniq = "$message_id_stamp-$message_id_serial";
+
+       my $du_part;
+       for ($sender, $repocommitter, $repoauthor) {
+               $du_part = extract_valid_address(sanitize_address($_));
+               last if (defined $du_part and $du_part ne '');
+       }
+       if (not defined $du_part or $du_part eq '') {
+               use Sys::Hostname qw();
+               $du_part = 'user@' . Sys::Hostname::hostname();
+       }
+       my $message_id_template = "<%s-git-send-email-%s>";
+       $message_id = sprintf($message_id_template, $uniq, $du_part);
+       #print "new message id = $message_id\n"; # Was useful for debugging
+}
+
+
+
+$time = time - scalar $#files;
+
+sub unquote_rfc2047 {
+       local ($_) = @_;
+       my $encoding;
+       if (s/=\?([^?]+)\?q\?(.*)\?=/$2/g) {
+               $encoding = $1;
+               s/_/ /g;
+               s/=([0-9A-F]{2})/chr(hex($1))/eg;
+       }
+       return wantarray ? ($_, $encoding) : $_;
+}
+
+# use the simplest quoting being able to handle the recipient
+sub sanitize_address
+{
+       my ($recipient) = @_;
+       my ($recipient_name, $recipient_addr) = ($recipient =~ /^(.*?)\s*(<.*)/);
+
+       if (not $recipient_name) {
+               return "$recipient";
+       }
+
+       # if recipient_name is already quoted, do nothing
+       if ($recipient_name =~ /^(".*"|=\?utf-8\?q\?.*\?=)$/) {
+               return $recipient;
+       }
+
+       # rfc2047 is needed if a non-ascii char is included
+       if ($recipient_name =~ /[^[:ascii:]]/) {
+               $recipient_name =~ s/([^-a-zA-Z0-9!*+\/])/sprintf("=%02X", ord($1))/eg;
+               $recipient_name =~ s/(.*)/=\?utf-8\?q\?$1\?=/;
+       }
+
+       # double quotes are needed if specials or CTLs are included
+       elsif ($recipient_name =~ /[][()<>@,;:\\".\000-\037\177]/) {
+               $recipient_name =~ s/(["\\\r])/\\$1/;
+               $recipient_name = "\"$recipient_name\"";
+       }
+
+       return "$recipient_name $recipient_addr";
+
+}
+
+sub send_message
+{
+       my @recipients = unique_email_list(@to);
+       @cc = (grep { my $cc = extract_valid_address($_);
+                     not grep { $cc eq $_ } @recipients
+                   }
+              map { sanitize_address($_) }
+              @cc);
+       my $to = join (",\n\t", @recipients);
+       @recipients = unique_email_list(@recipients,@cc,@bcclist);
+       @recipients = (map { extract_valid_address($_) } @recipients);
+       my $date = format_2822_time($time++);
+       my $gitversion = '@@GIT_VERSION@@';
+       if ($gitversion =~ m/..GIT_VERSION../) {
+           $gitversion = Git::version();
+       }
+
+       my $cc = join(", ", unique_email_list(@cc));
+       my $ccline = "";
+       if ($cc ne '') {
+               $ccline = "\nCc: $cc";
+       }
+       my $sanitized_sender = sanitize_address($sender);
+       make_message_id();
+
+       my $header = "From: $sanitized_sender
+To: $to${ccline}
+Subject: $subject
+Date: $date
+Message-Id: $message_id
+X-Mailer: git-send-email $gitversion
+";
+       if ($thread && $reply_to) {
+
+               $header .= "In-Reply-To: $reply_to\n";
+               $header .= "References: $references\n";
+       }
+       if (@xh) {
+               $header .= join("\n", @xh) . "\n";
+       }
+
+       my @sendmail_parameters = ('-i', @recipients);
+       my $raw_from = $sanitized_sender;
+       $raw_from = $envelope_sender if (defined $envelope_sender);
+       $raw_from = extract_valid_address($raw_from);
+       unshift (@sendmail_parameters,
+                       '-f', $raw_from) if(defined $envelope_sender);
+
+       if ($dry_run) {
+               # We don't want to send the email.
+       } elsif ($smtp_server =~ m#^/#) {
+               my $pid = open my $sm, '|-';
+               defined $pid or die $!;
+               if (!$pid) {
+                       exec($smtp_server, @sendmail_parameters) or die $!;
+               }
+               print $sm "$header\n$message";
+               close $sm or die $?;
+       } else {
+
+               if (!defined $smtp_server) {
+                       die "The required SMTP server is not properly defined."
+               }
+
+               if ($smtp_ssl) {
+                       $smtp_server_port ||= 465; # ssmtp
+                       require Net::SMTP::SSL;
+                       $smtp ||= Net::SMTP::SSL->new($smtp_server, Port => $smtp_server_port);
+               }
+               else {
+                       require Net::SMTP;
+                       $smtp ||= Net::SMTP->new((defined $smtp_server_port)
+                                                ? "$smtp_server:$smtp_server_port"
+                                                : $smtp_server);
+               }
+
+               if (!$smtp) {
+                       die "Unable to initialize SMTP properly.  Is there something wrong with your config?";
+               }
+
+               if ((defined $smtp_authuser) && (defined $smtp_authpass)) {
+                       $auth ||= $smtp->auth( $smtp_authuser, $smtp_authpass ) or die $smtp->message;
+               }
+               $smtp->mail( $raw_from ) or die $smtp->message;
+               $smtp->to( @recipients ) or die $smtp->message;
+               $smtp->data or die $smtp->message;
+               $smtp->datasend("$header\n$message") or die $smtp->message;
+               $smtp->dataend() or die $smtp->message;
+               $smtp->ok or die "Failed to send $subject\n".$smtp->message;
+       }
+       if ($quiet) {
+               printf (($dry_run ? "Dry-" : "")."Sent %s\n", $subject);
+       } else {
+               print (($dry_run ? "Dry-" : "")."OK. Log says:\n");
+               if ($smtp_server !~ m#^/#) {
+                       print "Server: $smtp_server\n";
+                       print "MAIL FROM:<$raw_from>\n";
+                       print "RCPT TO:".join(',',(map { "<$_>" } @recipients))."\n";
+               } else {
+                       print "Sendmail: $smtp_server ".join(' ',@sendmail_parameters)."\n";
+               }
+               print $header, "\n";
+               if ($smtp) {
+                       print "Result: ", $smtp->code, ' ',
+                               ($smtp->message =~ /\n([^\n]+\n)$/s), "\n";
+               } else {
+                       print "Result: OK\n";
+               }
+       }
+}
+
+$reply_to = $initial_reply_to;
+$references = $initial_reply_to || '';
+$subject = $initial_subject;
+
+foreach my $t (@files) {
+       open(F,"<",$t) or die "can't open file $t";
+
+       my $author = undef;
+       my $author_encoding;
+       my $has_content_type;
+       my $body_encoding;
+       @cc = @initial_cc;
+       @xh = ();
+       my $input_format = undef;
+       my $header_done = 0;
+       $message = "";
+       while(<F>) {
+               if (!$header_done) {
+                       if (/^From /) {
+                               $input_format = 'mbox';
+                               next;
+                       }
+                       chomp;
+                       if (!defined $input_format && /^[-A-Za-z]+:\s/) {
+                               $input_format = 'mbox';
+                       }
+
+                       if (defined $input_format && $input_format eq 'mbox') {
+                               if (/^Subject:\s+(.*)$/) {
+                                       $subject = $1;
+
+                               } elsif (/^(Cc|From):\s+(.*)$/) {
+                                       if (unquote_rfc2047($2) eq $sender) {
+                                               next if ($suppress_from);
+                                       }
+                                       elsif ($1 eq 'From') {
+                                               ($author, $author_encoding)
+                                                 = unquote_rfc2047($2);
+                                       }
+                                       printf("(mbox) Adding cc: %s from line '%s'\n",
+                                               $2, $_) unless $quiet;
+                                       push @cc, $2;
+                               }
+                               elsif (/^Content-type:/i) {
+                                       $has_content_type = 1;
+                                       if (/charset="?[^ "]+/) {
+                                               $body_encoding = $1;
+                                       }
+                                       push @xh, $_;
+                               }
+                               elsif (!/^Date:\s/ && /^[-A-Za-z]+:\s+\S/) {
+                                       push @xh, $_;
+                               }
+
+                       } else {
+                               # In the traditional
+                               # "send lots of email" format,
+                               # line 1 = cc
+                               # line 2 = subject
+                               # So let's support that, too.
+                               $input_format = 'lots';
+                               if (@cc == 0) {
+                                       printf("(non-mbox) Adding cc: %s from line '%s'\n",
+                                               $_, $_) unless $quiet;
+
+                                       push @cc, $_;
+
+                               } elsif (!defined $subject) {
+                                       $subject = $_;
+                               }
+                       }
+
+                       # A whitespace line will terminate the headers
+                       if (m/^\s*$/) {
+                               $header_done = 1;
+                       }
+               } else {
+                       $message .=  $_;
+                       if (/^(Signed-off-by|Cc): (.*)$/i && $signed_off_cc) {
+                               my $c = $2;
+                               chomp $c;
+                               next if ($c eq $sender and $suppress_from);
+                               push @cc, $c;
+                               printf("(sob) Adding cc: %s from line '%s'\n",
+                                       $c, $_) unless $quiet;
+                       }
+               }
+       }
+       close F;
+
+       if (defined $cc_cmd) {
+               open(F, "$cc_cmd $t |")
+                       or die "(cc-cmd) Could not execute '$cc_cmd'";
+               while(<F>) {
+                       my $c = $_;
+                       $c =~ s/^\s*//g;
+                       $c =~ s/\n$//g;
+                       next if ($c eq $sender and $suppress_from);
+                       push @cc, $c;
+                       printf("(cc-cmd) Adding cc: %s from: '%s'\n",
+                               $c, $cc_cmd) unless $quiet;
+               }
+               close F
+                       or die "(cc-cmd) failed to close pipe to '$cc_cmd'";
+       }
+
+       if (defined $author) {
+               $message = "From: $author\n\n$message";
+               if (defined $author_encoding) {
+                       if ($has_content_type) {
+                               if ($body_encoding eq $author_encoding) {
+                                       # ok, we already have the right encoding
+                               }
+                               else {
+                                       # uh oh, we should re-encode
+                               }
+                       }
+                       else {
+                               push @xh,
+                                 'MIME-Version: 1.0',
+                                 "Content-Type: text/plain; charset=$author_encoding",
+                                 'Content-Transfer-Encoding: 8bit';
+                       }
+               }
+       }
+
+       send_message();
+
+       # set up for the next message
+       if ($chain_reply_to || !defined $reply_to || length($reply_to) == 0) {
+               $reply_to = $message_id;
+               if (length $references > 0) {
+                       $references .= "\n $message_id";
+               } else {
+                       $references = "$message_id";
+               }
+       }
+}
+
+if ($compose) {
+       cleanup_compose_files();
+}
+
+sub cleanup_compose_files() {
+       unlink($compose_filename, $compose_filename . ".final");
+
+}
+
+$smtp->quit if $smtp;
+
+sub unique_email_list(@) {
+       my %seen;
+       my @emails;
+
+       foreach my $entry (@_) {
+               if (my $clean = extract_valid_address($entry)) {
+                       $seen{$clean} ||= 0;
+                       next if $seen{$clean}++;
+                       push @emails, $entry;
+               } else {
+                       print STDERR "W: unable to extract a valid address",
+                                       " from: $entry\n";
+               }
+       }
+       return @emails;
+}
diff --git a/git-sh-setup.sh b/git-sh-setup.sh
new file mode 100755 (executable)
index 0000000..5aa62dd
--- /dev/null
@@ -0,0 +1,147 @@
+#!/bin/sh
+#
+# This is included in commands that either have to be run from the toplevel
+# of the repository, or with GIT_DIR environment variable properly.
+# If the GIT_DIR does not look like the right correct git-repository,
+# it dies.
+
+# Having this variable in your environment would break scripts because
+# you would cause "cd" to be taken to unexpected places.  If you
+# like CDPATH, define it for your interactive shell sessions without
+# exporting it.
+unset CDPATH
+
+die() {
+       echo >&2 "$@"
+       exit 1
+}
+
+if test -n "$OPTIONS_SPEC"; then
+       usage() {
+               exec "$0" -h
+       }
+
+       parseopt_extra=
+       [ -n "$OPTIONS_KEEPDASHDASH" ] &&
+               parseopt_extra="--keep-dashdash"
+
+       eval "$(
+               echo "$OPTIONS_SPEC" |
+                       git rev-parse --parseopt $parseopt_extra -- "$@" ||
+               echo exit $?
+       )"
+else
+       usage() {
+               die "Usage: $0 $USAGE"
+       }
+
+       if [ -z "$LONG_USAGE" ]
+       then
+               LONG_USAGE="Usage: $0 $USAGE"
+       else
+               LONG_USAGE="Usage: $0 $USAGE
+
+$LONG_USAGE"
+       fi
+
+       case "$1" in
+               -h|--h|--he|--hel|--help)
+               echo "$LONG_USAGE"
+               exit
+       esac
+fi
+
+set_reflog_action() {
+       if [ -z "${GIT_REFLOG_ACTION:+set}" ]
+       then
+               GIT_REFLOG_ACTION="$*"
+               export GIT_REFLOG_ACTION
+       fi
+}
+
+git_editor() {
+       : "${GIT_EDITOR:=$(git config core.editor)}"
+       : "${GIT_EDITOR:=${VISUAL:-${EDITOR}}}"
+       case "$GIT_EDITOR,$TERM" in
+       ,dumb)
+               echo >&2 "No editor specified in GIT_EDITOR, core.editor, VISUAL,"
+               echo >&2 "or EDITOR. Tried to fall back to vi but terminal is dumb."
+               echo >&2 "Please set one of these variables to an appropriate"
+               echo >&2 "editor or run $0 with options that will not cause an"
+               echo >&2 "editor to be invoked (e.g., -m or -F for git-commit)."
+               exit 1
+               ;;
+       esac
+       eval "${GIT_EDITOR:=vi}" '"$@"'
+}
+
+is_bare_repository () {
+       git rev-parse --is-bare-repository
+}
+
+cd_to_toplevel () {
+       cdup=$(git rev-parse --show-cdup)
+       if test ! -z "$cdup"
+       then
+               cd "$cdup" || {
+                       echo >&2 "Cannot chdir to $cdup, the toplevel of the working tree"
+                       exit 1
+               }
+       fi
+}
+
+require_work_tree () {
+       test $(git rev-parse --is-inside-work-tree) = true ||
+       die "fatal: $0 cannot be used without a working tree."
+}
+
+get_author_ident_from_commit () {
+       pick_author_script='
+       /^author /{
+               s/'\''/'\''\\'\'\''/g
+               h
+               s/^author \([^<]*\) <[^>]*> .*$/\1/
+               s/'\''/'\''\'\'\''/g
+               s/.*/GIT_AUTHOR_NAME='\''&'\''/p
+
+               g
+               s/^author [^<]* <\([^>]*\)> .*$/\1/
+               s/'\''/'\''\'\'\''/g
+               s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p
+
+               g
+               s/^author [^<]* <[^>]*> \(.*\)$/\1/
+               s/'\''/'\''\'\'\''/g
+               s/.*/GIT_AUTHOR_DATE='\''&'\''/p
+
+               q
+       }
+       '
+       encoding=$(git config i18n.commitencoding || echo UTF-8)
+       git show -s --pretty=raw --encoding="$encoding" "$1" |
+       LANG=C LC_ALL=C sed -ne "$pick_author_script"
+}
+
+# Make sure we are in a valid repository of a vintage we understand.
+if [ -z "$SUBDIRECTORY_OK" ]
+then
+       : ${GIT_DIR=.git}
+       test -z "$(git rev-parse --show-cdup)" || {
+               exit=$?
+               echo >&2 "You need to run this command from the toplevel of the working tree."
+               exit $exit
+       }
+else
+       GIT_DIR=$(git rev-parse --git-dir) || {
+           exit=$?
+           echo >&2 "Failed to find a valid git directory."
+           exit $exit
+       }
+fi
+
+test -n "$GIT_DIR" && GIT_DIR=$(cd "$GIT_DIR" && pwd) || {
+    echo >&2 "Unable to determine absolute path of git directory"
+    exit 1
+}
+
+: ${GIT_OBJECT_DIRECTORY="$GIT_DIR/objects"}
diff --git a/git-stash.sh b/git-stash.sh
new file mode 100755 (executable)
index 0000000..f16fd9c
--- /dev/null
@@ -0,0 +1,236 @@
+#!/bin/sh
+# Copyright (c) 2007, Nanako Shiraishi
+
+USAGE='[  | save | list | show | apply | clear | create ]'
+
+SUBDIRECTORY_OK=Yes
+OPTIONS_SPEC=
+. git-sh-setup
+require_work_tree
+cd_to_toplevel
+
+TMP="$GIT_DIR/.git-stash.$$"
+trap 'rm -f "$TMP-*"' 0
+
+ref_stash=refs/stash
+
+no_changes () {
+       git diff-index --quiet --cached HEAD -- &&
+       git diff-files --quiet
+}
+
+clear_stash () {
+       if current=$(git rev-parse --verify $ref_stash 2>/dev/null)
+       then
+               git update-ref -d $ref_stash $current
+       fi
+}
+
+create_stash () {
+       stash_msg="$1"
+
+       if no_changes
+       then
+               exit 0
+       fi
+
+       # state of the base commit
+       if b_commit=$(git rev-parse --verify HEAD)
+       then
+               head=$(git log --no-color --abbrev-commit --pretty=oneline -n 1 HEAD --)
+       else
+               die "You do not have the initial commit yet"
+       fi
+
+       if branch=$(git symbolic-ref -q HEAD)
+       then
+               branch=${branch#refs/heads/}
+       else
+               branch='(no branch)'
+       fi
+       msg=$(printf '%s: %s' "$branch" "$head")
+
+       # state of the index
+       i_tree=$(git write-tree) &&
+       i_commit=$(printf 'index on %s\n' "$msg" |
+               git commit-tree $i_tree -p $b_commit) ||
+               die "Cannot save the current index state"
+
+       # state of the working tree
+       w_tree=$( (
+               rm -f "$TMP-index" &&
+               cp -p ${GIT_INDEX_FILE-"$GIT_DIR/index"} "$TMP-index" &&
+               GIT_INDEX_FILE="$TMP-index" &&
+               export GIT_INDEX_FILE &&
+               git read-tree -m $i_tree &&
+               git add -u &&
+               git write-tree &&
+               rm -f "$TMP-index"
+       ) ) ||
+               die "Cannot save the current worktree state"
+
+       # create the stash
+       if test -z "$stash_msg"
+       then
+               stash_msg=$(printf 'WIP on %s' "$msg")
+       else
+               stash_msg=$(printf 'On %s: %s' "$branch" "$stash_msg")
+       fi
+       w_commit=$(printf '%s\n' "$stash_msg" |
+               git commit-tree $w_tree -p $b_commit -p $i_commit) ||
+               die "Cannot record working tree state"
+}
+
+save_stash () {
+       stash_msg="$1"
+
+       if no_changes
+       then
+               echo >&2 'No local changes to save'
+               exit 0
+       fi
+       test -f "$GIT_DIR/logs/$ref_stash" ||
+               clear_stash || die "Cannot initialize stash"
+
+       create_stash "$stash_msg"
+
+       # Make sure the reflog for stash is kept.
+       : >>"$GIT_DIR/logs/$ref_stash"
+
+       git update-ref -m "$stash_msg" $ref_stash $w_commit ||
+               die "Cannot save the current status"
+       printf >&2 'Saved "%s"\n' "$stash_msg"
+}
+
+have_stash () {
+       git rev-parse --verify $ref_stash >/dev/null 2>&1
+}
+
+list_stash () {
+       have_stash || return 0
+       git log --no-color --pretty=oneline -g "$@" $ref_stash -- |
+       sed -n -e 's/^[.0-9a-f]* refs\///p'
+}
+
+show_stash () {
+       flags=$(git rev-parse --no-revs --flags "$@")
+       if test -z "$flags"
+       then
+               flags=--stat
+       fi
+       s=$(git rev-parse --revs-only --no-flags --default $ref_stash "$@")
+
+       w_commit=$(git rev-parse --verify "$s") &&
+       b_commit=$(git rev-parse --verify "$s^") &&
+       git diff $flags $b_commit $w_commit
+}
+
+apply_stash () {
+       git diff-files --quiet ||
+               die 'Cannot restore on top of a dirty state'
+
+       unstash_index=
+       case "$1" in
+       --index)
+               unstash_index=t
+               shift
+       esac
+
+       # current index state
+       c_tree=$(git write-tree) ||
+               die 'Cannot apply a stash in the middle of a merge'
+
+       # stash records the work tree, and is a merge between the
+       # base commit (first parent) and the index tree (second parent).
+       s=$(git rev-parse --revs-only --no-flags --default $ref_stash "$@") &&
+       w_tree=$(git rev-parse --verify "$s:") &&
+       b_tree=$(git rev-parse --verify "$s^1:") &&
+       i_tree=$(git rev-parse --verify "$s^2:") ||
+               die "$*: no valid stashed state found"
+
+       unstashed_index_tree=
+       if test -n "$unstash_index" && test "$b_tree" != "$i_tree"
+       then
+               git diff-tree --binary $s^2^..$s^2 | git apply --cached
+               test $? -ne 0 &&
+                       die 'Conflicts in index. Try without --index.'
+               unstashed_index_tree=$(git-write-tree) ||
+                       die 'Could not save index tree'
+               git reset
+       fi
+
+       eval "
+               GITHEAD_$w_tree='Stashed changes' &&
+               GITHEAD_$c_tree='Updated upstream' &&
+               GITHEAD_$b_tree='Version stash was based on' &&
+               export GITHEAD_$w_tree GITHEAD_$c_tree GITHEAD_$b_tree
+       "
+
+       if git-merge-recursive $b_tree -- $c_tree $w_tree
+       then
+               # No conflict
+               if test -n "$unstashed_index_tree"
+               then
+                       git read-tree "$unstashed_index_tree"
+               else
+                       a="$TMP-added" &&
+                       git diff-index --cached --name-only --diff-filter=A $c_tree >"$a" &&
+                       git read-tree --reset $c_tree &&
+                       git update-index --add --stdin <"$a" ||
+                               die "Cannot unstage modified files"
+                       rm -f "$a"
+               fi
+               git status || :
+       else
+               # Merge conflict; keep the exit status from merge-recursive
+               status=$?
+               if test -n "$unstash_index"
+               then
+                       echo >&2 'Index was not unstashed.'
+               fi
+               exit $status
+       fi
+}
+
+# Main command set
+case "$1" in
+list)
+       shift
+       if test $# = 0
+       then
+               set x -n 10
+               shift
+       fi
+       list_stash "$@"
+       ;;
+show)
+       shift
+       show_stash "$@"
+       ;;
+save)
+       shift
+       save_stash "$*" && git-reset --hard
+       ;;
+apply)
+       shift
+       apply_stash "$@"
+       ;;
+clear)
+       clear_stash
+       ;;
+create)
+       if test $# -gt 0 && test "$1" = create
+       then
+               shift
+       fi
+       create_stash "$*" && echo "$w_commit"
+       ;;
+*)
+       if test $# -eq 0
+       then
+               save_stash && git-reset --hard
+       else
+               usage
+       fi
+       ;;
+esac
diff --git a/git-submodule.sh b/git-submodule.sh
new file mode 100755 (executable)
index 0000000..ad9fe62
--- /dev/null
@@ -0,0 +1,364 @@
+#!/bin/sh
+#
+# git-submodules.sh: add, init, update or list git submodules
+#
+# Copyright (c) 2007 Lars Hjemli
+
+USAGE='[--quiet] [--cached] [add <repo> [-b branch]|status|init|update] [--] [<path>...]'
+OPTIONS_SPEC=
+. git-sh-setup
+require_work_tree
+
+add=
+branch=
+init=
+update=
+status=
+quiet=
+cached=
+
+#
+# print stuff on stdout unless -q was specified
+#
+say()
+{
+       if test -z "$quiet"
+       then
+               echo "$@"
+       fi
+}
+
+# NEEDSWORK: identical function exists in get_repo_base in clone.sh
+get_repo_base() {
+       (
+               cd "`/bin/pwd`" &&
+               cd "$1" || cd "$1.git" &&
+               {
+                       cd .git
+                       pwd
+               }
+       ) 2>/dev/null
+}
+
+# Resolve relative url by appending to parent's url
+resolve_relative_url ()
+{
+       branch="$(git symbolic-ref HEAD 2>/dev/null)"
+       remote="$(git config branch.${branch#refs/heads/}.remote)"
+       remote="${remote:-origin}"
+       remoteurl="$(git config remote.$remote.url)" ||
+               die "remote ($remote) does not have a url in .git/config"
+       url="$1"
+       while test -n "$url"
+       do
+               case "$url" in
+               ../*)
+                       url="${url#../}"
+                       remoteurl="${remoteurl%/*}"
+                       ;;
+               ./*)
+                       url="${url#./}"
+                       ;;
+               *)
+                       break;;
+               esac
+       done
+       echo "$remoteurl/$url"
+}
+
+#
+# Map submodule path to submodule name
+#
+# $1 = path
+#
+module_name()
+{
+       # Do we have "submodule.<something>.path = $1" defined in .gitmodules file?
+       re=$(printf '%s' "$1" | sed -e 's/[].[^$\\*]/\\&/g')
+       name=$( GIT_CONFIG=.gitmodules \
+               git config --get-regexp '^submodule\..*\.path$' |
+               sed -n -e 's|^submodule\.\(.*\)\.path '"$re"'$|\1|p' )
+       test -z "$name" &&
+       die "No submodule mapping found in .gitmodules for path '$path'"
+       echo "$name"
+}
+
+#
+# Clone a submodule
+#
+# Prior to calling, modules_update checks that a possibly existing
+# path is not a git repository.
+# Likewise, module_add checks that path does not exist at all,
+# since it is the location of a new submodule.
+#
+module_clone()
+{
+       path=$1
+       url=$2
+
+       # If there already is a directory at the submodule path,
+       # expect it to be empty (since that is the default checkout
+       # action) and try to remove it.
+       # Note: if $path is a symlink to a directory the test will
+       # succeed but the rmdir will fail. We might want to fix this.
+       if test -d "$path"
+       then
+               rmdir "$path" 2>/dev/null ||
+               die "Directory '$path' exist, but is neither empty nor a git repository"
+       fi
+
+       test -e "$path" &&
+       die "A file already exist at path '$path'"
+
+       git-clone -n "$url" "$path" ||
+       die "Clone of '$url' into submodule path '$path' failed"
+}
+
+#
+# Add a new submodule to the working tree, .gitmodules and the index
+#
+# $@ = repo [path]
+#
+# optional branch is stored in global branch variable
+#
+module_add()
+{
+       repo=$1
+       path=$2
+
+       if test -z "$repo"; then
+               usage
+       fi
+
+       case "$repo" in
+       ./*|../*)
+               # dereference source url relative to parent's url
+               realrepo="$(resolve_relative_url $repo)" ;;
+       *)
+               # Turn the source into an absolute path if
+               # it is local
+               if base=$(get_repo_base "$repo"); then
+                       repo="$base"
+               fi
+               realrepo=$repo
+               ;;
+       esac
+
+       # Guess path from repo if not specified or strip trailing slashes
+       if test -z "$path"; then
+               path=$(echo "$repo" | sed -e 's|/*$||' -e 's|:*/*\.git$||' -e 's|.*[/:]||g')
+       else
+               path=$(echo "$path" | sed -e 's|/*$||')
+       fi
+
+       test -e "$path" &&
+       die "'$path' already exists"
+
+       git ls-files --error-unmatch "$path" > /dev/null 2>&1 &&
+       die "'$path' already exists in the index"
+
+       module_clone "$path" "$realrepo" || exit
+       (unset GIT_DIR; cd "$path" && git checkout -q ${branch:+-b "$branch" "origin/$branch"}) ||
+       die "Unable to checkout submodule '$path'"
+       git add "$path" ||
+       die "Failed to add submodule '$path'"
+
+       GIT_CONFIG=.gitmodules git config submodule."$path".path "$path" &&
+       GIT_CONFIG=.gitmodules git config submodule."$path".url "$repo" &&
+       git add .gitmodules ||
+       die "Failed to register submodule '$path'"
+}
+
+#
+# Register submodules in .git/config
+#
+# $@ = requested paths (default to all)
+#
+modules_init()
+{
+       git ls-files --stage -- "$@" | grep -e '^160000 ' |
+       while read mode sha1 stage path
+       do
+               # Skip already registered paths
+               name=$(module_name "$path") || exit
+               url=$(git config submodule."$name".url)
+               test -z "$url" || continue
+
+               url=$(GIT_CONFIG=.gitmodules git config submodule."$name".url)
+               test -z "$url" &&
+               die "No url found for submodule path '$path' in .gitmodules"
+
+               # Possibly a url relative to parent
+               case "$url" in
+               ./*|../*)
+                       url="$(resolve_relative_url "$url")"
+                       ;;
+               esac
+
+               git config submodule."$name".url "$url" ||
+               die "Failed to register url for submodule path '$path'"
+
+               say "Submodule '$name' ($url) registered for path '$path'"
+       done
+}
+
+#
+# Update each submodule path to correct revision, using clone and checkout as needed
+#
+# $@ = requested paths (default to all)
+#
+modules_update()
+{
+       git ls-files --stage -- "$@" | grep -e '^160000 ' |
+       while read mode sha1 stage path
+       do
+               name=$(module_name "$path") || exit
+               url=$(git config submodule."$name".url)
+               if test -z "$url"
+               then
+                       # Only mention uninitialized submodules when its
+                       # path have been specified
+                       test "$#" != "0" &&
+                       say "Submodule path '$path' not initialized"
+                       continue
+               fi
+
+               if ! test -d "$path"/.git
+               then
+                       module_clone "$path" "$url" || exit
+                       subsha1=
+               else
+                       subsha1=$(unset GIT_DIR; cd "$path" &&
+                               git rev-parse --verify HEAD) ||
+                       die "Unable to find current revision in submodule path '$path'"
+               fi
+
+               if test "$subsha1" != "$sha1"
+               then
+                       (unset GIT_DIR; cd "$path" && git-fetch &&
+                               git-checkout -q "$sha1") ||
+                       die "Unable to checkout '$sha1' in submodule path '$path'"
+
+                       say "Submodule path '$path': checked out '$sha1'"
+               fi
+       done
+}
+
+set_name_rev () {
+       revname=$( (
+               unset GIT_DIR
+               cd "$1" && {
+                       git describe "$2" 2>/dev/null ||
+                       git describe --tags "$2" 2>/dev/null ||
+                       git describe --contains --tags "$2"
+               }
+       ) )
+       test -z "$revname" || revname=" ($revname)"
+}
+
+#
+# List all submodules, prefixed with:
+#  - submodule not initialized
+#  + different revision checked out
+#
+# If --cached was specified the revision in the index will be printed
+# instead of the currently checked out revision.
+#
+# $@ = requested paths (default to all)
+#
+modules_list()
+{
+       git ls-files --stage -- "$@" | grep -e '^160000 ' |
+       while read mode sha1 stage path
+       do
+               name=$(module_name "$path") || exit
+               url=$(git config submodule."$name".url)
+               if test -z "url" || ! test -d "$path"/.git
+               then
+                       say "-$sha1 $path"
+                       continue;
+               fi
+               set_name_rev "$path" "$sha1"
+               if git diff-files --quiet -- "$path"
+               then
+                       say " $sha1 $path$revname"
+               else
+                       if test -z "$cached"
+                       then
+                               sha1=$(unset GIT_DIR; cd "$path" && git rev-parse --verify HEAD)
+                               set_name_rev "$path" "$sha1"
+                       fi
+                       say "+$sha1 $path$revname"
+               fi
+       done
+}
+
+while test $# != 0
+do
+       case "$1" in
+       add)
+               add=1
+               ;;
+       init)
+               init=1
+               ;;
+       update)
+               update=1
+               ;;
+       status)
+               status=1
+               ;;
+       -q|--quiet)
+               quiet=1
+               ;;
+       -b|--branch)
+               case "$2" in
+               '')
+                       usage
+                       ;;
+               esac
+               branch="$2"; shift
+               ;;
+       --cached)
+               cached=1
+               ;;
+       --)
+               break
+               ;;
+       -*)
+               usage
+               ;;
+       *)
+               break
+               ;;
+       esac
+       shift
+done
+
+case "$add,$branch" in
+1,*)
+       ;;
+,)
+       ;;
+,*)
+       usage
+       ;;
+esac
+
+case "$add,$init,$update,$status,$cached" in
+1,,,,)
+       module_add "$@"
+       ;;
+,1,,,)
+       modules_init "$@"
+       ;;
+,,1,,)
+       modules_update "$@"
+       ;;
+,,,*,*)
+       modules_list "$@"
+       ;;
+*)
+       usage
+       ;;
+esac
diff --git a/git-svn.perl b/git-svn.perl
new file mode 100755 (executable)
index 0000000..9f884eb
--- /dev/null
@@ -0,0 +1,4663 @@
+#!/usr/bin/env perl
+# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
+# License: GPL v2 or later
+use warnings;
+use strict;
+use vars qw/   $AUTHOR $VERSION
+               $sha1 $sha1_short $_revision
+               $_q $_authors %users/;
+$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
+$VERSION = '@@GIT_VERSION@@';
+
+# From which subdir have we been invoked?
+my $cmd_dir_prefix = eval {
+       command_oneline([qw/rev-parse --show-prefix/], STDERR => 0)
+} || '';
+
+my $git_dir_user_set = 1 if defined $ENV{GIT_DIR};
+$ENV{GIT_DIR} ||= '.git';
+$Git::SVN::default_repo_id = 'svn';
+$Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn';
+$Git::SVN::Ra::_log_window_size = 100;
+
+$Git::SVN::Log::TZ = $ENV{TZ};
+$ENV{TZ} = 'UTC';
+$| = 1; # unbuffer STDOUT
+
+sub fatal (@) { print STDERR "@_\n"; exit 1 }
+require SVN::Core; # use()-ing this causes segfaults for me... *shrug*
+require SVN::Ra;
+require SVN::Delta;
+if ($SVN::Core::VERSION lt '1.1.0') {
+       fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)";
+}
+push @Git::SVN::Ra::ISA, 'SVN::Ra';
+push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
+push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor';
+use Carp qw/croak/;
+use Digest::MD5;
+use IO::File qw//;
+use File::Basename qw/dirname basename/;
+use File::Path qw/mkpath/;
+use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
+use IPC::Open3;
+use Git;
+
+BEGIN {
+       # import functions from Git into our packages, en masse
+       no strict 'refs';
+       foreach (qw/command command_oneline command_noisy command_output_pipe
+                   command_input_pipe command_close_pipe/) {
+               for my $package ( qw(SVN::Git::Editor SVN::Git::Fetcher
+                       Git::SVN::Migration Git::SVN::Log Git::SVN),
+                       __PACKAGE__) {
+                       *{"${package}::$_"} = \&{"Git::$_"};
+               }
+       }
+}
+
+my ($SVN);
+
+$sha1 = qr/[a-f\d]{40}/;
+$sha1_short = qr/[a-f\d]{4,40}/;
+my ($_stdin, $_help, $_edit,
+       $_message, $_file,
+       $_template, $_shared,
+       $_version, $_fetch_all, $_no_rebase,
+       $_merge, $_strategy, $_dry_run, $_local,
+       $_prefix, $_no_checkout, $_url, $_verbose);
+$Git::SVN::_follow_parent = 1;
+my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
+                    'config-dir=s' => \$Git::SVN::Ra::config_dir,
+                    'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache );
+my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent,
+               'authors-file|A=s' => \$_authors,
+               'repack:i' => \$Git::SVN::_repack,
+               'noMetadata' => \$Git::SVN::_no_metadata,
+               'useSvmProps' => \$Git::SVN::_use_svm_props,
+               'useSvnsyncProps' => \$Git::SVN::_use_svnsync_props,
+               'log-window-size=i' => \$Git::SVN::Ra::_log_window_size,
+               'no-checkout' => \$_no_checkout,
+               'quiet|q' => \$_q,
+               'repack-flags|repack-args|repack-opts=s' =>
+                  \$Git::SVN::_repack_flags,
+               'use-log-author' => \$Git::SVN::_use_log_author,
+               %remote_opts );
+
+my ($_trunk, $_tags, $_branches, $_stdlayout);
+my %icv;
+my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
+                  'trunk|T=s' => \$_trunk, 'tags|t=s' => \$_tags,
+                  'branches|b=s' => \$_branches, 'prefix=s' => \$_prefix,
+                  'stdlayout|s' => \$_stdlayout,
+                  'minimize-url|m' => \$Git::SVN::_minimize_url,
+                 'no-metadata' => sub { $icv{noMetadata} = 1 },
+                 'use-svm-props' => sub { $icv{useSvmProps} = 1 },
+                 'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 },
+                 'rewrite-root=s' => sub { $icv{rewriteRoot} = $_[1] },
+                  %remote_opts );
+my %cmt_opts = ( 'edit|e' => \$_edit,
+               'rmdir' => \$SVN::Git::Editor::_rmdir,
+               'find-copies-harder' => \$SVN::Git::Editor::_find_copies_harder,
+               'l=i' => \$SVN::Git::Editor::_rename_limit,
+               'copy-similarity|C=i'=> \$SVN::Git::Editor::_cp_similarity
+);
+
+my %cmd = (
+       fetch => [ \&cmd_fetch, "Download new revisions from SVN",
+                       { 'revision|r=s' => \$_revision,
+                         'fetch-all|all' => \$_fetch_all,
+                          %fc_opts } ],
+       clone => [ \&cmd_clone, "Initialize and fetch revisions",
+                       { 'revision|r=s' => \$_revision,
+                          %fc_opts, %init_opts } ],
+       init => [ \&cmd_init, "Initialize a repo for tracking" .
+                         " (requires URL argument)",
+                         \%init_opts ],
+       'multi-init' => [ \&cmd_multi_init,
+                         "Deprecated alias for ".
+                         "'$0 init -T<trunk> -b<branches> -t<tags>'",
+                         \%init_opts ],
+       dcommit => [ \&cmd_dcommit,
+                    'Commit several diffs to merge with upstream',
+                       { 'merge|m|M' => \$_merge,
+                         'strategy|s=s' => \$_strategy,
+                         'verbose|v' => \$_verbose,
+                         'dry-run|n' => \$_dry_run,
+                         'fetch-all|all' => \$_fetch_all,
+                         'no-rebase' => \$_no_rebase,
+                       %cmt_opts, %fc_opts } ],
+       'set-tree' => [ \&cmd_set_tree,
+                       "Set an SVN repository to a git tree-ish",
+                       { 'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ],
+       'create-ignore' => [ \&cmd_create_ignore,
+                            'Create a .gitignore per svn:ignore',
+                            { 'revision|r=i' => \$_revision
+                            } ],
+        'propget' => [ \&cmd_propget,
+                      'Print the value of a property on a file or directory',
+                      { 'revision|r=i' => \$_revision } ],
+        'proplist' => [ \&cmd_proplist,
+                      'List all properties of a file or directory',
+                      { 'revision|r=i' => \$_revision } ],
+       'show-ignore' => [ \&cmd_show_ignore, "Show svn:ignore listings",
+                       { 'revision|r=i' => \$_revision
+                       } ],
+       'show-externals' => [ \&cmd_show_externals, "Show svn:externals listings",
+                       { 'revision|r=i' => \$_revision
+                       } ],
+       'multi-fetch' => [ \&cmd_multi_fetch,
+                          "Deprecated alias for $0 fetch --all",
+                          { 'revision|r=s' => \$_revision, %fc_opts } ],
+       'migrate' => [ sub { },
+                      # no-op, we automatically run this anyways,
+                      'Migrate configuration/metadata/layout from
+                       previous versions of git-svn',
+                       { 'minimize' => \$Git::SVN::Migration::_minimize,
+                        %remote_opts } ],
+       'log' => [ \&Git::SVN::Log::cmd_show_log, 'Show commit logs',
+                       { 'limit=i' => \$Git::SVN::Log::limit,
+                         'revision|r=s' => \$_revision,
+                         'verbose|v' => \$Git::SVN::Log::verbose,
+                         'incremental' => \$Git::SVN::Log::incremental,
+                         'oneline' => \$Git::SVN::Log::oneline,
+                         'show-commit' => \$Git::SVN::Log::show_commit,
+                         'non-recursive' => \$Git::SVN::Log::non_recursive,
+                         'authors-file|A=s' => \$_authors,
+                         'color' => \$Git::SVN::Log::color,
+                         'pager=s' => \$Git::SVN::Log::pager
+                       } ],
+       'find-rev' => [ \&cmd_find_rev, "Translate between SVN revision numbers and tree-ish",
+                       {} ],
+       'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
+                       { 'merge|m|M' => \$_merge,
+                         'verbose|v' => \$_verbose,
+                         'strategy|s=s' => \$_strategy,
+                         'local|l' => \$_local,
+                         'fetch-all|all' => \$_fetch_all,
+                         %fc_opts } ],
+       'commit-diff' => [ \&cmd_commit_diff,
+                          'Commit a diff between two trees',
+                       { 'message|m=s' => \$_message,
+                         'file|F=s' => \$_file,
+                         'revision|r=s' => \$_revision,
+                       %cmt_opts } ],
+       'info' => [ \&cmd_info,
+                   "Show info about the latest SVN revision
+                    on the current branch",
+                   { 'url' => \$_url, } ],
+);
+
+my $cmd;
+for (my $i = 0; $i < @ARGV; $i++) {
+       if (defined $cmd{$ARGV[$i]}) {
+               $cmd = $ARGV[$i];
+               splice @ARGV, $i, 1;
+               last;
+       }
+};
+
+# make sure we're always running
+unless ($cmd =~ /(?:clone|init|multi-init)$/) {
+       unless (-d $ENV{GIT_DIR}) {
+               if ($git_dir_user_set) {
+                       die "GIT_DIR=$ENV{GIT_DIR} explicitly set, ",
+                           "but it is not a directory\n";
+               }
+               my $git_dir = delete $ENV{GIT_DIR};
+               chomp(my $cdup = command_oneline(qw/rev-parse --show-cdup/));
+               unless (length $cdup) {
+                       die "Already at toplevel, but $git_dir ",
+                           "not found '$cdup'\n";
+               }
+               chdir $cdup or die "Unable to chdir up to '$cdup'\n";
+               unless (-d $git_dir) {
+                       die "$git_dir still not found after going to ",
+                           "'$cdup'\n";
+               }
+               $ENV{GIT_DIR} = $git_dir;
+       }
+}
+
+my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
+
+read_repo_config(\%opts);
+Getopt::Long::Configure('pass_through') if ($cmd && $cmd eq 'log');
+my $rv = GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version,
+                    'minimize-connections' => \$Git::SVN::Migration::_minimize,
+                    'id|i=s' => \$Git::SVN::default_ref_id,
+                    'svn-remote|remote|R=s' => sub {
+                       $Git::SVN::no_reuse_existing = 1;
+                       $Git::SVN::default_repo_id = $_[1] });
+exit 1 if (!$rv && $cmd && $cmd ne 'log');
+
+usage(0) if $_help;
+version() if $_version;
+usage(1) unless defined $cmd;
+load_authors() if $_authors;
+
+unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) {
+       Git::SVN::Migration::migration_check();
+}
+Git::SVN::init_vars();
+eval {
+       Git::SVN::verify_remotes_sanity();
+       $cmd{$cmd}->[0]->(@ARGV);
+};
+fatal $@ if $@;
+post_fetch_checkout();
+exit 0;
+
+####################### primary functions ######################
+sub usage {
+       my $exit = shift || 0;
+       my $fd = $exit ? \*STDERR : \*STDOUT;
+       print $fd <<"";
+git-svn - bidirectional operations between a single Subversion tree and git
+Usage: $0 <command> [options] [arguments]\n
+
+       print $fd "Available commands:\n" unless $cmd;
+
+       foreach (sort keys %cmd) {
+               next if $cmd && $cmd ne $_;
+               next if /^multi-/; # don't show deprecated commands
+               print $fd '  ',pack('A17',$_),$cmd{$_}->[1],"\n";
+               foreach (sort keys %{$cmd{$_}->[2]}) {
+                       # mixed-case options are for .git/config only
+                       next if /[A-Z]/ && /^[a-z]+$/i;
+                       # prints out arguments as they should be passed:
+                       my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
+                       print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
+                                                       "--$_" : "-$_" }
+                                               split /\|/,$_)," $x\n";
+               }
+       }
+       print $fd <<"";
+\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
+arbitrary identifier if you're tracking multiple SVN branches/repositories in
+one git repository and want to keep them separate.  See git-svn(1) for more
+information.
+
+       exit $exit;
+}
+
+sub version {
+       print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n";
+       exit 0;
+}
+
+sub do_git_init_db {
+       unless (-d $ENV{GIT_DIR}) {
+               my @init_db = ('init');
+               push @init_db, "--template=$_template" if defined $_template;
+               if (defined $_shared) {
+                       if ($_shared =~ /[a-z]/) {
+                               push @init_db, "--shared=$_shared";
+                       } else {
+                               push @init_db, "--shared";
+                       }
+               }
+               command_noisy(@init_db);
+       }
+       my $set;
+       my $pfx = "svn-remote.$Git::SVN::default_repo_id";
+       foreach my $i (keys %icv) {
+               die "'$set' and '$i' cannot both be set\n" if $set;
+               next unless defined $icv{$i};
+               command_noisy('config', "$pfx.$i", $icv{$i});
+               $set = $i;
+       }
+}
+
+sub init_subdir {
+       my $repo_path = shift or return;
+       mkpath([$repo_path]) unless -d $repo_path;
+       chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n";
+       $ENV{GIT_DIR} = '.git';
+}
+
+sub cmd_clone {
+       my ($url, $path) = @_;
+       if (!defined $path &&
+           (defined $_trunk || defined $_branches || defined $_tags ||
+            defined $_stdlayout) &&
+           $url !~ m#^[a-z\+]+://#) {
+               $path = $url;
+       }
+       $path = basename($url) if !defined $path || !length $path;
+       cmd_init($url, $path);
+       Git::SVN::fetch_all($Git::SVN::default_repo_id);
+}
+
+sub cmd_init {
+       if (defined $_stdlayout) {
+               $_trunk = 'trunk' if (!defined $_trunk);
+               $_tags = 'tags' if (!defined $_tags);
+               $_branches = 'branches' if (!defined $_branches);
+       }
+       if (defined $_trunk || defined $_branches || defined $_tags) {
+               return cmd_multi_init(@_);
+       }
+       my $url = shift or die "SVN repository location required ",
+                              "as a command-line argument\n";
+       init_subdir(@_);
+       do_git_init_db();
+
+       Git::SVN->init($url);
+}
+
+sub cmd_fetch {
+       if (grep /^\d+=./, @_) {
+               die "'<rev>=<commit>' fetch arguments are ",
+                   "no longer supported.\n";
+       }
+       my ($remote) = @_;
+       if (@_ > 1) {
+               die "Usage: $0 fetch [--all] [svn-remote]\n";
+       }
+       $remote ||= $Git::SVN::default_repo_id;
+       if ($_fetch_all) {
+               cmd_multi_fetch();
+       } else {
+               Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes());
+       }
+}
+
+sub cmd_set_tree {
+       my (@commits) = @_;
+       if ($_stdin || !@commits) {
+               print "Reading from stdin...\n";
+               @commits = ();
+               while (<STDIN>) {
+                       if (/\b($sha1_short)\b/o) {
+                               unshift @commits, $1;
+                       }
+               }
+       }
+       my @revs;
+       foreach my $c (@commits) {
+               my @tmp = command('rev-parse',$c);
+               if (scalar @tmp == 1) {
+                       push @revs, $tmp[0];
+               } elsif (scalar @tmp > 1) {
+                       push @revs, reverse(command('rev-list',@tmp));
+               } else {
+                       fatal "Failed to rev-parse $c";
+               }
+       }
+       my $gs = Git::SVN->new;
+       my ($r_last, $cmt_last) = $gs->last_rev_commit;
+       $gs->fetch;
+       if (defined $gs->{last_rev} && $r_last != $gs->{last_rev}) {
+               fatal "There are new revisions that were fetched ",
+                     "and need to be merged (or acknowledged) ",
+                     "before committing.\nlast rev: $r_last\n",
+                     " current: $gs->{last_rev}";
+       }
+       $gs->set_tree($_) foreach @revs;
+       print "Done committing ",scalar @revs," revisions to SVN\n";
+}
+
+sub cmd_dcommit {
+       my $head = shift;
+       git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) }
+               'Cannot dcommit with a dirty index.  Commit your changes first, '
+               . "or stash them with `git stash'.\n";
+       $head ||= 'HEAD';
+       my @refs;
+       my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs);
+       print "Committing to $url ...\n";
+       unless ($gs) {
+               die "Unable to determine upstream SVN information from ",
+                   "$head history\n";
+       }
+       my $last_rev;
+       my ($linear_refs, $parents) = linearize_history($gs, \@refs);
+       if ($_no_rebase && scalar(@$linear_refs) > 1) {
+               warn "Attempting to commit more than one change while ",
+                    "--no-rebase is enabled.\n",
+                    "If these changes depend on each other, re-running ",
+                    "without --no-rebase will be required."
+       }
+       while (1) {
+               my $d = shift @$linear_refs or last;
+               unless (defined $last_rev) {
+                       (undef, $last_rev, undef) = cmt_metadata("$d~1");
+                       unless (defined $last_rev) {
+                               fatal "Unable to extract revision information ",
+                                     "from commit $d~1";
+                       }
+               }
+               if ($_dry_run) {
+                       print "diff-tree $d~1 $d\n";
+               } else {
+                       my $cmt_rev;
+                       my %ed_opts = ( r => $last_rev,
+                                       log => get_commit_entry($d)->{log},
+                                       ra => Git::SVN::Ra->new($gs->full_url),
+                                       config => SVN::Core::config_get_config(
+                                               $Git::SVN::Ra::config_dir
+                                       ),
+                                       tree_a => "$d~1",
+                                       tree_b => $d,
+                                       editor_cb => sub {
+                                              print "Committed r$_[0]\n";
+                                              $cmt_rev = $_[0];
+                                       },
+                                       svn_path => '');
+                       if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
+                               print "No changes\n$d~1 == $d\n";
+                       } elsif ($parents->{$d} && @{$parents->{$d}}) {
+                               $gs->{inject_parents_dcommit}->{$cmt_rev} =
+                                                              $parents->{$d};
+                       }
+                       $_fetch_all ? $gs->fetch_all : $gs->fetch;
+                       next if $_no_rebase;
+
+                       # we always want to rebase against the current HEAD,
+                       # not any head that was passed to us
+                       my @diff = command('diff-tree', $d,
+                                          $gs->refname, '--');
+                       my @finish;
+                       if (@diff) {
+                               @finish = rebase_cmd();
+                               print STDERR "W: $d and ", $gs->refname,
+                                            " differ, using @finish:\n",
+                                            join("\n", @diff), "\n";
+                       } else {
+                               print "No changes between current HEAD and ",
+                                     $gs->refname,
+                                     "\nResetting to the latest ",
+                                     $gs->refname, "\n";
+                               @finish = qw/reset --mixed/;
+                       }
+                       command_noisy(@finish, $gs->refname);
+                       if (@diff) {
+                               @refs = ();
+                               my ($url_, $rev_, $uuid_, $gs_) =
+                                             working_head_info($head, \@refs);
+                               my ($linear_refs_, $parents_) =
+                                             linearize_history($gs_, \@refs);
+                               if (scalar(@$linear_refs) !=
+                                   scalar(@$linear_refs_)) {
+                                       fatal "# of revisions changed ",
+                                         "\nbefore:\n",
+                                         join("\n", @$linear_refs),
+                                         "\n\nafter:\n",
+                                         join("\n", @$linear_refs_), "\n",
+                                         'If you are attempting to commit ',
+                                         "merges, try running:\n\t",
+                                         'git rebase --interactive',
+                                         '--preserve-merges ',
+                                         $gs->refname,
+                                         "\nBefore dcommitting";
+                               }
+                               if ($url_ ne $url) {
+                                       fatal "URL mismatch after rebase: ",
+                                             "$url_ != $url";
+                               }
+                               if ($uuid_ ne $uuid) {
+                                       fatal "uuid mismatch after rebase: ",
+                                             "$uuid_ != $uuid";
+                               }
+                               # remap parents
+                               my (%p, @l, $i);
+                               for ($i = 0; $i < scalar @$linear_refs; $i++) {
+                                       my $new = $linear_refs_->[$i] or next;
+                                       $p{$new} =
+                                               $parents->{$linear_refs->[$i]};
+                                       push @l, $new;
+                               }
+                               $parents = \%p;
+                               $linear_refs = \@l;
+                       }
+                       $last_rev = $cmt_rev;
+               }
+       }
+}
+
+sub cmd_find_rev {
+       my $revision_or_hash = shift;
+       my $result;
+       if ($revision_or_hash =~ /^r\d+$/) {
+               my $head = shift;
+               $head ||= 'HEAD';
+               my @refs;
+               my (undef, undef, undef, $gs) = working_head_info($head, \@refs);
+               unless ($gs) {
+                       die "Unable to determine upstream SVN information from ",
+                           "$head history\n";
+               }
+               my $desired_revision = substr($revision_or_hash, 1);
+               $result = $gs->rev_db_get($desired_revision);
+       } else {
+               my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
+               $result = $rev;
+       }
+       print "$result\n" if $result;
+}
+
+sub cmd_rebase {
+       command_noisy(qw/update-index --refresh/);
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       unless ($gs) {
+               die "Unable to determine upstream SVN information from ",
+                   "working tree history\n";
+       }
+       if (command(qw/diff-index HEAD --/)) {
+               print STDERR "Cannot rebase with uncommited changes:\n";
+               command_noisy('status');
+               exit 1;
+       }
+       unless ($_local) {
+               # rebase will checkout for us, so no need to do it explicitly
+               $_no_checkout = 'true';
+               $_fetch_all ? $gs->fetch_all : $gs->fetch;
+       }
+       command_noisy(rebase_cmd(), $gs->refname);
+}
+
+sub cmd_show_ignore {
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       $gs ||= Git::SVN->new;
+       my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
+       $gs->prop_walk($gs->{path}, $r, sub {
+               my ($gs, $path, $props) = @_;
+               print STDOUT "\n# $path\n";
+               my $s = $props->{'svn:ignore'} or return;
+               $s =~ s/[\r\n]+/\n/g;
+               chomp $s;
+               $s =~ s#^#$path#gm;
+               print STDOUT "$s\n";
+       });
+}
+
+sub cmd_show_externals {
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       $gs ||= Git::SVN->new;
+       my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
+       $gs->prop_walk($gs->{path}, $r, sub {
+               my ($gs, $path, $props) = @_;
+               print STDOUT "\n# $path\n";
+               my $s = $props->{'svn:externals'} or return;
+               $s =~ s/[\r\n]+/\n/g;
+               chomp $s;
+               $s =~ s#^#$path#gm;
+               print STDOUT "$s\n";
+       });
+}
+
+sub cmd_create_ignore {
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       $gs ||= Git::SVN->new;
+       my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
+       $gs->prop_walk($gs->{path}, $r, sub {
+               my ($gs, $path, $props) = @_;
+               # $path is of the form /path/to/dir/
+               my $ignore = '.' . $path . '.gitignore';
+               my $s = $props->{'svn:ignore'} or return;
+               open(GITIGNORE, '>', $ignore)
+                 or fatal("Failed to open `$ignore' for writing: $!");
+               $s =~ s/[\r\n]+/\n/g;
+               chomp $s;
+               # Prefix all patterns so that the ignore doesn't apply
+               # to sub-directories.
+               $s =~ s#^#/#gm;
+               print GITIGNORE "$s\n";
+               close(GITIGNORE)
+                 or fatal("Failed to close `$ignore': $!");
+               command_noisy('add', $ignore);
+       });
+}
+
+sub canonicalize_path {
+       my ($path) = @_;
+       my $dot_slash_added = 0;
+       if (substr($path, 0, 1) ne "/") {
+               $path = "./" . $path;
+               $dot_slash_added = 1;
+       }
+       # File::Spec->canonpath doesn't collapse x/../y into y (for a
+       # good reason), so let's do this manually.
+       $path =~ s#/+#/#g;
+       $path =~ s#/\.(?:/|$)#/#g;
+       $path =~ s#/[^/]+/\.\.##g;
+       $path =~ s#/$##g;
+       $path =~ s#^\./## if $dot_slash_added;
+       return $path;
+}
+
+# get_svnprops(PATH)
+# ------------------
+# Helper for cmd_propget and cmd_proplist below.
+sub get_svnprops {
+       my $path = shift;
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       $gs ||= Git::SVN->new;
+
+       # prefix THE PATH by the sub-directory from which the user
+       # invoked us.
+       $path = $cmd_dir_prefix . $path;
+       fatal("No such file or directory: $path") unless -e $path;
+       my $is_dir = -d $path ? 1 : 0;
+       $path = $gs->{path} . '/' . $path;
+
+       # canonicalize the path (otherwise libsvn will abort or fail to
+       # find the file)
+       $path = canonicalize_path($path);
+
+       my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
+       my $props;
+       if ($is_dir) {
+               (undef, undef, $props) = $gs->ra->get_dir($path, $r);
+       }
+       else {
+               (undef, $props) = $gs->ra->get_file($path, $r, undef);
+       }
+       return $props;
+}
+
+# cmd_propget (PROP, PATH)
+# ------------------------
+# Print the SVN property PROP for PATH.
+sub cmd_propget {
+       my ($prop, $path) = @_;
+       $path = '.' if not defined $path;
+       usage(1) if not defined $prop;
+       my $props = get_svnprops($path);
+       if (not defined $props->{$prop}) {
+               fatal("`$path' does not have a `$prop' SVN property.");
+       }
+       print $props->{$prop} . "\n";
+}
+
+# cmd_proplist (PATH)
+# -------------------
+# Print the list of SVN properties for PATH.
+sub cmd_proplist {
+       my $path = shift;
+       $path = '.' if not defined $path;
+       my $props = get_svnprops($path);
+       print "Properties on '$path':\n";
+       foreach (sort keys %{$props}) {
+               print "  $_\n";
+       }
+}
+
+sub cmd_multi_init {
+       my $url = shift;
+       unless (defined $_trunk || defined $_branches || defined $_tags) {
+               usage(1);
+       }
+
+       # there are currently some bugs that prevent multi-init/multi-fetch
+       # setups from working well without this.
+       $Git::SVN::_minimize_url = 1;
+
+       $_prefix = '' unless defined $_prefix;
+       if (defined $url) {
+               $url =~ s#/+$##;
+               init_subdir(@_);
+       }
+       do_git_init_db();
+       if (defined $_trunk) {
+               my $trunk_ref = $_prefix . 'trunk';
+               # try both old-style and new-style lookups:
+               my $gs_trunk = eval { Git::SVN->new($trunk_ref) };
+               unless ($gs_trunk) {
+                       my ($trunk_url, $trunk_path) =
+                                             complete_svn_url($url, $_trunk);
+                       $gs_trunk = Git::SVN->init($trunk_url, $trunk_path,
+                                                  undef, $trunk_ref);
+               }
+       }
+       return unless defined $_branches || defined $_tags;
+       my $ra = $url ? Git::SVN::Ra->new($url) : undef;
+       complete_url_ls_init($ra, $_branches, '--branches/-b', $_prefix);
+       complete_url_ls_init($ra, $_tags, '--tags/-t', $_prefix . 'tags/');
+}
+
+sub cmd_multi_fetch {
+       my $remotes = Git::SVN::read_all_remotes();
+       foreach my $repo_id (sort keys %$remotes) {
+               if ($remotes->{$repo_id}->{url}) {
+                       Git::SVN::fetch_all($repo_id, $remotes);
+               }
+       }
+}
+
+# this command is special because it requires no metadata
+sub cmd_commit_diff {
+       my ($ta, $tb, $url) = @_;
+       my $usage = "Usage: $0 commit-diff -r<revision> ".
+                   "<tree-ish> <tree-ish> [<URL>]";
+       fatal($usage) if (!defined $ta || !defined $tb);
+       my $svn_path;
+       if (!defined $url) {
+               my $gs = eval { Git::SVN->new };
+               if (!$gs) {
+                       fatal("Needed URL or usable git-svn --id in ",
+                             "the command-line\n", $usage);
+               }
+               $url = $gs->{url};
+               $svn_path = $gs->{path};
+       }
+       unless (defined $_revision) {
+               fatal("-r|--revision is a required argument\n", $usage);
+       }
+       if (defined $_message && defined $_file) {
+               fatal("Both --message/-m and --file/-F specified ",
+                     "for the commit message.\n",
+                     "I have no idea what you mean");
+       }
+       if (defined $_file) {
+               $_message = file_to_s($_file);
+       } else {
+               $_message ||= get_commit_entry($tb)->{log};
+       }
+       my $ra ||= Git::SVN::Ra->new($url);
+       $svn_path ||= $ra->{svn_path};
+       my $r = $_revision;
+       if ($r eq 'HEAD') {
+               $r = $ra->get_latest_revnum;
+       } elsif ($r !~ /^\d+$/) {
+               die "revision argument: $r not understood by git-svn\n";
+       }
+       my %ed_opts = ( r => $r,
+                       log => $_message,
+                       ra => $ra,
+                       tree_a => $ta,
+                       tree_b => $tb,
+                       editor_cb => sub { print "Committed r$_[0]\n" },
+                       svn_path => $svn_path );
+       if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
+               print "No changes\n$ta == $tb\n";
+       }
+}
+
+sub cmd_info {
+       my $path = canonicalize_path(shift or ".");
+       unless (scalar(@_) == 0) {
+               die "Too many arguments specified\n";
+       }
+
+       my ($file_type, $diff_status) = find_file_type_and_diff_status($path);
+
+       if (!$file_type && !$diff_status) {
+               print STDERR "$path:  (Not a versioned resource)\n\n";
+               return;
+       }
+
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       unless ($gs) {
+               die "Unable to determine upstream SVN information from ",
+                   "working tree history\n";
+       }
+       my $full_url = $url . ($path eq "." ? "" : "/$path");
+
+       if ($_url) {
+               print $full_url, "\n";
+               return;
+       }
+
+       my $result = "Path: $path\n";
+       $result .= "Name: " . basename($path) . "\n" if $file_type ne "dir";
+       $result .= "URL: " . $full_url . "\n";
+
+       eval {
+               my $repos_root = $gs->repos_root;
+               Git::SVN::remove_username($repos_root);
+               $result .= "Repository Root: $repos_root\n";
+       };
+       if ($@) {
+               $result .= "Repository Root: (offline)\n";
+       }
+       $result .= "Repository UUID: $uuid\n" unless $diff_status eq "A";
+       $result .= "Revision: " . ($diff_status eq "A" ? 0 : $rev) . "\n";
+
+       $result .= "Node Kind: " .
+                  ($file_type eq "dir" ? "directory" : "file") . "\n";
+
+       my $schedule = $diff_status eq "A"
+                      ? "add"
+                      : ($diff_status eq "D" ? "delete" : "normal");
+       $result .= "Schedule: $schedule\n";
+
+       if ($diff_status eq "A") {
+               print $result, "\n";
+               return;
+       }
+
+       my ($lc_author, $lc_rev, $lc_date_utc);
+       my @args = Git::SVN::Log::git_svn_log_cmd($rev, $rev, "--", $path);
+       my $log = command_output_pipe(@args);
+       my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
+       while (<$log>) {
+               if (/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {
+                       $lc_author = $1;
+                       $lc_date_utc = Git::SVN::Log::parse_git_date($2, $3);
+               } elsif (/^${esc_color}    (git-svn-id:.+)$/o) {
+                       (undef, $lc_rev, undef) = ::extract_metadata($1);
+               }
+       }
+       close $log;
+
+       Git::SVN::Log::set_local_timezone();
+
+       $result .= "Last Changed Author: $lc_author\n";
+       $result .= "Last Changed Rev: $lc_rev\n";
+       $result .= "Last Changed Date: " .
+                  Git::SVN::Log::format_svn_date($lc_date_utc) . "\n";
+
+       if ($file_type ne "dir") {
+               my $text_last_updated_date =
+                   ($diff_status eq "D" ? $lc_date_utc : (stat $path)[9]);
+               $result .=
+                   "Text Last Updated: " .
+                   Git::SVN::Log::format_svn_date($text_last_updated_date) .
+                   "\n";
+               my $checksum;
+               if ($diff_status eq "D") {
+                       my ($fh, $ctx) =
+                           command_output_pipe(qw(cat-file blob), "HEAD:$path");
+                       if ($file_type eq "link") {
+                               my $file_name = <$fh>;
+                               $checksum = md5sum("link $file_name");
+                       } else {
+                               $checksum = md5sum($fh);
+                       }
+                       command_close_pipe($fh, $ctx);
+               } elsif ($file_type eq "link") {
+                       my $file_name =
+                           command(qw(cat-file blob), "HEAD:$path");
+                       $checksum =
+                           md5sum("link " . $file_name);
+               } else {
+                       open FILE, "<", $path or die $!;
+                       $checksum = md5sum(\*FILE);
+                       close FILE or die $!;
+               }
+               $result .= "Checksum: " . $checksum . "\n";
+       }
+
+       print $result, "\n";
+}
+
+########################### utility functions #########################
+
+sub rebase_cmd {
+       my @cmd = qw/rebase/;
+       push @cmd, '-v' if $_verbose;
+       push @cmd, qw/--merge/ if $_merge;
+       push @cmd, "--strategy=$_strategy" if $_strategy;
+       @cmd;
+}
+
+sub post_fetch_checkout {
+       return if $_no_checkout;
+       my $gs = $Git::SVN::_head or return;
+       return if verify_ref('refs/heads/master^0');
+
+       my $valid_head = verify_ref('HEAD^0');
+       command_noisy(qw(update-ref refs/heads/master), $gs->refname);
+       return if ($valid_head || !verify_ref('HEAD^0'));
+
+       return if $ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;
+       my $index = $ENV{GIT_INDEX_FILE} || "$ENV{GIT_DIR}/index";
+       return if -f $index;
+
+       return if command_oneline(qw/rev-parse --is-inside-work-tree/) eq 'false';
+       return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true';
+       command_noisy(qw/read-tree -m -u -v HEAD HEAD/);
+       print STDERR "Checked out HEAD:\n  ",
+                    $gs->full_url, " r", $gs->last_rev, "\n";
+}
+
+sub complete_svn_url {
+       my ($url, $path) = @_;
+       $path =~ s#/+$##;
+       if ($path !~ m#^[a-z\+]+://#) {
+               if (!defined $url || $url !~ m#^[a-z\+]+://#) {
+                       fatal("E: '$path' is not a complete URL ",
+                             "and a separate URL is not specified");
+               }
+               return ($url, $path);
+       }
+       return ($path, '');
+}
+
+sub complete_url_ls_init {
+       my ($ra, $repo_path, $switch, $pfx) = @_;
+       unless ($repo_path) {
+               print STDERR "W: $switch not specified\n";
+               return;
+       }
+       $repo_path =~ s#/+$##;
+       if ($repo_path =~ m#^[a-z\+]+://#) {
+               $ra = Git::SVN::Ra->new($repo_path);
+               $repo_path = '';
+       } else {
+               $repo_path =~ s#^/+##;
+               unless ($ra) {
+                       fatal("E: '$repo_path' is not a complete URL ",
+                             "and a separate URL is not specified");
+               }
+       }
+       my $url = $ra->{url};
+       my $gs = Git::SVN->init($url, undef, undef, undef, 1);
+       my $k = "svn-remote.$gs->{repo_id}.url";
+       my $orig_url = eval { command_oneline(qw/config --get/, $k) };
+       if ($orig_url && ($orig_url ne $gs->{url})) {
+               die "$k already set: $orig_url\n",
+                   "wanted to set to: $gs->{url}\n";
+       }
+       command_oneline('config', $k, $gs->{url}) unless $orig_url;
+       my $remote_path = "$ra->{svn_path}/$repo_path/*";
+       $remote_path =~ s#/+#/#g;
+       $remote_path =~ s#^/##g;
+       my ($n) = ($switch =~ /^--(\w+)/);
+       if (length $pfx && $pfx !~ m#/$#) {
+               die "--prefix='$pfx' must have a trailing slash '/'\n";
+       }
+       command_noisy('config', "svn-remote.$gs->{repo_id}.$n",
+                               "$remote_path:refs/remotes/$pfx*");
+}
+
+sub verify_ref {
+       my ($ref) = @_;
+       eval { command_oneline([ 'rev-parse', '--verify', $ref ],
+                              { STDERR => 0 }); };
+}
+
+sub get_tree_from_treeish {
+       my ($treeish) = @_;
+       # $treeish can be a symbolic ref, too:
+       my $type = command_oneline(qw/cat-file -t/, $treeish);
+       my $expected;
+       while ($type eq 'tag') {
+               ($treeish, $type) = command(qw/cat-file tag/, $treeish);
+       }
+       if ($type eq 'commit') {
+               $expected = (grep /^tree /, command(qw/cat-file commit/,
+                                                   $treeish))[0];
+               ($expected) = ($expected =~ /^tree ($sha1)$/o);
+               die "Unable to get tree from $treeish\n" unless $expected;
+       } elsif ($type eq 'tree') {
+               $expected = $treeish;
+       } else {
+               die "$treeish is a $type, expected tree, tag or commit\n";
+       }
+       return $expected;
+}
+
+sub get_commit_entry {
+       my ($treeish) = shift;
+       my %log_entry = ( log => '', tree => get_tree_from_treeish($treeish) );
+       my $commit_editmsg = "$ENV{GIT_DIR}/COMMIT_EDITMSG";
+       my $commit_msg = "$ENV{GIT_DIR}/COMMIT_MSG";
+       open my $log_fh, '>', $commit_editmsg or croak $!;
+
+       my $type = command_oneline(qw/cat-file -t/, $treeish);
+       if ($type eq 'commit' || $type eq 'tag') {
+               my ($msg_fh, $ctx) = command_output_pipe('cat-file',
+                                                        $type, $treeish);
+               my $in_msg = 0;
+               while (<$msg_fh>) {
+                       if (!$in_msg) {
+                               $in_msg = 1 if (/^\s*$/);
+                       } elsif (/^git-svn-id: /) {
+                               # skip this for now, we regenerate the
+                               # correct one on re-fetch anyways
+                               # TODO: set *:merge properties or like...
+                       } else {
+                               print $log_fh $_ or croak $!;
+                       }
+               }
+               command_close_pipe($msg_fh, $ctx);
+       }
+       close $log_fh or croak $!;
+
+       if ($_edit || ($type eq 'tree')) {
+               my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
+               # TODO: strip out spaces, comments, like git-commit.sh
+               system($editor, $commit_editmsg);
+       }
+       rename $commit_editmsg, $commit_msg or croak $!;
+       open $log_fh, '<', $commit_msg or croak $!;
+       { local $/; chomp($log_entry{log} = <$log_fh>); }
+       close $log_fh or croak $!;
+       unlink $commit_msg;
+       \%log_entry;
+}
+
+sub s_to_file {
+       my ($str, $file, $mode) = @_;
+       open my $fd,'>',$file or croak $!;
+       print $fd $str,"\n" or croak $!;
+       close $fd or croak $!;
+       chmod ($mode &~ umask, $file) if (defined $mode);
+}
+
+sub file_to_s {
+       my $file = shift;
+       open my $fd,'<',$file or croak "$!: file: $file\n";
+       local $/;
+       my $ret = <$fd>;
+       close $fd or croak $!;
+       $ret =~ s/\s*$//s;
+       return $ret;
+}
+
+# '<svn username> = real-name <email address>' mapping based on git-svnimport:
+sub load_authors {
+       open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
+       my $log = $cmd eq 'log';
+       while (<$authors>) {
+               chomp;
+               next unless /^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
+               my ($user, $name, $email) = ($1, $2, $3);
+               if ($log) {
+                       $Git::SVN::Log::rusers{"$name <$email>"} = $user;
+               } else {
+                       $users{$user} = [$name, $email];
+               }
+       }
+       close $authors or croak $!;
+}
+
+# convert GetOpt::Long specs for use by git-config
+sub read_repo_config {
+       return unless -d $ENV{GIT_DIR};
+       my $opts = shift;
+       my @config_only;
+       foreach my $o (keys %$opts) {
+               # if we have mixedCase and a long option-only, then
+               # it's a config-only variable that we don't need for
+               # the command-line.
+               push @config_only, $o if ($o =~ /[A-Z]/ && $o =~ /^[a-z]+$/i);
+               my $v = $opts->{$o};
+               my ($key) = ($o =~ /^([a-zA-Z\-]+)/);
+               $key =~ s/-//g;
+               my $arg = 'git-config';
+               $arg .= ' --int' if ($o =~ /[:=]i$/);
+               $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
+               if (ref $v eq 'ARRAY') {
+                       chomp(my @tmp = `$arg --get-all svn.$key`);
+                       @$v = @tmp if @tmp;
+               } else {
+                       chomp(my $tmp = `$arg --get svn.$key`);
+                       if ($tmp && !($arg =~ / --bool/ && $tmp eq 'false')) {
+                               $$v = $tmp;
+                       }
+               }
+       }
+       delete @$opts{@config_only} if @config_only;
+}
+
+sub extract_metadata {
+       my $id = shift or return (undef, undef, undef);
+       my ($url, $rev, $uuid) = ($id =~ /^\s*git-svn-id:\s+(.*)\@(\d+)
+                                                       \s([a-f\d\-]+)$/x);
+       if (!defined $rev || !$uuid || !$url) {
+               # some of the original repositories I made had
+               # identifiers like this:
+               ($rev, $uuid) = ($id =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
+       }
+       return ($url, $rev, $uuid);
+}
+
+sub cmt_metadata {
+       return extract_metadata((grep(/^git-svn-id: /,
+               command(qw/cat-file commit/, shift)))[-1]);
+}
+
+sub working_head_info {
+       my ($head, $refs) = @_;
+       my @args = ('log', '--no-color', '--first-parent');
+       my ($fh, $ctx) = command_output_pipe(@args, $head);
+       my $hash;
+       my %max;
+       while (<$fh>) {
+               if ( m{^commit ($::sha1)$} ) {
+                       unshift @$refs, $hash if $hash and $refs;
+                       $hash = $1;
+                       next;
+               }
+               next unless s{^\s*(git-svn-id:)}{$1};
+               my ($url, $rev, $uuid) = extract_metadata($_);
+               if (defined $url && defined $rev) {
+                       next if $max{$url} and $max{$url} < $rev;
+                       if (my $gs = Git::SVN->find_by_url($url)) {
+                               my $c = $gs->rev_db_get($rev);
+                               if ($c && $c eq $hash) {
+                                       close $fh; # break the pipe
+                                       return ($url, $rev, $uuid, $gs);
+                               } else {
+                                       $max{$url} ||= $gs->rev_db_max;
+                               }
+                       }
+               }
+       }
+       command_close_pipe($fh, $ctx);
+       (undef, undef, undef, undef);
+}
+
+sub read_commit_parents {
+       my ($parents, $c) = @_;
+       chomp(my $p = command_oneline(qw/rev-list --parents -1/, $c));
+       $p =~ s/^($c)\s*// or die "rev-list --parents -1 $c failed!\n";
+       @{$parents->{$c}} = split(/ /, $p);
+}
+
+sub linearize_history {
+       my ($gs, $refs) = @_;
+       my %parents;
+       foreach my $c (@$refs) {
+               read_commit_parents(\%parents, $c);
+       }
+
+       my @linear_refs;
+       my %skip = ();
+       my $last_svn_commit = $gs->last_commit;
+       foreach my $c (reverse @$refs) {
+               next if $c eq $last_svn_commit;
+               last if $skip{$c};
+
+               unshift @linear_refs, $c;
+               $skip{$c} = 1;
+
+               # we only want the first parent to diff against for linear
+               # history, we save the rest to inject when we finalize the
+               # svn commit
+               my $fp_a = verify_ref("$c~1");
+               my $fp_b = shift @{$parents{$c}} if $parents{$c};
+               if (!$fp_a || !$fp_b) {
+                       die "Commit $c\n",
+                           "has no parent commit, and therefore ",
+                           "nothing to diff against.\n",
+                           "You should be working from a repository ",
+                           "originally created by git-svn\n";
+               }
+               if ($fp_a ne $fp_b) {
+                       die "$c~1 = $fp_a, however parsing commit $c ",
+                           "revealed that:\n$c~1 = $fp_b\nBUG!\n";
+               }
+
+               foreach my $p (@{$parents{$c}}) {
+                       $skip{$p} = 1;
+               }
+       }
+       (\@linear_refs, \%parents);
+}
+
+sub find_file_type_and_diff_status {
+       my ($path) = @_;
+       return ('dir', '') if $path eq '.';
+
+       my $diff_output =
+           command_oneline(qw(diff --cached --name-status --), $path) || "";
+       my $diff_status = (split(' ', $diff_output))[0] || "";
+
+       my $ls_tree = command_oneline(qw(ls-tree HEAD), $path) || "";
+
+       return (undef, undef) if !$diff_status && !$ls_tree;
+
+       if ($diff_status eq "A") {
+               return ("link", $diff_status) if -l $path;
+               return ("dir", $diff_status) if -d $path;
+               return ("file", $diff_status);
+       }
+
+       my $mode = (split(' ', $ls_tree))[0] || "";
+
+       return ("link", $diff_status) if $mode eq "120000";
+       return ("dir", $diff_status) if $mode eq "040000";
+       return ("file", $diff_status);
+}
+
+sub md5sum {
+       my $arg = shift;
+       my $ref = ref $arg;
+       my $md5 = Digest::MD5->new();
+        if ($ref eq 'GLOB' || $ref eq 'IO::File') {
+               $md5->addfile($arg) or croak $!;
+       } elsif ($ref eq 'SCALAR') {
+               $md5->add($$arg) or croak $!;
+       } elsif (!$ref) {
+               $md5->add($arg) or croak $!;
+       } else {
+               ::fatal "Can't provide MD5 hash for unknown ref type: '", $ref, "'";
+       }
+       return $md5->hexdigest();
+}
+
+package Git::SVN;
+use strict;
+use warnings;
+use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent
+            $_repack $_repack_flags $_use_svm_props $_head
+            $_use_svnsync_props $no_reuse_existing $_minimize_url
+           $_use_log_author/;
+use Carp qw/croak/;
+use File::Path qw/mkpath/;
+use File::Copy qw/copy/;
+use IPC::Open3;
+
+my $_repack_nr;
+# properties that we do not log:
+my %SKIP_PROP;
+BEGIN {
+       %SKIP_PROP = map { $_ => 1 } qw/svn:wc:ra_dav:version-url
+                                       svn:special svn:executable
+                                       svn:entry:committed-rev
+                                       svn:entry:last-author
+                                       svn:entry:uuid
+                                       svn:entry:committed-date/;
+
+       # some options are read globally, but can be overridden locally
+       # per [svn-remote "..."] section.  Command-line options will *NOT*
+       # override options set in an [svn-remote "..."] section
+       no strict 'refs';
+       for my $option (qw/follow_parent no_metadata use_svm_props
+                          use_svnsync_props/) {
+               my $key = $option;
+               $key =~ tr/_//d;
+               my $prop = "-$option";
+               *$option = sub {
+                       my ($self) = @_;
+                       return $self->{$prop} if exists $self->{$prop};
+                       my $k = "svn-remote.$self->{repo_id}.$key";
+                       eval { command_oneline(qw/config --get/, $k) };
+                       if ($@) {
+                               $self->{$prop} = ${"Git::SVN::_$option"};
+                       } else {
+                               my $v = command_oneline(qw/config --bool/,$k);
+                               $self->{$prop} = $v eq 'false' ? 0 : 1;
+                       }
+                       return $self->{$prop};
+               }
+       }
+}
+
+my %LOCKFILES;
+END { unlink keys %LOCKFILES if %LOCKFILES }
+
+sub resolve_local_globs {
+       my ($url, $fetch, $glob_spec) = @_;
+       return unless defined $glob_spec;
+       my $ref = $glob_spec->{ref};
+       my $path = $glob_spec->{path};
+       foreach (command(qw#for-each-ref --format=%(refname) refs/remotes#)) {
+               next unless m#^refs/remotes/$ref->{regex}$#;
+               my $p = $1;
+               my $pathname = desanitize_refname($path->full_path($p));
+               my $refname = desanitize_refname($ref->full_path($p));
+               if (my $existing = $fetch->{$pathname}) {
+                       if ($existing ne $refname) {
+                               die "Refspec conflict:\n",
+                                   "existing: refs/remotes/$existing\n",
+                                   " globbed: refs/remotes/$refname\n";
+                       }
+                       my $u = (::cmt_metadata("refs/remotes/$refname"))[0];
+                       $u =~ s!^\Q$url\E(/|$)!! or die
+                         "refs/remotes/$refname: '$url' not found in '$u'\n";
+                       if ($pathname ne $u) {
+                               warn "W: Refspec glob conflict ",
+                                    "(ref: refs/remotes/$refname):\n",
+                                    "expected path: $pathname\n",
+                                    "    real path: $u\n",
+                                    "Continuing ahead with $u\n";
+                               next;
+                       }
+               } else {
+                       $fetch->{$pathname} = $refname;
+               }
+       }
+}
+
+sub parse_revision_argument {
+       my ($base, $head) = @_;
+       if (!defined $::_revision || $::_revision eq 'BASE:HEAD') {
+               return ($base, $head);
+       }
+       return ($1, $2) if ($::_revision =~ /^(\d+):(\d+)$/);
+       return ($::_revision, $::_revision) if ($::_revision =~ /^\d+$/);
+       return ($head, $head) if ($::_revision eq 'HEAD');
+       return ($base, $1) if ($::_revision =~ /^BASE:(\d+)$/);
+       return ($1, $head) if ($::_revision =~ /^(\d+):HEAD$/);
+       die "revision argument: $::_revision not understood by git-svn\n";
+}
+
+sub fetch_all {
+       my ($repo_id, $remotes) = @_;
+       if (ref $repo_id) {
+               my $gs = $repo_id;
+               $repo_id = undef;
+               $repo_id = $gs->{repo_id};
+       }
+       $remotes ||= read_all_remotes();
+       my $remote = $remotes->{$repo_id} or
+                    die "[svn-remote \"$repo_id\"] unknown\n";
+       my $fetch = $remote->{fetch};
+       my $url = $remote->{url} or die "svn-remote.$repo_id.url not defined\n";
+       my (@gs, @globs);
+       my $ra = Git::SVN::Ra->new($url);
+       my $uuid = $ra->get_uuid;
+       my $head = $ra->get_latest_revnum;
+       my $base = defined $fetch ? $head : 0;
+
+       # read the max revs for wildcard expansion (branches/*, tags/*)
+       foreach my $t (qw/branches tags/) {
+               defined $remote->{$t} or next;
+               push @globs, $remote->{$t};
+               my $max_rev = eval { tmp_config(qw/--int --get/,
+                                        "svn-remote.$repo_id.${t}-maxRev") };
+               if (defined $max_rev && ($max_rev < $base)) {
+                       $base = $max_rev;
+               } elsif (!defined $max_rev) {
+                       $base = 0;
+               }
+       }
+
+       if ($fetch) {
+               foreach my $p (sort keys %$fetch) {
+                       my $gs = Git::SVN->new($fetch->{$p}, $repo_id, $p);
+                       my $lr = $gs->rev_db_max;
+                       if (defined $lr) {
+                               $base = $lr if ($lr < $base);
+                       }
+                       push @gs, $gs;
+               }
+       }
+
+       ($base, $head) = parse_revision_argument($base, $head);
+       $ra->gs_fetch_loop_common($base, $head, \@gs, \@globs);
+}
+
+sub read_all_remotes {
+       my $r = {};
+       foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
+               if (m!^(.+)\.fetch=\s*(.*)\s*:\s*refs/remotes/(.+)\s*$!) {
+                       my ($remote, $local_ref, $remote_ref) = ($1, $2, $3);
+                       $local_ref =~ s{^/}{};
+                       $r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
+               } elsif (m!^(.+)\.url=\s*(.*)\s*$!) {
+                       $r->{$1}->{url} = $2;
+               } elsif (m!^(.+)\.(branches|tags)=
+                          (.*):refs/remotes/(.+)\s*$/!x) {
+                       my ($p, $g) = ($3, $4);
+                       my $rs = $r->{$1}->{$2} = {
+                                         t => $2,
+                                         remote => $1,
+                                         path => Git::SVN::GlobSpec->new($p),
+                                         ref => Git::SVN::GlobSpec->new($g) };
+                       if (length($rs->{ref}->{right}) != 0) {
+                               die "The '*' glob character must be the last ",
+                                   "character of '$g'\n";
+                       }
+               }
+       }
+       $r;
+}
+
+sub init_vars {
+       if (defined $_repack) {
+               $_repack = 1000 if ($_repack <= 0);
+               $_repack_nr = $_repack;
+               $_repack_flags ||= '-d';
+       }
+}
+
+sub verify_remotes_sanity {
+       return unless -d $ENV{GIT_DIR};
+       my %seen;
+       foreach (command(qw/config -l/)) {
+               if (m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {
+                       if ($seen{$1}) {
+                               die "Remote ref refs/remote/$1 is tracked by",
+                                   "\n  \"$_\"\nand\n  \"$seen{$1}\"\n",
+                                   "Please resolve this ambiguity in ",
+                                   "your git configuration file before ",
+                                   "continuing\n";
+                       }
+                       $seen{$1} = $_;
+               }
+       }
+}
+
+# we allow more chars than remotes2config.sh...
+sub sanitize_remote_name {
+       my ($name) = @_;
+       $name =~ tr{A-Za-z0-9:,/+-}{.}c;
+       $name;
+}
+
+sub find_existing_remote {
+       my ($url, $remotes) = @_;
+       return undef if $no_reuse_existing;
+       my $existing;
+       foreach my $repo_id (keys %$remotes) {
+               my $u = $remotes->{$repo_id}->{url} or next;
+               next if $u ne $url;
+               $existing = $repo_id;
+               last;
+       }
+       $existing;
+}
+
+sub init_remote_config {
+       my ($self, $url, $no_write) = @_;
+       $url =~ s!/+$!!; # strip trailing slash
+       my $r = read_all_remotes();
+       my $existing = find_existing_remote($url, $r);
+       if ($existing) {
+               unless ($no_write) {
+                       print STDERR "Using existing ",
+                                    "[svn-remote \"$existing\"]\n";
+               }
+               $self->{repo_id} = $existing;
+       } elsif ($_minimize_url) {
+               my $min_url = Git::SVN::Ra->new($url)->minimize_url;
+               $existing = find_existing_remote($min_url, $r);
+               if ($existing) {
+                       unless ($no_write) {
+                               print STDERR "Using existing ",
+                                            "[svn-remote \"$existing\"]\n";
+                       }
+                       $self->{repo_id} = $existing;
+               }
+               if ($min_url ne $url) {
+                       unless ($no_write) {
+                               print STDERR "Using higher level of URL: ",
+                                            "$url => $min_url\n";
+                       }
+                       my $old_path = $self->{path};
+                       $self->{path} = $url;
+                       $self->{path} =~ s!^\Q$min_url\E(/|$)!!;
+                       if (length $old_path) {
+                               $self->{path} .= "/$old_path";
+                       }
+                       $url = $min_url;
+               }
+       }
+       my $orig_url;
+       if (!$existing) {
+               # verify that we aren't overwriting anything:
+               $orig_url = eval {
+                       command_oneline('config', '--get',
+                                       "svn-remote.$self->{repo_id}.url")
+               };
+               if ($orig_url && ($orig_url ne $url)) {
+                       die "svn-remote.$self->{repo_id}.url already set: ",
+                           "$orig_url\nwanted to set to: $url\n";
+               }
+       }
+       my ($xrepo_id, $xpath) = find_ref($self->refname);
+       if (defined $xpath) {
+               die "svn-remote.$xrepo_id.fetch already set to track ",
+                   "$xpath:refs/remotes/", $self->refname, "\n";
+       }
+       unless ($no_write) {
+               command_noisy('config',
+                             "svn-remote.$self->{repo_id}.url", $url);
+               $self->{path} =~ s{^/}{};
+               command_noisy('config', '--add',
+                             "svn-remote.$self->{repo_id}.fetch",
+                             "$self->{path}:".$self->refname);
+       }
+       $self->{url} = $url;
+}
+
+sub find_by_url { # repos_root and, path are optional
+       my ($class, $full_url, $repos_root, $path) = @_;
+
+       return undef unless defined $full_url;
+       remove_username($full_url);
+       remove_username($repos_root) if defined $repos_root;
+       my $remotes = read_all_remotes();
+       if (defined $full_url && defined $repos_root && !defined $path) {
+               $path = $full_url;
+               $path =~ s#^\Q$repos_root\E(?:/|$)##;
+       }
+       foreach my $repo_id (keys %$remotes) {
+               my $u = $remotes->{$repo_id}->{url} or next;
+               remove_username($u);
+               next if defined $repos_root && $repos_root ne $u;
+
+               my $fetch = $remotes->{$repo_id}->{fetch} || {};
+               foreach (qw/branches tags/) {
+                       resolve_local_globs($u, $fetch,
+                                           $remotes->{$repo_id}->{$_});
+               }
+               my $p = $path;
+               unless (defined $p) {
+                       $p = $full_url;
+                       $p =~ s#^\Q$u\E(?:/|$)## or next;
+               }
+               foreach my $f (keys %$fetch) {
+                       next if $f ne $p;
+                       return Git::SVN->new($fetch->{$f}, $repo_id, $f);
+               }
+       }
+       undef;
+}
+
+sub init {
+       my ($class, $url, $path, $repo_id, $ref_id, $no_write) = @_;
+       my $self = _new($class, $repo_id, $ref_id, $path);
+       if (defined $url) {
+               $self->init_remote_config($url, $no_write);
+       }
+       $self;
+}
+
+sub find_ref {
+       my ($ref_id) = @_;
+       foreach (command(qw/config -l/)) {
+               next unless m!^svn-remote\.(.+)\.fetch=
+                             \s*(.*)\s*:\s*refs/remotes/(.+)\s*$!x;
+               my ($repo_id, $path, $ref) = ($1, $2, $3);
+               if ($ref eq $ref_id) {
+                       $path = '' if ($path =~ m#^\./?#);
+                       return ($repo_id, $path);
+               }
+       }
+       (undef, undef, undef);
+}
+
+sub new {
+       my ($class, $ref_id, $repo_id, $path) = @_;
+       if (defined $ref_id && !defined $repo_id && !defined $path) {
+               ($repo_id, $path) = find_ref($ref_id);
+               if (!defined $repo_id) {
+                       die "Could not find a \"svn-remote.*.fetch\" key ",
+                           "in the repository configuration matching: ",
+                           "refs/remotes/$ref_id\n";
+               }
+       }
+       my $self = _new($class, $repo_id, $ref_id, $path);
+       if (!defined $self->{path} || !length $self->{path}) {
+               my $fetch = command_oneline('config', '--get',
+                                           "svn-remote.$repo_id.fetch",
+                                           ":refs/remotes/$ref_id\$") or
+                    die "Failed to read \"svn-remote.$repo_id.fetch\" ",
+                        "\":refs/remotes/$ref_id\$\" in config\n";
+               ($self->{path}, undef) = split(/\s*:\s*/, $fetch);
+       }
+       $self->{url} = command_oneline('config', '--get',
+                                      "svn-remote.$repo_id.url") or
+                  die "Failed to read \"svn-remote.$repo_id.url\" in config\n";
+       $self->rebuild;
+       $self;
+}
+
+sub refname {
+       my ($refname) = "refs/remotes/$_[0]->{ref_id}" ;
+
+       # It cannot end with a slash /, we'll throw up on this because
+       # SVN can't have directories with a slash in their name, either:
+       if ($refname =~ m{/$}) {
+               die "ref: '$refname' ends with a trailing slash, this is ",
+                   "not permitted by git nor Subversion\n";
+       }
+
+       # It cannot have ASCII control character space, tilde ~, caret ^,
+       # colon :, question-mark ?, asterisk *, space, or open bracket [
+       # anywhere.
+       #
+       # Additionally, % must be escaped because it is used for escaping
+       # and we want our escaped refname to be reversible
+       $refname =~ s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;
+
+       # no slash-separated component can begin with a dot .
+       # /.* becomes /%2E*
+       $refname =~ s{/\.}{/%2E}g;
+
+       # It cannot have two consecutive dots .. anywhere
+       # .. becomes %2E%2E
+       $refname =~ s{\.\.}{%2E%2E}g;
+
+       return $refname;
+}
+
+sub desanitize_refname {
+       my ($refname) = @_;
+       $refname =~ s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;
+       return $refname;
+}
+
+sub svm_uuid {
+       my ($self) = @_;
+       return $self->{svm}->{uuid} if $self->svm;
+       $self->ra;
+       unless ($self->{svm}) {
+               die "SVM UUID not cached, and reading remotely failed\n";
+       }
+       $self->{svm}->{uuid};
+}
+
+sub svm {
+       my ($self) = @_;
+       return $self->{svm} if $self->{svm};
+       my $svm;
+       # see if we have it in our config, first:
+       eval {
+               my $section = "svn-remote.$self->{repo_id}";
+               $svm = {
+                 source => tmp_config('--get', "$section.svm-source"),
+                 uuid => tmp_config('--get', "$section.svm-uuid"),
+                 replace => tmp_config('--get', "$section.svm-replace"),
+               }
+       };
+       if ($svm && $svm->{source} && $svm->{uuid} && $svm->{replace}) {
+               $self->{svm} = $svm;
+       }
+       $self->{svm};
+}
+
+sub _set_svm_vars {
+       my ($self, $ra) = @_;
+       return $ra if $self->svm;
+
+       my @err = ( "useSvmProps set, but failed to read SVM properties\n",
+                   "(svm:source, svm:uuid) ",
+                   "from the following URLs:\n" );
+       sub read_svm_props {
+               my ($self, $ra, $path, $r) = @_;
+               my $props = ($ra->get_dir($path, $r))[2];
+               my $src = $props->{'svm:source'};
+               my $uuid = $props->{'svm:uuid'};
+               return undef if (!$src || !$uuid);
+
+               chomp($src, $uuid);
+
+               $uuid =~ m{^[0-9a-f\-]{30,}$}
+                   or die "doesn't look right - svm:uuid is '$uuid'\n";
+
+               # the '!' is used to mark the repos_root!/relative/path
+               $src =~ s{/?!/?}{/};
+               $src =~ s{/+$}{}; # no trailing slashes please
+               # username is of no interest
+               $src =~ s{(^[a-z\+]*://)[^/@]*@}{$1};
+
+               my $replace = $ra->{url};
+               $replace .= "/$path" if length $path;
+
+               my $section = "svn-remote.$self->{repo_id}";
+               tmp_config("$section.svm-source", $src);
+               tmp_config("$section.svm-replace", $replace);
+               tmp_config("$section.svm-uuid", $uuid);
+               $self->{svm} = {
+                       source => $src,
+                       uuid => $uuid,
+                       replace => $replace
+               };
+       }
+
+       my $r = $ra->get_latest_revnum;
+       my $path = $self->{path};
+       my %tried;
+       while (length $path) {
+               unless ($tried{"$self->{url}/$path"}) {
+                       return $ra if $self->read_svm_props($ra, $path, $r);
+                       $tried{"$self->{url}/$path"} = 1;
+               }
+               $path =~ s#/?[^/]+$##;
+       }
+       die "Path: '$path' should be ''\n" if $path ne '';
+       return $ra if $self->read_svm_props($ra, $path, $r);
+       $tried{"$self->{url}/$path"} = 1;
+
+       if ($ra->{repos_root} eq $self->{url}) {
+               die @err, (map { "  $_\n" } keys %tried), "\n";
+       }
+
+       # nope, make sure we're connected to the repository root:
+       my $ok;
+       my @tried_b;
+       $path = $ra->{svn_path};
+       $ra = Git::SVN::Ra->new($ra->{repos_root});
+       while (length $path) {
+               unless ($tried{"$ra->{url}/$path"}) {
+                       $ok = $self->read_svm_props($ra, $path, $r);
+                       last if $ok;
+                       $tried{"$ra->{url}/$path"} = 1;
+               }
+               $path =~ s#/?[^/]+$##;
+       }
+       die "Path: '$path' should be ''\n" if $path ne '';
+       $ok ||= $self->read_svm_props($ra, $path, $r);
+       $tried{"$ra->{url}/$path"} = 1;
+       if (!$ok) {
+               die @err, (map { "  $_\n" } keys %tried), "\n";
+       }
+       Git::SVN::Ra->new($self->{url});
+}
+
+sub svnsync {
+       my ($self) = @_;
+       return $self->{svnsync} if $self->{svnsync};
+
+       if ($self->no_metadata) {
+               die "Can't have both 'noMetadata' and ",
+                   "'useSvnsyncProps' options set!\n";
+       }
+       if ($self->rewrite_root) {
+               die "Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",
+                   "options set!\n";
+       }
+
+       my $svnsync;
+       # see if we have it in our config, first:
+       eval {
+               my $section = "svn-remote.$self->{repo_id}";
+               $svnsync = {
+                 url => tmp_config('--get', "$section.svnsync-url"),
+                 uuid => tmp_config('--get', "$section.svnsync-uuid"),
+               }
+       };
+       if ($svnsync && $svnsync->{url} && $svnsync->{uuid}) {
+               return $self->{svnsync} = $svnsync;
+       }
+
+       my $err = "useSvnsyncProps set, but failed to read " .
+                 "svnsync property: svn:sync-from-";
+       my $rp = $self->ra->rev_proplist(0);
+
+       my $url = $rp->{'svn:sync-from-url'} or die $err . "url\n";
+       $url =~ m{^[a-z\+]+://} or
+                  die "doesn't look right - svn:sync-from-url is '$url'\n";
+
+       my $uuid = $rp->{'svn:sync-from-uuid'} or die $err . "uuid\n";
+       $uuid =~ m{^[0-9a-f\-]{30,}$} or
+                  die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
+
+       my $section = "svn-remote.$self->{repo_id}";
+       tmp_config('--add', "$section.svnsync-uuid", $uuid);
+       tmp_config('--add', "$section.svnsync-url", $url);
+       return $self->{svnsync} = { url => $url, uuid => $uuid };
+}
+
+# this allows us to memoize our SVN::Ra UUID locally and avoid a
+# remote lookup (useful for 'git svn log').
+sub ra_uuid {
+       my ($self) = @_;
+       unless ($self->{ra_uuid}) {
+               my $key = "svn-remote.$self->{repo_id}.uuid";
+               my $uuid = eval { tmp_config('--get', $key) };
+               if (!$@ && $uuid && $uuid =~ /^([a-f\d\-]{30,})$/) {
+                       $self->{ra_uuid} = $uuid;
+               } else {
+                       die "ra_uuid called without URL\n" unless $self->{url};
+                       $self->{ra_uuid} = $self->ra->get_uuid;
+                       tmp_config('--add', $key, $self->{ra_uuid});
+               }
+       }
+       $self->{ra_uuid};
+}
+
+sub _set_repos_root {
+       my ($self, $repos_root) = @_;
+       my $k = "svn-remote.$self->{repo_id}.reposRoot";
+       $repos_root ||= $self->ra->{repos_root};
+       tmp_config($k, $repos_root);
+       $repos_root;
+}
+
+sub repos_root {
+       my ($self) = @_;
+       my $k = "svn-remote.$self->{repo_id}.reposRoot";
+       eval { tmp_config('--get', $k) } || $self->_set_repos_root;
+}
+
+sub ra {
+       my ($self) = shift;
+       my $ra = Git::SVN::Ra->new($self->{url});
+       $self->_set_repos_root($ra->{repos_root});
+       if ($self->use_svm_props && !$self->{svm}) {
+               if ($self->no_metadata) {
+                       die "Can't have both 'noMetadata' and ",
+                           "'useSvmProps' options set!\n";
+               } elsif ($self->use_svnsync_props) {
+                       die "Can't have both 'useSvnsyncProps' and ",
+                           "'useSvmProps' options set!\n";
+               }
+               $ra = $self->_set_svm_vars($ra);
+               $self->{-want_revprops} = 1;
+       }
+       $ra;
+}
+
+sub rel_path {
+       my ($self) = @_;
+       my $repos_root = $self->ra->{repos_root};
+       return $self->{path} if ($self->{url} eq $repos_root);
+       my $url = $self->{url} .
+                 (length $self->{path} ? "/$self->{path}" : $self->{path});
+       $url =~ s!^\Q$repos_root\E(?:/+|$)!!g;
+       $url;
+}
+
+# prop_walk(PATH, REV, SUB)
+# -------------------------
+# Recursively traverse PATH at revision REV and invoke SUB for each
+# directory that contains a SVN property.  SUB will be invoked as
+# follows:  &SUB(gs, path, props);  where `gs' is this instance of
+# Git::SVN, `path' the path to the directory where the properties
+# `props' were found.  The `path' will be relative to point of checkout,
+# that is, if url://repo/trunk is the current Git branch, and that
+# directory contains a sub-directory `d', SUB will be invoked with `/d/'
+# as `path' (note the trailing `/').
+sub prop_walk {
+       my ($self, $path, $rev, $sub) = @_;
+
+       my ($dirent, undef, $props) = $self->ra->get_dir($path, $rev);
+       $path =~ s#^/*#/#g;
+       my $p = $path;
+       # Strip the irrelevant part of the path.
+       $p =~ s#^/+\Q$self->{path}\E(/|$)#/#;
+       # Ensure the path is terminated by a `/'.
+       $p =~ s#/*$#/#;
+
+       # The properties contain all the internal SVN stuff nobody
+       # (usually) cares about.
+       my $interesting_props = 0;
+       foreach (keys %{$props}) {
+               # If it doesn't start with `svn:', it must be a
+               # user-defined property.
+               ++$interesting_props and next if $_ !~ /^svn:/;
+               # FIXME: Fragile, if SVN adds new public properties,
+               # this needs to be updated.
+               ++$interesting_props if /^svn:(?:ignore|keywords|executable
+                                                |eol-style|mime-type
+                                                |externals|needs-lock)$/x;
+       }
+       &$sub($self, $p, $props) if $interesting_props;
+
+       foreach (sort keys %$dirent) {
+               next if $dirent->{$_}->{kind} != $SVN::Node::dir;
+               $self->prop_walk($path . '/' . $_, $rev, $sub);
+       }
+}
+
+sub last_rev { ($_[0]->last_rev_commit)[0] }
+sub last_commit { ($_[0]->last_rev_commit)[1] }
+
+# returns the newest SVN revision number and newest commit SHA1
+sub last_rev_commit {
+       my ($self) = @_;
+       if (defined $self->{last_rev} && defined $self->{last_commit}) {
+               return ($self->{last_rev}, $self->{last_commit});
+       }
+       my $c = ::verify_ref($self->refname.'^0');
+       if ($c && !$self->use_svm_props && !$self->no_metadata) {
+               my $rev = (::cmt_metadata($c))[1];
+               if (defined $rev) {
+                       ($self->{last_rev}, $self->{last_commit}) = ($rev, $c);
+                       return ($rev, $c);
+               }
+       }
+       my $db_path = $self->db_path;
+       unless (-e $db_path) {
+               ($self->{last_rev}, $self->{last_commit}) = (undef, undef);
+               return (undef, undef);
+       }
+       my $offset = -41; # from tail
+       my $rl;
+       open my $fh, '<', $db_path or croak "$db_path not readable: $!\n";
+       sysseek($fh, $offset, 2); # don't care for errors
+       sysread($fh, $rl, 41) == 41 or return (undef, undef);
+       chomp $rl;
+       while (('0' x40) eq $rl && sysseek($fh, 0, 1) != 0) {
+               $offset -= 41;
+               sysseek($fh, $offset, 2); # don't care for errors
+               sysread($fh, $rl, 41) == 41 or return (undef, undef);
+               chomp $rl;
+       }
+       if ($c && $c ne $rl) {
+               die "$db_path and ", $self->refname,
+                   " inconsistent!:\n$c != $rl\n";
+       }
+       my $rev = sysseek($fh, 0, 1) or croak $!;
+       $rev =  ($rev - 41) / 41;
+       close $fh or croak $!;
+       ($self->{last_rev}, $self->{last_commit}) = ($rev, $c);
+       return ($rev, $c);
+}
+
+sub get_fetch_range {
+       my ($self, $min, $max) = @_;
+       $max ||= $self->ra->get_latest_revnum;
+       $min ||= $self->rev_db_max;
+       (++$min, $max);
+}
+
+sub tmp_config {
+       my (@args) = @_;
+       my $old_def_config = "$ENV{GIT_DIR}/svn/config";
+       my $config = "$ENV{GIT_DIR}/svn/.metadata";
+       if (! -f $config && -f $old_def_config) {
+               rename $old_def_config, $config or
+                      die "Failed rename $old_def_config => $config: $!\n";
+       }
+       my $old_config = $ENV{GIT_CONFIG};
+       $ENV{GIT_CONFIG} = $config;
+       $@ = undef;
+       my @ret = eval {
+               unless (-f $config) {
+                       mkfile($config);
+                       open my $fh, '>', $config or
+                           die "Can't open $config: $!\n";
+                       print $fh "; This file is used internally by ",
+                                 "git-svn\n" or die
+                                 "Couldn't write to $config: $!\n";
+                       print $fh "; You should not have to edit it\n" or
+                             die "Couldn't write to $config: $!\n";
+                       close $fh or die "Couldn't close $config: $!\n";
+               }
+               command('config', @args);
+       };
+       my $err = $@;
+       if (defined $old_config) {
+               $ENV{GIT_CONFIG} = $old_config;
+       } else {
+               delete $ENV{GIT_CONFIG};
+       }
+       die $err if $err;
+       wantarray ? @ret : $ret[0];
+}
+
+sub tmp_index_do {
+       my ($self, $sub) = @_;
+       my $old_index = $ENV{GIT_INDEX_FILE};
+       $ENV{GIT_INDEX_FILE} = $self->{index};
+       $@ = undef;
+       my @ret = eval {
+               my ($dir, $base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);
+               mkpath([$dir]) unless -d $dir;
+               &$sub;
+       };
+       my $err = $@;
+       if (defined $old_index) {
+               $ENV{GIT_INDEX_FILE} = $old_index;
+       } else {
+               delete $ENV{GIT_INDEX_FILE};
+       }
+       die $err if $err;
+       wantarray ? @ret : $ret[0];
+}
+
+sub assert_index_clean {
+       my ($self, $treeish) = @_;
+
+       $self->tmp_index_do(sub {
+               command_noisy('read-tree', $treeish) unless -e $self->{index};
+               my $x = command_oneline('write-tree');
+               my ($y) = (command(qw/cat-file commit/, $treeish) =~
+                          /^tree ($::sha1)/mo);
+               return if $y eq $x;
+
+               warn "Index mismatch: $y != $x\nrereading $treeish\n";
+               unlink $self->{index} or die "unlink $self->{index}: $!\n";
+               command_noisy('read-tree', $treeish);
+               $x = command_oneline('write-tree');
+               if ($y ne $x) {
+                       ::fatal "trees ($treeish) $y != $x\n",
+                               "Something is seriously wrong...";
+               }
+       });
+}
+
+sub get_commit_parents {
+       my ($self, $log_entry) = @_;
+       my (%seen, @ret, @tmp);
+       # legacy support for 'set-tree'; this is only used by set_tree_cb:
+       if (my $ip = $self->{inject_parents}) {
+               if (my $commit = delete $ip->{$log_entry->{revision}}) {
+                       push @tmp, $commit;
+               }
+       }
+       if (my $cur = ::verify_ref($self->refname.'^0')) {
+               push @tmp, $cur;
+       }
+       if (my $ipd = $self->{inject_parents_dcommit}) {
+               if (my $commit = delete $ipd->{$log_entry->{revision}}) {
+                       push @tmp, @$commit;
+               }
+       }
+       push @tmp, $_ foreach (@{$log_entry->{parents}}, @tmp);
+       while (my $p = shift @tmp) {
+               next if $seen{$p};
+               $seen{$p} = 1;
+               push @ret, $p;
+               # MAXPARENT is defined to 16 in commit-tree.c:
+               last if @ret >= 16;
+       }
+       if (@tmp) {
+               die "r$log_entry->{revision}: No room for parents:\n\t",
+                   join("\n\t", @tmp), "\n";
+       }
+       @ret;
+}
+
+sub rewrite_root {
+       my ($self) = @_;
+       return $self->{-rewrite_root} if exists $self->{-rewrite_root};
+       my $k = "svn-remote.$self->{repo_id}.rewriteRoot";
+       my $rwr = eval { command_oneline(qw/config --get/, $k) };
+       if ($rwr) {
+               $rwr =~ s#/+$##;
+               if ($rwr !~ m#^[a-z\+]+://#) {
+                       die "$rwr is not a valid URL (key: $k)\n";
+               }
+       }
+       $self->{-rewrite_root} = $rwr;
+}
+
+sub metadata_url {
+       my ($self) = @_;
+       ($self->rewrite_root || $self->{url}) .
+          (length $self->{path} ? '/' . $self->{path} : '');
+}
+
+sub full_url {
+       my ($self) = @_;
+       $self->{url} . (length $self->{path} ? '/' . $self->{path} : '');
+}
+
+sub do_git_commit {
+       my ($self, $log_entry) = @_;
+       my $lr = $self->last_rev;
+       if (defined $lr && $lr >= $log_entry->{revision}) {
+               die "Last fetched revision of ", $self->refname,
+                   " was r$lr, but we are about to fetch: ",
+                   "r$log_entry->{revision}!\n";
+       }
+       if (my $c = $self->rev_db_get($log_entry->{revision})) {
+               croak "$log_entry->{revision} = $c already exists! ",
+                     "Why are we refetching it?\n";
+       }
+       $ENV{GIT_AUTHOR_NAME} = $log_entry->{name};
+       $ENV{GIT_AUTHOR_EMAIL} = $log_entry->{email};
+       $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_entry->{date};
+
+       $ENV{GIT_COMMITTER_NAME} = (defined $log_entry->{commit_name})
+                                               ? $log_entry->{commit_name}
+                                               : $log_entry->{name};
+       $ENV{GIT_COMMITTER_EMAIL} = (defined $log_entry->{commit_email})
+                                               ? $log_entry->{commit_email}
+                                               : $log_entry->{email};
+
+       my $tree = $log_entry->{tree};
+       if (!defined $tree) {
+               $tree = $self->tmp_index_do(sub {
+                                           command_oneline('write-tree') });
+       }
+       die "Tree is not a valid sha1: $tree\n" if $tree !~ /^$::sha1$/o;
+
+       my @exec = ('git-commit-tree', $tree);
+       foreach ($self->get_commit_parents($log_entry)) {
+               push @exec, '-p', $_;
+       }
+       defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec))
+                                                                  or croak $!;
+       print $msg_fh $log_entry->{log} or croak $!;
+       unless ($self->no_metadata) {
+               print $msg_fh "\ngit-svn-id: $log_entry->{metadata}\n"
+                             or croak $!;
+       }
+       $msg_fh->flush == 0 or croak $!;
+       close $msg_fh or croak $!;
+       chomp(my $commit = do { local $/; <$out_fh> });
+       close $out_fh or croak $!;
+       waitpid $pid, 0;
+       croak $? if $?;
+       if ($commit !~ /^$::sha1$/o) {
+               die "Failed to commit, invalid sha1: $commit\n";
+       }
+
+       $self->rev_db_set($log_entry->{revision}, $commit, 1);
+
+       $self->{last_rev} = $log_entry->{revision};
+       $self->{last_commit} = $commit;
+       print "r$log_entry->{revision}";
+       if (defined $log_entry->{svm_revision}) {
+                print " (\@$log_entry->{svm_revision})";
+                $self->rev_db_set($log_entry->{svm_revision}, $commit,
+                                  0, $self->svm_uuid);
+       }
+       print " = $commit ($self->{ref_id})\n";
+       if (defined $_repack && (--$_repack_nr == 0)) {
+               $_repack_nr = $_repack;
+               # repack doesn't use any arguments with spaces in them, does it?
+               print "Running git repack $_repack_flags ...\n";
+               command_noisy('repack', split(/\s+/, $_repack_flags));
+               print "Done repacking\n";
+       }
+       return $commit;
+}
+
+sub match_paths {
+       my ($self, $paths, $r) = @_;
+       return 1 if $self->{path} eq '';
+       if (my $path = $paths->{"/$self->{path}"}) {
+               return ($path->{action} eq 'D') ? 0 : 1;
+       }
+       $self->{path_regex} ||= qr/^\/\Q$self->{path}\E\//;
+       if (grep /$self->{path_regex}/, keys %$paths) {
+               return 1;
+       }
+       my $c = '';
+       foreach (split m#/#, $self->{path}) {
+               $c .= "/$_";
+               next unless ($paths->{$c} &&
+                            ($paths->{$c}->{action} =~ /^[AR]$/));
+               if ($self->ra->check_path($self->{path}, $r) ==
+                   $SVN::Node::dir) {
+                       return 1;
+               }
+       }
+       return 0;
+}
+
+sub find_parent_branch {
+       my ($self, $paths, $rev) = @_;
+       return undef unless $self->follow_parent;
+       unless (defined $paths) {
+               my $err_handler = $SVN::Error::handler;
+               $SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;
+               $self->ra->get_log([$self->{path}], $rev, $rev, 0, 1, 1, sub {
+                                  $paths =
+                                     Git::SVN::Ra::dup_changed_paths($_[0]) });
+               $SVN::Error::handler = $err_handler;
+       }
+       return undef unless defined $paths;
+
+       # look for a parent from another branch:
+       my @b_path_components = split m#/#, $self->rel_path;
+       my @a_path_components;
+       my $i;
+       while (@b_path_components) {
+               $i = $paths->{'/'.join('/', @b_path_components)};
+               last if $i && defined $i->{copyfrom_path};
+               unshift(@a_path_components, pop(@b_path_components));
+       }
+       return undef unless defined $i && defined $i->{copyfrom_path};
+       my $branch_from = $i->{copyfrom_path};
+       if (@a_path_components) {
+               print STDERR "branch_from: $branch_from => ";
+               $branch_from .= '/'.join('/', @a_path_components);
+               print STDERR $branch_from, "\n";
+       }
+       my $r = $i->{copyfrom_rev};
+       my $repos_root = $self->ra->{repos_root};
+       my $url = $self->ra->{url};
+       my $new_url = $repos_root . $branch_from;
+       print STDERR  "Found possible branch point: ",
+                     "$new_url => ", $self->full_url, ", $r\n";
+       $branch_from =~ s#^/##;
+       my $gs = Git::SVN->find_by_url($new_url, $repos_root, $branch_from);
+       unless ($gs) {
+               my $ref_id = $self->{ref_id};
+               $ref_id =~ s/\@\d+$//;
+               $ref_id .= "\@$r";
+               # just grow a tail if we're not unique enough :x
+               $ref_id .= '-' while find_ref($ref_id);
+               print STDERR "Initializing parent: $ref_id\n";
+               $gs = Git::SVN->init($new_url, '', $ref_id, $ref_id, 1);
+       }
+       my ($r0, $parent) = $gs->find_rev_before($r, 1);
+       if (!defined $r0 || !defined $parent) {
+               my ($base, $head) = parse_revision_argument(0, $r);
+               if ($base <= $r) {
+                       $gs->fetch($base, $r);
+               }
+               ($r0, $parent) = $gs->last_rev_commit;
+       }
+       if (defined $r0 && defined $parent) {
+               print STDERR "Found branch parent: ($self->{ref_id}) $parent\n";
+               my $ed;
+               if ($self->ra->can_do_switch) {
+                       $self->assert_index_clean($parent);
+                       print STDERR "Following parent with do_switch\n";
+                       # do_switch works with svn/trunk >= r22312, but that
+                       # is not included with SVN 1.4.3 (the latest version
+                       # at the moment), so we can't rely on it
+                       $self->{last_commit} = $parent;
+                       $ed = SVN::Git::Fetcher->new($self);
+                       $gs->ra->gs_do_switch($r0, $rev, $gs,
+                                             $self->full_url, $ed)
+                         or die "SVN connection failed somewhere...\n";
+               } elsif ($self->ra->trees_match($new_url, $r0,
+                                               $self->full_url, $rev)) {
+                       print STDERR "Trees match:\n",
+                                    "  $new_url\@$r0\n",
+                                    "  ${\$self->full_url}\@$rev\n",
+                                    "Following parent with no changes\n";
+                       $self->tmp_index_do(sub {
+                           command_noisy('read-tree', $parent);
+                       });
+                       $self->{last_commit} = $parent;
+               } else {
+                       print STDERR "Following parent with do_update\n";
+                       $ed = SVN::Git::Fetcher->new($self);
+                       $self->ra->gs_do_update($rev, $rev, $self, $ed)
+                         or die "SVN connection failed somewhere...\n";
+               }
+               print STDERR "Successfully followed parent\n";
+               return $self->make_log_entry($rev, [$parent], $ed);
+       }
+       return undef;
+}
+
+sub do_fetch {
+       my ($self, $paths, $rev) = @_;
+       my $ed;
+       my ($last_rev, @parents);
+       if (my $lc = $self->last_commit) {
+               # we can have a branch that was deleted, then re-added
+               # under the same name but copied from another path, in
+               # which case we'll have multiple parents (we don't
+               # want to break the original ref, nor lose copypath info):
+               if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
+                       push @{$log_entry->{parents}}, $lc;
+                       return $log_entry;
+               }
+               $ed = SVN::Git::Fetcher->new($self);
+               $last_rev = $self->{last_rev};
+               $ed->{c} = $lc;
+               @parents = ($lc);
+       } else {
+               $last_rev = $rev;
+               if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
+                       return $log_entry;
+               }
+               $ed = SVN::Git::Fetcher->new($self);
+       }
+       unless ($self->ra->gs_do_update($last_rev, $rev, $self, $ed)) {
+               die "SVN connection failed somewhere...\n";
+       }
+       $self->make_log_entry($rev, \@parents, $ed);
+}
+
+sub get_untracked {
+       my ($self, $ed) = @_;
+       my @out;
+       my $h = $ed->{empty};
+       foreach (sort keys %$h) {
+               my $act = $h->{$_} ? '+empty_dir' : '-empty_dir';
+               push @out, "  $act: " . uri_encode($_);
+               warn "W: $act: $_\n";
+       }
+       foreach my $t (qw/dir_prop file_prop/) {
+               $h = $ed->{$t} or next;
+               foreach my $path (sort keys %$h) {
+                       my $ppath = $path eq '' ? '.' : $path;
+                       foreach my $prop (sort keys %{$h->{$path}}) {
+                               next if $SKIP_PROP{$prop};
+                               my $v = $h->{$path}->{$prop};
+                               my $t_ppath_prop = "$t: " .
+                                                   uri_encode($ppath) . ' ' .
+                                                   uri_encode($prop);
+                               if (defined $v) {
+                                       push @out, "  +$t_ppath_prop " .
+                                                  uri_encode($v);
+                               } else {
+                                       push @out, "  -$t_ppath_prop";
+                               }
+                       }
+               }
+       }
+       foreach my $t (qw/absent_file absent_directory/) {
+               $h = $ed->{$t} or next;
+               foreach my $parent (sort keys %$h) {
+                       foreach my $path (sort @{$h->{$parent}}) {
+                               push @out, "  $t: " .
+                                          uri_encode("$parent/$path");
+                               warn "W: $t: $parent/$path ",
+                                    "Insufficient permissions?\n";
+                       }
+               }
+       }
+       \@out;
+}
+
+sub parse_svn_date {
+       my $date = shift || return '+0000 1970-01-01 00:00:00';
+       my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
+                                           (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x) or
+                                        croak "Unable to parse date: $date\n";
+       "+0000 $Y-$m-$d $H:$M:$S";
+}
+
+sub check_author {
+       my ($author) = @_;
+       if (!defined $author || length $author == 0) {
+               $author = '(no author)';
+       }
+       if (defined $::_authors && ! defined $::users{$author}) {
+               die "Author: $author not defined in $::_authors file\n";
+       }
+       $author;
+}
+
+sub make_log_entry {
+       my ($self, $rev, $parents, $ed) = @_;
+       my $untracked = $self->get_untracked($ed);
+
+       open my $un, '>>', "$self->{dir}/unhandled.log" or croak $!;
+       print $un "r$rev\n" or croak $!;
+       print $un $_, "\n" foreach @$untracked;
+       my %log_entry = ( parents => $parents || [], revision => $rev,
+                         log => '');
+
+       my $headrev;
+       my $logged = delete $self->{logged_rev_props};
+       if (!$logged || $self->{-want_revprops}) {
+               my $rp = $self->ra->rev_proplist($rev);
+               foreach (sort keys %$rp) {
+                       my $v = $rp->{$_};
+                       if (/^svn:(author|date|log)$/) {
+                               $log_entry{$1} = $v;
+                       } elsif ($_ eq 'svm:headrev') {
+                               $headrev = $v;
+                       } else {
+                               print $un "  rev_prop: ", uri_encode($_), ' ',
+                                         uri_encode($v), "\n";
+                       }
+               }
+       } else {
+               map { $log_entry{$_} = $logged->{$_} } keys %$logged;
+       }
+       close $un or croak $!;
+
+       $log_entry{date} = parse_svn_date($log_entry{date});
+       $log_entry{log} .= "\n";
+       my $author = $log_entry{author} = check_author($log_entry{author});
+       my ($name, $email) = defined $::users{$author} ? @{$::users{$author}}
+                                                      : ($author, undef);
+
+       my ($commit_name, $commit_email) = ($name, $email);
+       if ($_use_log_author) {
+               if ($log_entry{log} =~ /From:\s+(.*?)\s+<(.*)>\s*\n/) {
+                       ($name, $email) = ($1, $2);
+               } elsif ($log_entry{log} =~
+                                     /Signed-off-by:\s+(.*?)\s+<(.*)>\s*\n/) {
+                       ($name, $email) = ($1, $2);
+               }
+       }
+       if (defined $headrev && $self->use_svm_props) {
+               if ($self->rewrite_root) {
+                       die "Can't have both 'useSvmProps' and 'rewriteRoot' ",
+                           "options set!\n";
+               }
+               my ($uuid, $r) = $headrev =~ m{^([a-f\d\-]{30,}):(\d+)$};
+               # we don't want "SVM: initializing mirror for junk" ...
+               return undef if $r == 0;
+               my $svm = $self->svm;
+               if ($uuid ne $svm->{uuid}) {
+                       die "UUID mismatch on SVM path:\n",
+                           "expected: $svm->{uuid}\n",
+                           "     got: $uuid\n";
+               }
+               my $full_url = $self->full_url;
+               $full_url =~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or
+                            die "Failed to replace '$svm->{replace}' with ",
+                                "'$svm->{source}' in $full_url\n";
+               # throw away username for storing in records
+               remove_username($full_url);
+               $log_entry{metadata} = "$full_url\@$r $uuid";
+               $log_entry{svm_revision} = $r;
+               $email ||= "$author\@$uuid";
+               $commit_email ||= "$author\@$uuid";
+       } elsif ($self->use_svnsync_props) {
+               my $full_url = $self->svnsync->{url};
+               $full_url .= "/$self->{path}" if length $self->{path};
+               remove_username($full_url);
+               my $uuid = $self->svnsync->{uuid};
+               $log_entry{metadata} = "$full_url\@$rev $uuid";
+               $email ||= "$author\@$uuid";
+               $commit_email ||= "$author\@$uuid";
+       } else {
+               my $url = $self->metadata_url;
+               remove_username($url);
+               $log_entry{metadata} = "$url\@$rev " .
+                                      $self->ra->get_uuid;
+               $email ||= "$author\@" . $self->ra->get_uuid;
+               $commit_email ||= "$author\@" . $self->ra->get_uuid;
+       }
+       $log_entry{name} = $name;
+       $log_entry{email} = $email;
+       $log_entry{commit_name} = $commit_name;
+       $log_entry{commit_email} = $commit_email;
+       \%log_entry;
+}
+
+sub fetch {
+       my ($self, $min_rev, $max_rev, @parents) = @_;
+       my ($last_rev, $last_commit) = $self->last_rev_commit;
+       my ($base, $head) = $self->get_fetch_range($min_rev, $max_rev);
+       $self->ra->gs_fetch_loop_common($base, $head, [$self]);
+}
+
+sub set_tree_cb {
+       my ($self, $log_entry, $tree, $rev, $date, $author) = @_;
+       $self->{inject_parents} = { $rev => $tree };
+       $self->fetch(undef, undef);
+}
+
+sub set_tree {
+       my ($self, $tree) = (shift, shift);
+       my $log_entry = ::get_commit_entry($tree);
+       unless ($self->{last_rev}) {
+               fatal("Must have an existing revision to commit");
+       }
+       my %ed_opts = ( r => $self->{last_rev},
+                       log => $log_entry->{log},
+                       ra => $self->ra,
+                       tree_a => $self->{last_commit},
+                       tree_b => $tree,
+                       editor_cb => sub {
+                              $self->set_tree_cb($log_entry, $tree, @_) },
+                       svn_path => $self->{path} );
+       if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
+               print "No changes\nr$self->{last_rev} = $tree\n";
+       }
+}
+
+sub rebuild {
+       my ($self) = @_;
+       my $db_path = $self->db_path;
+       return if (-e $db_path && ! -z $db_path);
+       return unless ::verify_ref($self->refname.'^0');
+       if (-f $self->{db_root}) {
+               rename $self->{db_root}, $db_path or die
+                    "rename $self->{db_root} => $db_path failed: $!\n";
+               my ($dir, $base) = ($db_path =~ m#^(.*?)/?([^/]+)$#);
+               symlink $base, $self->{db_root} or die
+                    "symlink $base => $self->{db_root} failed: $!\n";
+               return;
+       }
+       print "Rebuilding $db_path ...\n";
+       my ($log, $ctx) = command_output_pipe("log", '--no-color', $self->refname);
+       my $latest;
+       my $full_url = $self->full_url;
+       remove_username($full_url);
+       my $svn_uuid;
+       my $c;
+       while (<$log>) {
+               if ( m{^commit ($::sha1)$} ) {
+                       $c = $1;
+                       next;
+               }
+               next unless s{^\s*(git-svn-id:)}{$1};
+               my ($url, $rev, $uuid) = ::extract_metadata($_);
+               remove_username($url);
+
+               # ignore merges (from set-tree)
+               next if (!defined $rev || !$uuid);
+
+               # if we merged or otherwise started elsewhere, this is
+               # how we break out of it
+               if ((defined $svn_uuid && ($uuid ne $svn_uuid)) ||
+                   ($full_url && $url && ($url ne $full_url))) {
+                       next;
+               }
+               $latest ||= $rev;
+               $svn_uuid ||= $uuid;
+
+               $self->rev_db_set($rev, $c);
+               print "r$rev = $c\n";
+       }
+       command_close_pipe($log, $ctx);
+       print "Done rebuilding $db_path\n";
+}
+
+# rev_db:
+# Tie::File seems to be prone to offset errors if revisions get sparse,
+# it's not that fast, either.  Tie::File is also not in Perl 5.6.  So
+# one of my favorite modules is out :<  Next up would be one of the DBM
+# modules, but I'm not sure which is most portable...  So I'll just
+# go with something that's plain-text, but still capable of
+# being randomly accessed.  So here's my ultra-simple fixed-width
+# database.  All records are 40 characters + "\n", so it's easy to seek
+# to a revision: (41 * rev) is the byte offset.
+# A record of 40 0s denotes an empty revision.
+# And yes, it's still pretty fast (faster than Tie::File).
+# These files are disposable unless noMetadata or useSvmProps is set
+
+sub _rev_db_set {
+       my ($fh, $rev, $commit) = @_;
+       my $offset = $rev * 41;
+       # assume that append is the common case:
+       seek $fh, 0, 2 or croak $!;
+       my $pos = tell $fh;
+       if ($pos < $offset) {
+               for (1 .. (($offset - $pos) / 41)) {
+                       print $fh (('0' x 40),"\n") or croak $!;
+               }
+       }
+       seek $fh, $offset, 0 or croak $!;
+       print $fh $commit,"\n" or croak $!;
+}
+
+sub mkfile {
+       my ($path) = @_;
+       unless (-e $path) {
+               my ($dir, $base) = ($path =~ m#^(.*?)/?([^/]+)$#);
+               mkpath([$dir]) unless -d $dir;
+               open my $fh, '>>', $path or die "Couldn't create $path: $!\n";
+               close $fh or die "Couldn't close (create) $path: $!\n";
+       }
+}
+
+sub rev_db_set {
+       my ($self, $rev, $commit, $update_ref, $uuid) = @_;
+       length $commit == 40 or die "arg3 must be a full SHA1 hexsum\n";
+       my $db = $self->db_path($uuid);
+       my $db_lock = "$db.lock";
+       my $sig;
+       if ($update_ref) {
+               $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} =
+                           $SIG{USR1} = $SIG{USR2} = sub { $sig = $_[0] };
+       }
+       mkfile($db);
+
+       $LOCKFILES{$db_lock} = 1;
+       my $sync;
+       # both of these options make our .rev_db file very, very important
+       # and we can't afford to lose it because rebuild() won't work
+       if ($self->use_svm_props || $self->no_metadata) {
+               $sync = 1;
+               copy($db, $db_lock) or die "rev_db_set(@_): ",
+                                          "Failed to copy: ",
+                                          "$db => $db_lock ($!)\n";
+       } else {
+               rename $db, $db_lock or die "rev_db_set(@_): ",
+                                           "Failed to rename: ",
+                                           "$db => $db_lock ($!)\n";
+       }
+       open my $fh, '+<', $db_lock or die "Couldn't open $db_lock: $!\n";
+       _rev_db_set($fh, $rev, $commit);
+       if ($sync) {
+               $fh->flush or die "Couldn't flush $db_lock: $!\n";
+               $fh->sync or die "Couldn't sync $db_lock: $!\n";
+       }
+       close $fh or croak $!;
+       if ($update_ref) {
+               $_head = $self;
+               command_noisy('update-ref', '-m', "r$rev",
+                             $self->refname, $commit);
+       }
+       rename $db_lock, $db or die "rev_db_set(@_): ", "Failed to rename: ",
+                                   "$db_lock => $db ($!)\n";
+       delete $LOCKFILES{$db_lock};
+       if ($update_ref) {
+               $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} =
+                           $SIG{USR1} = $SIG{USR2} = 'DEFAULT';
+               kill $sig, $$ if defined $sig;
+       }
+}
+
+sub rev_db_max {
+       my ($self) = @_;
+       $self->rebuild;
+       my $db_path = $self->db_path;
+       my @stat = stat $db_path or return 0;
+       ($stat[7] % 41) == 0 or die "$db_path inconsistent size: $stat[7]\n";
+       my $max = $stat[7] / 41;
+       (($max > 0) ? $max - 1 : 0);
+}
+
+sub rev_db_get {
+       my ($self, $rev, $uuid) = @_;
+       my $ret;
+       my $offset = $rev * 41;
+       my $db_path = $self->db_path($uuid);
+       return undef unless -e $db_path;
+       open my $fh, '<', $db_path or croak $!;
+       if (sysseek($fh, $offset, 0) == $offset) {
+               my $read = sysread($fh, $ret, 40);
+               $ret = undef if ($read != 40 || $ret eq ('0'x40));
+       }
+       close $fh or croak $!;
+       $ret;
+}
+
+# Finds the first svn revision that exists on (if $eq_ok is true) or
+# before $rev for the current branch.  It will not search any lower
+# than $min_rev.  Returns the git commit hash and svn revision number
+# if found, else (undef, undef).
+sub find_rev_before {
+       my ($self, $rev, $eq_ok, $min_rev) = @_;
+       --$rev unless $eq_ok;
+       $min_rev ||= 1;
+       while ($rev >= $min_rev) {
+               if (my $c = $self->rev_db_get($rev)) {
+                       return ($rev, $c);
+               }
+               --$rev;
+       }
+       return (undef, undef);
+}
+
+# Finds the first svn revision that exists on (if $eq_ok is true) or
+# after $rev for the current branch.  It will not search any higher
+# than $max_rev.  Returns the git commit hash and svn revision number
+# if found, else (undef, undef).
+sub find_rev_after {
+       my ($self, $rev, $eq_ok, $max_rev) = @_;
+       ++$rev unless $eq_ok;
+       $max_rev ||= $self->rev_db_max();
+       while ($rev <= $max_rev) {
+               if (my $c = $self->rev_db_get($rev)) {
+                       return ($rev, $c);
+               }
+               ++$rev;
+       }
+       return (undef, undef);
+}
+
+sub _new {
+       my ($class, $repo_id, $ref_id, $path) = @_;
+       unless (defined $repo_id && length $repo_id) {
+               $repo_id = $Git::SVN::default_repo_id;
+       }
+       unless (defined $ref_id && length $ref_id) {
+               $_[2] = $ref_id = $Git::SVN::default_ref_id;
+       }
+       $_[1] = $repo_id = sanitize_remote_name($repo_id);
+       my $dir = "$ENV{GIT_DIR}/svn/$ref_id";
+       $_[3] = $path = '' unless (defined $path);
+       mkpath(["$ENV{GIT_DIR}/svn"]);
+       bless {
+               ref_id => $ref_id, dir => $dir, index => "$dir/index",
+               path => $path, config => "$ENV{GIT_DIR}/svn/config",
+               db_root => "$dir/.rev_db", repo_id => $repo_id }, $class;
+}
+
+sub db_path {
+       my ($self, $uuid) = @_;
+       $uuid ||= $self->ra_uuid;
+       "$self->{db_root}.$uuid";
+}
+
+sub uri_encode {
+       my ($f) = @_;
+       $f =~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;
+       $f
+}
+
+sub remove_username {
+       $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
+}
+
+package Git::SVN::Prompt;
+use strict;
+use warnings;
+require SVN::Core;
+use vars qw/$_no_auth_cache $_username/;
+
+sub simple {
+       my ($cred, $realm, $default_username, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       $default_username = $_username if defined $_username;
+       if (defined $default_username && length $default_username) {
+               if (defined $realm && length $realm) {
+                       print STDERR "Authentication realm: $realm\n";
+                       STDERR->flush;
+               }
+               $cred->username($default_username);
+       } else {
+               username($cred, $realm, $may_save, $pool);
+       }
+       $cred->password(_read_password("Password for '" .
+                                      $cred->username . "': ", $realm));
+       $cred->may_save($may_save);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub ssl_server_trust {
+       my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       print STDERR "Error validating server certificate for '$realm':\n";
+       {
+               no warnings 'once';
+               # All variables SVN::Auth::SSL::* are used only once,
+               # so we're shutting up Perl warnings about this.
+               if ($failures & $SVN::Auth::SSL::UNKNOWNCA) {
+                       print STDERR " - The certificate is not issued ",
+                           "by a trusted authority. Use the\n",
+                           "   fingerprint to validate ",
+                           "the certificate manually!\n";
+               }
+               if ($failures & $SVN::Auth::SSL::CNMISMATCH) {
+                       print STDERR " - The certificate hostname ",
+                           "does not match.\n";
+               }
+               if ($failures & $SVN::Auth::SSL::NOTYETVALID) {
+                       print STDERR " - The certificate is not yet valid.\n";
+               }
+               if ($failures & $SVN::Auth::SSL::EXPIRED) {
+                       print STDERR " - The certificate has expired.\n";
+               }
+               if ($failures & $SVN::Auth::SSL::OTHER) {
+                       print STDERR " - The certificate has ",
+                           "an unknown error.\n";
+               }
+       } # no warnings 'once'
+       printf STDERR
+               "Certificate information:\n".
+               " - Hostname: %s\n".
+               " - Valid: from %s until %s\n".
+               " - Issuer: %s\n".
+               " - Fingerprint: %s\n",
+               map $cert_info->$_, qw(hostname valid_from valid_until
+                                      issuer_dname fingerprint);
+       my $choice;
+prompt:
+       print STDERR $may_save ?
+             "(R)eject, accept (t)emporarily or accept (p)ermanently? " :
+             "(R)eject or accept (t)emporarily? ";
+       STDERR->flush;
+       $choice = lc(substr(<STDIN> || 'R', 0, 1));
+       if ($choice =~ /^t$/i) {
+               $cred->may_save(undef);
+       } elsif ($choice =~ /^r$/i) {
+               return -1;
+       } elsif ($may_save && $choice =~ /^p$/i) {
+               $cred->may_save($may_save);
+       } else {
+               goto prompt;
+       }
+       $cred->accepted_failures($failures);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub ssl_client_cert {
+       my ($cred, $realm, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       print STDERR "Client certificate filename: ";
+       STDERR->flush;
+       chomp(my $filename = <STDIN>);
+       $cred->cert_file($filename);
+       $cred->may_save($may_save);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub ssl_client_cert_pw {
+       my ($cred, $realm, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       $cred->password(_read_password("Password: ", $realm));
+       $cred->may_save($may_save);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub username {
+       my ($cred, $realm, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       if (defined $realm && length $realm) {
+               print STDERR "Authentication realm: $realm\n";
+       }
+       my $username;
+       if (defined $_username) {
+               $username = $_username;
+       } else {
+               print STDERR "Username: ";
+               STDERR->flush;
+               chomp($username = <STDIN>);
+       }
+       $cred->username($username);
+       $cred->may_save($may_save);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _read_password {
+       my ($prompt, $realm) = @_;
+       print STDERR $prompt;
+       STDERR->flush;
+       require Term::ReadKey;
+       Term::ReadKey::ReadMode('noecho');
+       my $password = '';
+       while (defined(my $key = Term::ReadKey::ReadKey(0))) {
+               last if $key =~ /[\012\015]/; # \n\r
+               $password .= $key;
+       }
+       Term::ReadKey::ReadMode('restore');
+       print STDERR "\n";
+       STDERR->flush;
+       $password;
+}
+
+package SVN::Git::Fetcher;
+use vars qw/@ISA/;
+use strict;
+use warnings;
+use Carp qw/croak/;
+use IO::File qw//;
+
+# file baton members: path, mode_a, mode_b, pool, fh, blob, base
+sub new {
+       my ($class, $git_svn) = @_;
+       my $self = SVN::Delta::Editor->new;
+       bless $self, $class;
+       $self->{c} = $git_svn->{last_commit} if exists $git_svn->{last_commit};
+       $self->{empty} = {};
+       $self->{dir_prop} = {};
+       $self->{file_prop} = {};
+       $self->{absent_dir} = {};
+       $self->{absent_file} = {};
+       $self->{gii} = $git_svn->tmp_index_do(sub { Git::IndexInfo->new });
+       $self;
+}
+
+sub set_path_strip {
+       my ($self, $path) = @_;
+       $self->{path_strip} = qr/^\Q$path\E(\/|$)/ if length $path;
+}
+
+sub open_root {
+       { path => '' };
+}
+
+sub open_directory {
+       my ($self, $path, $pb, $rev) = @_;
+       { path => $path };
+}
+
+sub git_path {
+       my ($self, $path) = @_;
+       if ($self->{path_strip}) {
+               $path =~ s!$self->{path_strip}!! or
+                 die "Failed to strip path '$path' ($self->{path_strip})\n";
+       }
+       $path;
+}
+
+sub delete_entry {
+       my ($self, $path, $rev, $pb) = @_;
+
+       my $gpath = $self->git_path($path);
+       return undef if ($gpath eq '');
+
+       # remove entire directories.
+       if (command('ls-tree', $self->{c}, '--', $gpath) =~ /^040000 tree/) {
+               my ($ls, $ctx) = command_output_pipe(qw/ls-tree
+                                                    -r --name-only -z/,
+                                                    $self->{c}, '--', $gpath);
+               local $/ = "\0";
+               while (<$ls>) {
+                       chomp;
+                       $self->{gii}->remove($_);
+                       print "\tD\t$_\n" unless $::_q;
+               }
+               print "\tD\t$gpath/\n" unless $::_q;
+               command_close_pipe($ls, $ctx);
+               $self->{empty}->{$path} = 0
+       } else {
+               $self->{gii}->remove($gpath);
+               print "\tD\t$gpath\n" unless $::_q;
+       }
+       undef;
+}
+
+sub open_file {
+       my ($self, $path, $pb, $rev) = @_;
+       my $gpath = $self->git_path($path);
+       my ($mode, $blob) = (command('ls-tree', $self->{c}, '--', $gpath)
+                            =~ /^(\d{6}) blob ([a-f\d]{40})\t/);
+       unless (defined $mode && defined $blob) {
+               die "$path was not found in commit $self->{c} (r$rev)\n";
+       }
+       { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
+         pool => SVN::Pool->new, action => 'M' };
+}
+
+sub add_file {
+       my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
+       my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
+       delete $self->{empty}->{$dir};
+       { path => $path, mode_a => 100644, mode_b => 100644,
+         pool => SVN::Pool->new, action => 'A' };
+}
+
+sub add_directory {
+       my ($self, $path, $cp_path, $cp_rev) = @_;
+       my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
+       delete $self->{empty}->{$dir};
+       $self->{empty}->{$path} = 1;
+       { path => $path };
+}
+
+sub change_dir_prop {
+       my ($self, $db, $prop, $value) = @_;
+       $self->{dir_prop}->{$db->{path}} ||= {};
+       $self->{dir_prop}->{$db->{path}}->{$prop} = $value;
+       undef;
+}
+
+sub absent_directory {
+       my ($self, $path, $pb) = @_;
+       $self->{absent_dir}->{$pb->{path}} ||= [];
+       push @{$self->{absent_dir}->{$pb->{path}}}, $path;
+       undef;
+}
+
+sub absent_file {
+       my ($self, $path, $pb) = @_;
+       $self->{absent_file}->{$pb->{path}} ||= [];
+       push @{$self->{absent_file}->{$pb->{path}}}, $path;
+       undef;
+}
+
+sub change_file_prop {
+       my ($self, $fb, $prop, $value) = @_;
+       if ($prop eq 'svn:executable') {
+               if ($fb->{mode_b} != 120000) {
+                       $fb->{mode_b} = defined $value ? 100755 : 100644;
+               }
+       } elsif ($prop eq 'svn:special') {
+               $fb->{mode_b} = defined $value ? 120000 : 100644;
+       } else {
+               $self->{file_prop}->{$fb->{path}} ||= {};
+               $self->{file_prop}->{$fb->{path}}->{$prop} = $value;
+       }
+       undef;
+}
+
+sub apply_textdelta {
+       my ($self, $fb, $exp) = @_;
+       my $fh = IO::File->new_tmpfile;
+       $fh->autoflush(1);
+       # $fh gets auto-closed() by SVN::TxDelta::apply(),
+       # (but $base does not,) so dup() it for reading in close_file
+       open my $dup, '<&', $fh or croak $!;
+       my $base = IO::File->new_tmpfile;
+       $base->autoflush(1);
+       if ($fb->{blob}) {
+               defined (my $pid = fork) or croak $!;
+               if (!$pid) {
+                       open STDOUT, '>&', $base or croak $!;
+                       print STDOUT 'link ' if ($fb->{mode_a} == 120000);
+                       exec qw/git-cat-file blob/, $fb->{blob} or croak $!;
+               }
+               waitpid $pid, 0;
+               croak $? if $?;
+
+               if (defined $exp) {
+                       seek $base, 0, 0 or croak $!;
+                       my $got = ::md5sum($base);
+                       die "Checksum mismatch: $fb->{path} $fb->{blob}\n",
+                           "expected: $exp\n",
+                           "     got: $got\n" if ($got ne $exp);
+               }
+       }
+       seek $base, 0, 0 or croak $!;
+       $fb->{fh} = $dup;
+       $fb->{base} = $base;
+       [ SVN::TxDelta::apply($base, $fh, undef, $fb->{path}, $fb->{pool}) ];
+}
+
+sub close_file {
+       my ($self, $fb, $exp) = @_;
+       my $hash;
+       my $path = $self->git_path($fb->{path});
+       if (my $fh = $fb->{fh}) {
+               if (defined $exp) {
+                       seek($fh, 0, 0) or croak $!;
+                       my $got = ::md5sum($fh);
+                       if ($got ne $exp) {
+                               die "Checksum mismatch: $path\n",
+                                   "expected: $exp\n    got: $got\n";
+                       }
+               }
+               sysseek($fh, 0, 0) or croak $!;
+               if ($fb->{mode_b} == 120000) {
+                       sysread($fh, my $buf, 5) == 5 or croak $!;
+                       $buf eq 'link ' or die "$path has mode 120000",
+                                              "but is not a link\n";
+               }
+               defined(my $pid = open my $out,'-|') or die "Can't fork: $!\n";
+               if (!$pid) {
+                       open STDIN, '<&', $fh or croak $!;
+                       exec qw/git-hash-object -w --stdin/ or croak $!;
+               }
+               chomp($hash = do { local $/; <$out> });
+               close $out or croak $!;
+               close $fh or croak $!;
+               $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
+               close $fb->{base} or croak $!;
+       } else {
+               $hash = $fb->{blob} or die "no blob information\n";
+       }
+       $fb->{pool}->clear;
+       $self->{gii}->update($fb->{mode_b}, $hash, $path) or croak $!;
+       print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $::_q;
+       undef;
+}
+
+sub abort_edit {
+       my $self = shift;
+       $self->{nr} = $self->{gii}->{nr};
+       delete $self->{gii};
+       $self->SUPER::abort_edit(@_);
+}
+
+sub close_edit {
+       my $self = shift;
+       $self->{git_commit_ok} = 1;
+       $self->{nr} = $self->{gii}->{nr};
+       delete $self->{gii};
+       $self->SUPER::close_edit(@_);
+}
+
+package SVN::Git::Editor;
+use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;
+use strict;
+use warnings;
+use Carp qw/croak/;
+use IO::File;
+
+sub new {
+       my ($class, $opts) = @_;
+       foreach (qw/svn_path r ra tree_a tree_b log editor_cb/) {
+               die "$_ required!\n" unless (defined $opts->{$_});
+       }
+
+       my $pool = SVN::Pool->new;
+       my $mods = generate_diff($opts->{tree_a}, $opts->{tree_b});
+       my $types = check_diff_paths($opts->{ra}, $opts->{svn_path},
+                                    $opts->{r}, $mods);
+
+       # $opts->{ra} functions should not be used after this:
+       my @ce  = $opts->{ra}->get_commit_editor($opts->{log},
+                                               $opts->{editor_cb}, $pool);
+       my $self = SVN::Delta::Editor->new(@ce, $pool);
+       bless $self, $class;
+       foreach (qw/svn_path r tree_a tree_b/) {
+               $self->{$_} = $opts->{$_};
+       }
+       $self->{url} = $opts->{ra}->{url};
+       $self->{mods} = $mods;
+       $self->{types} = $types;
+       $self->{pool} = $pool;
+       $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
+       $self->{rm} = { };
+       $self->{path_prefix} = length $self->{svn_path} ?
+                              "$self->{svn_path}/" : '';
+       return $self;
+}
+
+sub generate_diff {
+       my ($tree_a, $tree_b) = @_;
+       my @diff_tree = qw(diff-tree -z -r);
+       if ($_cp_similarity) {
+               push @diff_tree, "-C$_cp_similarity";
+       } else {
+               push @diff_tree, '-C';
+       }
+       push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
+       push @diff_tree, "-l$_rename_limit" if defined $_rename_limit;
+       push @diff_tree, $tree_a, $tree_b;
+       my ($diff_fh, $ctx) = command_output_pipe(@diff_tree);
+       local $/ = "\0";
+       my $state = 'meta';
+       my @mods;
+       while (<$diff_fh>) {
+               chomp $_; # this gets rid of the trailing "\0"
+               if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
+                                       $::sha1\s($::sha1)\s
+                                       ([MTCRAD])\d*$/xo) {
+                       push @mods, {   mode_a => $1, mode_b => $2,
+                                       sha1_b => $3, chg => $4 };
+                       if ($4 =~ /^(?:C|R)$/) {
+                               $state = 'file_a';
+                       } else {
+                               $state = 'file_b';
+                       }
+               } elsif ($state eq 'file_a') {
+                       my $x = $mods[$#mods] or croak "Empty array\n";
+                       if ($x->{chg} !~ /^(?:C|R)$/) {
+                               croak "Error parsing $_, $x->{chg}\n";
+                       }
+                       $x->{file_a} = $_;
+                       $state = 'file_b';
+               } elsif ($state eq 'file_b') {
+                       my $x = $mods[$#mods] or croak "Empty array\n";
+                       if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
+                               croak "Error parsing $_, $x->{chg}\n";
+                       }
+                       if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
+                               croak "Error parsing $_, $x->{chg}\n";
+                       }
+                       $x->{file_b} = $_;
+                       $state = 'meta';
+               } else {
+                       croak "Error parsing $_\n";
+               }
+       }
+       command_close_pipe($diff_fh, $ctx);
+       \@mods;
+}
+
+sub check_diff_paths {
+       my ($ra, $pfx, $rev, $mods) = @_;
+       my %types;
+       $pfx .= '/' if length $pfx;
+
+       sub type_diff_paths {
+               my ($ra, $types, $path, $rev) = @_;
+               my @p = split m#/+#, $path;
+               my $c = shift @p;
+               unless (defined $types->{$c}) {
+                       $types->{$c} = $ra->check_path($c, $rev);
+               }
+               while (@p) {
+                       $c .= '/' . shift @p;
+                       next if defined $types->{$c};
+                       $types->{$c} = $ra->check_path($c, $rev);
+               }
+       }
+
+       foreach my $m (@$mods) {
+               foreach my $f (qw/file_a file_b/) {
+                       next unless defined $m->{$f};
+                       my ($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);
+                       if (length $pfx.$dir && ! defined $types{$dir}) {
+                               type_diff_paths($ra, \%types, $pfx.$dir, $rev);
+                       }
+               }
+       }
+       \%types;
+}
+
+sub split_path {
+       return ($_[0] =~ m#^(.*?)/?([^/]+)$#);
+}
+
+sub repo_path {
+       my ($self, $path) = @_;
+       $self->{path_prefix}.(defined $path ? $path : '');
+}
+
+sub url_path {
+       my ($self, $path) = @_;
+       if ($self->{url} =~ m#^https?://#) {
+               $path =~ s/([^a-zA-Z0-9_.-])/uc sprintf("%%%02x",ord($1))/eg;
+       }
+       $self->{url} . '/' . $self->repo_path($path);
+}
+
+sub rmdirs {
+       my ($self) = @_;
+       my $rm = $self->{rm};
+       delete $rm->{''}; # we never delete the url we're tracking
+       return unless %$rm;
+
+       foreach (keys %$rm) {
+               my @d = split m#/#, $_;
+               my $c = shift @d;
+               $rm->{$c} = 1;
+               while (@d) {
+                       $c .= '/' . shift @d;
+                       $rm->{$c} = 1;
+               }
+       }
+       delete $rm->{$self->{svn_path}};
+       delete $rm->{''}; # we never delete the url we're tracking
+       return unless %$rm;
+
+       my ($fh, $ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,
+                                            $self->{tree_b});
+       local $/ = "\0";
+       while (<$fh>) {
+               chomp;
+               my @dn = split m#/#, $_;
+               while (pop @dn) {
+                       delete $rm->{join '/', @dn};
+               }
+               unless (%$rm) {
+                       close $fh;
+                       return;
+               }
+       }
+       command_close_pipe($fh, $ctx);
+
+       my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
+       foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
+               $self->close_directory($bat->{$d}, $p);
+               my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
+               print "\tD+\t$d/\n" unless $::_q;
+               $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
+               delete $bat->{$d};
+       }
+}
+
+sub open_or_add_dir {
+       my ($self, $full_path, $baton) = @_;
+       my $t = $self->{types}->{$full_path};
+       if (!defined $t) {
+               die "$full_path not known in r$self->{r} or we have a bug!\n";
+       }
+       {
+               no warnings 'once';
+               # SVN::Node::none and SVN::Node::file are used only once,
+               # so we're shutting up Perl's warnings about them.
+               if ($t == $SVN::Node::none) {
+                       return $self->add_directory($full_path, $baton,
+                           undef, -1, $self->{pool});
+               } elsif ($t == $SVN::Node::dir) {
+                       return $self->open_directory($full_path, $baton,
+                           $self->{r}, $self->{pool});
+               } # no warnings 'once'
+               print STDERR "$full_path already exists in repository at ",
+                   "r$self->{r} and it is not a directory (",
+                   ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n";
+       } # no warnings 'once'
+       exit 1;
+}
+
+sub ensure_path {
+       my ($self, $path) = @_;
+       my $bat = $self->{bat};
+       my $repo_path = $self->repo_path($path);
+       return $bat->{''} unless (length $repo_path);
+       my @p = split m#/+#, $repo_path;
+       my $c = shift @p;
+       $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''});
+       while (@p) {
+               my $c0 = $c;
+               $c .= '/' . shift @p;
+               $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0});
+       }
+       return $bat->{$c};
+}
+
+sub A {
+       my ($self, $m) = @_;
+       my ($dir, $file) = split_path($m->{file_b});
+       my $pbat = $self->ensure_path($dir);
+       my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
+                                       undef, -1);
+       print "\tA\t$m->{file_b}\n" unless $::_q;
+       $self->chg_file($fbat, $m);
+       $self->close_file($fbat,undef,$self->{pool});
+}
+
+sub C {
+       my ($self, $m) = @_;
+       my ($dir, $file) = split_path($m->{file_b});
+       my $pbat = $self->ensure_path($dir);
+       my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
+                               $self->url_path($m->{file_a}), $self->{r});
+       print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
+       $self->chg_file($fbat, $m);
+       $self->close_file($fbat,undef,$self->{pool});
+}
+
+sub delete_entry {
+       my ($self, $path, $pbat) = @_;
+       my $rpath = $self->repo_path($path);
+       my ($dir, $file) = split_path($rpath);
+       $self->{rm}->{$dir} = 1;
+       $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool});
+}
+
+sub R {
+       my ($self, $m) = @_;
+       my ($dir, $file) = split_path($m->{file_b});
+       my $pbat = $self->ensure_path($dir);
+       my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
+                               $self->url_path($m->{file_a}), $self->{r});
+       print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
+       $self->chg_file($fbat, $m);
+       $self->close_file($fbat,undef,$self->{pool});
+
+       ($dir, $file) = split_path($m->{file_a});
+       $pbat = $self->ensure_path($dir);
+       $self->delete_entry($m->{file_a}, $pbat);
+}
+
+sub M {
+       my ($self, $m) = @_;
+       my ($dir, $file) = split_path($m->{file_b});
+       my $pbat = $self->ensure_path($dir);
+       my $fbat = $self->open_file($self->repo_path($m->{file_b}),
+                               $pbat,$self->{r},$self->{pool});
+       print "\t$m->{chg}\t$m->{file_b}\n" unless $::_q;
+       $self->chg_file($fbat, $m);
+       $self->close_file($fbat,undef,$self->{pool});
+}
+
+sub T { shift->M(@_) }
+
+sub change_file_prop {
+       my ($self, $fbat, $pname, $pval) = @_;
+       $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool});
+}
+
+sub chg_file {
+       my ($self, $fbat, $m) = @_;
+       if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
+               $self->change_file_prop($fbat,'svn:executable','*');
+       } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
+               $self->change_file_prop($fbat,'svn:executable',undef);
+       }
+       my $fh = IO::File->new_tmpfile or croak $!;
+       if ($m->{mode_b} =~ /^120/) {
+               print $fh 'link ' or croak $!;
+               $self->change_file_prop($fbat,'svn:special','*');
+       } elsif ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
+               $self->change_file_prop($fbat,'svn:special',undef);
+       }
+       defined(my $pid = fork) or croak $!;
+       if (!$pid) {
+               open STDOUT, '>&', $fh or croak $!;
+               exec qw/git-cat-file blob/, $m->{sha1_b} or croak $!;
+       }
+       waitpid $pid, 0;
+       croak $? if $?;
+       $fh->flush == 0 or croak $!;
+       seek $fh, 0, 0 or croak $!;
+
+       my $exp = ::md5sum($fh);
+       seek $fh, 0, 0 or croak $!;
+
+       my $pool = SVN::Pool->new;
+       my $atd = $self->apply_textdelta($fbat, undef, $pool);
+       my $got = SVN::TxDelta::send_stream($fh, @$atd, $pool);
+       die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp);
+       $pool->clear;
+
+       close $fh or croak $!;
+}
+
+sub D {
+       my ($self, $m) = @_;
+       my ($dir, $file) = split_path($m->{file_b});
+       my $pbat = $self->ensure_path($dir);
+       print "\tD\t$m->{file_b}\n" unless $::_q;
+       $self->delete_entry($m->{file_b}, $pbat);
+}
+
+sub close_edit {
+       my ($self) = @_;
+       my ($p,$bat) = ($self->{pool}, $self->{bat});
+       foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
+               next if $_ eq '';
+               $self->close_directory($bat->{$_}, $p);
+       }
+       $self->close_directory($bat->{''}, $p);
+       $self->SUPER::close_edit($p);
+       $p->clear;
+}
+
+sub abort_edit {
+       my ($self) = @_;
+       $self->SUPER::abort_edit($self->{pool});
+}
+
+sub DESTROY {
+       my $self = shift;
+       $self->SUPER::DESTROY(@_);
+       $self->{pool}->clear;
+}
+
+# this drives the editor
+sub apply_diff {
+       my ($self) = @_;
+       my $mods = $self->{mods};
+       my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
+       foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
+               my $f = $m->{chg};
+               if (defined $o{$f}) {
+                       $self->$f($m);
+               } else {
+                       fatal("Invalid change type: $f");
+               }
+       }
+       $self->rmdirs if $_rmdir;
+       if (@$mods == 0) {
+               $self->abort_edit;
+       } else {
+               $self->close_edit;
+       }
+       return scalar @$mods;
+}
+
+package Git::SVN::Ra;
+use vars qw/@ISA $config_dir $_log_window_size/;
+use strict;
+use warnings;
+my ($ra_invalid, $can_do_switch, %ignored_err, $RA);
+
+BEGIN {
+       # enforce temporary pool usage for some simple functions
+       no strict 'refs';
+       for my $f (qw/rev_proplist get_latest_revnum get_uuid get_repos_root/) {
+               my $SUPER = "SUPER::$f";
+               *$f = sub {
+                       my $self = shift;
+                       my $pool = SVN::Pool->new;
+                       my @ret = $self->$SUPER(@_,$pool);
+                       $pool->clear;
+                       wantarray ? @ret : $ret[0];
+               };
+       }
+}
+
+sub _auth_providers () {
+       [
+         SVN::Client::get_simple_provider(),
+         SVN::Client::get_ssl_server_trust_file_provider(),
+         SVN::Client::get_simple_prompt_provider(
+           \&Git::SVN::Prompt::simple, 2),
+         SVN::Client::get_ssl_client_cert_file_provider(),
+         SVN::Client::get_ssl_client_cert_prompt_provider(
+           \&Git::SVN::Prompt::ssl_client_cert, 2),
+         SVN::Client::get_ssl_client_cert_pw_prompt_provider(
+           \&Git::SVN::Prompt::ssl_client_cert_pw, 2),
+         SVN::Client::get_username_provider(),
+         SVN::Client::get_ssl_server_trust_prompt_provider(
+           \&Git::SVN::Prompt::ssl_server_trust),
+         SVN::Client::get_username_prompt_provider(
+           \&Git::SVN::Prompt::username, 2)
+       ]
+}
+
+sub escape_uri_only {
+       my ($uri) = @_;
+       my @tmp;
+       foreach (split m{/}, $uri) {
+               s/([^\w.-])/sprintf("%%%02X",ord($1))/eg;
+               push @tmp, $_;
+       }
+       join('/', @tmp);
+}
+
+sub escape_url {
+       my ($url) = @_;
+       if ($url =~ m#^(https?)://([^/]+)(.*)$#) {
+               my ($scheme, $domain, $uri) = ($1, $2, escape_uri_only($3));
+               $url = "$scheme://$domain$uri";
+       }
+       $url;
+}
+
+sub new {
+       my ($class, $url) = @_;
+       $url =~ s!/+$!!;
+       return $RA if ($RA && $RA->{url} eq $url);
+
+       SVN::_Core::svn_config_ensure($config_dir, undef);
+       my ($baton, $callbacks) = SVN::Core::auth_open_helper(_auth_providers);
+       my $config = SVN::Core::config_get_config($config_dir);
+       $RA = undef;
+       my $dont_store_passwords = 1;
+       my $conf_t = ${$config}{'config'};
+       {
+               no warnings 'once';
+               # The usage of $SVN::_Core::SVN_CONFIG_* variables
+               # produces warnings that variables are used only once.
+               # I had not found the better way to shut them up, so
+               # the warnings of type 'once' are disabled in this block.
+               if (SVN::_Core::svn_config_get_bool($conf_t,
+                   $SVN::_Core::SVN_CONFIG_SECTION_AUTH,
+                   $SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,
+                   1) == 0) {
+                       SVN::_Core::svn_auth_set_parameter($baton,
+                           $SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,
+                           bless (\$dont_store_passwords, "_p_void"));
+               }
+               if (SVN::_Core::svn_config_get_bool($conf_t,
+                   $SVN::_Core::SVN_CONFIG_SECTION_AUTH,
+                   $SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,
+                   1) == 0) {
+                       $Git::SVN::Prompt::_no_auth_cache = 1;
+               }
+       } # no warnings 'once'
+       my $self = SVN::Ra->new(url => escape_url($url), auth => $baton,
+                             config => $config,
+                             pool => SVN::Pool->new,
+                             auth_provider_callbacks => $callbacks);
+       $self->{url} = $url;
+       $self->{svn_path} = $url;
+       $self->{repos_root} = $self->get_repos_root;
+       $self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;
+       $self->{cache} = { check_path => { r => 0, data => {} },
+                          get_dir => { r => 0, data => {} } };
+       $RA = bless $self, $class;
+}
+
+sub check_path {
+       my ($self, $path, $r) = @_;
+       my $cache = $self->{cache}->{check_path};
+       if ($r == $cache->{r} && exists $cache->{data}->{$path}) {
+               return $cache->{data}->{$path};
+       }
+       my $pool = SVN::Pool->new;
+       my $t = $self->SUPER::check_path($path, $r, $pool);
+       $pool->clear;
+       if ($r != $cache->{r}) {
+               %{$cache->{data}} = ();
+               $cache->{r} = $r;
+       }
+       $cache->{data}->{$path} = $t;
+}
+
+sub get_dir {
+       my ($self, $dir, $r) = @_;
+       my $cache = $self->{cache}->{get_dir};
+       if ($r == $cache->{r}) {
+               if (my $x = $cache->{data}->{$dir}) {
+                       return wantarray ? @$x : $x->[0];
+               }
+       }
+       my $pool = SVN::Pool->new;
+       my ($d, undef, $props) = $self->SUPER::get_dir($dir, $r, $pool);
+       my %dirents = map { $_ => { kind => $d->{$_}->kind } } keys %$d;
+       $pool->clear;
+       if ($r != $cache->{r}) {
+               %{$cache->{data}} = ();
+               $cache->{r} = $r;
+       }
+       $cache->{data}->{$dir} = [ \%dirents, $r, $props ];
+       wantarray ? (\%dirents, $r, $props) : \%dirents;
+}
+
+sub DESTROY {
+       # do not call the real DESTROY since we store ourselves in $RA
+}
+
+sub get_log {
+       my ($self, @args) = @_;
+       my $pool = SVN::Pool->new;
+       splice(@args, 3, 1) if ($SVN::Core::VERSION le '1.2.0');
+       my $ret = $self->SUPER::get_log(@args, $pool);
+       $pool->clear;
+       $ret;
+}
+
+sub trees_match {
+       my ($self, $url1, $rev1, $url2, $rev2) = @_;
+       my $ctx = SVN::Client->new(auth => _auth_providers);
+       my $out = IO::File->new_tmpfile;
+
+       # older SVN (1.1.x) doesn't take $pool as the last parameter for
+       # $ctx->diff(), so we'll create a default one
+       my $pool = SVN::Pool->new_default_sub;
+
+       $ra_invalid = 1; # this will open a new SVN::Ra connection to $url1
+       $ctx->diff([], $url1, $rev1, $url2, $rev2, 1, 1, 0, $out, $out);
+       $out->flush;
+       my $ret = (($out->stat)[7] == 0);
+       close $out or croak $!;
+
+       $ret;
+}
+
+sub get_commit_editor {
+       my ($self, $log, $cb, $pool) = @_;
+       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
+       $self->SUPER::get_commit_editor($log, $cb, @lock, $pool);
+}
+
+sub gs_do_update {
+       my ($self, $rev_a, $rev_b, $gs, $editor) = @_;
+       my $new = ($rev_a == $rev_b);
+       my $path = $gs->{path};
+
+       if ($new && -e $gs->{index}) {
+               unlink $gs->{index} or die
+                 "Couldn't unlink index: $gs->{index}: $!\n";
+       }
+       my $pool = SVN::Pool->new;
+       $editor->set_path_strip($path);
+       my (@pc) = split m#/#, $path;
+       my $reporter = $self->do_update($rev_b, (@pc ? shift @pc : ''),
+                                       1, $editor, $pool);
+       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
+
+       # Since we can't rely on svn_ra_reparent being available, we'll
+       # just have to do some magic with set_path to make it so
+       # we only want a partial path.
+       my $sp = '';
+       my $final = join('/', @pc);
+       while (@pc) {
+               $reporter->set_path($sp, $rev_b, 0, @lock, $pool);
+               $sp .= '/' if length $sp;
+               $sp .= shift @pc;
+       }
+       die "BUG: '$sp' != '$final'\n" if ($sp ne $final);
+
+       $reporter->set_path($sp, $rev_a, $new, @lock, $pool);
+
+       $reporter->finish_report($pool);
+       $pool->clear;
+       $editor->{git_commit_ok};
+}
+
+# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and
+# svn_ra_reparent didn't work before 1.4)
+sub gs_do_switch {
+       my ($self, $rev_a, $rev_b, $gs, $url_b, $editor) = @_;
+       my $path = $gs->{path};
+       my $pool = SVN::Pool->new;
+
+       my $full_url = $self->{url};
+       my $old_url = $full_url;
+       $full_url .= '/' . escape_uri_only($path) if length $path;
+       my ($ra, $reparented);
+       if ($old_url ne $full_url) {
+               if ($old_url !~ m#^svn(\+ssh)?://#) {
+                       SVN::_Ra::svn_ra_reparent($self->{session}, $full_url,
+                                                 $pool);
+                       $self->{url} = $full_url;
+                       $reparented = 1;
+               } else {
+                       $_[0] = undef;
+                       $self = undef;
+                       $RA = undef;
+                       $ra = Git::SVN::Ra->new($full_url);
+                       $ra_invalid = 1;
+               }
+       }
+       $ra ||= $self;
+       my $reporter = $ra->do_switch($rev_b, '', 1, $url_b, $editor, $pool);
+       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
+       $reporter->set_path('', $rev_a, 0, @lock, $pool);
+       $reporter->finish_report($pool);
+
+       if ($reparented) {
+               SVN::_Ra::svn_ra_reparent($self->{session}, $old_url, $pool);
+               $self->{url} = $old_url;
+       }
+
+       $pool->clear;
+       $editor->{git_commit_ok};
+}
+
+sub longest_common_path {
+       my ($gsv, $globs) = @_;
+       my %common;
+       my $common_max = scalar @$gsv;
+
+       foreach my $gs (@$gsv) {
+               my @tmp = split m#/#, $gs->{path};
+               my $p = '';
+               foreach (@tmp) {
+                       $p .= length($p) ? "/$_" : $_;
+                       $common{$p} ||= 0;
+                       $common{$p}++;
+               }
+       }
+       $globs ||= [];
+       $common_max += scalar @$globs;
+       foreach my $glob (@$globs) {
+               my @tmp = split m#/#, $glob->{path}->{left};
+               my $p = '';
+               foreach (@tmp) {
+                       $p .= length($p) ? "/$_" : $_;
+                       $common{$p} ||= 0;
+                       $common{$p}++;
+               }
+       }
+
+       my $longest_path = '';
+       foreach (sort {length $b <=> length $a} keys %common) {
+               if ($common{$_} == $common_max) {
+                       $longest_path = $_;
+                       last;
+               }
+       }
+       $longest_path;
+}
+
+sub gs_fetch_loop_common {
+       my ($self, $base, $head, $gsv, $globs) = @_;
+       return if ($base > $head);
+       my $inc = $_log_window_size;
+       my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc);
+       my $longest_path = longest_common_path($gsv, $globs);
+       my $ra_url = $self->{url};
+       while (1) {
+               my %revs;
+               my $err;
+               my $err_handler = $SVN::Error::handler;
+               $SVN::Error::handler = sub {
+                       ($err) = @_;
+                       skip_unknown_revs($err);
+               };
+               sub _cb {
+                       my ($paths, $r, $author, $date, $log) = @_;
+                       [ dup_changed_paths($paths),
+                         { author => $author, date => $date, log => $log } ];
+               }
+               $self->get_log([$longest_path], $min, $max, 0, 1, 1,
+                              sub { $revs{$_[1]} = _cb(@_) });
+               if ($err && $max >= $head) {
+                       print STDERR "Path '$longest_path' ",
+                                    "was probably deleted:\n",
+                                    $err->expanded_message,
+                                    "\nWill attempt to follow ",
+                                    "revisions r$min .. r$max ",
+                                    "committed before the deletion\n";
+                       my $hi = $max;
+                       while (--$hi >= $min) {
+                               my $ok;
+                               $self->get_log([$longest_path], $min, $hi,
+                                              0, 1, 1, sub {
+                                              $ok ||= $_[1];
+                                              $revs{$_[1]} = _cb(@_) });
+                               if ($ok) {
+                                       print STDERR "r$min .. r$ok OK\n";
+                                       last;
+                               }
+                       }
+               }
+               $SVN::Error::handler = $err_handler;
+
+               my %exists = map { $_->{path} => $_ } @$gsv;
+               foreach my $r (sort {$a <=> $b} keys %revs) {
+                       my ($paths, $logged) = @{$revs{$r}};
+
+                       foreach my $gs ($self->match_globs(\%exists, $paths,
+                                                          $globs, $r)) {
+                               if ($gs->rev_db_max >= $r) {
+                                       next;
+                               }
+                               next unless $gs->match_paths($paths, $r);
+                               $gs->{logged_rev_props} = $logged;
+                               if (my $last_commit = $gs->last_commit) {
+                                       $gs->assert_index_clean($last_commit);
+                               }
+                               my $log_entry = $gs->do_fetch($paths, $r);
+                               if ($log_entry) {
+                                       $gs->do_git_commit($log_entry);
+                               }
+                       }
+                       foreach my $g (@$globs) {
+                               my $k = "svn-remote.$g->{remote}." .
+                                       "$g->{t}-maxRev";
+                               Git::SVN::tmp_config($k, $r);
+                       }
+                       if ($ra_invalid) {
+                               $_[0] = undef;
+                               $self = undef;
+                               $RA = undef;
+                               $self = Git::SVN::Ra->new($ra_url);
+                               $ra_invalid = undef;
+                       }
+               }
+               # pre-fill the .rev_db since it'll eventually get filled in
+               # with '0' x40 if something new gets committed
+               foreach my $gs (@$gsv) {
+                       next if defined $gs->rev_db_get($max);
+                       $gs->rev_db_set($max, 0 x40);
+               }
+               foreach my $g (@$globs) {
+                       my $k = "svn-remote.$g->{remote}.$g->{t}-maxRev";
+                       Git::SVN::tmp_config($k, $max);
+               }
+               last if $max >= $head;
+               $min = $max + 1;
+               $max += $inc;
+               $max = $head if ($max > $head);
+       }
+}
+
+sub match_globs {
+       my ($self, $exists, $paths, $globs, $r) = @_;
+
+       sub get_dir_check {
+               my ($self, $exists, $g, $r) = @_;
+               my @x = eval { $self->get_dir($g->{path}->{left}, $r) };
+               return unless scalar @x == 3;
+               my $dirents = $x[0];
+               foreach my $de (keys %$dirents) {
+                       next if $dirents->{$de}->{kind} != $SVN::Node::dir;
+                       my $p = $g->{path}->full_path($de);
+                       next if $exists->{$p};
+                       next if (length $g->{path}->{right} &&
+                                ($self->check_path($p, $r) !=
+                                 $SVN::Node::dir));
+                       $exists->{$p} = Git::SVN->init($self->{url}, $p, undef,
+                                        $g->{ref}->full_path($de), 1);
+               }
+       }
+       foreach my $g (@$globs) {
+               if (my $path = $paths->{"/$g->{path}->{left}"}) {
+                       if ($path->{action} =~ /^[AR]$/) {
+                               get_dir_check($self, $exists, $g, $r);
+                       }
+               }
+               foreach (keys %$paths) {
+                       if (/$g->{path}->{left_regex}/ &&
+                           !/$g->{path}->{regex}/) {
+                               next if $paths->{$_}->{action} !~ /^[AR]$/;
+                               get_dir_check($self, $exists, $g, $r);
+                       }
+                       next unless /$g->{path}->{regex}/;
+                       my $p = $1;
+                       my $pathname = $g->{path}->full_path($p);
+                       next if $exists->{$pathname};
+                       next if ($self->check_path($pathname, $r) !=
+                                $SVN::Node::dir);
+                       $exists->{$pathname} = Git::SVN->init(
+                                             $self->{url}, $pathname, undef,
+                                             $g->{ref}->full_path($p), 1);
+               }
+               my $c = '';
+               foreach (split m#/#, $g->{path}->{left}) {
+                       $c .= "/$_";
+                       next unless ($paths->{$c} &&
+                                    ($paths->{$c}->{action} =~ /^[AR]$/));
+                       get_dir_check($self, $exists, $g, $r);
+               }
+       }
+       values %$exists;
+}
+
+sub minimize_url {
+       my ($self) = @_;
+       return $self->{url} if ($self->{url} eq $self->{repos_root});
+       my $url = $self->{repos_root};
+       my @components = split(m!/!, $self->{svn_path});
+       my $c = '';
+       do {
+               $url .= "/$c" if length $c;
+               eval { (ref $self)->new($url)->get_latest_revnum };
+       } while ($@ && ($c = shift @components));
+       $url;
+}
+
+sub can_do_switch {
+       my $self = shift;
+       unless (defined $can_do_switch) {
+               my $pool = SVN::Pool->new;
+               my $rep = eval {
+                       $self->do_switch(1, '', 0, $self->{url},
+                                        SVN::Delta::Editor->new, $pool);
+               };
+               if ($@) {
+                       $can_do_switch = 0;
+               } else {
+                       $rep->abort_report($pool);
+                       $can_do_switch = 1;
+               }
+               $pool->clear;
+       }
+       $can_do_switch;
+}
+
+sub skip_unknown_revs {
+       my ($err) = @_;
+       my $errno = $err->apr_err();
+       # Maybe the branch we're tracking didn't
+       # exist when the repo started, so it's
+       # not an error if it doesn't, just continue
+       #
+       # Wonderfully consistent library, eh?
+       # 160013 - svn:// and file://
+       # 175002 - http(s)://
+       # 175007 - http(s):// (this repo required authorization, too...)
+       #   More codes may be discovered later...
+       if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
+               my $err_key = $err->expanded_message;
+               # revision numbers change every time, filter them out
+               $err_key =~ s/\d+/\0/g;
+               $err_key = "$errno\0$err_key";
+               unless ($ignored_err{$err_key}) {
+                       warn "W: Ignoring error from SVN, path probably ",
+                            "does not exist: ($errno): ",
+                            $err->expanded_message,"\n";
+                       $ignored_err{$err_key} = 1;
+               }
+               return;
+       }
+       die "Error from SVN, ($errno): ", $err->expanded_message,"\n";
+}
+
+# svn_log_changed_path_t objects passed to get_log are likely to be
+# overwritten even if only the refs are copied to an external variable,
+# so we should dup the structures in their entirety.  Using an externally
+# passed pool (instead of our temporary and quickly cleared pool in
+# Git::SVN::Ra) does not help matters at all...
+sub dup_changed_paths {
+       my ($paths) = @_;
+       return undef unless $paths;
+       my %ret;
+       foreach my $p (keys %$paths) {
+               my $i = $paths->{$p};
+               my %s = map { $_ => $i->$_ }
+                             qw/copyfrom_path copyfrom_rev action/;
+               $ret{$p} = \%s;
+       }
+       \%ret;
+}
+
+package Git::SVN::Log;
+use strict;
+use warnings;
+use POSIX qw/strftime/;
+use constant commit_log_separator => ('-' x 72) . "\n";
+use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline
+            %rusers $show_commit $incremental/;
+my $l_fmt;
+
+sub cmt_showable {
+       my ($c) = @_;
+       return 1 if defined $c->{r};
+
+       # big commit message got truncated by the 16k pretty buffer in rev-list
+       if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
+                               $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
+               @{$c->{l}} = ();
+               my @log = command(qw/cat-file commit/, $c->{c});
+
+               # shift off the headers
+               shift @log while ($log[0] ne '');
+               shift @log;
+
+               # TODO: make $c->{l} not have a trailing newline in the future
+               @{$c->{l}} = map { "$_\n" } grep !/^git-svn-id: /, @log;
+
+               (undef, $c->{r}, undef) = ::extract_metadata(
+                               (grep(/^git-svn-id: /, @log))[-1]);
+       }
+       return defined $c->{r};
+}
+
+sub log_use_color {
+       return 1 if $color;
+       my ($dc, $dcvar);
+       $dcvar = 'color.diff';
+       $dc = `git-config --get $dcvar`;
+       if ($dc eq '') {
+               # nothing at all; fallback to "diff.color"
+               $dcvar = 'diff.color';
+               $dc = `git-config --get $dcvar`;
+       }
+       chomp($dc);
+       if ($dc eq 'auto') {
+               my $pc;
+               $pc = `git-config --get color.pager`;
+               if ($pc eq '') {
+                       # does not have it -- fallback to pager.color
+                       $pc = `git-config --bool --get pager.color`;
+               }
+               else {
+                       $pc = `git-config --bool --get color.pager`;
+                       if ($?) {
+                               $pc = 'false';
+                       }
+               }
+               chomp($pc);
+               if (-t *STDOUT || (defined $pager && $pc eq 'true')) {
+                       return ($ENV{TERM} && $ENV{TERM} ne 'dumb');
+               }
+               return 0;
+       }
+       return 0 if $dc eq 'never';
+       return 1 if $dc eq 'always';
+       chomp($dc = `git-config --bool --get $dcvar`);
+       return ($dc eq 'true');
+}
+
+sub git_svn_log_cmd {
+       my ($r_min, $r_max, @args) = @_;
+       my $head = 'HEAD';
+       my (@files, @log_opts);
+       foreach my $x (@args) {
+               if ($x eq '--' || @files) {
+                       push @files, $x;
+               } else {
+                       if (::verify_ref("$x^0")) {
+                               $head = $x;
+                       } else {
+                               push @log_opts, $x;
+                       }
+               }
+       }
+
+       my ($url, $rev, $uuid, $gs) = ::working_head_info($head);
+       $gs ||= Git::SVN->_new;
+       my @cmd = (qw/log --abbrev-commit --pretty=raw --default/,
+                  $gs->refname);
+       push @cmd, '-r' unless $non_recursive;
+       push @cmd, qw/--raw --name-status/ if $verbose;
+       push @cmd, '--color' if log_use_color();
+       push @cmd, @log_opts;
+       if (defined $r_max && $r_max == $r_min) {
+               push @cmd, '--max-count=1';
+               if (my $c = $gs->rev_db_get($r_max)) {
+                       push @cmd, $c;
+               }
+       } elsif (defined $r_max) {
+               if ($r_max < $r_min) {
+                       ($r_min, $r_max) = ($r_max, $r_min);
+               }
+               my (undef, $c_max) = $gs->find_rev_before($r_max, 1, $r_min);
+               my (undef, $c_min) = $gs->find_rev_after($r_min, 1, $r_max);
+               # If there are no commits in the range, both $c_max and $c_min
+               # will be undefined.  If there is at least 1 commit in the
+               # range, both will be defined.
+               return () if !defined $c_min || !defined $c_max;
+               if ($c_min eq $c_max) {
+                       push @cmd, '--max-count=1', $c_min;
+               } else {
+                       push @cmd, '--boundary', "$c_min..$c_max";
+               }
+       }
+       return (@cmd, @files);
+}
+
+# adapted from pager.c
+sub config_pager {
+       $pager ||= $ENV{GIT_PAGER} || $ENV{PAGER};
+       if (!defined $pager) {
+               $pager = 'less';
+       } elsif (length $pager == 0 || $pager eq 'cat') {
+               $pager = undef;
+       }
+}
+
+sub run_pager {
+       return unless -t *STDOUT && defined $pager;
+       pipe my $rfd, my $wfd or return;
+       defined(my $pid = fork) or ::fatal "Can't fork: $!";
+       if (!$pid) {
+               open STDOUT, '>&', $wfd or
+                                    ::fatal "Can't redirect to stdout: $!";
+               return;
+       }
+       open STDIN, '<&', $rfd or ::fatal "Can't redirect stdin: $!";
+       $ENV{LESS} ||= 'FRSX';
+       exec $pager or ::fatal "Can't run pager: $! ($pager)";
+}
+
+sub format_svn_date {
+       return strftime("%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)", localtime(shift));
+}
+
+sub parse_git_date {
+       my ($t, $tz) = @_;
+       # Date::Parse isn't in the standard Perl distro :(
+       if ($tz =~ s/^\+//) {
+               $t += tz_to_s_offset($tz);
+       } elsif ($tz =~ s/^\-//) {
+               $t -= tz_to_s_offset($tz);
+       }
+       return $t;
+}
+
+sub set_local_timezone {
+       if (defined $TZ) {
+               $ENV{TZ} = $TZ;
+       } else {
+               delete $ENV{TZ};
+       }
+}
+
+sub tz_to_s_offset {
+       my ($tz) = @_;
+       $tz =~ s/(\d\d)$//;
+       return ($1 * 60) + ($tz * 3600);
+}
+
+sub get_author_info {
+       my ($dest, $author, $t, $tz) = @_;
+       $author =~ s/(?:^\s*|\s*$)//g;
+       $dest->{a_raw} = $author;
+       my $au;
+       if ($::_authors) {
+               $au = $rusers{$author} || undef;
+       }
+       if (!$au) {
+               ($au) = ($author =~ /<([^>]+)\@[^>]+>$/);
+       }
+       $dest->{t} = $t;
+       $dest->{tz} = $tz;
+       $dest->{a} = $au;
+       $dest->{t_utc} = parse_git_date($t, $tz);
+}
+
+sub process_commit {
+       my ($c, $r_min, $r_max, $defer) = @_;
+       if (defined $r_min && defined $r_max) {
+               if ($r_min == $c->{r} && $r_min == $r_max) {
+                       show_commit($c);
+                       return 0;
+               }
+               return 1 if $r_min == $r_max;
+               if ($r_min < $r_max) {
+                       # we need to reverse the print order
+                       return 0 if (defined $limit && --$limit < 0);
+                       push @$defer, $c;
+                       return 1;
+               }
+               if ($r_min != $r_max) {
+                       return 1 if ($r_min < $c->{r});
+                       return 1 if ($r_max > $c->{r});
+               }
+       }
+       return 0 if (defined $limit && --$limit < 0);
+       show_commit($c);
+       return 1;
+}
+
+sub show_commit {
+       my $c = shift;
+       if ($oneline) {
+               my $x = "\n";
+               if (my $l = $c->{l}) {
+                       while ($l->[0] =~ /^\s*$/) { shift @$l }
+                       $x = $l->[0];
+               }
+               $l_fmt ||= 'A' . length($c->{r});
+               print 'r',pack($l_fmt, $c->{r}),' | ';
+               print "$c->{c} | " if $show_commit;
+               print $x;
+       } else {
+               show_commit_normal($c);
+       }
+}
+
+sub show_commit_changed_paths {
+       my ($c) = @_;
+       return unless $c->{changed};
+       print "Changed paths:\n", @{$c->{changed}};
+}
+
+sub show_commit_normal {
+       my ($c) = @_;
+       print commit_log_separator, "r$c->{r} | ";
+       print "$c->{c} | " if $show_commit;
+       print "$c->{a} | ", format_svn_date($c->{t_utc}), ' | ';
+       my $nr_line = 0;
+
+       if (my $l = $c->{l}) {
+               while ($l->[$#$l] eq "\n" && $#$l > 0
+                                         && $l->[($#$l - 1)] eq "\n") {
+                       pop @$l;
+               }
+               $nr_line = scalar @$l;
+               if (!$nr_line) {
+                       print "1 line\n\n\n";
+               } else {
+                       if ($nr_line == 1) {
+                               $nr_line = '1 line';
+                       } else {
+                               $nr_line .= ' lines';
+                       }
+                       print $nr_line, "\n";
+                       show_commit_changed_paths($c);
+                       print "\n";
+                       print $_ foreach @$l;
+               }
+       } else {
+               print "1 line\n";
+               show_commit_changed_paths($c);
+               print "\n";
+
+       }
+       foreach my $x (qw/raw stat diff/) {
+               if ($c->{$x}) {
+                       print "\n";
+                       print $_ foreach @{$c->{$x}}
+               }
+       }
+}
+
+sub cmd_show_log {
+       my (@args) = @_;
+       my ($r_min, $r_max);
+       my $r_last = -1; # prevent dupes
+       set_local_timezone();
+       if (defined $::_revision) {
+               if ($::_revision =~ /^(\d+):(\d+)$/) {
+                       ($r_min, $r_max) = ($1, $2);
+               } elsif ($::_revision =~ /^\d+$/) {
+                       $r_min = $r_max = $::_revision;
+               } else {
+                       ::fatal "-r$::_revision is not supported, use ",
+                               "standard 'git log' arguments instead";
+               }
+       }
+
+       config_pager();
+       @args = git_svn_log_cmd($r_min, $r_max, @args);
+       if (!@args) {
+               print commit_log_separator unless $incremental || $oneline;
+               return;
+       }
+       my $log = command_output_pipe(@args);
+       run_pager();
+       my (@k, $c, $d, $stat);
+       my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
+       while (<$log>) {
+               if (/^${esc_color}commit -?($::sha1_short)/o) {
+                       my $cmt = $1;
+                       if ($c && cmt_showable($c) && $c->{r} != $r_last) {
+                               $r_last = $c->{r};
+                               process_commit($c, $r_min, $r_max, \@k) or
+                                                               goto out;
+                       }
+                       $d = undef;
+                       $c = { c => $cmt };
+               } elsif (/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {
+                       get_author_info($c, $1, $2, $3);
+               } elsif (/^${esc_color}(?:tree|parent|committer) /o) {
+                       # ignore
+               } elsif (/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {
+                       push @{$c->{raw}}, $_;
+               } elsif (/^${esc_color}[ACRMDT]\t/) {
+                       # we could add $SVN->{svn_path} here, but that requires
+                       # remote access at the moment (repo_path_split)...
+                       s#^(${esc_color})([ACRMDT])\t#$1   $2 #o;
+                       push @{$c->{changed}}, $_;
+               } elsif (/^${esc_color}diff /o) {
+                       $d = 1;
+                       push @{$c->{diff}}, $_;
+               } elsif ($d) {
+                       push @{$c->{diff}}, $_;
+               } elsif (/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*
+                         $esc_color*[\+\-]*$esc_color$/x) {
+                       $stat = 1;
+                       push @{$c->{stat}}, $_;
+               } elsif ($stat && /^ \d+ files changed, \d+ insertions/) {
+                       push @{$c->{stat}}, $_;
+                       $stat = undef;
+               } elsif (/^${esc_color}    (git-svn-id:.+)$/o) {
+                       ($c->{url}, $c->{r}, undef) = ::extract_metadata($1);
+               } elsif (s/^${esc_color}    //o) {
+                       push @{$c->{l}}, $_;
+               }
+       }
+       if ($c && defined $c->{r} && $c->{r} != $r_last) {
+               $r_last = $c->{r};
+               process_commit($c, $r_min, $r_max, \@k);
+       }
+       if (@k) {
+               ($r_min, $r_max) = ($r_max, $r_min);
+               process_commit($_, $r_min, $r_max) foreach reverse @k;
+       }
+out:
+       close $log;
+       print commit_log_separator unless $incremental || $oneline;
+}
+
+package Git::SVN::Migration;
+# these version numbers do NOT correspond to actual version numbers
+# of git nor git-svn.  They are just relative.
+#
+# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD
+#
+# v1 layout: .git/$id/info/url, refs/remotes/$id
+#
+# v2 layout: .git/svn/$id/info/url, refs/remotes/$id
+#
+# v3 layout: .git/svn/$id, refs/remotes/$id
+#            - info/url may remain for backwards compatibility
+#            - this is what we migrate up to this layout automatically,
+#            - this will be used by git svn init on single branches
+# v3.1 layout (auto migrated):
+#            - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink
+#              for backwards compatibility
+#
+# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id
+#            - this is only created for newly multi-init-ed
+#              repositories.  Similar in spirit to the
+#              --use-separate-remotes option in git-clone (now default)
+#            - we do not automatically migrate to this (following
+#              the example set by core git)
+use strict;
+use warnings;
+use Carp qw/croak/;
+use File::Path qw/mkpath/;
+use File::Basename qw/dirname basename/;
+use vars qw/$_minimize/;
+
+sub migrate_from_v0 {
+       my $git_dir = $ENV{GIT_DIR};
+       return undef unless -d $git_dir;
+       my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);
+       my $migrated = 0;
+       while (<$fh>) {
+               chomp;
+               my ($id, $orig_ref) = ($_, $_);
+               next unless $id =~ s#^refs/heads/(.+)-HEAD$#$1#;
+               next unless -f "$git_dir/$id/info/url";
+               my $new_ref = "refs/remotes/$id";
+               if (::verify_ref("$new_ref^0")) {
+                       print STDERR "W: $orig_ref is probably an old ",
+                                    "branch used by an ancient version of ",
+                                    "git-svn.\n",
+                                    "However, $new_ref also exists.\n",
+                                    "We will not be able ",
+                                    "to use this branch until this ",
+                                    "ambiguity is resolved.\n";
+                       next;
+               }
+               print STDERR "Migrating from v0 layout...\n" if !$migrated;
+               print STDERR "Renaming ref: $orig_ref => $new_ref\n";
+               command_noisy('update-ref', $new_ref, $orig_ref);
+               command_noisy('update-ref', '-d', $orig_ref, $orig_ref);
+               $migrated++;
+       }
+       command_close_pipe($fh, $ctx);
+       print STDERR "Done migrating from v0 layout...\n" if $migrated;
+       $migrated;
+}
+
+sub migrate_from_v1 {
+       my $git_dir = $ENV{GIT_DIR};
+       my $migrated = 0;
+       return $migrated unless -d $git_dir;
+       my $svn_dir = "$git_dir/svn";
+
+       # just in case somebody used 'svn' as their $id at some point...
+       return $migrated if -d $svn_dir && ! -f "$svn_dir/info/url";
+
+       print STDERR "Migrating from a git-svn v1 layout...\n";
+       mkpath([$svn_dir]);
+       print STDERR "Data from a previous version of git-svn exists, but\n\t",
+                    "$svn_dir\n\t(required for this version ",
+                    "($::VERSION) of git-svn) does not. exist\n";
+       my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);
+       while (<$fh>) {
+               my $x = $_;
+               next unless $x =~ s#^refs/remotes/##;
+               chomp $x;
+               next unless -f "$git_dir/$x/info/url";
+               my $u = eval { ::file_to_s("$git_dir/$x/info/url") };
+               next unless $u;
+               my $dn = dirname("$git_dir/svn/$x");
+               mkpath([$dn]) unless -d $dn;
+               if ($x eq 'svn') { # they used 'svn' as GIT_SVN_ID:
+                       mkpath(["$git_dir/svn/svn"]);
+                       print STDERR " - $git_dir/$x/info => ",
+                                       "$git_dir/svn/$x/info\n";
+                       rename "$git_dir/$x/info", "$git_dir/svn/$x/info" or
+                              croak "$!: $x";
+                       # don't worry too much about these, they probably
+                       # don't exist with repos this old (save for index,
+                       # and we can easily regenerate that)
+                       foreach my $f (qw/unhandled.log index .rev_db/) {
+                               rename "$git_dir/$x/$f", "$git_dir/svn/$x/$f";
+                       }
+               } else {
+                       print STDERR " - $git_dir/$x => $git_dir/svn/$x\n";
+                       rename "$git_dir/$x", "$git_dir/svn/$x" or
+                              croak "$!: $x";
+               }
+               $migrated++;
+       }
+       command_close_pipe($fh, $ctx);
+       print STDERR "Done migrating from a git-svn v1 layout\n";
+       $migrated;
+}
+
+sub read_old_urls {
+       my ($l_map, $pfx, $path) = @_;
+       my @dir;
+       foreach (<$path/*>) {
+               if (-r "$_/info/url") {
+                       $pfx .= '/' if $pfx && $pfx !~ m!/$!;
+                       my $ref_id = $pfx . basename $_;
+                       my $url = ::file_to_s("$_/info/url");
+                       $l_map->{$ref_id} = $url;
+               } elsif (-d $_) {
+                       push @dir, $_;
+               }
+       }
+       foreach (@dir) {
+               my $x = $_;
+               $x =~ s!^\Q$ENV{GIT_DIR}\E/svn/!!o;
+               read_old_urls($l_map, $x, $_);
+       }
+}
+
+sub migrate_from_v2 {
+       my @cfg = command(qw/config -l/);
+       return if grep /^svn-remote\..+\.url=/, @cfg;
+       my %l_map;
+       read_old_urls(\%l_map, '', "$ENV{GIT_DIR}/svn");
+       my $migrated = 0;
+
+       foreach my $ref_id (sort keys %l_map) {
+               eval { Git::SVN->init($l_map{$ref_id}, '', undef, $ref_id) };
+               if ($@) {
+                       Git::SVN->init($l_map{$ref_id}, '', $ref_id, $ref_id);
+               }
+               $migrated++;
+       }
+       $migrated;
+}
+
+sub minimize_connections {
+       my $r = Git::SVN::read_all_remotes();
+       my $new_urls = {};
+       my $root_repos = {};
+       foreach my $repo_id (keys %$r) {
+               my $url = $r->{$repo_id}->{url} or next;
+               my $fetch = $r->{$repo_id}->{fetch} or next;
+               my $ra = Git::SVN::Ra->new($url);
+
+               # skip existing cases where we already connect to the root
+               if (($ra->{url} eq $ra->{repos_root}) ||
+                   (Git::SVN::sanitize_remote_name($ra->{repos_root}) eq
+                    $repo_id)) {
+                       $root_repos->{$ra->{url}} = $repo_id;
+                       next;
+               }
+
+               my $root_ra = Git::SVN::Ra->new($ra->{repos_root});
+               my $root_path = $ra->{url};
+               $root_path =~ s#^\Q$ra->{repos_root}\E(/|$)##;
+               foreach my $path (keys %$fetch) {
+                       my $ref_id = $fetch->{$path};
+                       my $gs = Git::SVN->new($ref_id, $repo_id, $path);
+
+                       # make sure we can read when connecting to
+                       # a higher level of a repository
+                       my ($last_rev, undef) = $gs->last_rev_commit;
+                       if (!defined $last_rev) {
+                               $last_rev = eval {
+                                       $root_ra->get_latest_revnum;
+                               };
+                               next if $@;
+                       }
+                       my $new = $root_path;
+                       $new .= length $path ? "/$path" : '';
+                       eval {
+                               $root_ra->get_log([$new], $last_rev, $last_rev,
+                                                 0, 0, 1, sub { });
+                       };
+                       next if $@;
+                       $new_urls->{$ra->{repos_root}}->{$new} =
+                               { ref_id => $ref_id,
+                                 old_repo_id => $repo_id,
+                                 old_path => $path };
+               }
+       }
+
+       my @emptied;
+       foreach my $url (keys %$new_urls) {
+               # see if we can re-use an existing [svn-remote "repo_id"]
+               # instead of creating a(n ugly) new section:
+               my $repo_id = $root_repos->{$url} ||
+                             Git::SVN::sanitize_remote_name($url);
+
+               my $fetch = $new_urls->{$url};
+               foreach my $path (keys %$fetch) {
+                       my $x = $fetch->{$path};
+                       Git::SVN->init($url, $path, $repo_id, $x->{ref_id});
+                       my $pfx = "svn-remote.$x->{old_repo_id}";
+
+                       my $old_fetch = quotemeta("$x->{old_path}:".
+                                                 "refs/remotes/$x->{ref_id}");
+                       command_noisy(qw/config --unset/,
+                                     "$pfx.fetch", '^'. $old_fetch . '$');
+                       delete $r->{$x->{old_repo_id}}->
+                              {fetch}->{$x->{old_path}};
+                       if (!keys %{$r->{$x->{old_repo_id}}->{fetch}}) {
+                               command_noisy(qw/config --unset/,
+                                             "$pfx.url");
+                               push @emptied, $x->{old_repo_id}
+                       }
+               }
+       }
+       if (@emptied) {
+               my $file = $ENV{GIT_CONFIG} || $ENV{GIT_CONFIG_LOCAL} ||
+                          "$ENV{GIT_DIR}/config";
+               print STDERR <<EOF;
+The following [svn-remote] sections in your config file ($file) are empty
+and can be safely removed:
+EOF
+               print STDERR "[svn-remote \"$_\"]\n" foreach @emptied;
+       }
+}
+
+sub migration_check {
+       migrate_from_v0();
+       migrate_from_v1();
+       migrate_from_v2();
+       minimize_connections() if $_minimize;
+}
+
+package Git::IndexInfo;
+use strict;
+use warnings;
+use Git qw/command_input_pipe command_close_pipe/;
+
+sub new {
+       my ($class) = @_;
+       my ($gui, $ctx) = command_input_pipe(qw/update-index -z --index-info/);
+       bless { gui => $gui, ctx => $ctx, nr => 0}, $class;
+}
+
+sub remove {
+       my ($self, $path) = @_;
+       if (print { $self->{gui} } '0 ', 0 x 40, "\t", $path, "\0") {
+               return ++$self->{nr};
+       }
+       undef;
+}
+
+sub update {
+       my ($self, $mode, $hash, $path) = @_;
+       if (print { $self->{gui} } $mode, ' ', $hash, "\t", $path, "\0") {
+               return ++$self->{nr};
+       }
+       undef;
+}
+
+sub DESTROY {
+       my ($self) = @_;
+       command_close_pipe($self->{gui}, $self->{ctx});
+}
+
+package Git::SVN::GlobSpec;
+use strict;
+use warnings;
+
+sub new {
+       my ($class, $glob) = @_;
+       my $re = $glob;
+       $re =~ s!/+$!!g; # no need for trailing slashes
+       my $nr = ($re =~ s!^(.*)\*(.*)$!\(\[^/\]+\)!g);
+       my ($left, $right) = ($1, $2);
+       if ($nr > 1) {
+               die "Only one '*' wildcard expansion ",
+                   "is supported (got $nr): '$glob'\n";
+       } elsif ($nr == 0) {
+               die "One '*' is needed for glob: '$glob'\n";
+       }
+       $re = quotemeta($left) . $re . quotemeta($right);
+       if (length $left && !($left =~ s!/+$!!g)) {
+               die "Missing trailing '/' on left side of: '$glob' ($left)\n";
+       }
+       if (length $right && !($right =~ s!^/+!!g)) {
+               die "Missing leading '/' on right side of: '$glob' ($right)\n";
+       }
+       my $left_re = qr/^\/\Q$left\E(\/|$)/;
+       bless { left => $left, right => $right, left_regex => $left_re,
+               regex => qr/$re/, glob => $glob }, $class;
+}
+
+sub full_path {
+       my ($self, $path) = @_;
+       return (length $self->{left} ? "$self->{left}/" : '') .
+              $path . (length $self->{right} ? "/$self->{right}" : '');
+}
+
+__END__
+
+Data structures:
+
+
+$remotes = { # returned by read_all_remotes()
+       'svn' => {
+               # svn-remote.svn.url=https://svn.musicpd.org
+               url => 'https://svn.musicpd.org',
+               # svn-remote.svn.fetch=mpd/trunk:trunk
+               fetch => {
+                       'mpd/trunk' => 'trunk',
+               },
+               # svn-remote.svn.tags=mpd/tags/*:tags/*
+               tags => {
+                       path => {
+                               left => 'mpd/tags',
+                               right => '',
+                               regex => qr!mpd/tags/([^/]+)$!,
+                               glob => 'tags/*',
+                       },
+                       ref => {
+                               left => 'tags',
+                               right => '',
+                               regex => qr!tags/([^/]+)$!,
+                               glob => 'tags/*',
+                       },
+               }
+       }
+};
+
+$log_entry hashref as returned by libsvn_log_entry()
+{
+       log => 'whitespace-formatted log entry
+',                                             # trailing newline is preserved
+       revision => '8',                        # integer
+       date => '2004-02-24T17:01:44.108345Z',  # commit date
+       author => 'committer name'
+};
+
+
+# this is generated by generate_diff();
+@mods = array of diff-index line hashes, each element represents one line
+       of diff-index output
+
+diff-index line ($m hash)
+{
+       mode_a => first column of diff-index output, no leading ':',
+       mode_b => second column of diff-index output,
+       sha1_b => sha1sum of the final blob,
+       chg => change type [MCRADT],
+       file_a => original file name of a file (iff chg is 'C' or 'R')
+       file_b => new/current file name of a file (any chg)
+}
+;
+
+# retval of read_url_paths{,_all}();
+$l_map = {
+       # repository root url
+       'https://svn.musicpd.org' => {
+               # repository path               # GIT_SVN_ID
+               'mpd/trunk'             =>      'trunk',
+               'mpd/tags/0.11.5'       =>      'tags/0.11.5',
+       },
+}
+
+Notes:
+       I don't trust the each() function on unless I created %hash myself
+       because the internal iterator may not have started at base.
diff --git a/git.c b/git.c
new file mode 100644 (file)
index 0000000..15fec89
--- /dev/null
+++ b/git.c
@@ -0,0 +1,475 @@
+#include "builtin.h"
+#include "exec_cmd.h"
+#include "cache.h"
+#include "quote.h"
+
+const char git_usage_string[] =
+       "git [--version] [--exec-path[=GIT_EXEC_PATH]] [-p|--paginate|--no-pager] [--bare] [--git-dir=GIT_DIR] [--work-tree=GIT_WORK_TREE] [--help] COMMAND [ARGS]";
+
+static int handle_options(const char*** argv, int* argc, int* envchanged)
+{
+       int handled = 0;
+
+       while (*argc > 0) {
+               const char *cmd = (*argv)[0];
+               if (cmd[0] != '-')
+                       break;
+
+               /*
+                * For legacy reasons, the "version" and "help"
+                * commands can be written with "--" prepended
+                * to make them look like flags.
+                */
+               if (!strcmp(cmd, "--help") || !strcmp(cmd, "--version"))
+                       break;
+
+               /*
+                * Check remaining flags.
+                */
+               if (!prefixcmp(cmd, "--exec-path")) {
+                       cmd += 11;
+                       if (*cmd == '=')
+                               git_set_argv_exec_path(cmd + 1);
+                       else {
+                               puts(git_exec_path());
+                               exit(0);
+                       }
+               } else if (!strcmp(cmd, "-p") || !strcmp(cmd, "--paginate")) {
+                       setup_pager();
+               } else if (!strcmp(cmd, "--no-pager")) {
+                       setenv("GIT_PAGER", "cat", 1);
+                       if (envchanged)
+                               *envchanged = 1;
+               } else if (!strcmp(cmd, "--git-dir")) {
+                       if (*argc < 2) {
+                               fprintf(stderr, "No directory given for --git-dir.\n" );
+                               usage(git_usage_string);
+                       }
+                       setenv(GIT_DIR_ENVIRONMENT, (*argv)[1], 1);
+                       if (envchanged)
+                               *envchanged = 1;
+                       (*argv)++;
+                       (*argc)--;
+                       handled++;
+               } else if (!prefixcmp(cmd, "--git-dir=")) {
+                       setenv(GIT_DIR_ENVIRONMENT, cmd + 10, 1);
+                       if (envchanged)
+                               *envchanged = 1;
+               } else if (!strcmp(cmd, "--work-tree")) {
+                       if (*argc < 2) {
+                               fprintf(stderr, "No directory given for --work-tree.\n" );
+                               usage(git_usage_string);
+                       }
+                       setenv(GIT_WORK_TREE_ENVIRONMENT, (*argv)[1], 1);
+                       if (envchanged)
+                               *envchanged = 1;
+                       (*argv)++;
+                       (*argc)--;
+               } else if (!prefixcmp(cmd, "--work-tree=")) {
+                       setenv(GIT_WORK_TREE_ENVIRONMENT, cmd + 12, 1);
+                       if (envchanged)
+                               *envchanged = 1;
+               } else if (!strcmp(cmd, "--bare")) {
+                       static char git_dir[PATH_MAX+1];
+                       is_bare_repository_cfg = 1;
+                       setenv(GIT_DIR_ENVIRONMENT, getcwd(git_dir, sizeof(git_dir)), 0);
+                       if (envchanged)
+                               *envchanged = 1;
+               } else {
+                       fprintf(stderr, "Unknown option: %s\n", cmd);
+                       usage(git_usage_string);
+               }
+
+               (*argv)++;
+               (*argc)--;
+               handled++;
+       }
+       return handled;
+}
+
+static const char *alias_command;
+static char *alias_string;
+
+static int git_alias_config(const char *var, const char *value)
+{
+       if (!prefixcmp(var, "alias.") && !strcmp(var + 6, alias_command)) {
+               alias_string = xstrdup(value);
+       }
+       return 0;
+}
+
+static int split_cmdline(char *cmdline, const char ***argv)
+{
+       int src, dst, count = 0, size = 16;
+       char quoted = 0;
+
+       *argv = xmalloc(sizeof(char*) * size);
+
+       /* split alias_string */
+       (*argv)[count++] = cmdline;
+       for (src = dst = 0; cmdline[src];) {
+               char c = cmdline[src];
+               if (!quoted && isspace(c)) {
+                       cmdline[dst++] = 0;
+                       while (cmdline[++src]
+                                       && isspace(cmdline[src]))
+                               ; /* skip */
+                       if (count >= size) {
+                               size += 16;
+                               *argv = xrealloc(*argv, sizeof(char*) * size);
+                       }
+                       (*argv)[count++] = cmdline + dst;
+               } else if(!quoted && (c == '\'' || c == '"')) {
+                       quoted = c;
+                       src++;
+               } else if (c == quoted) {
+                       quoted = 0;
+                       src++;
+               } else {
+                       if (c == '\\' && quoted != '\'') {
+                               src++;
+                               c = cmdline[src];
+                               if (!c) {
+                                       free(*argv);
+                                       *argv = NULL;
+                                       return error("cmdline ends with \\");
+                               }
+                       }
+                       cmdline[dst++] = c;
+                       src++;
+               }
+       }
+
+       cmdline[dst] = 0;
+
+       if (quoted) {
+               free(*argv);
+               *argv = NULL;
+               return error("unclosed quote");
+       }
+
+       return count;
+}
+
+static int handle_alias(int *argcp, const char ***argv)
+{
+       int nongit = 0, envchanged = 0, ret = 0, saved_errno = errno;
+       const char *subdir;
+       int count, option_count;
+       const char** new_argv;
+
+       subdir = setup_git_directory_gently(&nongit);
+
+       alias_command = (*argv)[0];
+       git_config(git_alias_config);
+       if (alias_string) {
+               if (alias_string[0] == '!') {
+                       if (*argcp > 1) {
+                               struct strbuf buf;
+
+                               strbuf_init(&buf, PATH_MAX);
+                               strbuf_addstr(&buf, alias_string);
+                               sq_quote_argv(&buf, (*argv) + 1, PATH_MAX);
+                               free(alias_string);
+                               alias_string = buf.buf;
+                       }
+                       trace_printf("trace: alias to shell cmd: %s => %s\n",
+                                    alias_command, alias_string + 1);
+                       ret = system(alias_string + 1);
+                       if (ret >= 0 && WIFEXITED(ret) &&
+                           WEXITSTATUS(ret) != 127)
+                               exit(WEXITSTATUS(ret));
+                       die("Failed to run '%s' when expanding alias '%s'\n",
+                           alias_string + 1, alias_command);
+               }
+               count = split_cmdline(alias_string, &new_argv);
+               option_count = handle_options(&new_argv, &count, &envchanged);
+               if (envchanged)
+                       die("alias '%s' changes environment variables\n"
+                                "You can use '!git' in the alias to do this.",
+                                alias_command);
+               memmove(new_argv - option_count, new_argv,
+                               count * sizeof(char *));
+               new_argv -= option_count;
+
+               if (count < 1)
+                       die("empty alias for %s", alias_command);
+
+               if (!strcmp(alias_command, new_argv[0]))
+                       die("recursive alias: %s", alias_command);
+
+               trace_argv_printf(new_argv,
+                                 "trace: alias expansion: %s =>",
+                                 alias_command);
+
+               new_argv = xrealloc(new_argv, sizeof(char*) *
+                                   (count + *argcp + 1));
+               /* insert after command name */
+               memcpy(new_argv + count, *argv + 1, sizeof(char*) * *argcp);
+               new_argv[count+*argcp] = NULL;
+
+               *argv = new_argv;
+               *argcp += count - 1;
+
+               ret = 1;
+       }
+
+       if (subdir)
+               chdir(subdir);
+
+       errno = saved_errno;
+
+       return ret;
+}
+
+const char git_version_string[] = GIT_VERSION;
+
+#define RUN_SETUP      (1<<0)
+#define USE_PAGER      (1<<1)
+/*
+ * require working tree to be present -- anything uses this needs
+ * RUN_SETUP for reading from the configuration file.
+ */
+#define NEED_WORK_TREE (1<<2)
+
+struct cmd_struct {
+       const char *cmd;
+       int (*fn)(int, const char **, const char *);
+       int option;
+};
+
+static int run_command(struct cmd_struct *p, int argc, const char **argv)
+{
+       int status;
+       struct stat st;
+       const char *prefix;
+
+       prefix = NULL;
+       if (p->option & RUN_SETUP)
+               prefix = setup_git_directory();
+       if (p->option & USE_PAGER)
+               setup_pager();
+       if (p->option & NEED_WORK_TREE)
+               setup_work_tree();
+
+       trace_argv_printf(argv, "trace: built-in: git");
+
+       status = p->fn(argc, argv, prefix);
+       if (status)
+               return status & 0xff;
+
+       /* Somebody closed stdout? */
+       if (fstat(fileno(stdout), &st))
+               return 0;
+       /* Ignore write errors for pipes and sockets.. */
+       if (S_ISFIFO(st.st_mode) || S_ISSOCK(st.st_mode))
+               return 0;
+
+       /* Check for ENOSPC and EIO errors.. */
+       if (fflush(stdout))
+               die("write failure on standard output: %s", strerror(errno));
+       if (ferror(stdout))
+               die("unknown write failure on standard output");
+       if (fclose(stdout))
+               die("close failed on standard output: %s", strerror(errno));
+       return 0;
+}
+
+static void handle_internal_command(int argc, const char **argv)
+{
+       const char *cmd = argv[0];
+       static struct cmd_struct commands[] = {
+               { "add", cmd_add, RUN_SETUP | NEED_WORK_TREE },
+               { "annotate", cmd_annotate, RUN_SETUP },
+               { "apply", cmd_apply },
+               { "archive", cmd_archive },
+               { "blame", cmd_blame, RUN_SETUP },
+               { "branch", cmd_branch, RUN_SETUP },
+               { "bundle", cmd_bundle },
+               { "cat-file", cmd_cat_file, RUN_SETUP },
+               { "checkout-index", cmd_checkout_index,
+                       RUN_SETUP | NEED_WORK_TREE},
+               { "check-ref-format", cmd_check_ref_format },
+               { "check-attr", cmd_check_attr, RUN_SETUP | NEED_WORK_TREE },
+               { "cherry", cmd_cherry, RUN_SETUP },
+               { "cherry-pick", cmd_cherry_pick, RUN_SETUP | NEED_WORK_TREE },
+               { "clean", cmd_clean, RUN_SETUP | NEED_WORK_TREE },
+               { "commit", cmd_commit, RUN_SETUP | NEED_WORK_TREE },
+               { "commit-tree", cmd_commit_tree, RUN_SETUP },
+               { "config", cmd_config },
+               { "count-objects", cmd_count_objects, RUN_SETUP },
+               { "describe", cmd_describe, RUN_SETUP },
+               { "diff", cmd_diff },
+               { "diff-files", cmd_diff_files },
+               { "diff-index", cmd_diff_index, RUN_SETUP },
+               { "diff-tree", cmd_diff_tree, RUN_SETUP },
+               { "fast-export", cmd_fast_export, RUN_SETUP },
+               { "fetch", cmd_fetch, RUN_SETUP },
+               { "fetch-pack", cmd_fetch_pack, RUN_SETUP },
+               { "fetch--tool", cmd_fetch__tool, RUN_SETUP },
+               { "fmt-merge-msg", cmd_fmt_merge_msg, RUN_SETUP },
+               { "for-each-ref", cmd_for_each_ref, RUN_SETUP },
+               { "format-patch", cmd_format_patch, RUN_SETUP },
+               { "fsck", cmd_fsck, RUN_SETUP },
+               { "fsck-objects", cmd_fsck, RUN_SETUP },
+               { "gc", cmd_gc, RUN_SETUP },
+               { "get-tar-commit-id", cmd_get_tar_commit_id },
+               { "grep", cmd_grep, RUN_SETUP | USE_PAGER },
+               { "help", cmd_help },
+#ifndef NO_CURL
+               { "http-fetch", cmd_http_fetch, RUN_SETUP },
+#endif
+               { "init", cmd_init_db },
+               { "init-db", cmd_init_db },
+               { "log", cmd_log, RUN_SETUP | USE_PAGER },
+               { "ls-files", cmd_ls_files, RUN_SETUP },
+               { "ls-tree", cmd_ls_tree, RUN_SETUP },
+               { "ls-remote", cmd_ls_remote },
+               { "mailinfo", cmd_mailinfo },
+               { "mailsplit", cmd_mailsplit },
+               { "merge-base", cmd_merge_base, RUN_SETUP },
+               { "merge-file", cmd_merge_file },
+               { "merge-ours", cmd_merge_ours, RUN_SETUP },
+               { "mv", cmd_mv, RUN_SETUP | NEED_WORK_TREE },
+               { "name-rev", cmd_name_rev, RUN_SETUP },
+               { "pack-objects", cmd_pack_objects, RUN_SETUP },
+               { "peek-remote", cmd_ls_remote },
+               { "pickaxe", cmd_blame, RUN_SETUP },
+               { "prune", cmd_prune, RUN_SETUP },
+               { "prune-packed", cmd_prune_packed, RUN_SETUP },
+               { "push", cmd_push, RUN_SETUP },
+               { "read-tree", cmd_read_tree, RUN_SETUP },
+               { "reflog", cmd_reflog, RUN_SETUP },
+               { "repo-config", cmd_config },
+               { "rerere", cmd_rerere, RUN_SETUP },
+               { "reset", cmd_reset, RUN_SETUP },
+               { "rev-list", cmd_rev_list, RUN_SETUP },
+               { "rev-parse", cmd_rev_parse },
+               { "revert", cmd_revert, RUN_SETUP | NEED_WORK_TREE },
+               { "rm", cmd_rm, RUN_SETUP },
+               { "send-pack", cmd_send_pack, RUN_SETUP },
+               { "shortlog", cmd_shortlog, RUN_SETUP | USE_PAGER },
+               { "show-branch", cmd_show_branch, RUN_SETUP },
+               { "show", cmd_show, RUN_SETUP | USE_PAGER },
+               { "status", cmd_status, RUN_SETUP | NEED_WORK_TREE },
+               { "stripspace", cmd_stripspace },
+               { "symbolic-ref", cmd_symbolic_ref, RUN_SETUP },
+               { "tag", cmd_tag, RUN_SETUP },
+               { "tar-tree", cmd_tar_tree },
+               { "unpack-objects", cmd_unpack_objects, RUN_SETUP },
+               { "update-index", cmd_update_index, RUN_SETUP },
+               { "update-ref", cmd_update_ref, RUN_SETUP },
+               { "upload-archive", cmd_upload_archive },
+               { "verify-tag", cmd_verify_tag, RUN_SETUP },
+               { "version", cmd_version },
+               { "whatchanged", cmd_whatchanged, RUN_SETUP | USE_PAGER },
+               { "write-tree", cmd_write_tree, RUN_SETUP },
+               { "verify-pack", cmd_verify_pack },
+               { "show-ref", cmd_show_ref, RUN_SETUP },
+               { "pack-refs", cmd_pack_refs, RUN_SETUP },
+       };
+       int i;
+
+       /* Turn "git cmd --help" into "git help cmd" */
+       if (argc > 1 && !strcmp(argv[1], "--help")) {
+               argv[1] = argv[0];
+               argv[0] = cmd = "help";
+       }
+
+       for (i = 0; i < ARRAY_SIZE(commands); i++) {
+               struct cmd_struct *p = commands+i;
+               if (strcmp(p->cmd, cmd))
+                       continue;
+               exit(run_command(p, argc, argv));
+       }
+}
+
+int main(int argc, const char **argv)
+{
+       const char *cmd = argv[0] ? argv[0] : "git-help";
+       char *slash = strrchr(cmd, '/');
+       const char *cmd_path = NULL;
+       int done_alias = 0;
+
+       /*
+        * Take the basename of argv[0] as the command
+        * name, and the dirname as the default exec_path
+        * if we don't have anything better.
+        */
+       if (slash) {
+               *slash++ = 0;
+               cmd_path = cmd;
+               cmd = slash;
+       }
+
+       /*
+        * "git-xxxx" is the same as "git xxxx", but we obviously:
+        *
+        *  - cannot take flags in between the "git" and the "xxxx".
+        *  - cannot execute it externally (since it would just do
+        *    the same thing over again)
+        *
+        * So we just directly call the internal command handler, and
+        * die if that one cannot handle it.
+        */
+       if (!prefixcmp(cmd, "git-")) {
+               cmd += 4;
+               argv[0] = cmd;
+               handle_internal_command(argc, argv);
+               die("cannot handle %s internally", cmd);
+       }
+
+       /* Look for flags.. */
+       argv++;
+       argc--;
+       handle_options(&argv, &argc, NULL);
+       if (argc > 0) {
+               if (!prefixcmp(argv[0], "--"))
+                       argv[0] += 2;
+       } else {
+               /* The user didn't specify a command; give them help */
+               printf("usage: %s\n\n", git_usage_string);
+               list_common_cmds_help();
+               exit(1);
+       }
+       cmd = argv[0];
+
+       /*
+        * We use PATH to find git commands, but we prepend some higher
+        * precidence paths: the "--exec-path" option, the GIT_EXEC_PATH
+        * environment, and the $(gitexecdir) from the Makefile at build
+        * time.
+        */
+       setup_path(cmd_path);
+
+       while (1) {
+               /* See if it's an internal command */
+               handle_internal_command(argc, argv);
+
+               /* .. then try the external ones */
+               execv_git_cmd(argv);
+
+               /* It could be an alias -- this works around the insanity
+                * of overriding "git log" with "git show" by having
+                * alias.log = show
+                */
+               if (done_alias || !handle_alias(&argc, &argv))
+                       break;
+               done_alias = 1;
+       }
+
+       if (errno == ENOENT) {
+               if (done_alias) {
+                       fprintf(stderr, "Expansion of alias '%s' failed; "
+                               "'%s' is not a git-command\n",
+                               cmd, argv[0]);
+                       exit(1);
+               }
+               help_unknown_cmd(cmd);
+       }
+
+       fprintf(stderr, "Failed to run command '%s': %s\n",
+               cmd, strerror(errno));
+
+       return 1;
+}
diff --git a/git.spec.in b/git.spec.in
new file mode 100644 (file)
index 0000000..bdb293d
--- /dev/null
@@ -0,0 +1,251 @@
+# Pass --without docs to rpmbuild if you don't want the documentation
+
+%define python_path /usr/bin/python
+
+Name:          git
+Version:       @@VERSION@@
+Release:       1%{?dist}
+Summary:       Git core and tools
+License:       GPL
+Group:                 Development/Tools
+URL:           http://kernel.org/pub/software/scm/git/
+Source:        http://kernel.org/pub/software/scm/git/%{name}-%{version}.tar.gz
+BuildRequires: zlib-devel >= 1.2, openssl-devel, curl-devel, expat-devel  %{!?_without_docs:, xmlto, asciidoc > 6.0.3}
+BuildRoot:     %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+Requires:      git-core, git-svn, git-cvs, git-arch, git-email, gitk, git-gui, perl-Git
+
+%description
+Git is a fast, scalable, distributed revision control system with an
+unusually rich command set that provides both high-level operations
+and full access to internals.
+
+This is a dummy package which brings in all subpackages.
+
+%package core
+Summary:       Core git tools
+Group:         Development/Tools
+Requires:      zlib >= 1.2, rsync, curl, less, openssh-clients, expat
+Obsoletes:     git-p4
+%description core
+Git is a fast, scalable, distributed revision control system with an
+unusually rich command set that provides both high-level operations
+and full access to internals.
+
+These are the core tools with minimal dependencies.
+
+%package svn
+Summary:        Git tools for importing Subversion repositories
+Group:          Development/Tools
+Requires:       git-core = %{version}-%{release}, subversion
+%description svn
+Git tools for importing Subversion repositories.
+
+%package cvs
+Summary:        Git tools for importing CVS repositories
+Group:          Development/Tools
+Requires:       git-core = %{version}-%{release}, cvs, cvsps
+%description cvs
+Git tools for importing CVS repositories.
+
+%package arch
+Summary:        Git tools for importing Arch repositories
+Group:          Development/Tools
+Requires:       git-core = %{version}-%{release}, tla
+%description arch
+Git tools for importing Arch repositories.
+
+%package email
+Summary:        Git tools for sending email
+Group:          Development/Tools
+Requires:      git-core = %{version}-%{release}
+%description email
+Git tools for sending email.
+
+%package gui
+Summary:        Git GUI tool
+Group:          Development/Tools
+Requires:       git-core = %{version}-%{release}, tk >= 8.4
+%description gui
+Git GUI tool
+
+%package -n gitk
+Summary:        Git revision tree visualiser ('gitk')
+Group:          Development/Tools
+Requires:       git-core = %{version}-%{release}, tk >= 8.4
+%description -n gitk
+Git revision tree visualiser ('gitk')
+
+%package -n perl-Git
+Summary:        Perl interface to Git
+Group:          Development/Libraries
+Requires:       git-core = %{version}-%{release}
+Requires:       perl(:MODULE_COMPAT_%(eval "`%{__perl} -V:version`"; echo $version))
+BuildRequires:  perl(Error)
+
+%description -n perl-Git
+Perl interface to Git
+
+%prep
+%setup -q
+
+%build
+make %{_smp_mflags} CFLAGS="$RPM_OPT_FLAGS" \
+     ETC_GITCONFIG=/etc/gitconfig \
+     prefix=%{_prefix} all %{!?_without_docs: doc}
+
+%install
+rm -rf $RPM_BUILD_ROOT
+make %{_smp_mflags} CFLAGS="$RPM_OPT_FLAGS" DESTDIR=$RPM_BUILD_ROOT \
+     prefix=%{_prefix} mandir=%{_mandir} \
+     ETC_GITCONFIG=/etc/gitconfig \
+     PYTHON_PATH=%{python_path} \
+     INSTALLDIRS=vendor install %{!?_without_docs: install-doc}
+find $RPM_BUILD_ROOT -type f -name .packlist -exec rm -f {} ';'
+find $RPM_BUILD_ROOT -type f -name '*.bs' -empty -exec rm -f {} ';'
+find $RPM_BUILD_ROOT -type f -name perllocal.pod -exec rm -f {} ';'
+
+(find $RPM_BUILD_ROOT%{_bindir} -type f | grep -vE "archimport|svn|cvs|email|gitk|git-gui|git-citool" | sed -e s@^$RPM_BUILD_ROOT@@)               > bin-man-doc-files
+(find $RPM_BUILD_ROOT%{perl_vendorlib} -type f | sed -e s@^$RPM_BUILD_ROOT@@) >> perl-files
+%if %{!?_without_docs:1}0
+(find $RPM_BUILD_ROOT%{_mandir} $RPM_BUILD_ROOT/Documentation -type f | grep -vE "archimport|svn|git-cvs|email|gitk|git-gui|git-citool" | sed -e s@^$RPM_BUILD_ROOT@@ -e 's/$/*/' ) >> bin-man-doc-files
+%else
+rm -rf $RPM_BUILD_ROOT%{_mandir}
+%endif
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+%files
+# These are no files in the root package
+
+%files svn
+%defattr(-,root,root)
+%{_bindir}/*svn*
+%doc Documentation/*svn*.txt
+%{!?_without_docs: %{_mandir}/man1/*svn*.1*}
+%{!?_without_docs: %doc Documentation/*svn*.html }
+
+%files cvs
+%defattr(-,root,root)
+%doc Documentation/*git-cvs*.txt
+%{_bindir}/*cvs*
+%{!?_without_docs: %{_mandir}/man1/*cvs*.1*}
+%{!?_without_docs: %doc Documentation/*git-cvs*.html }
+
+%files arch
+%defattr(-,root,root)
+%doc Documentation/git-archimport.txt
+%{_bindir}/git-archimport
+%{!?_without_docs: %{_mandir}/man1/git-archimport.1*}
+%{!?_without_docs: %doc Documentation/git-archimport.html }
+
+%files email
+%defattr(-,root,root)
+%doc Documentation/*email*.txt
+%{_bindir}/*email*
+%{!?_without_docs: %{_mandir}/man1/*email*.1*}
+%{!?_without_docs: %doc Documentation/*email*.html }
+
+%files gui
+%defattr(-,root,root)
+%{_bindir}/git-gui
+%{_bindir}/git-citool
+%{_datadir}/git-gui/
+%{!?_without_docs: %{_mandir}/man1/git-gui.1*}
+%{!?_without_docs: %doc Documentation/git-gui.html}
+%{!?_without_docs: %{_mandir}/man1/git-citool.1*}
+%{!?_without_docs: %doc Documentation/git-citool.html}
+
+%files -n gitk
+%defattr(-,root,root)
+%doc Documentation/*gitk*.txt
+%{_bindir}/*gitk*
+%{!?_without_docs: %{_mandir}/man1/*gitk*.1*}
+%{!?_without_docs: %doc Documentation/*gitk*.html }
+
+%files -n perl-Git -f perl-files
+%defattr(-,root,root)
+
+%files core -f bin-man-doc-files
+%defattr(-,root,root)
+%{_datadir}/git-core/
+%doc README COPYING Documentation/*.txt
+%{!?_without_docs: %doc Documentation/*.html Documentation/howto}
+%{!?_without_docs: %doc Documentation/technical}
+
+%changelog
+* Sun Jul 15 2007 Sean Estabrooks <seanlkml@sympatico.ca>
+- Removed p4import.
+
+* Tue Jun 26 2007 Quy Tonthat <qtonthat@gmail.com>
+- Fixed problems looking for wrong manpages.
+
+* Thu Jun 21 2007 Shawn O. Pearce <spearce@spearce.org>
+- Added documentation files for git-gui
+
+* Tue May 13 2007 Quy Tonthat <qtonthat@gmail.com>
+- Added lib files for git-gui
+- Added Documentation/technical (As needed by Git Users Manual)
+
+* Tue May 8 2007 Quy Tonthat <qtonthat@gmail.com>
+- Added howto files
+
+* Tue Mar 27 2007 Eygene Ryabinkin <rea-git@codelabs.ru>
+- Added the git-p4 package: Perforce import stuff.
+
+* Mon Feb 13 2007 Nicolas Pitre <nico@cam.org>
+- Update core package description (Git isn't as stupid as it used to be)
+
+* Mon Feb 12 2007 Junio C Hamano <junkio@cox.net>
+- Add git-gui and git-citool.
+
+* Mon Nov 14 2005 H. Peter Anvin <hpa@zytor.com> 0.99.9j-1
+- Change subpackage names to git-<name> instead of git-core-<name>
+- Create empty root package which brings in all subpackages
+- Rename git-tk -> gitk
+
+* Thu Nov 10 2005 Chris Wright <chrisw@osdl.org> 0.99.9g-1
+- zlib dependency fix
+- Minor cleanups from split
+- Move arch import to separate package as well
+
+* Tue Sep 27 2005 Jim Radford <radford@blackbean.org>
+- Move programs with non-standard dependencies (svn, cvs, email)
+  into separate packages
+
+* Tue Sep 27 2005 H. Peter Anvin <hpa@zytor.com>
+- parallelize build
+- COPTS -> CFLAGS
+
+* Fri Sep 16 2005 Chris Wright <chrisw@osdl.org> 0.99.6-1
+- update to 0.99.6
+
+* Fri Sep 16 2005 Horst H. von Brand <vonbrand@inf.utfsm.cl>
+- Linus noticed that less is required, added to the dependencies
+
+* Sun Sep 11 2005 Horst H. von Brand <vonbrand@inf.utfsm.cl>
+- Updated dependencies
+- Don't assume manpages are gzipped
+
+* Thu Aug 18 2005 Chris Wright <chrisw@osdl.org> 0.99.4-4
+- drop sh_utils, sh-utils, diffutils, mktemp, and openssl Requires
+- use RPM_OPT_FLAGS in spec file, drop patch0
+
+* Wed Aug 17 2005 Tom "spot" Callaway <tcallawa@redhat.com> 0.99.4-3
+- use dist tag to differentiate between branches
+- use rpm optflags by default (patch0)
+- own %{_datadir}/git-core/
+
+* Mon Aug 15 2005 Chris Wright <chrisw@osdl.org>
+- update spec file to fix Buildroot, Requires, and drop Vendor
+
+* Sun Aug 07 2005 Horst H. von Brand <vonbrand@inf.utfsm.cl>
+- Redid the description
+- Cut overlong make line, loosened changelog a bit
+- I think Junio (or perhaps OSDL?) should be vendor...
+
+* Thu Jul 14 2005 Eric Biederman <ebiederm@xmission.com>
+- Add the man pages, and the --without docs build option
+
+* Wed Jul 7 2005 Chris Wright <chris@osdl.org>
+- initial git spec file
diff --git a/gitk-git/Makefile b/gitk-git/Makefile
new file mode 100644 (file)
index 0000000..9bc1e24
--- /dev/null
@@ -0,0 +1,29 @@
+# The default target of this Makefile is...
+all::
+
+prefix ?= $(HOME)
+bindir ?= $(prefix)/bin
+TCLTK_PATH ?= wish
+INSTALL ?= install
+RM ?= rm -f
+
+DESTDIR_SQ = $(subst ','\'',$(DESTDIR))
+bindir_SQ = $(subst ','\'',$(bindir))
+TCLTK_PATH_SQ = $(subst ','\'',$(TCLTK_PATH))
+
+ifndef V
+       QUIET          = @
+       QUIET_GEN      = $(QUIET)echo '   ' GEN $@ &&
+endif
+
+all:: gitk-wish
+install:: all
+       $(INSTALL) gitk-wish '$(DESTDIR_SQ)$(bindir_SQ)'/gitk
+clean::
+       $(RM) gitk-wish
+
+gitk-wish: gitk
+       $(QUIET_GEN)$(RM) $@ $@+ && \
+       sed -e '1,3s|^exec .* "$$0"|exec $(subst |,'\|',$(TCLTK_PATH_SQ)) "$$0"|' <gitk >$@+ && \
+       chmod +x $@+ && \
+       mv -f $@+ $@
diff --git a/gitk-git/gitk b/gitk-git/gitk
new file mode 100644 (file)
index 0000000..1da0b0a
--- /dev/null
@@ -0,0 +1,8661 @@
+#!/bin/sh
+# Tcl ignores the next line -*- tcl -*- \
+exec wish "$0" -- "$@"
+
+# Copyright (C) 2005-2006 Paul Mackerras.  All rights reserved.
+# This program is free software; it may be used, copied, modified
+# and distributed under the terms of the GNU General Public Licence,
+# either version 2, or (at your option) any later version.
+
+proc gitdir {} {
+    global env
+    if {[info exists env(GIT_DIR)]} {
+       return $env(GIT_DIR)
+    } else {
+       return [exec git rev-parse --git-dir]
+    }
+}
+
+# A simple scheduler for compute-intensive stuff.
+# The aim is to make sure that event handlers for GUI actions can
+# run at least every 50-100 ms.  Unfortunately fileevent handlers are
+# run before X event handlers, so reading from a fast source can
+# make the GUI completely unresponsive.
+proc run args {
+    global isonrunq runq
+
+    set script $args
+    if {[info exists isonrunq($script)]} return
+    if {$runq eq {}} {
+       after idle dorunq
+    }
+    lappend runq [list {} $script]
+    set isonrunq($script) 1
+}
+
+proc filerun {fd script} {
+    fileevent $fd readable [list filereadable $fd $script]
+}
+
+proc filereadable {fd script} {
+    global runq
+
+    fileevent $fd readable {}
+    if {$runq eq {}} {
+       after idle dorunq
+    }
+    lappend runq [list $fd $script]
+}
+
+proc dorunq {} {
+    global isonrunq runq
+
+    set tstart [clock clicks -milliseconds]
+    set t0 $tstart
+    while {$runq ne {}} {
+       set fd [lindex $runq 0 0]
+       set script [lindex $runq 0 1]
+       set repeat [eval $script]
+       set t1 [clock clicks -milliseconds]
+       set t [expr {$t1 - $t0}]
+       set runq [lrange $runq 1 end]
+       if {$repeat ne {} && $repeat} {
+           if {$fd eq {} || $repeat == 2} {
+               # script returns 1 if it wants to be readded
+               # file readers return 2 if they could do more straight away
+               lappend runq [list $fd $script]
+           } else {
+               fileevent $fd readable [list filereadable $fd $script]
+           }
+       } elseif {$fd eq {}} {
+           unset isonrunq($script)
+       }
+       set t0 $t1
+       if {$t1 - $tstart >= 80} break
+    }
+    if {$runq ne {}} {
+       after idle dorunq
+    }
+}
+
+# Start off a git rev-list process and arrange to read its output
+proc start_rev_list {view} {
+    global startmsecs
+    global commfd leftover tclencoding datemode
+    global viewargs viewfiles commitidx viewcomplete vnextroot
+    global showlocalchanges commitinterest mainheadid
+    global progressdirn progresscoords proglastnc curview
+
+    set startmsecs [clock clicks -milliseconds]
+    set commitidx($view) 0
+    set viewcomplete($view) 0
+    set vnextroot($view) 0
+    set order "--topo-order"
+    if {$datemode} {
+       set order "--date-order"
+    }
+    if {[catch {
+       set fd [open [concat | git log --no-color -z --pretty=raw $order --parents \
+                        --boundary $viewargs($view) "--" $viewfiles($view)] r]
+    } err]} {
+       error_popup "Error executing git rev-list: $err"
+       exit 1
+    }
+    set commfd($view) $fd
+    set leftover($view) {}
+    if {$showlocalchanges} {
+       lappend commitinterest($mainheadid) {dodiffindex}
+    }
+    fconfigure $fd -blocking 0 -translation lf -eofchar {}
+    if {$tclencoding != {}} {
+       fconfigure $fd -encoding $tclencoding
+    }
+    filerun $fd [list getcommitlines $fd $view]
+    nowbusy $view "Reading"
+    if {$view == $curview} {
+       set progressdirn 1
+       set progresscoords {0 0}
+       set proglastnc 0
+    }
+}
+
+proc stop_rev_list {} {
+    global commfd curview
+
+    if {![info exists commfd($curview)]} return
+    set fd $commfd($curview)
+    catch {
+       set pid [pid $fd]
+       exec kill $pid
+    }
+    catch {close $fd}
+    unset commfd($curview)
+}
+
+proc getcommits {} {
+    global phase canv curview
+
+    set phase getcommits
+    initlayout
+    start_rev_list $curview
+    show_status "Reading commits..."
+}
+
+# This makes a string representation of a positive integer which
+# sorts as a string in numerical order
+proc strrep {n} {
+    if {$n < 16} {
+       return [format "%x" $n]
+    } elseif {$n < 256} {
+       return [format "x%.2x" $n]
+    } elseif {$n < 65536} {
+       return [format "y%.4x" $n]
+    }
+    return [format "z%.8x" $n]
+}
+
+proc getcommitlines {fd view}  {
+    global commitlisted commitinterest
+    global leftover commfd
+    global displayorder commitidx viewcomplete commitrow commitdata
+    global parentlist children curview hlview
+    global vparentlist vdisporder vcmitlisted
+    global ordertok vnextroot idpending
+
+    set stuff [read $fd 500000]
+    # git log doesn't terminate the last commit with a null...
+    if {$stuff == {} && $leftover($view) ne {} && [eof $fd]} {
+       set stuff "\0"
+    }
+    if {$stuff == {}} {
+       if {![eof $fd]} {
+           return 1
+       }
+       # Check if we have seen any ids listed as parents that haven't
+       # appeared in the list
+       foreach vid [array names idpending "$view,*"] {
+           # should only get here if git log is buggy
+           set id [lindex [split $vid ","] 1]
+           set commitrow($vid) $commitidx($view)
+           incr commitidx($view)
+           if {$view == $curview} {
+               lappend parentlist {}
+               lappend displayorder $id
+               lappend commitlisted 0
+           } else {
+               lappend vparentlist($view) {}
+               lappend vdisporder($view) $id
+               lappend vcmitlisted($view) 0
+           }
+       }
+       set viewcomplete($view) 1
+       global viewname progresscoords
+       unset commfd($view)
+       notbusy $view
+       set progresscoords {0 0}
+       adjustprogress
+       # set it blocking so we wait for the process to terminate
+       fconfigure $fd -blocking 1
+       if {[catch {close $fd} err]} {
+           set fv {}
+           if {$view != $curview} {
+               set fv " for the \"$viewname($view)\" view"
+           }
+           if {[string range $err 0 4] == "usage"} {
+               set err "Gitk: error reading commits$fv:\
+                       bad arguments to git rev-list."
+               if {$viewname($view) eq "Command line"} {
+                   append err \
+                       "  (Note: arguments to gitk are passed to git rev-list\
+                        to allow selection of commits to be displayed.)"
+               }
+           } else {
+               set err "Error reading commits$fv: $err"
+           }
+           error_popup $err
+       }
+       if {$view == $curview} {
+           run chewcommits $view
+       }
+       return 0
+    }
+    set start 0
+    set gotsome 0
+    while 1 {
+       set i [string first "\0" $stuff $start]
+       if {$i < 0} {
+           append leftover($view) [string range $stuff $start end]
+           break
+       }
+       if {$start == 0} {
+           set cmit $leftover($view)
+           append cmit [string range $stuff 0 [expr {$i - 1}]]
+           set leftover($view) {}
+       } else {
+           set cmit [string range $stuff $start [expr {$i - 1}]]
+       }
+       set start [expr {$i + 1}]
+       set j [string first "\n" $cmit]
+       set ok 0
+       set listed 1
+       if {$j >= 0 && [string match "commit *" $cmit]} {
+           set ids [string range $cmit 7 [expr {$j - 1}]]
+           if {[string match {[-<>]*} $ids]} {
+               switch -- [string index $ids 0] {
+                   "-" {set listed 0}
+                   "<" {set listed 2}
+                   ">" {set listed 3}
+               }
+               set ids [string range $ids 1 end]
+           }
+           set ok 1
+           foreach id $ids {
+               if {[string length $id] != 40} {
+                   set ok 0
+                   break
+               }
+           }
+       }
+       if {!$ok} {
+           set shortcmit $cmit
+           if {[string length $shortcmit] > 80} {
+               set shortcmit "[string range $shortcmit 0 80]..."
+           }
+           error_popup "Can't parse git log output: {$shortcmit}"
+           exit 1
+       }
+       set id [lindex $ids 0]
+       if {![info exists ordertok($view,$id)]} {
+           set otok "o[strrep $vnextroot($view)]"
+           incr vnextroot($view)
+           set ordertok($view,$id) $otok
+       } else {
+           set otok $ordertok($view,$id)
+           unset idpending($view,$id)
+       }
+       if {$listed} {
+           set olds [lrange $ids 1 end]
+           if {[llength $olds] == 1} {
+               set p [lindex $olds 0]
+               lappend children($view,$p) $id
+               if {![info exists ordertok($view,$p)]} {
+                   set ordertok($view,$p) $ordertok($view,$id)
+                   set idpending($view,$p) 1
+               }
+           } else {
+               set i 0
+               foreach p $olds {
+                   if {$i == 0 || [lsearch -exact $olds $p] >= $i} {
+                       lappend children($view,$p) $id
+                   }
+                   if {![info exists ordertok($view,$p)]} {
+                       set ordertok($view,$p) "$otok[strrep $i]]"
+                       set idpending($view,$p) 1
+                   }
+                   incr i
+               }
+           }
+       } else {
+           set olds {}
+       }
+       if {![info exists children($view,$id)]} {
+           set children($view,$id) {}
+       }
+       set commitdata($id) [string range $cmit [expr {$j + 1}] end]
+       set commitrow($view,$id) $commitidx($view)
+       incr commitidx($view)
+       if {$view == $curview} {
+           lappend parentlist $olds
+           lappend displayorder $id
+           lappend commitlisted $listed
+       } else {
+           lappend vparentlist($view) $olds
+           lappend vdisporder($view) $id
+           lappend vcmitlisted($view) $listed
+       }
+       if {[info exists commitinterest($id)]} {
+           foreach script $commitinterest($id) {
+               eval [string map [list "%I" $id] $script]
+           }
+           unset commitinterest($id)
+       }
+       set gotsome 1
+    }
+    if {$gotsome} {
+       run chewcommits $view
+       if {$view == $curview} {
+           # update progress bar
+           global progressdirn progresscoords proglastnc
+           set inc [expr {($commitidx($view) - $proglastnc) * 0.0002}]
+           set proglastnc $commitidx($view)
+           set l [lindex $progresscoords 0]
+           set r [lindex $progresscoords 1]
+           if {$progressdirn} {
+               set r [expr {$r + $inc}]
+               if {$r >= 1.0} {
+                   set r 1.0
+                   set progressdirn 0
+               }
+               if {$r > 0.2} {
+                   set l [expr {$r - 0.2}]
+               }
+           } else {
+               set l [expr {$l - $inc}]
+               if {$l <= 0.0} {
+                   set l 0.0
+                   set progressdirn 1
+               }
+               set r [expr {$l + 0.2}]
+           }
+           set progresscoords [list $l $r]
+           adjustprogress
+       }
+    }
+    return 2
+}
+
+proc chewcommits {view} {
+    global curview hlview viewcomplete
+    global selectedline pending_select
+
+    if {$view == $curview} {
+       layoutmore
+       if {$viewcomplete($view)} {
+           global displayorder commitidx phase
+           global numcommits startmsecs
+
+           if {[info exists pending_select]} {
+               set row [first_real_row]
+               selectline $row 1
+           }
+           if {$commitidx($curview) > 0} {
+               #set ms [expr {[clock clicks -milliseconds] - $startmsecs}]
+               #puts "overall $ms ms for $numcommits commits"
+           } else {
+               show_status "No commits selected"
+           }
+           notbusy layout
+           set phase {}
+       }
+    }
+    if {[info exists hlview] && $view == $hlview} {
+       vhighlightmore
+    }
+    return 0
+}
+
+proc readcommit {id} {
+    if {[catch {set contents [exec git cat-file commit $id]}]} return
+    parsecommit $id $contents 0
+}
+
+proc updatecommits {} {
+    global viewdata curview phase displayorder ordertok idpending
+    global children commitrow selectedline thickerline showneartags
+
+    if {$phase ne {}} {
+       stop_rev_list
+       set phase {}
+    }
+    set n $curview
+    foreach id $displayorder {
+       catch {unset children($n,$id)}
+       catch {unset commitrow($n,$id)}
+       catch {unset ordertok($n,$id)}
+    }
+    foreach vid [array names idpending "$n,*"] {
+       unset idpending($vid)
+    }
+    set curview -1
+    catch {unset selectedline}
+    catch {unset thickerline}
+    catch {unset viewdata($n)}
+    readrefs
+    changedrefs
+    if {$showneartags} {
+       getallcommits
+    }
+    showview $n
+}
+
+proc parsecommit {id contents listed} {
+    global commitinfo cdate
+
+    set inhdr 1
+    set comment {}
+    set headline {}
+    set auname {}
+    set audate {}
+    set comname {}
+    set comdate {}
+    set hdrend [string first "\n\n" $contents]
+    if {$hdrend < 0} {
+       # should never happen...
+       set hdrend [string length $contents]
+    }
+    set header [string range $contents 0 [expr {$hdrend - 1}]]
+    set comment [string range $contents [expr {$hdrend + 2}] end]
+    foreach line [split $header "\n"] {
+       set tag [lindex $line 0]
+       if {$tag == "author"} {
+           set audate [lindex $line end-1]
+           set auname [lrange $line 1 end-2]
+       } elseif {$tag == "committer"} {
+           set comdate [lindex $line end-1]
+           set comname [lrange $line 1 end-2]
+       }
+    }
+    set headline {}
+    # take the first non-blank line of the comment as the headline
+    set headline [string trimleft $comment]
+    set i [string first "\n" $headline]
+    if {$i >= 0} {
+       set headline [string range $headline 0 $i]
+    }
+    set headline [string trimright $headline]
+    set i [string first "\r" $headline]
+    if {$i >= 0} {
+       set headline [string trimright [string range $headline 0 $i]]
+    }
+    if {!$listed} {
+       # git rev-list indents the comment by 4 spaces;
+       # if we got this via git cat-file, add the indentation
+       set newcomment {}
+       foreach line [split $comment "\n"] {
+           append newcomment "    "
+           append newcomment $line
+           append newcomment "\n"
+       }
+       set comment $newcomment
+    }
+    if {$comdate != {}} {
+       set cdate($id) $comdate
+    }
+    set commitinfo($id) [list $headline $auname $audate \
+                            $comname $comdate $comment]
+}
+
+proc getcommit {id} {
+    global commitdata commitinfo
+
+    if {[info exists commitdata($id)]} {
+       parsecommit $id $commitdata($id) 1
+    } else {
+       readcommit $id
+       if {![info exists commitinfo($id)]} {
+           set commitinfo($id) {"No commit information available"}
+       }
+    }
+    return 1
+}
+
+proc readrefs {} {
+    global tagids idtags headids idheads tagobjid
+    global otherrefids idotherrefs mainhead mainheadid
+
+    foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
+       catch {unset $v}
+    }
+    set refd [open [list | git show-ref -d] r]
+    while {[gets $refd line] >= 0} {
+       if {[string index $line 40] ne " "} continue
+       set id [string range $line 0 39]
+       set ref [string range $line 41 end]
+       if {![string match "refs/*" $ref]} continue
+       set name [string range $ref 5 end]
+       if {[string match "remotes/*" $name]} {
+           if {![string match "*/HEAD" $name]} {
+               set headids($name) $id
+               lappend idheads($id) $name
+           }
+       } elseif {[string match "heads/*" $name]} {
+           set name [string range $name 6 end]
+           set headids($name) $id
+           lappend idheads($id) $name
+       } elseif {[string match "tags/*" $name]} {
+           # this lets refs/tags/foo^{} overwrite refs/tags/foo,
+           # which is what we want since the former is the commit ID
+           set name [string range $name 5 end]
+           if {[string match "*^{}" $name]} {
+               set name [string range $name 0 end-3]
+           } else {
+               set tagobjid($name) $id
+           }
+           set tagids($name) $id
+           lappend idtags($id) $name
+       } else {
+           set otherrefids($name) $id
+           lappend idotherrefs($id) $name
+       }
+    }
+    catch {close $refd}
+    set mainhead {}
+    set mainheadid {}
+    catch {
+       set thehead [exec git symbolic-ref HEAD]
+       if {[string match "refs/heads/*" $thehead]} {
+           set mainhead [string range $thehead 11 end]
+           if {[info exists headids($mainhead)]} {
+               set mainheadid $headids($mainhead)
+           }
+       }
+    }
+}
+
+# skip over fake commits
+proc first_real_row {} {
+    global nullid nullid2 displayorder numcommits
+
+    for {set row 0} {$row < $numcommits} {incr row} {
+       set id [lindex $displayorder $row]
+       if {$id ne $nullid && $id ne $nullid2} {
+           break
+       }
+    }
+    return $row
+}
+
+# update things for a head moved to a child of its previous location
+proc movehead {id name} {
+    global headids idheads
+
+    removehead $headids($name) $name
+    set headids($name) $id
+    lappend idheads($id) $name
+}
+
+# update things when a head has been removed
+proc removehead {id name} {
+    global headids idheads
+
+    if {$idheads($id) eq $name} {
+       unset idheads($id)
+    } else {
+       set i [lsearch -exact $idheads($id) $name]
+       if {$i >= 0} {
+           set idheads($id) [lreplace $idheads($id) $i $i]
+       }
+    }
+    unset headids($name)
+}
+
+proc show_error {w top msg} {
+    message $w.m -text $msg -justify center -aspect 400
+    pack $w.m -side top -fill x -padx 20 -pady 20
+    button $w.ok -text OK -command "destroy $top"
+    pack $w.ok -side bottom -fill x
+    bind $top <Visibility> "grab $top; focus $top"
+    bind $top <Key-Return> "destroy $top"
+    tkwait window $top
+}
+
+proc error_popup msg {
+    set w .error
+    toplevel $w
+    wm transient $w .
+    show_error $w $w $msg
+}
+
+proc confirm_popup msg {
+    global confirm_ok
+    set confirm_ok 0
+    set w .confirm
+    toplevel $w
+    wm transient $w .
+    message $w.m -text $msg -justify center -aspect 400
+    pack $w.m -side top -fill x -padx 20 -pady 20
+    button $w.ok -text OK -command "set confirm_ok 1; destroy $w"
+    pack $w.ok -side left -fill x
+    button $w.cancel -text Cancel -command "destroy $w"
+    pack $w.cancel -side right -fill x
+    bind $w <Visibility> "grab $w; focus $w"
+    tkwait window $w
+    return $confirm_ok
+}
+
+proc makewindow {} {
+    global canv canv2 canv3 linespc charspc ctext cflist
+    global tabstop
+    global findtype findtypemenu findloc findstring fstring geometry
+    global entries sha1entry sha1string sha1but
+    global diffcontextstring diffcontext
+    global maincursor textcursor curtextcursor
+    global rowctxmenu fakerowmenu mergemax wrapcomment
+    global highlight_files gdttype
+    global searchstring sstring
+    global bgcolor fgcolor bglist fglist diffcolors selectbgcolor
+    global headctxmenu progresscanv progressitem progresscoords statusw
+    global fprogitem fprogcoord lastprogupdate progupdatepending
+    global rprogitem rprogcoord
+    global have_tk85
+
+    menu .bar
+    .bar add cascade -label "File" -menu .bar.file
+    .bar configure -font uifont
+    menu .bar.file
+    .bar.file add command -label "Update" -command updatecommits
+    .bar.file add command -label "Reread references" -command rereadrefs
+    .bar.file add command -label "List references" -command showrefs
+    .bar.file add command -label "Quit" -command doquit
+    .bar.file configure -font uifont
+    menu .bar.edit
+    .bar add cascade -label "Edit" -menu .bar.edit
+    .bar.edit add command -label "Preferences" -command doprefs
+    .bar.edit configure -font uifont
+
+    menu .bar.view -font uifont
+    .bar add cascade -label "View" -menu .bar.view
+    .bar.view add command -label "New view..." -command {newview 0}
+    .bar.view add command -label "Edit view..." -command editview \
+       -state disabled
+    .bar.view add command -label "Delete view" -command delview -state disabled
+    .bar.view add separator
+    .bar.view add radiobutton -label "All files" -command {showview 0} \
+       -variable selectedview -value 0
+
+    menu .bar.help
+    .bar add cascade -label "Help" -menu .bar.help
+    .bar.help add command -label "About gitk" -command about
+    .bar.help add command -label "Key bindings" -command keys
+    .bar.help configure -font uifont
+    . configure -menu .bar
+
+    # the gui has upper and lower half, parts of a paned window.
+    panedwindow .ctop -orient vertical
+
+    # possibly use assumed geometry
+    if {![info exists geometry(pwsash0)]} {
+        set geometry(topheight) [expr {15 * $linespc}]
+        set geometry(topwidth) [expr {80 * $charspc}]
+        set geometry(botheight) [expr {15 * $linespc}]
+        set geometry(botwidth) [expr {50 * $charspc}]
+        set geometry(pwsash0) "[expr {40 * $charspc}] 2"
+        set geometry(pwsash1) "[expr {60 * $charspc}] 2"
+    }
+
+    # the upper half will have a paned window, a scroll bar to the right, and some stuff below
+    frame .tf -height $geometry(topheight) -width $geometry(topwidth)
+    frame .tf.histframe
+    panedwindow .tf.histframe.pwclist -orient horizontal -sashpad 0 -handlesize 4
+
+    # create three canvases
+    set cscroll .tf.histframe.csb
+    set canv .tf.histframe.pwclist.canv
+    canvas $canv \
+       -selectbackground $selectbgcolor \
+       -background $bgcolor -bd 0 \
+       -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll"
+    .tf.histframe.pwclist add $canv
+    set canv2 .tf.histframe.pwclist.canv2
+    canvas $canv2 \
+       -selectbackground $selectbgcolor \
+       -background $bgcolor -bd 0 -yscrollincr $linespc
+    .tf.histframe.pwclist add $canv2
+    set canv3 .tf.histframe.pwclist.canv3
+    canvas $canv3 \
+       -selectbackground $selectbgcolor \
+       -background $bgcolor -bd 0 -yscrollincr $linespc
+    .tf.histframe.pwclist add $canv3
+    eval .tf.histframe.pwclist sash place 0 $geometry(pwsash0)
+    eval .tf.histframe.pwclist sash place 1 $geometry(pwsash1)
+
+    # a scroll bar to rule them
+    scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
+    pack $cscroll -side right -fill y
+    bind .tf.histframe.pwclist <Configure> {resizeclistpanes %W %w}
+    lappend bglist $canv $canv2 $canv3
+    pack .tf.histframe.pwclist -fill both -expand 1 -side left
+
+    # we have two button bars at bottom of top frame. Bar 1
+    frame .tf.bar
+    frame .tf.lbar -height 15
+
+    set sha1entry .tf.bar.sha1
+    set entries $sha1entry
+    set sha1but .tf.bar.sha1label
+    button $sha1but -text "SHA1 ID: " -state disabled -relief flat \
+       -command gotocommit -width 8 -font uifont
+    $sha1but conf -disabledforeground [$sha1but cget -foreground]
+    pack .tf.bar.sha1label -side left
+    entry $sha1entry -width 40 -font textfont -textvariable sha1string
+    trace add variable sha1string write sha1change
+    pack $sha1entry -side left -pady 2
+
+    image create bitmap bm-left -data {
+       #define left_width 16
+       #define left_height 16
+       static unsigned char left_bits[] = {
+       0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
+       0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
+       0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
+    }
+    image create bitmap bm-right -data {
+       #define right_width 16
+       #define right_height 16
+       static unsigned char right_bits[] = {
+       0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
+       0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
+       0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
+    }
+    button .tf.bar.leftbut -image bm-left -command goback \
+       -state disabled -width 26
+    pack .tf.bar.leftbut -side left -fill y
+    button .tf.bar.rightbut -image bm-right -command goforw \
+       -state disabled -width 26
+    pack .tf.bar.rightbut -side left -fill y
+
+    # Status label and progress bar
+    set statusw .tf.bar.status
+    label $statusw -width 15 -relief sunken -font uifont
+    pack $statusw -side left -padx 5
+    set h [expr {[font metrics uifont -linespace] + 2}]
+    set progresscanv .tf.bar.progress
+    canvas $progresscanv -relief sunken -height $h -borderwidth 2
+    set progressitem [$progresscanv create rect -1 0 0 $h -fill green]
+    set fprogitem [$progresscanv create rect -1 0 0 $h -fill yellow]
+    set rprogitem [$progresscanv create rect -1 0 0 $h -fill red]
+    pack $progresscanv -side right -expand 1 -fill x
+    set progresscoords {0 0}
+    set fprogcoord 0
+    set rprogcoord 0
+    bind $progresscanv <Configure> adjustprogress
+    set lastprogupdate [clock clicks -milliseconds]
+    set progupdatepending 0
+
+    # build up the bottom bar of upper window
+    label .tf.lbar.flabel -text "Find " -font uifont
+    button .tf.lbar.fnext -text "next" -command {dofind 1 1} -font uifont
+    button .tf.lbar.fprev -text "prev" -command {dofind -1 1} -font uifont
+    label .tf.lbar.flab2 -text " commit " -font uifont
+    pack .tf.lbar.flabel .tf.lbar.fnext .tf.lbar.fprev .tf.lbar.flab2 \
+       -side left -fill y
+    set gdttype "containing:"
+    set gm [tk_optionMenu .tf.lbar.gdttype gdttype \
+               "containing:" \
+               "touching paths:" \
+               "adding/removing string:"]
+    trace add variable gdttype write gdttype_change
+    $gm conf -font uifont
+    .tf.lbar.gdttype conf -font uifont
+    pack .tf.lbar.gdttype -side left -fill y
+
+    set findstring {}
+    set fstring .tf.lbar.findstring
+    lappend entries $fstring
+    entry $fstring -width 30 -font textfont -textvariable findstring
+    trace add variable findstring write find_change
+    set findtype Exact
+    set findtypemenu [tk_optionMenu .tf.lbar.findtype \
+                     findtype Exact IgnCase Regexp]
+    trace add variable findtype write findcom_change
+    .tf.lbar.findtype configure -font uifont
+    .tf.lbar.findtype.menu configure -font uifont
+    set findloc "All fields"
+    tk_optionMenu .tf.lbar.findloc findloc "All fields" Headline \
+       Comments Author Committer
+    trace add variable findloc write find_change
+    .tf.lbar.findloc configure -font uifont
+    .tf.lbar.findloc.menu configure -font uifont
+    pack .tf.lbar.findloc -side right
+    pack .tf.lbar.findtype -side right
+    pack $fstring -side left -expand 1 -fill x
+
+    # Finish putting the upper half of the viewer together
+    pack .tf.lbar -in .tf -side bottom -fill x
+    pack .tf.bar -in .tf -side bottom -fill x
+    pack .tf.histframe -fill both -side top -expand 1
+    .ctop add .tf
+    .ctop paneconfigure .tf -height $geometry(topheight)
+    .ctop paneconfigure .tf -width $geometry(topwidth)
+
+    # now build up the bottom
+    panedwindow .pwbottom -orient horizontal
+
+    # lower left, a text box over search bar, scroll bar to the right
+    # if we know window height, then that will set the lower text height, otherwise
+    # we set lower text height which will drive window height
+    if {[info exists geometry(main)]} {
+        frame .bleft -width $geometry(botwidth)
+    } else {
+        frame .bleft -width $geometry(botwidth) -height $geometry(botheight)
+    }
+    frame .bleft.top
+    frame .bleft.mid
+
+    button .bleft.top.search -text "Search" -command dosearch \
+       -font uifont
+    pack .bleft.top.search -side left -padx 5
+    set sstring .bleft.top.sstring
+    entry $sstring -width 20 -font textfont -textvariable searchstring
+    lappend entries $sstring
+    trace add variable searchstring write incrsearch
+    pack $sstring -side left -expand 1 -fill x
+    radiobutton .bleft.mid.diff -text "Diff" -font uifont \
+       -command changediffdisp -variable diffelide -value {0 0}
+    radiobutton .bleft.mid.old -text "Old version" -font uifont \
+       -command changediffdisp -variable diffelide -value {0 1}
+    radiobutton .bleft.mid.new -text "New version" -font uifont \
+       -command changediffdisp -variable diffelide -value {1 0}
+    label .bleft.mid.labeldiffcontext -text "      Lines of context: " \
+       -font uifont
+    pack .bleft.mid.diff .bleft.mid.old .bleft.mid.new -side left
+    spinbox .bleft.mid.diffcontext -width 5 -font textfont \
+       -from 1 -increment 1 -to 10000000 \
+       -validate all -validatecommand "diffcontextvalidate %P" \
+       -textvariable diffcontextstring
+    .bleft.mid.diffcontext set $diffcontext
+    trace add variable diffcontextstring write diffcontextchange
+    lappend entries .bleft.mid.diffcontext
+    pack .bleft.mid.labeldiffcontext .bleft.mid.diffcontext -side left
+    set ctext .bleft.ctext
+    text $ctext -background $bgcolor -foreground $fgcolor \
+       -state disabled -font textfont \
+       -yscrollcommand scrolltext -wrap none
+    if {$have_tk85} {
+       $ctext conf -tabstyle wordprocessor
+    }
+    scrollbar .bleft.sb -command "$ctext yview"
+    pack .bleft.top -side top -fill x
+    pack .bleft.mid -side top -fill x
+    pack .bleft.sb -side right -fill y
+    pack $ctext -side left -fill both -expand 1
+    lappend bglist $ctext
+    lappend fglist $ctext
+
+    $ctext tag conf comment -wrap $wrapcomment
+    $ctext tag conf filesep -font textfontbold -back "#aaaaaa"
+    $ctext tag conf hunksep -fore [lindex $diffcolors 2]
+    $ctext tag conf d0 -fore [lindex $diffcolors 0]
+    $ctext tag conf d1 -fore [lindex $diffcolors 1]
+    $ctext tag conf m0 -fore red
+    $ctext tag conf m1 -fore blue
+    $ctext tag conf m2 -fore green
+    $ctext tag conf m3 -fore purple
+    $ctext tag conf m4 -fore brown
+    $ctext tag conf m5 -fore "#009090"
+    $ctext tag conf m6 -fore magenta
+    $ctext tag conf m7 -fore "#808000"
+    $ctext tag conf m8 -fore "#009000"
+    $ctext tag conf m9 -fore "#ff0080"
+    $ctext tag conf m10 -fore cyan
+    $ctext tag conf m11 -fore "#b07070"
+    $ctext tag conf m12 -fore "#70b0f0"
+    $ctext tag conf m13 -fore "#70f0b0"
+    $ctext tag conf m14 -fore "#f0b070"
+    $ctext tag conf m15 -fore "#ff70b0"
+    $ctext tag conf mmax -fore darkgrey
+    set mergemax 16
+    $ctext tag conf mresult -font textfontbold
+    $ctext tag conf msep -font textfontbold
+    $ctext tag conf found -back yellow
+
+    .pwbottom add .bleft
+    .pwbottom paneconfigure .bleft -width $geometry(botwidth)
+
+    # lower right
+    frame .bright
+    frame .bright.mode
+    radiobutton .bright.mode.patch -text "Patch" \
+       -command reselectline -variable cmitmode -value "patch"
+    .bright.mode.patch configure -font uifont
+    radiobutton .bright.mode.tree -text "Tree" \
+       -command reselectline -variable cmitmode -value "tree"
+    .bright.mode.tree configure -font uifont
+    grid .bright.mode.patch .bright.mode.tree -sticky ew
+    pack .bright.mode -side top -fill x
+    set cflist .bright.cfiles
+    set indent [font measure mainfont "nn"]
+    text $cflist \
+       -selectbackground $selectbgcolor \
+       -background $bgcolor -foreground $fgcolor \
+       -font mainfont \
+       -tabs [list $indent [expr {2 * $indent}]] \
+       -yscrollcommand ".bright.sb set" \
+       -cursor [. cget -cursor] \
+       -spacing1 1 -spacing3 1
+    lappend bglist $cflist
+    lappend fglist $cflist
+    scrollbar .bright.sb -command "$cflist yview"
+    pack .bright.sb -side right -fill y
+    pack $cflist -side left -fill both -expand 1
+    $cflist tag configure highlight \
+       -background [$cflist cget -selectbackground]
+    $cflist tag configure bold -font mainfontbold
+
+    .pwbottom add .bright
+    .ctop add .pwbottom
+
+    # restore window position if known
+    if {[info exists geometry(main)]} {
+        wm geometry . "$geometry(main)"
+    }
+
+    if {[tk windowingsystem] eq {aqua}} {
+        set M1B M1
+    } else {
+        set M1B Control
+    }
+
+    bind .pwbottom <Configure> {resizecdetpanes %W %w}
+    pack .ctop -fill both -expand 1
+    bindall <1> {selcanvline %W %x %y}
+    #bindall <B1-Motion> {selcanvline %W %x %y}
+    if {[tk windowingsystem] == "win32"} {
+       bind . <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D }
+       bind $ctext <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D ; break }
+    } else {
+       bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
+       bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
+        if {[tk windowingsystem] eq "aqua"} {
+            bindall <MouseWheel> {
+                set delta [expr {- (%D)}]
+                allcanvs yview scroll $delta units
+            }
+        }
+    }
+    bindall <2> "canvscan mark %W %x %y"
+    bindall <B2-Motion> "canvscan dragto %W %x %y"
+    bindkey <Home> selfirstline
+    bindkey <End> sellastline
+    bind . <Key-Up> "selnextline -1"
+    bind . <Key-Down> "selnextline 1"
+    bind . <Shift-Key-Up> "dofind -1 0"
+    bind . <Shift-Key-Down> "dofind 1 0"
+    bindkey <Key-Right> "goforw"
+    bindkey <Key-Left> "goback"
+    bind . <Key-Prior> "selnextpage -1"
+    bind . <Key-Next> "selnextpage 1"
+    bind . <$M1B-Home> "allcanvs yview moveto 0.0"
+    bind . <$M1B-End> "allcanvs yview moveto 1.0"
+    bind . <$M1B-Key-Up> "allcanvs yview scroll -1 units"
+    bind . <$M1B-Key-Down> "allcanvs yview scroll 1 units"
+    bind . <$M1B-Key-Prior> "allcanvs yview scroll -1 pages"
+    bind . <$M1B-Key-Next> "allcanvs yview scroll 1 pages"
+    bindkey <Key-Delete> "$ctext yview scroll -1 pages"
+    bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
+    bindkey <Key-space> "$ctext yview scroll 1 pages"
+    bindkey p "selnextline -1"
+    bindkey n "selnextline 1"
+    bindkey z "goback"
+    bindkey x "goforw"
+    bindkey i "selnextline -1"
+    bindkey k "selnextline 1"
+    bindkey j "goback"
+    bindkey l "goforw"
+    bindkey b "$ctext yview scroll -1 pages"
+    bindkey d "$ctext yview scroll 18 units"
+    bindkey u "$ctext yview scroll -18 units"
+    bindkey / {dofind 1 1}
+    bindkey <Key-Return> {dofind 1 1}
+    bindkey ? {dofind -1 1}
+    bindkey f nextfile
+    bindkey <F5> updatecommits
+    bind . <$M1B-q> doquit
+    bind . <$M1B-f> {dofind 1 1}
+    bind . <$M1B-g> {dofind 1 0}
+    bind . <$M1B-r> dosearchback
+    bind . <$M1B-s> dosearch
+    bind . <$M1B-equal> {incrfont 1}
+    bind . <$M1B-KP_Add> {incrfont 1}
+    bind . <$M1B-minus> {incrfont -1}
+    bind . <$M1B-KP_Subtract> {incrfont -1}
+    wm protocol . WM_DELETE_WINDOW doquit
+    bind . <Button-1> "click %W"
+    bind $fstring <Key-Return> {dofind 1 1}
+    bind $sha1entry <Key-Return> gotocommit
+    bind $sha1entry <<PasteSelection>> clearsha1
+    bind $cflist <1> {sel_flist %W %x %y; break}
+    bind $cflist <B1-Motion> {sel_flist %W %x %y; break}
+    bind $cflist <ButtonRelease-1> {treeclick %W %x %y}
+    bind $cflist <Button-3> {pop_flist_menu %W %X %Y %x %y}
+
+    set maincursor [. cget -cursor]
+    set textcursor [$ctext cget -cursor]
+    set curtextcursor $textcursor
+
+    set rowctxmenu .rowctxmenu
+    menu $rowctxmenu -tearoff 0
+    $rowctxmenu add command -label "Diff this -> selected" \
+       -command {diffvssel 0}
+    $rowctxmenu add command -label "Diff selected -> this" \
+       -command {diffvssel 1}
+    $rowctxmenu add command -label "Make patch" -command mkpatch
+    $rowctxmenu add command -label "Create tag" -command mktag
+    $rowctxmenu add command -label "Write commit to file" -command writecommit
+    $rowctxmenu add command -label "Create new branch" -command mkbranch
+    $rowctxmenu add command -label "Cherry-pick this commit" \
+       -command cherrypick
+    $rowctxmenu add command -label "Reset HEAD branch to here" \
+       -command resethead
+
+    set fakerowmenu .fakerowmenu
+    menu $fakerowmenu -tearoff 0
+    $fakerowmenu add command -label "Diff this -> selected" \
+       -command {diffvssel 0}
+    $fakerowmenu add command -label "Diff selected -> this" \
+       -command {diffvssel 1}
+    $fakerowmenu add command -label "Make patch" -command mkpatch
+#    $fakerowmenu add command -label "Commit" -command {mkcommit 0}
+#    $fakerowmenu add command -label "Commit all" -command {mkcommit 1}
+#    $fakerowmenu add command -label "Revert local changes" -command revertlocal
+
+    set headctxmenu .headctxmenu
+    menu $headctxmenu -tearoff 0
+    $headctxmenu add command -label "Check out this branch" \
+       -command cobranch
+    $headctxmenu add command -label "Remove this branch" \
+       -command rmbranch
+
+    global flist_menu
+    set flist_menu .flistctxmenu
+    menu $flist_menu -tearoff 0
+    $flist_menu add command -label "Highlight this too" \
+       -command {flist_hl 0}
+    $flist_menu add command -label "Highlight this only" \
+       -command {flist_hl 1}
+}
+
+# Windows sends all mouse wheel events to the current focused window, not
+# the one where the mouse hovers, so bind those events here and redirect
+# to the correct window
+proc windows_mousewheel_redirector {W X Y D} {
+    global canv canv2 canv3
+    set w [winfo containing -displayof $W $X $Y]
+    if {$w ne ""} {
+       set u [expr {$D < 0 ? 5 : -5}]
+       if {$w == $canv || $w == $canv2 || $w == $canv3} {
+           allcanvs yview scroll $u units
+       } else {
+           catch {
+               $w yview scroll $u units
+           }
+       }
+    }
+}
+
+# mouse-2 makes all windows scan vertically, but only the one
+# the cursor is in scans horizontally
+proc canvscan {op w x y} {
+    global canv canv2 canv3
+    foreach c [list $canv $canv2 $canv3] {
+       if {$c == $w} {
+           $c scan $op $x $y
+       } else {
+           $c scan $op 0 $y
+       }
+    }
+}
+
+proc scrollcanv {cscroll f0 f1} {
+    $cscroll set $f0 $f1
+    drawfrac $f0 $f1
+    flushhighlights
+}
+
+# when we make a key binding for the toplevel, make sure
+# it doesn't get triggered when that key is pressed in the
+# find string entry widget.
+proc bindkey {ev script} {
+    global entries
+    bind . $ev $script
+    set escript [bind Entry $ev]
+    if {$escript == {}} {
+       set escript [bind Entry <Key>]
+    }
+    foreach e $entries {
+       bind $e $ev "$escript; break"
+    }
+}
+
+# set the focus back to the toplevel for any click outside
+# the entry widgets
+proc click {w} {
+    global ctext entries
+    foreach e [concat $entries $ctext] {
+       if {$w == $e} return
+    }
+    focus .
+}
+
+# Adjust the progress bar for a change in requested extent or canvas size
+proc adjustprogress {} {
+    global progresscanv progressitem progresscoords
+    global fprogitem fprogcoord lastprogupdate progupdatepending
+    global rprogitem rprogcoord
+
+    set w [expr {[winfo width $progresscanv] - 4}]
+    set x0 [expr {$w * [lindex $progresscoords 0]}]
+    set x1 [expr {$w * [lindex $progresscoords 1]}]
+    set h [winfo height $progresscanv]
+    $progresscanv coords $progressitem $x0 0 $x1 $h
+    $progresscanv coords $fprogitem 0 0 [expr {$w * $fprogcoord}] $h
+    $progresscanv coords $rprogitem 0 0 [expr {$w * $rprogcoord}] $h
+    set now [clock clicks -milliseconds]
+    if {$now >= $lastprogupdate + 100} {
+       set progupdatepending 0
+       update
+    } elseif {!$progupdatepending} {
+       set progupdatepending 1
+       after [expr {$lastprogupdate + 100 - $now}] doprogupdate
+    }
+}
+
+proc doprogupdate {} {
+    global lastprogupdate progupdatepending
+
+    if {$progupdatepending} {
+       set progupdatepending 0
+       set lastprogupdate [clock clicks -milliseconds]
+       update
+    }
+}
+
+proc savestuff {w} {
+    global canv canv2 canv3 mainfont textfont uifont tabstop
+    global stuffsaved findmergefiles maxgraphpct
+    global maxwidth showneartags showlocalchanges
+    global viewname viewfiles viewargs viewperm nextviewnum
+    global cmitmode wrapcomment datetimeformat limitdiffs
+    global colors bgcolor fgcolor diffcolors diffcontext selectbgcolor
+
+    if {$stuffsaved} return
+    if {![winfo viewable .]} return
+    catch {
+       set f [open "~/.gitk-new" w]
+       puts $f [list set mainfont $mainfont]
+       puts $f [list set textfont $textfont]
+       puts $f [list set uifont $uifont]
+       puts $f [list set tabstop $tabstop]
+       puts $f [list set findmergefiles $findmergefiles]
+       puts $f [list set maxgraphpct $maxgraphpct]
+       puts $f [list set maxwidth $maxwidth]
+       puts $f [list set cmitmode $cmitmode]
+       puts $f [list set wrapcomment $wrapcomment]
+       puts $f [list set showneartags $showneartags]
+       puts $f [list set showlocalchanges $showlocalchanges]
+       puts $f [list set datetimeformat $datetimeformat]
+       puts $f [list set limitdiffs $limitdiffs]
+       puts $f [list set bgcolor $bgcolor]
+       puts $f [list set fgcolor $fgcolor]
+       puts $f [list set colors $colors]
+       puts $f [list set diffcolors $diffcolors]
+       puts $f [list set diffcontext $diffcontext]
+       puts $f [list set selectbgcolor $selectbgcolor]
+
+       puts $f "set geometry(main) [wm geometry .]"
+       puts $f "set geometry(topwidth) [winfo width .tf]"
+       puts $f "set geometry(topheight) [winfo height .tf]"
+        puts $f "set geometry(pwsash0) \"[.tf.histframe.pwclist sash coord 0]\""
+        puts $f "set geometry(pwsash1) \"[.tf.histframe.pwclist sash coord 1]\""
+       puts $f "set geometry(botwidth) [winfo width .bleft]"
+       puts $f "set geometry(botheight) [winfo height .bleft]"
+
+       puts -nonewline $f "set permviews {"
+       for {set v 0} {$v < $nextviewnum} {incr v} {
+           if {$viewperm($v)} {
+               puts $f "{[list $viewname($v) $viewfiles($v) $viewargs($v)]}"
+           }
+       }
+       puts $f "}"
+       close $f
+       file rename -force "~/.gitk-new" "~/.gitk"
+    }
+    set stuffsaved 1
+}
+
+proc resizeclistpanes {win w} {
+    global oldwidth
+    if {[info exists oldwidth($win)]} {
+       set s0 [$win sash coord 0]
+       set s1 [$win sash coord 1]
+       if {$w < 60} {
+           set sash0 [expr {int($w/2 - 2)}]
+           set sash1 [expr {int($w*5/6 - 2)}]
+       } else {
+           set factor [expr {1.0 * $w / $oldwidth($win)}]
+           set sash0 [expr {int($factor * [lindex $s0 0])}]
+           set sash1 [expr {int($factor * [lindex $s1 0])}]
+           if {$sash0 < 30} {
+               set sash0 30
+           }
+           if {$sash1 < $sash0 + 20} {
+               set sash1 [expr {$sash0 + 20}]
+           }
+           if {$sash1 > $w - 10} {
+               set sash1 [expr {$w - 10}]
+               if {$sash0 > $sash1 - 20} {
+                   set sash0 [expr {$sash1 - 20}]
+               }
+           }
+       }
+       $win sash place 0 $sash0 [lindex $s0 1]
+       $win sash place 1 $sash1 [lindex $s1 1]
+    }
+    set oldwidth($win) $w
+}
+
+proc resizecdetpanes {win w} {
+    global oldwidth
+    if {[info exists oldwidth($win)]} {
+       set s0 [$win sash coord 0]
+       if {$w < 60} {
+           set sash0 [expr {int($w*3/4 - 2)}]
+       } else {
+           set factor [expr {1.0 * $w / $oldwidth($win)}]
+           set sash0 [expr {int($factor * [lindex $s0 0])}]
+           if {$sash0 < 45} {
+               set sash0 45
+           }
+           if {$sash0 > $w - 15} {
+               set sash0 [expr {$w - 15}]
+           }
+       }
+       $win sash place 0 $sash0 [lindex $s0 1]
+    }
+    set oldwidth($win) $w
+}
+
+proc allcanvs args {
+    global canv canv2 canv3
+    eval $canv $args
+    eval $canv2 $args
+    eval $canv3 $args
+}
+
+proc bindall {event action} {
+    global canv canv2 canv3
+    bind $canv $event $action
+    bind $canv2 $event $action
+    bind $canv3 $event $action
+}
+
+proc about {} {
+    global uifont
+    set w .about
+    if {[winfo exists $w]} {
+       raise $w
+       return
+    }
+    toplevel $w
+    wm title $w "About gitk"
+    message $w.m -text {
+Gitk - a commit viewer for git
+
+Copyright © 2005-2006 Paul Mackerras
+
+Use and redistribute under the terms of the GNU General Public License} \
+           -justify center -aspect 400 -border 2 -bg white -relief groove
+    pack $w.m -side top -fill x -padx 2 -pady 2
+    $w.m configure -font uifont
+    button $w.ok -text Close -command "destroy $w" -default active
+    pack $w.ok -side bottom
+    $w.ok configure -font uifont
+    bind $w <Visibility> "focus $w.ok"
+    bind $w <Key-Escape> "destroy $w"
+    bind $w <Key-Return> "destroy $w"
+}
+
+proc keys {} {
+    global uifont
+    set w .keys
+    if {[winfo exists $w]} {
+       raise $w
+       return
+    }
+    if {[tk windowingsystem] eq {aqua}} {
+       set M1T Cmd
+    } else {
+       set M1T Ctrl
+    }
+    toplevel $w
+    wm title $w "Gitk key bindings"
+    message $w.m -text "
+Gitk key bindings:
+
+<$M1T-Q>               Quit
+<Home>         Move to first commit
+<End>          Move to last commit
+<Up>, p, i     Move up one commit
+<Down>, n, k   Move down one commit
+<Left>, z, j   Go back in history list
+<Right>, x, l  Go forward in history list
+<PageUp>       Move up one page in commit list
+<PageDown>     Move down one page in commit list
+<$M1T-Home>    Scroll to top of commit list
+<$M1T-End>     Scroll to bottom of commit list
+<$M1T-Up>      Scroll commit list up one line
+<$M1T-Down>    Scroll commit list down one line
+<$M1T-PageUp>  Scroll commit list up one page
+<$M1T-PageDown>        Scroll commit list down one page
+<Shift-Up>     Find backwards (upwards, later commits)
+<Shift-Down>   Find forwards (downwards, earlier commits)
+<Delete>, b    Scroll diff view up one page
+<Backspace>    Scroll diff view up one page
+<Space>                Scroll diff view down one page
+u              Scroll diff view up 18 lines
+d              Scroll diff view down 18 lines
+<$M1T-F>               Find
+<$M1T-G>               Move to next find hit
+<Return>       Move to next find hit
+/              Move to next find hit, or redo find
+?              Move to previous find hit
+f              Scroll diff view to next file
+<$M1T-S>               Search for next hit in diff view
+<$M1T-R>               Search for previous hit in diff view
+<$M1T-KP+>     Increase font size
+<$M1T-plus>    Increase font size
+<$M1T-KP->     Decrease font size
+<$M1T-minus>   Decrease font size
+<F5>           Update
+" \
+           -justify left -bg white -border 2 -relief groove
+    pack $w.m -side top -fill both -padx 2 -pady 2
+    $w.m configure -font uifont
+    button $w.ok -text Close -command "destroy $w" -default active
+    pack $w.ok -side bottom
+    $w.ok configure -font uifont
+    bind $w <Visibility> "focus $w.ok"
+    bind $w <Key-Escape> "destroy $w"
+    bind $w <Key-Return> "destroy $w"
+}
+
+# Procedures for manipulating the file list window at the
+# bottom right of the overall window.
+
+proc treeview {w l openlevs} {
+    global treecontents treediropen treeheight treeparent treeindex
+
+    set ix 0
+    set treeindex() 0
+    set lev 0
+    set prefix {}
+    set prefixend -1
+    set prefendstack {}
+    set htstack {}
+    set ht 0
+    set treecontents() {}
+    $w conf -state normal
+    foreach f $l {
+       while {[string range $f 0 $prefixend] ne $prefix} {
+           if {$lev <= $openlevs} {
+               $w mark set e:$treeindex($prefix) "end -1c"
+               $w mark gravity e:$treeindex($prefix) left
+           }
+           set treeheight($prefix) $ht
+           incr ht [lindex $htstack end]
+           set htstack [lreplace $htstack end end]
+           set prefixend [lindex $prefendstack end]
+           set prefendstack [lreplace $prefendstack end end]
+           set prefix [string range $prefix 0 $prefixend]
+           incr lev -1
+       }
+       set tail [string range $f [expr {$prefixend+1}] end]
+       while {[set slash [string first "/" $tail]] >= 0} {
+           lappend htstack $ht
+           set ht 0
+           lappend prefendstack $prefixend
+           incr prefixend [expr {$slash + 1}]
+           set d [string range $tail 0 $slash]
+           lappend treecontents($prefix) $d
+           set oldprefix $prefix
+           append prefix $d
+           set treecontents($prefix) {}
+           set treeindex($prefix) [incr ix]
+           set treeparent($prefix) $oldprefix
+           set tail [string range $tail [expr {$slash+1}] end]
+           if {$lev <= $openlevs} {
+               set ht 1
+               set treediropen($prefix) [expr {$lev < $openlevs}]
+               set bm [expr {$lev == $openlevs? "tri-rt": "tri-dn"}]
+               $w mark set d:$ix "end -1c"
+               $w mark gravity d:$ix left
+               set str "\n"
+               for {set i 0} {$i < $lev} {incr i} {append str "\t"}
+               $w insert end $str
+               $w image create end -align center -image $bm -padx 1 \
+                   -name a:$ix
+               $w insert end $d [highlight_tag $prefix]
+               $w mark set s:$ix "end -1c"
+               $w mark gravity s:$ix left
+           }
+           incr lev
+       }
+       if {$tail ne {}} {
+           if {$lev <= $openlevs} {
+               incr ht
+               set str "\n"
+               for {set i 0} {$i < $lev} {incr i} {append str "\t"}
+               $w insert end $str
+               $w insert end $tail [highlight_tag $f]
+           }
+           lappend treecontents($prefix) $tail
+       }
+    }
+    while {$htstack ne {}} {
+       set treeheight($prefix) $ht
+       incr ht [lindex $htstack end]
+       set htstack [lreplace $htstack end end]
+       set prefixend [lindex $prefendstack end]
+       set prefendstack [lreplace $prefendstack end end]
+       set prefix [string range $prefix 0 $prefixend]
+    }
+    $w conf -state disabled
+}
+
+proc linetoelt {l} {
+    global treeheight treecontents
+
+    set y 2
+    set prefix {}
+    while {1} {
+       foreach e $treecontents($prefix) {
+           if {$y == $l} {
+               return "$prefix$e"
+           }
+           set n 1
+           if {[string index $e end] eq "/"} {
+               set n $treeheight($prefix$e)
+               if {$y + $n > $l} {
+                   append prefix $e
+                   incr y
+                   break
+               }
+           }
+           incr y $n
+       }
+    }
+}
+
+proc highlight_tree {y prefix} {
+    global treeheight treecontents cflist
+
+    foreach e $treecontents($prefix) {
+       set path $prefix$e
+       if {[highlight_tag $path] ne {}} {
+           $cflist tag add bold $y.0 "$y.0 lineend"
+       }
+       incr y
+       if {[string index $e end] eq "/" && $treeheight($path) > 1} {
+           set y [highlight_tree $y $path]
+       }
+    }
+    return $y
+}
+
+proc treeclosedir {w dir} {
+    global treediropen treeheight treeparent treeindex
+
+    set ix $treeindex($dir)
+    $w conf -state normal
+    $w delete s:$ix e:$ix
+    set treediropen($dir) 0
+    $w image configure a:$ix -image tri-rt
+    $w conf -state disabled
+    set n [expr {1 - $treeheight($dir)}]
+    while {$dir ne {}} {
+       incr treeheight($dir) $n
+       set dir $treeparent($dir)
+    }
+}
+
+proc treeopendir {w dir} {
+    global treediropen treeheight treeparent treecontents treeindex
+
+    set ix $treeindex($dir)
+    $w conf -state normal
+    $w image configure a:$ix -image tri-dn
+    $w mark set e:$ix s:$ix
+    $w mark gravity e:$ix right
+    set lev 0
+    set str "\n"
+    set n [llength $treecontents($dir)]
+    for {set x $dir} {$x ne {}} {set x $treeparent($x)} {
+       incr lev
+       append str "\t"
+       incr treeheight($x) $n
+    }
+    foreach e $treecontents($dir) {
+       set de $dir$e
+       if {[string index $e end] eq "/"} {
+           set iy $treeindex($de)
+           $w mark set d:$iy e:$ix
+           $w mark gravity d:$iy left
+           $w insert e:$ix $str
+           set treediropen($de) 0
+           $w image create e:$ix -align center -image tri-rt -padx 1 \
+               -name a:$iy
+           $w insert e:$ix $e [highlight_tag $de]
+           $w mark set s:$iy e:$ix
+           $w mark gravity s:$iy left
+           set treeheight($de) 1
+       } else {
+           $w insert e:$ix $str
+           $w insert e:$ix $e [highlight_tag $de]
+       }
+    }
+    $w mark gravity e:$ix left
+    $w conf -state disabled
+    set treediropen($dir) 1
+    set top [lindex [split [$w index @0,0] .] 0]
+    set ht [$w cget -height]
+    set l [lindex [split [$w index s:$ix] .] 0]
+    if {$l < $top} {
+       $w yview $l.0
+    } elseif {$l + $n + 1 > $top + $ht} {
+       set top [expr {$l + $n + 2 - $ht}]
+       if {$l < $top} {
+           set top $l
+       }
+       $w yview $top.0
+    }
+}
+
+proc treeclick {w x y} {
+    global treediropen cmitmode ctext cflist cflist_top
+
+    if {$cmitmode ne "tree"} return
+    if {![info exists cflist_top]} return
+    set l [lindex [split [$w index "@$x,$y"] "."] 0]
+    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
+    $cflist tag add highlight $l.0 "$l.0 lineend"
+    set cflist_top $l
+    if {$l == 1} {
+       $ctext yview 1.0
+       return
+    }
+    set e [linetoelt $l]
+    if {[string index $e end] ne "/"} {
+       showfile $e
+    } elseif {$treediropen($e)} {
+       treeclosedir $w $e
+    } else {
+       treeopendir $w $e
+    }
+}
+
+proc setfilelist {id} {
+    global treefilelist cflist
+
+    treeview $cflist $treefilelist($id) 0
+}
+
+image create bitmap tri-rt -background black -foreground blue -data {
+    #define tri-rt_width 13
+    #define tri-rt_height 13
+    static unsigned char tri-rt_bits[] = {
+       0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x70, 0x00, 0xf0, 0x00,
+       0xf0, 0x01, 0xf0, 0x00, 0x70, 0x00, 0x30, 0x00, 0x10, 0x00, 0x00, 0x00,
+       0x00, 0x00};
+} -maskdata {
+    #define tri-rt-mask_width 13
+    #define tri-rt-mask_height 13
+    static unsigned char tri-rt-mask_bits[] = {
+       0x08, 0x00, 0x18, 0x00, 0x38, 0x00, 0x78, 0x00, 0xf8, 0x00, 0xf8, 0x01,
+       0xf8, 0x03, 0xf8, 0x01, 0xf8, 0x00, 0x78, 0x00, 0x38, 0x00, 0x18, 0x00,
+       0x08, 0x00};
+}
+image create bitmap tri-dn -background black -foreground blue -data {
+    #define tri-dn_width 13
+    #define tri-dn_height 13
+    static unsigned char tri-dn_bits[] = {
+       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x07, 0xf8, 0x03,
+       0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+       0x00, 0x00};
+} -maskdata {
+    #define tri-dn-mask_width 13
+    #define tri-dn-mask_height 13
+    static unsigned char tri-dn-mask_bits[] = {
+       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x1f, 0xfe, 0x0f, 0xfc, 0x07,
+       0xf8, 0x03, 0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
+       0x00, 0x00};
+}
+
+image create bitmap reficon-T -background black -foreground yellow -data {
+    #define tagicon_width 13
+    #define tagicon_height 9
+    static unsigned char tagicon_bits[] = {
+       0x00, 0x00, 0x00, 0x00, 0xf0, 0x07, 0xf8, 0x07,
+       0xfc, 0x07, 0xf8, 0x07, 0xf0, 0x07, 0x00, 0x00, 0x00, 0x00};
+} -maskdata {
+    #define tagicon-mask_width 13
+    #define tagicon-mask_height 9
+    static unsigned char tagicon-mask_bits[] = {
+       0x00, 0x00, 0xf0, 0x0f, 0xf8, 0x0f, 0xfc, 0x0f,
+       0xfe, 0x0f, 0xfc, 0x0f, 0xf8, 0x0f, 0xf0, 0x0f, 0x00, 0x00};
+}
+set rectdata {
+    #define headicon_width 13
+    #define headicon_height 9
+    static unsigned char headicon_bits[] = {
+       0x00, 0x00, 0x00, 0x00, 0xf8, 0x07, 0xf8, 0x07,
+       0xf8, 0x07, 0xf8, 0x07, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x00};
+}
+set rectmask {
+    #define headicon-mask_width 13
+    #define headicon-mask_height 9
+    static unsigned char headicon-mask_bits[] = {
+       0x00, 0x00, 0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f,
+       0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f, 0x00, 0x00};
+}
+image create bitmap reficon-H -background black -foreground green \
+    -data $rectdata -maskdata $rectmask
+image create bitmap reficon-o -background black -foreground "#ddddff" \
+    -data $rectdata -maskdata $rectmask
+
+proc init_flist {first} {
+    global cflist cflist_top selectedline difffilestart
+
+    $cflist conf -state normal
+    $cflist delete 0.0 end
+    if {$first ne {}} {
+       $cflist insert end $first
+       set cflist_top 1
+       $cflist tag add highlight 1.0 "1.0 lineend"
+    } else {
+       catch {unset cflist_top}
+    }
+    $cflist conf -state disabled
+    set difffilestart {}
+}
+
+proc highlight_tag {f} {
+    global highlight_paths
+
+    foreach p $highlight_paths {
+       if {[string match $p $f]} {
+           return "bold"
+       }
+    }
+    return {}
+}
+
+proc highlight_filelist {} {
+    global cmitmode cflist
+
+    $cflist conf -state normal
+    if {$cmitmode ne "tree"} {
+       set end [lindex [split [$cflist index end] .] 0]
+       for {set l 2} {$l < $end} {incr l} {
+           set line [$cflist get $l.0 "$l.0 lineend"]
+           if {[highlight_tag $line] ne {}} {
+               $cflist tag add bold $l.0 "$l.0 lineend"
+           }
+       }
+    } else {
+       highlight_tree 2 {}
+    }
+    $cflist conf -state disabled
+}
+
+proc unhighlight_filelist {} {
+    global cflist
+
+    $cflist conf -state normal
+    $cflist tag remove bold 1.0 end
+    $cflist conf -state disabled
+}
+
+proc add_flist {fl} {
+    global cflist
+
+    $cflist conf -state normal
+    foreach f $fl {
+       $cflist insert end "\n"
+       $cflist insert end $f [highlight_tag $f]
+    }
+    $cflist conf -state disabled
+}
+
+proc sel_flist {w x y} {
+    global ctext difffilestart cflist cflist_top cmitmode
+
+    if {$cmitmode eq "tree"} return
+    if {![info exists cflist_top]} return
+    set l [lindex [split [$w index "@$x,$y"] "."] 0]
+    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
+    $cflist tag add highlight $l.0 "$l.0 lineend"
+    set cflist_top $l
+    if {$l == 1} {
+       $ctext yview 1.0
+    } else {
+       catch {$ctext yview [lindex $difffilestart [expr {$l - 2}]]}
+    }
+}
+
+proc pop_flist_menu {w X Y x y} {
+    global ctext cflist cmitmode flist_menu flist_menu_file
+    global treediffs diffids
+
+    stopfinding
+    set l [lindex [split [$w index "@$x,$y"] "."] 0]
+    if {$l <= 1} return
+    if {$cmitmode eq "tree"} {
+       set e [linetoelt $l]
+       if {[string index $e end] eq "/"} return
+    } else {
+       set e [lindex $treediffs($diffids) [expr {$l-2}]]
+    }
+    set flist_menu_file $e
+    tk_popup $flist_menu $X $Y
+}
+
+proc flist_hl {only} {
+    global flist_menu_file findstring gdttype
+
+    set x [shellquote $flist_menu_file]
+    if {$only || $findstring eq {} || $gdttype ne "touching paths:"} {
+       set findstring $x
+    } else {
+       append findstring " " $x
+    }
+    set gdttype "touching paths:"
+}
+
+# Functions for adding and removing shell-type quoting
+
+proc shellquote {str} {
+    if {![string match "*\['\"\\ \t]*" $str]} {
+       return $str
+    }
+    if {![string match "*\['\"\\]*" $str]} {
+       return "\"$str\""
+    }
+    if {![string match "*'*" $str]} {
+       return "'$str'"
+    }
+    return "\"[string map {\" \\\" \\ \\\\} $str]\""
+}
+
+proc shellarglist {l} {
+    set str {}
+    foreach a $l {
+       if {$str ne {}} {
+           append str " "
+       }
+       append str [shellquote $a]
+    }
+    return $str
+}
+
+proc shelldequote {str} {
+    set ret {}
+    set used -1
+    while {1} {
+       incr used
+       if {![regexp -start $used -indices "\['\"\\\\ \t]" $str first]} {
+           append ret [string range $str $used end]
+           set used [string length $str]
+           break
+       }
+       set first [lindex $first 0]
+       set ch [string index $str $first]
+       if {$first > $used} {
+           append ret [string range $str $used [expr {$first - 1}]]
+           set used $first
+       }
+       if {$ch eq " " || $ch eq "\t"} break
+       incr used
+       if {$ch eq "'"} {
+           set first [string first "'" $str $used]
+           if {$first < 0} {
+               error "unmatched single-quote"
+           }
+           append ret [string range $str $used [expr {$first - 1}]]
+           set used $first
+           continue
+       }
+       if {$ch eq "\\"} {
+           if {$used >= [string length $str]} {
+               error "trailing backslash"
+           }
+           append ret [string index $str $used]
+           continue
+       }
+       # here ch == "\""
+       while {1} {
+           if {![regexp -start $used -indices "\[\"\\\\]" $str first]} {
+               error "unmatched double-quote"
+           }
+           set first [lindex $first 0]
+           set ch [string index $str $first]
+           if {$first > $used} {
+               append ret [string range $str $used [expr {$first - 1}]]
+               set used $first
+           }
+           if {$ch eq "\""} break
+           incr used
+           append ret [string index $str $used]
+           incr used
+       }
+    }
+    return [list $used $ret]
+}
+
+proc shellsplit {str} {
+    set l {}
+    while {1} {
+       set str [string trimleft $str]
+       if {$str eq {}} break
+       set dq [shelldequote $str]
+       set n [lindex $dq 0]
+       set word [lindex $dq 1]
+       set str [string range $str $n end]
+       lappend l $word
+    }
+    return $l
+}
+
+# Code to implement multiple views
+
+proc newview {ishighlight} {
+    global nextviewnum newviewname newviewperm uifont newishighlight
+    global newviewargs revtreeargs
+
+    set newishighlight $ishighlight
+    set top .gitkview
+    if {[winfo exists $top]} {
+       raise $top
+       return
+    }
+    set newviewname($nextviewnum) "View $nextviewnum"
+    set newviewperm($nextviewnum) 0
+    set newviewargs($nextviewnum) [shellarglist $revtreeargs]
+    vieweditor $top $nextviewnum "Gitk view definition"
+}
+
+proc editview {} {
+    global curview
+    global viewname viewperm newviewname newviewperm
+    global viewargs newviewargs
+
+    set top .gitkvedit-$curview
+    if {[winfo exists $top]} {
+       raise $top
+       return
+    }
+    set newviewname($curview) $viewname($curview)
+    set newviewperm($curview) $viewperm($curview)
+    set newviewargs($curview) [shellarglist $viewargs($curview)]
+    vieweditor $top $curview "Gitk: edit view $viewname($curview)"
+}
+
+proc vieweditor {top n title} {
+    global newviewname newviewperm viewfiles
+    global uifont
+
+    toplevel $top
+    wm title $top $title
+    label $top.nl -text "Name" -font uifont
+    entry $top.name -width 20 -textvariable newviewname($n) -font uifont
+    grid $top.nl $top.name -sticky w -pady 5
+    checkbutton $top.perm -text "Remember this view" -variable newviewperm($n) \
+       -font uifont
+    grid $top.perm - -pady 5 -sticky w
+    message $top.al -aspect 1000 -font uifont \
+       -text "Commits to include (arguments to git rev-list):"
+    grid $top.al - -sticky w -pady 5
+    entry $top.args -width 50 -textvariable newviewargs($n) \
+       -background white -font uifont
+    grid $top.args - -sticky ew -padx 5
+    message $top.l -aspect 1000 -font uifont \
+       -text "Enter files and directories to include, one per line:"
+    grid $top.l - -sticky w
+    text $top.t -width 40 -height 10 -background white -font uifont
+    if {[info exists viewfiles($n)]} {
+       foreach f $viewfiles($n) {
+           $top.t insert end $f
+           $top.t insert end "\n"
+       }
+       $top.t delete {end - 1c} end
+       $top.t mark set insert 0.0
+    }
+    grid $top.t - -sticky ew -padx 5
+    frame $top.buts
+    button $top.buts.ok -text "OK" -command [list newviewok $top $n] \
+       -font uifont
+    button $top.buts.can -text "Cancel" -command [list destroy $top] \
+       -font uifont
+    grid $top.buts.ok $top.buts.can
+    grid columnconfigure $top.buts 0 -weight 1 -uniform a
+    grid columnconfigure $top.buts 1 -weight 1 -uniform a
+    grid $top.buts - -pady 10 -sticky ew
+    focus $top.t
+}
+
+proc doviewmenu {m first cmd op argv} {
+    set nmenu [$m index end]
+    for {set i $first} {$i <= $nmenu} {incr i} {
+       if {[$m entrycget $i -command] eq $cmd} {
+           eval $m $op $i $argv
+           break
+       }
+    }
+}
+
+proc allviewmenus {n op args} {
+    # global viewhlmenu
+
+    doviewmenu .bar.view 5 [list showview $n] $op $args
+    # doviewmenu $viewhlmenu 1 [list addvhighlight $n] $op $args
+}
+
+proc newviewok {top n} {
+    global nextviewnum newviewperm newviewname newishighlight
+    global viewname viewfiles viewperm selectedview curview
+    global viewargs newviewargs viewhlmenu
+
+    if {[catch {
+       set newargs [shellsplit $newviewargs($n)]
+    } err]} {
+       error_popup "Error in commit selection arguments: $err"
+       wm raise $top
+       focus $top
+       return
+    }
+    set files {}
+    foreach f [split [$top.t get 0.0 end] "\n"] {
+       set ft [string trim $f]
+       if {$ft ne {}} {
+           lappend files $ft
+       }
+    }
+    if {![info exists viewfiles($n)]} {
+       # creating a new view
+       incr nextviewnum
+       set viewname($n) $newviewname($n)
+       set viewperm($n) $newviewperm($n)
+       set viewfiles($n) $files
+       set viewargs($n) $newargs
+       addviewmenu $n
+       if {!$newishighlight} {
+           run showview $n
+       } else {
+           run addvhighlight $n
+       }
+    } else {
+       # editing an existing view
+       set viewperm($n) $newviewperm($n)
+       if {$newviewname($n) ne $viewname($n)} {
+           set viewname($n) $newviewname($n)
+           doviewmenu .bar.view 5 [list showview $n] \
+               entryconf [list -label $viewname($n)]
+           # doviewmenu $viewhlmenu 1 [list addvhighlight $n] \
+               # entryconf [list -label $viewname($n) -value $viewname($n)]
+       }
+       if {$files ne $viewfiles($n) || $newargs ne $viewargs($n)} {
+           set viewfiles($n) $files
+           set viewargs($n) $newargs
+           if {$curview == $n} {
+               run updatecommits
+           }
+       }
+    }
+    catch {destroy $top}
+}
+
+proc delview {} {
+    global curview viewdata viewperm hlview selectedhlview
+
+    if {$curview == 0} return
+    if {[info exists hlview] && $hlview == $curview} {
+       set selectedhlview None
+       unset hlview
+    }
+    allviewmenus $curview delete
+    set viewdata($curview) {}
+    set viewperm($curview) 0
+    showview 0
+}
+
+proc addviewmenu {n} {
+    global viewname viewhlmenu
+
+    .bar.view add radiobutton -label $viewname($n) \
+       -command [list showview $n] -variable selectedview -value $n
+    #$viewhlmenu add radiobutton -label $viewname($n) \
+    #  -command [list addvhighlight $n] -variable selectedhlview
+}
+
+proc flatten {var} {
+    global $var
+
+    set ret {}
+    foreach i [array names $var] {
+       lappend ret $i [set $var\($i\)]
+    }
+    return $ret
+}
+
+proc unflatten {var l} {
+    global $var
+
+    catch {unset $var}
+    foreach {i v} $l {
+       set $var\($i\) $v
+    }
+}
+
+proc showview {n} {
+    global curview viewdata viewfiles
+    global displayorder parentlist rowidlist rowisopt rowfinal
+    global colormap rowtextx commitrow nextcolor canvxmax
+    global numcommits commitlisted
+    global selectedline currentid canv canvy0
+    global treediffs
+    global pending_select phase
+    global commitidx
+    global commfd
+    global selectedview selectfirst
+    global vparentlist vdisporder vcmitlisted
+    global hlview selectedhlview commitinterest
+
+    if {$n == $curview} return
+    set selid {}
+    if {[info exists selectedline]} {
+       set selid $currentid
+       set y [yc $selectedline]
+       set ymax [lindex [$canv cget -scrollregion] 3]
+       set span [$canv yview]
+       set ytop [expr {[lindex $span 0] * $ymax}]
+       set ybot [expr {[lindex $span 1] * $ymax}]
+       if {$ytop < $y && $y < $ybot} {
+           set yscreen [expr {$y - $ytop}]
+       } else {
+           set yscreen [expr {($ybot - $ytop) / 2}]
+       }
+    } elseif {[info exists pending_select]} {
+       set selid $pending_select
+       unset pending_select
+    }
+    unselectline
+    normalline
+    if {$curview >= 0} {
+       set vparentlist($curview) $parentlist
+       set vdisporder($curview) $displayorder
+       set vcmitlisted($curview) $commitlisted
+       if {$phase ne {} ||
+           ![info exists viewdata($curview)] ||
+           [lindex $viewdata($curview) 0] ne {}} {
+           set viewdata($curview) \
+               [list $phase $rowidlist $rowisopt $rowfinal]
+       }
+    }
+    catch {unset treediffs}
+    clear_display
+    if {[info exists hlview] && $hlview == $n} {
+       unset hlview
+       set selectedhlview None
+    }
+    catch {unset commitinterest}
+
+    set curview $n
+    set selectedview $n
+    .bar.view entryconf Edit* -state [expr {$n == 0? "disabled": "normal"}]
+    .bar.view entryconf Delete* -state [expr {$n == 0? "disabled": "normal"}]
+
+    run refill_reflist
+    if {![info exists viewdata($n)]} {
+       if {$selid ne {}} {
+           set pending_select $selid
+       }
+       getcommits
+       return
+    }
+
+    set v $viewdata($n)
+    set phase [lindex $v 0]
+    set displayorder $vdisporder($n)
+    set parentlist $vparentlist($n)
+    set commitlisted $vcmitlisted($n)
+    set rowidlist [lindex $v 1]
+    set rowisopt [lindex $v 2]
+    set rowfinal [lindex $v 3]
+    set numcommits $commitidx($n)
+
+    catch {unset colormap}
+    catch {unset rowtextx}
+    set nextcolor 0
+    set canvxmax [$canv cget -width]
+    set curview $n
+    set row 0
+    setcanvscroll
+    set yf 0
+    set row {}
+    set selectfirst 0
+    if {$selid ne {} && [info exists commitrow($n,$selid)]} {
+       set row $commitrow($n,$selid)
+       # try to get the selected row in the same position on the screen
+       set ymax [lindex [$canv cget -scrollregion] 3]
+       set ytop [expr {[yc $row] - $yscreen}]
+       if {$ytop < 0} {
+           set ytop 0
+       }
+       set yf [expr {$ytop * 1.0 / $ymax}]
+    }
+    allcanvs yview moveto $yf
+    drawvisible
+    if {$row ne {}} {
+       selectline $row 0
+    } elseif {$selid ne {}} {
+       set pending_select $selid
+    } else {
+       set row [first_real_row]
+       if {$row < $numcommits} {
+           selectline $row 0
+       } else {
+           set selectfirst 1
+       }
+    }
+    if {$phase ne {}} {
+       if {$phase eq "getcommits"} {
+           show_status "Reading commits..."
+       }
+       run chewcommits $n
+    } elseif {$numcommits == 0} {
+       show_status "No commits selected"
+    }
+}
+
+# Stuff relating to the highlighting facility
+
+proc ishighlighted {row} {
+    global vhighlights fhighlights nhighlights rhighlights
+
+    if {[info exists nhighlights($row)] && $nhighlights($row) > 0} {
+       return $nhighlights($row)
+    }
+    if {[info exists vhighlights($row)] && $vhighlights($row) > 0} {
+       return $vhighlights($row)
+    }
+    if {[info exists fhighlights($row)] && $fhighlights($row) > 0} {
+       return $fhighlights($row)
+    }
+    if {[info exists rhighlights($row)] && $rhighlights($row) > 0} {
+       return $rhighlights($row)
+    }
+    return 0
+}
+
+proc bolden {row font} {
+    global canv linehtag selectedline boldrows
+
+    lappend boldrows $row
+    $canv itemconf $linehtag($row) -font $font
+    if {[info exists selectedline] && $row == $selectedline} {
+       $canv delete secsel
+       set t [eval $canv create rect [$canv bbox $linehtag($row)] \
+                  -outline {{}} -tags secsel \
+                  -fill [$canv cget -selectbackground]]
+       $canv lower $t
+    }
+}
+
+proc bolden_name {row font} {
+    global canv2 linentag selectedline boldnamerows
+
+    lappend boldnamerows $row
+    $canv2 itemconf $linentag($row) -font $font
+    if {[info exists selectedline] && $row == $selectedline} {
+       $canv2 delete secsel
+       set t [eval $canv2 create rect [$canv2 bbox $linentag($row)] \
+                  -outline {{}} -tags secsel \
+                  -fill [$canv2 cget -selectbackground]]
+       $canv2 lower $t
+    }
+}
+
+proc unbolden {} {
+    global boldrows
+
+    set stillbold {}
+    foreach row $boldrows {
+       if {![ishighlighted $row]} {
+           bolden $row mainfont
+       } else {
+           lappend stillbold $row
+       }
+    }
+    set boldrows $stillbold
+}
+
+proc addvhighlight {n} {
+    global hlview curview viewdata vhl_done vhighlights commitidx
+
+    if {[info exists hlview]} {
+       delvhighlight
+    }
+    set hlview $n
+    if {$n != $curview && ![info exists viewdata($n)]} {
+       set viewdata($n) [list getcommits {{}} 0 0 0]
+       set vparentlist($n) {}
+       set vdisporder($n) {}
+       set vcmitlisted($n) {}
+       start_rev_list $n
+    }
+    set vhl_done $commitidx($hlview)
+    if {$vhl_done > 0} {
+       drawvisible
+    }
+}
+
+proc delvhighlight {} {
+    global hlview vhighlights
+
+    if {![info exists hlview]} return
+    unset hlview
+    catch {unset vhighlights}
+    unbolden
+}
+
+proc vhighlightmore {} {
+    global hlview vhl_done commitidx vhighlights
+    global displayorder vdisporder curview
+
+    set max $commitidx($hlview)
+    if {$hlview == $curview} {
+       set disp $displayorder
+    } else {
+       set disp $vdisporder($hlview)
+    }
+    set vr [visiblerows]
+    set r0 [lindex $vr 0]
+    set r1 [lindex $vr 1]
+    for {set i $vhl_done} {$i < $max} {incr i} {
+       set id [lindex $disp $i]
+       if {[info exists commitrow($curview,$id)]} {
+           set row $commitrow($curview,$id)
+           if {$r0 <= $row && $row <= $r1} {
+               if {![highlighted $row]} {
+                   bolden $row mainfontbold
+               }
+               set vhighlights($row) 1
+           }
+       }
+    }
+    set vhl_done $max
+}
+
+proc askvhighlight {row id} {
+    global hlview vhighlights commitrow iddrawn
+
+    if {[info exists commitrow($hlview,$id)]} {
+       if {[info exists iddrawn($id)] && ![ishighlighted $row]} {
+           bolden $row mainfontbold
+       }
+       set vhighlights($row) 1
+    } else {
+       set vhighlights($row) 0
+    }
+}
+
+proc hfiles_change {} {
+    global highlight_files filehighlight fhighlights fh_serial
+    global highlight_paths gdttype
+
+    if {[info exists filehighlight]} {
+       # delete previous highlights
+       catch {close $filehighlight}
+       unset filehighlight
+       catch {unset fhighlights}
+       unbolden
+       unhighlight_filelist
+    }
+    set highlight_paths {}
+    after cancel do_file_hl $fh_serial
+    incr fh_serial
+    if {$highlight_files ne {}} {
+       after 300 do_file_hl $fh_serial
+    }
+}
+
+proc gdttype_change {name ix op} {
+    global gdttype highlight_files findstring findpattern
+
+    stopfinding
+    if {$findstring ne {}} {
+       if {$gdttype eq "containing:"} {
+           if {$highlight_files ne {}} {
+               set highlight_files {}
+               hfiles_change
+           }
+           findcom_change
+       } else {
+           if {$findpattern ne {}} {
+               set findpattern {}
+               findcom_change
+           }
+           set highlight_files $findstring
+           hfiles_change
+       }
+       drawvisible
+    }
+    # enable/disable findtype/findloc menus too
+}
+
+proc find_change {name ix op} {
+    global gdttype findstring highlight_files
+
+    stopfinding
+    if {$gdttype eq "containing:"} {
+       findcom_change
+    } else {
+       if {$highlight_files ne $findstring} {
+           set highlight_files $findstring
+           hfiles_change
+       }
+    }
+    drawvisible
+}
+
+proc findcom_change args {
+    global nhighlights boldnamerows
+    global findpattern findtype findstring gdttype
+
+    stopfinding
+    # delete previous highlights, if any
+    foreach row $boldnamerows {
+       bolden_name $row mainfont
+    }
+    set boldnamerows {}
+    catch {unset nhighlights}
+    unbolden
+    unmarkmatches
+    if {$gdttype ne "containing:" || $findstring eq {}} {
+       set findpattern {}
+    } elseif {$findtype eq "Regexp"} {
+       set findpattern $findstring
+    } else {
+       set e [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} \
+                  $findstring]
+       set findpattern "*$e*"
+    }
+}
+
+proc makepatterns {l} {
+    set ret {}
+    foreach e $l {
+       set ee [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} $e]
+       if {[string index $ee end] eq "/"} {
+           lappend ret "$ee*"
+       } else {
+           lappend ret $ee
+           lappend ret "$ee/*"
+       }
+    }
+    return $ret
+}
+
+proc do_file_hl {serial} {
+    global highlight_files filehighlight highlight_paths gdttype fhl_list
+
+    if {$gdttype eq "touching paths:"} {
+       if {[catch {set paths [shellsplit $highlight_files]}]} return
+       set highlight_paths [makepatterns $paths]
+       highlight_filelist
+       set gdtargs [concat -- $paths]
+    } elseif {$gdttype eq "adding/removing string:"} {
+       set gdtargs [list "-S$highlight_files"]
+    } else {
+       # must be "containing:", i.e. we're searching commit info
+       return
+    }
+    set cmd [concat | git diff-tree -r -s --stdin $gdtargs]
+    set filehighlight [open $cmd r+]
+    fconfigure $filehighlight -blocking 0
+    filerun $filehighlight readfhighlight
+    set fhl_list {}
+    drawvisible
+    flushhighlights
+}
+
+proc flushhighlights {} {
+    global filehighlight fhl_list
+
+    if {[info exists filehighlight]} {
+       lappend fhl_list {}
+       puts $filehighlight ""
+       flush $filehighlight
+    }
+}
+
+proc askfilehighlight {row id} {
+    global filehighlight fhighlights fhl_list
+
+    lappend fhl_list $id
+    set fhighlights($row) -1
+    puts $filehighlight $id
+}
+
+proc readfhighlight {} {
+    global filehighlight fhighlights commitrow curview iddrawn
+    global fhl_list find_dirn
+
+    if {![info exists filehighlight]} {
+       return 0
+    }
+    set nr 0
+    while {[incr nr] <= 100 && [gets $filehighlight line] >= 0} {
+       set line [string trim $line]
+       set i [lsearch -exact $fhl_list $line]
+       if {$i < 0} continue
+       for {set j 0} {$j < $i} {incr j} {
+           set id [lindex $fhl_list $j]
+           if {[info exists commitrow($curview,$id)]} {
+               set fhighlights($commitrow($curview,$id)) 0
+           }
+       }
+       set fhl_list [lrange $fhl_list [expr {$i+1}] end]
+       if {$line eq {}} continue
+       if {![info exists commitrow($curview,$line)]} continue
+       set row $commitrow($curview,$line)
+       if {[info exists iddrawn($line)] && ![ishighlighted $row]} {
+           bolden $row mainfontbold
+       }
+       set fhighlights($row) 1
+    }
+    if {[eof $filehighlight]} {
+       # strange...
+       puts "oops, git diff-tree died"
+       catch {close $filehighlight}
+       unset filehighlight
+       return 0
+    }
+    if {[info exists find_dirn]} {
+       run findmore
+    }
+    return 1
+}
+
+proc doesmatch {f} {
+    global findtype findpattern
+
+    if {$findtype eq "Regexp"} {
+       return [regexp $findpattern $f]
+    } elseif {$findtype eq "IgnCase"} {
+       return [string match -nocase $findpattern $f]
+    } else {
+       return [string match $findpattern $f]
+    }
+}
+
+proc askfindhighlight {row id} {
+    global nhighlights commitinfo iddrawn
+    global findloc
+    global markingmatches
+
+    if {![info exists commitinfo($id)]} {
+       getcommit $id
+    }
+    set info $commitinfo($id)
+    set isbold 0
+    set fldtypes {Headline Author Date Committer CDate Comments}
+    foreach f $info ty $fldtypes {
+       if {($findloc eq "All fields" || $findloc eq $ty) &&
+           [doesmatch $f]} {
+           if {$ty eq "Author"} {
+               set isbold 2
+               break
+           }
+           set isbold 1
+       }
+    }
+    if {$isbold && [info exists iddrawn($id)]} {
+       if {![ishighlighted $row]} {
+           bolden $row mainfontbold
+           if {$isbold > 1} {
+               bolden_name $row mainfontbold
+           }
+       }
+       if {$markingmatches} {
+           markrowmatches $row $id
+       }
+    }
+    set nhighlights($row) $isbold
+}
+
+proc markrowmatches {row id} {
+    global canv canv2 linehtag linentag commitinfo findloc
+
+    set headline [lindex $commitinfo($id) 0]
+    set author [lindex $commitinfo($id) 1]
+    $canv delete match$row
+    $canv2 delete match$row
+    if {$findloc eq "All fields" || $findloc eq "Headline"} {
+       set m [findmatches $headline]
+       if {$m ne {}} {
+           markmatches $canv $row $headline $linehtag($row) $m \
+               [$canv itemcget $linehtag($row) -font] $row
+       }
+    }
+    if {$findloc eq "All fields" || $findloc eq "Author"} {
+       set m [findmatches $author]
+       if {$m ne {}} {
+           markmatches $canv2 $row $author $linentag($row) $m \
+               [$canv2 itemcget $linentag($row) -font] $row
+       }
+    }
+}
+
+proc vrel_change {name ix op} {
+    global highlight_related
+
+    rhighlight_none
+    if {$highlight_related ne "None"} {
+       run drawvisible
+    }
+}
+
+# prepare for testing whether commits are descendents or ancestors of a
+proc rhighlight_sel {a} {
+    global descendent desc_todo ancestor anc_todo
+    global highlight_related rhighlights
+
+    catch {unset descendent}
+    set desc_todo [list $a]
+    catch {unset ancestor}
+    set anc_todo [list $a]
+    if {$highlight_related ne "None"} {
+       rhighlight_none
+       run drawvisible
+    }
+}
+
+proc rhighlight_none {} {
+    global rhighlights
+
+    catch {unset rhighlights}
+    unbolden
+}
+
+proc is_descendent {a} {
+    global curview children commitrow descendent desc_todo
+
+    set v $curview
+    set la $commitrow($v,$a)
+    set todo $desc_todo
+    set leftover {}
+    set done 0
+    for {set i 0} {$i < [llength $todo]} {incr i} {
+       set do [lindex $todo $i]
+       if {$commitrow($v,$do) < $la} {
+           lappend leftover $do
+           continue
+       }
+       foreach nk $children($v,$do) {
+           if {![info exists descendent($nk)]} {
+               set descendent($nk) 1
+               lappend todo $nk
+               if {$nk eq $a} {
+                   set done 1
+               }
+           }
+       }
+       if {$done} {
+           set desc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
+           return
+       }
+    }
+    set descendent($a) 0
+    set desc_todo $leftover
+}
+
+proc is_ancestor {a} {
+    global curview parentlist commitrow ancestor anc_todo
+
+    set v $curview
+    set la $commitrow($v,$a)
+    set todo $anc_todo
+    set leftover {}
+    set done 0
+    for {set i 0} {$i < [llength $todo]} {incr i} {
+       set do [lindex $todo $i]
+       if {![info exists commitrow($v,$do)] || $commitrow($v,$do) > $la} {
+           lappend leftover $do
+           continue
+       }
+       foreach np [lindex $parentlist $commitrow($v,$do)] {
+           if {![info exists ancestor($np)]} {
+               set ancestor($np) 1
+               lappend todo $np
+               if {$np eq $a} {
+                   set done 1
+               }
+           }
+       }
+       if {$done} {
+           set anc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
+           return
+       }
+    }
+    set ancestor($a) 0
+    set anc_todo $leftover
+}
+
+proc askrelhighlight {row id} {
+    global descendent highlight_related iddrawn rhighlights
+    global selectedline ancestor
+
+    if {![info exists selectedline]} return
+    set isbold 0
+    if {$highlight_related eq "Descendent" ||
+       $highlight_related eq "Not descendent"} {
+       if {![info exists descendent($id)]} {
+           is_descendent $id
+       }
+       if {$descendent($id) == ($highlight_related eq "Descendent")} {
+           set isbold 1
+       }
+    } elseif {$highlight_related eq "Ancestor" ||
+             $highlight_related eq "Not ancestor"} {
+       if {![info exists ancestor($id)]} {
+           is_ancestor $id
+       }
+       if {$ancestor($id) == ($highlight_related eq "Ancestor")} {
+           set isbold 1
+       }
+    }
+    if {[info exists iddrawn($id)]} {
+       if {$isbold && ![ishighlighted $row]} {
+           bolden $row mainfontbold
+       }
+    }
+    set rhighlights($row) $isbold
+}
+
+# Graph layout functions
+
+proc shortids {ids} {
+    set res {}
+    foreach id $ids {
+       if {[llength $id] > 1} {
+           lappend res [shortids $id]
+       } elseif {[regexp {^[0-9a-f]{40}$} $id]} {
+           lappend res [string range $id 0 7]
+       } else {
+           lappend res $id
+       }
+    }
+    return $res
+}
+
+proc ntimes {n o} {
+    set ret {}
+    set o [list $o]
+    for {set mask 1} {$mask <= $n} {incr mask $mask} {
+       if {($n & $mask) != 0} {
+           set ret [concat $ret $o]
+       }
+       set o [concat $o $o]
+    }
+    return $ret
+}
+
+# Work out where id should go in idlist so that order-token
+# values increase from left to right
+proc idcol {idlist id {i 0}} {
+    global ordertok curview
+
+    set t $ordertok($curview,$id)
+    if {$i >= [llength $idlist] ||
+       $t < $ordertok($curview,[lindex $idlist $i])} {
+       if {$i > [llength $idlist]} {
+           set i [llength $idlist]
+       }
+       while {[incr i -1] >= 0 &&
+              $t < $ordertok($curview,[lindex $idlist $i])} {}
+       incr i
+    } else {
+       if {$t > $ordertok($curview,[lindex $idlist $i])} {
+           while {[incr i] < [llength $idlist] &&
+                  $t >= $ordertok($curview,[lindex $idlist $i])} {}
+       }
+    }
+    return $i
+}
+
+proc initlayout {} {
+    global rowidlist rowisopt rowfinal displayorder commitlisted
+    global numcommits canvxmax canv
+    global nextcolor
+    global parentlist
+    global colormap rowtextx
+    global selectfirst
+
+    set numcommits 0
+    set displayorder {}
+    set commitlisted {}
+    set parentlist {}
+    set nextcolor 0
+    set rowidlist {}
+    set rowisopt {}
+    set rowfinal {}
+    set canvxmax [$canv cget -width]
+    catch {unset colormap}
+    catch {unset rowtextx}
+    set selectfirst 1
+}
+
+proc setcanvscroll {} {
+    global canv canv2 canv3 numcommits linespc canvxmax canvy0
+
+    set ymax [expr {$canvy0 + ($numcommits - 0.5) * $linespc + 2}]
+    $canv conf -scrollregion [list 0 0 $canvxmax $ymax]
+    $canv2 conf -scrollregion [list 0 0 0 $ymax]
+    $canv3 conf -scrollregion [list 0 0 0 $ymax]
+}
+
+proc visiblerows {} {
+    global canv numcommits linespc
+
+    set ymax [lindex [$canv cget -scrollregion] 3]
+    if {$ymax eq {} || $ymax == 0} return
+    set f [$canv yview]
+    set y0 [expr {int([lindex $f 0] * $ymax)}]
+    set r0 [expr {int(($y0 - 3) / $linespc) - 1}]
+    if {$r0 < 0} {
+       set r0 0
+    }
+    set y1 [expr {int([lindex $f 1] * $ymax)}]
+    set r1 [expr {int(($y1 - 3) / $linespc) + 1}]
+    if {$r1 >= $numcommits} {
+       set r1 [expr {$numcommits - 1}]
+    }
+    return [list $r0 $r1]
+}
+
+proc layoutmore {} {
+    global commitidx viewcomplete numcommits
+    global uparrowlen downarrowlen mingaplen curview
+
+    set show $commitidx($curview)
+    if {$show > $numcommits || $viewcomplete($curview)} {
+       showstuff $show $viewcomplete($curview)
+    }
+}
+
+proc showstuff {canshow last} {
+    global numcommits commitrow pending_select selectedline curview
+    global mainheadid displayorder selectfirst
+    global lastscrollset commitinterest
+
+    if {$numcommits == 0} {
+       global phase
+       set phase "incrdraw"
+       allcanvs delete all
+    }
+    set r0 $numcommits
+    set prev $numcommits
+    set numcommits $canshow
+    set t [clock clicks -milliseconds]
+    if {$prev < 100 || $last || $t - $lastscrollset > 500} {
+       set lastscrollset $t
+       setcanvscroll
+    }
+    set rows [visiblerows]
+    set r1 [lindex $rows 1]
+    if {$r1 >= $canshow} {
+       set r1 [expr {$canshow - 1}]
+    }
+    if {$r0 <= $r1} {
+       drawcommits $r0 $r1
+    }
+    if {[info exists pending_select] &&
+       [info exists commitrow($curview,$pending_select)] &&
+       $commitrow($curview,$pending_select) < $numcommits} {
+       selectline $commitrow($curview,$pending_select) 1
+    }
+    if {$selectfirst} {
+       if {[info exists selectedline] || [info exists pending_select]} {
+           set selectfirst 0
+       } else {
+           set l [first_real_row]
+           selectline $l 1
+           set selectfirst 0
+       }
+    }
+}
+
+proc doshowlocalchanges {} {
+    global curview mainheadid phase commitrow
+
+    if {[info exists commitrow($curview,$mainheadid)] &&
+       ($phase eq {} || $commitrow($curview,$mainheadid) < $numcommits - 1)} {
+       dodiffindex
+    } elseif {$phase ne {}} {
+       lappend commitinterest($mainheadid) {}
+    }
+}
+
+proc dohidelocalchanges {} {
+    global localfrow localirow lserial
+
+    if {$localfrow >= 0} {
+       removerow $localfrow
+       set localfrow -1
+       if {$localirow > 0} {
+           incr localirow -1
+       }
+    }
+    if {$localirow >= 0} {
+       removerow $localirow
+       set localirow -1
+    }
+    incr lserial
+}
+
+# spawn off a process to do git diff-index --cached HEAD
+proc dodiffindex {} {
+    global localirow localfrow lserial showlocalchanges
+
+    if {!$showlocalchanges} return
+    incr lserial
+    set localfrow -1
+    set localirow -1
+    set fd [open "|git diff-index --cached HEAD" r]
+    fconfigure $fd -blocking 0
+    filerun $fd [list readdiffindex $fd $lserial]
+}
+
+proc readdiffindex {fd serial} {
+    global localirow commitrow mainheadid nullid2 curview
+    global commitinfo commitdata lserial
+
+    set isdiff 1
+    if {[gets $fd line] < 0} {
+       if {![eof $fd]} {
+           return 1
+       }
+       set isdiff 0
+    }
+    # we only need to see one line and we don't really care what it says...
+    close $fd
+
+    # now see if there are any local changes not checked in to the index
+    if {$serial == $lserial} {
+       set fd [open "|git diff-files" r]
+       fconfigure $fd -blocking 0
+       filerun $fd [list readdifffiles $fd $serial]
+    }
+
+    if {$isdiff && $serial == $lserial && $localirow == -1} {
+       # add the line for the changes in the index to the graph
+       set localirow $commitrow($curview,$mainheadid)
+       set hl "Local changes checked in to index but not committed"
+       set commitinfo($nullid2) [list  $hl {} {} {} {} "    $hl\n"]
+       set commitdata($nullid2) "\n    $hl\n"
+       insertrow $localirow $nullid2
+    }
+    return 0
+}
+
+proc readdifffiles {fd serial} {
+    global localirow localfrow commitrow mainheadid nullid curview
+    global commitinfo commitdata lserial
+
+    set isdiff 1
+    if {[gets $fd line] < 0} {
+       if {![eof $fd]} {
+           return 1
+       }
+       set isdiff 0
+    }
+    # we only need to see one line and we don't really care what it says...
+    close $fd
+
+    if {$isdiff && $serial == $lserial && $localfrow == -1} {
+       # add the line for the local diff to the graph
+       if {$localirow >= 0} {
+           set localfrow $localirow
+           incr localirow
+       } else {
+           set localfrow $commitrow($curview,$mainheadid)
+       }
+       set hl "Local uncommitted changes, not checked in to index"
+       set commitinfo($nullid) [list  $hl {} {} {} {} "    $hl\n"]
+       set commitdata($nullid) "\n    $hl\n"
+       insertrow $localfrow $nullid
+    }
+    return 0
+}
+
+proc nextuse {id row} {
+    global commitrow curview children
+
+    if {[info exists children($curview,$id)]} {
+       foreach kid $children($curview,$id) {
+           if {![info exists commitrow($curview,$kid)]} {
+               return -1
+           }
+           if {$commitrow($curview,$kid) > $row} {
+               return $commitrow($curview,$kid)
+           }
+       }
+    }
+    if {[info exists commitrow($curview,$id)]} {
+       return $commitrow($curview,$id)
+    }
+    return -1
+}
+
+proc prevuse {id row} {
+    global commitrow curview children
+
+    set ret -1
+    if {[info exists children($curview,$id)]} {
+       foreach kid $children($curview,$id) {
+           if {![info exists commitrow($curview,$kid)]} break
+           if {$commitrow($curview,$kid) < $row} {
+               set ret $commitrow($curview,$kid)
+           }
+       }
+    }
+    return $ret
+}
+
+proc make_idlist {row} {
+    global displayorder parentlist uparrowlen downarrowlen mingaplen
+    global commitidx curview ordertok children commitrow
+
+    set r [expr {$row - $mingaplen - $downarrowlen - 1}]
+    if {$r < 0} {
+       set r 0
+    }
+    set ra [expr {$row - $downarrowlen}]
+    if {$ra < 0} {
+       set ra 0
+    }
+    set rb [expr {$row + $uparrowlen}]
+    if {$rb > $commitidx($curview)} {
+       set rb $commitidx($curview)
+    }
+    set ids {}
+    for {} {$r < $ra} {incr r} {
+       set nextid [lindex $displayorder [expr {$r + 1}]]
+       foreach p [lindex $parentlist $r] {
+           if {$p eq $nextid} continue
+           set rn [nextuse $p $r]
+           if {$rn >= $row &&
+               $rn <= $r + $downarrowlen + $mingaplen + $uparrowlen} {
+               lappend ids [list $ordertok($curview,$p) $p]
+           }
+       }
+    }
+    for {} {$r < $row} {incr r} {
+       set nextid [lindex $displayorder [expr {$r + 1}]]
+       foreach p [lindex $parentlist $r] {
+           if {$p eq $nextid} continue
+           set rn [nextuse $p $r]
+           if {$rn < 0 || $rn >= $row} {
+               lappend ids [list $ordertok($curview,$p) $p]
+           }
+       }
+    }
+    set id [lindex $displayorder $row]
+    lappend ids [list $ordertok($curview,$id) $id]
+    while {$r < $rb} {
+       foreach p [lindex $parentlist $r] {
+           set firstkid [lindex $children($curview,$p) 0]
+           if {$commitrow($curview,$firstkid) < $row} {
+               lappend ids [list $ordertok($curview,$p) $p]
+           }
+       }
+       incr r
+       set id [lindex $displayorder $r]
+       if {$id ne {}} {
+           set firstkid [lindex $children($curview,$id) 0]
+           if {$firstkid ne {} && $commitrow($curview,$firstkid) < $row} {
+               lappend ids [list $ordertok($curview,$id) $id]
+           }
+       }
+    }
+    set idlist {}
+    foreach idx [lsort -unique $ids] {
+       lappend idlist [lindex $idx 1]
+    }
+    return $idlist
+}
+
+proc rowsequal {a b} {
+    while {[set i [lsearch -exact $a {}]] >= 0} {
+       set a [lreplace $a $i $i]
+    }
+    while {[set i [lsearch -exact $b {}]] >= 0} {
+       set b [lreplace $b $i $i]
+    }
+    return [expr {$a eq $b}]
+}
+
+proc makeupline {id row rend col} {
+    global rowidlist uparrowlen downarrowlen mingaplen
+
+    for {set r $rend} {1} {set r $rstart} {
+       set rstart [prevuse $id $r]
+       if {$rstart < 0} return
+       if {$rstart < $row} break
+    }
+    if {$rstart + $uparrowlen + $mingaplen + $downarrowlen < $rend} {
+       set rstart [expr {$rend - $uparrowlen - 1}]
+    }
+    for {set r $rstart} {[incr r] <= $row} {} {
+       set idlist [lindex $rowidlist $r]
+       if {$idlist ne {} && [lsearch -exact $idlist $id] < 0} {
+           set col [idcol $idlist $id $col]
+           lset rowidlist $r [linsert $idlist $col $id]
+           changedrow $r
+       }
+    }
+}
+
+proc layoutrows {row endrow} {
+    global rowidlist rowisopt rowfinal displayorder
+    global uparrowlen downarrowlen maxwidth mingaplen
+    global children parentlist
+    global commitidx viewcomplete curview commitrow
+
+    set idlist {}
+    if {$row > 0} {
+       set rm1 [expr {$row - 1}]
+       foreach id [lindex $rowidlist $rm1] {
+           if {$id ne {}} {
+               lappend idlist $id
+           }
+       }
+       set final [lindex $rowfinal $rm1]
+    }
+    for {} {$row < $endrow} {incr row} {
+       set rm1 [expr {$row - 1}]
+       if {$rm1 < 0 || $idlist eq {}} {
+           set idlist [make_idlist $row]
+           set final 1
+       } else {
+           set id [lindex $displayorder $rm1]
+           set col [lsearch -exact $idlist $id]
+           set idlist [lreplace $idlist $col $col]
+           foreach p [lindex $parentlist $rm1] {
+               if {[lsearch -exact $idlist $p] < 0} {
+                   set col [idcol $idlist $p $col]
+                   set idlist [linsert $idlist $col $p]
+                   # if not the first child, we have to insert a line going up
+                   if {$id ne [lindex $children($curview,$p) 0]} {
+                       makeupline $p $rm1 $row $col
+                   }
+               }
+           }
+           set id [lindex $displayorder $row]
+           if {$row > $downarrowlen} {
+               set termrow [expr {$row - $downarrowlen - 1}]
+               foreach p [lindex $parentlist $termrow] {
+                   set i [lsearch -exact $idlist $p]
+                   if {$i < 0} continue
+                   set nr [nextuse $p $termrow]
+                   if {$nr < 0 || $nr >= $row + $mingaplen + $uparrowlen} {
+                       set idlist [lreplace $idlist $i $i]
+                   }
+               }
+           }
+           set col [lsearch -exact $idlist $id]
+           if {$col < 0} {
+               set col [idcol $idlist $id]
+               set idlist [linsert $idlist $col $id]
+               if {$children($curview,$id) ne {}} {
+                   makeupline $id $rm1 $row $col
+               }
+           }
+           set r [expr {$row + $uparrowlen - 1}]
+           if {$r < $commitidx($curview)} {
+               set x $col
+               foreach p [lindex $parentlist $r] {
+                   if {[lsearch -exact $idlist $p] >= 0} continue
+                   set fk [lindex $children($curview,$p) 0]
+                   if {$commitrow($curview,$fk) < $row} {
+                       set x [idcol $idlist $p $x]
+                       set idlist [linsert $idlist $x $p]
+                   }
+               }
+               if {[incr r] < $commitidx($curview)} {
+                   set p [lindex $displayorder $r]
+                   if {[lsearch -exact $idlist $p] < 0} {
+                       set fk [lindex $children($curview,$p) 0]
+                       if {$fk ne {} && $commitrow($curview,$fk) < $row} {
+                           set x [idcol $idlist $p $x]
+                           set idlist [linsert $idlist $x $p]
+                       }
+                   }
+               }
+           }
+       }
+       if {$final && !$viewcomplete($curview) &&
+           $row + $uparrowlen + $mingaplen + $downarrowlen
+               >= $commitidx($curview)} {
+           set final 0
+       }
+       set l [llength $rowidlist]
+       if {$row == $l} {
+           lappend rowidlist $idlist
+           lappend rowisopt 0
+           lappend rowfinal $final
+       } elseif {$row < $l} {
+           if {![rowsequal $idlist [lindex $rowidlist $row]]} {
+               lset rowidlist $row $idlist
+               changedrow $row
+           }
+           lset rowfinal $row $final
+       } else {
+           set pad [ntimes [expr {$row - $l}] {}]
+           set rowidlist [concat $rowidlist $pad]
+           lappend rowidlist $idlist
+           set rowfinal [concat $rowfinal $pad]
+           lappend rowfinal $final
+           set rowisopt [concat $rowisopt [ntimes [expr {$row - $l + 1}] 0]]
+       }
+    }
+    return $row
+}
+
+proc changedrow {row} {
+    global displayorder iddrawn rowisopt need_redisplay
+
+    set l [llength $rowisopt]
+    if {$row < $l} {
+       lset rowisopt $row 0
+       if {$row + 1 < $l} {
+           lset rowisopt [expr {$row + 1}] 0
+           if {$row + 2 < $l} {
+               lset rowisopt [expr {$row + 2}] 0
+           }
+       }
+    }
+    set id [lindex $displayorder $row]
+    if {[info exists iddrawn($id)]} {
+       set need_redisplay 1
+    }
+}
+
+proc insert_pad {row col npad} {
+    global rowidlist
+
+    set pad [ntimes $npad {}]
+    set idlist [lindex $rowidlist $row]
+    set bef [lrange $idlist 0 [expr {$col - 1}]]
+    set aft [lrange $idlist $col end]
+    set i [lsearch -exact $aft {}]
+    if {$i > 0} {
+       set aft [lreplace $aft $i $i]
+    }
+    lset rowidlist $row [concat $bef $pad $aft]
+    changedrow $row
+}
+
+proc optimize_rows {row col endrow} {
+    global rowidlist rowisopt displayorder curview children
+
+    if {$row < 1} {
+       set row 1
+    }
+    for {} {$row < $endrow} {incr row; set col 0} {
+       if {[lindex $rowisopt $row]} continue
+       set haspad 0
+       set y0 [expr {$row - 1}]
+       set ym [expr {$row - 2}]
+       set idlist [lindex $rowidlist $row]
+       set previdlist [lindex $rowidlist $y0]
+       if {$idlist eq {} || $previdlist eq {}} continue
+       if {$ym >= 0} {
+           set pprevidlist [lindex $rowidlist $ym]
+           if {$pprevidlist eq {}} continue
+       } else {
+           set pprevidlist {}
+       }
+       set x0 -1
+       set xm -1
+       for {} {$col < [llength $idlist]} {incr col} {
+           set id [lindex $idlist $col]
+           if {[lindex $previdlist $col] eq $id} continue
+           if {$id eq {}} {
+               set haspad 1
+               continue
+           }
+           set x0 [lsearch -exact $previdlist $id]
+           if {$x0 < 0} continue
+           set z [expr {$x0 - $col}]
+           set isarrow 0
+           set z0 {}
+           if {$ym >= 0} {
+               set xm [lsearch -exact $pprevidlist $id]
+               if {$xm >= 0} {
+                   set z0 [expr {$xm - $x0}]
+               }
+           }
+           if {$z0 eq {}} {
+               # if row y0 is the first child of $id then it's not an arrow
+               if {[lindex $children($curview,$id) 0] ne
+                   [lindex $displayorder $y0]} {
+                   set isarrow 1
+               }
+           }
+           if {!$isarrow && $id ne [lindex $displayorder $row] &&
+               [lsearch -exact [lindex $rowidlist [expr {$row+1}]] $id] < 0} {
+               set isarrow 1
+           }
+           # Looking at lines from this row to the previous row,
+           # make them go straight up if they end in an arrow on
+           # the previous row; otherwise make them go straight up
+           # or at 45 degrees.
+           if {$z < -1 || ($z < 0 && $isarrow)} {
+               # Line currently goes left too much;
+               # insert pads in the previous row, then optimize it
+               set npad [expr {-1 - $z + $isarrow}]
+               insert_pad $y0 $x0 $npad
+               if {$y0 > 0} {
+                   optimize_rows $y0 $x0 $row
+               }
+               set previdlist [lindex $rowidlist $y0]
+               set x0 [lsearch -exact $previdlist $id]
+               set z [expr {$x0 - $col}]
+               if {$z0 ne {}} {
+                   set pprevidlist [lindex $rowidlist $ym]
+                   set xm [lsearch -exact $pprevidlist $id]
+                   set z0 [expr {$xm - $x0}]
+               }
+           } elseif {$z > 1 || ($z > 0 && $isarrow)} {
+               # Line currently goes right too much;
+               # insert pads in this line
+               set npad [expr {$z - 1 + $isarrow}]
+               insert_pad $row $col $npad
+               set idlist [lindex $rowidlist $row]
+               incr col $npad
+               set z [expr {$x0 - $col}]
+               set haspad 1
+           }
+           if {$z0 eq {} && !$isarrow && $ym >= 0} {
+               # this line links to its first child on row $row-2
+               set id [lindex $displayorder $ym]
+               set xc [lsearch -exact $pprevidlist $id]
+               if {$xc >= 0} {
+                   set z0 [expr {$xc - $x0}]
+               }
+           }
+           # avoid lines jigging left then immediately right
+           if {$z0 ne {} && $z < 0 && $z0 > 0} {
+               insert_pad $y0 $x0 1
+               incr x0
+               optimize_rows $y0 $x0 $row
+               set previdlist [lindex $rowidlist $y0]
+           }
+       }
+       if {!$haspad} {
+           # Find the first column that doesn't have a line going right
+           for {set col [llength $idlist]} {[incr col -1] >= 0} {} {
+               set id [lindex $idlist $col]
+               if {$id eq {}} break
+               set x0 [lsearch -exact $previdlist $id]
+               if {$x0 < 0} {
+                   # check if this is the link to the first child
+                   set kid [lindex $displayorder $y0]
+                   if {[lindex $children($curview,$id) 0] eq $kid} {
+                       # it is, work out offset to child
+                       set x0 [lsearch -exact $previdlist $kid]
+                   }
+               }
+               if {$x0 <= $col} break
+           }
+           # Insert a pad at that column as long as it has a line and
+           # isn't the last column
+           if {$x0 >= 0 && [incr col] < [llength $idlist]} {
+               set idlist [linsert $idlist $col {}]
+               lset rowidlist $row $idlist
+               changedrow $row
+           }
+       }
+    }
+}
+
+proc xc {row col} {
+    global canvx0 linespc
+    return [expr {$canvx0 + $col * $linespc}]
+}
+
+proc yc {row} {
+    global canvy0 linespc
+    return [expr {$canvy0 + $row * $linespc}]
+}
+
+proc linewidth {id} {
+    global thickerline lthickness
+
+    set wid $lthickness
+    if {[info exists thickerline] && $id eq $thickerline} {
+       set wid [expr {2 * $lthickness}]
+    }
+    return $wid
+}
+
+proc rowranges {id} {
+    global commitrow curview children uparrowlen downarrowlen
+    global rowidlist
+
+    set kids $children($curview,$id)
+    if {$kids eq {}} {
+       return {}
+    }
+    set ret {}
+    lappend kids $id
+    foreach child $kids {
+       if {![info exists commitrow($curview,$child)]} break
+       set row $commitrow($curview,$child)
+       if {![info exists prev]} {
+           lappend ret [expr {$row + 1}]
+       } else {
+           if {$row <= $prevrow} {
+               puts "oops children out of order [shortids $id] $row < [shortids $prev] $prevrow"
+           }
+           # see if the line extends the whole way from prevrow to row
+           if {$row > $prevrow + $uparrowlen + $downarrowlen &&
+               [lsearch -exact [lindex $rowidlist \
+                           [expr {int(($row + $prevrow) / 2)}]] $id] < 0} {
+               # it doesn't, see where it ends
+               set r [expr {$prevrow + $downarrowlen}]
+               if {[lsearch -exact [lindex $rowidlist $r] $id] < 0} {
+                   while {[incr r -1] > $prevrow &&
+                          [lsearch -exact [lindex $rowidlist $r] $id] < 0} {}
+               } else {
+                   while {[incr r] <= $row &&
+                          [lsearch -exact [lindex $rowidlist $r] $id] >= 0} {}
+                   incr r -1
+               }
+               lappend ret $r
+               # see where it starts up again
+               set r [expr {$row - $uparrowlen}]
+               if {[lsearch -exact [lindex $rowidlist $r] $id] < 0} {
+                   while {[incr r] < $row &&
+                          [lsearch -exact [lindex $rowidlist $r] $id] < 0} {}
+               } else {
+                   while {[incr r -1] >= $prevrow &&
+                          [lsearch -exact [lindex $rowidlist $r] $id] >= 0} {}
+                   incr r
+               }
+               lappend ret $r
+           }
+       }
+       if {$child eq $id} {
+           lappend ret $row
+       }
+       set prev $id
+       set prevrow $row
+    }
+    return $ret
+}
+
+proc drawlineseg {id row endrow arrowlow} {
+    global rowidlist displayorder iddrawn linesegs
+    global canv colormap linespc curview maxlinelen parentlist
+
+    set cols [list [lsearch -exact [lindex $rowidlist $row] $id]]
+    set le [expr {$row + 1}]
+    set arrowhigh 1
+    while {1} {
+       set c [lsearch -exact [lindex $rowidlist $le] $id]
+       if {$c < 0} {
+           incr le -1
+           break
+       }
+       lappend cols $c
+       set x [lindex $displayorder $le]
+       if {$x eq $id} {
+           set arrowhigh 0
+           break
+       }
+       if {[info exists iddrawn($x)] || $le == $endrow} {
+           set c [lsearch -exact [lindex $rowidlist [expr {$le+1}]] $id]
+           if {$c >= 0} {
+               lappend cols $c
+               set arrowhigh 0
+           }
+           break
+       }
+       incr le
+    }
+    if {$le <= $row} {
+       return $row
+    }
+
+    set lines {}
+    set i 0
+    set joinhigh 0
+    if {[info exists linesegs($id)]} {
+       set lines $linesegs($id)
+       foreach li $lines {
+           set r0 [lindex $li 0]
+           if {$r0 > $row} {
+               if {$r0 == $le && [lindex $li 1] - $row <= $maxlinelen} {
+                   set joinhigh 1
+               }
+               break
+           }
+           incr i
+       }
+    }
+    set joinlow 0
+    if {$i > 0} {
+       set li [lindex $lines [expr {$i-1}]]
+       set r1 [lindex $li 1]
+       if {$r1 == $row && $le - [lindex $li 0] <= $maxlinelen} {
+           set joinlow 1
+       }
+    }
+
+    set x [lindex $cols [expr {$le - $row}]]
+    set xp [lindex $cols [expr {$le - 1 - $row}]]
+    set dir [expr {$xp - $x}]
+    if {$joinhigh} {
+       set ith [lindex $lines $i 2]
+       set coords [$canv coords $ith]
+       set ah [$canv itemcget $ith -arrow]
+       set arrowhigh [expr {$ah eq "first" || $ah eq "both"}]
+       set x2 [lindex $cols [expr {$le + 1 - $row}]]
+       if {$x2 ne {} && $x - $x2 == $dir} {
+           set coords [lrange $coords 0 end-2]
+       }
+    } else {
+       set coords [list [xc $le $x] [yc $le]]
+    }
+    if {$joinlow} {
+       set itl [lindex $lines [expr {$i-1}] 2]
+       set al [$canv itemcget $itl -arrow]
+       set arrowlow [expr {$al eq "last" || $al eq "both"}]
+    } elseif {$arrowlow} {
+       if {[lsearch -exact [lindex $rowidlist [expr {$row-1}]] $id] >= 0 ||
+           [lsearch -exact [lindex $parentlist [expr {$row-1}]] $id] >= 0} {
+           set arrowlow 0
+       }
+    }
+    set arrow [lindex {none first last both} [expr {$arrowhigh + 2*$arrowlow}]]
+    for {set y $le} {[incr y -1] > $row} {} {
+       set x $xp
+       set xp [lindex $cols [expr {$y - 1 - $row}]]
+       set ndir [expr {$xp - $x}]
+       if {$dir != $ndir || $xp < 0} {
+           lappend coords [xc $y $x] [yc $y]
+       }
+       set dir $ndir
+    }
+    if {!$joinlow} {
+       if {$xp < 0} {
+           # join parent line to first child
+           set ch [lindex $displayorder $row]
+           set xc [lsearch -exact [lindex $rowidlist $row] $ch]
+           if {$xc < 0} {
+               puts "oops: drawlineseg: child $ch not on row $row"
+           } elseif {$xc != $x} {
+               if {($arrowhigh && $le == $row + 1) || $dir == 0} {
+                   set d [expr {int(0.5 * $linespc)}]
+                   set x1 [xc $row $x]
+                   if {$xc < $x} {
+                       set x2 [expr {$x1 - $d}]
+                   } else {
+                       set x2 [expr {$x1 + $d}]
+                   }
+                   set y2 [yc $row]
+                   set y1 [expr {$y2 + $d}]
+                   lappend coords $x1 $y1 $x2 $y2
+               } elseif {$xc < $x - 1} {
+                   lappend coords [xc $row [expr {$x-1}]] [yc $row]
+               } elseif {$xc > $x + 1} {
+                   lappend coords [xc $row [expr {$x+1}]] [yc $row]
+               }
+               set x $xc
+           }
+           lappend coords [xc $row $x] [yc $row]
+       } else {
+           set xn [xc $row $xp]
+           set yn [yc $row]
+           lappend coords $xn $yn
+       }
+       if {!$joinhigh} {
+           assigncolor $id
+           set t [$canv create line $coords -width [linewidth $id] \
+                      -fill $colormap($id) -tags lines.$id -arrow $arrow]
+           $canv lower $t
+           bindline $t $id
+           set lines [linsert $lines $i [list $row $le $t]]
+       } else {
+           $canv coords $ith $coords
+           if {$arrow ne $ah} {
+               $canv itemconf $ith -arrow $arrow
+           }
+           lset lines $i 0 $row
+       }
+    } else {
+       set xo [lsearch -exact [lindex $rowidlist [expr {$row - 1}]] $id]
+       set ndir [expr {$xo - $xp}]
+       set clow [$canv coords $itl]
+       if {$dir == $ndir} {
+           set clow [lrange $clow 2 end]
+       }
+       set coords [concat $coords $clow]
+       if {!$joinhigh} {
+           lset lines [expr {$i-1}] 1 $le
+       } else {
+           # coalesce two pieces
+           $canv delete $ith
+           set b [lindex $lines [expr {$i-1}] 0]
+           set e [lindex $lines $i 1]
+           set lines [lreplace $lines [expr {$i-1}] $i [list $b $e $itl]]
+       }
+       $canv coords $itl $coords
+       if {$arrow ne $al} {
+           $canv itemconf $itl -arrow $arrow
+       }
+    }
+
+    set linesegs($id) $lines
+    return $le
+}
+
+proc drawparentlinks {id row} {
+    global rowidlist canv colormap curview parentlist
+    global idpos linespc
+
+    set rowids [lindex $rowidlist $row]
+    set col [lsearch -exact $rowids $id]
+    if {$col < 0} return
+    set olds [lindex $parentlist $row]
+    set row2 [expr {$row + 1}]
+    set x [xc $row $col]
+    set y [yc $row]
+    set y2 [yc $row2]
+    set d [expr {int(0.5 * $linespc)}]
+    set ymid [expr {$y + $d}]
+    set ids [lindex $rowidlist $row2]
+    # rmx = right-most X coord used
+    set rmx 0
+    foreach p $olds {
+       set i [lsearch -exact $ids $p]
+       if {$i < 0} {
+           puts "oops, parent $p of $id not in list"
+           continue
+       }
+       set x2 [xc $row2 $i]
+       if {$x2 > $rmx} {
+           set rmx $x2
+       }
+       set j [lsearch -exact $rowids $p]
+       if {$j < 0} {
+           # drawlineseg will do this one for us
+           continue
+       }
+       assigncolor $p
+       # should handle duplicated parents here...
+       set coords [list $x $y]
+       if {$i != $col} {
+           # if attaching to a vertical segment, draw a smaller
+           # slant for visual distinctness
+           if {$i == $j} {
+               if {$i < $col} {
+                   lappend coords [expr {$x2 + $d}] $y $x2 $ymid
+               } else {
+                   lappend coords [expr {$x2 - $d}] $y $x2 $ymid
+               }
+           } elseif {$i < $col && $i < $j} {
+               # segment slants towards us already
+               lappend coords [xc $row $j] $y
+           } else {
+               if {$i < $col - 1} {
+                   lappend coords [expr {$x2 + $linespc}] $y
+               } elseif {$i > $col + 1} {
+                   lappend coords [expr {$x2 - $linespc}] $y
+               }
+               lappend coords $x2 $y2
+           }
+       } else {
+           lappend coords $x2 $y2
+       }
+       set t [$canv create line $coords -width [linewidth $p] \
+                  -fill $colormap($p) -tags lines.$p]
+       $canv lower $t
+       bindline $t $p
+    }
+    if {$rmx > [lindex $idpos($id) 1]} {
+       lset idpos($id) 1 $rmx
+       redrawtags $id
+    }
+}
+
+proc drawlines {id} {
+    global canv
+
+    $canv itemconf lines.$id -width [linewidth $id]
+}
+
+proc drawcmittext {id row col} {
+    global linespc canv canv2 canv3 canvy0 fgcolor curview
+    global commitlisted commitinfo rowidlist parentlist
+    global rowtextx idpos idtags idheads idotherrefs
+    global linehtag linentag linedtag selectedline
+    global canvxmax boldrows boldnamerows fgcolor nullid nullid2
+
+    # listed is 0 for boundary, 1 for normal, 2 for left, 3 for right
+    set listed [lindex $commitlisted $row]
+    if {$id eq $nullid} {
+       set ofill red
+    } elseif {$id eq $nullid2} {
+       set ofill green
+    } else {
+       set ofill [expr {$listed != 0? "blue": "white"}]
+    }
+    set x [xc $row $col]
+    set y [yc $row]
+    set orad [expr {$linespc / 3}]
+    if {$listed <= 1} {
+       set t [$canv create oval [expr {$x - $orad}] [expr {$y - $orad}] \
+                  [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
+                  -fill $ofill -outline $fgcolor -width 1 -tags circle]
+    } elseif {$listed == 2} {
+       # triangle pointing left for left-side commits
+       set t [$canv create polygon \
+                  [expr {$x - $orad}] $y \
+                  [expr {$x + $orad - 1}] [expr {$y - $orad}] \
+                  [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
+                  -fill $ofill -outline $fgcolor -width 1 -tags circle]
+    } else {
+       # triangle pointing right for right-side commits
+       set t [$canv create polygon \
+                  [expr {$x + $orad - 1}] $y \
+                  [expr {$x - $orad}] [expr {$y - $orad}] \
+                  [expr {$x - $orad}] [expr {$y + $orad - 1}] \
+                  -fill $ofill -outline $fgcolor -width 1 -tags circle]
+    }
+    $canv raise $t
+    $canv bind $t <1> {selcanvline {} %x %y}
+    set rmx [llength [lindex $rowidlist $row]]
+    set olds [lindex $parentlist $row]
+    if {$olds ne {}} {
+       set nextids [lindex $rowidlist [expr {$row + 1}]]
+       foreach p $olds {
+           set i [lsearch -exact $nextids $p]
+           if {$i > $rmx} {
+               set rmx $i
+           }
+       }
+    }
+    set xt [xc $row $rmx]
+    set rowtextx($row) $xt
+    set idpos($id) [list $x $xt $y]
+    if {[info exists idtags($id)] || [info exists idheads($id)]
+       || [info exists idotherrefs($id)]} {
+       set xt [drawtags $id $x $xt $y]
+    }
+    set headline [lindex $commitinfo($id) 0]
+    set name [lindex $commitinfo($id) 1]
+    set date [lindex $commitinfo($id) 2]
+    set date [formatdate $date]
+    set font mainfont
+    set nfont mainfont
+    set isbold [ishighlighted $row]
+    if {$isbold > 0} {
+       lappend boldrows $row
+       set font mainfontbold
+       if {$isbold > 1} {
+           lappend boldnamerows $row
+           set nfont mainfontbold
+       }
+    }
+    set linehtag($row) [$canv create text $xt $y -anchor w -fill $fgcolor \
+                           -text $headline -font $font -tags text]
+    $canv bind $linehtag($row) <Button-3> "rowmenu %X %Y $id"
+    set linentag($row) [$canv2 create text 3 $y -anchor w -fill $fgcolor \
+                           -text $name -font $nfont -tags text]
+    set linedtag($row) [$canv3 create text 3 $y -anchor w -fill $fgcolor \
+                           -text $date -font mainfont -tags text]
+    if {[info exists selectedline] && $selectedline == $row} {
+       make_secsel $row
+    }
+    set xr [expr {$xt + [font measure $font $headline]}]
+    if {$xr > $canvxmax} {
+       set canvxmax $xr
+       setcanvscroll
+    }
+}
+
+proc drawcmitrow {row} {
+    global displayorder rowidlist nrows_drawn
+    global iddrawn markingmatches
+    global commitinfo parentlist numcommits
+    global filehighlight fhighlights findpattern nhighlights
+    global hlview vhighlights
+    global highlight_related rhighlights
+
+    if {$row >= $numcommits} return
+
+    set id [lindex $displayorder $row]
+    if {[info exists hlview] && ![info exists vhighlights($row)]} {
+       askvhighlight $row $id
+    }
+    if {[info exists filehighlight] && ![info exists fhighlights($row)]} {
+       askfilehighlight $row $id
+    }
+    if {$findpattern ne {} && ![info exists nhighlights($row)]} {
+       askfindhighlight $row $id
+    }
+    if {$highlight_related ne "None" && ![info exists rhighlights($row)]} {
+       askrelhighlight $row $id
+    }
+    if {![info exists iddrawn($id)]} {
+       set col [lsearch -exact [lindex $rowidlist $row] $id]
+       if {$col < 0} {
+           puts "oops, row $row id $id not in list"
+           return
+       }
+       if {![info exists commitinfo($id)]} {
+           getcommit $id
+       }
+       assigncolor $id
+       drawcmittext $id $row $col
+       set iddrawn($id) 1
+       incr nrows_drawn
+    }
+    if {$markingmatches} {
+       markrowmatches $row $id
+    }
+}
+
+proc drawcommits {row {endrow {}}} {
+    global numcommits iddrawn displayorder curview need_redisplay
+    global parentlist rowidlist rowfinal uparrowlen downarrowlen nrows_drawn
+
+    if {$row < 0} {
+       set row 0
+    }
+    if {$endrow eq {}} {
+       set endrow $row
+    }
+    if {$endrow >= $numcommits} {
+       set endrow [expr {$numcommits - 1}]
+    }
+
+    set rl1 [expr {$row - $downarrowlen - 3}]
+    if {$rl1 < 0} {
+       set rl1 0
+    }
+    set ro1 [expr {$row - 3}]
+    if {$ro1 < 0} {
+       set ro1 0
+    }
+    set r2 [expr {$endrow + $uparrowlen + 3}]
+    if {$r2 > $numcommits} {
+       set r2 $numcommits
+    }
+    for {set r $rl1} {$r < $r2} {incr r} {
+       if {[lindex $rowidlist $r] ne {} && [lindex $rowfinal $r]} {
+           if {$rl1 < $r} {
+               layoutrows $rl1 $r
+           }
+           set rl1 [expr {$r + 1}]
+       }
+    }
+    if {$rl1 < $r} {
+       layoutrows $rl1 $r
+    }
+    optimize_rows $ro1 0 $r2
+    if {$need_redisplay || $nrows_drawn > 2000} {
+       clear_display
+       drawvisible
+    }
+
+    # make the lines join to already-drawn rows either side
+    set r [expr {$row - 1}]
+    if {$r < 0 || ![info exists iddrawn([lindex $displayorder $r])]} {
+       set r $row
+    }
+    set er [expr {$endrow + 1}]
+    if {$er >= $numcommits ||
+       ![info exists iddrawn([lindex $displayorder $er])]} {
+       set er $endrow
+    }
+    for {} {$r <= $er} {incr r} {
+       set id [lindex $displayorder $r]
+       set wasdrawn [info exists iddrawn($id)]
+       drawcmitrow $r
+       if {$r == $er} break
+       set nextid [lindex $displayorder [expr {$r + 1}]]
+       if {$wasdrawn && [info exists iddrawn($nextid)]} continue
+       drawparentlinks $id $r
+
+       set rowids [lindex $rowidlist $r]
+       foreach lid $rowids {
+           if {$lid eq {}} continue
+           if {[info exists lineend($lid)] && $lineend($lid) > $r} continue
+           if {$lid eq $id} {
+               # see if this is the first child of any of its parents
+               foreach p [lindex $parentlist $r] {
+                   if {[lsearch -exact $rowids $p] < 0} {
+                       # make this line extend up to the child
+                       set lineend($p) [drawlineseg $p $r $er 0]
+                   }
+               }
+           } else {
+               set lineend($lid) [drawlineseg $lid $r $er 1]
+           }
+       }
+    }
+}
+
+proc drawfrac {f0 f1} {
+    global canv linespc
+
+    set ymax [lindex [$canv cget -scrollregion] 3]
+    if {$ymax eq {} || $ymax == 0} return
+    set y0 [expr {int($f0 * $ymax)}]
+    set row [expr {int(($y0 - 3) / $linespc) - 1}]
+    set y1 [expr {int($f1 * $ymax)}]
+    set endrow [expr {int(($y1 - 3) / $linespc) + 1}]
+    drawcommits $row $endrow
+}
+
+proc drawvisible {} {
+    global canv
+    eval drawfrac [$canv yview]
+}
+
+proc clear_display {} {
+    global iddrawn linesegs need_redisplay nrows_drawn
+    global vhighlights fhighlights nhighlights rhighlights
+
+    allcanvs delete all
+    catch {unset iddrawn}
+    catch {unset linesegs}
+    catch {unset vhighlights}
+    catch {unset fhighlights}
+    catch {unset nhighlights}
+    catch {unset rhighlights}
+    set need_redisplay 0
+    set nrows_drawn 0
+}
+
+proc findcrossings {id} {
+    global rowidlist parentlist numcommits displayorder
+
+    set cross {}
+    set ccross {}
+    foreach {s e} [rowranges $id] {
+       if {$e >= $numcommits} {
+           set e [expr {$numcommits - 1}]
+       }
+       if {$e <= $s} continue
+       for {set row $e} {[incr row -1] >= $s} {} {
+           set x [lsearch -exact [lindex $rowidlist $row] $id]
+           if {$x < 0} break
+           set olds [lindex $parentlist $row]
+           set kid [lindex $displayorder $row]
+           set kidx [lsearch -exact [lindex $rowidlist $row] $kid]
+           if {$kidx < 0} continue
+           set nextrow [lindex $rowidlist [expr {$row + 1}]]
+           foreach p $olds {
+               set px [lsearch -exact $nextrow $p]
+               if {$px < 0} continue
+               if {($kidx < $x && $x < $px) || ($px < $x && $x < $kidx)} {
+                   if {[lsearch -exact $ccross $p] >= 0} continue
+                   if {$x == $px + ($kidx < $px? -1: 1)} {
+                       lappend ccross $p
+                   } elseif {[lsearch -exact $cross $p] < 0} {
+                       lappend cross $p
+                   }
+               }
+           }
+       }
+    }
+    return [concat $ccross {{}} $cross]
+}
+
+proc assigncolor {id} {
+    global colormap colors nextcolor
+    global commitrow parentlist children children curview
+
+    if {[info exists colormap($id)]} return
+    set ncolors [llength $colors]
+    if {[info exists children($curview,$id)]} {
+       set kids $children($curview,$id)
+    } else {
+       set kids {}
+    }
+    if {[llength $kids] == 1} {
+       set child [lindex $kids 0]
+       if {[info exists colormap($child)]
+           && [llength [lindex $parentlist $commitrow($curview,$child)]] == 1} {
+           set colormap($id) $colormap($child)
+           return
+       }
+    }
+    set badcolors {}
+    set origbad {}
+    foreach x [findcrossings $id] {
+       if {$x eq {}} {
+           # delimiter between corner crossings and other crossings
+           if {[llength $badcolors] >= $ncolors - 1} break
+           set origbad $badcolors
+       }
+       if {[info exists colormap($x)]
+           && [lsearch -exact $badcolors $colormap($x)] < 0} {
+           lappend badcolors $colormap($x)
+       }
+    }
+    if {[llength $badcolors] >= $ncolors} {
+       set badcolors $origbad
+    }
+    set origbad $badcolors
+    if {[llength $badcolors] < $ncolors - 1} {
+       foreach child $kids {
+           if {[info exists colormap($child)]
+               && [lsearch -exact $badcolors $colormap($child)] < 0} {
+               lappend badcolors $colormap($child)
+           }
+           foreach p [lindex $parentlist $commitrow($curview,$child)] {
+               if {[info exists colormap($p)]
+                   && [lsearch -exact $badcolors $colormap($p)] < 0} {
+                   lappend badcolors $colormap($p)
+               }
+           }
+       }
+       if {[llength $badcolors] >= $ncolors} {
+           set badcolors $origbad
+       }
+    }
+    for {set i 0} {$i <= $ncolors} {incr i} {
+       set c [lindex $colors $nextcolor]
+       if {[incr nextcolor] >= $ncolors} {
+           set nextcolor 0
+       }
+       if {[lsearch -exact $badcolors $c]} break
+    }
+    set colormap($id) $c
+}
+
+proc bindline {t id} {
+    global canv
+
+    $canv bind $t <Enter> "lineenter %x %y $id"
+    $canv bind $t <Motion> "linemotion %x %y $id"
+    $canv bind $t <Leave> "lineleave $id"
+    $canv bind $t <Button-1> "lineclick %x %y $id 1"
+}
+
+proc drawtags {id x xt y1} {
+    global idtags idheads idotherrefs mainhead
+    global linespc lthickness
+    global canv commitrow rowtextx curview fgcolor bgcolor
+
+    set marks {}
+    set ntags 0
+    set nheads 0
+    if {[info exists idtags($id)]} {
+       set marks $idtags($id)
+       set ntags [llength $marks]
+    }
+    if {[info exists idheads($id)]} {
+       set marks [concat $marks $idheads($id)]
+       set nheads [llength $idheads($id)]
+    }
+    if {[info exists idotherrefs($id)]} {
+       set marks [concat $marks $idotherrefs($id)]
+    }
+    if {$marks eq {}} {
+       return $xt
+    }
+
+    set delta [expr {int(0.5 * ($linespc - $lthickness))}]
+    set yt [expr {$y1 - 0.5 * $linespc}]
+    set yb [expr {$yt + $linespc - 1}]
+    set xvals {}
+    set wvals {}
+    set i -1
+    foreach tag $marks {
+       incr i
+       if {$i >= $ntags && $i < $ntags + $nheads && $tag eq $mainhead} {
+           set wid [font measure mainfontbold $tag]
+       } else {
+           set wid [font measure mainfont $tag]
+       }
+       lappend xvals $xt
+       lappend wvals $wid
+       set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
+    }
+    set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
+              -width $lthickness -fill black -tags tag.$id]
+    $canv lower $t
+    foreach tag $marks x $xvals wid $wvals {
+       set xl [expr {$x + $delta}]
+       set xr [expr {$x + $delta + $wid + $lthickness}]
+       set font mainfont
+       if {[incr ntags -1] >= 0} {
+           # draw a tag
+           set t [$canv create polygon $x [expr {$yt + $delta}] $xl $yt \
+                      $xr $yt $xr $yb $xl $yb $x [expr {$yb - $delta}] \
+                      -width 1 -outline black -fill yellow -tags tag.$id]
+           $canv bind $t <1> [list showtag $tag 1]
+           set rowtextx($commitrow($curview,$id)) [expr {$xr + $linespc}]
+       } else {
+           # draw a head or other ref
+           if {[incr nheads -1] >= 0} {
+               set col green
+               if {$tag eq $mainhead} {
+                   set font mainfontbold
+               }
+           } else {
+               set col "#ddddff"
+           }
+           set xl [expr {$xl - $delta/2}]
+           $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
+               -width 1 -outline black -fill $col -tags tag.$id
+           if {[regexp {^(remotes/.*/|remotes/)} $tag match remoteprefix]} {
+               set rwid [font measure mainfont $remoteprefix]
+               set xi [expr {$x + 1}]
+               set yti [expr {$yt + 1}]
+               set xri [expr {$x + $rwid}]
+               $canv create polygon $xi $yti $xri $yti $xri $yb $xi $yb \
+                       -width 0 -fill "#ffddaa" -tags tag.$id
+           }
+       }
+       set t [$canv create text $xl $y1 -anchor w -text $tag -fill $fgcolor \
+                  -font $font -tags [list tag.$id text]]
+       if {$ntags >= 0} {
+           $canv bind $t <1> [list showtag $tag 1]
+       } elseif {$nheads >= 0} {
+           $canv bind $t <Button-3> [list headmenu %X %Y $id $tag]
+       }
+    }
+    return $xt
+}
+
+proc xcoord {i level ln} {
+    global canvx0 xspc1 xspc2
+
+    set x [expr {$canvx0 + $i * $xspc1($ln)}]
+    if {$i > 0 && $i == $level} {
+       set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
+    } elseif {$i > $level} {
+       set x [expr {$x + $xspc2 - $xspc1($ln)}]
+    }
+    return $x
+}
+
+proc show_status {msg} {
+    global canv fgcolor
+
+    clear_display
+    $canv create text 3 3 -anchor nw -text $msg -font mainfont \
+       -tags text -fill $fgcolor
+}
+
+# Insert a new commit as the child of the commit on row $row.
+# The new commit will be displayed on row $row and the commits
+# on that row and below will move down one row.
+proc insertrow {row newcmit} {
+    global displayorder parentlist commitlisted children
+    global commitrow curview rowidlist rowisopt rowfinal numcommits
+    global numcommits
+    global selectedline commitidx ordertok
+
+    if {$row >= $numcommits} {
+       puts "oops, inserting new row $row but only have $numcommits rows"
+       return
+    }
+    set p [lindex $displayorder $row]
+    set displayorder [linsert $displayorder $row $newcmit]
+    set parentlist [linsert $parentlist $row $p]
+    set kids $children($curview,$p)
+    lappend kids $newcmit
+    set children($curview,$p) $kids
+    set children($curview,$newcmit) {}
+    set commitlisted [linsert $commitlisted $row 1]
+    set l [llength $displayorder]
+    for {set r $row} {$r < $l} {incr r} {
+       set id [lindex $displayorder $r]
+       set commitrow($curview,$id) $r
+    }
+    incr commitidx($curview)
+    set ordertok($curview,$newcmit) $ordertok($curview,$p)
+
+    if {$row < [llength $rowidlist]} {
+       set idlist [lindex $rowidlist $row]
+       if {$idlist ne {}} {
+           if {[llength $kids] == 1} {
+               set col [lsearch -exact $idlist $p]
+               lset idlist $col $newcmit
+           } else {
+               set col [llength $idlist]
+               lappend idlist $newcmit
+           }
+       }
+       set rowidlist [linsert $rowidlist $row $idlist]
+       set rowisopt [linsert $rowisopt $row 0]
+       set rowfinal [linsert $rowfinal $row [lindex $rowfinal $row]]
+    }
+
+    incr numcommits
+
+    if {[info exists selectedline] && $selectedline >= $row} {
+       incr selectedline
+    }
+    redisplay
+}
+
+# Remove a commit that was inserted with insertrow on row $row.
+proc removerow {row} {
+    global displayorder parentlist commitlisted children
+    global commitrow curview rowidlist rowisopt rowfinal numcommits
+    global numcommits
+    global linesegends selectedline commitidx
+
+    if {$row >= $numcommits} {
+       puts "oops, removing row $row but only have $numcommits rows"
+       return
+    }
+    set rp1 [expr {$row + 1}]
+    set id [lindex $displayorder $row]
+    set p [lindex $parentlist $row]
+    set displayorder [lreplace $displayorder $row $row]
+    set parentlist [lreplace $parentlist $row $row]
+    set commitlisted [lreplace $commitlisted $row $row]
+    set kids $children($curview,$p)
+    set i [lsearch -exact $kids $id]
+    if {$i >= 0} {
+       set kids [lreplace $kids $i $i]
+       set children($curview,$p) $kids
+    }
+    set l [llength $displayorder]
+    for {set r $row} {$r < $l} {incr r} {
+       set id [lindex $displayorder $r]
+       set commitrow($curview,$id) $r
+    }
+    incr commitidx($curview) -1
+
+    if {$row < [llength $rowidlist]} {
+       set rowidlist [lreplace $rowidlist $row $row]
+       set rowisopt [lreplace $rowisopt $row $row]
+       set rowfinal [lreplace $rowfinal $row $row]
+    }
+
+    incr numcommits -1
+
+    if {[info exists selectedline] && $selectedline > $row} {
+       incr selectedline -1
+    }
+    redisplay
+}
+
+# Don't change the text pane cursor if it is currently the hand cursor,
+# showing that we are over a sha1 ID link.
+proc settextcursor {c} {
+    global ctext curtextcursor
+
+    if {[$ctext cget -cursor] == $curtextcursor} {
+       $ctext config -cursor $c
+    }
+    set curtextcursor $c
+}
+
+proc nowbusy {what {name {}}} {
+    global isbusy busyname statusw
+
+    if {[array names isbusy] eq {}} {
+       . config -cursor watch
+       settextcursor watch
+    }
+    set isbusy($what) 1
+    set busyname($what) $name
+    if {$name ne {}} {
+       $statusw conf -text $name
+    }
+}
+
+proc notbusy {what} {
+    global isbusy maincursor textcursor busyname statusw
+
+    catch {
+       unset isbusy($what)
+       if {$busyname($what) ne {} &&
+           [$statusw cget -text] eq $busyname($what)} {
+           $statusw conf -text {}
+       }
+    }
+    if {[array names isbusy] eq {}} {
+       . config -cursor $maincursor
+       settextcursor $textcursor
+    }
+}
+
+proc findmatches {f} {
+    global findtype findstring
+    if {$findtype == "Regexp"} {
+       set matches [regexp -indices -all -inline $findstring $f]
+    } else {
+       set fs $findstring
+       if {$findtype == "IgnCase"} {
+           set f [string tolower $f]
+           set fs [string tolower $fs]
+       }
+       set matches {}
+       set i 0
+       set l [string length $fs]
+       while {[set j [string first $fs $f $i]] >= 0} {
+           lappend matches [list $j [expr {$j+$l-1}]]
+           set i [expr {$j + $l}]
+       }
+    }
+    return $matches
+}
+
+proc dofind {{dirn 1} {wrap 1}} {
+    global findstring findstartline findcurline selectedline numcommits
+    global gdttype filehighlight fh_serial find_dirn findallowwrap
+
+    if {[info exists find_dirn]} {
+       if {$find_dirn == $dirn} return
+       stopfinding
+    }
+    focus .
+    if {$findstring eq {} || $numcommits == 0} return
+    if {![info exists selectedline]} {
+       set findstartline [lindex [visiblerows] [expr {$dirn < 0}]]
+    } else {
+       set findstartline $selectedline
+    }
+    set findcurline $findstartline
+    nowbusy finding "Searching"
+    if {$gdttype ne "containing:" && ![info exists filehighlight]} {
+       after cancel do_file_hl $fh_serial
+       do_file_hl $fh_serial
+    }
+    set find_dirn $dirn
+    set findallowwrap $wrap
+    run findmore
+}
+
+proc stopfinding {} {
+    global find_dirn findcurline fprogcoord
+
+    if {[info exists find_dirn]} {
+       unset find_dirn
+       unset findcurline
+       notbusy finding
+       set fprogcoord 0
+       adjustprogress
+    }
+}
+
+proc findmore {} {
+    global commitdata commitinfo numcommits findpattern findloc
+    global findstartline findcurline displayorder
+    global find_dirn gdttype fhighlights fprogcoord
+    global findallowwrap
+
+    if {![info exists find_dirn]} {
+       return 0
+    }
+    set fldtypes {Headline Author Date Committer CDate Comments}
+    set l $findcurline
+    set moretodo 0
+    if {$find_dirn > 0} {
+       incr l
+       if {$l >= $numcommits} {
+           set l 0
+       }
+       if {$l <= $findstartline} {
+           set lim [expr {$findstartline + 1}]
+       } else {
+           set lim $numcommits
+           set moretodo $findallowwrap
+       }
+    } else {
+       if {$l == 0} {
+           set l $numcommits
+       }
+       incr l -1
+       if {$l >= $findstartline} {
+           set lim [expr {$findstartline - 1}]
+       } else {
+           set lim -1
+           set moretodo $findallowwrap
+       }
+    }
+    set n [expr {($lim - $l) * $find_dirn}]
+    if {$n > 500} {
+       set n 500
+       set moretodo 1
+    }
+    set found 0
+    set domore 1
+    if {$gdttype eq "containing:"} {
+       for {} {$n > 0} {incr n -1; incr l $find_dirn} {
+           set id [lindex $displayorder $l]
+           # shouldn't happen unless git log doesn't give all the commits...
+           if {![info exists commitdata($id)]} continue
+           if {![doesmatch $commitdata($id)]} continue
+           if {![info exists commitinfo($id)]} {
+               getcommit $id
+           }
+           set info $commitinfo($id)
+           foreach f $info ty $fldtypes {
+               if {($findloc eq "All fields" || $findloc eq $ty) &&
+                   [doesmatch $f]} {
+                   set found 1
+                   break
+               }
+           }
+           if {$found} break
+       }
+    } else {
+       for {} {$n > 0} {incr n -1; incr l $find_dirn} {
+           set id [lindex $displayorder $l]
+           if {![info exists fhighlights($l)]} {
+               askfilehighlight $l $id
+               if {$domore} {
+                   set domore 0
+                   set findcurline [expr {$l - $find_dirn}]
+               }
+           } elseif {$fhighlights($l)} {
+               set found $domore
+               break
+           }
+       }
+    }
+    if {$found || ($domore && !$moretodo)} {
+       unset findcurline
+       unset find_dirn
+       notbusy finding
+       set fprogcoord 0
+       adjustprogress
+       if {$found} {
+           findselectline $l
+       } else {
+           bell
+       }
+       return 0
+    }
+    if {!$domore} {
+       flushhighlights
+    } else {
+       set findcurline [expr {$l - $find_dirn}]
+    }
+    set n [expr {($findcurline - $findstartline) * $find_dirn - 1}]
+    if {$n < 0} {
+       incr n $numcommits
+    }
+    set fprogcoord [expr {$n * 1.0 / $numcommits}]
+    adjustprogress
+    return $domore
+}
+
+proc findselectline {l} {
+    global findloc commentend ctext findcurline markingmatches gdttype
+
+    set markingmatches 1
+    set findcurline $l
+    selectline $l 1
+    if {$findloc == "All fields" || $findloc == "Comments"} {
+       # highlight the matches in the comments
+       set f [$ctext get 1.0 $commentend]
+       set matches [findmatches $f]
+       foreach match $matches {
+           set start [lindex $match 0]
+           set end [expr {[lindex $match 1] + 1}]
+           $ctext tag add found "1.0 + $start c" "1.0 + $end c"
+       }
+    }
+    drawvisible
+}
+
+# mark the bits of a headline or author that match a find string
+proc markmatches {canv l str tag matches font row} {
+    global selectedline
+
+    set bbox [$canv bbox $tag]
+    set x0 [lindex $bbox 0]
+    set y0 [lindex $bbox 1]
+    set y1 [lindex $bbox 3]
+    foreach match $matches {
+       set start [lindex $match 0]
+       set end [lindex $match 1]
+       if {$start > $end} continue
+       set xoff [font measure $font [string range $str 0 [expr {$start-1}]]]
+       set xlen [font measure $font [string range $str 0 [expr {$end}]]]
+       set t [$canv create rect [expr {$x0+$xoff}] $y0 \
+                  [expr {$x0+$xlen+2}] $y1 \
+                  -outline {} -tags [list match$l matches] -fill yellow]
+       $canv lower $t
+       if {[info exists selectedline] && $row == $selectedline} {
+           $canv raise $t secsel
+       }
+    }
+}
+
+proc unmarkmatches {} {
+    global markingmatches
+
+    allcanvs delete matches
+    set markingmatches 0
+    stopfinding
+}
+
+proc selcanvline {w x y} {
+    global canv canvy0 ctext linespc
+    global rowtextx
+    set ymax [lindex [$canv cget -scrollregion] 3]
+    if {$ymax == {}} return
+    set yfrac [lindex [$canv yview] 0]
+    set y [expr {$y + $yfrac * $ymax}]
+    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
+    if {$l < 0} {
+       set l 0
+    }
+    if {$w eq $canv} {
+       if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
+    }
+    unmarkmatches
+    selectline $l 1
+}
+
+proc commit_descriptor {p} {
+    global commitinfo
+    if {![info exists commitinfo($p)]} {
+       getcommit $p
+    }
+    set l "..."
+    if {[llength $commitinfo($p)] > 1} {
+       set l [lindex $commitinfo($p) 0]
+    }
+    return "$p ($l)\n"
+}
+
+# append some text to the ctext widget, and make any SHA1 ID
+# that we know about be a clickable link.
+proc appendwithlinks {text tags} {
+    global ctext commitrow linknum curview pendinglinks
+
+    set start [$ctext index "end - 1c"]
+    $ctext insert end $text $tags
+    set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
+    foreach l $links {
+       set s [lindex $l 0]
+       set e [lindex $l 1]
+       set linkid [string range $text $s $e]
+       incr e
+       $ctext tag delete link$linknum
+       $ctext tag add link$linknum "$start + $s c" "$start + $e c"
+       setlink $linkid link$linknum
+       incr linknum
+    }
+}
+
+proc setlink {id lk} {
+    global curview commitrow ctext pendinglinks commitinterest
+
+    if {[info exists commitrow($curview,$id)]} {
+       $ctext tag conf $lk -foreground blue -underline 1
+       $ctext tag bind $lk <1> [list selectline $commitrow($curview,$id) 1]
+       $ctext tag bind $lk <Enter> {linkcursor %W 1}
+       $ctext tag bind $lk <Leave> {linkcursor %W -1}
+    } else {
+       lappend pendinglinks($id) $lk
+       lappend commitinterest($id) {makelink %I}
+    }
+}
+
+proc makelink {id} {
+    global pendinglinks
+
+    if {![info exists pendinglinks($id)]} return
+    foreach lk $pendinglinks($id) {
+       setlink $id $lk
+    }
+    unset pendinglinks($id)
+}
+
+proc linkcursor {w inc} {
+    global linkentercount curtextcursor
+
+    if {[incr linkentercount $inc] > 0} {
+       $w configure -cursor hand2
+    } else {
+       $w configure -cursor $curtextcursor
+       if {$linkentercount < 0} {
+           set linkentercount 0
+       }
+    }
+}
+
+proc viewnextline {dir} {
+    global canv linespc
+
+    $canv delete hover
+    set ymax [lindex [$canv cget -scrollregion] 3]
+    set wnow [$canv yview]
+    set wtop [expr {[lindex $wnow 0] * $ymax}]
+    set newtop [expr {$wtop + $dir * $linespc}]
+    if {$newtop < 0} {
+       set newtop 0
+    } elseif {$newtop > $ymax} {
+       set newtop $ymax
+    }
+    allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
+}
+
+# add a list of tag or branch names at position pos
+# returns the number of names inserted
+proc appendrefs {pos ids var} {
+    global ctext commitrow linknum curview $var maxrefs
+
+    if {[catch {$ctext index $pos}]} {
+       return 0
+    }
+    $ctext conf -state normal
+    $ctext delete $pos "$pos lineend"
+    set tags {}
+    foreach id $ids {
+       foreach tag [set $var\($id\)] {
+           lappend tags [list $tag $id]
+       }
+    }
+    if {[llength $tags] > $maxrefs} {
+       $ctext insert $pos "many ([llength $tags])"
+    } else {
+       set tags [lsort -index 0 -decreasing $tags]
+       set sep {}
+       foreach ti $tags {
+           set id [lindex $ti 1]
+           set lk link$linknum
+           incr linknum
+           $ctext tag delete $lk
+           $ctext insert $pos $sep
+           $ctext insert $pos [lindex $ti 0] $lk
+           setlink $id $lk
+           set sep ", "
+       }
+    }
+    $ctext conf -state disabled
+    return [llength $tags]
+}
+
+# called when we have finished computing the nearby tags
+proc dispneartags {delay} {
+    global selectedline currentid showneartags tagphase
+
+    if {![info exists selectedline] || !$showneartags} return
+    after cancel dispnexttag
+    if {$delay} {
+       after 200 dispnexttag
+       set tagphase -1
+    } else {
+       after idle dispnexttag
+       set tagphase 0
+    }
+}
+
+proc dispnexttag {} {
+    global selectedline currentid showneartags tagphase ctext
+
+    if {![info exists selectedline] || !$showneartags} return
+    switch -- $tagphase {
+       0 {
+           set dtags [desctags $currentid]
+           if {$dtags ne {}} {
+               appendrefs precedes $dtags idtags
+           }
+       }
+       1 {
+           set atags [anctags $currentid]
+           if {$atags ne {}} {
+               appendrefs follows $atags idtags
+           }
+       }
+       2 {
+           set dheads [descheads $currentid]
+           if {$dheads ne {}} {
+               if {[appendrefs branch $dheads idheads] > 1
+                   && [$ctext get "branch -3c"] eq "h"} {
+                   # turn "Branch" into "Branches"
+                   $ctext conf -state normal
+                   $ctext insert "branch -2c" "es"
+                   $ctext conf -state disabled
+               }
+           }
+       }
+    }
+    if {[incr tagphase] <= 2} {
+       after idle dispnexttag
+    }
+}
+
+proc make_secsel {l} {
+    global linehtag linentag linedtag canv canv2 canv3
+
+    if {![info exists linehtag($l)]} return
+    $canv delete secsel
+    set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
+              -tags secsel -fill [$canv cget -selectbackground]]
+    $canv lower $t
+    $canv2 delete secsel
+    set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
+              -tags secsel -fill [$canv2 cget -selectbackground]]
+    $canv2 lower $t
+    $canv3 delete secsel
+    set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
+              -tags secsel -fill [$canv3 cget -selectbackground]]
+    $canv3 lower $t
+}
+
+proc selectline {l isnew} {
+    global canv ctext commitinfo selectedline
+    global displayorder
+    global canvy0 linespc parentlist children curview
+    global currentid sha1entry
+    global commentend idtags linknum
+    global mergemax numcommits pending_select
+    global cmitmode showneartags allcommits
+
+    catch {unset pending_select}
+    $canv delete hover
+    normalline
+    unsel_reflist
+    stopfinding
+    if {$l < 0 || $l >= $numcommits} return
+    set y [expr {$canvy0 + $l * $linespc}]
+    set ymax [lindex [$canv cget -scrollregion] 3]
+    set ytop [expr {$y - $linespc - 1}]
+    set ybot [expr {$y + $linespc + 1}]
+    set wnow [$canv yview]
+    set wtop [expr {[lindex $wnow 0] * $ymax}]
+    set wbot [expr {[lindex $wnow 1] * $ymax}]
+    set wh [expr {$wbot - $wtop}]
+    set newtop $wtop
+    if {$ytop < $wtop} {
+       if {$ybot < $wtop} {
+           set newtop [expr {$y - $wh / 2.0}]
+       } else {
+           set newtop $ytop
+           if {$newtop > $wtop - $linespc} {
+               set newtop [expr {$wtop - $linespc}]
+           }
+       }
+    } elseif {$ybot > $wbot} {
+       if {$ytop > $wbot} {
+           set newtop [expr {$y - $wh / 2.0}]
+       } else {
+           set newtop [expr {$ybot - $wh}]
+           if {$newtop < $wtop + $linespc} {
+               set newtop [expr {$wtop + $linespc}]
+           }
+       }
+    }
+    if {$newtop != $wtop} {
+       if {$newtop < 0} {
+           set newtop 0
+       }
+       allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
+       drawvisible
+    }
+
+    make_secsel $l
+
+    if {$isnew} {
+       addtohistory [list selectline $l 0]
+    }
+
+    set selectedline $l
+
+    set id [lindex $displayorder $l]
+    set currentid $id
+    $sha1entry delete 0 end
+    $sha1entry insert 0 $id
+    $sha1entry selection from 0
+    $sha1entry selection to end
+    rhighlight_sel $id
+
+    $ctext conf -state normal
+    clear_ctext
+    set linknum 0
+    set info $commitinfo($id)
+    set date [formatdate [lindex $info 2]]
+    $ctext insert end "Author: [lindex $info 1]  $date\n"
+    set date [formatdate [lindex $info 4]]
+    $ctext insert end "Committer: [lindex $info 3]  $date\n"
+    if {[info exists idtags($id)]} {
+       $ctext insert end "Tags:"
+       foreach tag $idtags($id) {
+           $ctext insert end " $tag"
+       }
+       $ctext insert end "\n"
+    }
+
+    set headers {}
+    set olds [lindex $parentlist $l]
+    if {[llength $olds] > 1} {
+       set np 0
+       foreach p $olds {
+           if {$np >= $mergemax} {
+               set tag mmax
+           } else {
+               set tag m$np
+           }
+           $ctext insert end "Parent: " $tag
+           appendwithlinks [commit_descriptor $p] {}
+           incr np
+       }
+    } else {
+       foreach p $olds {
+           append headers "Parent: [commit_descriptor $p]"
+       }
+    }
+
+    foreach c $children($curview,$id) {
+       append headers "Child:  [commit_descriptor $c]"
+    }
+
+    # make anything that looks like a SHA1 ID be a clickable link
+    appendwithlinks $headers {}
+    if {$showneartags} {
+       if {![info exists allcommits]} {
+           getallcommits
+       }
+       $ctext insert end "Branch: "
+       $ctext mark set branch "end -1c"
+       $ctext mark gravity branch left
+       $ctext insert end "\nFollows: "
+       $ctext mark set follows "end -1c"
+       $ctext mark gravity follows left
+       $ctext insert end "\nPrecedes: "
+       $ctext mark set precedes "end -1c"
+       $ctext mark gravity precedes left
+       $ctext insert end "\n"
+       dispneartags 1
+    }
+    $ctext insert end "\n"
+    set comment [lindex $info 5]
+    if {[string first "\r" $comment] >= 0} {
+       set comment [string map {"\r" "\n    "} $comment]
+    }
+    appendwithlinks $comment {comment}
+
+    $ctext tag remove found 1.0 end
+    $ctext conf -state disabled
+    set commentend [$ctext index "end - 1c"]
+
+    init_flist "Comments"
+    if {$cmitmode eq "tree"} {
+       gettree $id
+    } elseif {[llength $olds] <= 1} {
+       startdiff $id
+    } else {
+       mergediff $id $l
+    }
+}
+
+proc selfirstline {} {
+    unmarkmatches
+    selectline 0 1
+}
+
+proc sellastline {} {
+    global numcommits
+    unmarkmatches
+    set l [expr {$numcommits - 1}]
+    selectline $l 1
+}
+
+proc selnextline {dir} {
+    global selectedline
+    focus .
+    if {![info exists selectedline]} return
+    set l [expr {$selectedline + $dir}]
+    unmarkmatches
+    selectline $l 1
+}
+
+proc selnextpage {dir} {
+    global canv linespc selectedline numcommits
+
+    set lpp [expr {([winfo height $canv] - 2) / $linespc}]
+    if {$lpp < 1} {
+       set lpp 1
+    }
+    allcanvs yview scroll [expr {$dir * $lpp}] units
+    drawvisible
+    if {![info exists selectedline]} return
+    set l [expr {$selectedline + $dir * $lpp}]
+    if {$l < 0} {
+       set l 0
+    } elseif {$l >= $numcommits} {
+        set l [expr $numcommits - 1]
+    }
+    unmarkmatches
+    selectline $l 1
+}
+
+proc unselectline {} {
+    global selectedline currentid
+
+    catch {unset selectedline}
+    catch {unset currentid}
+    allcanvs delete secsel
+    rhighlight_none
+}
+
+proc reselectline {} {
+    global selectedline
+
+    if {[info exists selectedline]} {
+       selectline $selectedline 0
+    }
+}
+
+proc addtohistory {cmd} {
+    global history historyindex curview
+
+    set elt [list $curview $cmd]
+    if {$historyindex > 0
+       && [lindex $history [expr {$historyindex - 1}]] == $elt} {
+       return
+    }
+
+    if {$historyindex < [llength $history]} {
+       set history [lreplace $history $historyindex end $elt]
+    } else {
+       lappend history $elt
+    }
+    incr historyindex
+    if {$historyindex > 1} {
+       .tf.bar.leftbut conf -state normal
+    } else {
+       .tf.bar.leftbut conf -state disabled
+    }
+    .tf.bar.rightbut conf -state disabled
+}
+
+proc godo {elt} {
+    global curview
+
+    set view [lindex $elt 0]
+    set cmd [lindex $elt 1]
+    if {$curview != $view} {
+       showview $view
+    }
+    eval $cmd
+}
+
+proc goback {} {
+    global history historyindex
+    focus .
+
+    if {$historyindex > 1} {
+       incr historyindex -1
+       godo [lindex $history [expr {$historyindex - 1}]]
+       .tf.bar.rightbut conf -state normal
+    }
+    if {$historyindex <= 1} {
+       .tf.bar.leftbut conf -state disabled
+    }
+}
+
+proc goforw {} {
+    global history historyindex
+    focus .
+
+    if {$historyindex < [llength $history]} {
+       set cmd [lindex $history $historyindex]
+       incr historyindex
+       godo $cmd
+       .tf.bar.leftbut conf -state normal
+    }
+    if {$historyindex >= [llength $history]} {
+       .tf.bar.rightbut conf -state disabled
+    }
+}
+
+proc gettree {id} {
+    global treefilelist treeidlist diffids diffmergeid treepending
+    global nullid nullid2
+
+    set diffids $id
+    catch {unset diffmergeid}
+    if {![info exists treefilelist($id)]} {
+       if {![info exists treepending]} {
+           if {$id eq $nullid} {
+               set cmd [list | git ls-files]
+           } elseif {$id eq $nullid2} {
+               set cmd [list | git ls-files --stage -t]
+           } else {
+               set cmd [list | git ls-tree -r $id]
+           }
+           if {[catch {set gtf [open $cmd r]}]} {
+               return
+           }
+           set treepending $id
+           set treefilelist($id) {}
+           set treeidlist($id) {}
+           fconfigure $gtf -blocking 0
+           filerun $gtf [list gettreeline $gtf $id]
+       }
+    } else {
+       setfilelist $id
+    }
+}
+
+proc gettreeline {gtf id} {
+    global treefilelist treeidlist treepending cmitmode diffids nullid nullid2
+
+    set nl 0
+    while {[incr nl] <= 1000 && [gets $gtf line] >= 0} {
+       if {$diffids eq $nullid} {
+           set fname $line
+       } else {
+           if {$diffids ne $nullid2 && [lindex $line 1] ne "blob"} continue
+           set i [string first "\t" $line]
+           if {$i < 0} continue
+           set sha1 [lindex $line 2]
+           set fname [string range $line [expr {$i+1}] end]
+           if {[string index $fname 0] eq "\""} {
+               set fname [lindex $fname 0]
+           }
+           lappend treeidlist($id) $sha1
+       }
+       lappend treefilelist($id) $fname
+    }
+    if {![eof $gtf]} {
+       return [expr {$nl >= 1000? 2: 1}]
+    }
+    close $gtf
+    unset treepending
+    if {$cmitmode ne "tree"} {
+       if {![info exists diffmergeid]} {
+           gettreediffs $diffids
+       }
+    } elseif {$id ne $diffids} {
+       gettree $diffids
+    } else {
+       setfilelist $id
+    }
+    return 0
+}
+
+proc showfile {f} {
+    global treefilelist treeidlist diffids nullid nullid2
+    global ctext commentend
+
+    set i [lsearch -exact $treefilelist($diffids) $f]
+    if {$i < 0} {
+       puts "oops, $f not in list for id $diffids"
+       return
+    }
+    if {$diffids eq $nullid} {
+       if {[catch {set bf [open $f r]} err]} {
+           puts "oops, can't read $f: $err"
+           return
+       }
+    } else {
+       set blob [lindex $treeidlist($diffids) $i]
+       if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} {
+           puts "oops, error reading blob $blob: $err"
+           return
+       }
+    }
+    fconfigure $bf -blocking 0
+    filerun $bf [list getblobline $bf $diffids]
+    $ctext config -state normal
+    clear_ctext $commentend
+    $ctext insert end "\n"
+    $ctext insert end "$f\n" filesep
+    $ctext config -state disabled
+    $ctext yview $commentend
+    settabs 0
+}
+
+proc getblobline {bf id} {
+    global diffids cmitmode ctext
+
+    if {$id ne $diffids || $cmitmode ne "tree"} {
+       catch {close $bf}
+       return 0
+    }
+    $ctext config -state normal
+    set nl 0
+    while {[incr nl] <= 1000 && [gets $bf line] >= 0} {
+       $ctext insert end "$line\n"
+    }
+    if {[eof $bf]} {
+       # delete last newline
+       $ctext delete "end - 2c" "end - 1c"
+       close $bf
+       return 0
+    }
+    $ctext config -state disabled
+    return [expr {$nl >= 1000? 2: 1}]
+}
+
+proc mergediff {id l} {
+    global diffmergeid mdifffd
+    global diffids
+    global parentlist
+    global limitdiffs viewfiles curview
+
+    set diffmergeid $id
+    set diffids $id
+    # this doesn't seem to actually affect anything...
+    set cmd [concat | git diff-tree --no-commit-id --cc $id]
+    if {$limitdiffs && $viewfiles($curview) ne {}} {
+       set cmd [concat $cmd -- $viewfiles($curview)]
+    }
+    if {[catch {set mdf [open $cmd r]} err]} {
+       error_popup "Error getting merge diffs: $err"
+       return
+    }
+    fconfigure $mdf -blocking 0
+    set mdifffd($id) $mdf
+    set np [llength [lindex $parentlist $l]]
+    settabs $np
+    filerun $mdf [list getmergediffline $mdf $id $np]
+}
+
+proc getmergediffline {mdf id np} {
+    global diffmergeid ctext cflist mergemax
+    global difffilestart mdifffd
+
+    $ctext conf -state normal
+    set nr 0
+    while {[incr nr] <= 1000 && [gets $mdf line] >= 0} {
+       if {![info exists diffmergeid] || $id != $diffmergeid
+           || $mdf != $mdifffd($id)} {
+           close $mdf
+           return 0
+       }
+       if {[regexp {^diff --cc (.*)} $line match fname]} {
+           # start of a new file
+           $ctext insert end "\n"
+           set here [$ctext index "end - 1c"]
+           lappend difffilestart $here
+           add_flist [list $fname]
+           set l [expr {(78 - [string length $fname]) / 2}]
+           set pad [string range "----------------------------------------" 1 $l]
+           $ctext insert end "$pad $fname $pad\n" filesep
+       } elseif {[regexp {^@@} $line]} {
+           $ctext insert end "$line\n" hunksep
+       } elseif {[regexp {^[0-9a-f]{40}$} $line] || [regexp {^index} $line]} {
+           # do nothing
+       } else {
+           # parse the prefix - one ' ', '-' or '+' for each parent
+           set spaces {}
+           set minuses {}
+           set pluses {}
+           set isbad 0
+           for {set j 0} {$j < $np} {incr j} {
+               set c [string range $line $j $j]
+               if {$c == " "} {
+                   lappend spaces $j
+               } elseif {$c == "-"} {
+                   lappend minuses $j
+               } elseif {$c == "+"} {
+                   lappend pluses $j
+               } else {
+                   set isbad 1
+                   break
+               }
+           }
+           set tags {}
+           set num {}
+           if {!$isbad && $minuses ne {} && $pluses eq {}} {
+               # line doesn't appear in result, parents in $minuses have the line
+               set num [lindex $minuses 0]
+           } elseif {!$isbad && $pluses ne {} && $minuses eq {}} {
+               # line appears in result, parents in $pluses don't have the line
+               lappend tags mresult
+               set num [lindex $spaces 0]
+           }
+           if {$num ne {}} {
+               if {$num >= $mergemax} {
+                   set num "max"
+               }
+               lappend tags m$num
+           }
+           $ctext insert end "$line\n" $tags
+       }
+    }
+    $ctext conf -state disabled
+    if {[eof $mdf]} {
+       close $mdf
+       return 0
+    }
+    return [expr {$nr >= 1000? 2: 1}]
+}
+
+proc startdiff {ids} {
+    global treediffs diffids treepending diffmergeid nullid nullid2
+
+    settabs 1
+    set diffids $ids
+    catch {unset diffmergeid}
+    if {![info exists treediffs($ids)] ||
+       [lsearch -exact $ids $nullid] >= 0 ||
+       [lsearch -exact $ids $nullid2] >= 0} {
+       if {![info exists treepending]} {
+           gettreediffs $ids
+       }
+    } else {
+       addtocflist $ids
+    }
+}
+
+proc path_filter {filter name} {
+    foreach p $filter {
+       set l [string length $p]
+       if {[string index $p end] eq "/"} {
+           if {[string compare -length $l $p $name] == 0} {
+               return 1
+           }
+       } else {
+           if {[string compare -length $l $p $name] == 0 &&
+               ([string length $name] == $l ||
+                [string index $name $l] eq "/")} {
+               return 1
+           }
+       }
+    }
+    return 0
+}
+
+proc addtocflist {ids} {
+    global treediffs
+
+    add_flist $treediffs($ids)
+    getblobdiffs $ids
+}
+
+proc diffcmd {ids flags} {
+    global nullid nullid2
+
+    set i [lsearch -exact $ids $nullid]
+    set j [lsearch -exact $ids $nullid2]
+    if {$i >= 0} {
+       if {[llength $ids] > 1 && $j < 0} {
+           # comparing working directory with some specific revision
+           set cmd [concat | git diff-index $flags]
+           if {$i == 0} {
+               lappend cmd -R [lindex $ids 1]
+           } else {
+               lappend cmd [lindex $ids 0]
+           }
+       } else {
+           # comparing working directory with index
+           set cmd [concat | git diff-files $flags]
+           if {$j == 1} {
+               lappend cmd -R
+           }
+       }
+    } elseif {$j >= 0} {
+       set cmd [concat | git diff-index --cached $flags]
+       if {[llength $ids] > 1} {
+           # comparing index with specific revision
+           if {$i == 0} {
+               lappend cmd -R [lindex $ids 1]
+           } else {
+               lappend cmd [lindex $ids 0]
+           }
+       } else {
+           # comparing index with HEAD
+           lappend cmd HEAD
+       }
+    } else {
+       set cmd [concat | git diff-tree -r $flags $ids]
+    }
+    return $cmd
+}
+
+proc gettreediffs {ids} {
+    global treediff treepending
+
+    set treepending $ids
+    set treediff {}
+    if {[catch {set gdtf [open [diffcmd $ids {--no-commit-id}] r]}]} return
+    fconfigure $gdtf -blocking 0
+    filerun $gdtf [list gettreediffline $gdtf $ids]
+}
+
+proc gettreediffline {gdtf ids} {
+    global treediff treediffs treepending diffids diffmergeid
+    global cmitmode viewfiles curview limitdiffs
+
+    set nr 0
+    while {[incr nr] <= 1000 && [gets $gdtf line] >= 0} {
+       set i [string first "\t" $line]
+       if {$i >= 0} {
+           set file [string range $line [expr {$i+1}] end]
+           if {[string index $file 0] eq "\""} {
+               set file [lindex $file 0]
+           }
+           lappend treediff $file
+       }
+    }
+    if {![eof $gdtf]} {
+       return [expr {$nr >= 1000? 2: 1}]
+    }
+    close $gdtf
+    if {$limitdiffs && $viewfiles($curview) ne {}} {
+       set flist {}
+       foreach f $treediff {
+           if {[path_filter $viewfiles($curview) $f]} {
+               lappend flist $f
+           }
+       }
+       set treediffs($ids) $flist
+    } else {
+       set treediffs($ids) $treediff
+    }
+    unset treepending
+    if {$cmitmode eq "tree"} {
+       gettree $diffids
+    } elseif {$ids != $diffids} {
+       if {![info exists diffmergeid]} {
+           gettreediffs $diffids
+       }
+    } else {
+       addtocflist $ids
+    }
+    return 0
+}
+
+# empty string or positive integer
+proc diffcontextvalidate {v} {
+    return [regexp {^(|[1-9][0-9]*)$} $v]
+}
+
+proc diffcontextchange {n1 n2 op} {
+    global diffcontextstring diffcontext
+
+    if {[string is integer -strict $diffcontextstring]} {
+       if {$diffcontextstring > 0} {
+           set diffcontext $diffcontextstring
+           reselectline
+       }
+    }
+}
+
+proc getblobdiffs {ids} {
+    global blobdifffd diffids env
+    global diffinhdr treediffs
+    global diffcontext
+    global limitdiffs viewfiles curview
+
+    set cmd [diffcmd $ids "-p -C --no-commit-id -U$diffcontext"]
+    if {$limitdiffs && $viewfiles($curview) ne {}} {
+       set cmd [concat $cmd -- $viewfiles($curview)]
+    }
+    if {[catch {set bdf [open $cmd r]} err]} {
+       puts "error getting diffs: $err"
+       return
+    }
+    set diffinhdr 0
+    fconfigure $bdf -blocking 0
+    set blobdifffd($ids) $bdf
+    filerun $bdf [list getblobdiffline $bdf $diffids]
+}
+
+proc setinlist {var i val} {
+    global $var
+
+    while {[llength [set $var]] < $i} {
+       lappend $var {}
+    }
+    if {[llength [set $var]] == $i} {
+       lappend $var $val
+    } else {
+       lset $var $i $val
+    }
+}
+
+proc makediffhdr {fname ids} {
+    global ctext curdiffstart treediffs
+
+    set i [lsearch -exact $treediffs($ids) $fname]
+    if {$i >= 0} {
+       setinlist difffilestart $i $curdiffstart
+    }
+    set l [expr {(78 - [string length $fname]) / 2}]
+    set pad [string range "----------------------------------------" 1 $l]
+    $ctext insert $curdiffstart "$pad $fname $pad" filesep
+}
+
+proc getblobdiffline {bdf ids} {
+    global diffids blobdifffd ctext curdiffstart
+    global diffnexthead diffnextnote difffilestart
+    global diffinhdr treediffs
+
+    set nr 0
+    $ctext conf -state normal
+    while {[incr nr] <= 1000 && [gets $bdf line] >= 0} {
+       if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
+           close $bdf
+           return 0
+       }
+       if {![string compare -length 11 "diff --git " $line]} {
+           # trim off "diff --git "
+           set line [string range $line 11 end]
+           set diffinhdr 1
+           # start of a new file
+           $ctext insert end "\n"
+           set curdiffstart [$ctext index "end - 1c"]
+           $ctext insert end "\n" filesep
+           # If the name hasn't changed the length will be odd,
+           # the middle char will be a space, and the two bits either
+           # side will be a/name and b/name, or "a/name" and "b/name".
+           # If the name has changed we'll get "rename from" and
+           # "rename to" or "copy from" and "copy to" lines following this,
+           # and we'll use them to get the filenames.
+           # This complexity is necessary because spaces in the filename(s)
+           # don't get escaped.
+           set l [string length $line]
+           set i [expr {$l / 2}]
+           if {!(($l & 1) && [string index $line $i] eq " " &&
+                 [string range $line 2 [expr {$i - 1}]] eq \
+                     [string range $line [expr {$i + 3}] end])} {
+               continue
+           }
+           # unescape if quoted and chop off the a/ from the front
+           if {[string index $line 0] eq "\""} {
+               set fname [string range [lindex $line 0] 2 end]
+           } else {
+               set fname [string range $line 2 [expr {$i - 1}]]
+           }
+           makediffhdr $fname $ids
+
+       } elseif {[regexp {^@@ -([0-9]+)(,[0-9]+)? \+([0-9]+)(,[0-9]+)? @@(.*)} \
+                      $line match f1l f1c f2l f2c rest]} {
+           $ctext insert end "$line\n" hunksep
+           set diffinhdr 0
+
+       } elseif {$diffinhdr} {
+           if {![string compare -length 12 "rename from " $line]} {
+               set fname [string range $line [expr 6 + [string first " from " $line] ] end]
+               if {[string index $fname 0] eq "\""} {
+                   set fname [lindex $fname 0]
+               }
+               set i [lsearch -exact $treediffs($ids) $fname]
+               if {$i >= 0} {
+                   setinlist difffilestart $i $curdiffstart
+               }
+           } elseif {![string compare -length 10 $line "rename to "] ||
+                     ![string compare -length 8 $line "copy to "]} {
+               set fname [string range $line [expr 4 + [string first " to " $line] ] end]
+               if {[string index $fname 0] eq "\""} {
+                   set fname [lindex $fname 0]
+               }
+               makediffhdr $fname $ids
+           } elseif {[string compare -length 3 $line "---"] == 0} {
+               # do nothing
+               continue
+           } elseif {[string compare -length 3 $line "+++"] == 0} {
+               set diffinhdr 0
+               continue
+           }
+           $ctext insert end "$line\n" filesep
+
+       } else {
+           set x [string range $line 0 0]
+           if {$x == "-" || $x == "+"} {
+               set tag [expr {$x == "+"}]
+               $ctext insert end "$line\n" d$tag
+           } elseif {$x == " "} {
+               $ctext insert end "$line\n"
+           } else {
+               # "\ No newline at end of file",
+               # or something else we don't recognize
+               $ctext insert end "$line\n" hunksep
+           }
+       }
+    }
+    $ctext conf -state disabled
+    if {[eof $bdf]} {
+       close $bdf
+       return 0
+    }
+    return [expr {$nr >= 1000? 2: 1}]
+}
+
+proc changediffdisp {} {
+    global ctext diffelide
+
+    $ctext tag conf d0 -elide [lindex $diffelide 0]
+    $ctext tag conf d1 -elide [lindex $diffelide 1]
+}
+
+proc prevfile {} {
+    global difffilestart ctext
+    set prev [lindex $difffilestart 0]
+    set here [$ctext index @0,0]
+    foreach loc $difffilestart {
+       if {[$ctext compare $loc >= $here]} {
+           $ctext yview $prev
+           return
+       }
+       set prev $loc
+    }
+    $ctext yview $prev
+}
+
+proc nextfile {} {
+    global difffilestart ctext
+    set here [$ctext index @0,0]
+    foreach loc $difffilestart {
+       if {[$ctext compare $loc > $here]} {
+           $ctext yview $loc
+           return
+       }
+    }
+}
+
+proc clear_ctext {{first 1.0}} {
+    global ctext smarktop smarkbot
+    global pendinglinks
+
+    set l [lindex [split $first .] 0]
+    if {![info exists smarktop] || [$ctext compare $first < $smarktop.0]} {
+       set smarktop $l
+    }
+    if {![info exists smarkbot] || [$ctext compare $first < $smarkbot.0]} {
+       set smarkbot $l
+    }
+    $ctext delete $first end
+    if {$first eq "1.0"} {
+       catch {unset pendinglinks}
+    }
+}
+
+proc settabs {{firstab {}}} {
+    global firsttabstop tabstop ctext have_tk85
+
+    if {$firstab ne {} && $have_tk85} {
+       set firsttabstop $firstab
+    }
+    set w [font measure textfont "0"]
+    if {$firsttabstop != 0} {
+       $ctext conf -tabs [list [expr {($firsttabstop + $tabstop) * $w}] \
+                              [expr {($firsttabstop + 2 * $tabstop) * $w}]]
+    } elseif {$have_tk85 || $tabstop != 8} {
+       $ctext conf -tabs [expr {$tabstop * $w}]
+    } else {
+       $ctext conf -tabs {}
+    }
+}
+
+proc incrsearch {name ix op} {
+    global ctext searchstring searchdirn
+
+    $ctext tag remove found 1.0 end
+    if {[catch {$ctext index anchor}]} {
+       # no anchor set, use start of selection, or of visible area
+       set sel [$ctext tag ranges sel]
+       if {$sel ne {}} {
+           $ctext mark set anchor [lindex $sel 0]
+       } elseif {$searchdirn eq "-forwards"} {
+           $ctext mark set anchor @0,0
+       } else {
+           $ctext mark set anchor @0,[winfo height $ctext]
+       }
+    }
+    if {$searchstring ne {}} {
+       set here [$ctext search $searchdirn -- $searchstring anchor]
+       if {$here ne {}} {
+           $ctext see $here
+       }
+       searchmarkvisible 1
+    }
+}
+
+proc dosearch {} {
+    global sstring ctext searchstring searchdirn
+
+    focus $sstring
+    $sstring icursor end
+    set searchdirn -forwards
+    if {$searchstring ne {}} {
+       set sel [$ctext tag ranges sel]
+       if {$sel ne {}} {
+           set start "[lindex $sel 0] + 1c"
+       } elseif {[catch {set start [$ctext index anchor]}]} {
+           set start "@0,0"
+       }
+       set match [$ctext search -count mlen -- $searchstring $start]
+       $ctext tag remove sel 1.0 end
+       if {$match eq {}} {
+           bell
+           return
+       }
+       $ctext see $match
+       set mend "$match + $mlen c"
+       $ctext tag add sel $match $mend
+       $ctext mark unset anchor
+    }
+}
+
+proc dosearchback {} {
+    global sstring ctext searchstring searchdirn
+
+    focus $sstring
+    $sstring icursor end
+    set searchdirn -backwards
+    if {$searchstring ne {}} {
+       set sel [$ctext tag ranges sel]
+       if {$sel ne {}} {
+           set start [lindex $sel 0]
+       } elseif {[catch {set start [$ctext index anchor]}]} {
+           set start @0,[winfo height $ctext]
+       }
+       set match [$ctext search -backwards -count ml -- $searchstring $start]
+       $ctext tag remove sel 1.0 end
+       if {$match eq {}} {
+           bell
+           return
+       }
+       $ctext see $match
+       set mend "$match + $ml c"
+       $ctext tag add sel $match $mend
+       $ctext mark unset anchor
+    }
+}
+
+proc searchmark {first last} {
+    global ctext searchstring
+
+    set mend $first.0
+    while {1} {
+       set match [$ctext search -count mlen -- $searchstring $mend $last.end]
+       if {$match eq {}} break
+       set mend "$match + $mlen c"
+       $ctext tag add found $match $mend
+    }
+}
+
+proc searchmarkvisible {doall} {
+    global ctext smarktop smarkbot
+
+    set topline [lindex [split [$ctext index @0,0] .] 0]
+    set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0]
+    if {$doall || $botline < $smarktop || $topline > $smarkbot} {
+       # no overlap with previous
+       searchmark $topline $botline
+       set smarktop $topline
+       set smarkbot $botline
+    } else {
+       if {$topline < $smarktop} {
+           searchmark $topline [expr {$smarktop-1}]
+           set smarktop $topline
+       }
+       if {$botline > $smarkbot} {
+           searchmark [expr {$smarkbot+1}] $botline
+           set smarkbot $botline
+       }
+    }
+}
+
+proc scrolltext {f0 f1} {
+    global searchstring
+
+    .bleft.sb set $f0 $f1
+    if {$searchstring ne {}} {
+       searchmarkvisible 0
+    }
+}
+
+proc setcoords {} {
+    global linespc charspc canvx0 canvy0
+    global xspc1 xspc2 lthickness
+
+    set linespc [font metrics mainfont -linespace]
+    set charspc [font measure mainfont "m"]
+    set canvy0 [expr {int(3 + 0.5 * $linespc)}]
+    set canvx0 [expr {int(3 + 0.5 * $linespc)}]
+    set lthickness [expr {int($linespc / 9) + 1}]
+    set xspc1(0) $linespc
+    set xspc2 $linespc
+}
+
+proc redisplay {} {
+    global canv
+    global selectedline
+
+    set ymax [lindex [$canv cget -scrollregion] 3]
+    if {$ymax eq {} || $ymax == 0} return
+    set span [$canv yview]
+    clear_display
+    setcanvscroll
+    allcanvs yview moveto [lindex $span 0]
+    drawvisible
+    if {[info exists selectedline]} {
+       selectline $selectedline 0
+       allcanvs yview moveto [lindex $span 0]
+    }
+}
+
+proc parsefont {f n} {
+    global fontattr
+
+    set fontattr($f,family) [lindex $n 0]
+    set s [lindex $n 1]
+    if {$s eq {} || $s == 0} {
+       set s 10
+    } elseif {$s < 0} {
+       set s [expr {int(-$s / [winfo fpixels . 1p] + 0.5)}]
+    }
+    set fontattr($f,size) $s
+    set fontattr($f,weight) normal
+    set fontattr($f,slant) roman
+    foreach style [lrange $n 2 end] {
+       switch -- $style {
+           "normal" -
+           "bold"   {set fontattr($f,weight) $style}
+           "roman" -
+           "italic" {set fontattr($f,slant) $style}
+       }
+    }
+}
+
+proc fontflags {f {isbold 0}} {
+    global fontattr
+
+    return [list -family $fontattr($f,family) -size $fontattr($f,size) \
+               -weight [expr {$isbold? "bold": $fontattr($f,weight)}] \
+               -slant $fontattr($f,slant)]
+}
+
+proc fontname {f} {
+    global fontattr
+
+    set n [list $fontattr($f,family) $fontattr($f,size)]
+    if {$fontattr($f,weight) eq "bold"} {
+       lappend n "bold"
+    }
+    if {$fontattr($f,slant) eq "italic"} {
+       lappend n "italic"
+    }
+    return $n
+}
+
+proc incrfont {inc} {
+    global mainfont textfont ctext canv phase cflist showrefstop
+    global stopped entries fontattr
+
+    unmarkmatches
+    set s $fontattr(mainfont,size)
+    incr s $inc
+    if {$s < 1} {
+       set s 1
+    }
+    set fontattr(mainfont,size) $s
+    font config mainfont -size $s
+    font config mainfontbold -size $s
+    set mainfont [fontname mainfont]
+    set s $fontattr(textfont,size)
+    incr s $inc
+    if {$s < 1} {
+       set s 1
+    }
+    set fontattr(textfont,size) $s
+    font config textfont -size $s
+    font config textfontbold -size $s
+    set textfont [fontname textfont]
+    setcoords
+    settabs
+    redisplay
+}
+
+proc clearsha1 {} {
+    global sha1entry sha1string
+    if {[string length $sha1string] == 40} {
+       $sha1entry delete 0 end
+    }
+}
+
+proc sha1change {n1 n2 op} {
+    global sha1string currentid sha1but
+    if {$sha1string == {}
+       || ([info exists currentid] && $sha1string == $currentid)} {
+       set state disabled
+    } else {
+       set state normal
+    }
+    if {[$sha1but cget -state] == $state} return
+    if {$state == "normal"} {
+       $sha1but conf -state normal -relief raised -text "Goto: "
+    } else {
+       $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
+    }
+}
+
+proc gotocommit {} {
+    global sha1string currentid commitrow tagids headids
+    global displayorder numcommits curview
+
+    if {$sha1string == {}
+       || ([info exists currentid] && $sha1string == $currentid)} return
+    if {[info exists tagids($sha1string)]} {
+       set id $tagids($sha1string)
+    } elseif {[info exists headids($sha1string)]} {
+       set id $headids($sha1string)
+    } else {
+       set id [string tolower $sha1string]
+       if {[regexp {^[0-9a-f]{4,39}$} $id]} {
+           set matches {}
+           foreach i $displayorder {
+               if {[string match $id* $i]} {
+                   lappend matches $i
+               }
+           }
+           if {$matches ne {}} {
+               if {[llength $matches] > 1} {
+                   error_popup "Short SHA1 id $id is ambiguous"
+                   return
+               }
+               set id [lindex $matches 0]
+           }
+       }
+    }
+    if {[info exists commitrow($curview,$id)]} {
+       selectline $commitrow($curview,$id) 1
+       return
+    }
+    if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
+       set type "SHA1 id"
+    } else {
+       set type "Tag/Head"
+    }
+    error_popup "$type $sha1string is not known"
+}
+
+proc lineenter {x y id} {
+    global hoverx hovery hoverid hovertimer
+    global commitinfo canv
+
+    if {![info exists commitinfo($id)] && ![getcommit $id]} return
+    set hoverx $x
+    set hovery $y
+    set hoverid $id
+    if {[info exists hovertimer]} {
+       after cancel $hovertimer
+    }
+    set hovertimer [after 500 linehover]
+    $canv delete hover
+}
+
+proc linemotion {x y id} {
+    global hoverx hovery hoverid hovertimer
+
+    if {[info exists hoverid] && $id == $hoverid} {
+       set hoverx $x
+       set hovery $y
+       if {[info exists hovertimer]} {
+           after cancel $hovertimer
+       }
+       set hovertimer [after 500 linehover]
+    }
+}
+
+proc lineleave {id} {
+    global hoverid hovertimer canv
+
+    if {[info exists hoverid] && $id == $hoverid} {
+       $canv delete hover
+       if {[info exists hovertimer]} {
+           after cancel $hovertimer
+           unset hovertimer
+       }
+       unset hoverid
+    }
+}
+
+proc linehover {} {
+    global hoverx hovery hoverid hovertimer
+    global canv linespc lthickness
+    global commitinfo
+
+    set text [lindex $commitinfo($hoverid) 0]
+    set ymax [lindex [$canv cget -scrollregion] 3]
+    if {$ymax == {}} return
+    set yfrac [lindex [$canv yview] 0]
+    set x [expr {$hoverx + 2 * $linespc}]
+    set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
+    set x0 [expr {$x - 2 * $lthickness}]
+    set y0 [expr {$y - 2 * $lthickness}]
+    set x1 [expr {$x + [font measure mainfont $text] + 2 * $lthickness}]
+    set y1 [expr {$y + $linespc + 2 * $lthickness}]
+    set t [$canv create rectangle $x0 $y0 $x1 $y1 \
+              -fill \#ffff80 -outline black -width 1 -tags hover]
+    $canv raise $t
+    set t [$canv create text $x $y -anchor nw -text $text -tags hover \
+              -font mainfont]
+    $canv raise $t
+}
+
+proc clickisonarrow {id y} {
+    global lthickness
+
+    set ranges [rowranges $id]
+    set thresh [expr {2 * $lthickness + 6}]
+    set n [expr {[llength $ranges] - 1}]
+    for {set i 1} {$i < $n} {incr i} {
+       set row [lindex $ranges $i]
+       if {abs([yc $row] - $y) < $thresh} {
+           return $i
+       }
+    }
+    return {}
+}
+
+proc arrowjump {id n y} {
+    global canv
+
+    # 1 <-> 2, 3 <-> 4, etc...
+    set n [expr {(($n - 1) ^ 1) + 1}]
+    set row [lindex [rowranges $id] $n]
+    set yt [yc $row]
+    set ymax [lindex [$canv cget -scrollregion] 3]
+    if {$ymax eq {} || $ymax <= 0} return
+    set view [$canv yview]
+    set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
+    set yfrac [expr {$yt / $ymax - $yspan / 2}]
+    if {$yfrac < 0} {
+       set yfrac 0
+    }
+    allcanvs yview moveto $yfrac
+}
+
+proc lineclick {x y id isnew} {
+    global ctext commitinfo children canv thickerline curview commitrow
+
+    if {![info exists commitinfo($id)] && ![getcommit $id]} return
+    unmarkmatches
+    unselectline
+    normalline
+    $canv delete hover
+    # draw this line thicker than normal
+    set thickerline $id
+    drawlines $id
+    if {$isnew} {
+       set ymax [lindex [$canv cget -scrollregion] 3]
+       if {$ymax eq {}} return
+       set yfrac [lindex [$canv yview] 0]
+       set y [expr {$y + $yfrac * $ymax}]
+    }
+    set dirn [clickisonarrow $id $y]
+    if {$dirn ne {}} {
+       arrowjump $id $dirn $y
+       return
+    }
+
+    if {$isnew} {
+       addtohistory [list lineclick $x $y $id 0]
+    }
+    # fill the details pane with info about this line
+    $ctext conf -state normal
+    clear_ctext
+    settabs 0
+    $ctext insert end "Parent:\t"
+    $ctext insert end $id link0
+    setlink $id link0
+    set info $commitinfo($id)
+    $ctext insert end "\n\t[lindex $info 0]\n"
+    $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
+    set date [formatdate [lindex $info 2]]
+    $ctext insert end "\tDate:\t$date\n"
+    set kids $children($curview,$id)
+    if {$kids ne {}} {
+       $ctext insert end "\nChildren:"
+       set i 0
+       foreach child $kids {
+           incr i
+           if {![info exists commitinfo($child)] && ![getcommit $child]} continue
+           set info $commitinfo($child)
+           $ctext insert end "\n\t"
+           $ctext insert end $child link$i
+           setlink $child link$i
+           $ctext insert end "\n\t[lindex $info 0]"
+           $ctext insert end "\n\tAuthor:\t[lindex $info 1]"
+           set date [formatdate [lindex $info 2]]
+           $ctext insert end "\n\tDate:\t$date\n"
+       }
+    }
+    $ctext conf -state disabled
+    init_flist {}
+}
+
+proc normalline {} {
+    global thickerline
+    if {[info exists thickerline]} {
+       set id $thickerline
+       unset thickerline
+       drawlines $id
+    }
+}
+
+proc selbyid {id} {
+    global commitrow curview
+    if {[info exists commitrow($curview,$id)]} {
+       selectline $commitrow($curview,$id) 1
+    }
+}
+
+proc mstime {} {
+    global startmstime
+    if {![info exists startmstime]} {
+       set startmstime [clock clicks -milliseconds]
+    }
+    return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
+}
+
+proc rowmenu {x y id} {
+    global rowctxmenu commitrow selectedline rowmenuid curview
+    global nullid nullid2 fakerowmenu mainhead
+
+    stopfinding
+    set rowmenuid $id
+    if {![info exists selectedline]
+       || $commitrow($curview,$id) eq $selectedline} {
+       set state disabled
+    } else {
+       set state normal
+    }
+    if {$id ne $nullid && $id ne $nullid2} {
+       set menu $rowctxmenu
+       $menu entryconfigure 7 -label "Reset $mainhead branch to here"
+    } else {
+       set menu $fakerowmenu
+    }
+    $menu entryconfigure "Diff this*" -state $state
+    $menu entryconfigure "Diff selected*" -state $state
+    $menu entryconfigure "Make patch" -state $state
+    tk_popup $menu $x $y
+}
+
+proc diffvssel {dirn} {
+    global rowmenuid selectedline displayorder
+
+    if {![info exists selectedline]} return
+    if {$dirn} {
+       set oldid [lindex $displayorder $selectedline]
+       set newid $rowmenuid
+    } else {
+       set oldid $rowmenuid
+       set newid [lindex $displayorder $selectedline]
+    }
+    addtohistory [list doseldiff $oldid $newid]
+    doseldiff $oldid $newid
+}
+
+proc doseldiff {oldid newid} {
+    global ctext
+    global commitinfo
+
+    $ctext conf -state normal
+    clear_ctext
+    init_flist "Top"
+    $ctext insert end "From "
+    $ctext insert end $oldid link0
+    setlink $oldid link0
+    $ctext insert end "\n     "
+    $ctext insert end [lindex $commitinfo($oldid) 0]
+    $ctext insert end "\n\nTo   "
+    $ctext insert end $newid link1
+    setlink $newid link1
+    $ctext insert end "\n     "
+    $ctext insert end [lindex $commitinfo($newid) 0]
+    $ctext insert end "\n"
+    $ctext conf -state disabled
+    $ctext tag remove found 1.0 end
+    startdiff [list $oldid $newid]
+}
+
+proc mkpatch {} {
+    global rowmenuid currentid commitinfo patchtop patchnum
+
+    if {![info exists currentid]} return
+    set oldid $currentid
+    set oldhead [lindex $commitinfo($oldid) 0]
+    set newid $rowmenuid
+    set newhead [lindex $commitinfo($newid) 0]
+    set top .patch
+    set patchtop $top
+    catch {destroy $top}
+    toplevel $top
+    label $top.title -text "Generate patch"
+    grid $top.title - -pady 10
+    label $top.from -text "From:"
+    entry $top.fromsha1 -width 40 -relief flat
+    $top.fromsha1 insert 0 $oldid
+    $top.fromsha1 conf -state readonly
+    grid $top.from $top.fromsha1 -sticky w
+    entry $top.fromhead -width 60 -relief flat
+    $top.fromhead insert 0 $oldhead
+    $top.fromhead conf -state readonly
+    grid x $top.fromhead -sticky w
+    label $top.to -text "To:"
+    entry $top.tosha1 -width 40 -relief flat
+    $top.tosha1 insert 0 $newid
+    $top.tosha1 conf -state readonly
+    grid $top.to $top.tosha1 -sticky w
+    entry $top.tohead -width 60 -relief flat
+    $top.tohead insert 0 $newhead
+    $top.tohead conf -state readonly
+    grid x $top.tohead -sticky w
+    button $top.rev -text "Reverse" -command mkpatchrev -padx 5
+    grid $top.rev x -pady 10
+    label $top.flab -text "Output file:"
+    entry $top.fname -width 60
+    $top.fname insert 0 [file normalize "patch$patchnum.patch"]
+    incr patchnum
+    grid $top.flab $top.fname -sticky w
+    frame $top.buts
+    button $top.buts.gen -text "Generate" -command mkpatchgo
+    button $top.buts.can -text "Cancel" -command mkpatchcan
+    grid $top.buts.gen $top.buts.can
+    grid columnconfigure $top.buts 0 -weight 1 -uniform a
+    grid columnconfigure $top.buts 1 -weight 1 -uniform a
+    grid $top.buts - -pady 10 -sticky ew
+    focus $top.fname
+}
+
+proc mkpatchrev {} {
+    global patchtop
+
+    set oldid [$patchtop.fromsha1 get]
+    set oldhead [$patchtop.fromhead get]
+    set newid [$patchtop.tosha1 get]
+    set newhead [$patchtop.tohead get]
+    foreach e [list fromsha1 fromhead tosha1 tohead] \
+           v [list $newid $newhead $oldid $oldhead] {
+       $patchtop.$e conf -state normal
+       $patchtop.$e delete 0 end
+       $patchtop.$e insert 0 $v
+       $patchtop.$e conf -state readonly
+    }
+}
+
+proc mkpatchgo {} {
+    global patchtop nullid nullid2
+
+    set oldid [$patchtop.fromsha1 get]
+    set newid [$patchtop.tosha1 get]
+    set fname [$patchtop.fname get]
+    set cmd [diffcmd [list $oldid $newid] -p]
+    # trim off the initial "|"
+    set cmd [lrange $cmd 1 end]
+    lappend cmd >$fname &
+    if {[catch {eval exec $cmd} err]} {
+       error_popup "Error creating patch: $err"
+    }
+    catch {destroy $patchtop}
+    unset patchtop
+}
+
+proc mkpatchcan {} {
+    global patchtop
+
+    catch {destroy $patchtop}
+    unset patchtop
+}
+
+proc mktag {} {
+    global rowmenuid mktagtop commitinfo
+
+    set top .maketag
+    set mktagtop $top
+    catch {destroy $top}
+    toplevel $top
+    label $top.title -text "Create tag"
+    grid $top.title - -pady 10
+    label $top.id -text "ID:"
+    entry $top.sha1 -width 40 -relief flat
+    $top.sha1 insert 0 $rowmenuid
+    $top.sha1 conf -state readonly
+    grid $top.id $top.sha1 -sticky w
+    entry $top.head -width 60 -relief flat
+    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
+    $top.head conf -state readonly
+    grid x $top.head -sticky w
+    label $top.tlab -text "Tag name:"
+    entry $top.tag -width 60
+    grid $top.tlab $top.tag -sticky w
+    frame $top.buts
+    button $top.buts.gen -text "Create" -command mktaggo
+    button $top.buts.can -text "Cancel" -command mktagcan
+    grid $top.buts.gen $top.buts.can
+    grid columnconfigure $top.buts 0 -weight 1 -uniform a
+    grid columnconfigure $top.buts 1 -weight 1 -uniform a
+    grid $top.buts - -pady 10 -sticky ew
+    focus $top.tag
+}
+
+proc domktag {} {
+    global mktagtop env tagids idtags
+
+    set id [$mktagtop.sha1 get]
+    set tag [$mktagtop.tag get]
+    if {$tag == {}} {
+       error_popup "No tag name specified"
+       return
+    }
+    if {[info exists tagids($tag)]} {
+       error_popup "Tag \"$tag\" already exists"
+       return
+    }
+    if {[catch {
+       set dir [gitdir]
+       set fname [file join $dir "refs/tags" $tag]
+       set f [open $fname w]
+       puts $f $id
+       close $f
+    } err]} {
+       error_popup "Error creating tag: $err"
+       return
+    }
+
+    set tagids($tag) $id
+    lappend idtags($id) $tag
+    redrawtags $id
+    addedtag $id
+    dispneartags 0
+    run refill_reflist
+}
+
+proc redrawtags {id} {
+    global canv linehtag commitrow idpos selectedline curview
+    global canvxmax iddrawn
+
+    if {![info exists commitrow($curview,$id)]} return
+    if {![info exists iddrawn($id)]} return
+    drawcommits $commitrow($curview,$id)
+    $canv delete tag.$id
+    set xt [eval drawtags $id $idpos($id)]
+    $canv coords $linehtag($commitrow($curview,$id)) $xt [lindex $idpos($id) 2]
+    set text [$canv itemcget $linehtag($commitrow($curview,$id)) -text]
+    set xr [expr {$xt + [font measure mainfont $text]}]
+    if {$xr > $canvxmax} {
+       set canvxmax $xr
+       setcanvscroll
+    }
+    if {[info exists selectedline]
+       && $selectedline == $commitrow($curview,$id)} {
+       selectline $selectedline 0
+    }
+}
+
+proc mktagcan {} {
+    global mktagtop
+
+    catch {destroy $mktagtop}
+    unset mktagtop
+}
+
+proc mktaggo {} {
+    domktag
+    mktagcan
+}
+
+proc writecommit {} {
+    global rowmenuid wrcomtop commitinfo wrcomcmd
+
+    set top .writecommit
+    set wrcomtop $top
+    catch {destroy $top}
+    toplevel $top
+    label $top.title -text "Write commit to file"
+    grid $top.title - -pady 10
+    label $top.id -text "ID:"
+    entry $top.sha1 -width 40 -relief flat
+    $top.sha1 insert 0 $rowmenuid
+    $top.sha1 conf -state readonly
+    grid $top.id $top.sha1 -sticky w
+    entry $top.head -width 60 -relief flat
+    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
+    $top.head conf -state readonly
+    grid x $top.head -sticky w
+    label $top.clab -text "Command:"
+    entry $top.cmd -width 60 -textvariable wrcomcmd
+    grid $top.clab $top.cmd -sticky w -pady 10
+    label $top.flab -text "Output file:"
+    entry $top.fname -width 60
+    $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
+    grid $top.flab $top.fname -sticky w
+    frame $top.buts
+    button $top.buts.gen -text "Write" -command wrcomgo
+    button $top.buts.can -text "Cancel" -command wrcomcan
+    grid $top.buts.gen $top.buts.can
+    grid columnconfigure $top.buts 0 -weight 1 -uniform a
+    grid columnconfigure $top.buts 1 -weight 1 -uniform a
+    grid $top.buts - -pady 10 -sticky ew
+    focus $top.fname
+}
+
+proc wrcomgo {} {
+    global wrcomtop
+
+    set id [$wrcomtop.sha1 get]
+    set cmd "echo $id | [$wrcomtop.cmd get]"
+    set fname [$wrcomtop.fname get]
+    if {[catch {exec sh -c $cmd >$fname &} err]} {
+       error_popup "Error writing commit: $err"
+    }
+    catch {destroy $wrcomtop}
+    unset wrcomtop
+}
+
+proc wrcomcan {} {
+    global wrcomtop
+
+    catch {destroy $wrcomtop}
+    unset wrcomtop
+}
+
+proc mkbranch {} {
+    global rowmenuid mkbrtop
+
+    set top .makebranch
+    catch {destroy $top}
+    toplevel $top
+    label $top.title -text "Create new branch"
+    grid $top.title - -pady 10
+    label $top.id -text "ID:"
+    entry $top.sha1 -width 40 -relief flat
+    $top.sha1 insert 0 $rowmenuid
+    $top.sha1 conf -state readonly
+    grid $top.id $top.sha1 -sticky w
+    label $top.nlab -text "Name:"
+    entry $top.name -width 40
+    grid $top.nlab $top.name -sticky w
+    frame $top.buts
+    button $top.buts.go -text "Create" -command [list mkbrgo $top]
+    button $top.buts.can -text "Cancel" -command "catch {destroy $top}"
+    grid $top.buts.go $top.buts.can
+    grid columnconfigure $top.buts 0 -weight 1 -uniform a
+    grid columnconfigure $top.buts 1 -weight 1 -uniform a
+    grid $top.buts - -pady 10 -sticky ew
+    focus $top.name
+}
+
+proc mkbrgo {top} {
+    global headids idheads
+
+    set name [$top.name get]
+    set id [$top.sha1 get]
+    if {$name eq {}} {
+       error_popup "Please specify a name for the new branch"
+       return
+    }
+    catch {destroy $top}
+    nowbusy newbranch
+    update
+    if {[catch {
+       exec git branch $name $id
+    } err]} {
+       notbusy newbranch
+       error_popup $err
+    } else {
+       set headids($name) $id
+       lappend idheads($id) $name
+       addedhead $id $name
+       notbusy newbranch
+       redrawtags $id
+       dispneartags 0
+       run refill_reflist
+    }
+}
+
+proc cherrypick {} {
+    global rowmenuid curview commitrow
+    global mainhead
+
+    set oldhead [exec git rev-parse HEAD]
+    set dheads [descheads $rowmenuid]
+    if {$dheads ne {} && [lsearch -exact $dheads $oldhead] >= 0} {
+       set ok [confirm_popup "Commit [string range $rowmenuid 0 7] is already\
+                       included in branch $mainhead -- really re-apply it?"]
+       if {!$ok} return
+    }
+    nowbusy cherrypick "Cherry-picking"
+    update
+    # Unfortunately git-cherry-pick writes stuff to stderr even when
+    # no error occurs, and exec takes that as an indication of error...
+    if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} {
+       notbusy cherrypick
+       error_popup $err
+       return
+    }
+    set newhead [exec git rev-parse HEAD]
+    if {$newhead eq $oldhead} {
+       notbusy cherrypick
+       error_popup "No changes committed"
+       return
+    }
+    addnewchild $newhead $oldhead
+    if {[info exists commitrow($curview,$oldhead)]} {
+       insertrow $commitrow($curview,$oldhead) $newhead
+       if {$mainhead ne {}} {
+           movehead $newhead $mainhead
+           movedhead $newhead $mainhead
+       }
+       redrawtags $oldhead
+       redrawtags $newhead
+    }
+    notbusy cherrypick
+}
+
+proc resethead {} {
+    global mainheadid mainhead rowmenuid confirm_ok resettype
+
+    set confirm_ok 0
+    set w ".confirmreset"
+    toplevel $w
+    wm transient $w .
+    wm title $w "Confirm reset"
+    message $w.m -text \
+       "Reset branch $mainhead to [string range $rowmenuid 0 7]?" \
+       -justify center -aspect 1000
+    pack $w.m -side top -fill x -padx 20 -pady 20
+    frame $w.f -relief sunken -border 2
+    message $w.f.rt -text "Reset type:" -aspect 1000
+    grid $w.f.rt -sticky w
+    set resettype mixed
+    radiobutton $w.f.soft -value soft -variable resettype -justify left \
+       -text "Soft: Leave working tree and index untouched"
+    grid $w.f.soft -sticky w
+    radiobutton $w.f.mixed -value mixed -variable resettype -justify left \
+       -text "Mixed: Leave working tree untouched, reset index"
+    grid $w.f.mixed -sticky w
+    radiobutton $w.f.hard -value hard -variable resettype -justify left \
+       -text "Hard: Reset working tree and index\n(discard ALL local changes)"
+    grid $w.f.hard -sticky w
+    pack $w.f -side top -fill x
+    button $w.ok -text OK -command "set confirm_ok 1; destroy $w"
+    pack $w.ok -side left -fill x -padx 20 -pady 20
+    button $w.cancel -text Cancel -command "destroy $w"
+    pack $w.cancel -side right -fill x -padx 20 -pady 20
+    bind $w <Visibility> "grab $w; focus $w"
+    tkwait window $w
+    if {!$confirm_ok} return
+    if {[catch {set fd [open \
+           [list | sh -c "git reset --$resettype $rowmenuid 2>&1"] r]} err]} {
+       error_popup $err
+    } else {
+       dohidelocalchanges
+       filerun $fd [list readresetstat $fd]
+       nowbusy reset "Resetting"
+    }
+}
+
+proc readresetstat {fd} {
+    global mainhead mainheadid showlocalchanges rprogcoord
+
+    if {[gets $fd line] >= 0} {
+       if {[regexp {([0-9]+)% \(([0-9]+)/([0-9]+)\)} $line match p m n]} {
+           set rprogcoord [expr {1.0 * $m / $n}]
+           adjustprogress
+       }
+       return 1
+    }
+    set rprogcoord 0
+    adjustprogress
+    notbusy reset
+    if {[catch {close $fd} err]} {
+       error_popup $err
+    }
+    set oldhead $mainheadid
+    set newhead [exec git rev-parse HEAD]
+    if {$newhead ne $oldhead} {
+       movehead $newhead $mainhead
+       movedhead $newhead $mainhead
+       set mainheadid $newhead
+       redrawtags $oldhead
+       redrawtags $newhead
+    }
+    if {$showlocalchanges} {
+       doshowlocalchanges
+    }
+    return 0
+}
+
+# context menu for a head
+proc headmenu {x y id head} {
+    global headmenuid headmenuhead headctxmenu mainhead
+
+    stopfinding
+    set headmenuid $id
+    set headmenuhead $head
+    set state normal
+    if {$head eq $mainhead} {
+       set state disabled
+    }
+    $headctxmenu entryconfigure 0 -state $state
+    $headctxmenu entryconfigure 1 -state $state
+    tk_popup $headctxmenu $x $y
+}
+
+proc cobranch {} {
+    global headmenuid headmenuhead mainhead headids
+    global showlocalchanges mainheadid
+
+    # check the tree is clean first??
+    set oldmainhead $mainhead
+    nowbusy checkout "Checking out"
+    update
+    dohidelocalchanges
+    if {[catch {
+       exec git checkout -q $headmenuhead
+    } err]} {
+       notbusy checkout
+       error_popup $err
+    } else {
+       notbusy checkout
+       set mainhead $headmenuhead
+       set mainheadid $headmenuid
+       if {[info exists headids($oldmainhead)]} {
+           redrawtags $headids($oldmainhead)
+       }
+       redrawtags $headmenuid
+    }
+    if {$showlocalchanges} {
+       dodiffindex
+    }
+}
+
+proc rmbranch {} {
+    global headmenuid headmenuhead mainhead
+    global idheads
+
+    set head $headmenuhead
+    set id $headmenuid
+    # this check shouldn't be needed any more...
+    if {$head eq $mainhead} {
+       error_popup "Cannot delete the currently checked-out branch"
+       return
+    }
+    set dheads [descheads $id]
+    if {[llength $dheads] == 1 && $idheads($dheads) eq $head} {
+       # the stuff on this branch isn't on any other branch
+       if {![confirm_popup "The commits on branch $head aren't on any other\
+                       branch.\nReally delete branch $head?"]} return
+    }
+    nowbusy rmbranch
+    update
+    if {[catch {exec git branch -D $head} err]} {
+       notbusy rmbranch
+       error_popup $err
+       return
+    }
+    removehead $id $head
+    removedhead $id $head
+    redrawtags $id
+    notbusy rmbranch
+    dispneartags 0
+    run refill_reflist
+}
+
+# Display a list of tags and heads
+proc showrefs {} {
+    global showrefstop bgcolor fgcolor selectbgcolor
+    global bglist fglist reflistfilter reflist maincursor
+
+    set top .showrefs
+    set showrefstop $top
+    if {[winfo exists $top]} {
+       raise $top
+       refill_reflist
+       return
+    }
+    toplevel $top
+    wm title $top "Tags and heads: [file tail [pwd]]"
+    text $top.list -background $bgcolor -foreground $fgcolor \
+       -selectbackground $selectbgcolor -font mainfont \
+       -xscrollcommand "$top.xsb set" -yscrollcommand "$top.ysb set" \
+       -width 30 -height 20 -cursor $maincursor \
+       -spacing1 1 -spacing3 1 -state disabled
+    $top.list tag configure highlight -background $selectbgcolor
+    lappend bglist $top.list
+    lappend fglist $top.list
+    scrollbar $top.ysb -command "$top.list yview" -orient vertical
+    scrollbar $top.xsb -command "$top.list xview" -orient horizontal
+    grid $top.list $top.ysb -sticky nsew
+    grid $top.xsb x -sticky ew
+    frame $top.f
+    label $top.f.l -text "Filter: " -font uifont
+    entry $top.f.e -width 20 -textvariable reflistfilter -font uifont
+    set reflistfilter "*"
+    trace add variable reflistfilter write reflistfilter_change
+    pack $top.f.e -side right -fill x -expand 1
+    pack $top.f.l -side left
+    grid $top.f - -sticky ew -pady 2
+    button $top.close -command [list destroy $top] -text "Close" \
+       -font uifont
+    grid $top.close -
+    grid columnconfigure $top 0 -weight 1
+    grid rowconfigure $top 0 -weight 1
+    bind $top.list <1> {break}
+    bind $top.list <B1-Motion> {break}
+    bind $top.list <ButtonRelease-1> {sel_reflist %W %x %y; break}
+    set reflist {}
+    refill_reflist
+}
+
+proc sel_reflist {w x y} {
+    global showrefstop reflist headids tagids otherrefids
+
+    if {![winfo exists $showrefstop]} return
+    set l [lindex [split [$w index "@$x,$y"] "."] 0]
+    set ref [lindex $reflist [expr {$l-1}]]
+    set n [lindex $ref 0]
+    switch -- [lindex $ref 1] {
+       "H" {selbyid $headids($n)}
+       "T" {selbyid $tagids($n)}
+       "o" {selbyid $otherrefids($n)}
+    }
+    $showrefstop.list tag add highlight $l.0 "$l.0 lineend"
+}
+
+proc unsel_reflist {} {
+    global showrefstop
+
+    if {![info exists showrefstop] || ![winfo exists $showrefstop]} return
+    $showrefstop.list tag remove highlight 0.0 end
+}
+
+proc reflistfilter_change {n1 n2 op} {
+    global reflistfilter
+
+    after cancel refill_reflist
+    after 200 refill_reflist
+}
+
+proc refill_reflist {} {
+    global reflist reflistfilter showrefstop headids tagids otherrefids
+    global commitrow curview commitinterest
+
+    if {![info exists showrefstop] || ![winfo exists $showrefstop]} return
+    set refs {}
+    foreach n [array names headids] {
+       if {[string match $reflistfilter $n]} {
+           if {[info exists commitrow($curview,$headids($n))]} {
+               lappend refs [list $n H]
+           } else {
+               set commitinterest($headids($n)) {run refill_reflist}
+           }
+       }
+    }
+    foreach n [array names tagids] {
+       if {[string match $reflistfilter $n]} {
+           if {[info exists commitrow($curview,$tagids($n))]} {
+               lappend refs [list $n T]
+           } else {
+               set commitinterest($tagids($n)) {run refill_reflist}
+           }
+       }
+    }
+    foreach n [array names otherrefids] {
+       if {[string match $reflistfilter $n]} {
+           if {[info exists commitrow($curview,$otherrefids($n))]} {
+               lappend refs [list $n o]
+           } else {
+               set commitinterest($otherrefids($n)) {run refill_reflist}
+           }
+       }
+    }
+    set refs [lsort -index 0 $refs]
+    if {$refs eq $reflist} return
+
+    # Update the contents of $showrefstop.list according to the
+    # differences between $reflist (old) and $refs (new)
+    $showrefstop.list conf -state normal
+    $showrefstop.list insert end "\n"
+    set i 0
+    set j 0
+    while {$i < [llength $reflist] || $j < [llength $refs]} {
+       if {$i < [llength $reflist]} {
+           if {$j < [llength $refs]} {
+               set cmp [string compare [lindex $reflist $i 0] \
+                            [lindex $refs $j 0]]
+               if {$cmp == 0} {
+                   set cmp [string compare [lindex $reflist $i 1] \
+                                [lindex $refs $j 1]]
+               }
+           } else {
+               set cmp -1
+           }
+       } else {
+           set cmp 1
+       }
+       switch -- $cmp {
+           -1 {
+               $showrefstop.list delete "[expr {$j+1}].0" "[expr {$j+2}].0"
+               incr i
+           }
+           0 {
+               incr i
+               incr j
+           }
+           1 {
+               set l [expr {$j + 1}]
+               $showrefstop.list image create $l.0 -align baseline \
+                   -image reficon-[lindex $refs $j 1] -padx 2
+               $showrefstop.list insert $l.1 "[lindex $refs $j 0]\n"
+               incr j
+           }
+       }
+    }
+    set reflist $refs
+    # delete last newline
+    $showrefstop.list delete end-2c end-1c
+    $showrefstop.list conf -state disabled
+}
+
+# Stuff for finding nearby tags
+proc getallcommits {} {
+    global allcommits nextarc seeds allccache allcwait cachedarcs allcupdate
+    global idheads idtags idotherrefs allparents tagobjid
+
+    if {![info exists allcommits]} {
+       set nextarc 0
+       set allcommits 0
+       set seeds {}
+       set allcwait 0
+       set cachedarcs 0
+       set allccache [file join [gitdir] "gitk.cache"]
+       if {![catch {
+           set f [open $allccache r]
+           set allcwait 1
+           getcache $f
+       }]} return
+    }
+
+    if {$allcwait} {
+       return
+    }
+    set cmd [list | git rev-list --parents]
+    set allcupdate [expr {$seeds ne {}}]
+    if {!$allcupdate} {
+       set ids "--all"
+    } else {
+       set refs [concat [array names idheads] [array names idtags] \
+                     [array names idotherrefs]]
+       set ids {}
+       set tagobjs {}
+       foreach name [array names tagobjid] {
+           lappend tagobjs $tagobjid($name)
+       }
+       foreach id [lsort -unique $refs] {
+           if {![info exists allparents($id)] &&
+               [lsearch -exact $tagobjs $id] < 0} {
+               lappend ids $id
+           }
+       }
+       if {$ids ne {}} {
+           foreach id $seeds {
+               lappend ids "^$id"
+           }
+       }
+    }
+    if {$ids ne {}} {
+       set fd [open [concat $cmd $ids] r]
+       fconfigure $fd -blocking 0
+       incr allcommits
+       nowbusy allcommits
+       filerun $fd [list getallclines $fd]
+    } else {
+       dispneartags 0
+    }
+}
+
+# Since most commits have 1 parent and 1 child, we group strings of
+# such commits into "arcs" joining branch/merge points (BMPs), which
+# are commits that either don't have 1 parent or don't have 1 child.
+#
+# arcnos(id) - incoming arcs for BMP, arc we're on for other nodes
+# arcout(id) - outgoing arcs for BMP
+# arcids(a) - list of IDs on arc including end but not start
+# arcstart(a) - BMP ID at start of arc
+# arcend(a) - BMP ID at end of arc
+# growing(a) - arc a is still growing
+# arctags(a) - IDs out of arcids (excluding end) that have tags
+# archeads(a) - IDs out of arcids (excluding end) that have heads
+# The start of an arc is at the descendent end, so "incoming" means
+# coming from descendents, and "outgoing" means going towards ancestors.
+
+proc getallclines {fd} {
+    global allparents allchildren idtags idheads nextarc
+    global arcnos arcids arctags arcout arcend arcstart archeads growing
+    global seeds allcommits cachedarcs allcupdate
+    
+    set nid 0
+    while {[incr nid] <= 1000 && [gets $fd line] >= 0} {
+       set id [lindex $line 0]
+       if {[info exists allparents($id)]} {
+           # seen it already
+           continue
+       }
+       set cachedarcs 0
+       set olds [lrange $line 1 end]
+       set allparents($id) $olds
+       if {![info exists allchildren($id)]} {
+           set allchildren($id) {}
+           set arcnos($id) {}
+           lappend seeds $id
+       } else {
+           set a $arcnos($id)
+           if {[llength $olds] == 1 && [llength $a] == 1} {
+               lappend arcids($a) $id
+               if {[info exists idtags($id)]} {
+                   lappend arctags($a) $id
+               }
+               if {[info exists idheads($id)]} {
+                   lappend archeads($a) $id
+               }
+               if {[info exists allparents($olds)]} {
+                   # seen parent already
+                   if {![info exists arcout($olds)]} {
+                       splitarc $olds
+                   }
+                   lappend arcids($a) $olds
+                   set arcend($a) $olds
+                   unset growing($a)
+               }
+               lappend allchildren($olds) $id
+               lappend arcnos($olds) $a
+               continue
+           }
+       }
+       foreach a $arcnos($id) {
+           lappend arcids($a) $id
+           set arcend($a) $id
+           unset growing($a)
+       }
+
+       set ao {}
+       foreach p $olds {
+           lappend allchildren($p) $id
+           set a [incr nextarc]
+           set arcstart($a) $id
+           set archeads($a) {}
+           set arctags($a) {}
+           set archeads($a) {}
+           set arcids($a) {}
+           lappend ao $a
+           set growing($a) 1
+           if {[info exists allparents($p)]} {
+               # seen it already, may need to make a new branch
+               if {![info exists arcout($p)]} {
+                   splitarc $p
+               }
+               lappend arcids($a) $p
+               set arcend($a) $p
+               unset growing($a)
+           }
+           lappend arcnos($p) $a
+       }
+       set arcout($id) $ao
+    }
+    if {$nid > 0} {
+       global cached_dheads cached_dtags cached_atags
+       catch {unset cached_dheads}
+       catch {unset cached_dtags}
+       catch {unset cached_atags}
+    }
+    if {![eof $fd]} {
+       return [expr {$nid >= 1000? 2: 1}]
+    }
+    set cacheok 1
+    if {[catch {
+       fconfigure $fd -blocking 1
+       close $fd
+    } err]} {
+       # got an error reading the list of commits
+       # if we were updating, try rereading the whole thing again
+       if {$allcupdate} {
+           incr allcommits -1
+           dropcache $err
+           return
+       }
+       error_popup "Error reading commit topology information;\
+               branch and preceding/following tag information\
+               will be incomplete.\n($err)"
+       set cacheok 0
+    }
+    if {[incr allcommits -1] == 0} {
+       notbusy allcommits
+       if {$cacheok} {
+           run savecache
+       }
+    }
+    dispneartags 0
+    return 0
+}
+
+proc recalcarc {a} {
+    global arctags archeads arcids idtags idheads
+
+    set at {}
+    set ah {}
+    foreach id [lrange $arcids($a) 0 end-1] {
+       if {[info exists idtags($id)]} {
+           lappend at $id
+       }
+       if {[info exists idheads($id)]} {
+           lappend ah $id
+       }
+    }
+    set arctags($a) $at
+    set archeads($a) $ah
+}
+
+proc splitarc {p} {
+    global arcnos arcids nextarc arctags archeads idtags idheads
+    global arcstart arcend arcout allparents growing
+
+    set a $arcnos($p)
+    if {[llength $a] != 1} {
+       puts "oops splitarc called but [llength $a] arcs already"
+       return
+    }
+    set a [lindex $a 0]
+    set i [lsearch -exact $arcids($a) $p]
+    if {$i < 0} {
+       puts "oops splitarc $p not in arc $a"
+       return
+    }
+    set na [incr nextarc]
+    if {[info exists arcend($a)]} {
+       set arcend($na) $arcend($a)
+    } else {
+       set l [lindex $allparents([lindex $arcids($a) end]) 0]
+       set j [lsearch -exact $arcnos($l) $a]
+       set arcnos($l) [lreplace $arcnos($l) $j $j $na]
+    }
+    set tail [lrange $arcids($a) [expr {$i+1}] end]
+    set arcids($a) [lrange $arcids($a) 0 $i]
+    set arcend($a) $p
+    set arcstart($na) $p
+    set arcout($p) $na
+    set arcids($na) $tail
+    if {[info exists growing($a)]} {
+       set growing($na) 1
+       unset growing($a)
+    }
+
+    foreach id $tail {
+       if {[llength $arcnos($id)] == 1} {
+           set arcnos($id) $na
+       } else {
+           set j [lsearch -exact $arcnos($id) $a]
+           set arcnos($id) [lreplace $arcnos($id) $j $j $na]
+       }
+    }
+
+    # reconstruct tags and heads lists
+    if {$arctags($a) ne {} || $archeads($a) ne {}} {
+       recalcarc $a
+       recalcarc $na
+    } else {
+       set arctags($na) {}
+       set archeads($na) {}
+    }
+}
+
+# Update things for a new commit added that is a child of one
+# existing commit.  Used when cherry-picking.
+proc addnewchild {id p} {
+    global allparents allchildren idtags nextarc
+    global arcnos arcids arctags arcout arcend arcstart archeads growing
+    global seeds allcommits
+
+    if {![info exists allcommits] || ![info exists arcnos($p)]} return
+    set allparents($id) [list $p]
+    set allchildren($id) {}
+    set arcnos($id) {}
+    lappend seeds $id
+    lappend allchildren($p) $id
+    set a [incr nextarc]
+    set arcstart($a) $id
+    set archeads($a) {}
+    set arctags($a) {}
+    set arcids($a) [list $p]
+    set arcend($a) $p
+    if {![info exists arcout($p)]} {
+       splitarc $p
+    }
+    lappend arcnos($p) $a
+    set arcout($id) [list $a]
+}
+
+# This implements a cache for the topology information.
+# The cache saves, for each arc, the start and end of the arc,
+# the ids on the arc, and the outgoing arcs from the end.
+proc readcache {f} {
+    global arcnos arcids arcout arcstart arcend arctags archeads nextarc
+    global idtags idheads allparents cachedarcs possible_seeds seeds growing
+    global allcwait
+
+    set a $nextarc
+    set lim $cachedarcs
+    if {$lim - $a > 500} {
+       set lim [expr {$a + 500}]
+    }
+    if {[catch {
+       if {$a == $lim} {
+           # finish reading the cache and setting up arctags, etc.
+           set line [gets $f]
+           if {$line ne "1"} {error "bad final version"}
+           close $f
+           foreach id [array names idtags] {
+               if {[info exists arcnos($id)] && [llength $arcnos($id)] == 1 &&
+                   [llength $allparents($id)] == 1} {
+                   set a [lindex $arcnos($id) 0]
+                   if {$arctags($a) eq {}} {
+                       recalcarc $a
+                   }
+               }
+           }
+           foreach id [array names idheads] {
+               if {[info exists arcnos($id)] && [llength $arcnos($id)] == 1 &&
+                   [llength $allparents($id)] == 1} {
+                   set a [lindex $arcnos($id) 0]
+                   if {$archeads($a) eq {}} {
+                       recalcarc $a
+                   }
+               }
+           }
+           foreach id [lsort -unique $possible_seeds] {
+               if {$arcnos($id) eq {}} {
+                   lappend seeds $id
+               }
+           }
+           set allcwait 0
+       } else {
+           while {[incr a] <= $lim} {
+               set line [gets $f]
+               if {[llength $line] != 3} {error "bad line"}
+               set s [lindex $line 0]
+               set arcstart($a) $s
+               lappend arcout($s) $a
+               if {![info exists arcnos($s)]} {
+                   lappend possible_seeds $s
+                   set arcnos($s) {}
+               }
+               set e [lindex $line 1]
+               if {$e eq {}} {
+                   set growing($a) 1
+               } else {
+                   set arcend($a) $e
+                   if {![info exists arcout($e)]} {
+                       set arcout($e) {}
+                   }
+               }
+               set arcids($a) [lindex $line 2]
+               foreach id $arcids($a) {
+                   lappend allparents($s) $id
+                   set s $id
+                   lappend arcnos($id) $a
+               }
+               if {![info exists allparents($s)]} {
+                   set allparents($s) {}
+               }
+               set arctags($a) {}
+               set archeads($a) {}
+           }
+           set nextarc [expr {$a - 1}]
+       }
+    } err]} {
+       dropcache $err
+       return 0
+    }
+    if {!$allcwait} {
+       getallcommits
+    }
+    return $allcwait
+}
+
+proc getcache {f} {
+    global nextarc cachedarcs possible_seeds
+
+    if {[catch {
+       set line [gets $f]
+       if {[llength $line] != 2 || [lindex $line 0] ne "1"} {error "bad version"}
+       # make sure it's an integer
+       set cachedarcs [expr {int([lindex $line 1])}]
+       if {$cachedarcs < 0} {error "bad number of arcs"}
+       set nextarc 0
+       set possible_seeds {}
+       run readcache $f
+    } err]} {
+       dropcache $err
+    }
+    return 0
+}
+
+proc dropcache {err} {
+    global allcwait nextarc cachedarcs seeds
+
+    #puts "dropping cache ($err)"
+    foreach v {arcnos arcout arcids arcstart arcend growing \
+                  arctags archeads allparents allchildren} {
+       global $v
+       catch {unset $v}
+    }
+    set allcwait 0
+    set nextarc 0
+    set cachedarcs 0
+    set seeds {}
+    getallcommits
+}
+
+proc writecache {f} {
+    global cachearc cachedarcs allccache
+    global arcstart arcend arcnos arcids arcout
+
+    set a $cachearc
+    set lim $cachedarcs
+    if {$lim - $a > 1000} {
+       set lim [expr {$a + 1000}]
+    }
+    if {[catch {
+       while {[incr a] <= $lim} {
+           if {[info exists arcend($a)]} {
+               puts $f [list $arcstart($a) $arcend($a) $arcids($a)]
+           } else {
+               puts $f [list $arcstart($a) {} $arcids($a)]
+           }
+       }
+    } err]} {
+       catch {close $f}
+       catch {file delete $allccache}
+       #puts "writing cache failed ($err)"
+       return 0
+    }
+    set cachearc [expr {$a - 1}]
+    if {$a > $cachedarcs} {
+       puts $f "1"
+       close $f
+       return 0
+    }
+    return 1
+}
+
+proc savecache {} {
+    global nextarc cachedarcs cachearc allccache
+
+    if {$nextarc == $cachedarcs} return
+    set cachearc 0
+    set cachedarcs $nextarc
+    catch {
+       set f [open $allccache w]
+       puts $f [list 1 $cachedarcs]
+       run writecache $f
+    }
+}
+
+# Returns 1 if a is an ancestor of b, -1 if b is an ancestor of a,
+# or 0 if neither is true.
+proc anc_or_desc {a b} {
+    global arcout arcstart arcend arcnos cached_isanc
+
+    if {$arcnos($a) eq $arcnos($b)} {
+       # Both are on the same arc(s); either both are the same BMP,
+       # or if one is not a BMP, the other is also not a BMP or is
+       # the BMP at end of the arc (and it only has 1 incoming arc).
+       # Or both can be BMPs with no incoming arcs.
+       if {$a eq $b || $arcnos($a) eq {}} {
+           return 0
+       }
+       # assert {[llength $arcnos($a)] == 1}
+       set arc [lindex $arcnos($a) 0]
+       set i [lsearch -exact $arcids($arc) $a]
+       set j [lsearch -exact $arcids($arc) $b]
+       if {$i < 0 || $i > $j} {
+           return 1
+       } else {
+           return -1
+       }
+    }
+
+    if {![info exists arcout($a)]} {
+       set arc [lindex $arcnos($a) 0]
+       if {[info exists arcend($arc)]} {
+           set aend $arcend($arc)
+       } else {
+           set aend {}
+       }
+       set a $arcstart($arc)
+    } else {
+       set aend $a
+    }
+    if {![info exists arcout($b)]} {
+       set arc [lindex $arcnos($b) 0]
+       if {[info exists arcend($arc)]} {
+           set bend $arcend($arc)
+       } else {
+           set bend {}
+       }
+       set b $arcstart($arc)
+    } else {
+       set bend $b
+    }
+    if {$a eq $bend} {
+       return 1
+    }
+    if {$b eq $aend} {
+       return -1
+    }
+    if {[info exists cached_isanc($a,$bend)]} {
+       if {$cached_isanc($a,$bend)} {
+           return 1
+       }
+    }
+    if {[info exists cached_isanc($b,$aend)]} {
+       if {$cached_isanc($b,$aend)} {
+           return -1
+       }
+       if {[info exists cached_isanc($a,$bend)]} {
+           return 0
+       }
+    }
+
+    set todo [list $a $b]
+    set anc($a) a
+    set anc($b) b
+    for {set i 0} {$i < [llength $todo]} {incr i} {
+       set x [lindex $todo $i]
+       if {$anc($x) eq {}} {
+           continue
+       }
+       foreach arc $arcnos($x) {
+           set xd $arcstart($arc)
+           if {$xd eq $bend} {
+               set cached_isanc($a,$bend) 1
+               set cached_isanc($b,$aend) 0
+               return 1
+           } elseif {$xd eq $aend} {
+               set cached_isanc($b,$aend) 1
+               set cached_isanc($a,$bend) 0
+               return -1
+           }
+           if {![info exists anc($xd)]} {
+               set anc($xd) $anc($x)
+               lappend todo $xd
+           } elseif {$anc($xd) ne $anc($x)} {
+               set anc($xd) {}
+           }
+       }
+    }
+    set cached_isanc($a,$bend) 0
+    set cached_isanc($b,$aend) 0
+    return 0
+}
+
+# This identifies whether $desc has an ancestor that is
+# a growing tip of the graph and which is not an ancestor of $anc
+# and returns 0 if so and 1 if not.
+# If we subsequently discover a tag on such a growing tip, and that
+# turns out to be a descendent of $anc (which it could, since we
+# don't necessarily see children before parents), then $desc
+# isn't a good choice to display as a descendent tag of
+# $anc (since it is the descendent of another tag which is
+# a descendent of $anc).  Similarly, $anc isn't a good choice to
+# display as a ancestor tag of $desc.
+#
+proc is_certain {desc anc} {
+    global arcnos arcout arcstart arcend growing problems
+
+    set certain {}
+    if {[llength $arcnos($anc)] == 1} {
+       # tags on the same arc are certain
+       if {$arcnos($desc) eq $arcnos($anc)} {
+           return 1
+       }
+       if {![info exists arcout($anc)]} {
+           # if $anc is partway along an arc, use the start of the arc instead
+           set a [lindex $arcnos($anc) 0]
+           set anc $arcstart($a)
+       }
+    }
+    if {[llength $arcnos($desc)] > 1 || [info exists arcout($desc)]} {
+       set x $desc
+    } else {
+       set a [lindex $arcnos($desc) 0]
+       set x $arcend($a)
+    }
+    if {$x == $anc} {
+       return 1
+    }
+    set anclist [list $x]
+    set dl($x) 1
+    set nnh 1
+    set ngrowanc 0
+    for {set i 0} {$i < [llength $anclist] && ($nnh > 0 || $ngrowanc > 0)} {incr i} {
+       set x [lindex $anclist $i]
+       if {$dl($x)} {
+           incr nnh -1
+       }
+       set done($x) 1
+       foreach a $arcout($x) {
+           if {[info exists growing($a)]} {
+               if {![info exists growanc($x)] && $dl($x)} {
+                   set growanc($x) 1
+                   incr ngrowanc
+               }
+           } else {
+               set y $arcend($a)
+               if {[info exists dl($y)]} {
+                   if {$dl($y)} {
+                       if {!$dl($x)} {
+                           set dl($y) 0
+                           if {![info exists done($y)]} {
+                               incr nnh -1
+                           }
+                           if {[info exists growanc($x)]} {
+                               incr ngrowanc -1
+                           }
+                           set xl [list $y]
+                           for {set k 0} {$k < [llength $xl]} {incr k} {
+                               set z [lindex $xl $k]
+                               foreach c $arcout($z) {
+                                   if {[info exists arcend($c)]} {
+                                       set v $arcend($c)
+                                       if {[info exists dl($v)] && $dl($v)} {
+                                           set dl($v) 0
+                                           if {![info exists done($v)]} {
+                                               incr nnh -1
+                                           }
+                                           if {[info exists growanc($v)]} {
+                                               incr ngrowanc -1
+                                           }
+                                           lappend xl $v
+                                       }
+                                   }
+                               }
+                           }
+                       }
+                   }
+               } elseif {$y eq $anc || !$dl($x)} {
+                   set dl($y) 0
+                   lappend anclist $y
+               } else {
+                   set dl($y) 1
+                   lappend anclist $y
+                   incr nnh
+               }
+           }
+       }
+    }
+    foreach x [array names growanc] {
+       if {$dl($x)} {
+           return 0
+       }
+       return 0
+    }
+    return 1
+}
+
+proc validate_arctags {a} {
+    global arctags idtags
+
+    set i -1
+    set na $arctags($a)
+    foreach id $arctags($a) {
+       incr i
+       if {![info exists idtags($id)]} {
+           set na [lreplace $na $i $i]
+           incr i -1
+       }
+    }
+    set arctags($a) $na
+}
+
+proc validate_archeads {a} {
+    global archeads idheads
+
+    set i -1
+    set na $archeads($a)
+    foreach id $archeads($a) {
+       incr i
+       if {![info exists idheads($id)]} {
+           set na [lreplace $na $i $i]
+           incr i -1
+       }
+    }
+    set archeads($a) $na
+}
+
+# Return the list of IDs that have tags that are descendents of id,
+# ignoring IDs that are descendents of IDs already reported.
+proc desctags {id} {
+    global arcnos arcstart arcids arctags idtags allparents
+    global growing cached_dtags
+
+    if {![info exists allparents($id)]} {
+       return {}
+    }
+    set t1 [clock clicks -milliseconds]
+    set argid $id
+    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
+       # part-way along an arc; check that arc first
+       set a [lindex $arcnos($id) 0]
+       if {$arctags($a) ne {}} {
+           validate_arctags $a
+           set i [lsearch -exact $arcids($a) $id]
+           set tid {}
+           foreach t $arctags($a) {
+               set j [lsearch -exact $arcids($a) $t]
+               if {$j >= $i} break
+               set tid $t
+           }
+           if {$tid ne {}} {
+               return $tid
+           }
+       }
+       set id $arcstart($a)
+       if {[info exists idtags($id)]} {
+           return $id
+       }
+    }
+    if {[info exists cached_dtags($id)]} {
+       return $cached_dtags($id)
+    }
+
+    set origid $id
+    set todo [list $id]
+    set queued($id) 1
+    set nc 1
+    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
+       set id [lindex $todo $i]
+       set done($id) 1
+       set ta [info exists hastaggedancestor($id)]
+       if {!$ta} {
+           incr nc -1
+       }
+       # ignore tags on starting node
+       if {!$ta && $i > 0} {
+           if {[info exists idtags($id)]} {
+               set tagloc($id) $id
+               set ta 1
+           } elseif {[info exists cached_dtags($id)]} {
+               set tagloc($id) $cached_dtags($id)
+               set ta 1
+           }
+       }
+       foreach a $arcnos($id) {
+           set d $arcstart($a)
+           if {!$ta && $arctags($a) ne {}} {
+               validate_arctags $a
+               if {$arctags($a) ne {}} {
+                   lappend tagloc($id) [lindex $arctags($a) end]
+               }
+           }
+           if {$ta || $arctags($a) ne {}} {
+               set tomark [list $d]
+               for {set j 0} {$j < [llength $tomark]} {incr j} {
+                   set dd [lindex $tomark $j]
+                   if {![info exists hastaggedancestor($dd)]} {
+                       if {[info exists done($dd)]} {
+                           foreach b $arcnos($dd) {
+                               lappend tomark $arcstart($b)
+                           }
+                           if {[info exists tagloc($dd)]} {
+                               unset tagloc($dd)
+                           }
+                       } elseif {[info exists queued($dd)]} {
+                           incr nc -1
+                       }
+                       set hastaggedancestor($dd) 1
+                   }
+               }
+           }
+           if {![info exists queued($d)]} {
+               lappend todo $d
+               set queued($d) 1
+               if {![info exists hastaggedancestor($d)]} {
+                   incr nc
+               }
+           }
+       }
+    }
+    set tags {}
+    foreach id [array names tagloc] {
+       if {![info exists hastaggedancestor($id)]} {
+           foreach t $tagloc($id) {
+               if {[lsearch -exact $tags $t] < 0} {
+                   lappend tags $t
+               }
+           }
+       }
+    }
+    set t2 [clock clicks -milliseconds]
+    set loopix $i
+
+    # remove tags that are descendents of other tags
+    for {set i 0} {$i < [llength $tags]} {incr i} {
+       set a [lindex $tags $i]
+       for {set j 0} {$j < $i} {incr j} {
+           set b [lindex $tags $j]
+           set r [anc_or_desc $a $b]
+           if {$r == 1} {
+               set tags [lreplace $tags $j $j]
+               incr j -1
+               incr i -1
+           } elseif {$r == -1} {
+               set tags [lreplace $tags $i $i]
+               incr i -1
+               break
+           }
+       }
+    }
+
+    if {[array names growing] ne {}} {
+       # graph isn't finished, need to check if any tag could get
+       # eclipsed by another tag coming later.  Simply ignore any
+       # tags that could later get eclipsed.
+       set ctags {}
+       foreach t $tags {
+           if {[is_certain $t $origid]} {
+               lappend ctags $t
+           }
+       }
+       if {$tags eq $ctags} {
+           set cached_dtags($origid) $tags
+       } else {
+           set tags $ctags
+       }
+    } else {
+       set cached_dtags($origid) $tags
+    }
+    set t3 [clock clicks -milliseconds]
+    if {0 && $t3 - $t1 >= 100} {
+       puts "iterating descendents ($loopix/[llength $todo] nodes) took\
+           [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
+    }
+    return $tags
+}
+
+proc anctags {id} {
+    global arcnos arcids arcout arcend arctags idtags allparents
+    global growing cached_atags
+
+    if {![info exists allparents($id)]} {
+       return {}
+    }
+    set t1 [clock clicks -milliseconds]
+    set argid $id
+    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
+       # part-way along an arc; check that arc first
+       set a [lindex $arcnos($id) 0]
+       if {$arctags($a) ne {}} {
+           validate_arctags $a
+           set i [lsearch -exact $arcids($a) $id]
+           foreach t $arctags($a) {
+               set j [lsearch -exact $arcids($a) $t]
+               if {$j > $i} {
+                   return $t
+               }
+           }
+       }
+       if {![info exists arcend($a)]} {
+           return {}
+       }
+       set id $arcend($a)
+       if {[info exists idtags($id)]} {
+           return $id
+       }
+    }
+    if {[info exists cached_atags($id)]} {
+       return $cached_atags($id)
+    }
+
+    set origid $id
+    set todo [list $id]
+    set queued($id) 1
+    set taglist {}
+    set nc 1
+    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
+       set id [lindex $todo $i]
+       set done($id) 1
+       set td [info exists hastaggeddescendent($id)]
+       if {!$td} {
+           incr nc -1
+       }
+       # ignore tags on starting node
+       if {!$td && $i > 0} {
+           if {[info exists idtags($id)]} {
+               set tagloc($id) $id
+               set td 1
+           } elseif {[info exists cached_atags($id)]} {
+               set tagloc($id) $cached_atags($id)
+               set td 1
+           }
+       }
+       foreach a $arcout($id) {
+           if {!$td && $arctags($a) ne {}} {
+               validate_arctags $a
+               if {$arctags($a) ne {}} {
+                   lappend tagloc($id) [lindex $arctags($a) 0]
+               }
+           }
+           if {![info exists arcend($a)]} continue
+           set d $arcend($a)
+           if {$td || $arctags($a) ne {}} {
+               set tomark [list $d]
+               for {set j 0} {$j < [llength $tomark]} {incr j} {
+                   set dd [lindex $tomark $j]
+                   if {![info exists hastaggeddescendent($dd)]} {
+                       if {[info exists done($dd)]} {
+                           foreach b $arcout($dd) {
+                               if {[info exists arcend($b)]} {
+                                   lappend tomark $arcend($b)
+                               }
+                           }
+                           if {[info exists tagloc($dd)]} {
+                               unset tagloc($dd)
+                           }
+                       } elseif {[info exists queued($dd)]} {
+                           incr nc -1
+                       }
+                       set hastaggeddescendent($dd) 1
+                   }
+               }
+           }
+           if {![info exists queued($d)]} {
+               lappend todo $d
+               set queued($d) 1
+               if {![info exists hastaggeddescendent($d)]} {
+                   incr nc
+               }
+           }
+       }
+    }
+    set t2 [clock clicks -milliseconds]
+    set loopix $i
+    set tags {}
+    foreach id [array names tagloc] {
+       if {![info exists hastaggeddescendent($id)]} {
+           foreach t $tagloc($id) {
+               if {[lsearch -exact $tags $t] < 0} {
+                   lappend tags $t
+               }
+           }
+       }
+    }
+
+    # remove tags that are ancestors of other tags
+    for {set i 0} {$i < [llength $tags]} {incr i} {
+       set a [lindex $tags $i]
+       for {set j 0} {$j < $i} {incr j} {
+           set b [lindex $tags $j]
+           set r [anc_or_desc $a $b]
+           if {$r == -1} {
+               set tags [lreplace $tags $j $j]
+               incr j -1
+               incr i -1
+           } elseif {$r == 1} {
+               set tags [lreplace $tags $i $i]
+               incr i -1
+               break
+           }
+       }
+    }
+
+    if {[array names growing] ne {}} {
+       # graph isn't finished, need to check if any tag could get
+       # eclipsed by another tag coming later.  Simply ignore any
+       # tags that could later get eclipsed.
+       set ctags {}
+       foreach t $tags {
+           if {[is_certain $origid $t]} {
+               lappend ctags $t
+           }
+       }
+       if {$tags eq $ctags} {
+           set cached_atags($origid) $tags
+       } else {
+           set tags $ctags
+       }
+    } else {
+       set cached_atags($origid) $tags
+    }
+    set t3 [clock clicks -milliseconds]
+    if {0 && $t3 - $t1 >= 100} {
+       puts "iterating ancestors ($loopix/[llength $todo] nodes) took\
+           [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
+    }
+    return $tags
+}
+
+# Return the list of IDs that have heads that are descendents of id,
+# including id itself if it has a head.
+proc descheads {id} {
+    global arcnos arcstart arcids archeads idheads cached_dheads
+    global allparents
+
+    if {![info exists allparents($id)]} {
+       return {}
+    }
+    set aret {}
+    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
+       # part-way along an arc; check it first
+       set a [lindex $arcnos($id) 0]
+       if {$archeads($a) ne {}} {
+           validate_archeads $a
+           set i [lsearch -exact $arcids($a) $id]
+           foreach t $archeads($a) {
+               set j [lsearch -exact $arcids($a) $t]
+               if {$j > $i} break
+               lappend aret $t
+           }
+       }
+       set id $arcstart($a)
+    }
+    set origid $id
+    set todo [list $id]
+    set seen($id) 1
+    set ret {}
+    for {set i 0} {$i < [llength $todo]} {incr i} {
+       set id [lindex $todo $i]
+       if {[info exists cached_dheads($id)]} {
+           set ret [concat $ret $cached_dheads($id)]
+       } else {
+           if {[info exists idheads($id)]} {
+               lappend ret $id
+           }
+           foreach a $arcnos($id) {
+               if {$archeads($a) ne {}} {
+                   validate_archeads $a
+                   if {$archeads($a) ne {}} {
+                       set ret [concat $ret $archeads($a)]
+                   }
+               }
+               set d $arcstart($a)
+               if {![info exists seen($d)]} {
+                   lappend todo $d
+                   set seen($d) 1
+               }
+           }
+       }
+    }
+    set ret [lsort -unique $ret]
+    set cached_dheads($origid) $ret
+    return [concat $ret $aret]
+}
+
+proc addedtag {id} {
+    global arcnos arcout cached_dtags cached_atags
+
+    if {![info exists arcnos($id)]} return
+    if {![info exists arcout($id)]} {
+       recalcarc [lindex $arcnos($id) 0]
+    }
+    catch {unset cached_dtags}
+    catch {unset cached_atags}
+}
+
+proc addedhead {hid head} {
+    global arcnos arcout cached_dheads
+
+    if {![info exists arcnos($hid)]} return
+    if {![info exists arcout($hid)]} {
+       recalcarc [lindex $arcnos($hid) 0]
+    }
+    catch {unset cached_dheads}
+}
+
+proc removedhead {hid head} {
+    global cached_dheads
+
+    catch {unset cached_dheads}
+}
+
+proc movedhead {hid head} {
+    global arcnos arcout cached_dheads
+
+    if {![info exists arcnos($hid)]} return
+    if {![info exists arcout($hid)]} {
+       recalcarc [lindex $arcnos($hid) 0]
+    }
+    catch {unset cached_dheads}
+}
+
+proc changedrefs {} {
+    global cached_dheads cached_dtags cached_atags
+    global arctags archeads arcnos arcout idheads idtags
+
+    foreach id [concat [array names idheads] [array names idtags]] {
+       if {[info exists arcnos($id)] && ![info exists arcout($id)]} {
+           set a [lindex $arcnos($id) 0]
+           if {![info exists donearc($a)]} {
+               recalcarc $a
+               set donearc($a) 1
+           }
+       }
+    }
+    catch {unset cached_dtags}
+    catch {unset cached_atags}
+    catch {unset cached_dheads}
+}
+
+proc rereadrefs {} {
+    global idtags idheads idotherrefs mainhead
+
+    set refids [concat [array names idtags] \
+                   [array names idheads] [array names idotherrefs]]
+    foreach id $refids {
+       if {![info exists ref($id)]} {
+           set ref($id) [listrefs $id]
+       }
+    }
+    set oldmainhead $mainhead
+    readrefs
+    changedrefs
+    set refids [lsort -unique [concat $refids [array names idtags] \
+                       [array names idheads] [array names idotherrefs]]]
+    foreach id $refids {
+       set v [listrefs $id]
+       if {![info exists ref($id)] || $ref($id) != $v ||
+           ($id eq $oldmainhead && $id ne $mainhead) ||
+           ($id eq $mainhead && $id ne $oldmainhead)} {
+           redrawtags $id
+       }
+    }
+    run refill_reflist
+}
+
+proc listrefs {id} {
+    global idtags idheads idotherrefs
+
+    set x {}
+    if {[info exists idtags($id)]} {
+       set x $idtags($id)
+    }
+    set y {}
+    if {[info exists idheads($id)]} {
+       set y $idheads($id)
+    }
+    set z {}
+    if {[info exists idotherrefs($id)]} {
+       set z $idotherrefs($id)
+    }
+    return [list $x $y $z]
+}
+
+proc showtag {tag isnew} {
+    global ctext tagcontents tagids linknum tagobjid
+
+    if {$isnew} {
+       addtohistory [list showtag $tag 0]
+    }
+    $ctext conf -state normal
+    clear_ctext
+    settabs 0
+    set linknum 0
+    if {![info exists tagcontents($tag)]} {
+       catch {
+           set tagcontents($tag) [exec git cat-file tag $tagobjid($tag)]
+       }
+    }
+    if {[info exists tagcontents($tag)]} {
+       set text $tagcontents($tag)
+    } else {
+       set text "Tag: $tag\nId:  $tagids($tag)"
+    }
+    appendwithlinks $text {}
+    $ctext conf -state disabled
+    init_flist {}
+}
+
+proc doquit {} {
+    global stopped
+    set stopped 100
+    savestuff .
+    destroy .
+}
+
+proc mkfontdisp {font top which} {
+    global fontattr fontpref $font
+
+    set fontpref($font) [set $font]
+    button $top.${font}but -text $which -font optionfont \
+       -command [list choosefont $font $which]
+    label $top.$font -relief flat -font $font \
+       -text $fontattr($font,family) -justify left
+    grid x $top.${font}but $top.$font -sticky w
+}
+
+proc choosefont {font which} {
+    global fontparam fontlist fonttop fontattr
+
+    set fontparam(which) $which
+    set fontparam(font) $font
+    set fontparam(family) [font actual $font -family]
+    set fontparam(size) $fontattr($font,size)
+    set fontparam(weight) $fontattr($font,weight)
+    set fontparam(slant) $fontattr($font,slant)
+    set top .gitkfont
+    set fonttop $top
+    if {![winfo exists $top]} {
+       font create sample
+       eval font config sample [font actual $font]
+       toplevel $top
+       wm title $top "Gitk font chooser"
+       label $top.l -textvariable fontparam(which) -font uifont
+       pack $top.l -side top
+       set fontlist [lsort [font families]]
+       frame $top.f
+       listbox $top.f.fam -listvariable fontlist \
+           -yscrollcommand [list $top.f.sb set]
+       bind $top.f.fam <<ListboxSelect>> selfontfam
+       scrollbar $top.f.sb -command [list $top.f.fam yview]
+       pack $top.f.sb -side right -fill y
+       pack $top.f.fam -side left -fill both -expand 1
+       pack $top.f -side top -fill both -expand 1
+       frame $top.g
+       spinbox $top.g.size -from 4 -to 40 -width 4 \
+           -textvariable fontparam(size) \
+           -validatecommand {string is integer -strict %s}
+       checkbutton $top.g.bold -padx 5 \
+           -font {{Times New Roman} 12 bold} -text "B" -indicatoron 0 \
+           -variable fontparam(weight) -onvalue bold -offvalue normal
+       checkbutton $top.g.ital -padx 5 \
+           -font {{Times New Roman} 12 italic} -text "I" -indicatoron 0  \
+           -variable fontparam(slant) -onvalue italic -offvalue roman
+       pack $top.g.size $top.g.bold $top.g.ital -side left
+       pack $top.g -side top
+       canvas $top.c -width 150 -height 50 -border 2 -relief sunk \
+           -background white
+       $top.c create text 100 25 -anchor center -text $which -font sample \
+           -fill black -tags text
+       bind $top.c <Configure> [list centertext $top.c]
+       pack $top.c -side top -fill x
+       frame $top.buts
+       button $top.buts.ok -text "OK" -command fontok -default active \
+           -font uifont
+       button $top.buts.can -text "Cancel" -command fontcan -default normal \
+           -font uifont
+       grid $top.buts.ok $top.buts.can
+       grid columnconfigure $top.buts 0 -weight 1 -uniform a
+       grid columnconfigure $top.buts 1 -weight 1 -uniform a
+       pack $top.buts -side bottom -fill x
+       trace add variable fontparam write chg_fontparam
+    } else {
+       raise $top
+       $top.c itemconf text -text $which
+    }
+    set i [lsearch -exact $fontlist $fontparam(family)]
+    if {$i >= 0} {
+       $top.f.fam selection set $i
+       $top.f.fam see $i
+    }
+}
+
+proc centertext {w} {
+    $w coords text [expr {[winfo width $w] / 2}] [expr {[winfo height $w] / 2}]
+}
+
+proc fontok {} {
+    global fontparam fontpref prefstop
+
+    set f $fontparam(font)
+    set fontpref($f) [list $fontparam(family) $fontparam(size)]
+    if {$fontparam(weight) eq "bold"} {
+       lappend fontpref($f) "bold"
+    }
+    if {$fontparam(slant) eq "italic"} {
+       lappend fontpref($f) "italic"
+    }
+    set w $prefstop.$f
+    $w conf -text $fontparam(family) -font $fontpref($f)
+       
+    fontcan
+}
+
+proc fontcan {} {
+    global fonttop fontparam
+
+    if {[info exists fonttop]} {
+       catch {destroy $fonttop}
+       catch {font delete sample}
+       unset fonttop
+       unset fontparam
+    }
+}
+
+proc selfontfam {} {
+    global fonttop fontparam
+
+    set i [$fonttop.f.fam curselection]
+    if {$i ne {}} {
+       set fontparam(family) [$fonttop.f.fam get $i]
+    }
+}
+
+proc chg_fontparam {v sub op} {
+    global fontparam
+
+    font config sample -$sub $fontparam($sub)
+}
+
+proc doprefs {} {
+    global maxwidth maxgraphpct
+    global oldprefs prefstop showneartags showlocalchanges
+    global bgcolor fgcolor ctext diffcolors selectbgcolor
+    global uifont tabstop limitdiffs
+
+    set top .gitkprefs
+    set prefstop $top
+    if {[winfo exists $top]} {
+       raise $top
+       return
+    }
+    foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
+                  limitdiffs tabstop} {
+       set oldprefs($v) [set $v]
+    }
+    toplevel $top
+    wm title $top "Gitk preferences"
+    label $top.ldisp -text "Commit list display options"
+    $top.ldisp configure -font uifont
+    grid $top.ldisp - -sticky w -pady 10
+    label $top.spacer -text " "
+    label $top.maxwidthl -text "Maximum graph width (lines)" \
+       -font optionfont
+    spinbox $top.maxwidth -from 0 -to 100 -width 4 -textvariable maxwidth
+    grid $top.spacer $top.maxwidthl $top.maxwidth -sticky w
+    label $top.maxpctl -text "Maximum graph width (% of pane)" \
+       -font optionfont
+    spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct
+    grid x $top.maxpctl $top.maxpct -sticky w
+    frame $top.showlocal
+    label $top.showlocal.l -text "Show local changes" -font optionfont
+    checkbutton $top.showlocal.b -variable showlocalchanges
+    pack $top.showlocal.b $top.showlocal.l -side left
+    grid x $top.showlocal -sticky w
+
+    label $top.ddisp -text "Diff display options"
+    $top.ddisp configure -font uifont
+    grid $top.ddisp - -sticky w -pady 10
+    label $top.tabstopl -text "Tab spacing" -font optionfont
+    spinbox $top.tabstop -from 1 -to 20 -width 4 -textvariable tabstop
+    grid x $top.tabstopl $top.tabstop -sticky w
+    frame $top.ntag
+    label $top.ntag.l -text "Display nearby tags" -font optionfont
+    checkbutton $top.ntag.b -variable showneartags
+    pack $top.ntag.b $top.ntag.l -side left
+    grid x $top.ntag -sticky w
+    frame $top.ldiff
+    label $top.ldiff.l -text "Limit diffs to listed paths" -font optionfont
+    checkbutton $top.ldiff.b -variable limitdiffs
+    pack $top.ldiff.b $top.ldiff.l -side left
+    grid x $top.ldiff -sticky w
+
+    label $top.cdisp -text "Colors: press to choose"
+    $top.cdisp configure -font uifont
+    grid $top.cdisp - -sticky w -pady 10
+    label $top.bg -padx 40 -relief sunk -background $bgcolor
+    button $top.bgbut -text "Background" -font optionfont \
+       -command [list choosecolor bgcolor 0 $top.bg background setbg]
+    grid x $top.bgbut $top.bg -sticky w
+    label $top.fg -padx 40 -relief sunk -background $fgcolor
+    button $top.fgbut -text "Foreground" -font optionfont \
+       -command [list choosecolor fgcolor 0 $top.fg foreground setfg]
+    grid x $top.fgbut $top.fg -sticky w
+    label $top.diffold -padx 40 -relief sunk -background [lindex $diffcolors 0]
+    button $top.diffoldbut -text "Diff: old lines" -font optionfont \
+       -command [list choosecolor diffcolors 0 $top.diffold "diff old lines" \
+                     [list $ctext tag conf d0 -foreground]]
+    grid x $top.diffoldbut $top.diffold -sticky w
+    label $top.diffnew -padx 40 -relief sunk -background [lindex $diffcolors 1]
+    button $top.diffnewbut -text "Diff: new lines" -font optionfont \
+       -command [list choosecolor diffcolors 1 $top.diffnew "diff new lines" \
+                     [list $ctext tag conf d1 -foreground]]
+    grid x $top.diffnewbut $top.diffnew -sticky w
+    label $top.hunksep -padx 40 -relief sunk -background [lindex $diffcolors 2]
+    button $top.hunksepbut -text "Diff: hunk header" -font optionfont \
+       -command [list choosecolor diffcolors 2 $top.hunksep \
+                     "diff hunk header" \
+                     [list $ctext tag conf hunksep -foreground]]
+    grid x $top.hunksepbut $top.hunksep -sticky w
+    label $top.selbgsep -padx 40 -relief sunk -background $selectbgcolor
+    button $top.selbgbut -text "Select bg" -font optionfont \
+       -command [list choosecolor selectbgcolor 0 $top.selbgsep background setselbg]
+    grid x $top.selbgbut $top.selbgsep -sticky w
+
+    label $top.cfont -text "Fonts: press to choose"
+    $top.cfont configure -font uifont
+    grid $top.cfont - -sticky w -pady 10
+    mkfontdisp mainfont $top "Main font"
+    mkfontdisp textfont $top "Diff display font"
+    mkfontdisp uifont $top "User interface font"
+
+    frame $top.buts
+    button $top.buts.ok -text "OK" -command prefsok -default active
+    $top.buts.ok configure -font uifont
+    button $top.buts.can -text "Cancel" -command prefscan -default normal
+    $top.buts.can configure -font uifont
+    grid $top.buts.ok $top.buts.can
+    grid columnconfigure $top.buts 0 -weight 1 -uniform a
+    grid columnconfigure $top.buts 1 -weight 1 -uniform a
+    grid $top.buts - - -pady 10 -sticky ew
+    bind $top <Visibility> "focus $top.buts.ok"
+}
+
+proc choosecolor {v vi w x cmd} {
+    global $v
+
+    set c [tk_chooseColor -initialcolor [lindex [set $v] $vi] \
+              -title "Gitk: choose color for $x"]
+    if {$c eq {}} return
+    $w conf -background $c
+    lset $v $vi $c
+    eval $cmd $c
+}
+
+proc setselbg {c} {
+    global bglist cflist
+    foreach w $bglist {
+       $w configure -selectbackground $c
+    }
+    $cflist tag configure highlight \
+       -background [$cflist cget -selectbackground]
+    allcanvs itemconf secsel -fill $c
+}
+
+proc setbg {c} {
+    global bglist
+
+    foreach w $bglist {
+       $w conf -background $c
+    }
+}
+
+proc setfg {c} {
+    global fglist canv
+
+    foreach w $fglist {
+       $w conf -foreground $c
+    }
+    allcanvs itemconf text -fill $c
+    $canv itemconf circle -outline $c
+}
+
+proc prefscan {} {
+    global oldprefs prefstop
+
+    foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
+                  limitdiffs tabstop} {
+       global $v
+       set $v $oldprefs($v)
+    }
+    catch {destroy $prefstop}
+    unset prefstop
+    fontcan
+}
+
+proc prefsok {} {
+    global maxwidth maxgraphpct
+    global oldprefs prefstop showneartags showlocalchanges
+    global fontpref mainfont textfont uifont
+    global limitdiffs treediffs
+
+    catch {destroy $prefstop}
+    unset prefstop
+    fontcan
+    set fontchanged 0
+    if {$mainfont ne $fontpref(mainfont)} {
+       set mainfont $fontpref(mainfont)
+       parsefont mainfont $mainfont
+       eval font configure mainfont [fontflags mainfont]
+       eval font configure mainfontbold [fontflags mainfont 1]
+       setcoords
+       set fontchanged 1
+    }
+    if {$textfont ne $fontpref(textfont)} {
+       set textfont $fontpref(textfont)
+       parsefont textfont $textfont
+       eval font configure textfont [fontflags textfont]
+       eval font configure textfontbold [fontflags textfont 1]
+    }
+    if {$uifont ne $fontpref(uifont)} {
+       set uifont $fontpref(uifont)
+       parsefont uifont $uifont
+       eval font configure uifont [fontflags uifont]
+    }
+    settabs
+    if {$showlocalchanges != $oldprefs(showlocalchanges)} {
+       if {$showlocalchanges} {
+           doshowlocalchanges
+       } else {
+           dohidelocalchanges
+       }
+    }
+    if {$limitdiffs != $oldprefs(limitdiffs)} {
+       # treediffs elements are limited by path
+       catch {unset treediffs}
+    }
+    if {$fontchanged || $maxwidth != $oldprefs(maxwidth)
+       || $maxgraphpct != $oldprefs(maxgraphpct)} {
+       redisplay
+    } elseif {$showneartags != $oldprefs(showneartags) ||
+         $limitdiffs != $oldprefs(limitdiffs)} {
+       reselectline
+    }
+}
+
+proc formatdate {d} {
+    global datetimeformat
+    if {$d ne {}} {
+       set d [clock format $d -format $datetimeformat]
+    }
+    return $d
+}
+
+# This list of encoding names and aliases is distilled from
+# http://www.iana.org/assignments/character-sets.
+# Not all of them are supported by Tcl.
+set encoding_aliases {
+    { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII
+      ISO646-US US-ASCII us IBM367 cp367 csASCII }
+    { ISO-10646-UTF-1 csISO10646UTF1 }
+    { ISO_646.basic:1983 ref csISO646basic1983 }
+    { INVARIANT csINVARIANT }
+    { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion }
+    { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom }
+    { NATS-SEFI iso-ir-8-1 csNATSSEFI }
+    { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD }
+    { NATS-DANO iso-ir-9-1 csNATSDANO }
+    { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD }
+    { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish }
+    { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames }
+    { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 }
+    { ISO-2022-KR csISO2022KR }
+    { EUC-KR csEUCKR }
+    { ISO-2022-JP csISO2022JP }
+    { ISO-2022-JP-2 csISO2022JP2 }
+    { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7
+      csISO13JISC6220jp }
+    { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro }
+    { IT iso-ir-15 ISO646-IT csISO15Italian }
+    { PT iso-ir-16 ISO646-PT csISO16Portuguese }
+    { ES iso-ir-17 ISO646-ES csISO17Spanish }
+    { greek7-old iso-ir-18 csISO18Greek7Old }
+    { latin-greek iso-ir-19 csISO19LatinGreek }
+    { DIN_66003 iso-ir-21 de ISO646-DE csISO21German }
+    { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French }
+    { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 }
+    { ISO_5427 iso-ir-37 csISO5427Cyrillic }
+    { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 }
+    { BS_viewdata iso-ir-47 csISO47BSViewdata }
+    { INIS iso-ir-49 csISO49INIS }
+    { INIS-8 iso-ir-50 csISO50INIS8 }
+    { INIS-cyrillic iso-ir-51 csISO51INISCyrillic }
+    { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 }
+    { ISO_5428:1980 iso-ir-55 csISO5428Greek }
+    { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 }
+    { GB_2312-80 iso-ir-58 chinese csISO58GB231280 }
+    { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian
+      csISO60Norwegian1 }
+    { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 }
+    { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French }
+    { videotex-suppl iso-ir-70 csISO70VideotexSupp1 }
+    { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 }
+    { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 }
+    { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian }
+    { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 }
+    { greek7 iso-ir-88 csISO88Greek7 }
+    { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 }
+    { iso-ir-90 csISO90 }
+    { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a }
+    { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b
+      csISO92JISC62991984b }
+    { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd }
+    { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand }
+    { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add
+      csISO95JIS62291984handadd }
+    { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana }
+    { ISO_2033-1983 iso-ir-98 e13b csISO2033 }
+    { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS }
+    { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819
+      CP819 csISOLatin1 }
+    { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 }
+    { T.61-7bit iso-ir-102 csISO102T617bit }
+    { T.61-8bit T.61 iso-ir-103 csISO103T618bit }
+    { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 }
+    { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 }
+    { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic }
+    { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 }
+    { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 }
+    { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr }
+    { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708
+      arabic csISOLatinArabic }
+    { ISO_8859-6-E csISO88596E ISO-8859-6-E }
+    { ISO_8859-6-I csISO88596I ISO-8859-6-I }
+    { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118
+      greek greek8 csISOLatinGreek }
+    { T.101-G2 iso-ir-128 csISO128T101G2 }
+    { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew
+      csISOLatinHebrew }
+    { ISO_8859-8-E csISO88598E ISO-8859-8-E }
+    { ISO_8859-8-I csISO88598I ISO-8859-8-I }
+    { CSN_369103 iso-ir-139 csISO139CSN369103 }
+    { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 }
+    { ISO_6937-2-add iso-ir-142 csISOTextComm }
+    { IEC_P27-1 iso-ir-143 csISO143IECP271 }
+    { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic
+      csISOLatinCyrillic }
+    { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian }
+    { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian }
+    { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 }
+    { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT }
+    { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba }
+    { ISO_6937-2-25 iso-ir-152 csISO6937Add }
+    { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 }
+    { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp }
+    { ISO_10367-box iso-ir-155 csISO10367Box }
+    { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 }
+    { latin-lap lap iso-ir-158 csISO158Lap }
+    { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 }
+    { DS_2089 DS2089 ISO646-DK dk csISO646Danish }
+    { us-dk csUSDK }
+    { dk-us csDKUS }
+    { JIS_X0201 X0201 csHalfWidthKatakana }
+    { KSC5636 ISO646-KR csKSC5636 }
+    { ISO-10646-UCS-2 csUnicode }
+    { ISO-10646-UCS-4 csUCS4 }
+    { DEC-MCS dec csDECMCS }
+    { hp-roman8 roman8 r8 csHPRoman8 }
+    { macintosh mac csMacintosh }
+    { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl
+      csIBM037 }
+    { IBM038 EBCDIC-INT cp038 csIBM038 }
+    { IBM273 CP273 csIBM273 }
+    { IBM274 EBCDIC-BE CP274 csIBM274 }
+    { IBM275 EBCDIC-BR cp275 csIBM275 }
+    { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 }
+    { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 }
+    { IBM280 CP280 ebcdic-cp-it csIBM280 }
+    { IBM281 EBCDIC-JP-E cp281 csIBM281 }
+    { IBM284 CP284 ebcdic-cp-es csIBM284 }
+    { IBM285 CP285 ebcdic-cp-gb csIBM285 }
+    { IBM290 cp290 EBCDIC-JP-kana csIBM290 }
+    { IBM297 cp297 ebcdic-cp-fr csIBM297 }
+    { IBM420 cp420 ebcdic-cp-ar1 csIBM420 }
+    { IBM423 cp423 ebcdic-cp-gr csIBM423 }
+    { IBM424 cp424 ebcdic-cp-he csIBM424 }
+    { IBM437 cp437 437 csPC8CodePage437 }
+    { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 }
+    { IBM775 cp775 csPC775Baltic }
+    { IBM850 cp850 850 csPC850Multilingual }
+    { IBM851 cp851 851 csIBM851 }
+    { IBM852 cp852 852 csPCp852 }
+    { IBM855 cp855 855 csIBM855 }
+    { IBM857 cp857 857 csIBM857 }
+    { IBM860 cp860 860 csIBM860 }
+    { IBM861 cp861 861 cp-is csIBM861 }
+    { IBM862 cp862 862 csPC862LatinHebrew }
+    { IBM863 cp863 863 csIBM863 }
+    { IBM864 cp864 csIBM864 }
+    { IBM865 cp865 865 csIBM865 }
+    { IBM866 cp866 866 csIBM866 }
+    { IBM868 CP868 cp-ar csIBM868 }
+    { IBM869 cp869 869 cp-gr csIBM869 }
+    { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 }
+    { IBM871 CP871 ebcdic-cp-is csIBM871 }
+    { IBM880 cp880 EBCDIC-Cyrillic csIBM880 }
+    { IBM891 cp891 csIBM891 }
+    { IBM903 cp903 csIBM903 }
+    { IBM904 cp904 904 csIBBM904 }
+    { IBM905 CP905 ebcdic-cp-tr csIBM905 }
+    { IBM918 CP918 ebcdic-cp-ar2 csIBM918 }
+    { IBM1026 CP1026 csIBM1026 }
+    { EBCDIC-AT-DE csIBMEBCDICATDE }
+    { EBCDIC-AT-DE-A csEBCDICATDEA }
+    { EBCDIC-CA-FR csEBCDICCAFR }
+    { EBCDIC-DK-NO csEBCDICDKNO }
+    { EBCDIC-DK-NO-A csEBCDICDKNOA }
+    { EBCDIC-FI-SE csEBCDICFISE }
+    { EBCDIC-FI-SE-A csEBCDICFISEA }
+    { EBCDIC-FR csEBCDICFR }
+    { EBCDIC-IT csEBCDICIT }
+    { EBCDIC-PT csEBCDICPT }
+    { EBCDIC-ES csEBCDICES }
+    { EBCDIC-ES-A csEBCDICESA }
+    { EBCDIC-ES-S csEBCDICESS }
+    { EBCDIC-UK csEBCDICUK }
+    { EBCDIC-US csEBCDICUS }
+    { UNKNOWN-8BIT csUnknown8BiT }
+    { MNEMONIC csMnemonic }
+    { MNEM csMnem }
+    { VISCII csVISCII }
+    { VIQR csVIQR }
+    { KOI8-R csKOI8R }
+    { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro }
+    { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro }
+    { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro }
+    { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro }
+    { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro }
+    { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro }
+    { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro }
+    { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro }
+    { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro }
+    { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro }
+    { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro }
+    { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro }
+    { IBM1047 IBM-1047 }
+    { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian }
+    { Amiga-1251 Ami1251 Amiga1251 Ami-1251 }
+    { UNICODE-1-1 csUnicode11 }
+    { CESU-8 csCESU-8 }
+    { BOCU-1 csBOCU-1 }
+    { UNICODE-1-1-UTF-7 csUnicode11UTF7 }
+    { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic
+      l8 }
+    { ISO-8859-15 ISO_8859-15 Latin-9 }
+    { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 }
+    { GBK CP936 MS936 windows-936 }
+    { JIS_Encoding csJISEncoding }
+    { Shift_JIS MS_Kanji csShiftJIS }
+    { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese
+      EUC-JP }
+    { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese }
+    { ISO-10646-UCS-Basic csUnicodeASCII }
+    { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 }
+    { ISO-Unicode-IBM-1261 csUnicodeIBM1261 }
+    { ISO-Unicode-IBM-1268 csUnicodeIBM1268 }
+    { ISO-Unicode-IBM-1276 csUnicodeIBM1276 }
+    { ISO-Unicode-IBM-1264 csUnicodeIBM1264 }
+    { ISO-Unicode-IBM-1265 csUnicodeIBM1265 }
+    { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 }
+    { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 }
+    { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 }
+    { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 }
+    { Adobe-Standard-Encoding csAdobeStandardEncoding }
+    { Ventura-US csVenturaUS }
+    { Ventura-International csVenturaInternational }
+    { PC8-Danish-Norwegian csPC8DanishNorwegian }
+    { PC8-Turkish csPC8Turkish }
+    { IBM-Symbols csIBMSymbols }
+    { IBM-Thai csIBMThai }
+    { HP-Legal csHPLegal }
+    { HP-Pi-font csHPPiFont }
+    { HP-Math8 csHPMath8 }
+    { Adobe-Symbol-Encoding csHPPSMath }
+    { HP-DeskTop csHPDesktop }
+    { Ventura-Math csVenturaMath }
+    { Microsoft-Publishing csMicrosoftPublishing }
+    { Windows-31J csWindows31J }
+    { GB2312 csGB2312 }
+    { Big5 csBig5 }
+}
+
+proc tcl_encoding {enc} {
+    global encoding_aliases
+    set names [encoding names]
+    set lcnames [string tolower $names]
+    set enc [string tolower $enc]
+    set i [lsearch -exact $lcnames $enc]
+    if {$i < 0} {
+       # look for "isonnn" instead of "iso-nnn" or "iso_nnn"
+       if {[regsub {^iso[-_]} $enc iso encx]} {
+           set i [lsearch -exact $lcnames $encx]
+       }
+    }
+    if {$i < 0} {
+       foreach l $encoding_aliases {
+           set ll [string tolower $l]
+           if {[lsearch -exact $ll $enc] < 0} continue
+           # look through the aliases for one that tcl knows about
+           foreach e $ll {
+               set i [lsearch -exact $lcnames $e]
+               if {$i < 0} {
+                   if {[regsub {^iso[-_]} $e iso ex]} {
+                       set i [lsearch -exact $lcnames $ex]
+                   }
+               }
+               if {$i >= 0} break
+           }
+           break
+       }
+    }
+    if {$i >= 0} {
+       return [lindex $names $i]
+    }
+    return {}
+}
+
+# First check that Tcl/Tk is recent enough
+if {[catch {package require Tk 8.4} err]} {
+    show_error {} . "Sorry, gitk cannot run with this version of Tcl/Tk.\n\
+                    Gitk requires at least Tcl/Tk 8.4."
+    exit 1
+}
+
+# defaults...
+set datemode 0
+set wrcomcmd "git diff-tree --stdin -p --pretty"
+
+set gitencoding {}
+catch {
+    set gitencoding [exec git config --get i18n.commitencoding]
+}
+if {$gitencoding == ""} {
+    set gitencoding "utf-8"
+}
+set tclencoding [tcl_encoding $gitencoding]
+if {$tclencoding == {}} {
+    puts stderr "Warning: encoding $gitencoding is not supported by Tcl/Tk"
+}
+
+set mainfont {Helvetica 9}
+set textfont {Courier 9}
+set uifont {Helvetica 9 bold}
+set tabstop 8
+set findmergefiles 0
+set maxgraphpct 50
+set maxwidth 16
+set revlistorder 0
+set fastdate 0
+set uparrowlen 5
+set downarrowlen 5
+set mingaplen 100
+set cmitmode "patch"
+set wrapcomment "none"
+set showneartags 1
+set maxrefs 20
+set maxlinelen 200
+set showlocalchanges 1
+set limitdiffs 1
+set datetimeformat "%Y-%m-%d %H:%M:%S"
+
+set colors {green red blue magenta darkgrey brown orange}
+set bgcolor white
+set fgcolor black
+set diffcolors {red "#00a000" blue}
+set diffcontext 3
+set selectbgcolor gray85
+
+catch {source ~/.gitk}
+
+font create optionfont -family sans-serif -size -12
+
+parsefont mainfont $mainfont
+eval font create mainfont [fontflags mainfont]
+eval font create mainfontbold [fontflags mainfont 1]
+
+parsefont textfont $textfont
+eval font create textfont [fontflags textfont]
+eval font create textfontbold [fontflags textfont 1]
+
+parsefont uifont $uifont
+eval font create uifont [fontflags uifont]
+
+# check that we can find a .git directory somewhere...
+if {[catch {set gitdir [gitdir]}]} {
+    show_error {} . "Cannot find a git repository here."
+    exit 1
+}
+if {![file isdirectory $gitdir]} {
+    show_error {} . "Cannot find the git directory \"$gitdir\"."
+    exit 1
+}
+
+set mergeonly 0
+set revtreeargs {}
+set cmdline_files {}
+set i 0
+foreach arg $argv {
+    switch -- $arg {
+       "" { }
+       "-d" { set datemode 1 }
+       "--merge" {
+           set mergeonly 1
+           lappend revtreeargs $arg
+       }
+       "--" {
+           set cmdline_files [lrange $argv [expr {$i + 1}] end]
+           break
+       }
+       default {
+           lappend revtreeargs $arg
+       }
+    }
+    incr i
+}
+
+if {$i >= [llength $argv] && $revtreeargs ne {}} {
+    # no -- on command line, but some arguments (other than -d)
+    if {[catch {
+       set f [eval exec git rev-parse --no-revs --no-flags $revtreeargs]
+       set cmdline_files [split $f "\n"]
+       set n [llength $cmdline_files]
+       set revtreeargs [lrange $revtreeargs 0 end-$n]
+       # Unfortunately git rev-parse doesn't produce an error when
+       # something is both a revision and a filename.  To be consistent
+       # with git log and git rev-list, check revtreeargs for filenames.
+       foreach arg $revtreeargs {
+           if {[file exists $arg]} {
+               show_error {} . "Ambiguous argument '$arg': both revision\
+                                and filename"
+               exit 1
+           }
+       }
+    } err]} {
+       # unfortunately we get both stdout and stderr in $err,
+       # so look for "fatal:".
+       set i [string first "fatal:" $err]
+       if {$i > 0} {
+           set err [string range $err [expr {$i + 6}] end]
+       }
+       show_error {} . "Bad arguments to gitk:\n$err"
+       exit 1
+    }
+}
+
+if {$mergeonly} {
+    # find the list of unmerged files
+    set mlist {}
+    set nr_unmerged 0
+    if {[catch {
+       set fd [open "| git ls-files -u" r]
+    } err]} {
+       show_error {} . "Couldn't get list of unmerged files: $err"
+       exit 1
+    }
+    while {[gets $fd line] >= 0} {
+       set i [string first "\t" $line]
+       if {$i < 0} continue
+       set fname [string range $line [expr {$i+1}] end]
+       if {[lsearch -exact $mlist $fname] >= 0} continue
+       incr nr_unmerged
+       if {$cmdline_files eq {} || [path_filter $cmdline_files $fname]} {
+           lappend mlist $fname
+       }
+    }
+    catch {close $fd}
+    if {$mlist eq {}} {
+       if {$nr_unmerged == 0} {
+           show_error {} . "No files selected: --merge specified but\
+                            no files are unmerged."
+       } else {
+           show_error {} . "No files selected: --merge specified but\
+                            no unmerged files are within file limit."
+       }
+       exit 1
+    }
+    set cmdline_files $mlist
+}
+
+set nullid "0000000000000000000000000000000000000000"
+set nullid2 "0000000000000000000000000000000000000001"
+
+set have_tk85 [expr {[package vcompare $tk_version "8.5"] >= 0}]
+
+set runq {}
+set history {}
+set historyindex 0
+set fh_serial 0
+set nhl_names {}
+set highlight_paths {}
+set findpattern {}
+set searchdirn -forwards
+set boldrows {}
+set boldnamerows {}
+set diffelide {0 0}
+set markingmatches 0
+set linkentercount 0
+set need_redisplay 0
+set nrows_drawn 0
+set firsttabstop 0
+
+set nextviewnum 1
+set curview 0
+set selectedview 0
+set selectedhlview None
+set highlight_related None
+set highlight_files {}
+set viewfiles(0) {}
+set viewperm(0) 0
+set viewargs(0) {}
+
+set cmdlineok 0
+set stopped 0
+set stuffsaved 0
+set patchnum 0
+set localirow -1
+set localfrow -1
+set lserial 0
+setcoords
+makewindow
+# wait for the window to become visible
+tkwait visibility .
+wm title . "[file tail $argv0]: [file tail [pwd]]"
+readrefs
+
+if {$cmdline_files ne {} || $revtreeargs ne {}} {
+    # create a view for the files/dirs specified on the command line
+    set curview 1
+    set selectedview 1
+    set nextviewnum 2
+    set viewname(1) "Command line"
+    set viewfiles(1) $cmdline_files
+    set viewargs(1) $revtreeargs
+    set viewperm(1) 0
+    addviewmenu 1
+    .bar.view entryconf Edit* -state normal
+    .bar.view entryconf Delete* -state normal
+}
+
+if {[info exists permviews]} {
+    foreach v $permviews {
+       set n $nextviewnum
+       incr nextviewnum
+       set viewname($n) [lindex $v 0]
+       set viewfiles($n) [lindex $v 1]
+       set viewargs($n) [lindex $v 2]
+       set viewperm($n) 1
+       addviewmenu $n
+    }
+}
+getcommits
diff --git a/gitweb/INSTALL b/gitweb/INSTALL
new file mode 100644 (file)
index 0000000..9cd5b0a
--- /dev/null
@@ -0,0 +1,225 @@
+GIT web Interface (gitweb) Installation
+=======================================
+
+First you have to generate gitweb.cgi from gitweb.perl using
+"make gitweb/gitweb.cgi", then copy appropriate files (gitweb.cgi,
+gitweb.css, git-logo.png and git-favicon.png) to their destination.
+For example if git was (or is) installed with /usr prefix, you can do
+
+       $ make prefix=/usr gitweb/gitweb.cgi  ;# as yourself
+       # cp gitweb/git* /var/www/cgi-bin/    ;# as root
+
+Alternatively you can use autoconf generated ./configure script to
+set up path to git binaries (via config.mak.autogen), so you can write
+instead
+
+       $ make configure                     ;# as yourself
+       $ ./configure --prefix=/usr          ;# as yourself
+       $ make gitweb/gitweb.cgi             ;# as yourself
+       # cp gitweb/git* /var/www/cgi-bin/   ;# as root
+
+The above example assumes that your web server is configured to run
+[executable] files in /var/www/cgi-bin/ as server scripts (as CGI
+scripts).
+
+
+Build time configuration
+------------------------
+
+See also "How to configure gitweb for your local system" in README
+file for gitweb (in gitweb/README).
+
+- There are many configuration variables which affects building of
+  gitweb.cgi; see "default configuration for gitweb" section in main
+  (top dir) Makefile, and instructions for building gitweb/gitweb.cgi
+  target.
+
+  One of most important is where to find git wrapper binary. Gitweb
+  tries to find git wrapper at $(bindir)/git, so you have to set $bindir
+  when building gitweb.cgi, or $prefix from which $bindir is derived. If
+  you build and install gitweb together with the rest of git suite,
+  there should be no problems. Otherwise, if git was for example
+  installed from a binary package, you have to set $prefix (or $bindir)
+  accordingly.
+
+- Another important issue is where are git repositories you want to make
+  available to gitweb. By default gitweb search for repositories under
+  /pub/git; if you want to have projects somewhere else, like /home/git,
+  use GITWEB_PROJECTROOT build configuration variable.
+
+  By default all git repositories under projectroot are visible and
+  available to gitweb. List of projects is generated by default by
+  scanning the projectroot directory for git repositories. This can be
+  changed (configured) as described in "Gitweb repositories" section
+  below.
+
+  Note that gitweb deals directly with object database, and does not
+  need working directory; the name of the project is the name of its
+  repository object database, usually projectname.git for bare
+  repositories. If you want to provide gitweb access to non-bare (live)
+  repository, you can make projectname.git symbolic link under
+  projectroot linking to projectname/.git (but it is just
+  a suggestion).
+
+- You can control where gitweb tries to find its main CSS style file,
+  its favicon and logo with GITWEB_CSS, GITWEB_FAVICON and GITWEB_LOGO
+  build configuration variables. By default gitweb tries to find them
+  in the same directory as gitweb.cgi script.
+
+Build example
+~~~~~~~~~~~~~
+
+- To install gitweb to /var/www/cgi-bin/gitweb/ when git wrapper
+  is installed at /usr/local/bin/git and the repositories (projects)
+  we want to display are under /home/local/scm, you can do
+
+       make GITWEB_PROJECTROOT="/home/local/scm" \
+            GITWEB_CSS="/gitweb/gitweb.css" \
+            GITWEB_LOGO="/gitweb/git-logo.png" \
+            GITWEB_FAVICON="/gitweb/git-favicon.png" \
+            bindir=/usr/local/bin \
+            gitweb/gitweb.cgi
+
+       cp -fv ~/git/gitweb/gitweb.{cgi,css} \
+              ~/git/gitweb/git-{favicon,logo}.png \
+            /var/www/cgi-bin/gitweb/
+
+
+Gitweb config file
+------------------
+
+See also "Runtime gitweb configuration" section in README file
+for gitweb (in gitweb/README).
+
+- You can configure gitweb further using gitweb configuration file;
+  by default it is file named gitweb_config.perl in the same place as
+  gitweb.cgi script. You can control default place for config file
+  using GITWEB_CONFIG build configuration variable, and you can set it
+  using GITWEB_CONFIG environmental variable.
+
+- Gitweb config file is [fragment] of perl code. You can set variables
+  using "our $variable = value"; text from "#" character until the end
+  of a line is ignored. See perlsyn(1) for details.
+
+  See the top of gitweb.perl file for examples of customizable options.
+
+Config file example
+~~~~~~~~~~~~~~~~~~~
+
+To enable blame, pickaxe search, and snapshot support, while allowing
+individual projects to turn them off, put the following in your
+GITWEB_CONFIG file:
+
+       $feature{'blame'}{'default'} = [1];
+       $feature{'blame'}{'override'} = 1;
+
+       $feature{'pickaxe'}{'default'} = [1];
+       $feature{'pickaxe'}{'override'} = 1;
+
+       $feature{'snapshot'}{'default'} = ['zip', 'tgz'];
+       $feature{'snapshot'}{'override'} = 1;
+
+
+Gitweb repositories
+-------------------
+
+- By default all git repositories under projectroot are visible and
+  available to gitweb. List of projects is generated by default by
+  scanning the projectroot directory for git repositories (for object
+  databases to be more exact).
+
+  You can provide pre-generated list of [visible] repositories,
+  together with information about their owners (the project ownership
+  is taken from owner of repository directory otherwise), by setting
+  GITWEB_LIST build configuration variable (or $projects_list variable
+  in gitweb config file) to point to a plain file.
+
+  Each line of projects list file should consist of url-encoded path
+  to project repository database (relative to projectroot) separated
+  by space from url-encoded project owner; spaces in both project path
+  and project owner have to be encoded as either '%20' or '+'.
+
+  You can generate projects list index file using project_index action
+  (the 'TXT' link on projects list page) directly from gitweb.
+
+- By default even if project is not visible on projects list page, you
+  can view it nevertheless by hand-crafting gitweb URL. You can set
+  GITWEB_STRICT_EXPORT build configuration variable (or $strict_export
+  variable in gitweb config file) to only allow viewing of
+  repositories also shown on the overview page.
+
+- Alternatively, you can configure gitweb to only list and allow
+  viewing of the explicitly exported repositories, via
+  GITWEB_EXPORT_OK build configuration variable (or $export_ok
+  variable in gitweb config file). If it evaluates to true, gitweb
+  show repository only if this file exists in its object database
+  (if directory has the magic file $export_ok).
+
+Generating projects list using gitweb
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We assume that GITWEB_CONFIG has its default Makefile value, namely
+gitweb_config.perl. Put the following in gitweb_make_index.perl file:
+
+       $GITWEB_CONFIG = "gitweb_config.perl";
+       do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
+
+       $projects_list = $projectroot;
+
+Then create the following script to get list of project in the format
+suitable for GITWEB_LIST build configuration variable (or
+$projects_list variable in gitweb config):
+
+       #!/bin/sh
+
+       export GITWEB_CONFIG="gitweb_make_index.perl"
+       export GATEWAY_INTERFACE="CGI/1.1"
+       export HTTP_ACCEPT="*/*"
+       export REQUEST_METHOD="GET"
+       export QUERY_STRING="a=project_index"
+
+       perl -- /var/www/cgi-bin/gitweb.cgi
+
+
+Requirements
+------------
+
+ - Core git tools
+ - Perl
+ - Perl modules: CGI, Encode, Fcntl, File::Find, File::Basename.
+ - web server
+
+
+Example web server configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+See also "Webserver configuration" section in README file for gitweb
+(in gitweb/README).
+
+
+- Apache2, gitweb installed as CGI script,
+  under /var/www/cgi-bin/
+
+       ScriptAlias /cgi-bin/ "/var/www/cgi-bin/"
+
+       <Directory "/var/www/cgi-bin">
+           Options Indexes FollowSymlinks ExecCGI
+           AllowOverride None
+           Order allow,deny
+           Allow from all
+       </Directory>
+
+- Apache2, gitweb installed as mod_perl legacy script,
+  under /var/www/perl/
+
+       Alias /perl "/var/www/perl"
+
+       <Directory "/var/www/perl">
+           SetHandler perl-script
+           PerlResponseHandler ModPerl::Registry
+           PerlOptions +ParseHeaders
+           Options Indexes FollowSymlinks +ExecCGI
+           AllowOverride None
+           Order allow,deny
+           Allow from all
+       </Directory>
diff --git a/gitweb/README b/gitweb/README
new file mode 100644 (file)
index 0000000..b28f59f
--- /dev/null
@@ -0,0 +1,260 @@
+GIT web Interface
+=================
+
+The one working on:
+  http://www.kernel.org/git/
+
+From the git version 1.4.0 gitweb is bundled with git.
+
+
+How to configure gitweb for your local system
+---------------------------------------------
+
+See also "Build time configuration" section in INSTALL
+file for gitweb (in gitweb/INSTALL).
+
+You can specify the following configuration variables when building GIT:
+ * GIT_BINDIR
+   Points out where to find git executable.  You should set up it to
+   the place where git binary was installed (usually /usr/bin) if you
+   don't install git from sources together with gitweb.  [Default: $(bindir)]
+ * GITWEB_SITENAME
+   Shown in the title of all generated pages, defaults to the server name
+   (SERVER_NAME CGI environment variable) if not set. [No default]
+ * GITWEB_PROJECTROOT
+   The root directory for all projects shown by gitweb. Must be set
+   correctly for gitweb to find repositories to display.  See also
+   "Gitweb repositories" in INSTALL file for gitweb.  [Default: /pub/git]
+ * GITWEB_PROJECT_MAXDEPTH
+   The filesystem traversing limit for getting projects list; the number
+   is taken as depth relative to the projectroot.  It is used when
+   GITWEB_LIST is a directory (or is not set; then project root is used).
+   Is is meant to speed up project listing on large work trees by limiting
+   find depth.  [Default: 2007]
+ * GITWEB_LIST
+   Points to a directory to scan for projects (defaults to project root
+   if not set / if empty) or to a file with explicit listing of projects
+   (together with projects' ownership). See "Generating projects list
+   using gitweb" in INSTALL file for gitweb to find out how to generate
+   such file from scan of a directory. [No default, which means use root
+   directory for projects]
+ * GITWEB_EXPORT_OK
+   Show repository only if this file exists (in repository).  Only
+   effective if this variable evaluates to true.  [No default / Not set]
+ * GITWEB_STRICT_EXPORT
+   Only allow viewing of repositories also shown on the overview page.
+   This for example makes GITWEB_EXPORT_OK to decide if repository is
+   available and not only if it is shown.  If GITWEB_LIST points to
+   file with list of project, only those repositories listed would be
+   available for gitweb.  [No default]
+ * GITWEB_HOMETEXT
+   Points to an .html file which is included on the gitweb project
+   overview page ('projects_list' view), if it exists.  Relative to
+   gitweb.cgi script.  [Default: indextext.html]
+ * GITWEB_SITE_HEADER
+   Filename of html text to include at top of each page.  Relative to
+   gitweb.cgi script.  [No default]
+ * GITWEB_SITE_FOOTER
+   Filename of html text to include at bottom of each page.  Relative to
+   gitweb.cgi script.  [No default]
+ * GITWEB_HOME_LINK_STR
+   String of the home link on top of all pages, leading to $home_link
+   (usually main gitweb page, which means projects list).  Used as first
+   part of gitweb view "breadcrumb trail": <home> / <project> / <view>.
+   [Default: projects]
+ * GITWEB_SITENAME
+   Name of your site or organization to appear in page titles.  Set it
+   to something descriptive for clearer bookmarks etc.  If not set
+   (if empty) gitweb uses "$SERVER_NAME Git", or "Untitled Git" if
+   SERVER_NAME CGI environment variable is not set (e.g. if running
+   gitweb as standalone script).  [No default]
+ * GITWEB_BASE_URL
+   Git base URLs used for URL to where fetch project from, i.e. full
+   URL is "$git_base_url/$project".  Shown on projects summary page.
+   Repository URL for project can be also configured per repository; this
+   takes precendence over URL composed from base URL and project name.
+   Note that you can setup multiple base URLs (for example one for
+   git:// protocol access, one for http:// access) from gitweb config
+   file.  [No default]
+ * GITWEB_CSS
+   Points to the location where you put gitweb.css on your web server
+   (or to be more generic URI of gitweb stylesheet).  Relative to base
+   URI of gitweb.  Note that you can setup multiple stylesheets from
+   gitweb config file.  [Default: gitweb.css]
+ * GITWEB_LOGO
+   Points to the location where you put git-logo.png on your web server
+   (or to be more generic URI of logo, 72x27 size, displayed in top right
+   corner of each gitweb page, and used as logo for Atom feed).  Relative
+   to base URI of gitweb.  [Default: git-logo.png]
+ * GITWEB_FAVICON
+   Points to the location where you put git-favicon.png on your web server
+   (or to be more generic URI of favicon, assumed to be image/png type;
+   web browsers that support favicons (website icons) may display them
+   in the browser's URL bar and next to site name in bookmarks).  Relative
+   to base URI of gitweb.  [Default: git-favicon.png]
+ * GITWEB_CONFIG
+   This Perl file will be loaded using 'do' and can be used to override any
+   of the options above as well as some other options -- see the "Runtime
+   gitweb configuration" section below, and top of 'gitweb.cgi' for their
+   full list and description.  If the environment variable GITWEB_CONFIG
+   is set when gitweb.cgi is executed, then the file specified in the
+   environment variable will be loaded instead of the file specified
+   when gitweb.cgi was created.  [Default: gitweb_config.perl]
+
+
+Runtime gitweb configuration
+----------------------------
+
+You can adjust gitweb behaviour using the file specified in `GITWEB_CONFIG`
+(defaults to 'gitweb_config.perl' in the same directory as the CGI).
+The most notable thing that is not configurable at compile time are the
+optional features, stored in the '%features' variable.
+
+Ultimate description on how to reconfigure the default features setting
+in your `GITWEB_CONFIG` or per-project in `project.git/config` can be found
+as comments inside 'gitweb.cgi'.
+
+See also "Gitweb config file" (with example of gitweb config file), and
+"Gitweb repositories" sections in INSTALL file for gitweb.
+
+
+Gitweb config file is [fragment] of perl code. You can set variables
+using "our $variable = value"; text from "#" character until the end
+of a line is ignored. See perlsyn(1) man page for details.
+
+Below there is list of vaiables which you might want to set in gitweb config.
+See the top of 'gitweb.cgi' for the full list of variables and their
+descriptions.
+
+Gitweb config file variables
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can set, among others,  the following variables in gitweb config files:
+ * $GIT
+   Cure git executable to use.  By default set to "$GIT_BINDIR/git", which
+   in turn is by default set to "$(bindir)/git".  If you use git from binary
+   package, set this to "/usr/bin/git".  This can just be "git" if your
+   webserver has a sensible PATH.  If you have multiple git versions
+   installed it is / can be used to choose which one to use.
+ * $version
+   Gitweb version, set automatically when creating gitweb.cgi from
+   gitweb.perl. You might want to modify it if you are running modified
+   gitweb.
+ * $my_url, $my_uri
+   URL and absolute URL of gitweb script; you might need to set those
+   variables if you are using 'pathinfo' feature: see also below.
+ * $home_link
+   Target of the home link on top of all pages (the first part of view
+   "breadcrumbs").  By default set to absolute URI of a page; you might
+   need to set it up to [base] gitweb URI if you use 'pathinfo' feature
+   (alternative format of the URLs, with project name embedded directly
+   in the path part of URL).
+ * @stylesheets
+   List of URIs of stylesheets (relative to base URI of a page). You
+   might specify more than one stylesheet, for example use gitweb.css
+   as base, with site specific modifications in separate stylesheet
+   to make it easier to upgrade gitweb. You can add 'site' stylesheet
+   for example by using
+      push @stylesheets, "gitweb-site.css";
+   in gitweb config file.
+ * $logo_url, $logo_label
+   URI and label (title) of GIT logo link (or your site logo, if you choose
+   to use different logo image). By default they point to git homepage;
+   in the past they pointed to git documentation at www.kernel.org.
+ * $projects_list_description_width
+   The width (in characters) of the projects list "Description" column.
+   Longer descriptions will be cut (trying to cut at word boundary);
+   full description is available as 'title' attribute (usually shown on
+   mouseover).  By default set to 25, which might be too small if you
+   use long project descriptions.
+ * @git_base_url_list
+   List of git base URLs used for URL to where fetch project from, shown
+   in project summary page.  Full URL is "$git_base_url/$project".
+   You can setup multiple base URLs (for example one for  git:// protocol
+   access, and one for http:// "dumb" protocol access).  Note that per
+   repository configuration in 'cloneurl' file, or as values of gitweb.url
+   project config.
+ * $default_blob_plain_mimetype
+   Default mimetype for blob_plain (raw) view, if mimetype checking
+   doesn't result in some other type; by default 'text/plain'.
+ * $default_text_plain_charset
+   Default charset for text files. If not set, web serwer configuration
+   would be used.
+ * $mimetypes_file
+   File to use for (filename extension based) guessing of MIME types before
+   trying /etc/mime.types. Path, if relative, is taken currently as taken
+   relative to current git repositoy.
+ * $fallback_encoding
+   Gitweb assumes this charset if line contains non-UTF-8 characters.
+   Fallback decoding is used without error checking, so it can be even
+   'utf-8'. Value mist be valid encodig; see Encoding::Supported(3pm) man
+   page for a list.   By default 'latin1', aka. 'iso-8859-1'.
+ * @diff_opts
+   Rename detection options for git-diff and git-diff-tree. By default
+   ('-M'); set it to ('-C') or ('-C', '-C') to also detect copies, or
+   set it to () if you don't want to have renames detection.
+
+Per-repository gitweb configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also configure individual repositories shown in gitweb by creating
+file in the GIT_DIR of git repository, or by setting some repo configuration
+variable (in GIT_DIR/config).
+
+You can use the following files in repository:
+ * README.html
+   A .html file (HTML fragment) which is included on the gitweb project
+   summary page inside <div> block element. You can use it for longer
+   description of a project, to provide links for example to projects
+   homepage, etc.
+ * description (or gitweb.description)
+   Short (shortened by default to 25 characters in the projects list page)
+   single line description of a project (of a repository). Plain text file;
+   HTML will be escaped. By default set to
+     Unnamed repository; edit this file to name it for gitweb.
+   from the template during creating repository. You can use
+   gitweb.description repo configuration variable, but the file takes
+   precendence.
+ * cloneurl (or multiple-valued gitweb.url)
+   File with repository URL (used for clone and fetch), one per line.
+   Displayed in the project summary page. You can use multiple-valued
+   gitweb.url repository configuration variable for that, but the file
+   takes precendence.
+ * various gitweb.* config variables (in config)
+   Read description of %feature hash for detailed list, and some
+   descriptions.
+
+
+Webserver configuration
+-----------------------
+
+If you want to have one URL for both gitweb and your http://
+repositories, you can configure apache like this:
+
+<VirtualHost www:80>
+    ServerName git.domain.org
+    DocumentRoot /pub/git
+    RewriteEngine on
+    RewriteRule ^/(.*\.git/(?!/?(info|objects|refs)).*)?$ /cgi-bin/gitweb.cgi%{REQUEST_URI}  [L,PT]
+    SetEnv     GITWEB_CONFIG   /etc/gitweb.conf
+</VirtualHost>
+
+The above configuration expects your public repositories to live under
+/pub/git and will serve them as http://git.domain.org/dir-under-pub-git,
+both as cloneable GIT URL and as browseable gitweb interface.
+If you then start your git-daemon with --base-path=/pub/git --export-all
+then you can even use the git:// URL with exactly the same path.
+
+Setting the environment variable GITWEB_CONFIG will tell gitweb to use
+the named file (i.e. in this example /etc/gitweb.conf) as a
+configuration for gitweb.  Perl variables defined in here will
+override the defaults given at the head of the gitweb.perl (or
+gitweb.cgi).  Look at the comments in that file for information on
+which variables and what they mean.
+
+
+Originally written by:
+  Kay Sievers <kay.sievers@vrfy.org>
+
+Any comment/question/concern to:
+  Git mailing list <git@vger.kernel.org>
diff --git a/gitweb/git-favicon.png b/gitweb/git-favicon.png
new file mode 100644 (file)
index 0000000..de637c0
Binary files /dev/null and b/gitweb/git-favicon.png differ
diff --git a/gitweb/git-logo.png b/gitweb/git-logo.png
new file mode 100644 (file)
index 0000000..16ae8d5
Binary files /dev/null and b/gitweb/git-logo.png differ
diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css
new file mode 100644 (file)
index 0000000..446a1c3
--- /dev/null
@@ -0,0 +1,501 @@
+body {
+       font-family: sans-serif;
+       font-size: small;
+       border: solid #d9d8d1;
+       border-width: 1px;
+       margin: 10px;
+       background-color: #ffffff;
+       color: #000000;
+}
+
+a {
+       color: #0000cc;
+}
+
+a:hover, a:visited, a:active {
+       color: #880000;
+}
+
+span.cntrl {
+       border: dashed #aaaaaa;
+       border-width: 1px;
+       padding: 0px 2px 0px 2px;
+       margin:  0px 2px 0px 2px;
+}
+
+img.logo {
+       float: right;
+       border-width: 0px;
+}
+
+div.page_header {
+       height: 25px;
+       padding: 8px;
+       font-size: 150%;
+       font-weight: bold;
+       background-color: #d9d8d1;
+}
+
+div.page_header a:visited, a.header {
+       color: #0000cc;
+}
+
+div.page_header a:hover {
+       color: #880000;
+}
+
+div.page_nav {
+       padding: 8px;
+}
+
+div.page_nav a:visited {
+       color: #0000cc;
+}
+
+div.page_path {
+       padding: 8px;
+       font-weight: bold;
+       border: solid #d9d8d1;
+       border-width: 0px 0px 1px;
+}
+
+div.page_footer {
+       height: 17px;
+       padding: 4px 8px;
+       background-color: #d9d8d1;
+}
+
+div.page_footer_text {
+       float: left;
+       color: #555555;
+       font-style: italic;
+}
+
+div.page_body {
+       padding: 8px;
+       font-family: monospace;
+}
+
+div.title, a.title {
+       display: block;
+       padding: 6px 8px;
+       font-weight: bold;
+       background-color: #edece6;
+       text-decoration: none;
+       color: #000000;
+}
+
+div.readme {
+       padding: 8px;
+}
+
+a.title:hover {
+       background-color: #d9d8d1;
+}
+
+div.title_text {
+       padding: 6px 0px;
+       border: solid #d9d8d1;
+       border-width: 0px 0px 1px;
+       font-family: monospace;
+}
+
+div.log_body {
+       padding: 8px 8px 8px 150px;
+}
+
+span.age {
+       position: relative;
+       float: left;
+       width: 142px;
+       font-style: italic;
+}
+
+span.signoff {
+       color: #888888;
+}
+
+div.log_link {
+       padding: 0px 8px;
+       font-size: 70%;
+       font-family: sans-serif;
+       font-style: normal;
+       position: relative;
+       float: left;
+       width: 136px;
+}
+
+div.list_head {
+       padding: 6px 8px 4px;
+       border: solid #d9d8d1;
+       border-width: 1px 0px 0px;
+       font-style: italic;
+}
+
+div.author_date {
+       padding: 8px;
+       border: solid #d9d8d1;
+       border-width: 0px 0px 1px 0px;
+       font-style: italic;
+}
+
+a.list {
+       text-decoration: none;
+       color: #000000;
+}
+
+a.subject, a.name {
+       font-weight: bold;
+}
+
+table.tags a.subject {
+       font-weight: normal;
+}
+
+a.list:hover {
+       text-decoration: underline;
+       color: #880000;
+}
+
+a.text {
+       text-decoration: none;
+       color: #0000cc;
+}
+
+a.text:visited {
+       text-decoration: none;
+       color: #880000;
+}
+
+a.text:hover {
+       text-decoration: underline;
+       color: #880000;
+}
+
+table {
+       padding: 8px 4px;
+       border-spacing: 0;
+}
+
+table.diff_tree {
+       font-family: monospace;
+}
+
+table.combined.diff_tree th {
+       text-align: center;
+}
+
+table.combined.diff_tree td {
+       padding-right: 24px;
+}
+
+table.combined.diff_tree th.link,
+table.combined.diff_tree td.link {
+       padding: 0px 2px;
+}
+
+table.combined.diff_tree td.nochange a {
+       color: #6666ff;
+}
+
+table.combined.diff_tree td.nochange a:hover,
+table.combined.diff_tree td.nochange a:visited {
+       color: #d06666;
+}
+
+table.blame {
+       border-collapse: collapse;
+}
+
+table.blame td {
+       padding: 0px 5px;
+       font-size: 100%;
+       vertical-align: top;
+}
+
+th {
+       padding: 2px 5px;
+       font-size: 100%;
+       text-align: left;
+}
+
+tr.light:hover {
+       background-color: #edece6;
+}
+
+tr.dark {
+       background-color: #f6f6f0;
+}
+
+tr.dark2 {
+       background-color: #f6f6f0;
+}
+
+tr.dark:hover {
+       background-color: #edece6;
+}
+
+td {
+       padding: 2px 5px;
+       font-size: 100%;
+       vertical-align: top;
+}
+
+td.link, td.selflink {
+       padding: 2px 5px;
+       font-family: sans-serif;
+       font-size: 70%;
+}
+
+td.selflink {
+       padding-right: 0px;
+}
+
+td.sha1 {
+       font-family: monospace;
+}
+
+td.error {
+       color: red;
+       background-color: yellow;
+}
+
+td.current_head {
+       text-decoration: underline;
+}
+
+table.diff_tree span.file_status.new {
+       color: #008000;
+}
+
+table.diff_tree span.file_status.deleted {
+       color: #c00000;
+}
+
+table.diff_tree span.file_status.moved,
+table.diff_tree span.file_status.mode_chnge {
+       color: #777777;
+}
+
+table.diff_tree span.file_status.copied {
+  color: #70a070;
+}
+
+/* noage: "No commits" */
+table.project_list td.noage {
+       color: #808080;
+       font-style: italic;
+}
+
+/* age2: 60*60*24*2 <= age */
+table.project_list td.age2, table.blame td.age2 {
+       font-style: italic;
+}
+
+/* age1: 60*60*2 <= age < 60*60*24*2 */
+table.project_list td.age1 {
+       color: #009900;
+       font-style: italic;
+}
+
+table.blame td.age1 {
+       color: #009900;
+       background: transparent;
+}
+
+/* age0: age < 60*60*2 */
+table.project_list td.age0 {
+       color: #009900;
+       font-style: italic;
+       font-weight: bold;
+}
+
+table.blame td.age0 {
+       color: #009900;
+       background: transparent;
+       font-weight: bold;
+}
+
+td.pre, div.pre, div.diff {
+       font-family: monospace;
+       font-size: 12px;
+       white-space: pre;
+}
+
+td.mode {
+       font-family: monospace;
+}
+
+/* styling of diffs (patchsets): commitdiff and blobdiff views */
+div.diff.header,
+div.diff.extended_header {
+       white-space: normal;
+}
+
+div.diff.header {
+       font-weight: bold;
+
+       background-color: #edece6;
+
+       margin-top: 4px;
+       padding: 4px 0px 2px 0px;
+       border: solid #d9d8d1;
+       border-width: 1px 0px 1px 0px;
+}
+
+div.diff.header a.path {
+       text-decoration: underline;
+}
+
+div.diff.extended_header,
+div.diff.extended_header a.path,
+div.diff.extended_header a.hash {
+       color: #777777;
+}
+
+div.diff.extended_header .info {
+       color: #b0b0b0;
+}
+
+div.diff.extended_header {
+       background-color: #f6f5ee;
+       padding: 2px 0px 2px 0px;
+}
+
+div.diff a.list,
+div.diff a.path,
+div.diff a.hash {
+       text-decoration: none;
+}
+
+div.diff a.list:hover,
+div.diff a.path:hover,
+div.diff a.hash:hover {
+       text-decoration: underline;
+}
+
+div.diff.to_file a.path,
+div.diff.to_file {
+       color: #007000;
+}
+
+div.diff.add {
+       color: #008800;
+}
+
+div.diff.from_file a.path,
+div.diff.from_file {
+       color: #aa0000;
+}
+
+div.diff.rem {
+       color: #cc0000;
+}
+
+div.diff.chunk_header a,
+div.diff.chunk_header {
+       color: #990099;
+}
+
+div.diff.chunk_header {
+       border: dotted #ffe0ff;
+       border-width: 1px 0px 0px 0px;
+       margin-top: 2px;
+}
+
+div.diff.chunk_header span.chunk_info {
+       background-color: #ffeeff;
+}
+
+div.diff.chunk_header span.section {
+       color: #aa22aa;
+}
+
+div.diff.incomplete {
+       color: #cccccc;
+}
+
+div.diff.nodifferences {
+       font-weight: bold;
+       color: #600000;
+}
+
+div.index_include {
+       border: solid #d9d8d1;
+       border-width: 0px 0px 1px;
+       padding: 12px 8px;
+}
+
+div.search {
+       font-size: 100%;
+       font-weight: normal;
+       margin: 4px 8px;
+       float: right;
+       top: 56px;
+       right: 12px
+}
+
+td.linenr {
+       text-align: right;
+}
+
+a.linenr {
+       color: #999999;
+       text-decoration: none
+}
+
+a.rss_logo {
+       float: right;
+       padding: 3px 0px;
+       width: 35px;
+       line-height: 10px;
+       border: 1px solid;
+       border-color: #fcc7a5 #7d3302 #3e1a01 #ff954e;
+       color: #ffffff;
+       background-color: #ff6600;
+       font-weight: bold;
+       font-family: sans-serif;
+       font-size: 70%;
+       text-align: center;
+       text-decoration: none;
+}
+
+a.rss_logo:hover {
+       background-color: #ee5500;
+}
+
+span.refs span {
+       padding: 0px 4px;
+       font-size: 70%;
+       font-weight: normal;
+       border: 1px solid;
+       background-color: #ffaaff;
+       border-color: #ffccff #ff00ee #ff00ee #ffccff;
+}
+
+span.refs span.ref {
+       background-color: #aaaaff;
+       border-color: #ccccff #0033cc #0033cc #ccccff;
+}
+
+span.refs span.tag {
+       background-color: #ffffaa;
+       border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
+}
+
+span.refs span.head {
+       background-color: #aaffaa;
+       border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
+}
+
+span.atnight {
+       color: #cc0000;
+}
+
+span.match {
+       color: #e00000;
+}
+
+div.binary {
+       font-style: italic;
+}
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
new file mode 100755 (executable)
index 0000000..24b3158
--- /dev/null
@@ -0,0 +1,5699 @@
+#!/usr/bin/perl
+
+# gitweb - simple web interface to track changes in git repositories
+#
+# (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
+# (C) 2005, Christian Gierke
+#
+# This program is licensed under the GPLv2
+
+use strict;
+use warnings;
+use CGI qw(:standard :escapeHTML -nosticky);
+use CGI::Util qw(unescape);
+use CGI::Carp qw(fatalsToBrowser);
+use Encode;
+use Fcntl ':mode';
+use File::Find qw();
+use File::Basename qw(basename);
+binmode STDOUT, ':utf8';
+
+BEGIN {
+       CGI->compile() if $ENV{'MOD_PERL'};
+}
+
+our $cgi = new CGI;
+our $version = "++GIT_VERSION++";
+our $my_url = $cgi->url();
+our $my_uri = $cgi->url(-absolute => 1);
+
+# core git executable to use
+# this can just be "git" if your webserver has a sensible PATH
+our $GIT = "++GIT_BINDIR++/git";
+
+# absolute fs-path which will be prepended to the project path
+#our $projectroot = "/pub/scm";
+our $projectroot = "++GITWEB_PROJECTROOT++";
+
+# fs traversing limit for getting project list
+# the number is relative to the projectroot
+our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
+
+# target of the home link on top of all pages
+our $home_link = $my_uri || "/";
+
+# string of the home link on top of all pages
+our $home_link_str = "++GITWEB_HOME_LINK_STR++";
+
+# name of your site or organization to appear in page titles
+# replace this with something more descriptive for clearer bookmarks
+our $site_name = "++GITWEB_SITENAME++"
+                 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
+
+# filename of html text to include at top of each page
+our $site_header = "++GITWEB_SITE_HEADER++";
+# html text to include at home page
+our $home_text = "++GITWEB_HOMETEXT++";
+# filename of html text to include at bottom of each page
+our $site_footer = "++GITWEB_SITE_FOOTER++";
+
+# URI of stylesheets
+our @stylesheets = ("++GITWEB_CSS++");
+# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
+our $stylesheet = undef;
+# URI of GIT logo (72x27 size)
+our $logo = "++GITWEB_LOGO++";
+# URI of GIT favicon, assumed to be image/png type
+our $favicon = "++GITWEB_FAVICON++";
+
+# URI and label (title) of GIT logo link
+#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
+#our $logo_label = "git documentation";
+our $logo_url = "http://git.or.cz/";
+our $logo_label = "git homepage";
+
+# source of projects list
+our $projects_list = "++GITWEB_LIST++";
+
+# the width (in characters) of the projects list "Description" column
+our $projects_list_description_width = 25;
+
+# default order of projects list
+# valid values are none, project, descr, owner, and age
+our $default_projects_order = "project";
+
+# show repository only if this file exists
+# (only effective if this variable evaluates to true)
+our $export_ok = "++GITWEB_EXPORT_OK++";
+
+# only allow viewing of repositories also shown on the overview page
+our $strict_export = "++GITWEB_STRICT_EXPORT++";
+
+# list of git base URLs used for URL to where fetch project from,
+# i.e. full URL is "$git_base_url/$project"
+our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
+
+# default blob_plain mimetype and default charset for text/plain blob
+our $default_blob_plain_mimetype = 'text/plain';
+our $default_text_plain_charset  = undef;
+
+# file to use for guessing MIME types before trying /etc/mime.types
+# (relative to the current git repository)
+our $mimetypes_file = undef;
+
+# assume this charset if line contains non-UTF-8 characters;
+# it should be valid encoding (see Encoding::Supported(3pm) for list),
+# for which encoding all byte sequences are valid, for example
+# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
+# could be even 'utf-8' for the old behavior)
+our $fallback_encoding = 'latin1';
+
+# rename detection options for git-diff and git-diff-tree
+# - default is '-M', with the cost proportional to
+#   (number of removed files) * (number of new files).
+# - more costly is '-C' (which implies '-M'), with the cost proportional to
+#   (number of changed files + number of removed files) * (number of new files)
+# - even more costly is '-C', '--find-copies-harder' with cost
+#   (number of files in the original tree) * (number of new files)
+# - one might want to include '-B' option, e.g. '-B', '-M'
+our @diff_opts = ('-M'); # taken from git_commit
+
+# information about snapshot formats that gitweb is capable of serving
+our %known_snapshot_formats = (
+       # name => {
+       #       'display' => display name,
+       #       'type' => mime type,
+       #       'suffix' => filename suffix,
+       #       'format' => --format for git-archive,
+       #       'compressor' => [compressor command and arguments]
+       #                       (array reference, optional)}
+       #
+       'tgz' => {
+               'display' => 'tar.gz',
+               'type' => 'application/x-gzip',
+               'suffix' => '.tar.gz',
+               'format' => 'tar',
+               'compressor' => ['gzip']},
+
+       'tbz2' => {
+               'display' => 'tar.bz2',
+               'type' => 'application/x-bzip2',
+               'suffix' => '.tar.bz2',
+               'format' => 'tar',
+               'compressor' => ['bzip2']},
+
+       'zip' => {
+               'display' => 'zip',
+               'type' => 'application/x-zip',
+               'suffix' => '.zip',
+               'format' => 'zip'},
+);
+
+# Aliases so we understand old gitweb.snapshot values in repository
+# configuration.
+our %known_snapshot_format_aliases = (
+       'gzip'  => 'tgz',
+       'bzip2' => 'tbz2',
+
+       # backward compatibility: legacy gitweb config support
+       'x-gzip' => undef, 'gz' => undef,
+       'x-bzip2' => undef, 'bz2' => undef,
+       'x-zip' => undef, '' => undef,
+);
+
+# You define site-wide feature defaults here; override them with
+# $GITWEB_CONFIG as necessary.
+our %feature = (
+       # feature => {
+       #       'sub' => feature-sub (subroutine),
+       #       'override' => allow-override (boolean),
+       #       'default' => [ default options...] (array reference)}
+       #
+       # if feature is overridable (it means that allow-override has true value),
+       # then feature-sub will be called with default options as parameters;
+       # return value of feature-sub indicates if to enable specified feature
+       #
+       # if there is no 'sub' key (no feature-sub), then feature cannot be
+       # overriden
+       #
+       # use gitweb_check_feature(<feature>) to check if <feature> is enabled
+
+       # Enable the 'blame' blob view, showing the last commit that modified
+       # each line in the file. This can be very CPU-intensive.
+
+       # To enable system wide have in $GITWEB_CONFIG
+       # $feature{'blame'}{'default'} = [1];
+       # To have project specific config enable override in $GITWEB_CONFIG
+       # $feature{'blame'}{'override'} = 1;
+       # and in project config gitweb.blame = 0|1;
+       'blame' => {
+               'sub' => \&feature_blame,
+               'override' => 0,
+               'default' => [0]},
+
+       # Enable the 'snapshot' link, providing a compressed archive of any
+       # tree. This can potentially generate high traffic if you have large
+       # project.
+
+       # Value is a list of formats defined in %known_snapshot_formats that
+       # you wish to offer.
+       # To disable system wide have in $GITWEB_CONFIG
+       # $feature{'snapshot'}{'default'} = [];
+       # To have project specific config enable override in $GITWEB_CONFIG
+       # $feature{'snapshot'}{'override'} = 1;
+       # and in project config, a comma-separated list of formats or "none"
+       # to disable.  Example: gitweb.snapshot = tbz2,zip;
+       'snapshot' => {
+               'sub' => \&feature_snapshot,
+               'override' => 0,
+               'default' => ['tgz']},
+
+       # Enable text search, which will list the commits which match author,
+       # committer or commit text to a given string.  Enabled by default.
+       # Project specific override is not supported.
+       'search' => {
+               'override' => 0,
+               'default' => [1]},
+
+       # Enable grep search, which will list the files in currently selected
+       # tree containing the given string. Enabled by default. This can be
+       # potentially CPU-intensive, of course.
+
+       # To enable system wide have in $GITWEB_CONFIG
+       # $feature{'grep'}{'default'} = [1];
+       # To have project specific config enable override in $GITWEB_CONFIG
+       # $feature{'grep'}{'override'} = 1;
+       # and in project config gitweb.grep = 0|1;
+       'grep' => {
+               'override' => 0,
+               'default' => [1]},
+
+       # Enable the pickaxe search, which will list the commits that modified
+       # a given string in a file. This can be practical and quite faster
+       # alternative to 'blame', but still potentially CPU-intensive.
+
+       # To enable system wide have in $GITWEB_CONFIG
+       # $feature{'pickaxe'}{'default'} = [1];
+       # To have project specific config enable override in $GITWEB_CONFIG
+       # $feature{'pickaxe'}{'override'} = 1;
+       # and in project config gitweb.pickaxe = 0|1;
+       'pickaxe' => {
+               'sub' => \&feature_pickaxe,
+               'override' => 0,
+               'default' => [1]},
+
+       # Make gitweb use an alternative format of the URLs which can be
+       # more readable and natural-looking: project name is embedded
+       # directly in the path and the query string contains other
+       # auxiliary information. All gitweb installations recognize
+       # URL in either format; this configures in which formats gitweb
+       # generates links.
+
+       # To enable system wide have in $GITWEB_CONFIG
+       # $feature{'pathinfo'}{'default'} = [1];
+       # Project specific override is not supported.
+
+       # Note that you will need to change the default location of CSS,
+       # favicon, logo and possibly other files to an absolute URL. Also,
+       # if gitweb.cgi serves as your indexfile, you will need to force
+       # $my_uri to contain the script name in your $GITWEB_CONFIG.
+       'pathinfo' => {
+               'override' => 0,
+               'default' => [0]},
+
+       # Make gitweb consider projects in project root subdirectories
+       # to be forks of existing projects. Given project $projname.git,
+       # projects matching $projname/*.git will not be shown in the main
+       # projects list, instead a '+' mark will be added to $projname
+       # there and a 'forks' view will be enabled for the project, listing
+       # all the forks. If project list is taken from a file, forks have
+       # to be listed after the main project.
+
+       # To enable system wide have in $GITWEB_CONFIG
+       # $feature{'forks'}{'default'} = [1];
+       # Project specific override is not supported.
+       'forks' => {
+               'override' => 0,
+               'default' => [0]},
+);
+
+sub gitweb_check_feature {
+       my ($name) = @_;
+       return unless exists $feature{$name};
+       my ($sub, $override, @defaults) = (
+               $feature{$name}{'sub'},
+               $feature{$name}{'override'},
+               @{$feature{$name}{'default'}});
+       if (!$override) { return @defaults; }
+       if (!defined $sub) {
+               warn "feature $name is not overrideable";
+               return @defaults;
+       }
+       return $sub->(@defaults);
+}
+
+sub feature_blame {
+       my ($val) = git_get_project_config('blame', '--bool');
+
+       if ($val eq 'true') {
+               return 1;
+       } elsif ($val eq 'false') {
+               return 0;
+       }
+
+       return $_[0];
+}
+
+sub feature_snapshot {
+       my (@fmts) = @_;
+
+       my ($val) = git_get_project_config('snapshot');
+
+       if ($val) {
+               @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
+       }
+
+       return @fmts;
+}
+
+sub feature_grep {
+       my ($val) = git_get_project_config('grep', '--bool');
+
+       if ($val eq 'true') {
+               return (1);
+       } elsif ($val eq 'false') {
+               return (0);
+       }
+
+       return ($_[0]);
+}
+
+sub feature_pickaxe {
+       my ($val) = git_get_project_config('pickaxe', '--bool');
+
+       if ($val eq 'true') {
+               return (1);
+       } elsif ($val eq 'false') {
+               return (0);
+       }
+
+       return ($_[0]);
+}
+
+# checking HEAD file with -e is fragile if the repository was
+# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
+# and then pruned.
+sub check_head_link {
+       my ($dir) = @_;
+       my $headfile = "$dir/HEAD";
+       return ((-e $headfile) ||
+               (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
+}
+
+sub check_export_ok {
+       my ($dir) = @_;
+       return (check_head_link($dir) &&
+               (!$export_ok || -e "$dir/$export_ok"));
+}
+
+# process alternate names for backward compatibility
+# filter out unsupported (unknown) snapshot formats
+sub filter_snapshot_fmts {
+       my @fmts = @_;
+
+       @fmts = map {
+               exists $known_snapshot_format_aliases{$_} ?
+                      $known_snapshot_format_aliases{$_} : $_} @fmts;
+       @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
+
+}
+
+our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
+do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
+
+# version of the core git binary
+our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown";
+
+$projects_list ||= $projectroot;
+
+# ======================================================================
+# input validation and dispatch
+our $action = $cgi->param('a');
+if (defined $action) {
+       if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
+               die_error(undef, "Invalid action parameter");
+       }
+}
+
+# parameters which are pathnames
+our $project = $cgi->param('p');
+if (defined $project) {
+       if (!validate_pathname($project) ||
+           !(-d "$projectroot/$project") ||
+           !check_head_link("$projectroot/$project") ||
+           ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
+           ($strict_export && !project_in_list($project))) {
+               undef $project;
+               die_error(undef, "No such project");
+       }
+}
+
+our $file_name = $cgi->param('f');
+if (defined $file_name) {
+       if (!validate_pathname($file_name)) {
+               die_error(undef, "Invalid file parameter");
+       }
+}
+
+our $file_parent = $cgi->param('fp');
+if (defined $file_parent) {
+       if (!validate_pathname($file_parent)) {
+               die_error(undef, "Invalid file parent parameter");
+       }
+}
+
+# parameters which are refnames
+our $hash = $cgi->param('h');
+if (defined $hash) {
+       if (!validate_refname($hash)) {
+               die_error(undef, "Invalid hash parameter");
+       }
+}
+
+our $hash_parent = $cgi->param('hp');
+if (defined $hash_parent) {
+       if (!validate_refname($hash_parent)) {
+               die_error(undef, "Invalid hash parent parameter");
+       }
+}
+
+our $hash_base = $cgi->param('hb');
+if (defined $hash_base) {
+       if (!validate_refname($hash_base)) {
+               die_error(undef, "Invalid hash base parameter");
+       }
+}
+
+my %allowed_options = (
+       "--no-merges" => [ qw(rss atom log shortlog history) ],
+);
+
+our @extra_options = $cgi->param('opt');
+if (defined @extra_options) {
+       foreach my $opt (@extra_options) {
+               if (not exists $allowed_options{$opt}) {
+                       die_error(undef, "Invalid option parameter");
+               }
+               if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
+                       die_error(undef, "Invalid option parameter for this action");
+               }
+       }
+}
+
+our $hash_parent_base = $cgi->param('hpb');
+if (defined $hash_parent_base) {
+       if (!validate_refname($hash_parent_base)) {
+               die_error(undef, "Invalid hash parent base parameter");
+       }
+}
+
+# other parameters
+our $page = $cgi->param('pg');
+if (defined $page) {
+       if ($page =~ m/[^0-9]/) {
+               die_error(undef, "Invalid page parameter");
+       }
+}
+
+our $searchtype = $cgi->param('st');
+if (defined $searchtype) {
+       if ($searchtype =~ m/[^a-z]/) {
+               die_error(undef, "Invalid searchtype parameter");
+       }
+}
+
+our $searchtext = $cgi->param('s');
+our $search_regexp;
+if (defined $searchtext) {
+       if (length($searchtext) < 2) {
+               die_error(undef, "At least two characters are required for search parameter");
+       }
+       $search_regexp = quotemeta $searchtext;
+}
+
+# now read PATH_INFO and use it as alternative to parameters
+sub evaluate_path_info {
+       return if defined $project;
+       my $path_info = $ENV{"PATH_INFO"};
+       return if !$path_info;
+       $path_info =~ s,^/+,,;
+       return if !$path_info;
+       # find which part of PATH_INFO is project
+       $project = $path_info;
+       $project =~ s,/+$,,;
+       while ($project && !check_head_link("$projectroot/$project")) {
+               $project =~ s,/*[^/]*$,,;
+       }
+       # validate project
+       $project = validate_pathname($project);
+       if (!$project ||
+           ($export_ok && !-e "$projectroot/$project/$export_ok") ||
+           ($strict_export && !project_in_list($project))) {
+               undef $project;
+               return;
+       }
+       # do not change any parameters if an action is given using the query string
+       return if $action;
+       $path_info =~ s,^$project/*,,;
+       my ($refname, $pathname) = split(/:/, $path_info, 2);
+       if (defined $pathname) {
+               # we got "project.git/branch:filename" or "project.git/branch:dir/"
+               # we could use git_get_type(branch:pathname), but it needs $git_dir
+               $pathname =~ s,^/+,,;
+               if (!$pathname || substr($pathname, -1) eq "/") {
+                       $action  ||= "tree";
+                       $pathname =~ s,/$,,;
+               } else {
+                       $action  ||= "blob_plain";
+               }
+               $hash_base ||= validate_refname($refname);
+               $file_name ||= validate_pathname($pathname);
+       } elsif (defined $refname) {
+               # we got "project.git/branch"
+               $action ||= "shortlog";
+               $hash   ||= validate_refname($refname);
+       }
+}
+evaluate_path_info();
+
+# path to the current git repository
+our $git_dir;
+$git_dir = "$projectroot/$project" if $project;
+
+# dispatch
+my %actions = (
+       "blame" => \&git_blame2,
+       "blobdiff" => \&git_blobdiff,
+       "blobdiff_plain" => \&git_blobdiff_plain,
+       "blob" => \&git_blob,
+       "blob_plain" => \&git_blob_plain,
+       "commitdiff" => \&git_commitdiff,
+       "commitdiff_plain" => \&git_commitdiff_plain,
+       "commit" => \&git_commit,
+       "forks" => \&git_forks,
+       "heads" => \&git_heads,
+       "history" => \&git_history,
+       "log" => \&git_log,
+       "rss" => \&git_rss,
+       "atom" => \&git_atom,
+       "search" => \&git_search,
+       "search_help" => \&git_search_help,
+       "shortlog" => \&git_shortlog,
+       "summary" => \&git_summary,
+       "tag" => \&git_tag,
+       "tags" => \&git_tags,
+       "tree" => \&git_tree,
+       "snapshot" => \&git_snapshot,
+       "object" => \&git_object,
+       # those below don't need $project
+       "opml" => \&git_opml,
+       "project_list" => \&git_project_list,
+       "project_index" => \&git_project_index,
+);
+
+if (!defined $action) {
+       if (defined $hash) {
+               $action = git_get_type($hash);
+       } elsif (defined $hash_base && defined $file_name) {
+               $action = git_get_type("$hash_base:$file_name");
+       } elsif (defined $project) {
+               $action = 'summary';
+       } else {
+               $action = 'project_list';
+       }
+}
+if (!defined($actions{$action})) {
+       die_error(undef, "Unknown action");
+}
+if ($action !~ m/^(opml|project_list|project_index)$/ &&
+    !$project) {
+       die_error(undef, "Project needed");
+}
+$actions{$action}->();
+exit;
+
+## ======================================================================
+## action links
+
+sub href(%) {
+       my %params = @_;
+       # default is to use -absolute url() i.e. $my_uri
+       my $href = $params{-full} ? $my_url : $my_uri;
+
+       # XXX: Warning: If you touch this, check the search form for updating,
+       # too.
+
+       my @mapping = (
+               project => "p",
+               action => "a",
+               file_name => "f",
+               file_parent => "fp",
+               hash => "h",
+               hash_parent => "hp",
+               hash_base => "hb",
+               hash_parent_base => "hpb",
+               page => "pg",
+               order => "o",
+               searchtext => "s",
+               searchtype => "st",
+               snapshot_format => "sf",
+               extra_options => "opt",
+       );
+       my %mapping = @mapping;
+
+       if ($params{-replay}) {
+               while (my ($name, $symbol) = each %mapping) {
+                       if (!exists $params{$name}) {
+                               # to allow for multivalued params we use arrayref form
+                               $params{$name} = [ $cgi->param($symbol) ];
+                       }
+               }
+       }
+
+       $params{'project'} = $project unless exists $params{'project'};
+
+       my ($use_pathinfo) = gitweb_check_feature('pathinfo');
+       if ($use_pathinfo) {
+               # use PATH_INFO for project name
+               $href .= "/$params{'project'}" if defined $params{'project'};
+               delete $params{'project'};
+
+               # Summary just uses the project path URL
+               if (defined $params{'action'} && $params{'action'} eq 'summary') {
+                       delete $params{'action'};
+               }
+       }
+
+       # now encode the parameters explicitly
+       my @result = ();
+       for (my $i = 0; $i < @mapping; $i += 2) {
+               my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
+               if (defined $params{$name}) {
+                       if (ref($params{$name}) eq "ARRAY") {
+                               foreach my $par (@{$params{$name}}) {
+                                       push @result, $symbol . "=" . esc_param($par);
+                               }
+                       } else {
+                               push @result, $symbol . "=" . esc_param($params{$name});
+                       }
+               }
+       }
+       $href .= "?" . join(';', @result) if scalar @result;
+
+       return $href;
+}
+
+
+## ======================================================================
+## validation, quoting/unquoting and escaping
+
+sub validate_pathname {
+       my $input = shift || return undef;
+
+       # no '.' or '..' as elements of path, i.e. no '.' nor '..'
+       # at the beginning, at the end, and between slashes.
+       # also this catches doubled slashes
+       if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
+               return undef;
+       }
+       # no null characters
+       if ($input =~ m!\0!) {
+               return undef;
+       }
+       return $input;
+}
+
+sub validate_refname {
+       my $input = shift || return undef;
+
+       # textual hashes are O.K.
+       if ($input =~ m/^[0-9a-fA-F]{40}$/) {
+               return $input;
+       }
+       # it must be correct pathname
+       $input = validate_pathname($input)
+               or return undef;
+       # restrictions on ref name according to git-check-ref-format
+       if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
+               return undef;
+       }
+       return $input;
+}
+
+# decode sequences of octets in utf8 into Perl's internal form,
+# which is utf-8 with utf8 flag set if needed.  gitweb writes out
+# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
+sub to_utf8 {
+       my $str = shift;
+       if (utf8::valid($str)) {
+               utf8::decode($str);
+               return $str;
+       } else {
+               return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
+       }
+}
+
+# quote unsafe chars, but keep the slash, even when it's not
+# correct, but quoted slashes look too horrible in bookmarks
+sub esc_param {
+       my $str = shift;
+       $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
+       $str =~ s/\+/%2B/g;
+       $str =~ s/ /\+/g;
+       return $str;
+}
+
+# quote unsafe chars in whole URL, so some charactrs cannot be quoted
+sub esc_url {
+       my $str = shift;
+       $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
+       $str =~ s/\+/%2B/g;
+       $str =~ s/ /\+/g;
+       return $str;
+}
+
+# replace invalid utf8 character with SUBSTITUTION sequence
+sub esc_html ($;%) {
+       my $str = shift;
+       my %opts = @_;
+
+       $str = to_utf8($str);
+       $str = $cgi->escapeHTML($str);
+       if ($opts{'-nbsp'}) {
+               $str =~ s/ /&nbsp;/g;
+       }
+       $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
+       return $str;
+}
+
+# quote control characters and escape filename to HTML
+sub esc_path {
+       my $str = shift;
+       my %opts = @_;
+
+       $str = to_utf8($str);
+       $str = $cgi->escapeHTML($str);
+       if ($opts{'-nbsp'}) {
+               $str =~ s/ /&nbsp;/g;
+       }
+       $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
+       return $str;
+}
+
+# Make control characters "printable", using character escape codes (CEC)
+sub quot_cec {
+       my $cntrl = shift;
+       my %es = ( # character escape codes, aka escape sequences
+                  "\t" => '\t',   # tab            (HT)
+                  "\n" => '\n',   # line feed      (LF)
+                  "\r" => '\r',   # carrige return (CR)
+                  "\f" => '\f',   # form feed      (FF)
+                  "\b" => '\b',   # backspace      (BS)
+                  "\a" => '\a',   # alarm (bell)   (BEL)
+                  "\e" => '\e',   # escape         (ESC)
+                  "\013" => '\v', # vertical tab   (VT)
+                  "\000" => '\0', # nul character  (NUL)
+                  );
+       my $chr = ( (exists $es{$cntrl})
+                   ? $es{$cntrl}
+                   : sprintf('\%03o', ord($cntrl)) );
+       return "<span class=\"cntrl\">$chr</span>";
+}
+
+# Alternatively use unicode control pictures codepoints,
+# Unicode "printable representation" (PR)
+sub quot_upr {
+       my $cntrl = shift;
+       my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
+       return "<span class=\"cntrl\">$chr</span>";
+}
+
+# git may return quoted and escaped filenames
+sub unquote {
+       my $str = shift;
+
+       sub unq {
+               my $seq = shift;
+               my %es = ( # character escape codes, aka escape sequences
+                       't' => "\t",   # tab            (HT, TAB)
+                       'n' => "\n",   # newline        (NL)
+                       'r' => "\r",   # return         (CR)
+                       'f' => "\f",   # form feed      (FF)
+                       'b' => "\b",   # backspace      (BS)
+                       'a' => "\a",   # alarm (bell)   (BEL)
+                       'e' => "\e",   # escape         (ESC)
+                       'v' => "\013", # vertical tab   (VT)
+               );
+
+               if ($seq =~ m/^[0-7]{1,3}$/) {
+                       # octal char sequence
+                       return chr(oct($seq));
+               } elsif (exists $es{$seq}) {
+                       # C escape sequence, aka character escape code
+                       return $es{$seq}
+               }
+               # quoted ordinary character
+               return $seq;
+       }
+
+       if ($str =~ m/^"(.*)"$/) {
+               # needs unquoting
+               $str = $1;
+               $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
+       }
+       return $str;
+}
+
+# escape tabs (convert tabs to spaces)
+sub untabify {
+       my $line = shift;
+
+       while ((my $pos = index($line, "\t")) != -1) {
+               if (my $count = (8 - ($pos % 8))) {
+                       my $spaces = ' ' x $count;
+                       $line =~ s/\t/$spaces/;
+               }
+       }
+
+       return $line;
+}
+
+sub project_in_list {
+       my $project = shift;
+       my @list = git_get_projects_list();
+       return @list && scalar(grep { $_->{'path'} eq $project } @list);
+}
+
+## ----------------------------------------------------------------------
+## HTML aware string manipulation
+
+sub chop_str {
+       my $str = shift;
+       my $len = shift;
+       my $add_len = shift || 10;
+
+       # allow only $len chars, but don't cut a word if it would fit in $add_len
+       # if it doesn't fit, cut it if it's still longer than the dots we would add
+       $str =~ m/^(.{0,$len}[^ \/\-_:\.@]{0,$add_len})(.*)/;
+       my $body = $1;
+       my $tail = $2;
+       if (length($tail) > 4) {
+               $tail = " ...";
+               $body =~ s/&[^;]*$//; # remove chopped character entities
+       }
+       return "$body$tail";
+}
+
+# takes the same arguments as chop_str, but also wraps a <span> around the
+# result with a title attribute if it does get chopped. Additionally, the
+# string is HTML-escaped.
+sub chop_and_escape_str {
+       my $str = shift;
+       my $len = shift;
+       my $add_len = shift || 10;
+
+       my $chopped = chop_str($str, $len, $add_len);
+       if ($chopped eq $str) {
+               return esc_html($chopped);
+       } else {
+               return qq{<span title="} . esc_html($str) . qq{">} .
+                       esc_html($chopped) . qq{</span>};
+       }
+}
+
+## ----------------------------------------------------------------------
+## functions returning short strings
+
+# CSS class for given age value (in seconds)
+sub age_class {
+       my $age = shift;
+
+       if (!defined $age) {
+               return "noage";
+       } elsif ($age < 60*60*2) {
+               return "age0";
+       } elsif ($age < 60*60*24*2) {
+               return "age1";
+       } else {
+               return "age2";
+       }
+}
+
+# convert age in seconds to "nn units ago" string
+sub age_string {
+       my $age = shift;
+       my $age_str;
+
+       if ($age > 60*60*24*365*2) {
+               $age_str = (int $age/60/60/24/365);
+               $age_str .= " years ago";
+       } elsif ($age > 60*60*24*(365/12)*2) {
+               $age_str = int $age/60/60/24/(365/12);
+               $age_str .= " months ago";
+       } elsif ($age > 60*60*24*7*2) {
+               $age_str = int $age/60/60/24/7;
+               $age_str .= " weeks ago";
+       } elsif ($age > 60*60*24*2) {
+               $age_str = int $age/60/60/24;
+               $age_str .= " days ago";
+       } elsif ($age > 60*60*2) {
+               $age_str = int $age/60/60;
+               $age_str .= " hours ago";
+       } elsif ($age > 60*2) {
+               $age_str = int $age/60;
+               $age_str .= " min ago";
+       } elsif ($age > 2) {
+               $age_str = int $age;
+               $age_str .= " sec ago";
+       } else {
+               $age_str .= " right now";
+       }
+       return $age_str;
+}
+
+use constant {
+       S_IFINVALID => 0030000,
+       S_IFGITLINK => 0160000,
+};
+
+# submodule/subproject, a commit object reference
+sub S_ISGITLINK($) {
+       my $mode = shift;
+
+       return (($mode & S_IFMT) == S_IFGITLINK)
+}
+
+# convert file mode in octal to symbolic file mode string
+sub mode_str {
+       my $mode = oct shift;
+
+       if (S_ISGITLINK($mode)) {
+               return 'm---------';
+       } elsif (S_ISDIR($mode & S_IFMT)) {
+               return 'drwxr-xr-x';
+       } elsif (S_ISLNK($mode)) {
+               return 'lrwxrwxrwx';
+       } elsif (S_ISREG($mode)) {
+               # git cares only about the executable bit
+               if ($mode & S_IXUSR) {
+                       return '-rwxr-xr-x';
+               } else {
+                       return '-rw-r--r--';
+               };
+       } else {
+               return '----------';
+       }
+}
+
+# convert file mode in octal to file type string
+sub file_type {
+       my $mode = shift;
+
+       if ($mode !~ m/^[0-7]+$/) {
+               return $mode;
+       } else {
+               $mode = oct $mode;
+       }
+
+       if (S_ISGITLINK($mode)) {
+               return "submodule";
+       } elsif (S_ISDIR($mode & S_IFMT)) {
+               return "directory";
+       } elsif (S_ISLNK($mode)) {
+               return "symlink";
+       } elsif (S_ISREG($mode)) {
+               return "file";
+       } else {
+               return "unknown";
+       }
+}
+
+# convert file mode in octal to file type description string
+sub file_type_long {
+       my $mode = shift;
+
+       if ($mode !~ m/^[0-7]+$/) {
+               return $mode;
+       } else {
+               $mode = oct $mode;
+       }
+
+       if (S_ISGITLINK($mode)) {
+               return "submodule";
+       } elsif (S_ISDIR($mode & S_IFMT)) {
+               return "directory";
+       } elsif (S_ISLNK($mode)) {
+               return "symlink";
+       } elsif (S_ISREG($mode)) {
+               if ($mode & S_IXUSR) {
+                       return "executable";
+               } else {
+                       return "file";
+               };
+       } else {
+               return "unknown";
+       }
+}
+
+
+## ----------------------------------------------------------------------
+## functions returning short HTML fragments, or transforming HTML fragments
+## which don't belong to other sections
+
+# format line of commit message.
+sub format_log_line_html {
+       my $line = shift;
+
+       $line = esc_html($line, -nbsp=>1);
+       if ($line =~ m/([0-9a-fA-F]{8,40})/) {
+               my $hash_text = $1;
+               my $link =
+                       $cgi->a({-href => href(action=>"object", hash=>$hash_text),
+                               -class => "text"}, $hash_text);
+               $line =~ s/$hash_text/$link/;
+       }
+       return $line;
+}
+
+# format marker of refs pointing to given object
+sub format_ref_marker {
+       my ($refs, $id) = @_;
+       my $markers = '';
+
+       if (defined $refs->{$id}) {
+               foreach my $ref (@{$refs->{$id}}) {
+                       my ($type, $name) = qw();
+                       # e.g. tags/v2.6.11 or heads/next
+                       if ($ref =~ m!^(.*?)s?/(.*)$!) {
+                               $type = $1;
+                               $name = $2;
+                       } else {
+                               $type = "ref";
+                               $name = $ref;
+                       }
+
+                       $markers .= " <span class=\"$type\" title=\"$ref\">" .
+                                   esc_html($name) . "</span>";
+               }
+       }
+
+       if ($markers) {
+               return ' <span class="refs">'. $markers . '</span>';
+       } else {
+               return "";
+       }
+}
+
+# format, perhaps shortened and with markers, title line
+sub format_subject_html {
+       my ($long, $short, $href, $extra) = @_;
+       $extra = '' unless defined($extra);
+
+       if (length($short) < length($long)) {
+               return $cgi->a({-href => $href, -class => "list subject",
+                               -title => to_utf8($long)},
+                      esc_html($short) . $extra);
+       } else {
+               return $cgi->a({-href => $href, -class => "list subject"},
+                      esc_html($long)  . $extra);
+       }
+}
+
+# format git diff header line, i.e. "diff --(git|combined|cc) ..."
+sub format_git_diff_header_line {
+       my $line = shift;
+       my $diffinfo = shift;
+       my ($from, $to) = @_;
+
+       if ($diffinfo->{'nparents'}) {
+               # combined diff
+               $line =~ s!^(diff (.*?) )"?.*$!$1!;
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+                                        esc_path($to->{'file'}));
+               } else { # file was deleted (no href)
+                       $line .= esc_path($to->{'file'});
+               }
+       } else {
+               # "ordinary" diff
+               $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
+               if ($from->{'href'}) {
+                       $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
+                                        'a/' . esc_path($from->{'file'}));
+               } else { # file was added (no href)
+                       $line .= 'a/' . esc_path($from->{'file'});
+               }
+               $line .= ' ';
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+                                        'b/' . esc_path($to->{'file'}));
+               } else { # file was deleted
+                       $line .= 'b/' . esc_path($to->{'file'});
+               }
+       }
+
+       return "<div class=\"diff header\">$line</div>\n";
+}
+
+# format extended diff header line, before patch itself
+sub format_extended_diff_header_line {
+       my $line = shift;
+       my $diffinfo = shift;
+       my ($from, $to) = @_;
+
+       # match <path>
+       if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
+               $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+                                      esc_path($from->{'file'}));
+       }
+       if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
+               $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+                                esc_path($to->{'file'}));
+       }
+       # match single <mode>
+       if ($line =~ m/\s(\d{6})$/) {
+               $line .= '<span class="info"> (' .
+                        file_type_long($1) .
+                        ')</span>';
+       }
+       # match <hash>
+       if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
+               # can match only for combined diff
+               $line = 'index ';
+               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+                       if ($from->{'href'}[$i]) {
+                               $line .= $cgi->a({-href=>$from->{'href'}[$i],
+                                                 -class=>"hash"},
+                                                substr($diffinfo->{'from_id'}[$i],0,7));
+                       } else {
+                               $line .= '0' x 7;
+                       }
+                       # separator
+                       $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
+               }
+               $line .= '..';
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+                                        substr($diffinfo->{'to_id'},0,7));
+               } else {
+                       $line .= '0' x 7;
+               }
+
+       } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
+               # can match only for ordinary diff
+               my ($from_link, $to_link);
+               if ($from->{'href'}) {
+                       $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
+                                            substr($diffinfo->{'from_id'},0,7));
+               } else {
+                       $from_link = '0' x 7;
+               }
+               if ($to->{'href'}) {
+                       $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+                                          substr($diffinfo->{'to_id'},0,7));
+               } else {
+                       $to_link = '0' x 7;
+               }
+               my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
+               $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
+       }
+
+       return $line . "<br/>\n";
+}
+
+# format from-file/to-file diff header
+sub format_diff_from_to_header {
+       my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
+       my $line;
+       my $result = '';
+
+       $line = $from_line;
+       #assert($line =~ m/^---/) if DEBUG;
+       # no extra formatting for "^--- /dev/null"
+       if (! $diffinfo->{'nparents'}) {
+               # ordinary (single parent) diff
+               if ($line =~ m!^--- "?a/!) {
+                       if ($from->{'href'}) {
+                               $line = '--- a/' .
+                                       $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+                                               esc_path($from->{'file'}));
+                       } else {
+                               $line = '--- a/' .
+                                       esc_path($from->{'file'});
+                       }
+               }
+               $result .= qq!<div class="diff from_file">$line</div>\n!;
+
+       } else {
+               # combined diff (merge commit)
+               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+                       if ($from->{'href'}[$i]) {
+                               $line = '--- ' .
+                                       $cgi->a({-href=>href(action=>"blobdiff",
+                                                            hash_parent=>$diffinfo->{'from_id'}[$i],
+                                                            hash_parent_base=>$parents[$i],
+                                                            file_parent=>$from->{'file'}[$i],
+                                                            hash=>$diffinfo->{'to_id'},
+                                                            hash_base=>$hash,
+                                                            file_name=>$to->{'file'}),
+                                                -class=>"path",
+                                                -title=>"diff" . ($i+1)},
+                                               $i+1) .
+                                       '/' .
+                                       $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
+                                               esc_path($from->{'file'}[$i]));
+                       } else {
+                               $line = '--- /dev/null';
+                       }
+                       $result .= qq!<div class="diff from_file">$line</div>\n!;
+               }
+       }
+
+       $line = $to_line;
+       #assert($line =~ m/^\+\+\+/) if DEBUG;
+       # no extra formatting for "^+++ /dev/null"
+       if ($line =~ m!^\+\+\+ "?b/!) {
+               if ($to->{'href'}) {
+                       $line = '+++ b/' .
+                               $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+                                       esc_path($to->{'file'}));
+               } else {
+                       $line = '+++ b/' .
+                               esc_path($to->{'file'});
+               }
+       }
+       $result .= qq!<div class="diff to_file">$line</div>\n!;
+
+       return $result;
+}
+
+# create note for patch simplified by combined diff
+sub format_diff_cc_simplified {
+       my ($diffinfo, @parents) = @_;
+       my $result = '';
+
+       $result .= "<div class=\"diff header\">" .
+                  "diff --cc ";
+       if (!is_deleted($diffinfo)) {
+               $result .= $cgi->a({-href => href(action=>"blob",
+                                                 hash_base=>$hash,
+                                                 hash=>$diffinfo->{'to_id'},
+                                                 file_name=>$diffinfo->{'to_file'}),
+                                   -class => "path"},
+                                  esc_path($diffinfo->{'to_file'}));
+       } else {
+               $result .= esc_path($diffinfo->{'to_file'});
+       }
+       $result .= "</div>\n" . # class="diff header"
+                  "<div class=\"diff nodifferences\">" .
+                  "Simple merge" .
+                  "</div>\n"; # class="diff nodifferences"
+
+       return $result;
+}
+
+# format patch (diff) line (not to be used for diff headers)
+sub format_diff_line {
+       my $line = shift;
+       my ($from, $to) = @_;
+       my $diff_class = "";
+
+       chomp $line;
+
+       if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
+               # combined diff
+               my $prefix = substr($line, 0, scalar @{$from->{'href'}});
+               if ($line =~ m/^\@{3}/) {
+                       $diff_class = " chunk_header";
+               } elsif ($line =~ m/^\\/) {
+                       $diff_class = " incomplete";
+               } elsif ($prefix =~ tr/+/+/) {
+                       $diff_class = " add";
+               } elsif ($prefix =~ tr/-/-/) {
+                       $diff_class = " rem";
+               }
+       } else {
+               # assume ordinary diff
+               my $char = substr($line, 0, 1);
+               if ($char eq '+') {
+                       $diff_class = " add";
+               } elsif ($char eq '-') {
+                       $diff_class = " rem";
+               } elsif ($char eq '@') {
+                       $diff_class = " chunk_header";
+               } elsif ($char eq "\\") {
+                       $diff_class = " incomplete";
+               }
+       }
+       $line = untabify($line);
+       if ($from && $to && $line =~ m/^\@{2} /) {
+               my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
+                       $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
+
+               $from_lines = 0 unless defined $from_lines;
+               $to_lines   = 0 unless defined $to_lines;
+
+               if ($from->{'href'}) {
+                       $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
+                                            -class=>"list"}, $from_text);
+               }
+               if ($to->{'href'}) {
+                       $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
+                                            -class=>"list"}, $to_text);
+               }
+               $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
+                       "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
+               return "<div class=\"diff$diff_class\">$line</div>\n";
+       } elsif ($from && $to && $line =~ m/^\@{3}/) {
+               my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
+               my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
+
+               @from_text = split(' ', $ranges);
+               for (my $i = 0; $i < @from_text; ++$i) {
+                       ($from_start[$i], $from_nlines[$i]) =
+                               (split(',', substr($from_text[$i], 1)), 0);
+               }
+
+               $to_text   = pop @from_text;
+               $to_start  = pop @from_start;
+               $to_nlines = pop @from_nlines;
+
+               $line = "<span class=\"chunk_info\">$prefix ";
+               for (my $i = 0; $i < @from_text; ++$i) {
+                       if ($from->{'href'}[$i]) {
+                               $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
+                                                 -class=>"list"}, $from_text[$i]);
+                       } else {
+                               $line .= $from_text[$i];
+                       }
+                       $line .= " ";
+               }
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
+                                         -class=>"list"}, $to_text);
+               } else {
+                       $line .= $to_text;
+               }
+               $line .= " $prefix</span>" .
+                        "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
+               return "<div class=\"diff$diff_class\">$line</div>\n";
+       }
+       return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
+}
+
+# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
+# linked.  Pass the hash of the tree/commit to snapshot.
+sub format_snapshot_links {
+       my ($hash) = @_;
+       my @snapshot_fmts = gitweb_check_feature('snapshot');
+       @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
+       my $num_fmts = @snapshot_fmts;
+       if ($num_fmts > 1) {
+               # A parenthesized list of links bearing format names.
+               # e.g. "snapshot (_tar.gz_ _zip_)"
+               return "snapshot (" . join(' ', map
+                       $cgi->a({
+                               -href => href(
+                                       action=>"snapshot",
+                                       hash=>$hash,
+                                       snapshot_format=>$_
+                               )
+                       }, $known_snapshot_formats{$_}{'display'})
+               , @snapshot_fmts) . ")";
+       } elsif ($num_fmts == 1) {
+               # A single "snapshot" link whose tooltip bears the format name.
+               # i.e. "_snapshot_"
+               my ($fmt) = @snapshot_fmts;
+               return
+                       $cgi->a({
+                               -href => href(
+                                       action=>"snapshot",
+                                       hash=>$hash,
+                                       snapshot_format=>$fmt
+                               ),
+                               -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
+                       }, "snapshot");
+       } else { # $num_fmts == 0
+               return undef;
+       }
+}
+
+## ----------------------------------------------------------------------
+## git utility subroutines, invoking git commands
+
+# returns path to the core git executable and the --git-dir parameter as list
+sub git_cmd {
+       return $GIT, '--git-dir='.$git_dir;
+}
+
+# returns path to the core git executable and the --git-dir parameter as string
+sub git_cmd_str {
+       return join(' ', git_cmd());
+}
+
+# get HEAD ref of given project as hash
+sub git_get_head_hash {
+       my $project = shift;
+       my $o_git_dir = $git_dir;
+       my $retval = undef;
+       $git_dir = "$projectroot/$project";
+       if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
+               my $head = <$fd>;
+               close $fd;
+               if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
+                       $retval = $1;
+               }
+       }
+       if (defined $o_git_dir) {
+               $git_dir = $o_git_dir;
+       }
+       return $retval;
+}
+
+# get type of given object
+sub git_get_type {
+       my $hash = shift;
+
+       open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
+       my $type = <$fd>;
+       close $fd or return;
+       chomp $type;
+       return $type;
+}
+
+# repository configuration
+our $config_file = '';
+our %config;
+
+# store multiple values for single key as anonymous array reference
+# single values stored directly in the hash, not as [ <value> ]
+sub hash_set_multi {
+       my ($hash, $key, $value) = @_;
+
+       if (!exists $hash->{$key}) {
+               $hash->{$key} = $value;
+       } elsif (!ref $hash->{$key}) {
+               $hash->{$key} = [ $hash->{$key}, $value ];
+       } else {
+               push @{$hash->{$key}}, $value;
+       }
+}
+
+# return hash of git project configuration
+# optionally limited to some section, e.g. 'gitweb'
+sub git_parse_project_config {
+       my $section_regexp = shift;
+       my %config;
+
+       local $/ = "\0";
+
+       open my $fh, "-|", git_cmd(), "config", '-z', '-l',
+               or return;
+
+       while (my $keyval = <$fh>) {
+               chomp $keyval;
+               my ($key, $value) = split(/\n/, $keyval, 2);
+
+               hash_set_multi(\%config, $key, $value)
+                       if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
+       }
+       close $fh;
+
+       return %config;
+}
+
+# convert config value to boolean, 'true' or 'false'
+# no value, number > 0, 'true' and 'yes' values are true
+# rest of values are treated as false (never as error)
+sub config_to_bool {
+       my $val = shift;
+
+       # strip leading and trailing whitespace
+       $val =~ s/^\s+//;
+       $val =~ s/\s+$//;
+
+       return (!defined $val ||               # section.key
+               ($val =~ /^\d+$/ && $val) ||   # section.key = 1
+               ($val =~ /^(?:true|yes)$/i));  # section.key = true
+}
+
+# convert config value to simple decimal number
+# an optional value suffix of 'k', 'm', or 'g' will cause the value
+# to be multiplied by 1024, 1048576, or 1073741824
+sub config_to_int {
+       my $val = shift;
+
+       # strip leading and trailing whitespace
+       $val =~ s/^\s+//;
+       $val =~ s/\s+$//;
+
+       if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
+               $unit = lc($unit);
+               # unknown unit is treated as 1
+               return $num * ($unit eq 'g' ? 1073741824 :
+                              $unit eq 'm' ?    1048576 :
+                              $unit eq 'k' ?       1024 : 1);
+       }
+       return $val;
+}
+
+# convert config value to array reference, if needed
+sub config_to_multi {
+       my $val = shift;
+
+       return ref($val) ? $val : [ $val ];
+}
+
+sub git_get_project_config {
+       my ($key, $type) = @_;
+
+       # key sanity check
+       return unless ($key);
+       $key =~ s/^gitweb\.//;
+       return if ($key =~ m/\W/);
+
+       # type sanity check
+       if (defined $type) {
+               $type =~ s/^--//;
+               $type = undef
+                       unless ($type eq 'bool' || $type eq 'int');
+       }
+
+       # get config
+       if (!defined $config_file ||
+           $config_file ne "$git_dir/config") {
+               %config = git_parse_project_config('gitweb');
+               $config_file = "$git_dir/config";
+       }
+
+       # ensure given type
+       if (!defined $type) {
+               return $config{"gitweb.$key"};
+       } elsif ($type eq 'bool') {
+               # backward compatibility: 'git config --bool' returns true/false
+               return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
+       } elsif ($type eq 'int') {
+               return config_to_int($config{"gitweb.$key"});
+       }
+       return $config{"gitweb.$key"};
+}
+
+# get hash of given path at given ref
+sub git_get_hash_by_path {
+       my $base = shift;
+       my $path = shift || return undef;
+       my $type = shift;
+
+       $path =~ s,/+$,,;
+
+       open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
+               or die_error(undef, "Open git-ls-tree failed");
+       my $line = <$fd>;
+       close $fd or return undef;
+
+       if (!defined $line) {
+               # there is no tree or hash given by $path at $base
+               return undef;
+       }
+
+       #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
+       $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
+       if (defined $type && $type ne $2) {
+               # type doesn't match
+               return undef;
+       }
+       return $3;
+}
+
+# get path of entry with given hash at given tree-ish (ref)
+# used to get 'from' filename for combined diff (merge commit) for renames
+sub git_get_path_by_hash {
+       my $base = shift || return;
+       my $hash = shift || return;
+
+       local $/ = "\0";
+
+       open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
+               or return undef;
+       while (my $line = <$fd>) {
+               chomp $line;
+
+               #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
+               #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
+               if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
+                       close $fd;
+                       return $1;
+               }
+       }
+       close $fd;
+       return undef;
+}
+
+## ......................................................................
+## git utility functions, directly accessing git repository
+
+sub git_get_project_description {
+       my $path = shift;
+
+       $git_dir = "$projectroot/$path";
+       open my $fd, "$projectroot/$path/description"
+               or return git_get_project_config('description');
+       my $descr = <$fd>;
+       close $fd;
+       if (defined $descr) {
+               chomp $descr;
+       }
+       return $descr;
+}
+
+sub git_get_project_url_list {
+       my $path = shift;
+
+       $git_dir = "$projectroot/$path";
+       open my $fd, "$projectroot/$path/cloneurl"
+               or return wantarray ?
+               @{ config_to_multi(git_get_project_config('url')) } :
+                  config_to_multi(git_get_project_config('url'));
+       my @git_project_url_list = map { chomp; $_ } <$fd>;
+       close $fd;
+
+       return wantarray ? @git_project_url_list : \@git_project_url_list;
+}
+
+sub git_get_projects_list {
+       my ($filter) = @_;
+       my @list;
+
+       $filter ||= '';
+       $filter =~ s/\.git$//;
+
+       my ($check_forks) = gitweb_check_feature('forks');
+
+       if (-d $projects_list) {
+               # search in directory
+               my $dir = $projects_list . ($filter ? "/$filter" : '');
+               # remove the trailing "/"
+               $dir =~ s!/+$!!;
+               my $pfxlen = length("$dir");
+               my $pfxdepth = ($dir =~ tr!/!!);
+
+               File::Find::find({
+                       follow_fast => 1, # follow symbolic links
+                       follow_skip => 2, # ignore duplicates
+                       dangling_symlinks => 0, # ignore dangling symlinks, silently
+                       wanted => sub {
+                               # skip project-list toplevel, if we get it.
+                               return if (m!^[/.]$!);
+                               # only directories can be git repositories
+                               return unless (-d $_);
+                               # don't traverse too deep (Find is super slow on os x)
+                               if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
+                                       $File::Find::prune = 1;
+                                       return;
+                               }
+
+                               my $subdir = substr($File::Find::name, $pfxlen + 1);
+                               # we check related file in $projectroot
+                               if ($check_forks and $subdir =~ m#/.#) {
+                                       $File::Find::prune = 1;
+                               } elsif (check_export_ok("$projectroot/$filter/$subdir")) {
+                                       push @list, { path => ($filter ? "$filter/" : '') . $subdir };
+                                       $File::Find::prune = 1;
+                               }
+                       },
+               }, "$dir");
+
+       } elsif (-f $projects_list) {
+               # read from file(url-encoded):
+               # 'git%2Fgit.git Linus+Torvalds'
+               # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
+               # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
+               my %paths;
+               open my ($fd), $projects_list or return;
+       PROJECT:
+               while (my $line = <$fd>) {
+                       chomp $line;
+                       my ($path, $owner) = split ' ', $line;
+                       $path = unescape($path);
+                       $owner = unescape($owner);
+                       if (!defined $path) {
+                               next;
+                       }
+                       if ($filter ne '') {
+                               # looking for forks;
+                               my $pfx = substr($path, 0, length($filter));
+                               if ($pfx ne $filter) {
+                                       next PROJECT;
+                               }
+                               my $sfx = substr($path, length($filter));
+                               if ($sfx !~ /^\/.*\.git$/) {
+                                       next PROJECT;
+                               }
+                       } elsif ($check_forks) {
+                       PATH:
+                               foreach my $filter (keys %paths) {
+                                       # looking for forks;
+                                       my $pfx = substr($path, 0, length($filter));
+                                       if ($pfx ne $filter) {
+                                               next PATH;
+                                       }
+                                       my $sfx = substr($path, length($filter));
+                                       if ($sfx !~ /^\/.*\.git$/) {
+                                               next PATH;
+                                       }
+                                       # is a fork, don't include it in
+                                       # the list
+                                       next PROJECT;
+                               }
+                       }
+                       if (check_export_ok("$projectroot/$path")) {
+                               my $pr = {
+                                       path => $path,
+                                       owner => to_utf8($owner),
+                               };
+                               push @list, $pr;
+                               (my $forks_path = $path) =~ s/\.git$//;
+                               $paths{$forks_path}++;
+                       }
+               }
+               close $fd;
+       }
+       return @list;
+}
+
+our $gitweb_project_owner = undef;
+sub git_get_project_list_from_file {
+
+       return if (defined $gitweb_project_owner);
+
+       $gitweb_project_owner = {};
+       # read from file (url-encoded):
+       # 'git%2Fgit.git Linus+Torvalds'
+       # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
+       # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
+       if (-f $projects_list) {
+               open (my $fd , $projects_list);
+               while (my $line = <$fd>) {
+                       chomp $line;
+                       my ($pr, $ow) = split ' ', $line;
+                       $pr = unescape($pr);
+                       $ow = unescape($ow);
+                       $gitweb_project_owner->{$pr} = to_utf8($ow);
+               }
+               close $fd;
+       }
+}
+
+sub git_get_project_owner {
+       my $project = shift;
+       my $owner;
+
+       return undef unless $project;
+
+       if (!defined $gitweb_project_owner) {
+               git_get_project_list_from_file();
+       }
+
+       if (exists $gitweb_project_owner->{$project}) {
+               $owner = $gitweb_project_owner->{$project};
+       }
+       if (!defined $owner) {
+               $owner = get_file_owner("$projectroot/$project");
+       }
+
+       return $owner;
+}
+
+sub git_get_last_activity {
+       my ($path) = @_;
+       my $fd;
+
+       $git_dir = "$projectroot/$path";
+       open($fd, "-|", git_cmd(), 'for-each-ref',
+            '--format=%(committer)',
+            '--sort=-committerdate',
+            '--count=1',
+            'refs/heads') or return;
+       my $most_recent = <$fd>;
+       close $fd or return;
+       if (defined $most_recent &&
+           $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
+               my $timestamp = $1;
+               my $age = time - $timestamp;
+               return ($age, age_string($age));
+       }
+       return (undef, undef);
+}
+
+sub git_get_references {
+       my $type = shift || "";
+       my %refs;
+       # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
+       # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
+       open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
+               ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
+               or return;
+
+       while (my $line = <$fd>) {
+               chomp $line;
+               if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) {
+                       if (defined $refs{$1}) {
+                               push @{$refs{$1}}, $2;
+                       } else {
+                               $refs{$1} = [ $2 ];
+                       }
+               }
+       }
+       close $fd or return;
+       return \%refs;
+}
+
+sub git_get_rev_name_tags {
+       my $hash = shift || return undef;
+
+       open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
+               or return;
+       my $name_rev = <$fd>;
+       close $fd;
+
+       if ($name_rev =~ m|^$hash tags/(.*)$|) {
+               return $1;
+       } else {
+               # catches also '$hash undefined' output
+               return undef;
+       }
+}
+
+## ----------------------------------------------------------------------
+## parse to hash functions
+
+sub parse_date {
+       my $epoch = shift;
+       my $tz = shift || "-0000";
+
+       my %date;
+       my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
+       my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
+       my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
+       $date{'hour'} = $hour;
+       $date{'minute'} = $min;
+       $date{'mday'} = $mday;
+       $date{'day'} = $days[$wday];
+       $date{'month'} = $months[$mon];
+       $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
+                            $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
+       $date{'mday-time'} = sprintf "%d %s %02d:%02d",
+                            $mday, $months[$mon], $hour ,$min;
+       $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
+                            1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
+
+       $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
+       my $local = $epoch + ((int $1 + ($2/60)) * 3600);
+       ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
+       $date{'hour_local'} = $hour;
+       $date{'minute_local'} = $min;
+       $date{'tz_local'} = $tz;
+       $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
+                                 1900+$year, $mon+1, $mday,
+                                 $hour, $min, $sec, $tz);
+       return %date;
+}
+
+sub parse_tag {
+       my $tag_id = shift;
+       my %tag;
+       my @comment;
+
+       open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
+       $tag{'id'} = $tag_id;
+       while (my $line = <$fd>) {
+               chomp $line;
+               if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
+                       $tag{'object'} = $1;
+               } elsif ($line =~ m/^type (.+)$/) {
+                       $tag{'type'} = $1;
+               } elsif ($line =~ m/^tag (.+)$/) {
+                       $tag{'name'} = $1;
+               } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
+                       $tag{'author'} = $1;
+                       $tag{'epoch'} = $2;
+                       $tag{'tz'} = $3;
+               } elsif ($line =~ m/--BEGIN/) {
+                       push @comment, $line;
+                       last;
+               } elsif ($line eq "") {
+                       last;
+               }
+       }
+       push @comment, <$fd>;
+       $tag{'comment'} = \@comment;
+       close $fd or return;
+       if (!defined $tag{'name'}) {
+               return
+       };
+       return %tag
+}
+
+sub parse_commit_text {
+       my ($commit_text, $withparents) = @_;
+       my @commit_lines = split '\n', $commit_text;
+       my %co;
+
+       pop @commit_lines; # Remove '\0'
+
+       if (! @commit_lines) {
+               return;
+       }
+
+       my $header = shift @commit_lines;
+       if ($header !~ m/^[0-9a-fA-F]{40}/) {
+               return;
+       }
+       ($co{'id'}, my @parents) = split ' ', $header;
+       while (my $line = shift @commit_lines) {
+               last if $line eq "\n";
+               if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
+                       $co{'tree'} = $1;
+               } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
+                       push @parents, $1;
+               } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
+                       $co{'author'} = $1;
+                       $co{'author_epoch'} = $2;
+                       $co{'author_tz'} = $3;
+                       if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
+                               $co{'author_name'}  = $1;
+                               $co{'author_email'} = $2;
+                       } else {
+                               $co{'author_name'} = $co{'author'};
+                       }
+               } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
+                       $co{'committer'} = $1;
+                       $co{'committer_epoch'} = $2;
+                       $co{'committer_tz'} = $3;
+                       $co{'committer_name'} = $co{'committer'};
+                       if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
+                               $co{'committer_name'}  = $1;
+                               $co{'committer_email'} = $2;
+                       } else {
+                               $co{'committer_name'} = $co{'committer'};
+                       }
+               }
+       }
+       if (!defined $co{'tree'}) {
+               return;
+       };
+       $co{'parents'} = \@parents;
+       $co{'parent'} = $parents[0];
+
+       foreach my $title (@commit_lines) {
+               $title =~ s/^    //;
+               if ($title ne "") {
+                       $co{'title'} = chop_str($title, 80, 5);
+                       # remove leading stuff of merges to make the interesting part visible
+                       if (length($title) > 50) {
+                               $title =~ s/^Automatic //;
+                               $title =~ s/^merge (of|with) /Merge ... /i;
+                               if (length($title) > 50) {
+                                       $title =~ s/(http|rsync):\/\///;
+                               }
+                               if (length($title) > 50) {
+                                       $title =~ s/(master|www|rsync)\.//;
+                               }
+                               if (length($title) > 50) {
+                                       $title =~ s/kernel.org:?//;
+                               }
+                               if (length($title) > 50) {
+                                       $title =~ s/\/pub\/scm//;
+                               }
+                       }
+                       $co{'title_short'} = chop_str($title, 50, 5);
+                       last;
+               }
+       }
+       if ($co{'title'} eq "") {
+               $co{'title'} = $co{'title_short'} = '(no commit message)';
+       }
+       # remove added spaces
+       foreach my $line (@commit_lines) {
+               $line =~ s/^    //;
+       }
+       $co{'comment'} = \@commit_lines;
+
+       my $age = time - $co{'committer_epoch'};
+       $co{'age'} = $age;
+       $co{'age_string'} = age_string($age);
+       my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
+       if ($age > 60*60*24*7*2) {
+               $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
+               $co{'age_string_age'} = $co{'age_string'};
+       } else {
+               $co{'age_string_date'} = $co{'age_string'};
+               $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
+       }
+       return %co;
+}
+
+sub parse_commit {
+       my ($commit_id) = @_;
+       my %co;
+
+       local $/ = "\0";
+
+       open my $fd, "-|", git_cmd(), "rev-list",
+               "--parents",
+               "--header",
+               "--max-count=1",
+               $commit_id,
+               "--",
+               or die_error(undef, "Open git-rev-list failed");
+       %co = parse_commit_text(<$fd>, 1);
+       close $fd;
+
+       return %co;
+}
+
+sub parse_commits {
+       my ($commit_id, $maxcount, $skip, $arg, $filename) = @_;
+       my @cos;
+
+       $maxcount ||= 1;
+       $skip ||= 0;
+
+       local $/ = "\0";
+
+       open my $fd, "-|", git_cmd(), "rev-list",
+               "--header",
+               ($arg ? ($arg) : ()),
+               ("--max-count=" . $maxcount),
+               ("--skip=" . $skip),
+               @extra_options,
+               $commit_id,
+               "--",
+               ($filename ? ($filename) : ())
+               or die_error(undef, "Open git-rev-list failed");
+       while (my $line = <$fd>) {
+               my %co = parse_commit_text($line);
+               push @cos, \%co;
+       }
+       close $fd;
+
+       return wantarray ? @cos : \@cos;
+}
+
+# parse ref from ref_file, given by ref_id, with given type
+sub parse_ref {
+       my $ref_file = shift;
+       my $ref_id = shift;
+       my $type = shift || git_get_type($ref_id);
+       my %ref_item;
+
+       $ref_item{'type'} = $type;
+       $ref_item{'id'} = $ref_id;
+       $ref_item{'epoch'} = 0;
+       $ref_item{'age'} = "unknown";
+       if ($type eq "tag") {
+               my %tag = parse_tag($ref_id);
+               $ref_item{'comment'} = $tag{'comment'};
+               if ($tag{'type'} eq "commit") {
+                       my %co = parse_commit($tag{'object'});
+                       $ref_item{'epoch'} = $co{'committer_epoch'};
+                       $ref_item{'age'} = $co{'age_string'};
+               } elsif (defined($tag{'epoch'})) {
+                       my $age = time - $tag{'epoch'};
+                       $ref_item{'epoch'} = $tag{'epoch'};
+                       $ref_item{'age'} = age_string($age);
+               }
+               $ref_item{'reftype'} = $tag{'type'};
+               $ref_item{'name'} = $tag{'name'};
+               $ref_item{'refid'} = $tag{'object'};
+       } elsif ($type eq "commit"){
+               my %co = parse_commit($ref_id);
+               $ref_item{'reftype'} = "commit";
+               $ref_item{'name'} = $ref_file;
+               $ref_item{'title'} = $co{'title'};
+               $ref_item{'refid'} = $ref_id;
+               $ref_item{'epoch'} = $co{'committer_epoch'};
+               $ref_item{'age'} = $co{'age_string'};
+       } else {
+               $ref_item{'reftype'} = $type;
+               $ref_item{'name'} = $ref_file;
+               $ref_item{'refid'} = $ref_id;
+       }
+
+       return %ref_item;
+}
+
+# parse line of git-diff-tree "raw" output
+sub parse_difftree_raw_line {
+       my $line = shift;
+       my %res;
+
+       # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
+       # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
+       if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
+               $res{'from_mode'} = $1;
+               $res{'to_mode'} = $2;
+               $res{'from_id'} = $3;
+               $res{'to_id'} = $4;
+               $res{'status'} = $res{'status_str'} = $5;
+               $res{'similarity'} = $6;
+               if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
+                       ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
+               } else {
+                       $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
+               }
+       }
+       # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
+       # combined diff (for merge commit)
+       elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
+               $res{'nparents'}  = length($1);
+               $res{'from_mode'} = [ split(' ', $2) ];
+               $res{'to_mode'} = pop @{$res{'from_mode'}};
+               $res{'from_id'} = [ split(' ', $3) ];
+               $res{'to_id'} = pop @{$res{'from_id'}};
+               $res{'status_str'} = $4;
+               $res{'status'} = [ split('', $4) ];
+               $res{'to_file'} = unquote($5);
+       }
+       # 'c512b523472485aef4fff9e57b229d9d243c967f'
+       elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
+               $res{'commit'} = $1;
+       }
+
+       return wantarray ? %res : \%res;
+}
+
+# wrapper: return parsed line of git-diff-tree "raw" output
+# (the argument might be raw line, or parsed info)
+sub parsed_difftree_line {
+       my $line_or_ref = shift;
+
+       if (ref($line_or_ref) eq "HASH") {
+               # pre-parsed (or generated by hand)
+               return $line_or_ref;
+       } else {
+               return parse_difftree_raw_line($line_or_ref);
+       }
+}
+
+# parse line of git-ls-tree output
+sub parse_ls_tree_line ($;%) {
+       my $line = shift;
+       my %opts = @_;
+       my %res;
+
+       #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
+       $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
+
+       $res{'mode'} = $1;
+       $res{'type'} = $2;
+       $res{'hash'} = $3;
+       if ($opts{'-z'}) {
+               $res{'name'} = $4;
+       } else {
+               $res{'name'} = unquote($4);
+       }
+
+       return wantarray ? %res : \%res;
+}
+
+# generates _two_ hashes, references to which are passed as 2 and 3 argument
+sub parse_from_to_diffinfo {
+       my ($diffinfo, $from, $to, @parents) = @_;
+
+       if ($diffinfo->{'nparents'}) {
+               # combined diff
+               $from->{'file'} = [];
+               $from->{'href'} = [];
+               fill_from_file_info($diffinfo, @parents)
+                       unless exists $diffinfo->{'from_file'};
+               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+                       $from->{'file'}[$i] =
+                               defined $diffinfo->{'from_file'}[$i] ?
+                                       $diffinfo->{'from_file'}[$i] :
+                                       $diffinfo->{'to_file'};
+                       if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
+                               $from->{'href'}[$i] = href(action=>"blob",
+                                                          hash_base=>$parents[$i],
+                                                          hash=>$diffinfo->{'from_id'}[$i],
+                                                          file_name=>$from->{'file'}[$i]);
+                       } else {
+                               $from->{'href'}[$i] = undef;
+                       }
+               }
+       } else {
+               # ordinary (not combined) diff
+               $from->{'file'} = $diffinfo->{'from_file'};
+               if ($diffinfo->{'status'} ne "A") { # not new (added) file
+                       $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
+                                              hash=>$diffinfo->{'from_id'},
+                                              file_name=>$from->{'file'});
+               } else {
+                       delete $from->{'href'};
+               }
+       }
+
+       $to->{'file'} = $diffinfo->{'to_file'};
+       if (!is_deleted($diffinfo)) { # file exists in result
+               $to->{'href'} = href(action=>"blob", hash_base=>$hash,
+                                    hash=>$diffinfo->{'to_id'},
+                                    file_name=>$to->{'file'});
+       } else {
+               delete $to->{'href'};
+       }
+}
+
+## ......................................................................
+## parse to array of hashes functions
+
+sub git_get_heads_list {
+       my $limit = shift;
+       my @headslist;
+
+       open my $fd, '-|', git_cmd(), 'for-each-ref',
+               ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
+               '--format=%(objectname) %(refname) %(subject)%00%(committer)',
+               'refs/heads'
+               or return;
+       while (my $line = <$fd>) {
+               my %ref_item;
+
+               chomp $line;
+               my ($refinfo, $committerinfo) = split(/\0/, $line);
+               my ($hash, $name, $title) = split(' ', $refinfo, 3);
+               my ($committer, $epoch, $tz) =
+                       ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
+               $name =~ s!^refs/heads/!!;
+
+               $ref_item{'name'}  = $name;
+               $ref_item{'id'}    = $hash;
+               $ref_item{'title'} = $title || '(no commit message)';
+               $ref_item{'epoch'} = $epoch;
+               if ($epoch) {
+                       $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
+               } else {
+                       $ref_item{'age'} = "unknown";
+               }
+
+               push @headslist, \%ref_item;
+       }
+       close $fd;
+
+       return wantarray ? @headslist : \@headslist;
+}
+
+sub git_get_tags_list {
+       my $limit = shift;
+       my @tagslist;
+
+       open my $fd, '-|', git_cmd(), 'for-each-ref',
+               ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
+               '--format=%(objectname) %(objecttype) %(refname) '.
+               '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
+               'refs/tags'
+               or return;
+       while (my $line = <$fd>) {
+               my %ref_item;
+
+               chomp $line;
+               my ($refinfo, $creatorinfo) = split(/\0/, $line);
+               my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
+               my ($creator, $epoch, $tz) =
+                       ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
+               $name =~ s!^refs/tags/!!;
+
+               $ref_item{'type'} = $type;
+               $ref_item{'id'} = $id;
+               $ref_item{'name'} = $name;
+               if ($type eq "tag") {
+                       $ref_item{'subject'} = $title;
+                       $ref_item{'reftype'} = $reftype;
+                       $ref_item{'refid'}   = $refid;
+               } else {
+                       $ref_item{'reftype'} = $type;
+                       $ref_item{'refid'}   = $id;
+               }
+
+               if ($type eq "tag" || $type eq "commit") {
+                       $ref_item{'epoch'} = $epoch;
+                       if ($epoch) {
+                               $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
+                       } else {
+                               $ref_item{'age'} = "unknown";
+                       }
+               }
+
+               push @tagslist, \%ref_item;
+       }
+       close $fd;
+
+       return wantarray ? @tagslist : \@tagslist;
+}
+
+## ----------------------------------------------------------------------
+## filesystem-related functions
+
+sub get_file_owner {
+       my $path = shift;
+
+       my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
+       my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
+       if (!defined $gcos) {
+               return undef;
+       }
+       my $owner = $gcos;
+       $owner =~ s/[,;].*$//;
+       return to_utf8($owner);
+}
+
+## ......................................................................
+## mimetype related functions
+
+sub mimetype_guess_file {
+       my $filename = shift;
+       my $mimemap = shift;
+       -r $mimemap or return undef;
+
+       my %mimemap;
+       open(MIME, $mimemap) or return undef;
+       while (<MIME>) {
+               next if m/^#/; # skip comments
+               my ($mime, $exts) = split(/\t+/);
+               if (defined $exts) {
+                       my @exts = split(/\s+/, $exts);
+                       foreach my $ext (@exts) {
+                               $mimemap{$ext} = $mime;
+                       }
+               }
+       }
+       close(MIME);
+
+       $filename =~ /\.([^.]*)$/;
+       return $mimemap{$1};
+}
+
+sub mimetype_guess {
+       my $filename = shift;
+       my $mime;
+       $filename =~ /\./ or return undef;
+
+       if ($mimetypes_file) {
+               my $file = $mimetypes_file;
+               if ($file !~ m!^/!) { # if it is relative path
+                       # it is relative to project
+                       $file = "$projectroot/$project/$file";
+               }
+               $mime = mimetype_guess_file($filename, $file);
+       }
+       $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
+       return $mime;
+}
+
+sub blob_mimetype {
+       my $fd = shift;
+       my $filename = shift;
+
+       if ($filename) {
+               my $mime = mimetype_guess($filename);
+               $mime and return $mime;
+       }
+
+       # just in case
+       return $default_blob_plain_mimetype unless $fd;
+
+       if (-T $fd) {
+               return 'text/plain' .
+                      ($default_text_plain_charset ? '; charset='.$default_text_plain_charset : '');
+       } elsif (! $filename) {
+               return 'application/octet-stream';
+       } elsif ($filename =~ m/\.png$/i) {
+               return 'image/png';
+       } elsif ($filename =~ m/\.gif$/i) {
+               return 'image/gif';
+       } elsif ($filename =~ m/\.jpe?g$/i) {
+               return 'image/jpeg';
+       } else {
+               return 'application/octet-stream';
+       }
+}
+
+## ======================================================================
+## functions printing HTML: header, footer, error page
+
+sub git_header_html {
+       my $status = shift || "200 OK";
+       my $expires = shift;
+
+       my $title = "$site_name";
+       if (defined $project) {
+               $title .= " - " . to_utf8($project);
+               if (defined $action) {
+                       $title .= "/$action";
+                       if (defined $file_name) {
+                               $title .= " - " . esc_path($file_name);
+                               if ($action eq "tree" && $file_name !~ m|/$|) {
+                                       $title .= "/";
+                               }
+                       }
+               }
+       }
+       my $content_type;
+       # require explicit support from the UA if we are to send the page as
+       # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
+       # we have to do this because MSIE sometimes globs '*/*', pretending to
+       # support xhtml+xml but choking when it gets what it asked for.
+       if (defined $cgi->http('HTTP_ACCEPT') &&
+           $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
+           $cgi->Accept('application/xhtml+xml') != 0) {
+               $content_type = 'application/xhtml+xml';
+       } else {
+               $content_type = 'text/html';
+       }
+       print $cgi->header(-type=>$content_type, -charset => 'utf-8',
+                          -status=> $status, -expires => $expires);
+       my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
+       print <<EOF;
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
+<!-- git core binaries version $git_version -->
+<head>
+<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
+<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
+<meta name="robots" content="index, nofollow"/>
+<title>$title</title>
+EOF
+# print out each stylesheet that exist
+       if (defined $stylesheet) {
+#provides backwards capability for those people who define style sheet in a config file
+               print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
+       } else {
+               foreach my $stylesheet (@stylesheets) {
+                       next unless $stylesheet;
+                       print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
+               }
+       }
+       if (defined $project) {
+               printf('<link rel="alternate" title="%s log RSS feed" '.
+                      'href="%s" type="application/rss+xml" />'."\n",
+                      esc_param($project), href(action=>"rss"));
+               printf('<link rel="alternate" title="%s log RSS feed (no merges)" '.
+                      'href="%s" type="application/rss+xml" />'."\n",
+                      esc_param($project), href(action=>"rss",
+                                                extra_options=>"--no-merges"));
+               printf('<link rel="alternate" title="%s log Atom feed" '.
+                      'href="%s" type="application/atom+xml" />'."\n",
+                      esc_param($project), href(action=>"atom"));
+               printf('<link rel="alternate" title="%s log Atom feed (no merges)" '.
+                      'href="%s" type="application/atom+xml" />'."\n",
+                      esc_param($project), href(action=>"atom",
+                                                extra_options=>"--no-merges"));
+       } else {
+               printf('<link rel="alternate" title="%s projects list" '.
+                      'href="%s" type="text/plain; charset=utf-8"/>'."\n",
+                      $site_name, href(project=>undef, action=>"project_index"));
+               printf('<link rel="alternate" title="%s projects feeds" '.
+                      'href="%s" type="text/x-opml"/>'."\n",
+                      $site_name, href(project=>undef, action=>"opml"));
+       }
+       if (defined $favicon) {
+               print qq(<link rel="shortcut icon" href="$favicon" type="image/png"/>\n);
+       }
+
+       print "</head>\n" .
+             "<body>\n";
+
+       if (-f $site_header) {
+               open (my $fd, $site_header);
+               print <$fd>;
+               close $fd;
+       }
+
+       print "<div class=\"page_header\">\n" .
+             $cgi->a({-href => esc_url($logo_url),
+                      -title => $logo_label},
+                     qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
+       print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
+       if (defined $project) {
+               print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
+               if (defined $action) {
+                       print " / $action";
+               }
+               print "\n";
+       }
+       print "</div>\n";
+
+       my ($have_search) = gitweb_check_feature('search');
+       if ((defined $project) && ($have_search)) {
+               if (!defined $searchtext) {
+                       $searchtext = "";
+               }
+               my $search_hash;
+               if (defined $hash_base) {
+                       $search_hash = $hash_base;
+               } elsif (defined $hash) {
+                       $search_hash = $hash;
+               } else {
+                       $search_hash = "HEAD";
+               }
+               my $action = $my_uri;
+               my ($use_pathinfo) = gitweb_check_feature('pathinfo');
+               if ($use_pathinfo) {
+                       $action .= "/$project";
+               } else {
+                       $cgi->param("p", $project);
+               }
+               $cgi->param("a", "search");
+               $cgi->param("h", $search_hash);
+               print $cgi->startform(-method => "get", -action => $action) .
+                     "<div class=\"search\">\n" .
+                     (!$use_pathinfo && $cgi->hidden(-name => "p") . "\n") .
+                     $cgi->hidden(-name => "a") . "\n" .
+                     $cgi->hidden(-name => "h") . "\n" .
+                     $cgi->popup_menu(-name => 'st', -default => 'commit',
+                                      -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
+                     $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
+                     " search:\n",
+                     $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
+                     "</div>" .
+                     $cgi->end_form() . "\n";
+       }
+}
+
+sub git_footer_html {
+       print "<div class=\"page_footer\">\n";
+       if (defined $project) {
+               my $descr = git_get_project_description($project);
+               if (defined $descr) {
+                       print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
+               }
+               print $cgi->a({-href => href(action=>"rss"),
+                             -class => "rss_logo"}, "RSS") . " ";
+               print $cgi->a({-href => href(action=>"atom"),
+                             -class => "rss_logo"}, "Atom") . "\n";
+       } else {
+               print $cgi->a({-href => href(project=>undef, action=>"opml"),
+                             -class => "rss_logo"}, "OPML") . " ";
+               print $cgi->a({-href => href(project=>undef, action=>"project_index"),
+                             -class => "rss_logo"}, "TXT") . "\n";
+       }
+       print "</div>\n" ;
+
+       if (-f $site_footer) {
+               open (my $fd, $site_footer);
+               print <$fd>;
+               close $fd;
+       }
+
+       print "</body>\n" .
+             "</html>";
+}
+
+sub die_error {
+       my $status = shift || "403 Forbidden";
+       my $error = shift || "Malformed query, file missing or permission denied";
+
+       git_header_html($status);
+       print <<EOF;
+<div class="page_body">
+<br /><br />
+$status - $error
+<br />
+</div>
+EOF
+       git_footer_html();
+       exit;
+}
+
+## ----------------------------------------------------------------------
+## functions printing or outputting HTML: navigation
+
+sub git_print_page_nav {
+       my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
+       $extra = '' if !defined $extra; # pager or formats
+
+       my @navs = qw(summary shortlog log commit commitdiff tree);
+       if ($suppress) {
+               @navs = grep { $_ ne $suppress } @navs;
+       }
+
+       my %arg = map { $_ => {action=>$_} } @navs;
+       if (defined $head) {
+               for (qw(commit commitdiff)) {
+                       $arg{$_}{'hash'} = $head;
+               }
+               if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
+                       for (qw(shortlog log)) {
+                               $arg{$_}{'hash'} = $head;
+                       }
+               }
+       }
+       $arg{'tree'}{'hash'} = $treehead if defined $treehead;
+       $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
+
+       print "<div class=\"page_nav\">\n" .
+               (join " | ",
+                map { $_ eq $current ?
+                      $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
+                } @navs);
+       print "<br/>\n$extra<br/>\n" .
+             "</div>\n";
+}
+
+sub format_paging_nav {
+       my ($action, $hash, $head, $page, $nrevs) = @_;
+       my $paging_nav;
+
+
+       if ($hash ne $head || $page) {
+               $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
+       } else {
+               $paging_nav .= "HEAD";
+       }
+
+       if ($page > 0) {
+               $paging_nav .= " &sdot; " .
+                       $cgi->a({-href => href(-replay=>1, page=>$page-1),
+                                -accesskey => "p", -title => "Alt-p"}, "prev");
+       } else {
+               $paging_nav .= " &sdot; prev";
+       }
+
+       if ($nrevs >= (100 * ($page+1)-1)) {
+               $paging_nav .= " &sdot; " .
+                       $cgi->a({-href => href(-replay=>1, page=>$page+1),
+                                -accesskey => "n", -title => "Alt-n"}, "next");
+       } else {
+               $paging_nav .= " &sdot; next";
+       }
+
+       return $paging_nav;
+}
+
+## ......................................................................
+## functions printing or outputting HTML: div
+
+sub git_print_header_div {
+       my ($action, $title, $hash, $hash_base) = @_;
+       my %args = ();
+
+       $args{'action'} = $action;
+       $args{'hash'} = $hash if $hash;
+       $args{'hash_base'} = $hash_base if $hash_base;
+
+       print "<div class=\"header\">\n" .
+             $cgi->a({-href => href(%args), -class => "title"},
+             $title ? $title : $action) .
+             "\n</div>\n";
+}
+
+#sub git_print_authorship (\%) {
+sub git_print_authorship {
+       my $co = shift;
+
+       my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
+       print "<div class=\"author_date\">" .
+             esc_html($co->{'author_name'}) .
+             " [$ad{'rfc2822'}";
+       if ($ad{'hour_local'} < 6) {
+               printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
+                      $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
+       } else {
+               printf(" (%02d:%02d %s)",
+                      $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
+       }
+       print "]</div>\n";
+}
+
+sub git_print_page_path {
+       my $name = shift;
+       my $type = shift;
+       my $hb = shift;
+
+
+       print "<div class=\"page_path\">";
+       print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
+                     -title => 'tree root'}, to_utf8("[$project]"));
+       print " / ";
+       if (defined $name) {
+               my @dirname = split '/', $name;
+               my $basename = pop @dirname;
+               my $fullname = '';
+
+               foreach my $dir (@dirname) {
+                       $fullname .= ($fullname ? '/' : '') . $dir;
+                       print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
+                                                    hash_base=>$hb),
+                                     -title => $fullname}, esc_path($dir));
+                       print " / ";
+               }
+               if (defined $type && $type eq 'blob') {
+                       print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
+                                                    hash_base=>$hb),
+                                     -title => $name}, esc_path($basename));
+               } elsif (defined $type && $type eq 'tree') {
+                       print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
+                                                    hash_base=>$hb),
+                                     -title => $name}, esc_path($basename));
+                       print " / ";
+               } else {
+                       print esc_path($basename);
+               }
+       }
+       print "<br/></div>\n";
+}
+
+# sub git_print_log (\@;%) {
+sub git_print_log ($;%) {
+       my $log = shift;
+       my %opts = @_;
+
+       if ($opts{'-remove_title'}) {
+               # remove title, i.e. first line of log
+               shift @$log;
+       }
+       # remove leading empty lines
+       while (defined $log->[0] && $log->[0] eq "") {
+               shift @$log;
+       }
+
+       # print log
+       my $signoff = 0;
+       my $empty = 0;
+       foreach my $line (@$log) {
+               if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
+                       $signoff = 1;
+                       $empty = 0;
+                       if (! $opts{'-remove_signoff'}) {
+                               print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
+                               next;
+                       } else {
+                               # remove signoff lines
+                               next;
+                       }
+               } else {
+                       $signoff = 0;
+               }
+
+               # print only one empty line
+               # do not print empty line after signoff
+               if ($line eq "") {
+                       next if ($empty || $signoff);
+                       $empty = 1;
+               } else {
+                       $empty = 0;
+               }
+
+               print format_log_line_html($line) . "<br/>\n";
+       }
+
+       if ($opts{'-final_empty_line'}) {
+               # end with single empty line
+               print "<br/>\n" unless $empty;
+       }
+}
+
+# return link target (what link points to)
+sub git_get_link_target {
+       my $hash = shift;
+       my $link_target;
+
+       # read link
+       open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+               or return;
+       {
+               local $/;
+               $link_target = <$fd>;
+       }
+       close $fd
+               or return;
+
+       return $link_target;
+}
+
+# given link target, and the directory (basedir) the link is in,
+# return target of link relative to top directory (top tree);
+# return undef if it is not possible (including absolute links).
+sub normalize_link_target {
+       my ($link_target, $basedir, $hash_base) = @_;
+
+       # we can normalize symlink target only if $hash_base is provided
+       return unless $hash_base;
+
+       # absolute symlinks (beginning with '/') cannot be normalized
+       return if (substr($link_target, 0, 1) eq '/');
+
+       # normalize link target to path from top (root) tree (dir)
+       my $path;
+       if ($basedir) {
+               $path = $basedir . '/' . $link_target;
+       } else {
+               # we are in top (root) tree (dir)
+               $path = $link_target;
+       }
+
+       # remove //, /./, and /../
+       my @path_parts;
+       foreach my $part (split('/', $path)) {
+               # discard '.' and ''
+               next if (!$part || $part eq '.');
+               # handle '..'
+               if ($part eq '..') {
+                       if (@path_parts) {
+                               pop @path_parts;
+                       } else {
+                               # link leads outside repository (outside top dir)
+                               return;
+                       }
+               } else {
+                       push @path_parts, $part;
+               }
+       }
+       $path = join('/', @path_parts);
+
+       return $path;
+}
+
+# print tree entry (row of git_tree), but without encompassing <tr> element
+sub git_print_tree_entry {
+       my ($t, $basedir, $hash_base, $have_blame) = @_;
+
+       my %base_key = ();
+       $base_key{'hash_base'} = $hash_base if defined $hash_base;
+
+       # The format of a table row is: mode list link.  Where mode is
+       # the mode of the entry, list is the name of the entry, an href,
+       # and link is the action links of the entry.
+
+       print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
+       if ($t->{'type'} eq "blob") {
+               print "<td class=\"list\">" .
+                       $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
+                                              file_name=>"$basedir$t->{'name'}", %base_key),
+                               -class => "list"}, esc_path($t->{'name'}));
+               if (S_ISLNK(oct $t->{'mode'})) {
+                       my $link_target = git_get_link_target($t->{'hash'});
+                       if ($link_target) {
+                               my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
+                               if (defined $norm_target) {
+                                       print " -> " .
+                                             $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
+                                                                    file_name=>$norm_target),
+                                                      -title => $norm_target}, esc_path($link_target));
+                               } else {
+                                       print " -> " . esc_path($link_target);
+                               }
+                       }
+               }
+               print "</td>\n";
+               print "<td class=\"link\">";
+               print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
+                                            file_name=>"$basedir$t->{'name'}", %base_key)},
+                             "blob");
+               if ($have_blame) {
+                       print " | " .
+                             $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
+                                                    file_name=>"$basedir$t->{'name'}", %base_key)},
+                                     "blame");
+               }
+               if (defined $hash_base) {
+                       print " | " .
+                             $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
+                                                    hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
+                                     "history");
+               }
+               print " | " .
+                       $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
+                                              file_name=>"$basedir$t->{'name'}")},
+                               "raw");
+               print "</td>\n";
+
+       } elsif ($t->{'type'} eq "tree") {
+               print "<td class=\"list\">";
+               print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
+                                            file_name=>"$basedir$t->{'name'}", %base_key)},
+                             esc_path($t->{'name'}));
+               print "</td>\n";
+               print "<td class=\"link\">";
+               print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
+                                            file_name=>"$basedir$t->{'name'}", %base_key)},
+                             "tree");
+               if (defined $hash_base) {
+                       print " | " .
+                             $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
+                                                    file_name=>"$basedir$t->{'name'}")},
+                                     "history");
+               }
+               print "</td>\n";
+       } else {
+               # unknown object: we can only present history for it
+               # (this includes 'commit' object, i.e. submodule support)
+               print "<td class=\"list\">" .
+                     esc_path($t->{'name'}) .
+                     "</td>\n";
+               print "<td class=\"link\">";
+               if (defined $hash_base) {
+                       print $cgi->a({-href => href(action=>"history",
+                                                    hash_base=>$hash_base,
+                                                    file_name=>"$basedir$t->{'name'}")},
+                                     "history");
+               }
+               print "</td>\n";
+       }
+}
+
+## ......................................................................
+## functions printing large fragments of HTML
+
+# get pre-image filenames for merge (combined) diff
+sub fill_from_file_info {
+       my ($diff, @parents) = @_;
+
+       $diff->{'from_file'} = [ ];
+       $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
+       for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
+               if ($diff->{'status'}[$i] eq 'R' ||
+                   $diff->{'status'}[$i] eq 'C') {
+                       $diff->{'from_file'}[$i] =
+                               git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
+               }
+       }
+
+       return $diff;
+}
+
+# is current raw difftree line of file deletion
+sub is_deleted {
+       my $diffinfo = shift;
+
+       return $diffinfo->{'status_str'} =~ /D/;
+}
+
+# does patch correspond to [previous] difftree raw line
+# $diffinfo  - hashref of parsed raw diff format
+# $patchinfo - hashref of parsed patch diff format
+#              (the same keys as in $diffinfo)
+sub is_patch_split {
+       my ($diffinfo, $patchinfo) = @_;
+
+       return defined $diffinfo && defined $patchinfo
+               && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
+}
+
+
+sub git_difftree_body {
+       my ($difftree, $hash, @parents) = @_;
+       my ($parent) = $parents[0];
+       my ($have_blame) = gitweb_check_feature('blame');
+       print "<div class=\"list_head\">\n";
+       if ($#{$difftree} > 10) {
+               print(($#{$difftree} + 1) . " files changed:\n");
+       }
+       print "</div>\n";
+
+       print "<table class=\"" .
+             (@parents > 1 ? "combined " : "") .
+             "diff_tree\">\n";
+
+       # header only for combined diff in 'commitdiff' view
+       my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
+       if ($has_header) {
+               # table header
+               print "<thead><tr>\n" .
+                      "<th></th><th></th>\n"; # filename, patchN link
+               for (my $i = 0; $i < @parents; $i++) {
+                       my $par = $parents[$i];
+                       print "<th>" .
+                             $cgi->a({-href => href(action=>"commitdiff",
+                                                    hash=>$hash, hash_parent=>$par),
+                                      -title => 'commitdiff to parent number ' .
+                                                 ($i+1) . ': ' . substr($par,0,7)},
+                                     $i+1) .
+                             "&nbsp;</th>\n";
+               }
+               print "</tr></thead>\n<tbody>\n";
+       }
+
+       my $alternate = 1;
+       my $patchno = 0;
+       foreach my $line (@{$difftree}) {
+               my $diff = parsed_difftree_line($line);
+
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+
+               if (exists $diff->{'nparents'}) { # combined diff
+
+                       fill_from_file_info($diff, @parents)
+                               unless exists $diff->{'from_file'};
+
+                       if (!is_deleted($diff)) {
+                               # file exists in the result (child) commit
+                               print "<td>" .
+                                     $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                            file_name=>$diff->{'to_file'},
+                                                            hash_base=>$hash),
+                                             -class => "list"}, esc_path($diff->{'to_file'})) .
+                                     "</td>\n";
+                       } else {
+                               print "<td>" .
+                                     esc_path($diff->{'to_file'}) .
+                                     "</td>\n";
+                       }
+
+                       if ($action eq 'commitdiff') {
+                               # link to patch
+                               $patchno++;
+                               print "<td class=\"link\">" .
+                                     $cgi->a({-href => "#patch$patchno"}, "patch") .
+                                     " | " .
+                                     "</td>\n";
+                       }
+
+                       my $has_history = 0;
+                       my $not_deleted = 0;
+                       for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
+                               my $hash_parent = $parents[$i];
+                               my $from_hash = $diff->{'from_id'}[$i];
+                               my $from_path = $diff->{'from_file'}[$i];
+                               my $status = $diff->{'status'}[$i];
+
+                               $has_history ||= ($status ne 'A');
+                               $not_deleted ||= ($status ne 'D');
+
+                               if ($status eq 'A') {
+                                       print "<td  class=\"link\" align=\"right\"> | </td>\n";
+                               } elsif ($status eq 'D') {
+                                       print "<td class=\"link\">" .
+                                             $cgi->a({-href => href(action=>"blob",
+                                                                    hash_base=>$hash,
+                                                                    hash=>$from_hash,
+                                                                    file_name=>$from_path)},
+                                                     "blob" . ($i+1)) .
+                                             " | </td>\n";
+                               } else {
+                                       if ($diff->{'to_id'} eq $from_hash) {
+                                               print "<td class=\"link nochange\">";
+                                       } else {
+                                               print "<td class=\"link\">";
+                                       }
+                                       print $cgi->a({-href => href(action=>"blobdiff",
+                                                                    hash=>$diff->{'to_id'},
+                                                                    hash_parent=>$from_hash,
+                                                                    hash_base=>$hash,
+                                                                    hash_parent_base=>$hash_parent,
+                                                                    file_name=>$diff->{'to_file'},
+                                                                    file_parent=>$from_path)},
+                                                     "diff" . ($i+1)) .
+                                             " | </td>\n";
+                               }
+                       }
+
+                       print "<td class=\"link\">";
+                       if ($not_deleted) {
+                               print $cgi->a({-href => href(action=>"blob",
+                                                            hash=>$diff->{'to_id'},
+                                                            file_name=>$diff->{'to_file'},
+                                                            hash_base=>$hash)},
+                                             "blob");
+                               print " | " if ($has_history);
+                       }
+                       if ($has_history) {
+                               print $cgi->a({-href => href(action=>"history",
+                                                            file_name=>$diff->{'to_file'},
+                                                            hash_base=>$hash)},
+                                             "history");
+                       }
+                       print "</td>\n";
+
+                       print "</tr>\n";
+                       next; # instead of 'else' clause, to avoid extra indent
+               }
+               # else ordinary diff
+
+               my ($to_mode_oct, $to_mode_str, $to_file_type);
+               my ($from_mode_oct, $from_mode_str, $from_file_type);
+               if ($diff->{'to_mode'} ne ('0' x 6)) {
+                       $to_mode_oct = oct $diff->{'to_mode'};
+                       if (S_ISREG($to_mode_oct)) { # only for regular file
+                               $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
+                       }
+                       $to_file_type = file_type($diff->{'to_mode'});
+               }
+               if ($diff->{'from_mode'} ne ('0' x 6)) {
+                       $from_mode_oct = oct $diff->{'from_mode'};
+                       if (S_ISREG($to_mode_oct)) { # only for regular file
+                               $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
+                       }
+                       $from_file_type = file_type($diff->{'from_mode'});
+               }
+
+               if ($diff->{'status'} eq "A") { # created
+                       my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
+                       $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
+                       $mode_chng   .= "]</span>";
+                       print "<td>";
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff->{'file'}),
+                                     -class => "list"}, esc_path($diff->{'file'}));
+                       print "</td>\n";
+                       print "<td>$mode_chng</td>\n";
+                       print "<td class=\"link\">";
+                       if ($action eq 'commitdiff') {
+                               # link to patch
+                               $patchno++;
+                               print $cgi->a({-href => "#patch$patchno"}, "patch");
+                               print " | ";
+                       }
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff->{'file'})},
+                                     "blob");
+                       print "</td>\n";
+
+               } elsif ($diff->{'status'} eq "D") { # deleted
+                       my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
+                       print "<td>";
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
+                                                    hash_base=>$parent, file_name=>$diff->{'file'}),
+                                      -class => "list"}, esc_path($diff->{'file'}));
+                       print "</td>\n";
+                       print "<td>$mode_chng</td>\n";
+                       print "<td class=\"link\">";
+                       if ($action eq 'commitdiff') {
+                               # link to patch
+                               $patchno++;
+                               print $cgi->a({-href => "#patch$patchno"}, "patch");
+                               print " | ";
+                       }
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
+                                                    hash_base=>$parent, file_name=>$diff->{'file'})},
+                                     "blob") . " | ";
+                       if ($have_blame) {
+                               print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
+                                                            file_name=>$diff->{'file'})},
+                                             "blame") . " | ";
+                       }
+                       print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
+                                                    file_name=>$diff->{'file'})},
+                                     "history");
+                       print "</td>\n";
+
+               } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
+                       my $mode_chnge = "";
+                       if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
+                               $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
+                               if ($from_file_type ne $to_file_type) {
+                                       $mode_chnge .= " from $from_file_type to $to_file_type";
+                               }
+                               if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
+                                       if ($from_mode_str && $to_mode_str) {
+                                               $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
+                                       } elsif ($to_mode_str) {
+                                               $mode_chnge .= " mode: $to_mode_str";
+                                       }
+                               }
+                               $mode_chnge .= "]</span>\n";
+                       }
+                       print "<td>";
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff->{'file'}),
+                                     -class => "list"}, esc_path($diff->{'file'}));
+                       print "</td>\n";
+                       print "<td>$mode_chnge</td>\n";
+                       print "<td class=\"link\">";
+                       if ($action eq 'commitdiff') {
+                               # link to patch
+                               $patchno++;
+                               print $cgi->a({-href => "#patch$patchno"}, "patch") .
+                                     " | ";
+                       } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
+                               # "commit" view and modified file (not onlu mode changed)
+                               print $cgi->a({-href => href(action=>"blobdiff",
+                                                            hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
+                                                            hash_base=>$hash, hash_parent_base=>$parent,
+                                                            file_name=>$diff->{'file'})},
+                                             "diff") .
+                                     " | ";
+                       }
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff->{'file'})},
+                                      "blob") . " | ";
+                       if ($have_blame) {
+                               print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
+                                                            file_name=>$diff->{'file'})},
+                                             "blame") . " | ";
+                       }
+                       print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
+                                                    file_name=>$diff->{'file'})},
+                                     "history");
+                       print "</td>\n";
+
+               } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
+                       my %status_name = ('R' => 'moved', 'C' => 'copied');
+                       my $nstatus = $status_name{$diff->{'status'}};
+                       my $mode_chng = "";
+                       if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
+                               # mode also for directories, so we cannot use $to_mode_str
+                               $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
+                       }
+                       print "<td>" .
+                             $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
+                                                    hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
+                                     -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
+                             "<td><span class=\"file_status $nstatus\">[$nstatus from " .
+                             $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
+                                                    hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
+                                     -class => "list"}, esc_path($diff->{'from_file'})) .
+                             " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
+                             "<td class=\"link\">";
+                       if ($action eq 'commitdiff') {
+                               # link to patch
+                               $patchno++;
+                               print $cgi->a({-href => "#patch$patchno"}, "patch") .
+                                     " | ";
+                       } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
+                               # "commit" view and modified file (not only pure rename or copy)
+                               print $cgi->a({-href => href(action=>"blobdiff",
+                                                            hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
+                                                            hash_base=>$hash, hash_parent_base=>$parent,
+                                                            file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
+                                             "diff") .
+                                     " | ";
+                       }
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$parent, file_name=>$diff->{'to_file'})},
+                                     "blob") . " | ";
+                       if ($have_blame) {
+                               print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
+                                                            file_name=>$diff->{'to_file'})},
+                                             "blame") . " | ";
+                       }
+                       print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
+                                                   file_name=>$diff->{'to_file'})},
+                                     "history");
+                       print "</td>\n";
+
+               } # we should not encounter Unmerged (U) or Unknown (X) status
+               print "</tr>\n";
+       }
+       print "</tbody>" if $has_header;
+       print "</table>\n";
+}
+
+sub git_patchset_body {
+       my ($fd, $difftree, $hash, @hash_parents) = @_;
+       my ($hash_parent) = $hash_parents[0];
+
+       my $is_combined = (@hash_parents > 1);
+       my $patch_idx = 0;
+       my $patch_number = 0;
+       my $patch_line;
+       my $diffinfo;
+       my $to_name;
+       my (%from, %to);
+
+       print "<div class=\"patchset\">\n";
+
+       # skip to first patch
+       while ($patch_line = <$fd>) {
+               chomp $patch_line;
+
+               last if ($patch_line =~ m/^diff /);
+       }
+
+ PATCH:
+       while ($patch_line) {
+
+               # parse "git diff" header line
+               if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
+                       # $1 is from_name, which we do not use
+                       $to_name = unquote($2);
+                       $to_name =~ s!^b/!!;
+               } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
+                       # $1 is 'cc' or 'combined', which we do not use
+                       $to_name = unquote($2);
+               } else {
+                       $to_name = undef;
+               }
+
+               # check if current patch belong to current raw line
+               # and parse raw git-diff line if needed
+               if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
+                       # this is continuation of a split patch
+                       print "<div class=\"patch cont\">\n";
+               } else {
+                       # advance raw git-diff output if needed
+                       $patch_idx++ if defined $diffinfo;
+
+                       # read and prepare patch information
+                       $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
+
+                       # compact combined diff output can have some patches skipped
+                       # find which patch (using pathname of result) we are at now;
+                       if ($is_combined) {
+                               while ($to_name ne $diffinfo->{'to_file'}) {
+                                       print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
+                                             format_diff_cc_simplified($diffinfo, @hash_parents) .
+                                             "</div>\n";  # class="patch"
+
+                                       $patch_idx++;
+                                       $patch_number++;
+
+                                       last if $patch_idx > $#$difftree;
+                                       $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
+                               }
+                       }
+
+                       # modifies %from, %to hashes
+                       parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
+
+                       # this is first patch for raw difftree line with $patch_idx index
+                       # we index @$difftree array from 0, but number patches from 1
+                       print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
+               }
+
+               # git diff header
+               #assert($patch_line =~ m/^diff /) if DEBUG;
+               #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
+               $patch_number++;
+               # print "git diff" header
+               print format_git_diff_header_line($patch_line, $diffinfo,
+                                                 \%from, \%to);
+
+               # print extended diff header
+               print "<div class=\"diff extended_header\">\n";
+       EXTENDED_HEADER:
+               while ($patch_line = <$fd>) {
+                       chomp $patch_line;
+
+                       last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
+
+                       print format_extended_diff_header_line($patch_line, $diffinfo,
+                                                              \%from, \%to);
+               }
+               print "</div>\n"; # class="diff extended_header"
+
+               # from-file/to-file diff header
+               if (! $patch_line) {
+                       print "</div>\n"; # class="patch"
+                       last PATCH;
+               }
+               next PATCH if ($patch_line =~ m/^diff /);
+               #assert($patch_line =~ m/^---/) if DEBUG;
+
+               my $last_patch_line = $patch_line;
+               $patch_line = <$fd>;
+               chomp $patch_line;
+               #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
+
+               print format_diff_from_to_header($last_patch_line, $patch_line,
+                                                $diffinfo, \%from, \%to,
+                                                @hash_parents);
+
+               # the patch itself
+       LINE:
+               while ($patch_line = <$fd>) {
+                       chomp $patch_line;
+
+                       next PATCH if ($patch_line =~ m/^diff /);
+
+                       print format_diff_line($patch_line, \%from, \%to);
+               }
+
+       } continue {
+               print "</div>\n"; # class="patch"
+       }
+
+       # for compact combined (--cc) format, with chunk and patch simpliciaction
+       # patchset might be empty, but there might be unprocessed raw lines
+       for (++$patch_idx if $patch_number > 0;
+            $patch_idx < @$difftree;
+            ++$patch_idx) {
+               # read and prepare patch information
+               $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
+
+               # generate anchor for "patch" links in difftree / whatchanged part
+               print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
+                     format_diff_cc_simplified($diffinfo, @hash_parents) .
+                     "</div>\n";  # class="patch"
+
+               $patch_number++;
+       }
+
+       if ($patch_number == 0) {
+               if (@hash_parents > 1) {
+                       print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
+               } else {
+                       print "<div class=\"diff nodifferences\">No differences found</div>\n";
+               }
+       }
+
+       print "</div>\n"; # class="patchset"
+}
+
+# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
+
+sub git_project_list_body {
+       my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
+
+       my ($check_forks) = gitweb_check_feature('forks');
+
+       my @projects;
+       foreach my $pr (@$projlist) {
+               my (@aa) = git_get_last_activity($pr->{'path'});
+               unless (@aa) {
+                       next;
+               }
+               ($pr->{'age'}, $pr->{'age_string'}) = @aa;
+               if (!defined $pr->{'descr'}) {
+                       my $descr = git_get_project_description($pr->{'path'}) || "";
+                       $pr->{'descr_long'} = to_utf8($descr);
+                       $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
+               }
+               if (!defined $pr->{'owner'}) {
+                       $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
+               }
+               if ($check_forks) {
+                       my $pname = $pr->{'path'};
+                       if (($pname =~ s/\.git$//) &&
+                           ($pname !~ /\/$/) &&
+                           (-d "$projectroot/$pname")) {
+                               $pr->{'forks'} = "-d $projectroot/$pname";
+                       }
+                       else {
+                               $pr->{'forks'} = 0;
+                       }
+               }
+               push @projects, $pr;
+       }
+
+       $order ||= $default_projects_order;
+       $from = 0 unless defined $from;
+       $to = $#projects if (!defined $to || $#projects < $to);
+
+       print "<table class=\"project_list\">\n";
+       unless ($no_header) {
+               print "<tr>\n";
+               if ($check_forks) {
+                       print "<th></th>\n";
+               }
+               if ($order eq "project") {
+                       @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
+                       print "<th>Project</th>\n";
+               } else {
+                       print "<th>" .
+                             $cgi->a({-href => href(project=>undef, order=>'project'),
+                                      -class => "header"}, "Project") .
+                             "</th>\n";
+               }
+               if ($order eq "descr") {
+                       @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
+                       print "<th>Description</th>\n";
+               } else {
+                       print "<th>" .
+                             $cgi->a({-href => href(project=>undef, order=>'descr'),
+                                      -class => "header"}, "Description") .
+                             "</th>\n";
+               }
+               if ($order eq "owner") {
+                       @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
+                       print "<th>Owner</th>\n";
+               } else {
+                       print "<th>" .
+                             $cgi->a({-href => href(project=>undef, order=>'owner'),
+                                      -class => "header"}, "Owner") .
+                             "</th>\n";
+               }
+               if ($order eq "age") {
+                       @projects = sort {$a->{'age'} <=> $b->{'age'}} @projects;
+                       print "<th>Last Change</th>\n";
+               } else {
+                       print "<th>" .
+                             $cgi->a({-href => href(project=>undef, order=>'age'),
+                                      -class => "header"}, "Last Change") .
+                             "</th>\n";
+               }
+               print "<th></th>\n" .
+                     "</tr>\n";
+       }
+       my $alternate = 1;
+       for (my $i = $from; $i <= $to; $i++) {
+               my $pr = $projects[$i];
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+               if ($check_forks) {
+                       print "<td>";
+                       if ($pr->{'forks'}) {
+                               print "<!-- $pr->{'forks'} -->\n";
+                               print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
+                       }
+                       print "</td>\n";
+               }
+               print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
+                                       -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
+                     "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
+                                       -class => "list", -title => $pr->{'descr_long'}},
+                                       esc_html($pr->{'descr'})) . "</td>\n" .
+                     "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
+               print "<td class=\"". age_class($pr->{'age'}) . "\">" .
+                     (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
+                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
+                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
+                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
+                     ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
+                     "</td>\n" .
+                     "</tr>\n";
+       }
+       if (defined $extra) {
+               print "<tr>\n";
+               if ($check_forks) {
+                       print "<td></td>\n";
+               }
+               print "<td colspan=\"5\">$extra</td>\n" .
+                     "</tr>\n";
+       }
+       print "</table>\n";
+}
+
+sub git_shortlog_body {
+       # uses global variable $project
+       my ($commitlist, $from, $to, $refs, $extra) = @_;
+
+       $from = 0 unless defined $from;
+       $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
+
+       print "<table class=\"shortlog\">\n";
+       my $alternate = 1;
+       for (my $i = $from; $i <= $to; $i++) {
+               my %co = %{$commitlist->[$i]};
+               my $commit = $co{'id'};
+               my $ref = format_ref_marker($refs, $commit);
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+               my $author = chop_and_escape_str($co{'author_name'}, 10);
+               # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
+               print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+                     "<td><i>" . $author . "</i></td>\n" .
+                     "<td>";
+               print format_subject_html($co{'title'}, $co{'title_short'},
+                                         href(action=>"commit", hash=>$commit), $ref);
+               print "</td>\n" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
+                     $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
+                     $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
+               my $snapshot_links = format_snapshot_links($commit);
+               if (defined $snapshot_links) {
+                       print " | " . $snapshot_links;
+               }
+               print "</td>\n" .
+                     "</tr>\n";
+       }
+       if (defined $extra) {
+               print "<tr>\n" .
+                     "<td colspan=\"4\">$extra</td>\n" .
+                     "</tr>\n";
+       }
+       print "</table>\n";
+}
+
+sub git_history_body {
+       # Warning: assumes constant type (blob or tree) during history
+       my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
+
+       $from = 0 unless defined $from;
+       $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
+
+       print "<table class=\"history\">\n";
+       my $alternate = 1;
+       for (my $i = $from; $i <= $to; $i++) {
+               my %co = %{$commitlist->[$i]};
+               if (!%co) {
+                       next;
+               }
+               my $commit = $co{'id'};
+
+               my $ref = format_ref_marker($refs, $commit);
+
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+       # shortlog uses      chop_str($co{'author_name'}, 10)
+               my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
+               print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+                     "<td><i>" . $author . "</i></td>\n" .
+                     "<td>";
+               # originally git_history used chop_str($co{'title'}, 50)
+               print format_subject_html($co{'title'}, $co{'title_short'},
+                                         href(action=>"commit", hash=>$commit), $ref);
+               print "</td>\n" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
+                     $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
+
+               if ($ftype eq 'blob') {
+                       my $blob_current = git_get_hash_by_path($hash_base, $file_name);
+                       my $blob_parent  = git_get_hash_by_path($commit, $file_name);
+                       if (defined $blob_current && defined $blob_parent &&
+                                       $blob_current ne $blob_parent) {
+                               print " | " .
+                                       $cgi->a({-href => href(action=>"blobdiff",
+                                                              hash=>$blob_current, hash_parent=>$blob_parent,
+                                                              hash_base=>$hash_base, hash_parent_base=>$commit,
+                                                              file_name=>$file_name)},
+                                               "diff to current");
+                       }
+               }
+               print "</td>\n" .
+                     "</tr>\n";
+       }
+       if (defined $extra) {
+               print "<tr>\n" .
+                     "<td colspan=\"4\">$extra</td>\n" .
+                     "</tr>\n";
+       }
+       print "</table>\n";
+}
+
+sub git_tags_body {
+       # uses global variable $project
+       my ($taglist, $from, $to, $extra) = @_;
+       $from = 0 unless defined $from;
+       $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
+
+       print "<table class=\"tags\">\n";
+       my $alternate = 1;
+       for (my $i = $from; $i <= $to; $i++) {
+               my $entry = $taglist->[$i];
+               my %tag = %$entry;
+               my $comment = $tag{'subject'};
+               my $comment_short;
+               if (defined $comment) {
+                       $comment_short = chop_str($comment, 30, 5);
+               }
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+               if (defined $tag{'age'}) {
+                       print "<td><i>$tag{'age'}</i></td>\n";
+               } else {
+                       print "<td></td>\n";
+               }
+               print "<td>" .
+                     $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
+                              -class => "list name"}, esc_html($tag{'name'})) .
+                     "</td>\n" .
+                     "<td>";
+               if (defined $comment) {
+                       print format_subject_html($comment, $comment_short,
+                                                 href(action=>"tag", hash=>$tag{'id'}));
+               }
+               print "</td>\n" .
+                     "<td class=\"selflink\">";
+               if ($tag{'type'} eq "tag") {
+                       print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
+               } else {
+                       print "&nbsp;";
+               }
+               print "</td>\n" .
+                     "<td class=\"link\">" . " | " .
+                     $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
+               if ($tag{'reftype'} eq "commit") {
+                       print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") .
+                             " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log");
+               } elsif ($tag{'reftype'} eq "blob") {
+                       print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
+               }
+               print "</td>\n" .
+                     "</tr>";
+       }
+       if (defined $extra) {
+               print "<tr>\n" .
+                     "<td colspan=\"5\">$extra</td>\n" .
+                     "</tr>\n";
+       }
+       print "</table>\n";
+}
+
+sub git_heads_body {
+       # uses global variable $project
+       my ($headlist, $head, $from, $to, $extra) = @_;
+       $from = 0 unless defined $from;
+       $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
+
+       print "<table class=\"heads\">\n";
+       my $alternate = 1;
+       for (my $i = $from; $i <= $to; $i++) {
+               my $entry = $headlist->[$i];
+               my %ref = %$entry;
+               my $curr = $ref{'id'} eq $head;
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+               print "<td><i>$ref{'age'}</i></td>\n" .
+                     ($curr ? "<td class=\"current_head\">" : "<td>") .
+                     $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'}),
+                              -class => "list name"},esc_html($ref{'name'})) .
+                     "</td>\n" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'})}, "shortlog") . " | " .
+                     $cgi->a({-href => href(action=>"log", hash=>$ref{'name'})}, "log") . " | " .
+                     $cgi->a({-href => href(action=>"tree", hash=>$ref{'name'}, hash_base=>$ref{'name'})}, "tree") .
+                     "</td>\n" .
+                     "</tr>";
+       }
+       if (defined $extra) {
+               print "<tr>\n" .
+                     "<td colspan=\"3\">$extra</td>\n" .
+                     "</tr>\n";
+       }
+       print "</table>\n";
+}
+
+sub git_search_grep_body {
+       my ($commitlist, $from, $to, $extra) = @_;
+       $from = 0 unless defined $from;
+       $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
+
+       print "<table class=\"commit_search\">\n";
+       my $alternate = 1;
+       for (my $i = $from; $i <= $to; $i++) {
+               my %co = %{$commitlist->[$i]};
+               if (!%co) {
+                       next;
+               }
+               my $commit = $co{'id'};
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+               my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
+               print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+                     "<td><i>" . $author . "</i></td>\n" .
+                     "<td>" .
+                     $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
+                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
+               my $comment = $co{'comment'};
+               foreach my $line (@$comment) {
+                       if ($line =~ m/^(.*)($search_regexp)(.*)$/i) {
+                               my $lead = esc_html($1) || "";
+                               $lead = chop_str($lead, 30, 10);
+                               my $match = esc_html($2) || "";
+                               my $trail = esc_html($3) || "";
+                               $trail = chop_str($trail, 30, 10);
+                               my $text = "$lead<span class=\"match\">$match</span>$trail";
+                               print chop_str($text, 80, 5) . "<br/>\n";
+                       }
+               }
+               print "</td>\n" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
+                     " | " .
+                     $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
+                     " | " .
+                     $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
+               print "</td>\n" .
+                     "</tr>\n";
+       }
+       if (defined $extra) {
+               print "<tr>\n" .
+                     "<td colspan=\"3\">$extra</td>\n" .
+                     "</tr>\n";
+       }
+       print "</table>\n";
+}
+
+## ======================================================================
+## ======================================================================
+## actions
+
+sub git_project_list {
+       my $order = $cgi->param('o');
+       if (defined $order && $order !~ m/none|project|descr|owner|age/) {
+               die_error(undef, "Unknown order parameter");
+       }
+
+       my @list = git_get_projects_list();
+       if (!@list) {
+               die_error(undef, "No projects found");
+       }
+
+       git_header_html();
+       if (-f $home_text) {
+               print "<div class=\"index_include\">\n";
+               open (my $fd, $home_text);
+               print <$fd>;
+               close $fd;
+               print "</div>\n";
+       }
+       git_project_list_body(\@list, $order);
+       git_footer_html();
+}
+
+sub git_forks {
+       my $order = $cgi->param('o');
+       if (defined $order && $order !~ m/none|project|descr|owner|age/) {
+               die_error(undef, "Unknown order parameter");
+       }
+
+       my @list = git_get_projects_list($project);
+       if (!@list) {
+               die_error(undef, "No forks found");
+       }
+
+       git_header_html();
+       git_print_page_nav('','');
+       git_print_header_div('summary', "$project forks");
+       git_project_list_body(\@list, $order);
+       git_footer_html();
+}
+
+sub git_project_index {
+       my @projects = git_get_projects_list($project);
+
+       print $cgi->header(
+               -type => 'text/plain',
+               -charset => 'utf-8',
+               -content_disposition => 'inline; filename="index.aux"');
+
+       foreach my $pr (@projects) {
+               if (!exists $pr->{'owner'}) {
+                       $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
+               }
+
+               my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
+               # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
+               $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
+               $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
+               $path  =~ s/ /\+/g;
+               $owner =~ s/ /\+/g;
+
+               print "$path $owner\n";
+       }
+}
+
+sub git_summary {
+       my $descr = git_get_project_description($project) || "none";
+       my %co = parse_commit("HEAD");
+       my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
+       my $head = $co{'id'};
+
+       my $owner = git_get_project_owner($project);
+
+       my $refs = git_get_references();
+       # These get_*_list functions return one more to allow us to see if
+       # there are more ...
+       my @taglist  = git_get_tags_list(16);
+       my @headlist = git_get_heads_list(16);
+       my @forklist;
+       my ($check_forks) = gitweb_check_feature('forks');
+
+       if ($check_forks) {
+               @forklist = git_get_projects_list($project);
+       }
+
+       git_header_html();
+       git_print_page_nav('summary','', $head);
+
+       print "<div class=\"title\">&nbsp;</div>\n";
+       print "<table class=\"projects_list\">\n" .
+             "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
+             "<tr><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
+       if (defined $cd{'rfc2822'}) {
+               print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
+       }
+
+       # use per project git URL list in $projectroot/$project/cloneurl
+       # or make project git URL from git base URL and project name
+       my $url_tag = "URL";
+       my @url_list = git_get_project_url_list($project);
+       @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
+       foreach my $git_url (@url_list) {
+               next unless $git_url;
+               print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
+               $url_tag = "";
+       }
+       print "</table>\n";
+
+       if (-s "$projectroot/$project/README.html") {
+               if (open my $fd, "$projectroot/$project/README.html") {
+                       print "<div class=\"title\">readme</div>\n" .
+                             "<div class=\"readme\">\n";
+                       print $_ while (<$fd>);
+                       print "\n</div>\n"; # class="readme"
+                       close $fd;
+               }
+       }
+
+       # we need to request one more than 16 (0..15) to check if
+       # those 16 are all
+       my @commitlist = $head ? parse_commits($head, 17) : ();
+       if (@commitlist) {
+               git_print_header_div('shortlog');
+               git_shortlog_body(\@commitlist, 0, 15, $refs,
+                                 $#commitlist <=  15 ? undef :
+                                 $cgi->a({-href => href(action=>"shortlog")}, "..."));
+       }
+
+       if (@taglist) {
+               git_print_header_div('tags');
+               git_tags_body(\@taglist, 0, 15,
+                             $#taglist <=  15 ? undef :
+                             $cgi->a({-href => href(action=>"tags")}, "..."));
+       }
+
+       if (@headlist) {
+               git_print_header_div('heads');
+               git_heads_body(\@headlist, $head, 0, 15,
+                              $#headlist <= 15 ? undef :
+                              $cgi->a({-href => href(action=>"heads")}, "..."));
+       }
+
+       if (@forklist) {
+               git_print_header_div('forks');
+               git_project_list_body(\@forklist, undef, 0, 15,
+                                     $#forklist <= 15 ? undef :
+                                     $cgi->a({-href => href(action=>"forks")}, "..."),
+                                     'noheader');
+       }
+
+       git_footer_html();
+}
+
+sub git_tag {
+       my $head = git_get_head_hash($project);
+       git_header_html();
+       git_print_page_nav('','', $head,undef,$head);
+       my %tag = parse_tag($hash);
+
+       if (! %tag) {
+               die_error(undef, "Unknown tag object");
+       }
+
+       git_print_header_div('commit', esc_html($tag{'name'}), $hash);
+       print "<div class=\"title_text\">\n" .
+             "<table class=\"object_header\">\n" .
+             "<tr>\n" .
+             "<td>object</td>\n" .
+             "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
+                              $tag{'object'}) . "</td>\n" .
+             "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
+                                             $tag{'type'}) . "</td>\n" .
+             "</tr>\n";
+       if (defined($tag{'author'})) {
+               my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
+               print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
+               print "<tr><td></td><td>" . $ad{'rfc2822'} .
+                       sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
+                       "</td></tr>\n";
+       }
+       print "</table>\n\n" .
+             "</div>\n";
+       print "<div class=\"page_body\">";
+       my $comment = $tag{'comment'};
+       foreach my $line (@$comment) {
+               chomp $line;
+               print esc_html($line, -nbsp=>1) . "<br/>\n";
+       }
+       print "</div>\n";
+       git_footer_html();
+}
+
+sub git_blame2 {
+       my $fd;
+       my $ftype;
+
+       my ($have_blame) = gitweb_check_feature('blame');
+       if (!$have_blame) {
+               die_error('403 Permission denied', "Permission denied");
+       }
+       die_error('404 Not Found', "File name not defined") if (!$file_name);
+       $hash_base ||= git_get_head_hash($project);
+       die_error(undef, "Couldn't find base commit") unless ($hash_base);
+       my %co = parse_commit($hash_base)
+               or die_error(undef, "Reading commit failed");
+       if (!defined $hash) {
+               $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
+                       or die_error(undef, "Error looking up file");
+       }
+       $ftype = git_get_type($hash);
+       if ($ftype !~ "blob") {
+               die_error('400 Bad Request', "Object is not a blob");
+       }
+       open ($fd, "-|", git_cmd(), "blame", '-p', '--',
+             $file_name, $hash_base)
+               or die_error(undef, "Open git-blame failed");
+       git_header_html();
+       my $formats_nav =
+               $cgi->a({-href => href(action=>"blob", -replay=>1)},
+                       "blob") .
+               " | " .
+               $cgi->a({-href => href(action=>"history", -replay=>1)},
+                       "history") .
+               " | " .
+               $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
+                       "HEAD");
+       git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
+       git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
+       git_print_page_path($file_name, $ftype, $hash_base);
+       my @rev_color = (qw(light2 dark2));
+       my $num_colors = scalar(@rev_color);
+       my $current_color = 0;
+       my $last_rev;
+       print <<HTML;
+<div class="page_body">
+<table class="blame">
+<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
+HTML
+       my %metainfo = ();
+       while (1) {
+               $_ = <$fd>;
+               last unless defined $_;
+               my ($full_rev, $orig_lineno, $lineno, $group_size) =
+                   /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
+               if (!exists $metainfo{$full_rev}) {
+                       $metainfo{$full_rev} = {};
+               }
+               my $meta = $metainfo{$full_rev};
+               while (<$fd>) {
+                       last if (s/^\t//);
+                       if (/^(\S+) (.*)$/) {
+                               $meta->{$1} = $2;
+                       }
+               }
+               my $data = $_;
+               chomp $data;
+               my $rev = substr($full_rev, 0, 8);
+               my $author = $meta->{'author'};
+               my %date = parse_date($meta->{'author-time'},
+                                     $meta->{'author-tz'});
+               my $date = $date{'iso-tz'};
+               if ($group_size) {
+                       $current_color = ++$current_color % $num_colors;
+               }
+               print "<tr class=\"$rev_color[$current_color]\">\n";
+               if ($group_size) {
+                       print "<td class=\"sha1\"";
+                       print " title=\"". esc_html($author) . ", $date\"";
+                       print " rowspan=\"$group_size\"" if ($group_size > 1);
+                       print ">";
+                       print $cgi->a({-href => href(action=>"commit",
+                                                    hash=>$full_rev,
+                                                    file_name=>$file_name)},
+                                     esc_html($rev));
+                       print "</td>\n";
+               }
+               open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
+                       or die_error(undef, "Open git-rev-parse failed");
+               my $parent_commit = <$dd>;
+               close $dd;
+               chomp($parent_commit);
+               my $blamed = href(action => 'blame',
+                                 file_name => $meta->{'filename'},
+                                 hash_base => $parent_commit);
+               print "<td class=\"linenr\">";
+               print $cgi->a({ -href => "$blamed#l$orig_lineno",
+                               -id => "l$lineno",
+                               -class => "linenr" },
+                             esc_html($lineno));
+               print "</td>";
+               print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
+               print "</tr>\n";
+       }
+       print "</table>\n";
+       print "</div>";
+       close $fd
+               or print "Reading blob failed\n";
+       git_footer_html();
+}
+
+sub git_blame {
+       my $fd;
+
+       my ($have_blame) = gitweb_check_feature('blame');
+       if (!$have_blame) {
+               die_error('403 Permission denied', "Permission denied");
+       }
+       die_error('404 Not Found', "File name not defined") if (!$file_name);
+       $hash_base ||= git_get_head_hash($project);
+       die_error(undef, "Couldn't find base commit") unless ($hash_base);
+       my %co = parse_commit($hash_base)
+               or die_error(undef, "Reading commit failed");
+       if (!defined $hash) {
+               $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
+                       or die_error(undef, "Error lookup file");
+       }
+       open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base)
+               or die_error(undef, "Open git-annotate failed");
+       git_header_html();
+       my $formats_nav =
+               $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
+                       "blob") .
+               " | " .
+               $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
+                       "history") .
+               " | " .
+               $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
+                       "HEAD");
+       git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
+       git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
+       git_print_page_path($file_name, 'blob', $hash_base);
+       print "<div class=\"page_body\">\n";
+       print <<HTML;
+<table class="blame">
+  <tr>
+    <th>Commit</th>
+    <th>Age</th>
+    <th>Author</th>
+    <th>Line</th>
+    <th>Data</th>
+  </tr>
+HTML
+       my @line_class = (qw(light dark));
+       my $line_class_len = scalar (@line_class);
+       my $line_class_num = $#line_class;
+       while (my $line = <$fd>) {
+               my $long_rev;
+               my $short_rev;
+               my $author;
+               my $time;
+               my $lineno;
+               my $data;
+               my $age;
+               my $age_str;
+               my $age_class;
+
+               chomp $line;
+               $line_class_num = ($line_class_num + 1) % $line_class_len;
+
+               if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) {
+                       $long_rev = $1;
+                       $author   = $2;
+                       $time     = $3;
+                       $lineno   = $4;
+                       $data     = $5;
+               } else {
+                       print qq(  <tr><td colspan="5" class="error">Unable to parse: $line</td></tr>\n);
+                       next;
+               }
+               $short_rev  = substr ($long_rev, 0, 8);
+               $age        = time () - $time;
+               $age_str    = age_string ($age);
+               $age_str    =~ s/ /&nbsp;/g;
+               $age_class  = age_class($age);
+               $author     = esc_html ($author);
+               $author     =~ s/ /&nbsp;/g;
+
+               $data = untabify($data);
+               $data = esc_html ($data);
+
+               print <<HTML;
+  <tr class="$line_class[$line_class_num]">
+    <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
+    <td class="$age_class">$age_str</td>
+    <td>$author</td>
+    <td class="linenr"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td>
+    <td class="pre">$data</td>
+  </tr>
+HTML
+       } # while (my $line = <$fd>)
+       print "</table>\n\n";
+       close $fd
+               or print "Reading blob failed.\n";
+       print "</div>";
+       git_footer_html();
+}
+
+sub git_tags {
+       my $head = git_get_head_hash($project);
+       git_header_html();
+       git_print_page_nav('','', $head,undef,$head);
+       git_print_header_div('summary', $project);
+
+       my @tagslist = git_get_tags_list();
+       if (@tagslist) {
+               git_tags_body(\@tagslist);
+       }
+       git_footer_html();
+}
+
+sub git_heads {
+       my $head = git_get_head_hash($project);
+       git_header_html();
+       git_print_page_nav('','', $head,undef,$head);
+       git_print_header_div('summary', $project);
+
+       my @headslist = git_get_heads_list();
+       if (@headslist) {
+               git_heads_body(\@headslist, $head);
+       }
+       git_footer_html();
+}
+
+sub git_blob_plain {
+       my $expires;
+
+       if (!defined $hash) {
+               if (defined $file_name) {
+                       my $base = $hash_base || git_get_head_hash($project);
+                       $hash = git_get_hash_by_path($base, $file_name, "blob")
+                               or die_error(undef, "Error lookup file");
+               } else {
+                       die_error(undef, "No file name defined");
+               }
+       } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
+               # blobs defined by non-textual hash id's can be cached
+               $expires = "+1d";
+       }
+
+       my $type = shift;
+       open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+               or die_error(undef, "Couldn't cat $file_name, $hash");
+
+       $type ||= blob_mimetype($fd, $file_name);
+
+       # save as filename, even when no $file_name is given
+       my $save_as = "$hash";
+       if (defined $file_name) {
+               $save_as = $file_name;
+       } elsif ($type =~ m/^text\//) {
+               $save_as .= '.txt';
+       }
+
+       print $cgi->header(
+               -type => "$type",
+               -expires=>$expires,
+               -content_disposition => 'inline; filename="' . "$save_as" . '"');
+       undef $/;
+       binmode STDOUT, ':raw';
+       print <$fd>;
+       binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
+       $/ = "\n";
+       close $fd;
+}
+
+sub git_blob {
+       my $expires;
+
+       if (!defined $hash) {
+               if (defined $file_name) {
+                       my $base = $hash_base || git_get_head_hash($project);
+                       $hash = git_get_hash_by_path($base, $file_name, "blob")
+                               or die_error(undef, "Error lookup file");
+               } else {
+                       die_error(undef, "No file name defined");
+               }
+       } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
+               # blobs defined by non-textual hash id's can be cached
+               $expires = "+1d";
+       }
+
+       my ($have_blame) = gitweb_check_feature('blame');
+       open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+               or die_error(undef, "Couldn't cat $file_name, $hash");
+       my $mimetype = blob_mimetype($fd, $file_name);
+       if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)!) {
+               close $fd;
+               return git_blob_plain($mimetype);
+       }
+       # we can have blame only for text/* mimetype
+       $have_blame &&= ($mimetype =~ m!^text/!);
+
+       git_header_html(undef, $expires);
+       my $formats_nav = '';
+       if (defined $hash_base && (my %co = parse_commit($hash_base))) {
+               if (defined $file_name) {
+                       if ($have_blame) {
+                               $formats_nav .=
+                                       $cgi->a({-href => href(action=>"blame", -replay=>1)},
+                                               "blame") .
+                                       " | ";
+                       }
+                       $formats_nav .=
+                               $cgi->a({-href => href(action=>"history", -replay=>1)},
+                                       "history") .
+                               " | " .
+                               $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
+                                       "raw") .
+                               " | " .
+                               $cgi->a({-href => href(action=>"blob",
+                                                      hash_base=>"HEAD", file_name=>$file_name)},
+                                       "HEAD");
+               } else {
+                       $formats_nav .=
+                               $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
+                                       "raw");
+               }
+               git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
+               git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
+       } else {
+               print "<div class=\"page_nav\">\n" .
+                     "<br/><br/></div>\n" .
+                     "<div class=\"title\">$hash</div>\n";
+       }
+       git_print_page_path($file_name, "blob", $hash_base);
+       print "<div class=\"page_body\">\n";
+       if ($mimetype =~ m!^text/!) {
+               my $nr;
+               while (my $line = <$fd>) {
+                       chomp $line;
+                       $nr++;
+                       $line = untabify($line);
+                       printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
+                              $nr, $nr, $nr, esc_html($line, -nbsp=>1);
+               }
+       } elsif ($mimetype =~ m!^image/!) {
+               print qq!<img type="$mimetype"!;
+               if ($file_name) {
+                       print qq! alt="$file_name" title="$file_name"!;
+               }
+               print qq! src="! .
+                     href(action=>"blob_plain", hash=>$hash,
+                          hash_base=>$hash_base, file_name=>$file_name) .
+                     qq!" />\n!;
+       }
+       close $fd
+               or print "Reading blob failed.\n";
+       print "</div>";
+       git_footer_html();
+}
+
+sub git_tree {
+       if (!defined $hash_base) {
+               $hash_base = "HEAD";
+       }
+       if (!defined $hash) {
+               if (defined $file_name) {
+                       $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
+               } else {
+                       $hash = $hash_base;
+               }
+       }
+       $/ = "\0";
+       open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
+               or die_error(undef, "Open git-ls-tree failed");
+       my @entries = map { chomp; $_ } <$fd>;
+       close $fd or die_error(undef, "Reading tree failed");
+       $/ = "\n";
+
+       my $refs = git_get_references();
+       my $ref = format_ref_marker($refs, $hash_base);
+       git_header_html();
+       my $basedir = '';
+       my ($have_blame) = gitweb_check_feature('blame');
+       if (defined $hash_base && (my %co = parse_commit($hash_base))) {
+               my @views_nav = ();
+               if (defined $file_name) {
+                       push @views_nav,
+                               $cgi->a({-href => href(action=>"history", -replay=>1)},
+                                       "history"),
+                               $cgi->a({-href => href(action=>"tree",
+                                                      hash_base=>"HEAD", file_name=>$file_name)},
+                                       "HEAD"),
+               }
+               my $snapshot_links = format_snapshot_links($hash);
+               if (defined $snapshot_links) {
+                       # FIXME: Should be available when we have no hash base as well.
+                       push @views_nav, $snapshot_links;
+               }
+               git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
+               git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
+       } else {
+               undef $hash_base;
+               print "<div class=\"page_nav\">\n";
+               print "<br/><br/></div>\n";
+               print "<div class=\"title\">$hash</div>\n";
+       }
+       if (defined $file_name) {
+               $basedir = $file_name;
+               if ($basedir ne '' && substr($basedir, -1) ne '/') {
+                       $basedir .= '/';
+               }
+       }
+       git_print_page_path($file_name, 'tree', $hash_base);
+       print "<div class=\"page_body\">\n";
+       print "<table class=\"tree\">\n";
+       my $alternate = 1;
+       # '..' (top directory) link if possible
+       if (defined $hash_base &&
+           defined $file_name && $file_name =~ m![^/]+$!) {
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+
+               my $up = $file_name;
+               $up =~ s!/?[^/]+$!!;
+               undef $up unless $up;
+               # based on git_print_tree_entry
+               print '<td class="mode">' . mode_str('040000') . "</td>\n";
+               print '<td class="list">';
+               print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
+                                            file_name=>$up)},
+                             "..");
+               print "</td>\n";
+               print "<td class=\"link\"></td>\n";
+
+               print "</tr>\n";
+       }
+       foreach my $line (@entries) {
+               my %t = parse_ls_tree_line($line, -z => 1);
+
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+
+               git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
+
+               print "</tr>\n";
+       }
+       print "</table>\n" .
+             "</div>";
+       git_footer_html();
+}
+
+sub git_snapshot {
+       my @supported_fmts = gitweb_check_feature('snapshot');
+       @supported_fmts = filter_snapshot_fmts(@supported_fmts);
+
+       my $format = $cgi->param('sf');
+       if (!@supported_fmts) {
+               die_error('403 Permission denied', "Permission denied");
+       }
+       # default to first supported snapshot format
+       $format ||= $supported_fmts[0];
+       if ($format !~ m/^[a-z0-9]+$/) {
+               die_error(undef, "Invalid snapshot format parameter");
+       } elsif (!exists($known_snapshot_formats{$format})) {
+               die_error(undef, "Unknown snapshot format");
+       } elsif (!grep($_ eq $format, @supported_fmts)) {
+               die_error(undef, "Unsupported snapshot format");
+       }
+
+       if (!defined $hash) {
+               $hash = git_get_head_hash($project);
+       }
+
+       my $git_command = git_cmd_str();
+       my $name = $project;
+       $name =~ s,([^/])/*\.git$,$1,;
+       $name = basename($name);
+       my $filename = to_utf8($name);
+       $name =~ s/\047/\047\\\047\047/g;
+       my $cmd;
+       $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
+       $cmd = "$git_command archive " .
+               "--format=$known_snapshot_formats{$format}{'format'} " .
+               "--prefix=\'$name\'/ $hash";
+       if (exists $known_snapshot_formats{$format}{'compressor'}) {
+               $cmd .= ' | ' . join ' ', @{$known_snapshot_formats{$format}{'compressor'}};
+       }
+
+       print $cgi->header(
+               -type => $known_snapshot_formats{$format}{'type'},
+               -content_disposition => 'inline; filename="' . "$filename" . '"',
+               -status => '200 OK');
+
+       open my $fd, "-|", $cmd
+               or die_error(undef, "Execute git-archive failed");
+       binmode STDOUT, ':raw';
+       print <$fd>;
+       binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
+       close $fd;
+}
+
+sub git_log {
+       my $head = git_get_head_hash($project);
+       if (!defined $hash) {
+               $hash = $head;
+       }
+       if (!defined $page) {
+               $page = 0;
+       }
+       my $refs = git_get_references();
+
+       my @commitlist = parse_commits($hash, 101, (100 * $page));
+
+       my $paging_nav = format_paging_nav('log', $hash, $head, $page, (100 * ($page+1)));
+
+       git_header_html();
+       git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
+
+       if (!@commitlist) {
+               my %co = parse_commit($hash);
+
+               git_print_header_div('summary', $project);
+               print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
+       }
+       my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
+       for (my $i = 0; $i <= $to; $i++) {
+               my %co = %{$commitlist[$i]};
+               next if !%co;
+               my $commit = $co{'id'};
+               my $ref = format_ref_marker($refs, $commit);
+               my %ad = parse_date($co{'author_epoch'});
+               git_print_header_div('commit',
+                              "<span class=\"age\">$co{'age_string'}</span>" .
+                              esc_html($co{'title'}) . $ref,
+                              $commit);
+               print "<div class=\"title_text\">\n" .
+                     "<div class=\"log_link\">\n" .
+                     $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
+                     " | " .
+                     $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
+                     " | " .
+                     $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
+                     "<br/>\n" .
+                     "</div>\n" .
+                     "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
+                     "</div>\n";
+
+               print "<div class=\"log_body\">\n";
+               git_print_log($co{'comment'}, -final_empty_line=> 1);
+               print "</div>\n";
+       }
+       if ($#commitlist >= 100) {
+               print "<div class=\"page_nav\">\n";
+               print $cgi->a({-href => href(-replay=>1, page=>$page+1),
+                              -accesskey => "n", -title => "Alt-n"}, "next");
+               print "</div>\n";
+       }
+       git_footer_html();
+}
+
+sub git_commit {
+       $hash ||= $hash_base || "HEAD";
+       my %co = parse_commit($hash);
+       if (!%co) {
+               die_error(undef, "Unknown commit object");
+       }
+       my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
+       my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
+
+       my $parent  = $co{'parent'};
+       my $parents = $co{'parents'}; # listref
+
+       # we need to prepare $formats_nav before any parameter munging
+       my $formats_nav;
+       if (!defined $parent) {
+               # --root commitdiff
+               $formats_nav .= '(initial)';
+       } elsif (@$parents == 1) {
+               # single parent commit
+               $formats_nav .=
+                       '(parent: ' .
+                       $cgi->a({-href => href(action=>"commit",
+                                              hash=>$parent)},
+                               esc_html(substr($parent, 0, 7))) .
+                       ')';
+       } else {
+               # merge commit
+               $formats_nav .=
+                       '(merge: ' .
+                       join(' ', map {
+                               $cgi->a({-href => href(action=>"commit",
+                                                      hash=>$_)},
+                                       esc_html(substr($_, 0, 7)));
+                       } @$parents ) .
+                       ')';
+       }
+
+       if (!defined $parent) {
+               $parent = "--root";
+       }
+       my @difftree;
+       open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
+               @diff_opts,
+               (@$parents <= 1 ? $parent : '-c'),
+               $hash, "--"
+               or die_error(undef, "Open git-diff-tree failed");
+       @difftree = map { chomp; $_ } <$fd>;
+       close $fd or die_error(undef, "Reading git-diff-tree failed");
+
+       # non-textual hash id's can be cached
+       my $expires;
+       if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
+               $expires = "+1d";
+       }
+       my $refs = git_get_references();
+       my $ref = format_ref_marker($refs, $co{'id'});
+
+       git_header_html(undef, $expires);
+       git_print_page_nav('commit', '',
+                          $hash, $co{'tree'}, $hash,
+                          $formats_nav);
+
+       if (defined $co{'parent'}) {
+               git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
+       } else {
+               git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
+       }
+       print "<div class=\"title_text\">\n" .
+             "<table class=\"object_header\">\n";
+       print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
+             "<tr>" .
+             "<td></td><td> $ad{'rfc2822'}";
+       if ($ad{'hour_local'} < 6) {
+               printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
+                      $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
+       } else {
+               printf(" (%02d:%02d %s)",
+                      $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
+       }
+       print "</td>" .
+             "</tr>\n";
+       print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
+       print "<tr><td></td><td> $cd{'rfc2822'}" .
+             sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
+             "</td></tr>\n";
+       print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
+       print "<tr>" .
+             "<td>tree</td>" .
+             "<td class=\"sha1\">" .
+             $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
+                      class => "list"}, $co{'tree'}) .
+             "</td>" .
+             "<td class=\"link\">" .
+             $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
+                     "tree");
+       my $snapshot_links = format_snapshot_links($hash);
+       if (defined $snapshot_links) {
+               print " | " . $snapshot_links;
+       }
+       print "</td>" .
+             "</tr>\n";
+
+       foreach my $par (@$parents) {
+               print "<tr>" .
+                     "<td>parent</td>" .
+                     "<td class=\"sha1\">" .
+                     $cgi->a({-href => href(action=>"commit", hash=>$par),
+                              class => "list"}, $par) .
+                     "</td>" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
+                     " | " .
+                     $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
+                     "</td>" .
+                     "</tr>\n";
+       }
+       print "</table>".
+             "</div>\n";
+
+       print "<div class=\"page_body\">\n";
+       git_print_log($co{'comment'});
+       print "</div>\n";
+
+       git_difftree_body(\@difftree, $hash, @$parents);
+
+       git_footer_html();
+}
+
+sub git_object {
+       # object is defined by:
+       # - hash or hash_base alone
+       # - hash_base and file_name
+       my $type;
+
+       # - hash or hash_base alone
+       if ($hash || ($hash_base && !defined $file_name)) {
+               my $object_id = $hash || $hash_base;
+
+               my $git_command = git_cmd_str();
+               open my $fd, "-|", "$git_command cat-file -t $object_id 2>/dev/null"
+                       or die_error('404 Not Found', "Object does not exist");
+               $type = <$fd>;
+               chomp $type;
+               close $fd
+                       or die_error('404 Not Found', "Object does not exist");
+
+       # - hash_base and file_name
+       } elsif ($hash_base && defined $file_name) {
+               $file_name =~ s,/+$,,;
+
+               system(git_cmd(), "cat-file", '-e', $hash_base) == 0
+                       or die_error('404 Not Found', "Base object does not exist");
+
+               # here errors should not hapen
+               open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
+                       or die_error(undef, "Open git-ls-tree failed");
+               my $line = <$fd>;
+               close $fd;
+
+               #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
+               unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
+                       die_error('404 Not Found', "File or directory for given base does not exist");
+               }
+               $type = $2;
+               $hash = $3;
+       } else {
+               die_error('404 Not Found', "Not enough information to find object");
+       }
+
+       print $cgi->redirect(-uri => href(action=>$type, -full=>1,
+                                         hash=>$hash, hash_base=>$hash_base,
+                                         file_name=>$file_name),
+                            -status => '302 Found');
+}
+
+sub git_blobdiff {
+       my $format = shift || 'html';
+
+       my $fd;
+       my @difftree;
+       my %diffinfo;
+       my $expires;
+
+       # preparing $fd and %diffinfo for git_patchset_body
+       # new style URI
+       if (defined $hash_base && defined $hash_parent_base) {
+               if (defined $file_name) {
+                       # read raw output
+                       open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+                               $hash_parent_base, $hash_base,
+                               "--", (defined $file_parent ? $file_parent : ()), $file_name
+                               or die_error(undef, "Open git-diff-tree failed");
+                       @difftree = map { chomp; $_ } <$fd>;
+                       close $fd
+                               or die_error(undef, "Reading git-diff-tree failed");
+                       @difftree
+                               or die_error('404 Not Found', "Blob diff not found");
+
+               } elsif (defined $hash &&
+                        $hash =~ /[0-9a-fA-F]{40}/) {
+                       # try to find filename from $hash
+
+                       # read filtered raw output
+                       open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+                               $hash_parent_base, $hash_base, "--"
+                               or die_error(undef, "Open git-diff-tree failed");
+                       @difftree =
+                               # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
+                               # $hash == to_id
+                               grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
+                               map { chomp; $_ } <$fd>;
+                       close $fd
+                               or die_error(undef, "Reading git-diff-tree failed");
+                       @difftree
+                               or die_error('404 Not Found', "Blob diff not found");
+
+               } else {
+                       die_error('404 Not Found', "Missing one of the blob diff parameters");
+               }
+
+               if (@difftree > 1) {
+                       die_error('404 Not Found', "Ambiguous blob diff specification");
+               }
+
+               %diffinfo = parse_difftree_raw_line($difftree[0]);
+               $file_parent ||= $diffinfo{'from_file'} || $file_name;
+               $file_name   ||= $diffinfo{'to_file'};
+
+               $hash_parent ||= $diffinfo{'from_id'};
+               $hash        ||= $diffinfo{'to_id'};
+
+               # non-textual hash id's can be cached
+               if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
+                   $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
+                       $expires = '+1d';
+               }
+
+               # open patch output
+               open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+                       '-p', ($format eq 'html' ? "--full-index" : ()),
+                       $hash_parent_base, $hash_base,
+                       "--", (defined $file_parent ? $file_parent : ()), $file_name
+                       or die_error(undef, "Open git-diff-tree failed");
+       }
+
+       # old/legacy style URI
+       if (!%diffinfo && # if new style URI failed
+           defined $hash && defined $hash_parent) {
+               # fake git-diff-tree raw output
+               $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
+               $diffinfo{'from_id'} = $hash_parent;
+               $diffinfo{'to_id'}   = $hash;
+               if (defined $file_name) {
+                       if (defined $file_parent) {
+                               $diffinfo{'status'} = '2';
+                               $diffinfo{'from_file'} = $file_parent;
+                               $diffinfo{'to_file'}   = $file_name;
+                       } else { # assume not renamed
+                               $diffinfo{'status'} = '1';
+                               $diffinfo{'from_file'} = $file_name;
+                               $diffinfo{'to_file'}   = $file_name;
+                       }
+               } else { # no filename given
+                       $diffinfo{'status'} = '2';
+                       $diffinfo{'from_file'} = $hash_parent;
+                       $diffinfo{'to_file'}   = $hash;
+               }
+
+               # non-textual hash id's can be cached
+               if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
+                   $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
+                       $expires = '+1d';
+               }
+
+               # open patch output
+               open $fd, "-|", git_cmd(), "diff", @diff_opts,
+                       '-p', ($format eq 'html' ? "--full-index" : ()),
+                       $hash_parent, $hash, "--"
+                       or die_error(undef, "Open git-diff failed");
+       } else  {
+               die_error('404 Not Found', "Missing one of the blob diff parameters")
+                       unless %diffinfo;
+       }
+
+       # header
+       if ($format eq 'html') {
+               my $formats_nav =
+                       $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
+                               "raw");
+               git_header_html(undef, $expires);
+               if (defined $hash_base && (my %co = parse_commit($hash_base))) {
+                       git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
+                       git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
+               } else {
+                       print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
+                       print "<div class=\"title\">$hash vs $hash_parent</div>\n";
+               }
+               if (defined $file_name) {
+                       git_print_page_path($file_name, "blob", $hash_base);
+               } else {
+                       print "<div class=\"page_path\"></div>\n";
+               }
+
+       } elsif ($format eq 'plain') {
+               print $cgi->header(
+                       -type => 'text/plain',
+                       -charset => 'utf-8',
+                       -expires => $expires,
+                       -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
+
+               print "X-Git-Url: " . $cgi->self_url() . "\n\n";
+
+       } else {
+               die_error(undef, "Unknown blobdiff format");
+       }
+
+       # patch
+       if ($format eq 'html') {
+               print "<div class=\"page_body\">\n";
+
+               git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
+               close $fd;
+
+               print "</div>\n"; # class="page_body"
+               git_footer_html();
+
+       } else {
+               while (my $line = <$fd>) {
+                       $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
+                       $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
+
+                       print $line;
+
+                       last if $line =~ m!^\+\+\+!;
+               }
+               local $/ = undef;
+               print <$fd>;
+               close $fd;
+       }
+}
+
+sub git_blobdiff_plain {
+       git_blobdiff('plain');
+}
+
+sub git_commitdiff {
+       my $format = shift || 'html';
+       $hash ||= $hash_base || "HEAD";
+       my %co = parse_commit($hash);
+       if (!%co) {
+               die_error(undef, "Unknown commit object");
+       }
+
+       # choose format for commitdiff for merge
+       if (! defined $hash_parent && @{$co{'parents'}} > 1) {
+               $hash_parent = '--cc';
+       }
+       # we need to prepare $formats_nav before almost any parameter munging
+       my $formats_nav;
+       if ($format eq 'html') {
+               $formats_nav =
+                       $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
+                               "raw");
+
+               if (defined $hash_parent &&
+                   $hash_parent ne '-c' && $hash_parent ne '--cc') {
+                       # commitdiff with two commits given
+                       my $hash_parent_short = $hash_parent;
+                       if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
+                               $hash_parent_short = substr($hash_parent, 0, 7);
+                       }
+                       $formats_nav .=
+                               ' (from';
+                       for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
+                               if ($co{'parents'}[$i] eq $hash_parent) {
+                                       $formats_nav .= ' parent ' . ($i+1);
+                                       last;
+                               }
+                       }
+                       $formats_nav .= ': ' .
+                               $cgi->a({-href => href(action=>"commitdiff",
+                                                      hash=>$hash_parent)},
+                                       esc_html($hash_parent_short)) .
+                               ')';
+               } elsif (!$co{'parent'}) {
+                       # --root commitdiff
+                       $formats_nav .= ' (initial)';
+               } elsif (scalar @{$co{'parents'}} == 1) {
+                       # single parent commit
+                       $formats_nav .=
+                               ' (parent: ' .
+                               $cgi->a({-href => href(action=>"commitdiff",
+                                                      hash=>$co{'parent'})},
+                                       esc_html(substr($co{'parent'}, 0, 7))) .
+                               ')';
+               } else {
+                       # merge commit
+                       if ($hash_parent eq '--cc') {
+                               $formats_nav .= ' | ' .
+                                       $cgi->a({-href => href(action=>"commitdiff",
+                                                              hash=>$hash, hash_parent=>'-c')},
+                                               'combined');
+                       } else { # $hash_parent eq '-c'
+                               $formats_nav .= ' | ' .
+                                       $cgi->a({-href => href(action=>"commitdiff",
+                                                              hash=>$hash, hash_parent=>'--cc')},
+                                               'compact');
+                       }
+                       $formats_nav .=
+                               ' (merge: ' .
+                               join(' ', map {
+                                       $cgi->a({-href => href(action=>"commitdiff",
+                                                              hash=>$_)},
+                                               esc_html(substr($_, 0, 7)));
+                               } @{$co{'parents'}} ) .
+                               ')';
+               }
+       }
+
+       my $hash_parent_param = $hash_parent;
+       if (!defined $hash_parent_param) {
+               # --cc for multiple parents, --root for parentless
+               $hash_parent_param =
+                       @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
+       }
+
+       # read commitdiff
+       my $fd;
+       my @difftree;
+       if ($format eq 'html') {
+               open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+                       "--no-commit-id", "--patch-with-raw", "--full-index",
+                       $hash_parent_param, $hash, "--"
+                       or die_error(undef, "Open git-diff-tree failed");
+
+               while (my $line = <$fd>) {
+                       chomp $line;
+                       # empty line ends raw part of diff-tree output
+                       last unless $line;
+                       push @difftree, scalar parse_difftree_raw_line($line);
+               }
+
+       } elsif ($format eq 'plain') {
+               open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+                       '-p', $hash_parent_param, $hash, "--"
+                       or die_error(undef, "Open git-diff-tree failed");
+
+       } else {
+               die_error(undef, "Unknown commitdiff format");
+       }
+
+       # non-textual hash id's can be cached
+       my $expires;
+       if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
+               $expires = "+1d";
+       }
+
+       # write commit message
+       if ($format eq 'html') {
+               my $refs = git_get_references();
+               my $ref = format_ref_marker($refs, $co{'id'});
+
+               git_header_html(undef, $expires);
+               git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
+               git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
+               git_print_authorship(\%co);
+               print "<div class=\"page_body\">\n";
+               if (@{$co{'comment'}} > 1) {
+                       print "<div class=\"log\">\n";
+                       git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
+                       print "</div>\n"; # class="log"
+               }
+
+       } elsif ($format eq 'plain') {
+               my $refs = git_get_references("tags");
+               my $tagname = git_get_rev_name_tags($hash);
+               my $filename = basename($project) . "-$hash.patch";
+
+               print $cgi->header(
+                       -type => 'text/plain',
+                       -charset => 'utf-8',
+                       -expires => $expires,
+                       -content_disposition => 'inline; filename="' . "$filename" . '"');
+               my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
+               print <<TEXT;
+From: $co{'author'}
+Date: $ad{'rfc2822'} ($ad{'tz_local'})
+Subject: $co{'title'}
+TEXT
+               print "X-Git-Tag: $tagname\n" if $tagname;
+               print "X-Git-Url: " . $cgi->self_url() . "\n\n";
+
+               foreach my $line (@{$co{'comment'}}) {
+                       print "$line\n";
+               }
+               print "---\n\n";
+       }
+
+       # write patch
+       if ($format eq 'html') {
+               my $use_parents = !defined $hash_parent ||
+                       $hash_parent eq '-c' || $hash_parent eq '--cc';
+               git_difftree_body(\@difftree, $hash,
+                                 $use_parents ? @{$co{'parents'}} : $hash_parent);
+               print "<br/>\n";
+
+               git_patchset_body($fd, \@difftree, $hash,
+                                 $use_parents ? @{$co{'parents'}} : $hash_parent);
+               close $fd;
+               print "</div>\n"; # class="page_body"
+               git_footer_html();
+
+       } elsif ($format eq 'plain') {
+               local $/ = undef;
+               print <$fd>;
+               close $fd
+                       or print "Reading git-diff-tree failed\n";
+       }
+}
+
+sub git_commitdiff_plain {
+       git_commitdiff('plain');
+}
+
+sub git_history {
+       if (!defined $hash_base) {
+               $hash_base = git_get_head_hash($project);
+       }
+       if (!defined $page) {
+               $page = 0;
+       }
+       my $ftype;
+       my %co = parse_commit($hash_base);
+       if (!%co) {
+               die_error(undef, "Unknown commit object");
+       }
+
+       my $refs = git_get_references();
+       my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
+
+       if (!defined $hash && defined $file_name) {
+               $hash = git_get_hash_by_path($hash_base, $file_name);
+       }
+       if (defined $hash) {
+               $ftype = git_get_type($hash);
+       }
+
+       my @commitlist = parse_commits($hash_base, 101, (100 * $page), "--full-history", $file_name);
+
+       my $paging_nav = '';
+       if ($page > 0) {
+               $paging_nav .=
+                       $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
+                                              file_name=>$file_name)},
+                               "first");
+               $paging_nav .= " &sdot; " .
+                       $cgi->a({-href => href(-replay=>1, page=>$page-1),
+                                -accesskey => "p", -title => "Alt-p"}, "prev");
+       } else {
+               $paging_nav .= "first";
+               $paging_nav .= " &sdot; prev";
+       }
+       my $next_link = '';
+       if ($#commitlist >= 100) {
+               $next_link =
+                       $cgi->a({-href => href(-replay=>1, page=>$page+1),
+                                -accesskey => "n", -title => "Alt-n"}, "next");
+               $paging_nav .= " &sdot; $next_link";
+       } else {
+               $paging_nav .= " &sdot; next";
+       }
+
+       git_header_html();
+       git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
+       git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
+       git_print_page_path($file_name, $ftype, $hash_base);
+
+       git_history_body(\@commitlist, 0, 99,
+                        $refs, $hash_base, $ftype, $next_link);
+
+       git_footer_html();
+}
+
+sub git_search {
+       my ($have_search) = gitweb_check_feature('search');
+       if (!$have_search) {
+               die_error('403 Permission denied', "Permission denied");
+       }
+       if (!defined $searchtext) {
+               die_error(undef, "Text field empty");
+       }
+       if (!defined $hash) {
+               $hash = git_get_head_hash($project);
+       }
+       my %co = parse_commit($hash);
+       if (!%co) {
+               die_error(undef, "Unknown commit object");
+       }
+       if (!defined $page) {
+               $page = 0;
+       }
+
+       $searchtype ||= 'commit';
+       if ($searchtype eq 'pickaxe') {
+               # pickaxe may take all resources of your box and run for several minutes
+               # with every query - so decide by yourself how public you make this feature
+               my ($have_pickaxe) = gitweb_check_feature('pickaxe');
+               if (!$have_pickaxe) {
+                       die_error('403 Permission denied', "Permission denied");
+               }
+       }
+       if ($searchtype eq 'grep') {
+               my ($have_grep) = gitweb_check_feature('grep');
+               if (!$have_grep) {
+                       die_error('403 Permission denied', "Permission denied");
+               }
+       }
+
+       git_header_html();
+
+       if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
+               my $greptype;
+               if ($searchtype eq 'commit') {
+                       $greptype = "--grep=";
+               } elsif ($searchtype eq 'author') {
+                       $greptype = "--author=";
+               } elsif ($searchtype eq 'committer') {
+                       $greptype = "--committer=";
+               }
+               $greptype .= $search_regexp;
+               my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype);
+
+               my $paging_nav = '';
+               if ($page > 0) {
+                       $paging_nav .=
+                               $cgi->a({-href => href(action=>"search", hash=>$hash,
+                                                      searchtext=>$searchtext, searchtype=>$searchtype)},
+                                       "first");
+                       $paging_nav .= " &sdot; " .
+                               $cgi->a({-href => href(-replay=>1, page=>$page-1),
+                                        -accesskey => "p", -title => "Alt-p"}, "prev");
+               } else {
+                       $paging_nav .= "first";
+                       $paging_nav .= " &sdot; prev";
+               }
+               my $next_link = '';
+               if ($#commitlist >= 100) {
+                       $next_link =
+                               $cgi->a({-href => href(-replay=>1, page=>$page+1),
+                                        -accesskey => "n", -title => "Alt-n"}, "next");
+                       $paging_nav .= " &sdot; $next_link";
+               } else {
+                       $paging_nav .= " &sdot; next";
+               }
+
+               if ($#commitlist >= 100) {
+               }
+
+               git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
+               git_print_header_div('commit', esc_html($co{'title'}), $hash);
+               git_search_grep_body(\@commitlist, 0, 99, $next_link);
+       }
+
+       if ($searchtype eq 'pickaxe') {
+               git_print_page_nav('','', $hash,$co{'tree'},$hash);
+               git_print_header_div('commit', esc_html($co{'title'}), $hash);
+
+               print "<table class=\"pickaxe search\">\n";
+               my $alternate = 1;
+               $/ = "\n";
+               my $git_command = git_cmd_str();
+               my $searchqtext = $searchtext;
+               $searchqtext =~ s/'/'\\''/;
+               open my $fd, "-|", "$git_command rev-list $hash | " .
+                       "$git_command diff-tree -r --stdin -S\'$searchqtext\'";
+               undef %co;
+               my @files;
+               while (my $line = <$fd>) {
+                       if (%co && $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
+                               my %set;
+                               $set{'file'} = $6;
+                               $set{'from_id'} = $3;
+                               $set{'to_id'} = $4;
+                               $set{'id'} = $set{'to_id'};
+                               if ($set{'id'} =~ m/0{40}/) {
+                                       $set{'id'} = $set{'from_id'};
+                               }
+                               if ($set{'id'} =~ m/0{40}/) {
+                                       next;
+                               }
+                               push @files, \%set;
+                       } elsif ($line =~ m/^([0-9a-fA-F]{40})$/){
+                               if (%co) {
+                                       if ($alternate) {
+                                               print "<tr class=\"dark\">\n";
+                                       } else {
+                                               print "<tr class=\"light\">\n";
+                                       }
+                                       $alternate ^= 1;
+                                       my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
+                                       print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+                                             "<td><i>" . $author . "</i></td>\n" .
+                                             "<td>" .
+                                             $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
+                                                     -class => "list subject"},
+                                                     chop_and_escape_str($co{'title'}, 50) . "<br/>");
+                                       while (my $setref = shift @files) {
+                                               my %set = %$setref;
+                                               print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
+                                                                            hash=>$set{'id'}, file_name=>$set{'file'}),
+                                                             -class => "list"},
+                                                             "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
+                                                     "<br/>\n";
+                                       }
+                                       print "</td>\n" .
+                                             "<td class=\"link\">" .
+                                             $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
+                                             " | " .
+                                             $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
+                                       print "</td>\n" .
+                                             "</tr>\n";
+                               }
+                               %co = parse_commit($1);
+                       }
+               }
+               close $fd;
+
+               print "</table>\n";
+       }
+
+       if ($searchtype eq 'grep') {
+               git_print_page_nav('','', $hash,$co{'tree'},$hash);
+               git_print_header_div('commit', esc_html($co{'title'}), $hash);
+
+               print "<table class=\"grep_search\">\n";
+               my $alternate = 1;
+               my $matches = 0;
+               $/ = "\n";
+               open my $fd, "-|", git_cmd(), 'grep', '-n', '-i', '-E', $searchtext, $co{'tree'};
+               my $lastfile = '';
+               while (my $line = <$fd>) {
+                       chomp $line;
+                       my ($file, $lno, $ltext, $binary);
+                       last if ($matches++ > 1000);
+                       if ($line =~ /^Binary file (.+) matches$/) {
+                               $file = $1;
+                               $binary = 1;
+                       } else {
+                               (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
+                       }
+                       if ($file ne $lastfile) {
+                               $lastfile and print "</td></tr>\n";
+                               if ($alternate++) {
+                                       print "<tr class=\"dark\">\n";
+                               } else {
+                                       print "<tr class=\"light\">\n";
+                               }
+                               print "<td class=\"list\">".
+                                       $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
+                                                              file_name=>"$file"),
+                                               -class => "list"}, esc_path($file));
+                               print "</td><td>\n";
+                               $lastfile = $file;
+                       }
+                       if ($binary) {
+                               print "<div class=\"binary\">Binary file</div>\n";
+                       } else {
+                               $ltext = untabify($ltext);
+                               if ($ltext =~ m/^(.*)($searchtext)(.*)$/i) {
+                                       $ltext = esc_html($1, -nbsp=>1);
+                                       $ltext .= '<span class="match">';
+                                       $ltext .= esc_html($2, -nbsp=>1);
+                                       $ltext .= '</span>';
+                                       $ltext .= esc_html($3, -nbsp=>1);
+                               } else {
+                                       $ltext = esc_html($ltext, -nbsp=>1);
+                               }
+                               print "<div class=\"pre\">" .
+                                       $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
+                                                              file_name=>"$file").'#l'.$lno,
+                                               -class => "linenr"}, sprintf('%4i', $lno))
+                                       . ' ' .  $ltext . "</div>\n";
+                       }
+               }
+               if ($lastfile) {
+                       print "</td></tr>\n";
+                       if ($matches > 1000) {
+                               print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
+                       }
+               } else {
+                       print "<div class=\"diff nodifferences\">No matches found</div>\n";
+               }
+               close $fd;
+
+               print "</table>\n";
+       }
+       git_footer_html();
+}
+
+sub git_search_help {
+       git_header_html();
+       git_print_page_nav('','', $hash,$hash,$hash);
+       print <<EOT;
+<dl>
+<dt><b>commit</b></dt>
+<dd>The commit messages and authorship information will be scanned for the given string.</dd>
+EOT
+       my ($have_grep) = gitweb_check_feature('grep');
+       if ($have_grep) {
+               print <<EOT;
+<dt><b>grep</b></dt>
+<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
+    a different one) are searched for the given
+<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a>
+(POSIX extended) and the matches are listed. On large
+trees, this search can take a while and put some strain on the server, so please use it with
+some consideration.</dd>
+EOT
+       }
+       print <<EOT;
+<dt><b>author</b></dt>
+<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given string.</dd>
+<dt><b>committer</b></dt>
+<dd>Name and e-mail of the committer and date of commit will be scanned for the given string.</dd>
+EOT
+       my ($have_pickaxe) = gitweb_check_feature('pickaxe');
+       if ($have_pickaxe) {
+               print <<EOT;
+<dt><b>pickaxe</b></dt>
+<dd>All commits that caused the string to appear or disappear from any file (changes that
+added, removed or "modified" the string) will be listed. This search can take a while and
+takes a lot of strain on the server, so please use it wisely.</dd>
+EOT
+       }
+       print "</dl>\n";
+       git_footer_html();
+}
+
+sub git_shortlog {
+       my $head = git_get_head_hash($project);
+       if (!defined $hash) {
+               $hash = $head;
+       }
+       if (!defined $page) {
+               $page = 0;
+       }
+       my $refs = git_get_references();
+
+       my @commitlist = parse_commits($hash, 101, (100 * $page));
+
+       my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, (100 * ($page+1)));
+       my $next_link = '';
+       if ($#commitlist >= 100) {
+               $next_link =
+                       $cgi->a({-href => href(-replay=>1, page=>$page+1),
+                                -accesskey => "n", -title => "Alt-n"}, "next");
+       }
+
+       git_header_html();
+       git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
+       git_print_header_div('summary', $project);
+
+       git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
+
+       git_footer_html();
+}
+
+## ......................................................................
+## feeds (RSS, Atom; OPML)
+
+sub git_feed {
+       my $format = shift || 'atom';
+       my ($have_blame) = gitweb_check_feature('blame');
+
+       # Atom: http://www.atomenabled.org/developers/syndication/
+       # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
+       if ($format ne 'rss' && $format ne 'atom') {
+               die_error(undef, "Unknown web feed format");
+       }
+
+       # log/feed of current (HEAD) branch, log of given branch, history of file/directory
+       my $head = $hash || 'HEAD';
+       my @commitlist = parse_commits($head, 150, 0, undef, $file_name);
+
+       my %latest_commit;
+       my %latest_date;
+       my $content_type = "application/$format+xml";
+       if (defined $cgi->http('HTTP_ACCEPT') &&
+                $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
+               # browser (feed reader) prefers text/xml
+               $content_type = 'text/xml';
+       }
+       if (defined($commitlist[0])) {
+               %latest_commit = %{$commitlist[0]};
+               %latest_date   = parse_date($latest_commit{'author_epoch'});
+               print $cgi->header(
+                       -type => $content_type,
+                       -charset => 'utf-8',
+                       -last_modified => $latest_date{'rfc2822'});
+       } else {
+               print $cgi->header(
+                       -type => $content_type,
+                       -charset => 'utf-8');
+       }
+
+       # Optimization: skip generating the body if client asks only
+       # for Last-Modified date.
+       return if ($cgi->request_method() eq 'HEAD');
+
+       # header variables
+       my $title = "$site_name - $project/$action";
+       my $feed_type = 'log';
+       if (defined $hash) {
+               $title .= " - '$hash'";
+               $feed_type = 'branch log';
+               if (defined $file_name) {
+                       $title .= " :: $file_name";
+                       $feed_type = 'history';
+               }
+       } elsif (defined $file_name) {
+               $title .= " - $file_name";
+               $feed_type = 'history';
+       }
+       $title .= " $feed_type";
+       my $descr = git_get_project_description($project);
+       if (defined $descr) {
+               $descr = esc_html($descr);
+       } else {
+               $descr = "$project " .
+                        ($format eq 'rss' ? 'RSS' : 'Atom') .
+                        " feed";
+       }
+       my $owner = git_get_project_owner($project);
+       $owner = esc_html($owner);
+
+       #header
+       my $alt_url;
+       if (defined $file_name) {
+               $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
+       } elsif (defined $hash) {
+               $alt_url = href(-full=>1, action=>"log", hash=>$hash);
+       } else {
+               $alt_url = href(-full=>1, action=>"summary");
+       }
+       print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
+       if ($format eq 'rss') {
+               print <<XML;
+<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+<channel>
+XML
+               print "<title>$title</title>\n" .
+                     "<link>$alt_url</link>\n" .
+                     "<description>$descr</description>\n" .
+                     "<language>en</language>\n";
+       } elsif ($format eq 'atom') {
+               print <<XML;
+<feed xmlns="http://www.w3.org/2005/Atom">
+XML
+               print "<title>$title</title>\n" .
+                     "<subtitle>$descr</subtitle>\n" .
+                     '<link rel="alternate" type="text/html" href="' .
+                     $alt_url . '" />' . "\n" .
+                     '<link rel="self" type="' . $content_type . '" href="' .
+                     $cgi->self_url() . '" />' . "\n" .
+                     "<id>" . href(-full=>1) . "</id>\n" .
+                     # use project owner for feed author
+                     "<author><name>$owner</name></author>\n";
+               if (defined $favicon) {
+                       print "<icon>" . esc_url($favicon) . "</icon>\n";
+               }
+               if (defined $logo_url) {
+                       # not twice as wide as tall: 72 x 27 pixels
+                       print "<logo>" . esc_url($logo) . "</logo>\n";
+               }
+               if (! %latest_date) {
+                       # dummy date to keep the feed valid until commits trickle in:
+                       print "<updated>1970-01-01T00:00:00Z</updated>\n";
+               } else {
+                       print "<updated>$latest_date{'iso-8601'}</updated>\n";
+               }
+       }
+
+       # contents
+       for (my $i = 0; $i <= $#commitlist; $i++) {
+               my %co = %{$commitlist[$i]};
+               my $commit = $co{'id'};
+               # we read 150, we always show 30 and the ones more recent than 48 hours
+               if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
+                       last;
+               }
+               my %cd = parse_date($co{'author_epoch'});
+
+               # get list of changed files
+               open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+                       $co{'parent'} || "--root",
+                       $co{'id'}, "--", (defined $file_name ? $file_name : ())
+                       or next;
+               my @difftree = map { chomp; $_ } <$fd>;
+               close $fd
+                       or next;
+
+               # print element (entry, item)
+               my $co_url = href(-full=>1, action=>"commit", hash=>$commit);
+               if ($format eq 'rss') {
+                       print "<item>\n" .
+                             "<title>" . esc_html($co{'title'}) . "</title>\n" .
+                             "<author>" . esc_html($co{'author'}) . "</author>\n" .
+                             "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
+                             "<guid isPermaLink=\"true\">$co_url</guid>\n" .
+                             "<link>$co_url</link>\n" .
+                             "<description>" . esc_html($co{'title'}) . "</description>\n" .
+                             "<content:encoded>" .
+                             "<![CDATA[\n";
+               } elsif ($format eq 'atom') {
+                       print "<entry>\n" .
+                             "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
+                             "<updated>$cd{'iso-8601'}</updated>\n" .
+                             "<author>\n" .
+                             "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
+                       if ($co{'author_email'}) {
+                               print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
+                       }
+                       print "</author>\n" .
+                             # use committer for contributor
+                             "<contributor>\n" .
+                             "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
+                       if ($co{'committer_email'}) {
+                               print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
+                       }
+                       print "</contributor>\n" .
+                             "<published>$cd{'iso-8601'}</published>\n" .
+                             "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
+                             "<id>$co_url</id>\n" .
+                             "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
+                             "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
+               }
+               my $comment = $co{'comment'};
+               print "<pre>\n";
+               foreach my $line (@$comment) {
+                       $line = esc_html($line);
+                       print "$line\n";
+               }
+               print "</pre><ul>\n";
+               foreach my $difftree_line (@difftree) {
+                       my %difftree = parse_difftree_raw_line($difftree_line);
+                       next if !$difftree{'from_id'};
+
+                       my $file = $difftree{'file'} || $difftree{'to_file'};
+
+                       print "<li>" .
+                             "[" .
+                             $cgi->a({-href => href(-full=>1, action=>"blobdiff",
+                                                    hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
+                                                    hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
+                                                    file_name=>$file, file_parent=>$difftree{'from_file'}),
+                                     -title => "diff"}, 'D');
+                       if ($have_blame) {
+                               print $cgi->a({-href => href(-full=>1, action=>"blame",
+                                                            file_name=>$file, hash_base=>$commit),
+                                             -title => "blame"}, 'B');
+                       }
+                       # if this is not a feed of a file history
+                       if (!defined $file_name || $file_name ne $file) {
+                               print $cgi->a({-href => href(-full=>1, action=>"history",
+                                                            file_name=>$file, hash=>$commit),
+                                             -title => "history"}, 'H');
+                       }
+                       $file = esc_path($file);
+                       print "] ".
+                             "$file</li>\n";
+               }
+               if ($format eq 'rss') {
+                       print "</ul>]]>\n" .
+                             "</content:encoded>\n" .
+                             "</item>\n";
+               } elsif ($format eq 'atom') {
+                       print "</ul>\n</div>\n" .
+                             "</content>\n" .
+                             "</entry>\n";
+               }
+       }
+
+       # end of feed
+       if ($format eq 'rss') {
+               print "</channel>\n</rss>\n";
+       }       elsif ($format eq 'atom') {
+               print "</feed>\n";
+       }
+}
+
+sub git_rss {
+       git_feed('rss');
+}
+
+sub git_atom {
+       git_feed('atom');
+}
+
+sub git_opml {
+       my @list = git_get_projects_list();
+
+       print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
+       print <<XML;
+<?xml version="1.0" encoding="utf-8"?>
+<opml version="1.0">
+<head>
+  <title>$site_name OPML Export</title>
+</head>
+<body>
+<outline text="git RSS feeds">
+XML
+
+       foreach my $pr (@list) {
+               my %proj = %$pr;
+               my $head = git_get_head_hash($proj{'path'});
+               if (!defined $head) {
+                       next;
+               }
+               $git_dir = "$projectroot/$proj{'path'}";
+               my %co = parse_commit($head);
+               if (!%co) {
+                       next;
+               }
+
+               my $path = esc_html(chop_str($proj{'path'}, 25, 5));
+               my $rss  = "$my_url?p=$proj{'path'};a=rss";
+               my $html = "$my_url?p=$proj{'path'};a=summary";
+               print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
+       }
+       print <<XML;
+</outline>
+</body>
+</opml>
+XML
+}
diff --git a/gitweb/test/Märchen b/gitweb/test/Märchen
new file mode 100644 (file)
index 0000000..8f7a1d3
--- /dev/null
@@ -0,0 +1,2 @@
+Märchen
+Märchen
diff --git a/gitweb/test/file with spaces b/gitweb/test/file with spaces
new file mode 100644 (file)
index 0000000..f108543
--- /dev/null
@@ -0,0 +1,4 @@
+This
+filename
+contains
+spaces.
diff --git a/gitweb/test/file+plus+sign b/gitweb/test/file+plus+sign
new file mode 100644 (file)
index 0000000..fd05278
--- /dev/null
@@ -0,0 +1,6 @@
+This
+filename
+contains
++
+plus
+chars.
diff --git a/grep.c b/grep.c
new file mode 100644 (file)
index 0000000..f67d671
--- /dev/null
+++ b/grep.c
@@ -0,0 +1,565 @@
+#include "cache.h"
+#include "grep.h"
+#include "xdiff-interface.h"
+
+void append_grep_pattern(struct grep_opt *opt, const char *pat,
+                        const char *origin, int no, enum grep_pat_token t)
+{
+       struct grep_pat *p = xcalloc(1, sizeof(*p));
+       p->pattern = pat;
+       p->origin = origin;
+       p->no = no;
+       p->token = t;
+       *opt->pattern_tail = p;
+       opt->pattern_tail = &p->next;
+       p->next = NULL;
+}
+
+static void compile_regexp(struct grep_pat *p, struct grep_opt *opt)
+{
+       int err = regcomp(&p->regexp, p->pattern, opt->regflags);
+       if (err) {
+               char errbuf[1024];
+               char where[1024];
+               if (p->no)
+                       sprintf(where, "In '%s' at %d, ",
+                               p->origin, p->no);
+               else if (p->origin)
+                       sprintf(where, "%s, ", p->origin);
+               else
+                       where[0] = 0;
+               regerror(err, &p->regexp, errbuf, 1024);
+               regfree(&p->regexp);
+               die("%s'%s': %s", where, p->pattern, errbuf);
+       }
+}
+
+static struct grep_expr *compile_pattern_or(struct grep_pat **);
+static struct grep_expr *compile_pattern_atom(struct grep_pat **list)
+{
+       struct grep_pat *p;
+       struct grep_expr *x;
+
+       p = *list;
+       switch (p->token) {
+       case GREP_PATTERN: /* atom */
+       case GREP_PATTERN_HEAD:
+       case GREP_PATTERN_BODY:
+               x = xcalloc(1, sizeof (struct grep_expr));
+               x->node = GREP_NODE_ATOM;
+               x->u.atom = p;
+               *list = p->next;
+               return x;
+       case GREP_OPEN_PAREN:
+               *list = p->next;
+               x = compile_pattern_or(list);
+               if (!x)
+                       return NULL;
+               if (!*list || (*list)->token != GREP_CLOSE_PAREN)
+                       die("unmatched parenthesis");
+               *list = (*list)->next;
+               return x;
+       default:
+               return NULL;
+       }
+}
+
+static struct grep_expr *compile_pattern_not(struct grep_pat **list)
+{
+       struct grep_pat *p;
+       struct grep_expr *x;
+
+       p = *list;
+       switch (p->token) {
+       case GREP_NOT:
+               if (!p->next)
+                       die("--not not followed by pattern expression");
+               *list = p->next;
+               x = xcalloc(1, sizeof (struct grep_expr));
+               x->node = GREP_NODE_NOT;
+               x->u.unary = compile_pattern_not(list);
+               if (!x->u.unary)
+                       die("--not followed by non pattern expression");
+               return x;
+       default:
+               return compile_pattern_atom(list);
+       }
+}
+
+static struct grep_expr *compile_pattern_and(struct grep_pat **list)
+{
+       struct grep_pat *p;
+       struct grep_expr *x, *y, *z;
+
+       x = compile_pattern_not(list);
+       p = *list;
+       if (p && p->token == GREP_AND) {
+               if (!p->next)
+                       die("--and not followed by pattern expression");
+               *list = p->next;
+               y = compile_pattern_and(list);
+               if (!y)
+                       die("--and not followed by pattern expression");
+               z = xcalloc(1, sizeof (struct grep_expr));
+               z->node = GREP_NODE_AND;
+               z->u.binary.left = x;
+               z->u.binary.right = y;
+               return z;
+       }
+       return x;
+}
+
+static struct grep_expr *compile_pattern_or(struct grep_pat **list)
+{
+       struct grep_pat *p;
+       struct grep_expr *x, *y, *z;
+
+       x = compile_pattern_and(list);
+       p = *list;
+       if (x && p && p->token != GREP_CLOSE_PAREN) {
+               y = compile_pattern_or(list);
+               if (!y)
+                       die("not a pattern expression %s", p->pattern);
+               z = xcalloc(1, sizeof (struct grep_expr));
+               z->node = GREP_NODE_OR;
+               z->u.binary.left = x;
+               z->u.binary.right = y;
+               return z;
+       }
+       return x;
+}
+
+static struct grep_expr *compile_pattern_expr(struct grep_pat **list)
+{
+       return compile_pattern_or(list);
+}
+
+void compile_grep_patterns(struct grep_opt *opt)
+{
+       struct grep_pat *p;
+
+       if (opt->all_match)
+               opt->extended = 1;
+
+       for (p = opt->pattern_list; p; p = p->next) {
+               switch (p->token) {
+               case GREP_PATTERN: /* atom */
+               case GREP_PATTERN_HEAD:
+               case GREP_PATTERN_BODY:
+                       if (!opt->fixed)
+                               compile_regexp(p, opt);
+                       break;
+               default:
+                       opt->extended = 1;
+                       break;
+               }
+       }
+
+       if (!opt->extended)
+               return;
+
+       /* Then bundle them up in an expression.
+        * A classic recursive descent parser would do.
+        */
+       p = opt->pattern_list;
+       opt->pattern_expression = compile_pattern_expr(&p);
+       if (p)
+               die("incomplete pattern expression: %s", p->pattern);
+}
+
+static void free_pattern_expr(struct grep_expr *x)
+{
+       switch (x->node) {
+       case GREP_NODE_ATOM:
+               break;
+       case GREP_NODE_NOT:
+               free_pattern_expr(x->u.unary);
+               break;
+       case GREP_NODE_AND:
+       case GREP_NODE_OR:
+               free_pattern_expr(x->u.binary.left);
+               free_pattern_expr(x->u.binary.right);
+               break;
+       }
+       free(x);
+}
+
+void free_grep_patterns(struct grep_opt *opt)
+{
+       struct grep_pat *p, *n;
+
+       for (p = opt->pattern_list; p; p = n) {
+               n = p->next;
+               switch (p->token) {
+               case GREP_PATTERN: /* atom */
+               case GREP_PATTERN_HEAD:
+               case GREP_PATTERN_BODY:
+                       regfree(&p->regexp);
+                       break;
+               default:
+                       break;
+               }
+               free(p);
+       }
+
+       if (!opt->extended)
+               return;
+       free_pattern_expr(opt->pattern_expression);
+}
+
+static char *end_of_line(char *cp, unsigned long *left)
+{
+       unsigned long l = *left;
+       while (l && *cp != '\n') {
+               l--;
+               cp++;
+       }
+       *left = l;
+       return cp;
+}
+
+static int word_char(char ch)
+{
+       return isalnum(ch) || ch == '_';
+}
+
+static void show_line(struct grep_opt *opt, const char *bol, const char *eol,
+                     const char *name, unsigned lno, char sign)
+{
+       if (opt->pathname)
+               printf("%s%c", name, sign);
+       if (opt->linenum)
+               printf("%d%c", lno, sign);
+       printf("%.*s\n", (int)(eol-bol), bol);
+}
+
+static int fixmatch(const char *pattern, char *line, regmatch_t *match)
+{
+       char *hit = strstr(line, pattern);
+       if (!hit) {
+               match->rm_so = match->rm_eo = -1;
+               return REG_NOMATCH;
+       }
+       else {
+               match->rm_so = hit - line;
+               match->rm_eo = match->rm_so + strlen(pattern);
+               return 0;
+       }
+}
+
+static int match_one_pattern(struct grep_opt *opt, struct grep_pat *p, char *bol, char *eol, enum grep_context ctx)
+{
+       int hit = 0;
+       int at_true_bol = 1;
+       regmatch_t pmatch[10];
+
+       if ((p->token != GREP_PATTERN) &&
+           ((p->token == GREP_PATTERN_HEAD) != (ctx == GREP_CONTEXT_HEAD)))
+               return 0;
+
+ again:
+       if (!opt->fixed) {
+               regex_t *exp = &p->regexp;
+               hit = !regexec(exp, bol, ARRAY_SIZE(pmatch),
+                              pmatch, 0);
+       }
+       else {
+               hit = !fixmatch(p->pattern, bol, pmatch);
+       }
+
+       if (hit && opt->word_regexp) {
+               if ((pmatch[0].rm_so < 0) ||
+                   (eol - bol) <= pmatch[0].rm_so ||
+                   (pmatch[0].rm_eo < 0) ||
+                   (eol - bol) < pmatch[0].rm_eo)
+                       die("regexp returned nonsense");
+
+               /* Match beginning must be either beginning of the
+                * line, or at word boundary (i.e. the last char must
+                * not be a word char).  Similarly, match end must be
+                * either end of the line, or at word boundary
+                * (i.e. the next char must not be a word char).
+                */
+               if ( ((pmatch[0].rm_so == 0 && at_true_bol) ||
+                     !word_char(bol[pmatch[0].rm_so-1])) &&
+                    ((pmatch[0].rm_eo == (eol-bol)) ||
+                     !word_char(bol[pmatch[0].rm_eo])) )
+                       ;
+               else
+                       hit = 0;
+
+               if (!hit && pmatch[0].rm_so + bol + 1 < eol) {
+                       /* There could be more than one match on the
+                        * line, and the first match might not be
+                        * strict word match.  But later ones could be!
+                        */
+                       bol = pmatch[0].rm_so + bol + 1;
+                       at_true_bol = 0;
+                       goto again;
+               }
+       }
+       return hit;
+}
+
+static int match_expr_eval(struct grep_opt *o,
+                          struct grep_expr *x,
+                          char *bol, char *eol,
+                          enum grep_context ctx,
+                          int collect_hits)
+{
+       int h = 0;
+
+       switch (x->node) {
+       case GREP_NODE_ATOM:
+               h = match_one_pattern(o, x->u.atom, bol, eol, ctx);
+               break;
+       case GREP_NODE_NOT:
+               h = !match_expr_eval(o, x->u.unary, bol, eol, ctx, 0);
+               break;
+       case GREP_NODE_AND:
+               if (!collect_hits)
+                       return (match_expr_eval(o, x->u.binary.left,
+                                               bol, eol, ctx, 0) &&
+                               match_expr_eval(o, x->u.binary.right,
+                                               bol, eol, ctx, 0));
+               h = match_expr_eval(o, x->u.binary.left, bol, eol, ctx, 0);
+               h &= match_expr_eval(o, x->u.binary.right, bol, eol, ctx, 0);
+               break;
+       case GREP_NODE_OR:
+               if (!collect_hits)
+                       return (match_expr_eval(o, x->u.binary.left,
+                                               bol, eol, ctx, 0) ||
+                               match_expr_eval(o, x->u.binary.right,
+                                               bol, eol, ctx, 0));
+               h = match_expr_eval(o, x->u.binary.left, bol, eol, ctx, 0);
+               x->u.binary.left->hit |= h;
+               h |= match_expr_eval(o, x->u.binary.right, bol, eol, ctx, 1);
+               break;
+       default:
+               die("Unexpected node type (internal error) %d\n", x->node);
+       }
+       if (collect_hits)
+               x->hit |= h;
+       return h;
+}
+
+static int match_expr(struct grep_opt *opt, char *bol, char *eol,
+                     enum grep_context ctx, int collect_hits)
+{
+       struct grep_expr *x = opt->pattern_expression;
+       return match_expr_eval(opt, x, bol, eol, ctx, collect_hits);
+}
+
+static int match_line(struct grep_opt *opt, char *bol, char *eol,
+                     enum grep_context ctx, int collect_hits)
+{
+       struct grep_pat *p;
+       if (opt->extended)
+               return match_expr(opt, bol, eol, ctx, collect_hits);
+
+       /* we do not call with collect_hits without being extended */
+       for (p = opt->pattern_list; p; p = p->next) {
+               if (match_one_pattern(opt, p, bol, eol, ctx))
+                       return 1;
+       }
+       return 0;
+}
+
+static int grep_buffer_1(struct grep_opt *opt, const char *name,
+                        char *buf, unsigned long size, int collect_hits)
+{
+       char *bol = buf;
+       unsigned long left = size;
+       unsigned lno = 1;
+       struct pre_context_line {
+               char *bol;
+               char *eol;
+       } *prev = NULL, *pcl;
+       unsigned last_hit = 0;
+       unsigned last_shown = 0;
+       int binary_match_only = 0;
+       const char *hunk_mark = "";
+       unsigned count = 0;
+       enum grep_context ctx = GREP_CONTEXT_HEAD;
+
+       if (buffer_is_binary(buf, size)) {
+               switch (opt->binary) {
+               case GREP_BINARY_DEFAULT:
+                       binary_match_only = 1;
+                       break;
+               case GREP_BINARY_NOMATCH:
+                       return 0; /* Assume unmatch */
+                       break;
+               default:
+                       break;
+               }
+       }
+
+       if (opt->pre_context)
+               prev = xcalloc(opt->pre_context, sizeof(*prev));
+       if (opt->pre_context || opt->post_context)
+               hunk_mark = "--\n";
+
+       while (left) {
+               char *eol, ch;
+               int hit;
+
+               eol = end_of_line(bol, &left);
+               ch = *eol;
+               *eol = 0;
+
+               if ((ctx == GREP_CONTEXT_HEAD) && (eol == bol))
+                       ctx = GREP_CONTEXT_BODY;
+
+               hit = match_line(opt, bol, eol, ctx, collect_hits);
+               *eol = ch;
+
+               if (collect_hits)
+                       goto next_line;
+
+               /* "grep -v -e foo -e bla" should list lines
+                * that do not have either, so inversion should
+                * be done outside.
+                */
+               if (opt->invert)
+                       hit = !hit;
+               if (opt->unmatch_name_only) {
+                       if (hit)
+                               return 0;
+                       goto next_line;
+               }
+               if (hit) {
+                       count++;
+                       if (opt->status_only)
+                               return 1;
+                       if (binary_match_only) {
+                               printf("Binary file %s matches\n", name);
+                               return 1;
+                       }
+                       if (opt->name_only) {
+                               printf("%s\n", name);
+                               return 1;
+                       }
+                       /* Hit at this line.  If we haven't shown the
+                        * pre-context lines, we would need to show them.
+                        * When asked to do "count", this still show
+                        * the context which is nonsense, but the user
+                        * deserves to get that ;-).
+                        */
+                       if (opt->pre_context) {
+                               unsigned from;
+                               if (opt->pre_context < lno)
+                                       from = lno - opt->pre_context;
+                               else
+                                       from = 1;
+                               if (from <= last_shown)
+                                       from = last_shown + 1;
+                               if (last_shown && from != last_shown + 1)
+                                       printf(hunk_mark);
+                               while (from < lno) {
+                                       pcl = &prev[lno-from-1];
+                                       show_line(opt, pcl->bol, pcl->eol,
+                                                 name, from, '-');
+                                       from++;
+                               }
+                               last_shown = lno-1;
+                       }
+                       if (last_shown && lno != last_shown + 1)
+                               printf(hunk_mark);
+                       if (!opt->count)
+                               show_line(opt, bol, eol, name, lno, ':');
+                       last_shown = last_hit = lno;
+               }
+               else if (last_hit &&
+                        lno <= last_hit + opt->post_context) {
+                       /* If the last hit is within the post context,
+                        * we need to show this line.
+                        */
+                       if (last_shown && lno != last_shown + 1)
+                               printf(hunk_mark);
+                       show_line(opt, bol, eol, name, lno, '-');
+                       last_shown = lno;
+               }
+               if (opt->pre_context) {
+                       memmove(prev+1, prev,
+                               (opt->pre_context-1) * sizeof(*prev));
+                       prev->bol = bol;
+                       prev->eol = eol;
+               }
+
+       next_line:
+               bol = eol + 1;
+               if (!left)
+                       break;
+               left--;
+               lno++;
+       }
+
+       free(prev);
+       if (collect_hits)
+               return 0;
+
+       if (opt->status_only)
+               return 0;
+       if (opt->unmatch_name_only) {
+               /* We did not see any hit, so we want to show this */
+               printf("%s\n", name);
+               return 1;
+       }
+
+       /* NEEDSWORK:
+        * The real "grep -c foo *.c" gives many "bar.c:0" lines,
+        * which feels mostly useless but sometimes useful.  Maybe
+        * make it another option?  For now suppress them.
+        */
+       if (opt->count && count)
+               printf("%s:%u\n", name, count);
+       return !!last_hit;
+}
+
+static void clr_hit_marker(struct grep_expr *x)
+{
+       /* All-hit markers are meaningful only at the very top level
+        * OR node.
+        */
+       while (1) {
+               x->hit = 0;
+               if (x->node != GREP_NODE_OR)
+                       return;
+               x->u.binary.left->hit = 0;
+               x = x->u.binary.right;
+       }
+}
+
+static int chk_hit_marker(struct grep_expr *x)
+{
+       /* Top level nodes have hit markers.  See if they all are hits */
+       while (1) {
+               if (x->node != GREP_NODE_OR)
+                       return x->hit;
+               if (!x->u.binary.left->hit)
+                       return 0;
+               x = x->u.binary.right;
+       }
+}
+
+int grep_buffer(struct grep_opt *opt, const char *name, char *buf, unsigned long size)
+{
+       /*
+        * we do not have to do the two-pass grep when we do not check
+        * buffer-wide "all-match".
+        */
+       if (!opt->all_match)
+               return grep_buffer_1(opt, name, buf, size, 0);
+
+       /* Otherwise the toplevel "or" terms hit a bit differently.
+        * We first clear hit markers from them.
+        */
+       clr_hit_marker(opt->pattern_expression);
+       grep_buffer_1(opt, name, buf, size, 1);
+
+       if (!chk_hit_marker(opt->pattern_expression))
+               return 0;
+
+       return grep_buffer_1(opt, name, buf, size, 0);
+}
diff --git a/grep.h b/grep.h
new file mode 100644 (file)
index 0000000..d252dd2
--- /dev/null
+++ b/grep.h
@@ -0,0 +1,81 @@
+#ifndef GREP_H
+#define GREP_H
+
+enum grep_pat_token {
+       GREP_PATTERN,
+       GREP_PATTERN_HEAD,
+       GREP_PATTERN_BODY,
+       GREP_AND,
+       GREP_OPEN_PAREN,
+       GREP_CLOSE_PAREN,
+       GREP_NOT,
+       GREP_OR,
+};
+
+enum grep_context {
+       GREP_CONTEXT_HEAD,
+       GREP_CONTEXT_BODY,
+};
+
+struct grep_pat {
+       struct grep_pat *next;
+       const char *origin;
+       int no;
+       enum grep_pat_token token;
+       const char *pattern;
+       regex_t regexp;
+};
+
+enum grep_expr_node {
+       GREP_NODE_ATOM,
+       GREP_NODE_NOT,
+       GREP_NODE_AND,
+       GREP_NODE_OR,
+};
+
+struct grep_expr {
+       enum grep_expr_node node;
+       unsigned hit;
+       union {
+               struct grep_pat *atom;
+               struct grep_expr *unary;
+               struct {
+                       struct grep_expr *left;
+                       struct grep_expr *right;
+               } binary;
+       } u;
+};
+
+struct grep_opt {
+       struct grep_pat *pattern_list;
+       struct grep_pat **pattern_tail;
+       struct grep_expr *pattern_expression;
+       int prefix_length;
+       regex_t regexp;
+       unsigned linenum:1;
+       unsigned invert:1;
+       unsigned status_only:1;
+       unsigned name_only:1;
+       unsigned unmatch_name_only:1;
+       unsigned count:1;
+       unsigned word_regexp:1;
+       unsigned fixed:1;
+       unsigned all_match:1;
+#define GREP_BINARY_DEFAULT    0
+#define GREP_BINARY_NOMATCH    1
+#define GREP_BINARY_TEXT       2
+       unsigned binary:2;
+       unsigned extended:1;
+       unsigned relative:1;
+       unsigned pathname:1;
+       int regflags;
+       unsigned pre_context;
+       unsigned post_context;
+};
+
+extern void append_grep_pattern(struct grep_opt *opt, const char *pat, const char *origin, int no, enum grep_pat_token t);
+extern void compile_grep_patterns(struct grep_opt *opt);
+extern void free_grep_patterns(struct grep_opt *opt);
+extern int grep_buffer(struct grep_opt *opt, const char *name, char *buf, unsigned long size);
+
+#endif
diff --git a/hash-object.c b/hash-object.c
new file mode 100644 (file)
index 0000000..0a58f3f
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ * Copyright (C) Junio C Hamano, 2005
+ */
+#include "cache.h"
+#include "blob.h"
+
+static void hash_object(const char *path, enum object_type type, int write_object)
+{
+       int fd;
+       struct stat st;
+       unsigned char sha1[20];
+       fd = open(path, O_RDONLY);
+       if (fd < 0 ||
+           fstat(fd, &st) < 0 ||
+           index_fd(sha1, fd, &st, write_object, type, path))
+               die(write_object
+                   ? "Unable to add %s to database"
+                   : "Unable to hash %s", path);
+       printf("%s\n", sha1_to_hex(sha1));
+}
+
+static void hash_stdin(const char *type, int write_object)
+{
+       unsigned char sha1[20];
+       if (index_pipe(sha1, 0, type, write_object))
+               die("Unable to add stdin to database");
+       printf("%s\n", sha1_to_hex(sha1));
+}
+
+static const char hash_object_usage[] =
+"git-hash-object [-t <type>] [-w] [--stdin] <file>...";
+
+int main(int argc, char **argv)
+{
+       int i;
+       const char *type = blob_type;
+       int write_object = 0;
+       const char *prefix = NULL;
+       int prefix_length = -1;
+       int no_more_flags = 0;
+
+       git_config(git_default_config);
+
+       for (i = 1 ; i < argc; i++) {
+               if (!no_more_flags && argv[i][0] == '-') {
+                       if (!strcmp(argv[i], "-t")) {
+                               if (argc <= ++i)
+                                       usage(hash_object_usage);
+                               type = argv[i];
+                       }
+                       else if (!strcmp(argv[i], "-w")) {
+                               if (prefix_length < 0) {
+                                       prefix = setup_git_directory();
+                                       prefix_length =
+                                               prefix ? strlen(prefix) : 0;
+                               }
+                               write_object = 1;
+                       }
+                       else if (!strcmp(argv[i], "--")) {
+                               no_more_flags = 1;
+                       }
+                       else if (!strcmp(argv[i], "--help"))
+                               usage(hash_object_usage);
+                       else if (!strcmp(argv[i], "--stdin")) {
+                               hash_stdin(type, write_object);
+                       }
+                       else
+                               usage(hash_object_usage);
+               }
+               else {
+                       const char *arg = argv[i];
+                       if (0 <= prefix_length)
+                               arg = prefix_filename(prefix, prefix_length,
+                                                     arg);
+                       hash_object(arg, type_from_string(type), write_object);
+                       no_more_flags = 1;
+               }
+       }
+       return 0;
+}
diff --git a/hash.c b/hash.c
new file mode 100644 (file)
index 0000000..7b492d4
--- /dev/null
+++ b/hash.c
@@ -0,0 +1,110 @@
+/*
+ * Some generic hashing helpers.
+ */
+#include "cache.h"
+#include "hash.h"
+
+/*
+ * Look up a hash entry in the hash table. Return the pointer to
+ * the existing entry, or the empty slot if none existed. The caller
+ * can then look at the (*ptr) to see whether it existed or not.
+ */
+static struct hash_table_entry *lookup_hash_entry(unsigned int hash, struct hash_table *table)
+{
+       unsigned int size = table->size, nr = hash % size;
+       struct hash_table_entry *array = table->array;
+
+       while (array[nr].ptr) {
+               if (array[nr].hash == hash)
+                       break;
+               nr++;
+               if (nr >= size)
+                       nr = 0;
+       }
+       return array + nr;
+}
+
+
+/*
+ * Insert a new hash entry pointer into the table.
+ *
+ * If that hash entry already existed, return the pointer to
+ * the existing entry (and the caller can create a list of the
+ * pointers or do anything else). If it didn't exist, return
+ * NULL (and the caller knows the pointer has been inserted).
+ */
+static void **insert_hash_entry(unsigned int hash, void *ptr, struct hash_table *table)
+{
+       struct hash_table_entry *entry = lookup_hash_entry(hash, table);
+
+       if (!entry->ptr) {
+               entry->ptr = ptr;
+               entry->hash = hash;
+               table->nr++;
+               return NULL;
+       }
+       return &entry->ptr;
+}
+
+static void grow_hash_table(struct hash_table *table)
+{
+       unsigned int i;
+       unsigned int old_size = table->size, new_size;
+       struct hash_table_entry *old_array = table->array, *new_array;
+
+       new_size = alloc_nr(old_size);
+       new_array = xcalloc(sizeof(struct hash_table_entry), new_size);
+       table->size = new_size;
+       table->array = new_array;
+       table->nr = 0;
+       for (i = 0; i < old_size; i++) {
+               unsigned int hash = old_array[i].hash;
+               void *ptr = old_array[i].ptr;
+               if (ptr)
+                       insert_hash_entry(hash, ptr, table);
+       }
+       free(old_array);
+}
+
+void *lookup_hash(unsigned int hash, struct hash_table *table)
+{
+       if (!table->array)
+               return NULL;
+       return &lookup_hash_entry(hash, table)->ptr;
+}
+
+void **insert_hash(unsigned int hash, void *ptr, struct hash_table *table)
+{
+       unsigned int nr = table->nr;
+       if (nr >= table->size/2)
+               grow_hash_table(table);
+       return insert_hash_entry(hash, ptr, table);
+}
+
+int for_each_hash(struct hash_table *table, int (*fn)(void *))
+{
+       int sum = 0;
+       unsigned int i;
+       unsigned int size = table->size;
+       struct hash_table_entry *array = table->array;
+
+       for (i = 0; i < size; i++) {
+               void *ptr = array->ptr;
+               array++;
+               if (ptr) {
+                       int val = fn(ptr);
+                       if (val < 0)
+                               return val;
+                       sum += val;
+               }
+       }
+       return sum;
+}
+
+void free_hash(struct hash_table *table)
+{
+       free(table->array);
+       table->array = NULL;
+       table->size = 0;
+       table->nr = 0;
+}
diff --git a/hash.h b/hash.h
new file mode 100644 (file)
index 0000000..a8b0fbb
--- /dev/null
+++ b/hash.h
@@ -0,0 +1,43 @@
+#ifndef HASH_H
+#define HASH_H
+
+/*
+ * These are some simple generic hash table helper functions.
+ * Not necessarily suitable for all users, but good for things
+ * where you want to just keep track of a list of things, and
+ * have a good hash to use on them.
+ *
+ * It keeps the hash table at roughly 50-75% free, so the memory
+ * cost of the hash table itself is roughly
+ *
+ *     3 * 2*sizeof(void *) * nr_of_objects
+ *
+ * bytes.
+ *
+ * FIXME: on 64-bit architectures, we waste memory. It would be
+ * good to have just 32-bit pointers, requiring a special allocator
+ * for hashed entries or something.
+ */
+struct hash_table_entry {
+       unsigned int hash;
+       void *ptr;
+};
+
+struct hash_table {
+       unsigned int size, nr;
+       struct hash_table_entry *array;
+};
+
+extern void *lookup_hash(unsigned int hash, struct hash_table *table);
+extern void **insert_hash(unsigned int hash, void *ptr, struct hash_table *table);
+extern int for_each_hash(struct hash_table *table, int (*fn)(void *));
+extern void free_hash(struct hash_table *table);
+
+static inline void init_hash(struct hash_table *table)
+{
+       table->size = 0;
+       table->nr = 0;
+       table->array = NULL;
+}
+
+#endif
diff --git a/help.c b/help.c
new file mode 100644 (file)
index 0000000..37a9c25
--- /dev/null
+++ b/help.c
@@ -0,0 +1,292 @@
+/*
+ * builtin-help.c
+ *
+ * Builtin help-related commands (help, usage, version)
+ */
+#include "cache.h"
+#include "builtin.h"
+#include "exec_cmd.h"
+#include "common-cmds.h"
+
+/* most GUI terminals set COLUMNS (although some don't export it) */
+static int term_columns(void)
+{
+       char *col_string = getenv("COLUMNS");
+       int n_cols;
+
+       if (col_string && (n_cols = atoi(col_string)) > 0)
+               return n_cols;
+
+#ifdef TIOCGWINSZ
+       {
+               struct winsize ws;
+               if (!ioctl(1, TIOCGWINSZ, &ws)) {
+                       if (ws.ws_col)
+                               return ws.ws_col;
+               }
+       }
+#endif
+
+       return 80;
+}
+
+static inline void mput_char(char c, unsigned int num)
+{
+       while(num--)
+               putchar(c);
+}
+
+static struct cmdnames {
+       int alloc;
+       int cnt;
+       struct cmdname {
+               size_t len;
+               char name[1];
+       } **names;
+} main_cmds, other_cmds;
+
+static void add_cmdname(struct cmdnames *cmds, const char *name, int len)
+{
+       struct cmdname *ent = xmalloc(sizeof(*ent) + len);
+
+       ent->len = len;
+       memcpy(ent->name, name, len);
+       ent->name[len] = 0;
+
+       ALLOC_GROW(cmds->names, cmds->cnt + 1, cmds->alloc);
+       cmds->names[cmds->cnt++] = ent;
+}
+
+static int cmdname_compare(const void *a_, const void *b_)
+{
+       struct cmdname *a = *(struct cmdname **)a_;
+       struct cmdname *b = *(struct cmdname **)b_;
+       return strcmp(a->name, b->name);
+}
+
+static void uniq(struct cmdnames *cmds)
+{
+       int i, j;
+
+       if (!cmds->cnt)
+               return;
+
+       for (i = j = 1; i < cmds->cnt; i++)
+               if (strcmp(cmds->names[i]->name, cmds->names[i-1]->name))
+                       cmds->names[j++] = cmds->names[i];
+
+       cmds->cnt = j;
+}
+
+static void exclude_cmds(struct cmdnames *cmds, struct cmdnames *excludes)
+{
+       int ci, cj, ei;
+       int cmp;
+
+       ci = cj = ei = 0;
+       while (ci < cmds->cnt && ei < excludes->cnt) {
+               cmp = strcmp(cmds->names[ci]->name, excludes->names[ei]->name);
+               if (cmp < 0)
+                       cmds->names[cj++] = cmds->names[ci++];
+               else if (cmp == 0)
+                       ci++, ei++;
+               else if (cmp > 0)
+                       ei++;
+       }
+
+       while (ci < cmds->cnt)
+               cmds->names[cj++] = cmds->names[ci++];
+
+       cmds->cnt = cj;
+}
+
+static void pretty_print_string_list(struct cmdnames *cmds, int longest)
+{
+       int cols = 1, rows;
+       int space = longest + 1; /* min 1 SP between words */
+       int max_cols = term_columns() - 1; /* don't print *on* the edge */
+       int i, j;
+
+       if (space < max_cols)
+               cols = max_cols / space;
+       rows = (cmds->cnt + cols - 1) / cols;
+
+       for (i = 0; i < rows; i++) {
+               printf("  ");
+
+               for (j = 0; j < cols; j++) {
+                       int n = j * rows + i;
+                       int size = space;
+                       if (n >= cmds->cnt)
+                               break;
+                       if (j == cols-1 || n + rows >= cmds->cnt)
+                               size = 1;
+                       printf("%-*s", size, cmds->names[n]->name);
+               }
+               putchar('\n');
+       }
+}
+
+static unsigned int list_commands_in_dir(struct cmdnames *cmds,
+                                        const char *path)
+{
+       unsigned int longest = 0;
+       const char *prefix = "git-";
+       int prefix_len = strlen(prefix);
+       DIR *dir = opendir(path);
+       struct dirent *de;
+
+       if (!dir || chdir(path))
+               return 0;
+
+       while ((de = readdir(dir)) != NULL) {
+               struct stat st;
+               int entlen;
+
+               if (prefixcmp(de->d_name, prefix))
+                       continue;
+
+               if (stat(de->d_name, &st) || /* stat, not lstat */
+                   !S_ISREG(st.st_mode) ||
+                   !(st.st_mode & S_IXUSR))
+                       continue;
+
+               entlen = strlen(de->d_name) - prefix_len;
+               if (has_extension(de->d_name, ".exe"))
+                       entlen -= 4;
+
+               if (longest < entlen)
+                       longest = entlen;
+
+               add_cmdname(cmds, de->d_name + prefix_len, entlen);
+       }
+       closedir(dir);
+
+       return longest;
+}
+
+static void list_commands(void)
+{
+       unsigned int longest = 0;
+       unsigned int len;
+       const char *env_path = getenv("PATH");
+       char *paths, *path, *colon;
+       const char *exec_path = git_exec_path();
+
+       if (exec_path)
+               longest = list_commands_in_dir(&main_cmds, exec_path);
+
+       if (!env_path) {
+               fprintf(stderr, "PATH not set\n");
+               exit(1);
+       }
+
+       path = paths = xstrdup(env_path);
+       while (1) {
+               if ((colon = strchr(path, ':')))
+                       *colon = 0;
+
+               len = list_commands_in_dir(&other_cmds, path);
+               if (len > longest)
+                       longest = len;
+
+               if (!colon)
+                       break;
+               path = colon + 1;
+       }
+       free(paths);
+
+       qsort(main_cmds.names, main_cmds.cnt,
+             sizeof(*main_cmds.names), cmdname_compare);
+       uniq(&main_cmds);
+
+       qsort(other_cmds.names, other_cmds.cnt,
+             sizeof(*other_cmds.names), cmdname_compare);
+       uniq(&other_cmds);
+       exclude_cmds(&other_cmds, &main_cmds);
+
+       if (main_cmds.cnt) {
+               printf("available git commands in '%s'\n", exec_path);
+               printf("----------------------------");
+               mput_char('-', strlen(exec_path));
+               putchar('\n');
+               pretty_print_string_list(&main_cmds, longest);
+               putchar('\n');
+       }
+
+       if (other_cmds.cnt) {
+               printf("git commands available from elsewhere on your $PATH\n");
+               printf("---------------------------------------------------\n");
+               pretty_print_string_list(&other_cmds, longest);
+               putchar('\n');
+       }
+}
+
+void list_common_cmds_help(void)
+{
+       int i, longest = 0;
+
+       for (i = 0; i < ARRAY_SIZE(common_cmds); i++) {
+               if (longest < strlen(common_cmds[i].name))
+                       longest = strlen(common_cmds[i].name);
+       }
+
+       puts("The most commonly used git commands are:");
+       for (i = 0; i < ARRAY_SIZE(common_cmds); i++) {
+               printf("   %s   ", common_cmds[i].name);
+               mput_char(' ', longest - strlen(common_cmds[i].name));
+               puts(common_cmds[i].help);
+       }
+}
+
+static void show_man_page(const char *git_cmd)
+{
+       const char *page;
+
+       if (!prefixcmp(git_cmd, "git"))
+               page = git_cmd;
+       else {
+               int page_len = strlen(git_cmd) + 4;
+               char *p = xmalloc(page_len + 1);
+               strcpy(p, "git-");
+               strcpy(p + 4, git_cmd);
+               p[page_len] = 0;
+               page = p;
+       }
+
+       execlp("man", "man", page, NULL);
+}
+
+void help_unknown_cmd(const char *cmd)
+{
+       fprintf(stderr, "git: '%s' is not a git-command. See 'git --help'.\n", cmd);
+       exit(1);
+}
+
+int cmd_version(int argc, const char **argv, const char *prefix)
+{
+       printf("git version %s\n", git_version_string);
+       return 0;
+}
+
+int cmd_help(int argc, const char **argv, const char *prefix)
+{
+       const char *help_cmd = argc > 1 ? argv[1] : NULL;
+
+       if (!help_cmd) {
+               printf("usage: %s\n\n", git_usage_string);
+               list_common_cmds_help();
+               exit(0);
+       }
+
+       else if (!strcmp(help_cmd, "--all") || !strcmp(help_cmd, "-a")) {
+               printf("usage: %s\n\n", git_usage_string);
+               list_commands();
+               exit(0);
+       }
+
+       else
+               show_man_page(help_cmd);
+
+       return 0;
+}
diff --git a/http-push.c b/http-push.c
new file mode 100644 (file)
index 0000000..78283b4
--- /dev/null
@@ -0,0 +1,2550 @@
+#include "cache.h"
+#include "commit.h"
+#include "pack.h"
+#include "tag.h"
+#include "blob.h"
+#include "http.h"
+#include "refs.h"
+#include "diff.h"
+#include "revision.h"
+#include "exec_cmd.h"
+#include "remote.h"
+
+#include <expat.h>
+
+static const char http_push_usage[] =
+"git-http-push [--all] [--dry-run] [--force] [--verbose] <remote> [<head>...]\n";
+
+#ifndef XML_STATUS_OK
+enum XML_Status {
+  XML_STATUS_OK = 1,
+  XML_STATUS_ERROR = 0
+};
+#define XML_STATUS_OK    1
+#define XML_STATUS_ERROR 0
+#endif
+
+#define PREV_BUF_SIZE 4096
+#define RANGE_HEADER_SIZE 30
+
+/* DAV methods */
+#define DAV_LOCK "LOCK"
+#define DAV_MKCOL "MKCOL"
+#define DAV_MOVE "MOVE"
+#define DAV_PROPFIND "PROPFIND"
+#define DAV_PUT "PUT"
+#define DAV_UNLOCK "UNLOCK"
+#define DAV_DELETE "DELETE"
+
+/* DAV lock flags */
+#define DAV_PROP_LOCKWR (1u << 0)
+#define DAV_PROP_LOCKEX (1u << 1)
+#define DAV_LOCK_OK (1u << 2)
+
+/* DAV XML properties */
+#define DAV_CTX_LOCKENTRY ".multistatus.response.propstat.prop.supportedlock.lockentry"
+#define DAV_CTX_LOCKTYPE_WRITE ".multistatus.response.propstat.prop.supportedlock.lockentry.locktype.write"
+#define DAV_CTX_LOCKTYPE_EXCLUSIVE ".multistatus.response.propstat.prop.supportedlock.lockentry.lockscope.exclusive"
+#define DAV_ACTIVELOCK_OWNER ".prop.lockdiscovery.activelock.owner.href"
+#define DAV_ACTIVELOCK_TIMEOUT ".prop.lockdiscovery.activelock.timeout"
+#define DAV_ACTIVELOCK_TOKEN ".prop.lockdiscovery.activelock.locktoken.href"
+#define DAV_PROPFIND_RESP ".multistatus.response"
+#define DAV_PROPFIND_NAME ".multistatus.response.href"
+#define DAV_PROPFIND_COLLECTION ".multistatus.response.propstat.prop.resourcetype.collection"
+
+/* DAV request body templates */
+#define PROPFIND_SUPPORTEDLOCK_REQUEST "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop xmlns:R=\"%s\">\n<D:supportedlock/>\n</D:prop>\n</D:propfind>"
+#define PROPFIND_ALL_REQUEST "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/>\n</D:propfind>"
+#define LOCK_REQUEST "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:lockinfo xmlns:D=\"DAV:\">\n<D:lockscope><D:exclusive/></D:lockscope>\n<D:locktype><D:write/></D:locktype>\n<D:owner>\n<D:href>mailto:%s</D:href>\n</D:owner>\n</D:lockinfo>"
+
+#define LOCK_TIME 600
+#define LOCK_REFRESH 30
+
+/* bits #0-15 in revision.h */
+
+#define LOCAL    (1u<<16)
+#define REMOTE   (1u<<17)
+#define FETCHING (1u<<18)
+#define PUSHING  (1u<<19)
+
+/* We allow "recursive" symbolic refs. Only within reason, though */
+#define MAXDEPTH 5
+
+static int pushing;
+static int aborted;
+static signed char remote_dir_exists[256];
+
+static struct curl_slist *no_pragma_header;
+static struct curl_slist *default_headers;
+
+static int push_verbosely;
+static int push_all = MATCH_REFS_NONE;
+static int force_all;
+static int dry_run;
+
+static struct object_list *objects;
+
+struct repo
+{
+       char *url;
+       int path_len;
+       int has_info_refs;
+       int can_update_info_refs;
+       int has_info_packs;
+       struct packed_git *packs;
+       struct remote_lock *locks;
+};
+
+static struct repo *remote;
+
+enum transfer_state {
+       NEED_FETCH,
+       RUN_FETCH_LOOSE,
+       RUN_FETCH_PACKED,
+       NEED_PUSH,
+       RUN_MKCOL,
+       RUN_PUT,
+       RUN_MOVE,
+       ABORTED,
+       COMPLETE,
+};
+
+struct transfer_request
+{
+       struct object *obj;
+       char *url;
+       char *dest;
+       struct remote_lock *lock;
+       struct curl_slist *headers;
+       struct buffer buffer;
+       char filename[PATH_MAX];
+       char tmpfile[PATH_MAX];
+       int local_fileno;
+       FILE *local_stream;
+       enum transfer_state state;
+       CURLcode curl_result;
+       char errorstr[CURL_ERROR_SIZE];
+       long http_code;
+       unsigned char real_sha1[20];
+       SHA_CTX c;
+       z_stream stream;
+       int zret;
+       int rename;
+       void *userData;
+       struct active_request_slot *slot;
+       struct transfer_request *next;
+};
+
+static struct transfer_request *request_queue_head;
+
+struct xml_ctx
+{
+       char *name;
+       int len;
+       char *cdata;
+       void (*userFunc)(struct xml_ctx *ctx, int tag_closed);
+       void *userData;
+};
+
+struct remote_lock
+{
+       char *url;
+       char *owner;
+       char *token;
+       time_t start_time;
+       long timeout;
+       int refreshing;
+       struct remote_lock *next;
+};
+
+/* Flags that control remote_ls processing */
+#define PROCESS_FILES (1u << 0)
+#define PROCESS_DIRS  (1u << 1)
+#define RECURSIVE     (1u << 2)
+
+/* Flags that remote_ls passes to callback functions */
+#define IS_DIR (1u << 0)
+
+struct remote_ls_ctx
+{
+       char *path;
+       void (*userFunc)(struct remote_ls_ctx *ls);
+       void *userData;
+       int flags;
+       char *dentry_name;
+       int dentry_flags;
+       struct remote_ls_ctx *parent;
+};
+
+static void finish_request(struct transfer_request *request);
+static void release_request(struct transfer_request *request);
+
+static void process_response(void *callback_data)
+{
+       struct transfer_request *request =
+               (struct transfer_request *)callback_data;
+
+       finish_request(request);
+}
+
+#ifdef USE_CURL_MULTI
+static size_t fwrite_sha1_file(void *ptr, size_t eltsize, size_t nmemb,
+                              void *data)
+{
+       unsigned char expn[4096];
+       size_t size = eltsize * nmemb;
+       int posn = 0;
+       struct transfer_request *request = (struct transfer_request *)data;
+       do {
+               ssize_t retval = xwrite(request->local_fileno,
+                                      (char *) ptr + posn, size - posn);
+               if (retval < 0)
+                       return posn;
+               posn += retval;
+       } while (posn < size);
+
+       request->stream.avail_in = size;
+       request->stream.next_in = ptr;
+       do {
+               request->stream.next_out = expn;
+               request->stream.avail_out = sizeof(expn);
+               request->zret = inflate(&request->stream, Z_SYNC_FLUSH);
+               SHA1_Update(&request->c, expn,
+                           sizeof(expn) - request->stream.avail_out);
+       } while (request->stream.avail_in && request->zret == Z_OK);
+       data_received++;
+       return size;
+}
+
+static void start_fetch_loose(struct transfer_request *request)
+{
+       char *hex = sha1_to_hex(request->obj->sha1);
+       char *filename;
+       char prevfile[PATH_MAX];
+       char *url;
+       char *posn;
+       int prevlocal;
+       unsigned char prev_buf[PREV_BUF_SIZE];
+       ssize_t prev_read = 0;
+       long prev_posn = 0;
+       char range[RANGE_HEADER_SIZE];
+       struct curl_slist *range_header = NULL;
+       struct active_request_slot *slot;
+
+       filename = sha1_file_name(request->obj->sha1);
+       snprintf(request->filename, sizeof(request->filename), "%s", filename);
+       snprintf(request->tmpfile, sizeof(request->tmpfile),
+                "%s.temp", filename);
+
+       snprintf(prevfile, sizeof(prevfile), "%s.prev", request->filename);
+       unlink(prevfile);
+       rename(request->tmpfile, prevfile);
+       unlink(request->tmpfile);
+
+       if (request->local_fileno != -1)
+               error("fd leakage in start: %d", request->local_fileno);
+       request->local_fileno = open(request->tmpfile,
+                                    O_WRONLY | O_CREAT | O_EXCL, 0666);
+       /* This could have failed due to the "lazy directory creation";
+        * try to mkdir the last path component.
+        */
+       if (request->local_fileno < 0 && errno == ENOENT) {
+               char *dir = strrchr(request->tmpfile, '/');
+               if (dir) {
+                       *dir = 0;
+                       mkdir(request->tmpfile, 0777);
+                       *dir = '/';
+               }
+               request->local_fileno = open(request->tmpfile,
+                                            O_WRONLY | O_CREAT | O_EXCL, 0666);
+       }
+
+       if (request->local_fileno < 0) {
+               request->state = ABORTED;
+               error("Couldn't create temporary file %s for %s: %s",
+                     request->tmpfile, request->filename, strerror(errno));
+               return;
+       }
+
+       memset(&request->stream, 0, sizeof(request->stream));
+
+       inflateInit(&request->stream);
+
+       SHA1_Init(&request->c);
+
+       url = xmalloc(strlen(remote->url) + 50);
+       request->url = xmalloc(strlen(remote->url) + 50);
+       strcpy(url, remote->url);
+       posn = url + strlen(remote->url);
+       strcpy(posn, "objects/");
+       posn += 8;
+       memcpy(posn, hex, 2);
+       posn += 2;
+       *(posn++) = '/';
+       strcpy(posn, hex + 2);
+       strcpy(request->url, url);
+
+       /* If a previous temp file is present, process what was already
+          fetched. */
+       prevlocal = open(prevfile, O_RDONLY);
+       if (prevlocal != -1) {
+               do {
+                       prev_read = xread(prevlocal, prev_buf, PREV_BUF_SIZE);
+                       if (prev_read>0) {
+                               if (fwrite_sha1_file(prev_buf,
+                                                    1,
+                                                    prev_read,
+                                                    request) == prev_read) {
+                                       prev_posn += prev_read;
+                               } else {
+                                       prev_read = -1;
+                               }
+                       }
+               } while (prev_read > 0);
+               close(prevlocal);
+       }
+       unlink(prevfile);
+
+       /* Reset inflate/SHA1 if there was an error reading the previous temp
+          file; also rewind to the beginning of the local file. */
+       if (prev_read == -1) {
+               memset(&request->stream, 0, sizeof(request->stream));
+               inflateInit(&request->stream);
+               SHA1_Init(&request->c);
+               if (prev_posn>0) {
+                       prev_posn = 0;
+                       lseek(request->local_fileno, 0, SEEK_SET);
+                       ftruncate(request->local_fileno, 0);
+               }
+       }
+
+       slot = get_active_slot();
+       slot->callback_func = process_response;
+       slot->callback_data = request;
+       request->slot = slot;
+
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, request);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_sha1_file);
+       curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, request->errorstr);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_pragma_header);
+
+       /* If we have successfully processed data from a previous fetch
+          attempt, only fetch the data we don't already have. */
+       if (prev_posn>0) {
+               if (push_verbosely)
+                       fprintf(stderr,
+                               "Resuming fetch of object %s at byte %ld\n",
+                               hex, prev_posn);
+               sprintf(range, "Range: bytes=%ld-", prev_posn);
+               range_header = curl_slist_append(range_header, range);
+               curl_easy_setopt(slot->curl,
+                                CURLOPT_HTTPHEADER, range_header);
+       }
+
+       /* Try to get the request started, abort the request on error */
+       request->state = RUN_FETCH_LOOSE;
+       if (!start_active_slot(slot)) {
+               fprintf(stderr, "Unable to start GET request\n");
+               remote->can_update_info_refs = 0;
+               release_request(request);
+       }
+}
+
+static void start_mkcol(struct transfer_request *request)
+{
+       char *hex = sha1_to_hex(request->obj->sha1);
+       struct active_request_slot *slot;
+       char *posn;
+
+       request->url = xmalloc(strlen(remote->url) + 13);
+       strcpy(request->url, remote->url);
+       posn = request->url + strlen(remote->url);
+       strcpy(posn, "objects/");
+       posn += 8;
+       memcpy(posn, hex, 2);
+       posn += 2;
+       strcpy(posn, "/");
+
+       slot = get_active_slot();
+       slot->callback_func = process_response;
+       slot->callback_data = request;
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); /* undo PUT setup */
+       curl_easy_setopt(slot->curl, CURLOPT_URL, request->url);
+       curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, request->errorstr);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_MKCOL);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+
+       if (start_active_slot(slot)) {
+               request->slot = slot;
+               request->state = RUN_MKCOL;
+       } else {
+               request->state = ABORTED;
+               free(request->url);
+               request->url = NULL;
+       }
+}
+#endif
+
+static void start_fetch_packed(struct transfer_request *request)
+{
+       char *url;
+       struct packed_git *target;
+       FILE *packfile;
+       char *filename;
+       long prev_posn = 0;
+       char range[RANGE_HEADER_SIZE];
+       struct curl_slist *range_header = NULL;
+
+       struct transfer_request *check_request = request_queue_head;
+       struct active_request_slot *slot;
+
+       target = find_sha1_pack(request->obj->sha1, remote->packs);
+       if (!target) {
+               fprintf(stderr, "Unable to fetch %s, will not be able to update server info refs\n", sha1_to_hex(request->obj->sha1));
+               remote->can_update_info_refs = 0;
+               release_request(request);
+               return;
+       }
+
+       fprintf(stderr, "Fetching pack %s\n", sha1_to_hex(target->sha1));
+       fprintf(stderr, " which contains %s\n", sha1_to_hex(request->obj->sha1));
+
+       filename = sha1_pack_name(target->sha1);
+       snprintf(request->filename, sizeof(request->filename), "%s", filename);
+       snprintf(request->tmpfile, sizeof(request->tmpfile),
+                "%s.temp", filename);
+
+       url = xmalloc(strlen(remote->url) + 64);
+       sprintf(url, "%sobjects/pack/pack-%s.pack",
+               remote->url, sha1_to_hex(target->sha1));
+
+       /* Make sure there isn't another open request for this pack */
+       while (check_request) {
+               if (check_request->state == RUN_FETCH_PACKED &&
+                   !strcmp(check_request->url, url)) {
+                       free(url);
+                       release_request(request);
+                       return;
+               }
+               check_request = check_request->next;
+       }
+
+       packfile = fopen(request->tmpfile, "a");
+       if (!packfile) {
+               fprintf(stderr, "Unable to open local file %s for pack",
+                       request->tmpfile);
+               remote->can_update_info_refs = 0;
+               free(url);
+               return;
+       }
+
+       slot = get_active_slot();
+       slot->callback_func = process_response;
+       slot->callback_data = request;
+       request->slot = slot;
+       request->local_stream = packfile;
+       request->userData = target;
+
+       request->url = url;
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, packfile);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_pragma_header);
+       slot->local = packfile;
+
+       /* If there is data present from a previous transfer attempt,
+          resume where it left off */
+       prev_posn = ftell(packfile);
+       if (prev_posn>0) {
+               if (push_verbosely)
+                       fprintf(stderr,
+                               "Resuming fetch of pack %s at byte %ld\n",
+                               sha1_to_hex(target->sha1), prev_posn);
+               sprintf(range, "Range: bytes=%ld-", prev_posn);
+               range_header = curl_slist_append(range_header, range);
+               curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, range_header);
+       }
+
+       /* Try to get the request started, abort the request on error */
+       request->state = RUN_FETCH_PACKED;
+       if (!start_active_slot(slot)) {
+               fprintf(stderr, "Unable to start GET request\n");
+               remote->can_update_info_refs = 0;
+               release_request(request);
+       }
+}
+
+static void start_put(struct transfer_request *request)
+{
+       char *hex = sha1_to_hex(request->obj->sha1);
+       struct active_request_slot *slot;
+       char *posn;
+       enum object_type type;
+       char hdr[50];
+       void *unpacked;
+       unsigned long len;
+       int hdrlen;
+       ssize_t size;
+       z_stream stream;
+
+       unpacked = read_sha1_file(request->obj->sha1, &type, &len);
+       hdrlen = sprintf(hdr, "%s %lu", typename(type), len) + 1;
+
+       /* Set it up */
+       memset(&stream, 0, sizeof(stream));
+       deflateInit(&stream, zlib_compression_level);
+       size = deflateBound(&stream, len + hdrlen);
+       request->buffer.buffer = xmalloc(size);
+
+       /* Compress it */
+       stream.next_out = request->buffer.buffer;
+       stream.avail_out = size;
+
+       /* First header.. */
+       stream.next_in = (void *)hdr;
+       stream.avail_in = hdrlen;
+       while (deflate(&stream, 0) == Z_OK)
+               /* nothing */;
+
+       /* Then the data itself.. */
+       stream.next_in = unpacked;
+       stream.avail_in = len;
+       while (deflate(&stream, Z_FINISH) == Z_OK)
+               /* nothing */;
+       deflateEnd(&stream);
+       free(unpacked);
+
+       request->buffer.size = stream.total_out;
+       request->buffer.posn = 0;
+
+       request->url = xmalloc(strlen(remote->url) +
+                              strlen(request->lock->token) + 51);
+       strcpy(request->url, remote->url);
+       posn = request->url + strlen(remote->url);
+       strcpy(posn, "objects/");
+       posn += 8;
+       memcpy(posn, hex, 2);
+       posn += 2;
+       *(posn++) = '/';
+       strcpy(posn, hex + 2);
+       request->dest = xmalloc(strlen(request->url) + 14);
+       sprintf(request->dest, "Destination: %s", request->url);
+       posn += 38;
+       *(posn++) = '_';
+       strcpy(posn, request->lock->token);
+
+       slot = get_active_slot();
+       slot->callback_func = process_response;
+       slot->callback_data = request;
+       curl_easy_setopt(slot->curl, CURLOPT_INFILE, &request->buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, request->buffer.size);
+       curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PUT);
+       curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_PUT, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 0);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, request->url);
+
+       if (start_active_slot(slot)) {
+               request->slot = slot;
+               request->state = RUN_PUT;
+       } else {
+               request->state = ABORTED;
+               free(request->url);
+               request->url = NULL;
+       }
+}
+
+static void start_move(struct transfer_request *request)
+{
+       struct active_request_slot *slot;
+       struct curl_slist *dav_headers = NULL;
+
+       slot = get_active_slot();
+       slot->callback_func = process_response;
+       slot->callback_data = request;
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); /* undo PUT setup */
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_MOVE);
+       dav_headers = curl_slist_append(dav_headers, request->dest);
+       dav_headers = curl_slist_append(dav_headers, "Overwrite: T");
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, request->url);
+
+       if (start_active_slot(slot)) {
+               request->slot = slot;
+               request->state = RUN_MOVE;
+       } else {
+               request->state = ABORTED;
+               free(request->url);
+               request->url = NULL;
+       }
+}
+
+static int refresh_lock(struct remote_lock *lock)
+{
+       struct active_request_slot *slot;
+       struct slot_results results;
+       char *if_header;
+       char timeout_header[25];
+       struct curl_slist *dav_headers = NULL;
+       int rc = 0;
+
+       lock->refreshing = 1;
+
+       if_header = xmalloc(strlen(lock->token) + 25);
+       sprintf(if_header, "If: (<opaquelocktoken:%s>)", lock->token);
+       sprintf(timeout_header, "Timeout: Second-%ld", lock->timeout);
+       dav_headers = curl_slist_append(dav_headers, if_header);
+       dav_headers = curl_slist_append(dav_headers, timeout_header);
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, lock->url);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_LOCK);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers);
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK) {
+                       fprintf(stderr, "LOCK HTTP error %ld\n",
+                               results.http_code);
+               } else {
+                       lock->start_time = time(NULL);
+                       rc = 1;
+               }
+       }
+
+       lock->refreshing = 0;
+       curl_slist_free_all(dav_headers);
+       free(if_header);
+
+       return rc;
+}
+
+static void check_locks(void)
+{
+       struct remote_lock *lock = remote->locks;
+       time_t current_time = time(NULL);
+       int time_remaining;
+
+       while (lock) {
+               time_remaining = lock->start_time + lock->timeout -
+                       current_time;
+               if (!lock->refreshing && time_remaining < LOCK_REFRESH) {
+                       if (!refresh_lock(lock)) {
+                               fprintf(stderr,
+                                       "Unable to refresh lock for %s\n",
+                                       lock->url);
+                               aborted = 1;
+                               return;
+                       }
+               }
+               lock = lock->next;
+       }
+}
+
+static void release_request(struct transfer_request *request)
+{
+       struct transfer_request *entry = request_queue_head;
+
+       if (request == request_queue_head) {
+               request_queue_head = request->next;
+       } else {
+               while (entry->next != NULL && entry->next != request)
+                       entry = entry->next;
+               if (entry->next == request)
+                       entry->next = entry->next->next;
+       }
+
+       if (request->local_fileno != -1)
+               close(request->local_fileno);
+       if (request->local_stream)
+               fclose(request->local_stream);
+       if (request->url != NULL)
+               free(request->url);
+       free(request);
+}
+
+static void finish_request(struct transfer_request *request)
+{
+       struct stat st;
+       struct packed_git *target;
+       struct packed_git **lst;
+
+       request->curl_result = request->slot->curl_result;
+       request->http_code = request->slot->http_code;
+       request->slot = NULL;
+
+       /* Keep locks active */
+       check_locks();
+
+       if (request->headers != NULL)
+               curl_slist_free_all(request->headers);
+
+       /* URL is reused for MOVE after PUT */
+       if (request->state != RUN_PUT) {
+               free(request->url);
+               request->url = NULL;
+       }
+
+       if (request->state == RUN_MKCOL) {
+               if (request->curl_result == CURLE_OK ||
+                   request->http_code == 405) {
+                       remote_dir_exists[request->obj->sha1[0]] = 1;
+                       start_put(request);
+               } else {
+                       fprintf(stderr, "MKCOL %s failed, aborting (%d/%ld)\n",
+                               sha1_to_hex(request->obj->sha1),
+                               request->curl_result, request->http_code);
+                       request->state = ABORTED;
+                       aborted = 1;
+               }
+       } else if (request->state == RUN_PUT) {
+               if (request->curl_result == CURLE_OK) {
+                       start_move(request);
+               } else {
+                       fprintf(stderr, "PUT %s failed, aborting (%d/%ld)\n",
+                               sha1_to_hex(request->obj->sha1),
+                               request->curl_result, request->http_code);
+                       request->state = ABORTED;
+                       aborted = 1;
+               }
+       } else if (request->state == RUN_MOVE) {
+               if (request->curl_result == CURLE_OK) {
+                       if (push_verbosely)
+                               fprintf(stderr, "    sent %s\n",
+                                       sha1_to_hex(request->obj->sha1));
+                       request->obj->flags |= REMOTE;
+                       release_request(request);
+               } else {
+                       fprintf(stderr, "MOVE %s failed, aborting (%d/%ld)\n",
+                               sha1_to_hex(request->obj->sha1),
+                               request->curl_result, request->http_code);
+                       request->state = ABORTED;
+                       aborted = 1;
+               }
+       } else if (request->state == RUN_FETCH_LOOSE) {
+               fchmod(request->local_fileno, 0444);
+               close(request->local_fileno); request->local_fileno = -1;
+
+               if (request->curl_result != CURLE_OK &&
+                   request->http_code != 416) {
+                       if (stat(request->tmpfile, &st) == 0) {
+                               if (st.st_size == 0)
+                                       unlink(request->tmpfile);
+                       }
+               } else {
+                       if (request->http_code == 416)
+                               fprintf(stderr, "Warning: requested range invalid; we may already have all the data.\n");
+
+                       inflateEnd(&request->stream);
+                       SHA1_Final(request->real_sha1, &request->c);
+                       if (request->zret != Z_STREAM_END) {
+                               unlink(request->tmpfile);
+                       } else if (hashcmp(request->obj->sha1, request->real_sha1)) {
+                               unlink(request->tmpfile);
+                       } else {
+                               request->rename =
+                                       move_temp_to_file(
+                                               request->tmpfile,
+                                               request->filename);
+                               if (request->rename == 0) {
+                                       request->obj->flags |= (LOCAL | REMOTE);
+                               }
+                       }
+               }
+
+               /* Try fetching packed if necessary */
+               if (request->obj->flags & LOCAL)
+                       release_request(request);
+               else
+                       start_fetch_packed(request);
+
+       } else if (request->state == RUN_FETCH_PACKED) {
+               if (request->curl_result != CURLE_OK) {
+                       fprintf(stderr, "Unable to get pack file %s\n%s",
+                               request->url, curl_errorstr);
+                       remote->can_update_info_refs = 0;
+               } else {
+                       off_t pack_size = ftell(request->local_stream);
+
+                       fclose(request->local_stream);
+                       request->local_stream = NULL;
+                       if (!move_temp_to_file(request->tmpfile,
+                                              request->filename)) {
+                               target = (struct packed_git *)request->userData;
+                               target->pack_size = pack_size;
+                               lst = &remote->packs;
+                               while (*lst != target)
+                                       lst = &((*lst)->next);
+                               *lst = (*lst)->next;
+
+                               if (!verify_pack(target, 0))
+                                       install_packed_git(target);
+                               else
+                                       remote->can_update_info_refs = 0;
+                       }
+               }
+               release_request(request);
+       }
+}
+
+#ifdef USE_CURL_MULTI
+static int fill_active_slot(void *unused)
+{
+       struct transfer_request *request = request_queue_head;
+
+       if (aborted)
+               return 0;
+
+       for (request = request_queue_head; request; request = request->next) {
+               if (request->state == NEED_FETCH) {
+                       start_fetch_loose(request);
+                       return 1;
+               } else if (pushing && request->state == NEED_PUSH) {
+                       if (remote_dir_exists[request->obj->sha1[0]] == 1) {
+                               start_put(request);
+                       } else {
+                               start_mkcol(request);
+                       }
+                       return 1;
+               }
+       }
+       return 0;
+}
+#endif
+
+static void get_remote_object_list(unsigned char parent);
+
+static void add_fetch_request(struct object *obj)
+{
+       struct transfer_request *request;
+
+       check_locks();
+
+       /*
+        * Don't fetch the object if it's known to exist locally
+        * or is already in the request queue
+        */
+       if (remote_dir_exists[obj->sha1[0]] == -1)
+               get_remote_object_list(obj->sha1[0]);
+       if (obj->flags & (LOCAL | FETCHING))
+               return;
+
+       obj->flags |= FETCHING;
+       request = xmalloc(sizeof(*request));
+       request->obj = obj;
+       request->url = NULL;
+       request->lock = NULL;
+       request->headers = NULL;
+       request->local_fileno = -1;
+       request->local_stream = NULL;
+       request->state = NEED_FETCH;
+       request->next = request_queue_head;
+       request_queue_head = request;
+
+#ifdef USE_CURL_MULTI
+       fill_active_slots();
+       step_active_slots();
+#endif
+}
+
+static int add_send_request(struct object *obj, struct remote_lock *lock)
+{
+       struct transfer_request *request = request_queue_head;
+       struct packed_git *target;
+
+       /* Keep locks active */
+       check_locks();
+
+       /*
+        * Don't push the object if it's known to exist on the remote
+        * or is already in the request queue
+        */
+       if (remote_dir_exists[obj->sha1[0]] == -1)
+               get_remote_object_list(obj->sha1[0]);
+       if (obj->flags & (REMOTE | PUSHING))
+               return 0;
+       target = find_sha1_pack(obj->sha1, remote->packs);
+       if (target) {
+               obj->flags |= REMOTE;
+               return 0;
+       }
+
+       obj->flags |= PUSHING;
+       request = xmalloc(sizeof(*request));
+       request->obj = obj;
+       request->url = NULL;
+       request->lock = lock;
+       request->headers = NULL;
+       request->local_fileno = -1;
+       request->local_stream = NULL;
+       request->state = NEED_PUSH;
+       request->next = request_queue_head;
+       request_queue_head = request;
+
+#ifdef USE_CURL_MULTI
+       fill_active_slots();
+       step_active_slots();
+#endif
+
+       return 1;
+}
+
+static int fetch_index(unsigned char *sha1)
+{
+       char *hex = sha1_to_hex(sha1);
+       char *filename;
+       char *url;
+       char tmpfile[PATH_MAX];
+       long prev_posn = 0;
+       char range[RANGE_HEADER_SIZE];
+       struct curl_slist *range_header = NULL;
+
+       FILE *indexfile;
+       struct active_request_slot *slot;
+       struct slot_results results;
+
+       /* Don't use the index if the pack isn't there */
+       url = xmalloc(strlen(remote->url) + 64);
+       sprintf(url, "%sobjects/pack/pack-%s.pack", remote->url, hex);
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 1);
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK) {
+                       free(url);
+                       return error("Unable to verify pack %s is available",
+                                    hex);
+               }
+       } else {
+               return error("Unable to start request");
+       }
+
+       if (has_pack_index(sha1))
+               return 0;
+
+       if (push_verbosely)
+               fprintf(stderr, "Getting index for pack %s\n", hex);
+
+       sprintf(url, "%sobjects/pack/pack-%s.idx", remote->url, hex);
+
+       filename = sha1_pack_index_name(sha1);
+       snprintf(tmpfile, sizeof(tmpfile), "%s.temp", filename);
+       indexfile = fopen(tmpfile, "a");
+       if (!indexfile)
+               return error("Unable to open local file %s for pack index",
+                            tmpfile);
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 0);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, indexfile);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_pragma_header);
+       slot->local = indexfile;
+
+       /* If there is data present from a previous transfer attempt,
+          resume where it left off */
+       prev_posn = ftell(indexfile);
+       if (prev_posn>0) {
+               if (push_verbosely)
+                       fprintf(stderr,
+                               "Resuming fetch of index for pack %s at byte %ld\n",
+                               hex, prev_posn);
+               sprintf(range, "Range: bytes=%ld-", prev_posn);
+               range_header = curl_slist_append(range_header, range);
+               curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, range_header);
+       }
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK) {
+                       free(url);
+                       fclose(indexfile);
+                       return error("Unable to get pack index %s\n%s", url,
+                                    curl_errorstr);
+               }
+       } else {
+               free(url);
+               fclose(indexfile);
+               return error("Unable to start request");
+       }
+
+       free(url);
+       fclose(indexfile);
+
+       return move_temp_to_file(tmpfile, filename);
+}
+
+static int setup_index(unsigned char *sha1)
+{
+       struct packed_git *new_pack;
+
+       if (fetch_index(sha1))
+               return -1;
+
+       new_pack = parse_pack_index(sha1);
+       new_pack->next = remote->packs;
+       remote->packs = new_pack;
+       return 0;
+}
+
+static int fetch_indices(void)
+{
+       unsigned char sha1[20];
+       char *url;
+       struct buffer buffer;
+       char *data;
+       int i = 0;
+
+       struct active_request_slot *slot;
+       struct slot_results results;
+
+       data = xcalloc(1, 4096);
+       buffer.size = 4096;
+       buffer.posn = 0;
+       buffer.buffer = data;
+
+       if (push_verbosely)
+               fprintf(stderr, "Getting pack list\n");
+
+       url = xmalloc(strlen(remote->url) + 20);
+       sprintf(url, "%sobjects/info/packs", remote->url);
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL);
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK) {
+                       free(buffer.buffer);
+                       free(url);
+                       if (results.http_code == 404)
+                               return 0;
+                       else
+                               return error("%s", curl_errorstr);
+               }
+       } else {
+               free(buffer.buffer);
+               free(url);
+               return error("Unable to start request");
+       }
+       free(url);
+
+       data = buffer.buffer;
+       while (i < buffer.posn) {
+               switch (data[i]) {
+               case 'P':
+                       i++;
+                       if (i + 52 < buffer.posn &&
+                           !prefixcmp(data + i, " pack-") &&
+                           !prefixcmp(data + i + 46, ".pack\n")) {
+                               get_sha1_hex(data + i + 6, sha1);
+                               setup_index(sha1);
+                               i += 51;
+                               break;
+                       }
+               default:
+                       while (data[i] != '\n')
+                               i++;
+               }
+               i++;
+       }
+
+       free(buffer.buffer);
+       return 0;
+}
+
+static inline int needs_quote(int ch)
+{
+       if (((ch >= 'A') && (ch <= 'Z'))
+                       || ((ch >= 'a') && (ch <= 'z'))
+                       || ((ch >= '0') && (ch <= '9'))
+                       || (ch == '/')
+                       || (ch == '-')
+                       || (ch == '.'))
+               return 0;
+       return 1;
+}
+
+static inline int hex(int v)
+{
+       if (v < 10) return '0' + v;
+       else return 'A' + v - 10;
+}
+
+static char *quote_ref_url(const char *base, const char *ref)
+{
+       const char *cp;
+       char *dp, *qref;
+       int len, baselen, ch;
+
+       baselen = strlen(base);
+       len = baselen + 1;
+       for (cp = ref; (ch = *cp) != 0; cp++, len++)
+               if (needs_quote(ch))
+                       len += 2; /* extra two hex plus replacement % */
+       qref = xmalloc(len);
+       memcpy(qref, base, baselen);
+       for (cp = ref, dp = qref + baselen; (ch = *cp) != 0; cp++) {
+               if (needs_quote(ch)) {
+                       *dp++ = '%';
+                       *dp++ = hex((ch >> 4) & 0xF);
+                       *dp++ = hex(ch & 0xF);
+               }
+               else
+                       *dp++ = ch;
+       }
+       *dp = 0;
+
+       return qref;
+}
+
+int fetch_ref(char *ref, unsigned char *sha1)
+{
+        char *url;
+        char hex[42];
+        struct buffer buffer;
+       char *base = remote->url;
+       struct active_request_slot *slot;
+       struct slot_results results;
+        buffer.size = 41;
+        buffer.posn = 0;
+        buffer.buffer = hex;
+        hex[41] = '\0';
+
+       url = quote_ref_url(base, ref);
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK)
+                       return error("Couldn't get %s for %s\n%s",
+                                    url, ref, curl_errorstr);
+       } else {
+               return error("Unable to start request");
+       }
+
+        hex[40] = '\0';
+        get_sha1_hex(hex, sha1);
+        return 0;
+}
+
+static void one_remote_object(const char *hex)
+{
+       unsigned char sha1[20];
+       struct object *obj;
+
+       if (get_sha1_hex(hex, sha1) != 0)
+               return;
+
+       obj = lookup_object(sha1);
+       if (!obj)
+               obj = parse_object(sha1);
+
+       /* Ignore remote objects that don't exist locally */
+       if (!obj)
+               return;
+
+       obj->flags |= REMOTE;
+       if (!object_list_contains(objects, obj))
+               object_list_insert(obj, &objects);
+}
+
+static void handle_lockprop_ctx(struct xml_ctx *ctx, int tag_closed)
+{
+       int *lock_flags = (int *)ctx->userData;
+
+       if (tag_closed) {
+               if (!strcmp(ctx->name, DAV_CTX_LOCKENTRY)) {
+                       if ((*lock_flags & DAV_PROP_LOCKEX) &&
+                           (*lock_flags & DAV_PROP_LOCKWR)) {
+                               *lock_flags |= DAV_LOCK_OK;
+                       }
+                       *lock_flags &= DAV_LOCK_OK;
+               } else if (!strcmp(ctx->name, DAV_CTX_LOCKTYPE_WRITE)) {
+                       *lock_flags |= DAV_PROP_LOCKWR;
+               } else if (!strcmp(ctx->name, DAV_CTX_LOCKTYPE_EXCLUSIVE)) {
+                       *lock_flags |= DAV_PROP_LOCKEX;
+               }
+       }
+}
+
+static void handle_new_lock_ctx(struct xml_ctx *ctx, int tag_closed)
+{
+       struct remote_lock *lock = (struct remote_lock *)ctx->userData;
+
+       if (tag_closed && ctx->cdata) {
+               if (!strcmp(ctx->name, DAV_ACTIVELOCK_OWNER)) {
+                       lock->owner = xmalloc(strlen(ctx->cdata) + 1);
+                       strcpy(lock->owner, ctx->cdata);
+               } else if (!strcmp(ctx->name, DAV_ACTIVELOCK_TIMEOUT)) {
+                       if (!prefixcmp(ctx->cdata, "Second-"))
+                               lock->timeout =
+                                       strtol(ctx->cdata + 7, NULL, 10);
+               } else if (!strcmp(ctx->name, DAV_ACTIVELOCK_TOKEN)) {
+                       if (!prefixcmp(ctx->cdata, "opaquelocktoken:")) {
+                               lock->token = xmalloc(strlen(ctx->cdata) - 15);
+                               strcpy(lock->token, ctx->cdata + 16);
+                       }
+               }
+       }
+}
+
+static void one_remote_ref(char *refname);
+
+static void
+xml_start_tag(void *userData, const char *name, const char **atts)
+{
+       struct xml_ctx *ctx = (struct xml_ctx *)userData;
+       const char *c = strchr(name, ':');
+       int new_len;
+
+       if (c == NULL)
+               c = name;
+       else
+               c++;
+
+       new_len = strlen(ctx->name) + strlen(c) + 2;
+
+       if (new_len > ctx->len) {
+               ctx->name = xrealloc(ctx->name, new_len);
+               ctx->len = new_len;
+       }
+       strcat(ctx->name, ".");
+       strcat(ctx->name, c);
+
+       free(ctx->cdata);
+       ctx->cdata = NULL;
+
+       ctx->userFunc(ctx, 0);
+}
+
+static void
+xml_end_tag(void *userData, const char *name)
+{
+       struct xml_ctx *ctx = (struct xml_ctx *)userData;
+       const char *c = strchr(name, ':');
+       char *ep;
+
+       ctx->userFunc(ctx, 1);
+
+       if (c == NULL)
+               c = name;
+       else
+               c++;
+
+       ep = ctx->name + strlen(ctx->name) - strlen(c) - 1;
+       *ep = 0;
+}
+
+static void
+xml_cdata(void *userData, const XML_Char *s, int len)
+{
+       struct xml_ctx *ctx = (struct xml_ctx *)userData;
+       free(ctx->cdata);
+       ctx->cdata = xmemdupz(s, len);
+}
+
+static struct remote_lock *lock_remote(const char *path, long timeout)
+{
+       struct active_request_slot *slot;
+       struct slot_results results;
+       struct buffer out_buffer;
+       struct buffer in_buffer;
+       char *out_data;
+       char *in_data;
+       char *url;
+       char *ep;
+       char timeout_header[25];
+       struct remote_lock *lock = NULL;
+       XML_Parser parser = XML_ParserCreate(NULL);
+       enum XML_Status result;
+       struct curl_slist *dav_headers = NULL;
+       struct xml_ctx ctx;
+
+       url = xmalloc(strlen(remote->url) + strlen(path) + 1);
+       sprintf(url, "%s%s", remote->url, path);
+
+       /* Make sure leading directories exist for the remote ref */
+       ep = strchr(url + strlen(remote->url) + 1, '/');
+       while (ep) {
+               *ep = 0;
+               slot = get_active_slot();
+               slot->results = &results;
+               curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1);
+               curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+               curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_MKCOL);
+               curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+               if (start_active_slot(slot)) {
+                       run_active_slot(slot);
+                       if (results.curl_result != CURLE_OK &&
+                           results.http_code != 405) {
+                               fprintf(stderr,
+                                       "Unable to create branch path %s\n",
+                                       url);
+                               free(url);
+                               return NULL;
+                       }
+               } else {
+                       fprintf(stderr, "Unable to start MKCOL request\n");
+                       free(url);
+                       return NULL;
+               }
+               *ep = '/';
+               ep = strchr(ep + 1, '/');
+       }
+
+       out_buffer.size = strlen(LOCK_REQUEST) + strlen(git_default_email) - 2;
+       out_data = xmalloc(out_buffer.size + 1);
+       snprintf(out_data, out_buffer.size + 1, LOCK_REQUEST, git_default_email);
+       out_buffer.posn = 0;
+       out_buffer.buffer = out_data;
+
+       in_buffer.size = 4096;
+       in_data = xmalloc(in_buffer.size);
+       in_buffer.posn = 0;
+       in_buffer.buffer = in_data;
+
+       sprintf(timeout_header, "Timeout: Second-%ld", timeout);
+       dav_headers = curl_slist_append(dav_headers, timeout_header);
+       dav_headers = curl_slist_append(dav_headers, "Content-Type: text/xml");
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_INFILE, &out_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, out_buffer.size);
+       curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &in_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_LOCK);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers);
+
+       lock = xcalloc(1, sizeof(*lock));
+       lock->timeout = -1;
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result == CURLE_OK) {
+                       ctx.name = xcalloc(10, 1);
+                       ctx.len = 0;
+                       ctx.cdata = NULL;
+                       ctx.userFunc = handle_new_lock_ctx;
+                       ctx.userData = lock;
+                       XML_SetUserData(parser, &ctx);
+                       XML_SetElementHandler(parser, xml_start_tag,
+                                             xml_end_tag);
+                       XML_SetCharacterDataHandler(parser, xml_cdata);
+                       result = XML_Parse(parser, in_buffer.buffer,
+                                          in_buffer.posn, 1);
+                       free(ctx.name);
+                       if (result != XML_STATUS_OK) {
+                               fprintf(stderr, "XML error: %s\n",
+                                       XML_ErrorString(
+                                               XML_GetErrorCode(parser)));
+                               lock->timeout = -1;
+                       }
+               }
+       } else {
+               fprintf(stderr, "Unable to start LOCK request\n");
+       }
+
+       curl_slist_free_all(dav_headers);
+       free(out_data);
+       free(in_data);
+
+       if (lock->token == NULL || lock->timeout <= 0) {
+               if (lock->token != NULL)
+                       free(lock->token);
+               if (lock->owner != NULL)
+                       free(lock->owner);
+               free(url);
+               free(lock);
+               lock = NULL;
+       } else {
+               lock->url = url;
+               lock->start_time = time(NULL);
+               lock->next = remote->locks;
+               remote->locks = lock;
+       }
+
+       return lock;
+}
+
+static int unlock_remote(struct remote_lock *lock)
+{
+       struct active_request_slot *slot;
+       struct slot_results results;
+       struct remote_lock *prev = remote->locks;
+       char *lock_token_header;
+       struct curl_slist *dav_headers = NULL;
+       int rc = 0;
+
+       lock_token_header = xmalloc(strlen(lock->token) + 31);
+       sprintf(lock_token_header, "Lock-Token: <opaquelocktoken:%s>",
+               lock->token);
+       dav_headers = curl_slist_append(dav_headers, lock_token_header);
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, lock->url);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_UNLOCK);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers);
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result == CURLE_OK)
+                       rc = 1;
+               else
+                       fprintf(stderr, "UNLOCK HTTP error %ld\n",
+                               results.http_code);
+       } else {
+               fprintf(stderr, "Unable to start UNLOCK request\n");
+       }
+
+       curl_slist_free_all(dav_headers);
+       free(lock_token_header);
+
+       if (remote->locks == lock) {
+               remote->locks = lock->next;
+       } else {
+               while (prev && prev->next != lock)
+                       prev = prev->next;
+               if (prev)
+                       prev->next = prev->next->next;
+       }
+
+       if (lock->owner != NULL)
+               free(lock->owner);
+       free(lock->url);
+       free(lock->token);
+       free(lock);
+
+       return rc;
+}
+
+static void remote_ls(const char *path, int flags,
+                     void (*userFunc)(struct remote_ls_ctx *ls),
+                     void *userData);
+
+static void process_ls_object(struct remote_ls_ctx *ls)
+{
+       unsigned int *parent = (unsigned int *)ls->userData;
+       char *path = ls->dentry_name;
+       char *obj_hex;
+
+       if (!strcmp(ls->path, ls->dentry_name) && (ls->flags & IS_DIR)) {
+               remote_dir_exists[*parent] = 1;
+               return;
+       }
+
+       if (strlen(path) != 49)
+               return;
+       path += 8;
+       obj_hex = xmalloc(strlen(path));
+       /* NB: path is not null-terminated, can not use strlcpy here */
+       memcpy(obj_hex, path, 2);
+       strcpy(obj_hex + 2, path + 3);
+       one_remote_object(obj_hex);
+       free(obj_hex);
+}
+
+static void process_ls_ref(struct remote_ls_ctx *ls)
+{
+       if (!strcmp(ls->path, ls->dentry_name) && (ls->dentry_flags & IS_DIR)) {
+               fprintf(stderr, "  %s\n", ls->dentry_name);
+               return;
+       }
+
+       if (!(ls->dentry_flags & IS_DIR))
+               one_remote_ref(ls->dentry_name);
+}
+
+static void handle_remote_ls_ctx(struct xml_ctx *ctx, int tag_closed)
+{
+       struct remote_ls_ctx *ls = (struct remote_ls_ctx *)ctx->userData;
+
+       if (tag_closed) {
+               if (!strcmp(ctx->name, DAV_PROPFIND_RESP) && ls->dentry_name) {
+                       if (ls->dentry_flags & IS_DIR) {
+                               if (ls->flags & PROCESS_DIRS) {
+                                       ls->userFunc(ls);
+                               }
+                               if (strcmp(ls->dentry_name, ls->path) &&
+                                   ls->flags & RECURSIVE) {
+                                       remote_ls(ls->dentry_name,
+                                                 ls->flags,
+                                                 ls->userFunc,
+                                                 ls->userData);
+                               }
+                       } else if (ls->flags & PROCESS_FILES) {
+                               ls->userFunc(ls);
+                       }
+               } else if (!strcmp(ctx->name, DAV_PROPFIND_NAME) && ctx->cdata) {
+                       ls->dentry_name = xmalloc(strlen(ctx->cdata) -
+                                                 remote->path_len + 1);
+                       strcpy(ls->dentry_name, ctx->cdata + remote->path_len);
+               } else if (!strcmp(ctx->name, DAV_PROPFIND_COLLECTION)) {
+                       ls->dentry_flags |= IS_DIR;
+               }
+       } else if (!strcmp(ctx->name, DAV_PROPFIND_RESP)) {
+               free(ls->dentry_name);
+               ls->dentry_name = NULL;
+               ls->dentry_flags = 0;
+       }
+}
+
+static void remote_ls(const char *path, int flags,
+                     void (*userFunc)(struct remote_ls_ctx *ls),
+                     void *userData)
+{
+       char *url = xmalloc(strlen(remote->url) + strlen(path) + 1);
+       struct active_request_slot *slot;
+       struct slot_results results;
+       struct buffer in_buffer;
+       struct buffer out_buffer;
+       char *in_data;
+       char *out_data;
+       XML_Parser parser = XML_ParserCreate(NULL);
+       enum XML_Status result;
+       struct curl_slist *dav_headers = NULL;
+       struct xml_ctx ctx;
+       struct remote_ls_ctx ls;
+
+       ls.flags = flags;
+       ls.path = xstrdup(path);
+       ls.dentry_name = NULL;
+       ls.dentry_flags = 0;
+       ls.userData = userData;
+       ls.userFunc = userFunc;
+
+       sprintf(url, "%s%s", remote->url, path);
+
+       out_buffer.size = strlen(PROPFIND_ALL_REQUEST);
+       out_data = xmalloc(out_buffer.size + 1);
+       snprintf(out_data, out_buffer.size + 1, PROPFIND_ALL_REQUEST);
+       out_buffer.posn = 0;
+       out_buffer.buffer = out_data;
+
+       in_buffer.size = 4096;
+       in_data = xmalloc(in_buffer.size);
+       in_buffer.posn = 0;
+       in_buffer.buffer = in_data;
+
+       dav_headers = curl_slist_append(dav_headers, "Depth: 1");
+       dav_headers = curl_slist_append(dav_headers, "Content-Type: text/xml");
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_INFILE, &out_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, out_buffer.size);
+       curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &in_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PROPFIND);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers);
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result == CURLE_OK) {
+                       ctx.name = xcalloc(10, 1);
+                       ctx.len = 0;
+                       ctx.cdata = NULL;
+                       ctx.userFunc = handle_remote_ls_ctx;
+                       ctx.userData = &ls;
+                       XML_SetUserData(parser, &ctx);
+                       XML_SetElementHandler(parser, xml_start_tag,
+                                             xml_end_tag);
+                       XML_SetCharacterDataHandler(parser, xml_cdata);
+                       result = XML_Parse(parser, in_buffer.buffer,
+                                          in_buffer.posn, 1);
+                       free(ctx.name);
+
+                       if (result != XML_STATUS_OK) {
+                               fprintf(stderr, "XML error: %s\n",
+                                       XML_ErrorString(
+                                               XML_GetErrorCode(parser)));
+                       }
+               }
+       } else {
+               fprintf(stderr, "Unable to start PROPFIND request\n");
+       }
+
+       free(ls.path);
+       free(url);
+       free(out_data);
+       free(in_buffer.buffer);
+       curl_slist_free_all(dav_headers);
+}
+
+static void get_remote_object_list(unsigned char parent)
+{
+       char path[] = "objects/XX/";
+       static const char hex[] = "0123456789abcdef";
+       unsigned int val = parent;
+
+       path[8] = hex[val >> 4];
+       path[9] = hex[val & 0xf];
+       remote_dir_exists[val] = 0;
+       remote_ls(path, (PROCESS_FILES | PROCESS_DIRS),
+                 process_ls_object, &val);
+}
+
+static int locking_available(void)
+{
+       struct active_request_slot *slot;
+       struct slot_results results;
+       struct buffer in_buffer;
+       struct buffer out_buffer;
+       char *in_data;
+       char *out_data;
+       XML_Parser parser = XML_ParserCreate(NULL);
+       enum XML_Status result;
+       struct curl_slist *dav_headers = NULL;
+       struct xml_ctx ctx;
+       int lock_flags = 0;
+
+       out_buffer.size =
+               strlen(PROPFIND_SUPPORTEDLOCK_REQUEST) +
+               strlen(remote->url) - 2;
+       out_data = xmalloc(out_buffer.size + 1);
+       snprintf(out_data, out_buffer.size + 1,
+                PROPFIND_SUPPORTEDLOCK_REQUEST, remote->url);
+       out_buffer.posn = 0;
+       out_buffer.buffer = out_data;
+
+       in_buffer.size = 4096;
+       in_data = xmalloc(in_buffer.size);
+       in_buffer.posn = 0;
+       in_buffer.buffer = in_data;
+
+       dav_headers = curl_slist_append(dav_headers, "Depth: 0");
+       dav_headers = curl_slist_append(dav_headers, "Content-Type: text/xml");
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_INFILE, &out_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, out_buffer.size);
+       curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &in_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, remote->url);
+       curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PROPFIND);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers);
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result == CURLE_OK) {
+                       ctx.name = xcalloc(10, 1);
+                       ctx.len = 0;
+                       ctx.cdata = NULL;
+                       ctx.userFunc = handle_lockprop_ctx;
+                       ctx.userData = &lock_flags;
+                       XML_SetUserData(parser, &ctx);
+                       XML_SetElementHandler(parser, xml_start_tag,
+                                             xml_end_tag);
+                       result = XML_Parse(parser, in_buffer.buffer,
+                                          in_buffer.posn, 1);
+                       free(ctx.name);
+
+                       if (result != XML_STATUS_OK) {
+                               fprintf(stderr, "XML error: %s\n",
+                                       XML_ErrorString(
+                                               XML_GetErrorCode(parser)));
+                               lock_flags = 0;
+                       }
+               }
+       } else {
+               fprintf(stderr, "Unable to start PROPFIND request\n");
+       }
+
+       free(out_data);
+       free(in_buffer.buffer);
+       curl_slist_free_all(dav_headers);
+
+       return lock_flags;
+}
+
+static struct object_list **add_one_object(struct object *obj, struct object_list **p)
+{
+       struct object_list *entry = xmalloc(sizeof(struct object_list));
+       entry->item = obj;
+       entry->next = *p;
+       *p = entry;
+       return &entry->next;
+}
+
+static struct object_list **process_blob(struct blob *blob,
+                                        struct object_list **p,
+                                        struct name_path *path,
+                                        const char *name)
+{
+       struct object *obj = &blob->object;
+
+       obj->flags |= LOCAL;
+
+       if (obj->flags & (UNINTERESTING | SEEN))
+               return p;
+
+       obj->flags |= SEEN;
+       return add_one_object(obj, p);
+}
+
+static struct object_list **process_tree(struct tree *tree,
+                                        struct object_list **p,
+                                        struct name_path *path,
+                                        const char *name)
+{
+       struct object *obj = &tree->object;
+       struct tree_desc desc;
+       struct name_entry entry;
+       struct name_path me;
+
+       obj->flags |= LOCAL;
+
+       if (obj->flags & (UNINTERESTING | SEEN))
+               return p;
+       if (parse_tree(tree) < 0)
+               die("bad tree object %s", sha1_to_hex(obj->sha1));
+
+       obj->flags |= SEEN;
+       name = xstrdup(name);
+       p = add_one_object(obj, p);
+       me.up = path;
+       me.elem = name;
+       me.elem_len = strlen(name);
+
+       init_tree_desc(&desc, tree->buffer, tree->size);
+
+       while (tree_entry(&desc, &entry)) {
+               if (S_ISDIR(entry.mode))
+                       p = process_tree(lookup_tree(entry.sha1), p, &me, name);
+               else
+                       p = process_blob(lookup_blob(entry.sha1), p, &me, name);
+       }
+       free(tree->buffer);
+       tree->buffer = NULL;
+       return p;
+}
+
+static int get_delta(struct rev_info *revs, struct remote_lock *lock)
+{
+       int i;
+       struct commit *commit;
+       struct object_list **p = &objects;
+       int count = 0;
+
+       while ((commit = get_revision(revs)) != NULL) {
+               p = process_tree(commit->tree, p, NULL, "");
+               commit->object.flags |= LOCAL;
+               if (!(commit->object.flags & UNINTERESTING))
+                       count += add_send_request(&commit->object, lock);
+       }
+
+       for (i = 0; i < revs->pending.nr; i++) {
+               struct object_array_entry *entry = revs->pending.objects + i;
+               struct object *obj = entry->item;
+               const char *name = entry->name;
+
+               if (obj->flags & (UNINTERESTING | SEEN))
+                       continue;
+               if (obj->type == OBJ_TAG) {
+                       obj->flags |= SEEN;
+                       p = add_one_object(obj, p);
+                       continue;
+               }
+               if (obj->type == OBJ_TREE) {
+                       p = process_tree((struct tree *)obj, p, NULL, name);
+                       continue;
+               }
+               if (obj->type == OBJ_BLOB) {
+                       p = process_blob((struct blob *)obj, p, NULL, name);
+                       continue;
+               }
+               die("unknown pending object %s (%s)", sha1_to_hex(obj->sha1), name);
+       }
+
+       while (objects) {
+               if (!(objects->item->flags & UNINTERESTING))
+                       count += add_send_request(objects->item, lock);
+               objects = objects->next;
+       }
+
+       return count;
+}
+
+static int update_remote(unsigned char *sha1, struct remote_lock *lock)
+{
+       struct active_request_slot *slot;
+       struct slot_results results;
+       char *out_data;
+       char *if_header;
+       struct buffer out_buffer;
+       struct curl_slist *dav_headers = NULL;
+       int i;
+
+       if_header = xmalloc(strlen(lock->token) + 25);
+       sprintf(if_header, "If: (<opaquelocktoken:%s>)", lock->token);
+       dav_headers = curl_slist_append(dav_headers, if_header);
+
+       out_buffer.size = 41;
+       out_data = xmalloc(out_buffer.size + 1);
+       i = snprintf(out_data, out_buffer.size + 1, "%s\n", sha1_to_hex(sha1));
+       if (i != out_buffer.size) {
+               fprintf(stderr, "Unable to initialize PUT request body\n");
+               return 0;
+       }
+       out_buffer.posn = 0;
+       out_buffer.buffer = out_data;
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_INFILE, &out_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, out_buffer.size);
+       curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PUT);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers);
+       curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_PUT, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, lock->url);
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               free(out_data);
+               free(if_header);
+               if (results.curl_result != CURLE_OK) {
+                       fprintf(stderr,
+                               "PUT error: curl result=%d, HTTP code=%ld\n",
+                               results.curl_result, results.http_code);
+                       /* We should attempt recovery? */
+                       return 0;
+               }
+       } else {
+               free(out_data);
+               free(if_header);
+               fprintf(stderr, "Unable to start PUT request\n");
+               return 0;
+       }
+
+       return 1;
+}
+
+static struct ref *local_refs, **local_tail;
+static struct ref *remote_refs, **remote_tail;
+
+static int one_local_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct ref *ref;
+       int len = strlen(refname) + 1;
+       ref = xcalloc(1, sizeof(*ref) + len);
+       hashcpy(ref->new_sha1, sha1);
+       memcpy(ref->name, refname, len);
+       *local_tail = ref;
+       local_tail = &ref->next;
+       return 0;
+}
+
+static void one_remote_ref(char *refname)
+{
+       struct ref *ref;
+       unsigned char remote_sha1[20];
+       struct object *obj;
+       int len = strlen(refname) + 1;
+
+       if (fetch_ref(refname, remote_sha1) != 0) {
+               fprintf(stderr,
+                       "Unable to fetch ref %s from %s\n",
+                       refname, remote->url);
+               return;
+       }
+
+       /*
+        * Fetch a copy of the object if it doesn't exist locally - it
+        * may be required for updating server info later.
+        */
+       if (remote->can_update_info_refs && !has_sha1_file(remote_sha1)) {
+               obj = lookup_unknown_object(remote_sha1);
+               if (obj) {
+                       fprintf(stderr, "  fetch %s for %s\n",
+                               sha1_to_hex(remote_sha1), refname);
+                       add_fetch_request(obj);
+               }
+       }
+
+       ref = xcalloc(1, sizeof(*ref) + len);
+       hashcpy(ref->old_sha1, remote_sha1);
+       memcpy(ref->name, refname, len);
+       *remote_tail = ref;
+       remote_tail = &ref->next;
+}
+
+static void get_local_heads(void)
+{
+       local_tail = &local_refs;
+       for_each_ref(one_local_ref, NULL);
+}
+
+static void get_dav_remote_heads(void)
+{
+       remote_tail = &remote_refs;
+       remote_ls("refs/", (PROCESS_FILES | PROCESS_DIRS | RECURSIVE), process_ls_ref, NULL);
+}
+
+static int is_zero_sha1(const unsigned char *sha1)
+{
+       int i;
+
+       for (i = 0; i < 20; i++) {
+               if (*sha1++)
+                       return 0;
+       }
+       return 1;
+}
+
+static void unmark_and_free(struct commit_list *list, unsigned int mark)
+{
+       while (list) {
+               struct commit_list *temp = list;
+               temp->item->object.flags &= ~mark;
+               list = temp->next;
+               free(temp);
+       }
+}
+
+static int ref_newer(const unsigned char *new_sha1,
+                    const unsigned char *old_sha1)
+{
+       struct object *o;
+       struct commit *old, *new;
+       struct commit_list *list, *used;
+       int found = 0;
+
+       /* Both new and old must be commit-ish and new is descendant of
+        * old.  Otherwise we require --force.
+        */
+       o = deref_tag(parse_object(old_sha1), NULL, 0);
+       if (!o || o->type != OBJ_COMMIT)
+               return 0;
+       old = (struct commit *) o;
+
+       o = deref_tag(parse_object(new_sha1), NULL, 0);
+       if (!o || o->type != OBJ_COMMIT)
+               return 0;
+       new = (struct commit *) o;
+
+       if (parse_commit(new) < 0)
+               return 0;
+
+       used = list = NULL;
+       commit_list_insert(new, &list);
+       while (list) {
+               new = pop_most_recent_commit(&list, TMP_MARK);
+               commit_list_insert(new, &used);
+               if (new == old) {
+                       found = 1;
+                       break;
+               }
+       }
+       unmark_and_free(list, TMP_MARK);
+       unmark_and_free(used, TMP_MARK);
+       return found;
+}
+
+static void mark_edge_parents_uninteresting(struct commit *commit)
+{
+       struct commit_list *parents;
+
+       for (parents = commit->parents; parents; parents = parents->next) {
+               struct commit *parent = parents->item;
+               if (!(parent->object.flags & UNINTERESTING))
+                       continue;
+               mark_tree_uninteresting(parent->tree);
+       }
+}
+
+static void mark_edges_uninteresting(struct commit_list *list)
+{
+       for ( ; list; list = list->next) {
+               struct commit *commit = list->item;
+
+               if (commit->object.flags & UNINTERESTING) {
+                       mark_tree_uninteresting(commit->tree);
+                       continue;
+               }
+               mark_edge_parents_uninteresting(commit);
+       }
+}
+
+static void add_remote_info_ref(struct remote_ls_ctx *ls)
+{
+       struct buffer *buf = (struct buffer *)ls->userData;
+       unsigned char remote_sha1[20];
+       struct object *o;
+       int len;
+       char *ref_info;
+
+       if (fetch_ref(ls->dentry_name, remote_sha1) != 0) {
+               fprintf(stderr,
+                       "Unable to fetch ref %s from %s\n",
+                       ls->dentry_name, remote->url);
+               aborted = 1;
+               return;
+       }
+
+       o = parse_object(remote_sha1);
+       if (!o) {
+               fprintf(stderr,
+                       "Unable to parse object %s for remote ref %s\n",
+                       sha1_to_hex(remote_sha1), ls->dentry_name);
+               aborted = 1;
+               return;
+       }
+
+       len = strlen(ls->dentry_name) + 42;
+       ref_info = xcalloc(len + 1, 1);
+       sprintf(ref_info, "%s   %s\n",
+               sha1_to_hex(remote_sha1), ls->dentry_name);
+       fwrite_buffer(ref_info, 1, len, buf);
+       free(ref_info);
+
+       if (o->type == OBJ_TAG) {
+               o = deref_tag(o, ls->dentry_name, 0);
+               if (o) {
+                       len = strlen(ls->dentry_name) + 45;
+                       ref_info = xcalloc(len + 1, 1);
+                       sprintf(ref_info, "%s   %s^{}\n",
+                               sha1_to_hex(o->sha1), ls->dentry_name);
+                       fwrite_buffer(ref_info, 1, len, buf);
+                       free(ref_info);
+               }
+       }
+}
+
+static void update_remote_info_refs(struct remote_lock *lock)
+{
+       struct buffer buffer;
+       struct active_request_slot *slot;
+       struct slot_results results;
+       char *if_header;
+       struct curl_slist *dav_headers = NULL;
+
+       buffer.buffer = xcalloc(1, 4096);
+       buffer.size = 4096;
+       buffer.posn = 0;
+       remote_ls("refs/", (PROCESS_FILES | RECURSIVE),
+                 add_remote_info_ref, &buffer);
+       if (!aborted) {
+               if_header = xmalloc(strlen(lock->token) + 25);
+               sprintf(if_header, "If: (<opaquelocktoken:%s>)", lock->token);
+               dav_headers = curl_slist_append(dav_headers, if_header);
+
+               slot = get_active_slot();
+               slot->results = &results;
+               curl_easy_setopt(slot->curl, CURLOPT_INFILE, &buffer);
+               curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, buffer.posn);
+               curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer);
+               curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+               curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PUT);
+               curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers);
+               curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1);
+               curl_easy_setopt(slot->curl, CURLOPT_PUT, 1);
+               curl_easy_setopt(slot->curl, CURLOPT_URL, lock->url);
+
+               buffer.posn = 0;
+
+               if (start_active_slot(slot)) {
+                       run_active_slot(slot);
+                       if (results.curl_result != CURLE_OK) {
+                               fprintf(stderr,
+                                       "PUT error: curl result=%d, HTTP code=%ld\n",
+                                       results.curl_result, results.http_code);
+                       }
+               }
+               free(if_header);
+       }
+       free(buffer.buffer);
+}
+
+static int remote_exists(const char *path)
+{
+       char *url = xmalloc(strlen(remote->url) + strlen(path) + 1);
+       struct active_request_slot *slot;
+       struct slot_results results;
+
+       sprintf(url, "%s%s", remote->url, path);
+
+        slot = get_active_slot();
+       slot->results = &results;
+        curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+        curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 1);
+
+        if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.http_code == 404)
+                       return 0;
+               else if (results.curl_result == CURLE_OK)
+                       return 1;
+               else
+                       fprintf(stderr, "HEAD HTTP error %ld\n", results.http_code);
+       } else {
+               fprintf(stderr, "Unable to start HEAD request\n");
+       }
+
+       return -1;
+}
+
+static void fetch_symref(const char *path, char **symref, unsigned char *sha1)
+{
+       char *url;
+       struct buffer buffer;
+       struct active_request_slot *slot;
+       struct slot_results results;
+
+       url = xmalloc(strlen(remote->url) + strlen(path) + 1);
+       sprintf(url, "%s%s", remote->url, path);
+
+       buffer.size = 4096;
+       buffer.posn = 0;
+       buffer.buffer = xmalloc(buffer.size);
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK) {
+                       die("Couldn't get %s for remote symref\n%s",
+                           url, curl_errorstr);
+               }
+       } else {
+               die("Unable to start remote symref request");
+       }
+       free(url);
+
+       if (*symref != NULL)
+               free(*symref);
+       *symref = NULL;
+       hashclr(sha1);
+
+       if (buffer.posn == 0)
+               return;
+
+       /* If it's a symref, set the refname; otherwise try for a sha1 */
+       if (!prefixcmp((char *)buffer.buffer, "ref: ")) {
+               *symref = xmemdupz((char *)buffer.buffer + 5, buffer.posn - 6);
+       } else {
+               get_sha1_hex(buffer.buffer, sha1);
+       }
+
+       free(buffer.buffer);
+}
+
+static int verify_merge_base(unsigned char *head_sha1, unsigned char *branch_sha1)
+{
+       struct commit *head = lookup_commit(head_sha1);
+       struct commit *branch = lookup_commit(branch_sha1);
+       struct commit_list *merge_bases = get_merge_bases(head, branch, 1);
+
+       return (merge_bases && !merge_bases->next && merge_bases->item == branch);
+}
+
+static int delete_remote_branch(char *pattern, int force)
+{
+       struct ref *refs = remote_refs;
+       struct ref *remote_ref = NULL;
+       unsigned char head_sha1[20];
+       char *symref = NULL;
+       int match;
+       int patlen = strlen(pattern);
+       int i;
+       struct active_request_slot *slot;
+       struct slot_results results;
+       char *url;
+
+       /* Find the remote branch(es) matching the specified branch name */
+       for (match = 0; refs; refs = refs->next) {
+               char *name = refs->name;
+               int namelen = strlen(name);
+               if (namelen < patlen ||
+                   memcmp(name + namelen - patlen, pattern, patlen))
+                       continue;
+               if (namelen != patlen && name[namelen - patlen - 1] != '/')
+                       continue;
+               match++;
+               remote_ref = refs;
+       }
+       if (match == 0)
+               return error("No remote branch matches %s", pattern);
+       if (match != 1)
+               return error("More than one remote branch matches %s",
+                            pattern);
+
+       /*
+        * Remote HEAD must be a symref (not exactly foolproof; a remote
+        * symlink to a symref will look like a symref)
+        */
+       fetch_symref("HEAD", &symref, head_sha1);
+       if (!symref)
+               return error("Remote HEAD is not a symref");
+
+       /* Remote branch must not be the remote HEAD */
+       for (i=0; symref && i<MAXDEPTH; i++) {
+               if (!strcmp(remote_ref->name, symref))
+                       return error("Remote branch %s is the current HEAD",
+                                    remote_ref->name);
+               fetch_symref(symref, &symref, head_sha1);
+       }
+
+       /* Run extra sanity checks if delete is not forced */
+       if (!force) {
+               /* Remote HEAD must resolve to a known object */
+               if (symref)
+                       return error("Remote HEAD symrefs too deep");
+               if (is_zero_sha1(head_sha1))
+                       return error("Unable to resolve remote HEAD");
+               if (!has_sha1_file(head_sha1))
+                       return error("Remote HEAD resolves to object %s\nwhich does not exist locally, perhaps you need to fetch?", sha1_to_hex(head_sha1));
+
+               /* Remote branch must resolve to a known object */
+               if (is_zero_sha1(remote_ref->old_sha1))
+                       return error("Unable to resolve remote branch %s",
+                                    remote_ref->name);
+               if (!has_sha1_file(remote_ref->old_sha1))
+                       return error("Remote branch %s resolves to object %s\nwhich does not exist locally, perhaps you need to fetch?", remote_ref->name, sha1_to_hex(remote_ref->old_sha1));
+
+               /* Remote branch must be an ancestor of remote HEAD */
+               if (!verify_merge_base(head_sha1, remote_ref->old_sha1)) {
+                       return error("The branch '%s' is not an ancestor "
+                                    "of your current HEAD.\n"
+                                    "If you are sure you want to delete it,"
+                                    " run:\n\t'git http-push -D %s %s'",
+                                    remote_ref->name, remote->url, pattern);
+               }
+       }
+
+       /* Send delete request */
+       fprintf(stderr, "Removing remote branch '%s'\n", remote_ref->name);
+       url = xmalloc(strlen(remote->url) + strlen(remote_ref->name) + 1);
+       sprintf(url, "%s%s", remote->url, remote_ref->name);
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_DELETE);
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               free(url);
+               if (results.curl_result != CURLE_OK)
+                       return error("DELETE request failed (%d/%ld)\n",
+                                    results.curl_result, results.http_code);
+       } else {
+               free(url);
+               return error("Unable to start DELETE request");
+       }
+
+       return 0;
+}
+
+int main(int argc, char **argv)
+{
+       struct transfer_request *request;
+       struct transfer_request *next_request;
+       int nr_refspec = 0;
+       char **refspec = NULL;
+       struct remote_lock *ref_lock = NULL;
+       struct remote_lock *info_ref_lock = NULL;
+       struct rev_info revs;
+       int delete_branch = 0;
+       int force_delete = 0;
+       int objects_to_send;
+       int rc = 0;
+       int i;
+       int new_refs;
+       struct ref *ref;
+
+       setup_git_directory();
+
+       remote = xcalloc(sizeof(*remote), 1);
+
+       argv++;
+       for (i = 1; i < argc; i++, argv++) {
+               char *arg = *argv;
+
+               if (*arg == '-') {
+                       if (!strcmp(arg, "--all")) {
+                               push_all = MATCH_REFS_ALL;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--force")) {
+                               force_all = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--dry-run")) {
+                               dry_run = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--verbose")) {
+                               push_verbosely = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "-d")) {
+                               delete_branch = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "-D")) {
+                               delete_branch = 1;
+                               force_delete = 1;
+                               continue;
+                       }
+               }
+               if (!remote->url) {
+                       char *path = strstr(arg, "//");
+                       remote->url = arg;
+                       if (path) {
+                               path = strchr(path+2, '/');
+                               if (path)
+                                       remote->path_len = strlen(path);
+                       }
+                       continue;
+               }
+               refspec = argv;
+               nr_refspec = argc - i;
+               break;
+       }
+
+       if (!remote->url)
+               usage(http_push_usage);
+
+       if (delete_branch && nr_refspec != 1)
+               die("You must specify only one branch name when deleting a remote branch");
+
+       memset(remote_dir_exists, -1, 256);
+
+       http_init();
+
+       no_pragma_header = curl_slist_append(no_pragma_header, "Pragma:");
+       default_headers = curl_slist_append(default_headers, "Range:");
+       default_headers = curl_slist_append(default_headers, "Destination:");
+       default_headers = curl_slist_append(default_headers, "If:");
+       default_headers = curl_slist_append(default_headers,
+                                           "Pragma: no-cache");
+
+       /* Verify DAV compliance/lock support */
+       if (!locking_available()) {
+               fprintf(stderr, "Error: no DAV locking support on remote repo %s\n", remote->url);
+               rc = 1;
+               goto cleanup;
+       }
+
+       /* Check whether the remote has server info files */
+       remote->can_update_info_refs = 0;
+       remote->has_info_refs = remote_exists("info/refs");
+       remote->has_info_packs = remote_exists("objects/info/packs");
+       if (remote->has_info_refs) {
+               info_ref_lock = lock_remote("info/refs", LOCK_TIME);
+               if (info_ref_lock)
+                       remote->can_update_info_refs = 1;
+       }
+       if (remote->has_info_packs)
+               fetch_indices();
+
+       /* Get a list of all local and remote heads to validate refspecs */
+       get_local_heads();
+       fprintf(stderr, "Fetching remote heads...\n");
+       get_dav_remote_heads();
+
+       /* Remove a remote branch if -d or -D was specified */
+       if (delete_branch) {
+               if (delete_remote_branch(refspec[0], force_delete) == -1)
+                       fprintf(stderr, "Unable to delete remote branch %s\n",
+                               refspec[0]);
+               goto cleanup;
+       }
+
+       /* match them up */
+       if (!remote_tail)
+               remote_tail = &remote_refs;
+       if (match_refs(local_refs, remote_refs, &remote_tail,
+                      nr_refspec, (const char **) refspec, push_all))
+               return -1;
+       if (!remote_refs) {
+               fprintf(stderr, "No refs in common and none specified; doing nothing.\n");
+               return 0;
+       }
+
+       new_refs = 0;
+       for (ref = remote_refs; ref; ref = ref->next) {
+               char old_hex[60], *new_hex;
+               const char *commit_argv[4];
+               int commit_argc;
+               char *new_sha1_hex, *old_sha1_hex;
+
+               if (!ref->peer_ref)
+                       continue;
+               if (!hashcmp(ref->old_sha1, ref->peer_ref->new_sha1)) {
+                       if (push_verbosely || 1)
+                               fprintf(stderr, "'%s': up-to-date\n", ref->name);
+                       continue;
+               }
+
+               if (!force_all &&
+                   !is_zero_sha1(ref->old_sha1) &&
+                   !ref->force) {
+                       if (!has_sha1_file(ref->old_sha1) ||
+                           !ref_newer(ref->peer_ref->new_sha1,
+                                      ref->old_sha1)) {
+                               /*
+                                * We do not have the remote ref, or
+                                * we know that the remote ref is not
+                                * an ancestor of what we are trying to
+                                * push.  Either way this can be losing
+                                * commits at the remote end and likely
+                                * we were not up to date to begin with.
+                                */
+                               error("remote '%s' is not an ancestor of\n"
+                                     "local '%s'.\n"
+                                     "Maybe you are not up-to-date and "
+                                     "need to pull first?",
+                                     ref->name,
+                                     ref->peer_ref->name);
+                               rc = -2;
+                               continue;
+                       }
+               }
+               hashcpy(ref->new_sha1, ref->peer_ref->new_sha1);
+               if (is_zero_sha1(ref->new_sha1)) {
+                       error("cannot happen anymore");
+                       rc = -3;
+                       continue;
+               }
+               new_refs++;
+               strcpy(old_hex, sha1_to_hex(ref->old_sha1));
+               new_hex = sha1_to_hex(ref->new_sha1);
+
+               fprintf(stderr, "updating '%s'", ref->name);
+               if (strcmp(ref->name, ref->peer_ref->name))
+                       fprintf(stderr, " using '%s'", ref->peer_ref->name);
+               fprintf(stderr, "\n  from %s\n  to   %s\n", old_hex, new_hex);
+               if (dry_run)
+                       continue;
+
+               /* Lock remote branch ref */
+               ref_lock = lock_remote(ref->name, LOCK_TIME);
+               if (ref_lock == NULL) {
+                       fprintf(stderr, "Unable to lock remote branch %s\n",
+                               ref->name);
+                       rc = 1;
+                       continue;
+               }
+
+               /* Set up revision info for this refspec */
+               commit_argc = 3;
+               new_sha1_hex = xstrdup(sha1_to_hex(ref->new_sha1));
+               old_sha1_hex = NULL;
+               commit_argv[1] = "--objects";
+               commit_argv[2] = new_sha1_hex;
+               if (!push_all && !is_zero_sha1(ref->old_sha1)) {
+                       old_sha1_hex = xmalloc(42);
+                       sprintf(old_sha1_hex, "^%s",
+                               sha1_to_hex(ref->old_sha1));
+                       commit_argv[3] = old_sha1_hex;
+                       commit_argc++;
+               }
+               init_revisions(&revs, setup_git_directory());
+               setup_revisions(commit_argc, commit_argv, &revs, NULL);
+               free(new_sha1_hex);
+               if (old_sha1_hex) {
+                       free(old_sha1_hex);
+                       commit_argv[1] = NULL;
+               }
+
+               /* Generate a list of objects that need to be pushed */
+               pushing = 0;
+               prepare_revision_walk(&revs);
+               mark_edges_uninteresting(revs.commits);
+               objects_to_send = get_delta(&revs, ref_lock);
+               finish_all_active_slots();
+
+               /* Push missing objects to remote, this would be a
+                  convenient time to pack them first if appropriate. */
+               pushing = 1;
+               if (objects_to_send)
+                       fprintf(stderr, "    sending %d objects\n",
+                               objects_to_send);
+#ifdef USE_CURL_MULTI
+               fill_active_slots();
+               add_fill_function(NULL, fill_active_slot);
+#endif
+               finish_all_active_slots();
+
+               /* Update the remote branch if all went well */
+               if (aborted || !update_remote(ref->new_sha1, ref_lock)) {
+                       rc = 1;
+                       goto unlock;
+               }
+
+       unlock:
+               if (!rc)
+                       fprintf(stderr, "    done\n");
+               unlock_remote(ref_lock);
+               check_locks();
+       }
+
+       /* Update remote server info if appropriate */
+       if (remote->has_info_refs && new_refs) {
+               if (info_ref_lock && remote->can_update_info_refs) {
+                       fprintf(stderr, "Updating remote server info\n");
+                       if (!dry_run)
+                               update_remote_info_refs(info_ref_lock);
+               } else {
+                       fprintf(stderr, "Unable to update server info\n");
+               }
+       }
+       if (info_ref_lock)
+               unlock_remote(info_ref_lock);
+
+ cleanup:
+       free(remote);
+
+       curl_slist_free_all(no_pragma_header);
+       curl_slist_free_all(default_headers);
+
+       http_cleanup();
+
+       request = request_queue_head;
+       while (request != NULL) {
+               next_request = request->next;
+               release_request(request);
+               request = next_request;
+       }
+
+       return rc;
+}
diff --git a/http-walker.c b/http-walker.c
new file mode 100644 (file)
index 0000000..a3fb596
--- /dev/null
@@ -0,0 +1,1035 @@
+#include "cache.h"
+#include "commit.h"
+#include "pack.h"
+#include "walker.h"
+#include "http.h"
+
+#define PREV_BUF_SIZE 4096
+#define RANGE_HEADER_SIZE 30
+
+struct alt_base
+{
+       char *base;
+       int got_indices;
+       struct packed_git *packs;
+       struct alt_base *next;
+};
+
+enum object_request_state {
+       WAITING,
+       ABORTED,
+       ACTIVE,
+       COMPLETE,
+};
+
+struct object_request
+{
+       struct walker *walker;
+       unsigned char sha1[20];
+       struct alt_base *repo;
+       char *url;
+       char filename[PATH_MAX];
+       char tmpfile[PATH_MAX];
+       int local;
+       enum object_request_state state;
+       CURLcode curl_result;
+       char errorstr[CURL_ERROR_SIZE];
+       long http_code;
+       unsigned char real_sha1[20];
+       SHA_CTX c;
+       z_stream stream;
+       int zret;
+       int rename;
+       struct active_request_slot *slot;
+       struct object_request *next;
+};
+
+struct alternates_request {
+       struct walker *walker;
+       const char *base;
+       char *url;
+       struct buffer *buffer;
+       struct active_request_slot *slot;
+       int http_specific;
+};
+
+struct walker_data {
+       const char *url;
+       int got_alternates;
+       struct alt_base *alt;
+       struct curl_slist *no_pragma_header;
+};
+
+static struct object_request *object_queue_head;
+
+static size_t fwrite_sha1_file(void *ptr, size_t eltsize, size_t nmemb,
+                              void *data)
+{
+       unsigned char expn[4096];
+       size_t size = eltsize * nmemb;
+       int posn = 0;
+       struct object_request *obj_req = (struct object_request *)data;
+       do {
+               ssize_t retval = xwrite(obj_req->local,
+                                      (char *) ptr + posn, size - posn);
+               if (retval < 0)
+                       return posn;
+               posn += retval;
+       } while (posn < size);
+
+       obj_req->stream.avail_in = size;
+       obj_req->stream.next_in = ptr;
+       do {
+               obj_req->stream.next_out = expn;
+               obj_req->stream.avail_out = sizeof(expn);
+               obj_req->zret = inflate(&obj_req->stream, Z_SYNC_FLUSH);
+               SHA1_Update(&obj_req->c, expn,
+                           sizeof(expn) - obj_req->stream.avail_out);
+       } while (obj_req->stream.avail_in && obj_req->zret == Z_OK);
+       data_received++;
+       return size;
+}
+
+static int missing__target(int code, int result)
+{
+       return  /* file:// URL -- do we ever use one??? */
+               (result == CURLE_FILE_COULDNT_READ_FILE) ||
+               /* http:// and https:// URL */
+               (code == 404 && result == CURLE_HTTP_RETURNED_ERROR) ||
+               /* ftp:// URL */
+               (code == 550 && result == CURLE_FTP_COULDNT_RETR_FILE)
+               ;
+}
+
+#define missing_target(a) missing__target((a)->http_code, (a)->curl_result)
+
+static void fetch_alternates(struct walker *walker, const char *base);
+
+static void process_object_response(void *callback_data);
+
+static void start_object_request(struct walker *walker,
+                                struct object_request *obj_req)
+{
+       char *hex = sha1_to_hex(obj_req->sha1);
+       char prevfile[PATH_MAX];
+       char *url;
+       char *posn;
+       int prevlocal;
+       unsigned char prev_buf[PREV_BUF_SIZE];
+       ssize_t prev_read = 0;
+       long prev_posn = 0;
+       char range[RANGE_HEADER_SIZE];
+       struct curl_slist *range_header = NULL;
+       struct active_request_slot *slot;
+       struct walker_data *data = walker->data;
+
+       snprintf(prevfile, sizeof(prevfile), "%s.prev", obj_req->filename);
+       unlink(prevfile);
+       rename(obj_req->tmpfile, prevfile);
+       unlink(obj_req->tmpfile);
+
+       if (obj_req->local != -1)
+               error("fd leakage in start: %d", obj_req->local);
+       obj_req->local = open(obj_req->tmpfile,
+                             O_WRONLY | O_CREAT | O_EXCL, 0666);
+       /* This could have failed due to the "lazy directory creation";
+        * try to mkdir the last path component.
+        */
+       if (obj_req->local < 0 && errno == ENOENT) {
+               char *dir = strrchr(obj_req->tmpfile, '/');
+               if (dir) {
+                       *dir = 0;
+                       mkdir(obj_req->tmpfile, 0777);
+                       *dir = '/';
+               }
+               obj_req->local = open(obj_req->tmpfile,
+                                     O_WRONLY | O_CREAT | O_EXCL, 0666);
+       }
+
+       if (obj_req->local < 0) {
+               obj_req->state = ABORTED;
+               error("Couldn't create temporary file %s for %s: %s",
+                     obj_req->tmpfile, obj_req->filename, strerror(errno));
+               return;
+       }
+
+       memset(&obj_req->stream, 0, sizeof(obj_req->stream));
+
+       inflateInit(&obj_req->stream);
+
+       SHA1_Init(&obj_req->c);
+
+       url = xmalloc(strlen(obj_req->repo->base) + 51);
+       obj_req->url = xmalloc(strlen(obj_req->repo->base) + 51);
+       strcpy(url, obj_req->repo->base);
+       posn = url + strlen(obj_req->repo->base);
+       strcpy(posn, "/objects/");
+       posn += 9;
+       memcpy(posn, hex, 2);
+       posn += 2;
+       *(posn++) = '/';
+       strcpy(posn, hex + 2);
+       strcpy(obj_req->url, url);
+
+       /* If a previous temp file is present, process what was already
+          fetched. */
+       prevlocal = open(prevfile, O_RDONLY);
+       if (prevlocal != -1) {
+               do {
+                       prev_read = xread(prevlocal, prev_buf, PREV_BUF_SIZE);
+                       if (prev_read>0) {
+                               if (fwrite_sha1_file(prev_buf,
+                                                    1,
+                                                    prev_read,
+                                                    obj_req) == prev_read) {
+                                       prev_posn += prev_read;
+                               } else {
+                                       prev_read = -1;
+                               }
+                       }
+               } while (prev_read > 0);
+               close(prevlocal);
+       }
+       unlink(prevfile);
+
+       /* Reset inflate/SHA1 if there was an error reading the previous temp
+          file; also rewind to the beginning of the local file. */
+       if (prev_read == -1) {
+               memset(&obj_req->stream, 0, sizeof(obj_req->stream));
+               inflateInit(&obj_req->stream);
+               SHA1_Init(&obj_req->c);
+               if (prev_posn>0) {
+                       prev_posn = 0;
+                       lseek(obj_req->local, 0, SEEK_SET);
+                       ftruncate(obj_req->local, 0);
+               }
+       }
+
+       slot = get_active_slot();
+       slot->callback_func = process_object_response;
+       slot->callback_data = obj_req;
+       obj_req->slot = slot;
+
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, obj_req);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_sha1_file);
+       curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, obj_req->errorstr);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, data->no_pragma_header);
+
+       /* If we have successfully processed data from a previous fetch
+          attempt, only fetch the data we don't already have. */
+       if (prev_posn>0) {
+               if (walker->get_verbosely)
+                       fprintf(stderr,
+                               "Resuming fetch of object %s at byte %ld\n",
+                               hex, prev_posn);
+               sprintf(range, "Range: bytes=%ld-", prev_posn);
+               range_header = curl_slist_append(range_header, range);
+               curl_easy_setopt(slot->curl,
+                                CURLOPT_HTTPHEADER, range_header);
+       }
+
+       /* Try to get the request started, abort the request on error */
+       obj_req->state = ACTIVE;
+       if (!start_active_slot(slot)) {
+               obj_req->state = ABORTED;
+               obj_req->slot = NULL;
+               close(obj_req->local); obj_req->local = -1;
+               free(obj_req->url);
+               return;
+       }
+}
+
+static void finish_object_request(struct object_request *obj_req)
+{
+       struct stat st;
+
+       fchmod(obj_req->local, 0444);
+       close(obj_req->local); obj_req->local = -1;
+
+       if (obj_req->http_code == 416) {
+               fprintf(stderr, "Warning: requested range invalid; we may already have all the data.\n");
+       } else if (obj_req->curl_result != CURLE_OK) {
+               if (stat(obj_req->tmpfile, &st) == 0)
+                       if (st.st_size == 0)
+                               unlink(obj_req->tmpfile);
+               return;
+       }
+
+       inflateEnd(&obj_req->stream);
+       SHA1_Final(obj_req->real_sha1, &obj_req->c);
+       if (obj_req->zret != Z_STREAM_END) {
+               unlink(obj_req->tmpfile);
+               return;
+       }
+       if (hashcmp(obj_req->sha1, obj_req->real_sha1)) {
+               unlink(obj_req->tmpfile);
+               return;
+       }
+       obj_req->rename =
+               move_temp_to_file(obj_req->tmpfile, obj_req->filename);
+
+       if (obj_req->rename == 0)
+               walker_say(obj_req->walker, "got %s\n", sha1_to_hex(obj_req->sha1));
+}
+
+static void process_object_response(void *callback_data)
+{
+       struct object_request *obj_req =
+               (struct object_request *)callback_data;
+       struct walker *walker = obj_req->walker;
+       struct walker_data *data = walker->data;
+       struct alt_base *alt = data->alt;
+
+       obj_req->curl_result = obj_req->slot->curl_result;
+       obj_req->http_code = obj_req->slot->http_code;
+       obj_req->slot = NULL;
+       obj_req->state = COMPLETE;
+
+       /* Use alternates if necessary */
+       if (missing_target(obj_req)) {
+               fetch_alternates(walker, alt->base);
+               if (obj_req->repo->next != NULL) {
+                       obj_req->repo =
+                               obj_req->repo->next;
+                       close(obj_req->local);
+                       obj_req->local = -1;
+                       start_object_request(walker, obj_req);
+                       return;
+               }
+       }
+
+       finish_object_request(obj_req);
+}
+
+static void release_object_request(struct object_request *obj_req)
+{
+       struct object_request *entry = object_queue_head;
+
+       if (obj_req->local != -1)
+               error("fd leakage in release: %d", obj_req->local);
+       if (obj_req == object_queue_head) {
+               object_queue_head = obj_req->next;
+       } else {
+               while (entry->next != NULL && entry->next != obj_req)
+                       entry = entry->next;
+               if (entry->next == obj_req)
+                       entry->next = entry->next->next;
+       }
+
+       free(obj_req->url);
+       free(obj_req);
+}
+
+#ifdef USE_CURL_MULTI
+static int fill_active_slot(struct walker *walker)
+{
+       struct object_request *obj_req;
+
+       for (obj_req = object_queue_head; obj_req; obj_req = obj_req->next) {
+               if (obj_req->state == WAITING) {
+                       if (has_sha1_file(obj_req->sha1))
+                               obj_req->state = COMPLETE;
+                       else {
+                               start_object_request(walker, obj_req);
+                               return 1;
+                       }
+               }
+       }
+       return 0;
+}
+#endif
+
+static void prefetch(struct walker *walker, unsigned char *sha1)
+{
+       struct object_request *newreq;
+       struct object_request *tail;
+       struct walker_data *data = walker->data;
+       char *filename = sha1_file_name(sha1);
+
+       newreq = xmalloc(sizeof(*newreq));
+       newreq->walker = walker;
+       hashcpy(newreq->sha1, sha1);
+       newreq->repo = data->alt;
+       newreq->url = NULL;
+       newreq->local = -1;
+       newreq->state = WAITING;
+       snprintf(newreq->filename, sizeof(newreq->filename), "%s", filename);
+       snprintf(newreq->tmpfile, sizeof(newreq->tmpfile),
+                "%s.temp", filename);
+       newreq->slot = NULL;
+       newreq->next = NULL;
+
+       if (object_queue_head == NULL) {
+               object_queue_head = newreq;
+       } else {
+               tail = object_queue_head;
+               while (tail->next != NULL) {
+                       tail = tail->next;
+               }
+               tail->next = newreq;
+       }
+
+#ifdef USE_CURL_MULTI
+       fill_active_slots();
+       step_active_slots();
+#endif
+}
+
+static int fetch_index(struct walker *walker, struct alt_base *repo, unsigned char *sha1)
+{
+       char *hex = sha1_to_hex(sha1);
+       char *filename;
+       char *url;
+       char tmpfile[PATH_MAX];
+       long prev_posn = 0;
+       char range[RANGE_HEADER_SIZE];
+       struct curl_slist *range_header = NULL;
+       struct walker_data *data = walker->data;
+
+       FILE *indexfile;
+       struct active_request_slot *slot;
+       struct slot_results results;
+
+       if (has_pack_index(sha1))
+               return 0;
+
+       if (walker->get_verbosely)
+               fprintf(stderr, "Getting index for pack %s\n", hex);
+
+       url = xmalloc(strlen(repo->base) + 64);
+       sprintf(url, "%s/objects/pack/pack-%s.idx", repo->base, hex);
+
+       filename = sha1_pack_index_name(sha1);
+       snprintf(tmpfile, sizeof(tmpfile), "%s.temp", filename);
+       indexfile = fopen(tmpfile, "a");
+       if (!indexfile)
+               return error("Unable to open local file %s for pack index",
+                            tmpfile);
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, indexfile);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, data->no_pragma_header);
+       slot->local = indexfile;
+
+       /* If there is data present from a previous transfer attempt,
+          resume where it left off */
+       prev_posn = ftell(indexfile);
+       if (prev_posn>0) {
+               if (walker->get_verbosely)
+                       fprintf(stderr,
+                               "Resuming fetch of index for pack %s at byte %ld\n",
+                               hex, prev_posn);
+               sprintf(range, "Range: bytes=%ld-", prev_posn);
+               range_header = curl_slist_append(range_header, range);
+               curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, range_header);
+       }
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK) {
+                       fclose(indexfile);
+                       return error("Unable to get pack index %s\n%s", url,
+                                    curl_errorstr);
+               }
+       } else {
+               fclose(indexfile);
+               return error("Unable to start request");
+       }
+
+       fclose(indexfile);
+
+       return move_temp_to_file(tmpfile, filename);
+}
+
+static int setup_index(struct walker *walker, struct alt_base *repo, unsigned char *sha1)
+{
+       struct packed_git *new_pack;
+       if (has_pack_file(sha1))
+               return 0; /* don't list this as something we can get */
+
+       if (fetch_index(walker, repo, sha1))
+               return -1;
+
+       new_pack = parse_pack_index(sha1);
+       new_pack->next = repo->packs;
+       repo->packs = new_pack;
+       return 0;
+}
+
+static void process_alternates_response(void *callback_data)
+{
+       struct alternates_request *alt_req =
+               (struct alternates_request *)callback_data;
+       struct walker *walker = alt_req->walker;
+       struct walker_data *cdata = walker->data;
+       struct active_request_slot *slot = alt_req->slot;
+       struct alt_base *tail = cdata->alt;
+       const char *base = alt_req->base;
+       static const char null_byte = '\0';
+       char *data;
+       int i = 0;
+
+       if (alt_req->http_specific) {
+               if (slot->curl_result != CURLE_OK ||
+                   !alt_req->buffer->posn) {
+
+                       /* Try reusing the slot to get non-http alternates */
+                       alt_req->http_specific = 0;
+                       sprintf(alt_req->url, "%s/objects/info/alternates",
+                               base);
+                       curl_easy_setopt(slot->curl, CURLOPT_URL,
+                                        alt_req->url);
+                       active_requests++;
+                       slot->in_use = 1;
+                       if (slot->finished != NULL)
+                               (*slot->finished) = 0;
+                       if (!start_active_slot(slot)) {
+                               cdata->got_alternates = -1;
+                               slot->in_use = 0;
+                               if (slot->finished != NULL)
+                                       (*slot->finished) = 1;
+                       }
+                       return;
+               }
+       } else if (slot->curl_result != CURLE_OK) {
+               if (!missing_target(slot)) {
+                       cdata->got_alternates = -1;
+                       return;
+               }
+       }
+
+       fwrite_buffer(&null_byte, 1, 1, alt_req->buffer);
+       alt_req->buffer->posn--;
+       data = alt_req->buffer->buffer;
+
+       while (i < alt_req->buffer->posn) {
+               int posn = i;
+               while (posn < alt_req->buffer->posn && data[posn] != '\n')
+                       posn++;
+               if (data[posn] == '\n') {
+                       int okay = 0;
+                       int serverlen = 0;
+                       struct alt_base *newalt;
+                       char *target = NULL;
+                       if (data[i] == '/') {
+                               /* This counts
+                                * http://git.host/pub/scm/linux.git/
+                                * -----------here^
+                                * so memcpy(dst, base, serverlen) will
+                                * copy up to "...git.host".
+                                */
+                               const char *colon_ss = strstr(base,"://");
+                               if (colon_ss) {
+                                       serverlen = (strchr(colon_ss + 3, '/')
+                                                    - base);
+                                       okay = 1;
+                               }
+                       } else if (!memcmp(data + i, "../", 3)) {
+                               /* Relative URL; chop the corresponding
+                                * number of subpath from base (and ../
+                                * from data), and concatenate the result.
+                                *
+                                * The code first drops ../ from data, and
+                                * then drops one ../ from data and one path
+                                * from base.  IOW, one extra ../ is dropped
+                                * from data than path is dropped from base.
+                                *
+                                * This is not wrong.  The alternate in
+                                *     http://git.host/pub/scm/linux.git/
+                                * to borrow from
+                                *     http://git.host/pub/scm/linus.git/
+                                * is ../../linus.git/objects/.  You need
+                                * two ../../ to borrow from your direct
+                                * neighbour.
+                                */
+                               i += 3;
+                               serverlen = strlen(base);
+                               while (i + 2 < posn &&
+                                      !memcmp(data + i, "../", 3)) {
+                                       do {
+                                               serverlen--;
+                                       } while (serverlen &&
+                                                base[serverlen - 1] != '/');
+                                       i += 3;
+                               }
+                               /* If the server got removed, give up. */
+                               okay = strchr(base, ':') - base + 3 <
+                                       serverlen;
+                       } else if (alt_req->http_specific) {
+                               char *colon = strchr(data + i, ':');
+                               char *slash = strchr(data + i, '/');
+                               if (colon && slash && colon < data + posn &&
+                                   slash < data + posn && colon < slash) {
+                                       okay = 1;
+                               }
+                       }
+                       /* skip "objects\n" at end */
+                       if (okay) {
+                               target = xmalloc(serverlen + posn - i - 6);
+                               memcpy(target, base, serverlen);
+                               memcpy(target + serverlen, data + i,
+                                      posn - i - 7);
+                               target[serverlen + posn - i - 7] = 0;
+                               if (walker->get_verbosely)
+                                       fprintf(stderr,
+                                               "Also look at %s\n", target);
+                               newalt = xmalloc(sizeof(*newalt));
+                               newalt->next = NULL;
+                               newalt->base = target;
+                               newalt->got_indices = 0;
+                               newalt->packs = NULL;
+
+                               while (tail->next != NULL)
+                                       tail = tail->next;
+                               tail->next = newalt;
+                       }
+               }
+               i = posn + 1;
+       }
+
+       cdata->got_alternates = 1;
+}
+
+static void fetch_alternates(struct walker *walker, const char *base)
+{
+       struct buffer buffer;
+       char *url;
+       char *data;
+       struct active_request_slot *slot;
+       struct alternates_request alt_req;
+       struct walker_data *cdata = walker->data;
+
+       /* If another request has already started fetching alternates,
+          wait for them to arrive and return to processing this request's
+          curl message */
+#ifdef USE_CURL_MULTI
+       while (cdata->got_alternates == 0) {
+               step_active_slots();
+       }
+#endif
+
+       /* Nothing to do if they've already been fetched */
+       if (cdata->got_alternates == 1)
+               return;
+
+       /* Start the fetch */
+       cdata->got_alternates = 0;
+
+       data = xmalloc(4096);
+       buffer.size = 4096;
+       buffer.posn = 0;
+       buffer.buffer = data;
+
+       if (walker->get_verbosely)
+               fprintf(stderr, "Getting alternates list for %s\n", base);
+
+       url = xmalloc(strlen(base) + 31);
+       sprintf(url, "%s/objects/info/http-alternates", base);
+
+       /* Use a callback to process the result, since another request
+          may fail and need to have alternates loaded before continuing */
+       slot = get_active_slot();
+       slot->callback_func = process_alternates_response;
+       alt_req.walker = walker;
+       slot->callback_data = &alt_req;
+
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+
+       alt_req.base = base;
+       alt_req.url = url;
+       alt_req.buffer = &buffer;
+       alt_req.http_specific = 1;
+       alt_req.slot = slot;
+
+       if (start_active_slot(slot))
+               run_active_slot(slot);
+       else
+               cdata->got_alternates = -1;
+
+       free(data);
+       free(url);
+}
+
+static int fetch_indices(struct walker *walker, struct alt_base *repo)
+{
+       unsigned char sha1[20];
+       char *url;
+       struct buffer buffer;
+       char *data;
+       int i = 0;
+
+       struct active_request_slot *slot;
+       struct slot_results results;
+
+       if (repo->got_indices)
+               return 0;
+
+       data = xmalloc(4096);
+       buffer.size = 4096;
+       buffer.posn = 0;
+       buffer.buffer = data;
+
+       if (walker->get_verbosely)
+               fprintf(stderr, "Getting pack list for %s\n", repo->base);
+
+       url = xmalloc(strlen(repo->base) + 21);
+       sprintf(url, "%s/objects/info/packs", repo->base);
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL);
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK) {
+                       if (missing_target(&results)) {
+                               repo->got_indices = 1;
+                               free(buffer.buffer);
+                               return 0;
+                       } else {
+                               repo->got_indices = 0;
+                               free(buffer.buffer);
+                               return error("%s", curl_errorstr);
+                       }
+               }
+       } else {
+               repo->got_indices = 0;
+               free(buffer.buffer);
+               return error("Unable to start request");
+       }
+
+       data = buffer.buffer;
+       while (i < buffer.posn) {
+               switch (data[i]) {
+               case 'P':
+                       i++;
+                       if (i + 52 <= buffer.posn &&
+                           !prefixcmp(data + i, " pack-") &&
+                           !prefixcmp(data + i + 46, ".pack\n")) {
+                               get_sha1_hex(data + i + 6, sha1);
+                               setup_index(walker, repo, sha1);
+                               i += 51;
+                               break;
+                       }
+               default:
+                       while (i < buffer.posn && data[i] != '\n')
+                               i++;
+               }
+               i++;
+       }
+
+       free(buffer.buffer);
+       repo->got_indices = 1;
+       return 0;
+}
+
+static int fetch_pack(struct walker *walker, struct alt_base *repo, unsigned char *sha1)
+{
+       char *url;
+       struct packed_git *target;
+       struct packed_git **lst;
+       FILE *packfile;
+       char *filename;
+       char tmpfile[PATH_MAX];
+       int ret;
+       long prev_posn = 0;
+       char range[RANGE_HEADER_SIZE];
+       struct curl_slist *range_header = NULL;
+       struct walker_data *data = walker->data;
+
+       struct active_request_slot *slot;
+       struct slot_results results;
+
+       if (fetch_indices(walker, repo))
+               return -1;
+       target = find_sha1_pack(sha1, repo->packs);
+       if (!target)
+               return -1;
+
+       if (walker->get_verbosely) {
+               fprintf(stderr, "Getting pack %s\n",
+                       sha1_to_hex(target->sha1));
+               fprintf(stderr, " which contains %s\n",
+                       sha1_to_hex(sha1));
+       }
+
+       url = xmalloc(strlen(repo->base) + 65);
+       sprintf(url, "%s/objects/pack/pack-%s.pack",
+               repo->base, sha1_to_hex(target->sha1));
+
+       filename = sha1_pack_name(target->sha1);
+       snprintf(tmpfile, sizeof(tmpfile), "%s.temp", filename);
+       packfile = fopen(tmpfile, "a");
+       if (!packfile)
+               return error("Unable to open local file %s for pack",
+                            tmpfile);
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, packfile);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, data->no_pragma_header);
+       slot->local = packfile;
+
+       /* If there is data present from a previous transfer attempt,
+          resume where it left off */
+       prev_posn = ftell(packfile);
+       if (prev_posn>0) {
+               if (walker->get_verbosely)
+                       fprintf(stderr,
+                               "Resuming fetch of pack %s at byte %ld\n",
+                               sha1_to_hex(target->sha1), prev_posn);
+               sprintf(range, "Range: bytes=%ld-", prev_posn);
+               range_header = curl_slist_append(range_header, range);
+               curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, range_header);
+       }
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK) {
+                       fclose(packfile);
+                       return error("Unable to get pack file %s\n%s", url,
+                                    curl_errorstr);
+               }
+       } else {
+               fclose(packfile);
+               return error("Unable to start request");
+       }
+
+       target->pack_size = ftell(packfile);
+       fclose(packfile);
+
+       ret = move_temp_to_file(tmpfile, filename);
+       if (ret)
+               return ret;
+
+       lst = &repo->packs;
+       while (*lst != target)
+               lst = &((*lst)->next);
+       *lst = (*lst)->next;
+
+       if (verify_pack(target, 0))
+               return -1;
+       install_packed_git(target);
+
+       return 0;
+}
+
+static void abort_object_request(struct object_request *obj_req)
+{
+       if (obj_req->local >= 0) {
+               close(obj_req->local);
+               obj_req->local = -1;
+       }
+       unlink(obj_req->tmpfile);
+       if (obj_req->slot) {
+               release_active_slot(obj_req->slot);
+               obj_req->slot = NULL;
+       }
+       release_object_request(obj_req);
+}
+
+static int fetch_object(struct walker *walker, struct alt_base *repo, unsigned char *sha1)
+{
+       char *hex = sha1_to_hex(sha1);
+       int ret = 0;
+       struct object_request *obj_req = object_queue_head;
+
+       while (obj_req != NULL && hashcmp(obj_req->sha1, sha1))
+               obj_req = obj_req->next;
+       if (obj_req == NULL)
+               return error("Couldn't find request for %s in the queue", hex);
+
+       if (has_sha1_file(obj_req->sha1)) {
+               abort_object_request(obj_req);
+               return 0;
+       }
+
+#ifdef USE_CURL_MULTI
+       while (obj_req->state == WAITING) {
+               step_active_slots();
+       }
+#else
+       start_object_request(walker, obj_req);
+#endif
+
+       while (obj_req->state == ACTIVE) {
+               run_active_slot(obj_req->slot);
+       }
+       if (obj_req->local != -1) {
+               close(obj_req->local); obj_req->local = -1;
+       }
+
+       if (obj_req->state == ABORTED) {
+               ret = error("Request for %s aborted", hex);
+       } else if (obj_req->curl_result != CURLE_OK &&
+                  obj_req->http_code != 416) {
+               if (missing_target(obj_req))
+                       ret = -1; /* Be silent, it is probably in a pack. */
+               else
+                       ret = error("%s (curl_result = %d, http_code = %ld, sha1 = %s)",
+                                   obj_req->errorstr, obj_req->curl_result,
+                                   obj_req->http_code, hex);
+       } else if (obj_req->zret != Z_STREAM_END) {
+               walker->corrupt_object_found++;
+               ret = error("File %s (%s) corrupt", hex, obj_req->url);
+       } else if (hashcmp(obj_req->sha1, obj_req->real_sha1)) {
+               ret = error("File %s has bad hash", hex);
+       } else if (obj_req->rename < 0) {
+               ret = error("unable to write sha1 filename %s",
+                           obj_req->filename);
+       }
+
+       release_object_request(obj_req);
+       return ret;
+}
+
+static int fetch(struct walker *walker, unsigned char *sha1)
+{
+       struct walker_data *data = walker->data;
+       struct alt_base *altbase = data->alt;
+
+       if (!fetch_object(walker, altbase, sha1))
+               return 0;
+       while (altbase) {
+               if (!fetch_pack(walker, altbase, sha1))
+                       return 0;
+               fetch_alternates(walker, data->alt->base);
+               altbase = altbase->next;
+       }
+       return error("Unable to find %s under %s", sha1_to_hex(sha1),
+                    data->alt->base);
+}
+
+static inline int needs_quote(int ch)
+{
+       if (((ch >= 'A') && (ch <= 'Z'))
+                       || ((ch >= 'a') && (ch <= 'z'))
+                       || ((ch >= '0') && (ch <= '9'))
+                       || (ch == '/')
+                       || (ch == '-')
+                       || (ch == '.'))
+               return 0;
+       return 1;
+}
+
+static inline int hex(int v)
+{
+       if (v < 10) return '0' + v;
+       else return 'A' + v - 10;
+}
+
+static char *quote_ref_url(const char *base, const char *ref)
+{
+       const char *cp;
+       char *dp, *qref;
+       int len, baselen, ch;
+
+       baselen = strlen(base);
+       len = baselen + 7; /* "/refs/" + NUL */
+       for (cp = ref; (ch = *cp) != 0; cp++, len++)
+               if (needs_quote(ch))
+                       len += 2; /* extra two hex plus replacement % */
+       qref = xmalloc(len);
+       memcpy(qref, base, baselen);
+       memcpy(qref + baselen, "/refs/", 6);
+       for (cp = ref, dp = qref + baselen + 6; (ch = *cp) != 0; cp++) {
+               if (needs_quote(ch)) {
+                       *dp++ = '%';
+                       *dp++ = hex((ch >> 4) & 0xF);
+                       *dp++ = hex(ch & 0xF);
+               }
+               else
+                       *dp++ = ch;
+       }
+       *dp = 0;
+
+       return qref;
+}
+
+static int fetch_ref(struct walker *walker, char *ref, unsigned char *sha1)
+{
+        char *url;
+        char hex[42];
+        struct buffer buffer;
+       struct walker_data *data = walker->data;
+       const char *base = data->alt->base;
+       struct active_request_slot *slot;
+       struct slot_results results;
+        buffer.size = 41;
+        buffer.posn = 0;
+        buffer.buffer = hex;
+        hex[41] = '\0';
+
+       url = quote_ref_url(base, ref);
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, url);
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK)
+                       return error("Couldn't get %s for %s\n%s",
+                                    url, ref, curl_errorstr);
+       } else {
+               return error("Unable to start request");
+       }
+
+        hex[40] = '\0';
+        get_sha1_hex(hex, sha1);
+        return 0;
+}
+
+static void cleanup(struct walker *walker)
+{
+       struct walker_data *data = walker->data;
+       http_cleanup();
+
+       curl_slist_free_all(data->no_pragma_header);
+}
+
+struct walker *get_http_walker(const char *url)
+{
+       char *s;
+       struct walker_data *data = xmalloc(sizeof(struct walker_data));
+       struct walker *walker = xmalloc(sizeof(struct walker));
+
+       http_init();
+
+       data->no_pragma_header = curl_slist_append(NULL, "Pragma:");
+
+       data->alt = xmalloc(sizeof(*data->alt));
+       data->alt->base = xmalloc(strlen(url) + 1);
+       strcpy(data->alt->base, url);
+       for (s = data->alt->base + strlen(data->alt->base) - 1; *s == '/'; --s)
+               *s = 0;
+
+       data->alt->got_indices = 0;
+       data->alt->packs = NULL;
+       data->alt->next = NULL;
+       data->got_alternates = -1;
+
+       walker->corrupt_object_found = 0;
+       walker->fetch = fetch;
+       walker->fetch_ref = fetch_ref;
+       walker->prefetch = prefetch;
+       walker->cleanup = cleanup;
+       walker->data = data;
+
+#ifdef USE_CURL_MULTI
+       add_fill_function(walker, (int (*)(void *)) fill_active_slot);
+#endif
+
+       return walker;
+}
diff --git a/http.c b/http.c
new file mode 100644 (file)
index 0000000..e4aa9c1
--- /dev/null
+++ b/http.c
@@ -0,0 +1,561 @@
+#include "http.h"
+
+int data_received;
+int active_requests = 0;
+
+#ifdef USE_CURL_MULTI
+int max_requests = -1;
+CURLM *curlm;
+#endif
+#ifndef NO_CURL_EASY_DUPHANDLE
+CURL *curl_default;
+#endif
+char curl_errorstr[CURL_ERROR_SIZE];
+
+int curl_ssl_verify = -1;
+char *ssl_cert = NULL;
+#if LIBCURL_VERSION_NUM >= 0x070902
+char *ssl_key = NULL;
+#endif
+#if LIBCURL_VERSION_NUM >= 0x070908
+char *ssl_capath = NULL;
+#endif
+char *ssl_cainfo = NULL;
+long curl_low_speed_limit = -1;
+long curl_low_speed_time = -1;
+int curl_ftp_no_epsv = 0;
+char *curl_http_proxy = NULL;
+
+struct curl_slist *pragma_header;
+
+struct active_request_slot *active_queue_head = NULL;
+
+size_t fread_buffer(void *ptr, size_t eltsize, size_t nmemb,
+                          struct buffer *buffer)
+{
+       size_t size = eltsize * nmemb;
+       if (size > buffer->size - buffer->posn)
+               size = buffer->size - buffer->posn;
+       memcpy(ptr, (char *) buffer->buffer + buffer->posn, size);
+       buffer->posn += size;
+       return size;
+}
+
+size_t fwrite_buffer(const void *ptr, size_t eltsize,
+                           size_t nmemb, struct buffer *buffer)
+{
+       size_t size = eltsize * nmemb;
+       if (size > buffer->size - buffer->posn) {
+               buffer->size = buffer->size * 3 / 2;
+               if (buffer->size < buffer->posn + size)
+                       buffer->size = buffer->posn + size;
+               buffer->buffer = xrealloc(buffer->buffer, buffer->size);
+       }
+       memcpy((char *) buffer->buffer + buffer->posn, ptr, size);
+       buffer->posn += size;
+       data_received++;
+       return size;
+}
+
+size_t fwrite_null(const void *ptr, size_t eltsize,
+                         size_t nmemb, struct buffer *buffer)
+{
+       data_received++;
+       return eltsize * nmemb;
+}
+
+static void finish_active_slot(struct active_request_slot *slot);
+
+#ifdef USE_CURL_MULTI
+static void process_curl_messages(void)
+{
+       int num_messages;
+       struct active_request_slot *slot;
+       CURLMsg *curl_message = curl_multi_info_read(curlm, &num_messages);
+
+       while (curl_message != NULL) {
+               if (curl_message->msg == CURLMSG_DONE) {
+                       int curl_result = curl_message->data.result;
+                       slot = active_queue_head;
+                       while (slot != NULL &&
+                              slot->curl != curl_message->easy_handle)
+                               slot = slot->next;
+                       if (slot != NULL) {
+                               curl_multi_remove_handle(curlm, slot->curl);
+                               slot->curl_result = curl_result;
+                               finish_active_slot(slot);
+                       } else {
+                               fprintf(stderr, "Received DONE message for unknown request!\n");
+                       }
+               } else {
+                       fprintf(stderr, "Unknown CURL message received: %d\n",
+                               (int)curl_message->msg);
+               }
+               curl_message = curl_multi_info_read(curlm, &num_messages);
+       }
+}
+#endif
+
+static int http_options(const char *var, const char *value)
+{
+       if (!strcmp("http.sslverify", var)) {
+               if (curl_ssl_verify == -1) {
+                       curl_ssl_verify = git_config_bool(var, value);
+               }
+               return 0;
+       }
+
+       if (!strcmp("http.sslcert", var)) {
+               if (ssl_cert == NULL) {
+                       ssl_cert = xmalloc(strlen(value)+1);
+                       strcpy(ssl_cert, value);
+               }
+               return 0;
+       }
+#if LIBCURL_VERSION_NUM >= 0x070902
+       if (!strcmp("http.sslkey", var)) {
+               if (ssl_key == NULL) {
+                       ssl_key = xmalloc(strlen(value)+1);
+                       strcpy(ssl_key, value);
+               }
+               return 0;
+       }
+#endif
+#if LIBCURL_VERSION_NUM >= 0x070908
+       if (!strcmp("http.sslcapath", var)) {
+               if (ssl_capath == NULL) {
+                       ssl_capath = xmalloc(strlen(value)+1);
+                       strcpy(ssl_capath, value);
+               }
+               return 0;
+       }
+#endif
+       if (!strcmp("http.sslcainfo", var)) {
+               if (ssl_cainfo == NULL) {
+                       ssl_cainfo = xmalloc(strlen(value)+1);
+                       strcpy(ssl_cainfo, value);
+               }
+               return 0;
+       }
+
+#ifdef USE_CURL_MULTI
+       if (!strcmp("http.maxrequests", var)) {
+               if (max_requests == -1)
+                       max_requests = git_config_int(var, value);
+               return 0;
+       }
+#endif
+
+       if (!strcmp("http.lowspeedlimit", var)) {
+               if (curl_low_speed_limit == -1)
+                       curl_low_speed_limit = (long)git_config_int(var, value);
+               return 0;
+       }
+       if (!strcmp("http.lowspeedtime", var)) {
+               if (curl_low_speed_time == -1)
+                       curl_low_speed_time = (long)git_config_int(var, value);
+               return 0;
+       }
+
+       if (!strcmp("http.noepsv", var)) {
+               curl_ftp_no_epsv = git_config_bool(var, value);
+               return 0;
+       }
+       if (!strcmp("http.proxy", var)) {
+               if (curl_http_proxy == NULL) {
+                       curl_http_proxy = xmalloc(strlen(value)+1);
+                       strcpy(curl_http_proxy, value);
+               }
+               return 0;
+       }
+
+       /* Fall back on the default ones */
+       return git_default_config(var, value);
+}
+
+static CURL* get_curl_handle(void)
+{
+       CURL* result = curl_easy_init();
+
+       curl_easy_setopt(result, CURLOPT_SSL_VERIFYPEER, curl_ssl_verify);
+#if LIBCURL_VERSION_NUM >= 0x070907
+       curl_easy_setopt(result, CURLOPT_NETRC, CURL_NETRC_OPTIONAL);
+#endif
+
+       if (ssl_cert != NULL)
+               curl_easy_setopt(result, CURLOPT_SSLCERT, ssl_cert);
+#if LIBCURL_VERSION_NUM >= 0x070902
+       if (ssl_key != NULL)
+               curl_easy_setopt(result, CURLOPT_SSLKEY, ssl_key);
+#endif
+#if LIBCURL_VERSION_NUM >= 0x070908
+       if (ssl_capath != NULL)
+               curl_easy_setopt(result, CURLOPT_CAPATH, ssl_capath);
+#endif
+       if (ssl_cainfo != NULL)
+               curl_easy_setopt(result, CURLOPT_CAINFO, ssl_cainfo);
+       curl_easy_setopt(result, CURLOPT_FAILONERROR, 1);
+
+       if (curl_low_speed_limit > 0 && curl_low_speed_time > 0) {
+               curl_easy_setopt(result, CURLOPT_LOW_SPEED_LIMIT,
+                                curl_low_speed_limit);
+               curl_easy_setopt(result, CURLOPT_LOW_SPEED_TIME,
+                                curl_low_speed_time);
+       }
+
+       curl_easy_setopt(result, CURLOPT_FOLLOWLOCATION, 1);
+
+       if (getenv("GIT_CURL_VERBOSE"))
+               curl_easy_setopt(result, CURLOPT_VERBOSE, 1);
+
+       curl_easy_setopt(result, CURLOPT_USERAGENT, GIT_USER_AGENT);
+
+       if (curl_ftp_no_epsv)
+               curl_easy_setopt(result, CURLOPT_FTP_USE_EPSV, 0);
+
+       if (curl_http_proxy)
+               curl_easy_setopt(result, CURLOPT_PROXY, curl_http_proxy);
+
+       return result;
+}
+
+void http_init(void)
+{
+       char *low_speed_limit;
+       char *low_speed_time;
+
+       curl_global_init(CURL_GLOBAL_ALL);
+
+       pragma_header = curl_slist_append(pragma_header, "Pragma: no-cache");
+
+#ifdef USE_CURL_MULTI
+       {
+               char *http_max_requests = getenv("GIT_HTTP_MAX_REQUESTS");
+               if (http_max_requests != NULL)
+                       max_requests = atoi(http_max_requests);
+       }
+
+       curlm = curl_multi_init();
+       if (curlm == NULL) {
+               fprintf(stderr, "Error creating curl multi handle.\n");
+               exit(1);
+       }
+#endif
+
+       if (getenv("GIT_SSL_NO_VERIFY"))
+               curl_ssl_verify = 0;
+
+       ssl_cert = getenv("GIT_SSL_CERT");
+#if LIBCURL_VERSION_NUM >= 0x070902
+       ssl_key = getenv("GIT_SSL_KEY");
+#endif
+#if LIBCURL_VERSION_NUM >= 0x070908
+       ssl_capath = getenv("GIT_SSL_CAPATH");
+#endif
+       ssl_cainfo = getenv("GIT_SSL_CAINFO");
+
+       low_speed_limit = getenv("GIT_HTTP_LOW_SPEED_LIMIT");
+       if (low_speed_limit != NULL)
+               curl_low_speed_limit = strtol(low_speed_limit, NULL, 10);
+       low_speed_time = getenv("GIT_HTTP_LOW_SPEED_TIME");
+       if (low_speed_time != NULL)
+               curl_low_speed_time = strtol(low_speed_time, NULL, 10);
+
+       git_config(http_options);
+
+       if (curl_ssl_verify == -1)
+               curl_ssl_verify = 1;
+
+#ifdef USE_CURL_MULTI
+       if (max_requests < 1)
+               max_requests = DEFAULT_MAX_REQUESTS;
+#endif
+
+       if (getenv("GIT_CURL_FTP_NO_EPSV"))
+               curl_ftp_no_epsv = 1;
+
+#ifndef NO_CURL_EASY_DUPHANDLE
+       curl_default = get_curl_handle();
+#endif
+}
+
+void http_cleanup(void)
+{
+       struct active_request_slot *slot = active_queue_head;
+#ifdef USE_CURL_MULTI
+       char *wait_url;
+#endif
+
+       while (slot != NULL) {
+               struct active_request_slot *next = slot->next;
+#ifdef USE_CURL_MULTI
+               if (slot->in_use) {
+                       curl_easy_getinfo(slot->curl,
+                                         CURLINFO_EFFECTIVE_URL,
+                                         &wait_url);
+                       fprintf(stderr, "Waiting for %s\n", wait_url);
+                       run_active_slot(slot);
+               }
+#endif
+               if (slot->curl != NULL)
+                       curl_easy_cleanup(slot->curl);
+               free(slot);
+               slot = next;
+       }
+       active_queue_head = NULL;
+
+#ifndef NO_CURL_EASY_DUPHANDLE
+       curl_easy_cleanup(curl_default);
+#endif
+
+#ifdef USE_CURL_MULTI
+       curl_multi_cleanup(curlm);
+#endif
+       curl_global_cleanup();
+
+       curl_slist_free_all(pragma_header);
+       pragma_header = NULL;
+}
+
+struct active_request_slot *get_active_slot(void)
+{
+       struct active_request_slot *slot = active_queue_head;
+       struct active_request_slot *newslot;
+
+#ifdef USE_CURL_MULTI
+       int num_transfers;
+
+       /* Wait for a slot to open up if the queue is full */
+       while (active_requests >= max_requests) {
+               curl_multi_perform(curlm, &num_transfers);
+               if (num_transfers < active_requests) {
+                       process_curl_messages();
+               }
+       }
+#endif
+
+       while (slot != NULL && slot->in_use) {
+               slot = slot->next;
+       }
+       if (slot == NULL) {
+               newslot = xmalloc(sizeof(*newslot));
+               newslot->curl = NULL;
+               newslot->in_use = 0;
+               newslot->next = NULL;
+
+               slot = active_queue_head;
+               if (slot == NULL) {
+                       active_queue_head = newslot;
+               } else {
+                       while (slot->next != NULL) {
+                               slot = slot->next;
+                       }
+                       slot->next = newslot;
+               }
+               slot = newslot;
+       }
+
+       if (slot->curl == NULL) {
+#ifdef NO_CURL_EASY_DUPHANDLE
+               slot->curl = get_curl_handle();
+#else
+               slot->curl = curl_easy_duphandle(curl_default);
+#endif
+       }
+
+       active_requests++;
+       slot->in_use = 1;
+       slot->local = NULL;
+       slot->results = NULL;
+       slot->finished = NULL;
+       slot->callback_data = NULL;
+       slot->callback_func = NULL;
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, pragma_header);
+       curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, curl_errorstr);
+       curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, NULL);
+       curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, NULL);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, NULL);
+       curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 0);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1);
+
+       return slot;
+}
+
+int start_active_slot(struct active_request_slot *slot)
+{
+#ifdef USE_CURL_MULTI
+       CURLMcode curlm_result = curl_multi_add_handle(curlm, slot->curl);
+       int num_transfers;
+
+       if (curlm_result != CURLM_OK &&
+           curlm_result != CURLM_CALL_MULTI_PERFORM) {
+               active_requests--;
+               slot->in_use = 0;
+               return 0;
+       }
+
+       /*
+        * We know there must be something to do, since we just added
+        * something.
+        */
+       curl_multi_perform(curlm, &num_transfers);
+#endif
+       return 1;
+}
+
+#ifdef USE_CURL_MULTI
+struct fill_chain {
+       void *data;
+       int (*fill)(void *);
+       struct fill_chain *next;
+};
+
+static struct fill_chain *fill_cfg = NULL;
+
+void add_fill_function(void *data, int (*fill)(void *))
+{
+       struct fill_chain *new = malloc(sizeof(*new));
+       struct fill_chain **linkp = &fill_cfg;
+       new->data = data;
+       new->fill = fill;
+       new->next = NULL;
+       while (*linkp)
+               linkp = &(*linkp)->next;
+       *linkp = new;
+}
+
+void fill_active_slots(void)
+{
+       struct active_request_slot *slot = active_queue_head;
+
+       while (active_requests < max_requests) {
+               struct fill_chain *fill;
+               for (fill = fill_cfg; fill; fill = fill->next)
+                       if (fill->fill(fill->data))
+                               break;
+
+               if (!fill)
+                       break;
+       }
+
+       while (slot != NULL) {
+               if (!slot->in_use && slot->curl != NULL) {
+                       curl_easy_cleanup(slot->curl);
+                       slot->curl = NULL;
+               }
+               slot = slot->next;
+       }
+}
+
+void step_active_slots(void)
+{
+       int num_transfers;
+       CURLMcode curlm_result;
+
+       do {
+               curlm_result = curl_multi_perform(curlm, &num_transfers);
+       } while (curlm_result == CURLM_CALL_MULTI_PERFORM);
+       if (num_transfers < active_requests) {
+               process_curl_messages();
+               fill_active_slots();
+       }
+}
+#endif
+
+void run_active_slot(struct active_request_slot *slot)
+{
+#ifdef USE_CURL_MULTI
+       long last_pos = 0;
+       long current_pos;
+       fd_set readfds;
+       fd_set writefds;
+       fd_set excfds;
+       int max_fd;
+       struct timeval select_timeout;
+       int finished = 0;
+
+       slot->finished = &finished;
+       while (!finished) {
+               data_received = 0;
+               step_active_slots();
+
+               if (!data_received && slot->local != NULL) {
+                       current_pos = ftell(slot->local);
+                       if (current_pos > last_pos)
+                               data_received++;
+                       last_pos = current_pos;
+               }
+
+               if (slot->in_use && !data_received) {
+                       max_fd = 0;
+                       FD_ZERO(&readfds);
+                       FD_ZERO(&writefds);
+                       FD_ZERO(&excfds);
+                       select_timeout.tv_sec = 0;
+                       select_timeout.tv_usec = 50000;
+                       select(max_fd, &readfds, &writefds,
+                              &excfds, &select_timeout);
+               }
+       }
+#else
+       while (slot->in_use) {
+               slot->curl_result = curl_easy_perform(slot->curl);
+               finish_active_slot(slot);
+       }
+#endif
+}
+
+static void closedown_active_slot(struct active_request_slot *slot)
+{
+        active_requests--;
+        slot->in_use = 0;
+}
+
+void release_active_slot(struct active_request_slot *slot)
+{
+       closedown_active_slot(slot);
+       if (slot->curl) {
+#ifdef USE_CURL_MULTI
+               curl_multi_remove_handle(curlm, slot->curl);
+#endif
+               curl_easy_cleanup(slot->curl);
+               slot->curl = NULL;
+       }
+#ifdef USE_CURL_MULTI
+       fill_active_slots();
+#endif
+}
+
+static void finish_active_slot(struct active_request_slot *slot)
+{
+       closedown_active_slot(slot);
+        curl_easy_getinfo(slot->curl, CURLINFO_HTTP_CODE, &slot->http_code);
+
+       if (slot->finished != NULL)
+               (*slot->finished) = 1;
+
+       /* Store slot results so they can be read after the slot is reused */
+       if (slot->results != NULL) {
+               slot->results->curl_result = slot->curl_result;
+               slot->results->http_code = slot->http_code;
+       }
+
+        /* Run callback if appropriate */
+        if (slot->callback_func != NULL) {
+                slot->callback_func(slot->callback_data);
+        }
+}
+
+void finish_all_active_slots(void)
+{
+       struct active_request_slot *slot = active_queue_head;
+
+       while (slot != NULL)
+               if (slot->in_use) {
+                       run_active_slot(slot);
+                       slot = active_queue_head;
+               } else {
+                       slot = slot->next;
+               }
+}
diff --git a/http.h b/http.h
new file mode 100644 (file)
index 0000000..72abac2
--- /dev/null
+++ b/http.h
@@ -0,0 +1,103 @@
+#ifndef HTTP_H
+#define HTTP_H
+
+#include "cache.h"
+
+#include <curl/curl.h>
+#include <curl/easy.h>
+
+#if LIBCURL_VERSION_NUM >= 0x071000
+#define USE_CURL_MULTI
+#define DEFAULT_MAX_REQUESTS 5
+#endif
+
+#if LIBCURL_VERSION_NUM < 0x070704
+#define curl_global_cleanup() do { /* nothing */ } while(0)
+#endif
+#if LIBCURL_VERSION_NUM < 0x070800
+#define curl_global_init(a) do { /* nothing */ } while(0)
+#endif
+
+#if (LIBCURL_VERSION_NUM < 0x070c04) || (LIBCURL_VERSION_NUM == 0x071000)
+#define NO_CURL_EASY_DUPHANDLE
+#endif
+
+#if LIBCURL_VERSION_NUM < 0x070a03
+#define CURLE_HTTP_RETURNED_ERROR CURLE_HTTP_NOT_FOUND
+#endif
+
+struct slot_results
+{
+       CURLcode curl_result;
+       long http_code;
+};
+
+struct active_request_slot
+{
+       CURL *curl;
+       FILE *local;
+       int in_use;
+       CURLcode curl_result;
+       long http_code;
+       int *finished;
+       struct slot_results *results;
+       void *callback_data;
+       void (*callback_func)(void *data);
+       struct active_request_slot *next;
+};
+
+struct buffer
+{
+        size_t posn;
+        size_t size;
+        void *buffer;
+};
+
+/* Curl request read/write callbacks */
+extern size_t fread_buffer(void *ptr, size_t eltsize, size_t nmemb,
+                          struct buffer *buffer);
+extern size_t fwrite_buffer(const void *ptr, size_t eltsize,
+                           size_t nmemb, struct buffer *buffer);
+extern size_t fwrite_null(const void *ptr, size_t eltsize,
+                         size_t nmemb, struct buffer *buffer);
+
+/* Slot lifecycle functions */
+extern struct active_request_slot *get_active_slot(void);
+extern int start_active_slot(struct active_request_slot *slot);
+extern void run_active_slot(struct active_request_slot *slot);
+extern void finish_all_active_slots(void);
+extern void release_active_slot(struct active_request_slot *slot);
+
+#ifdef USE_CURL_MULTI
+extern void fill_active_slots(void);
+extern void add_fill_function(void *data, int (*fill)(void *));
+extern void step_active_slots(void);
+#endif
+
+extern void http_init(void);
+extern void http_cleanup(void);
+
+extern int data_received;
+extern int active_requests;
+
+#ifndef NO_CURL_EASY_DUPHANDLE
+extern CURL *curl_default;
+#endif
+extern char curl_errorstr[CURL_ERROR_SIZE];
+
+extern int curl_ssl_verify;
+extern char *ssl_cert;
+#if LIBCURL_VERSION_NUM >= 0x070902
+extern char *ssl_key;
+#endif
+#if LIBCURL_VERSION_NUM >= 0x070908
+extern char *ssl_capath;
+#endif
+extern char *ssl_cainfo;
+extern long curl_low_speed_limit;
+extern long curl_low_speed_time;
+
+extern struct curl_slist *pragma_header;
+extern struct curl_slist *no_range_header;
+
+#endif /* HTTP_H */
diff --git a/ident.c b/ident.c
new file mode 100644 (file)
index 0000000..7631698
--- /dev/null
+++ b/ident.c
@@ -0,0 +1,263 @@
+/*
+ * ident.c
+ *
+ * create git identifier lines of the form "name <email> date"
+ *
+ * Copyright (C) 2005 Linus Torvalds
+ */
+#include "cache.h"
+
+static char git_default_date[50];
+
+static void copy_gecos(const struct passwd *w, char *name, size_t sz)
+{
+       char *src, *dst;
+       size_t len, nlen;
+
+       nlen = strlen(w->pw_name);
+
+       /* Traditionally GECOS field had office phone numbers etc, separated
+        * with commas.  Also & stands for capitalized form of the login name.
+        */
+
+       for (len = 0, dst = name, src = w->pw_gecos; len < sz; src++) {
+               int ch = *src;
+               if (ch != '&') {
+                       *dst++ = ch;
+                       if (ch == 0 || ch == ',')
+                               break;
+                       len++;
+                       continue;
+               }
+               if (len + nlen < sz) {
+                       /* Sorry, Mr. McDonald... */
+                       *dst++ = toupper(*w->pw_name);
+                       memcpy(dst, w->pw_name + 1, nlen - 1);
+                       dst += nlen - 1;
+               }
+       }
+       if (len < sz)
+               name[len] = 0;
+       else
+               die("Your parents must have hated you!");
+
+}
+
+static void copy_email(const struct passwd *pw)
+{
+       /*
+        * Make up a fake email address
+        * (name + '@' + hostname [+ '.' + domainname])
+        */
+       size_t len = strlen(pw->pw_name);
+       if (len > sizeof(git_default_email)/2)
+               die("Your sysadmin must hate you!");
+       memcpy(git_default_email, pw->pw_name, len);
+       git_default_email[len++] = '@';
+       gethostname(git_default_email + len, sizeof(git_default_email) - len);
+       if (!strchr(git_default_email+len, '.')) {
+               struct hostent *he = gethostbyname(git_default_email + len);
+               char *domainname;
+
+               len = strlen(git_default_email);
+               git_default_email[len++] = '.';
+               if (he && (domainname = strchr(he->h_name, '.')))
+                       strlcpy(git_default_email + len, domainname + 1,
+                               sizeof(git_default_email) - len);
+               else
+                       strlcpy(git_default_email + len, "(none)",
+                               sizeof(git_default_email) - len);
+       }
+}
+
+static void setup_ident(void)
+{
+       struct passwd *pw = NULL;
+
+       /* Get the name ("gecos") */
+       if (!git_default_name[0]) {
+               pw = getpwuid(getuid());
+               if (!pw)
+                       die("You don't exist. Go away!");
+               copy_gecos(pw, git_default_name, sizeof(git_default_name));
+       }
+
+       if (!git_default_email[0]) {
+               const char *email = getenv("EMAIL");
+
+               if (email && email[0])
+                       strlcpy(git_default_email, email,
+                               sizeof(git_default_email));
+               else {
+                       if (!pw)
+                               pw = getpwuid(getuid());
+                       if (!pw)
+                               die("You don't exist. Go away!");
+                       copy_email(pw);
+               }
+       }
+
+       /* And set the default date */
+       if (!git_default_date[0])
+               datestamp(git_default_date, sizeof(git_default_date));
+}
+
+static int add_raw(char *buf, size_t size, int offset, const char *str)
+{
+       size_t len = strlen(str);
+       if (offset + len > size)
+               return size;
+       memcpy(buf + offset, str, len);
+       return offset + len;
+}
+
+static int crud(unsigned char c)
+{
+       return  c <= 32  ||
+               c == '.' ||
+               c == ',' ||
+               c == ':' ||
+               c == ';' ||
+               c == '<' ||
+               c == '>' ||
+               c == '"' ||
+               c == '\'';
+}
+
+/*
+ * Copy over a string to the destination, but avoid special
+ * characters ('\n', '<' and '>') and remove crud at the end
+ */
+static int copy(char *buf, size_t size, int offset, const char *src)
+{
+       size_t i, len;
+       unsigned char c;
+
+       /* Remove crud from the beginning.. */
+       while ((c = *src) != 0) {
+               if (!crud(c))
+                       break;
+               src++;
+       }
+
+       /* Remove crud from the end.. */
+       len = strlen(src);
+       while (len > 0) {
+               c = src[len-1];
+               if (!crud(c))
+                       break;
+               --len;
+       }
+
+       /*
+        * Copy the rest to the buffer, but avoid the special
+        * characters '\n' '<' and '>' that act as delimiters on
+        * a identification line
+        */
+       for (i = 0; i < len; i++) {
+               c = *src++;
+               switch (c) {
+               case '\n': case '<': case '>':
+                       continue;
+               }
+               if (offset >= size)
+                       return size;
+               buf[offset++] = c;
+       }
+       return offset;
+}
+
+static const char au_env[] = "GIT_AUTHOR_NAME";
+static const char co_env[] = "GIT_COMMITTER_NAME";
+static const char *env_hint =
+"\n"
+"*** Your name cannot be determined from your system services (gecos).\n"
+"\n"
+"Run\n"
+"\n"
+"  git config --global user.email \"you@example.com\"\n"
+"  git config --global user.name \"Your Name\"\n"
+"\n"
+"to set your account\'s default identity.\n"
+"Omit --global to set the identity only in this repository.\n"
+"\n";
+
+static const char *fmt_ident_1(const char *name, const char *email,
+                              const char *date_str, int flag)
+{
+       static char buffer[1000];
+       char date[50];
+       int i;
+       int error_on_no_name = !!(flag & 01);
+       int name_addr_only = !!(flag & 02);
+
+       setup_ident();
+       if (!name)
+               name = git_default_name;
+       if (!email)
+               email = git_default_email;
+
+       if (!*name) {
+               struct passwd *pw;
+
+               if (0 <= error_on_no_name &&
+                   name == git_default_name && env_hint) {
+                       fprintf(stderr, env_hint, au_env, co_env);
+                       env_hint = NULL; /* warn only once, for "git-var -l" */
+               }
+               if (0 < error_on_no_name)
+                       die("empty ident %s <%s> not allowed", name, email);
+               pw = getpwuid(getuid());
+               if (!pw)
+                       die("You don't exist. Go away!");
+               strlcpy(git_default_name, pw->pw_name,
+                       sizeof(git_default_name));
+               name = git_default_name;
+       }
+
+       strcpy(date, git_default_date);
+       if (!name_addr_only && date_str)
+               parse_date(date_str, date, sizeof(date));
+
+       i = copy(buffer, sizeof(buffer), 0, name);
+       i = add_raw(buffer, sizeof(buffer), i, " <");
+       i = copy(buffer, sizeof(buffer), i, email);
+       if (!name_addr_only) {
+               i = add_raw(buffer, sizeof(buffer), i,  "> ");
+               i = copy(buffer, sizeof(buffer), i, date);
+       } else {
+               i = add_raw(buffer, sizeof(buffer), i, ">");
+       }
+       if (i >= sizeof(buffer))
+               die("Impossibly long personal identifier");
+       buffer[i] = 0;
+       return buffer;
+}
+
+const char *fmt_ident(const char *name, const char *email,
+                     const char *date_str, int error_on_no_name)
+{
+       int flag = (error_on_no_name ? 01 : 0);
+       return fmt_ident_1(name, email, date_str, flag);
+}
+
+const char *fmt_name(const char *name, const char *email)
+{
+       return fmt_ident_1(name, email, NULL, 03);
+}
+
+const char *git_author_info(int error_on_no_name)
+{
+       return fmt_ident(getenv("GIT_AUTHOR_NAME"),
+                        getenv("GIT_AUTHOR_EMAIL"),
+                        getenv("GIT_AUTHOR_DATE"),
+                        error_on_no_name);
+}
+
+const char *git_committer_info(int error_on_no_name)
+{
+       return fmt_ident(getenv("GIT_COMMITTER_NAME"),
+                        getenv("GIT_COMMITTER_EMAIL"),
+                        getenv("GIT_COMMITTER_DATE"),
+                        error_on_no_name);
+}
diff --git a/imap-send.c b/imap-send.c
new file mode 100644 (file)
index 0000000..a429a76
--- /dev/null
@@ -0,0 +1,1338 @@
+/*
+ * git-imap-send - drops patches into an imap Drafts folder
+ *                 derived from isync/mbsync - mailbox synchronizer
+ *
+ * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org>
+ * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net>
+ * Copyright (C) 2004 Theodore Y. Ts'o <tytso@mit.edu>
+ * Copyright (C) 2006 Mike McCormack
+ *
+ *  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
+ */
+
+#include "cache.h"
+
+typedef struct store_conf {
+       char *name;
+       const char *path; /* should this be here? its interpretation is driver-specific */
+       char *map_inbox;
+       char *trash;
+       unsigned max_size; /* off_t is overkill */
+       unsigned trash_remote_new:1, trash_only_new:1;
+} store_conf_t;
+
+typedef struct string_list {
+       struct string_list *next;
+       char string[1];
+} string_list_t;
+
+typedef struct channel_conf {
+       struct channel_conf *next;
+       char *name;
+       store_conf_t *master, *slave;
+       char *master_name, *slave_name;
+       char *sync_state;
+       string_list_t *patterns;
+       int mops, sops;
+       unsigned max_messages; /* for slave only */
+} channel_conf_t;
+
+typedef struct group_conf {
+       struct group_conf *next;
+       char *name;
+       string_list_t *channels;
+} group_conf_t;
+
+/* For message->status */
+#define M_RECENT       (1<<0) /* unsyncable flag; maildir_* depend on this being 1<<0 */
+#define M_DEAD         (1<<1) /* expunged */
+#define M_FLAGS        (1<<2) /* flags fetched */
+
+typedef struct message {
+       struct message *next;
+       /* string_list_t *keywords; */
+       size_t size; /* zero implies "not fetched" */
+       int uid;
+       unsigned char flags, status;
+} message_t;
+
+typedef struct store {
+       store_conf_t *conf; /* foreign */
+
+       /* currently open mailbox */
+       const char *name; /* foreign! maybe preset? */
+       char *path; /* own */
+       message_t *msgs; /* own */
+       int uidvalidity;
+       unsigned char opts; /* maybe preset? */
+       /* note that the following do _not_ reflect stats from msgs, but mailbox totals */
+       int count; /* # of messages */
+       int recent; /* # of recent messages - don't trust this beyond the initial read */
+} store_t;
+
+typedef struct {
+       char *data;
+       int len;
+       unsigned char flags;
+       unsigned int crlf:1;
+} msg_data_t;
+
+#define DRV_OK          0
+#define DRV_MSG_BAD     -1
+#define DRV_BOX_BAD     -2
+#define DRV_STORE_BAD   -3
+
+static int Verbose, Quiet;
+
+static void imap_info( const char *, ... );
+static void imap_warn( const char *, ... );
+
+static char *next_arg( char ** );
+
+static void free_generic_messages( message_t * );
+
+static int nfsnprintf( char *buf, int blen, const char *fmt, ... );
+
+static int nfvasprintf(char **strp, const char *fmt, va_list ap)
+{
+       int len;
+       char tmp[8192];
+
+       len = vsnprintf(tmp, sizeof(tmp), fmt, ap);
+       if (len < 0)
+               die("Fatal: Out of memory\n");
+       if (len >= sizeof(tmp))
+               die("imap command overflow !\n");
+       *strp = xmemdupz(tmp, len);
+       return len;
+}
+
+static void arc4_init( void );
+static unsigned char arc4_getbyte( void );
+
+typedef struct imap_server_conf {
+       char *name;
+       char *tunnel;
+       char *host;
+       int port;
+       char *user;
+       char *pass;
+} imap_server_conf_t;
+
+typedef struct imap_store_conf {
+       store_conf_t gen;
+       imap_server_conf_t *server;
+       unsigned use_namespace:1;
+} imap_store_conf_t;
+
+#define NIL    (void*)0x1
+#define LIST   (void*)0x2
+
+typedef struct _list {
+       struct _list *next, *child;
+       char *val;
+       int len;
+} list_t;
+
+typedef struct {
+       int fd;
+} Socket_t;
+
+typedef struct {
+       Socket_t sock;
+       int bytes;
+       int offset;
+       char buf[1024];
+} buffer_t;
+
+struct imap_cmd;
+
+typedef struct imap {
+       int uidnext; /* from SELECT responses */
+       list_t *ns_personal, *ns_other, *ns_shared; /* NAMESPACE info */
+       unsigned caps, rcaps; /* CAPABILITY results */
+       /* command queue */
+       int nexttag, num_in_progress, literal_pending;
+       struct imap_cmd *in_progress, **in_progress_append;
+       buffer_t buf; /* this is BIG, so put it last */
+} imap_t;
+
+typedef struct imap_store {
+       store_t gen;
+       int uidvalidity;
+       imap_t *imap;
+       const char *prefix;
+       unsigned /*currentnc:1,*/ trashnc:1;
+} imap_store_t;
+
+struct imap_cmd_cb {
+       int (*cont)( imap_store_t *ctx, struct imap_cmd *cmd, const char *prompt );
+       void (*done)( imap_store_t *ctx, struct imap_cmd *cmd, int response);
+       void *ctx;
+       char *data;
+       int dlen;
+       int uid;
+       unsigned create:1, trycreate:1;
+};
+
+struct imap_cmd {
+       struct imap_cmd *next;
+       struct imap_cmd_cb cb;
+       char *cmd;
+       int tag;
+};
+
+#define CAP(cap) (imap->caps & (1 << (cap)))
+
+enum CAPABILITY {
+       NOLOGIN = 0,
+       UIDPLUS,
+       LITERALPLUS,
+       NAMESPACE,
+};
+
+static const char *cap_list[] = {
+       "LOGINDISABLED",
+       "UIDPLUS",
+       "LITERAL+",
+       "NAMESPACE",
+};
+
+#define RESP_OK    0
+#define RESP_NO    1
+#define RESP_BAD   2
+
+static int get_cmd_result( imap_store_t *ctx, struct imap_cmd *tcmd );
+
+
+static const char *Flags[] = {
+       "Draft",
+       "Flagged",
+       "Answered",
+       "Seen",
+       "Deleted",
+};
+
+static void
+socket_perror( const char *func, Socket_t *sock, int ret )
+{
+       if (ret < 0)
+               perror( func );
+       else
+               fprintf( stderr, "%s: unexpected EOF\n", func );
+}
+
+static int
+socket_read( Socket_t *sock, char *buf, int len )
+{
+       ssize_t n = xread( sock->fd, buf, len );
+       if (n <= 0) {
+               socket_perror( "read", sock, n );
+               close( sock->fd );
+               sock->fd = -1;
+       }
+       return n;
+}
+
+static int
+socket_write( Socket_t *sock, const char *buf, int len )
+{
+       int n = write_in_full( sock->fd, buf, len );
+       if (n != len) {
+               socket_perror( "write", sock, n );
+               close( sock->fd );
+               sock->fd = -1;
+       }
+       return n;
+}
+
+/* simple line buffering */
+static int
+buffer_gets( buffer_t * b, char **s )
+{
+       int n;
+       int start = b->offset;
+
+       *s = b->buf + start;
+
+       for (;;) {
+               /* make sure we have enough data to read the \r\n sequence */
+               if (b->offset + 1 >= b->bytes) {
+                       if (start) {
+                               /* shift down used bytes */
+                               *s = b->buf;
+
+                               assert( start <= b->bytes );
+                               n = b->bytes - start;
+
+                               if (n)
+                                       memmove(b->buf, b->buf + start, n);
+                               b->offset -= start;
+                               b->bytes = n;
+                               start = 0;
+                       }
+
+                       n = socket_read( &b->sock, b->buf + b->bytes,
+                                        sizeof(b->buf) - b->bytes );
+
+                       if (n <= 0)
+                               return -1;
+
+                       b->bytes += n;
+               }
+
+               if (b->buf[b->offset] == '\r') {
+                       assert( b->offset + 1 < b->bytes );
+                       if (b->buf[b->offset + 1] == '\n') {
+                               b->buf[b->offset] = 0;  /* terminate the string */
+                               b->offset += 2; /* next line */
+                               if (Verbose)
+                                       puts( *s );
+                               return 0;
+                       }
+               }
+
+               b->offset++;
+       }
+       /* not reached */
+}
+
+static void
+imap_info( const char *msg, ... )
+{
+       va_list va;
+
+       if (!Quiet) {
+               va_start( va, msg );
+               vprintf( msg, va );
+               va_end( va );
+               fflush( stdout );
+       }
+}
+
+static void
+imap_warn( const char *msg, ... )
+{
+       va_list va;
+
+       if (Quiet < 2) {
+               va_start( va, msg );
+               vfprintf( stderr, msg, va );
+               va_end( va );
+       }
+}
+
+static char *
+next_arg( char **s )
+{
+       char *ret;
+
+       if (!s || !*s)
+               return NULL;
+       while (isspace( (unsigned char) **s ))
+               (*s)++;
+       if (!**s) {
+               *s = NULL;
+               return NULL;
+       }
+       if (**s == '"') {
+               ++*s;
+               ret = *s;
+               *s = strchr( *s, '"' );
+       } else {
+               ret = *s;
+               while (**s && !isspace( (unsigned char) **s ))
+                       (*s)++;
+       }
+       if (*s) {
+               if (**s)
+                       *(*s)++ = 0;
+               if (!**s)
+                       *s = NULL;
+       }
+       return ret;
+}
+
+static void
+free_generic_messages( message_t *msgs )
+{
+       message_t *tmsg;
+
+       for (; msgs; msgs = tmsg) {
+               tmsg = msgs->next;
+               free( msgs );
+       }
+}
+
+static int
+nfsnprintf( char *buf, int blen, const char *fmt, ... )
+{
+       int ret;
+       va_list va;
+
+       va_start( va, fmt );
+       if (blen <= 0 || (unsigned)(ret = vsnprintf( buf, blen, fmt, va )) >= (unsigned)blen)
+               die( "Fatal: buffer too small. Please report a bug.\n");
+       va_end( va );
+       return ret;
+}
+
+static struct {
+       unsigned char i, j, s[256];
+} rs;
+
+static void
+arc4_init( void )
+{
+       int i, fd;
+       unsigned char j, si, dat[128];
+
+       if ((fd = open( "/dev/urandom", O_RDONLY )) < 0 && (fd = open( "/dev/random", O_RDONLY )) < 0) {
+               fprintf( stderr, "Fatal: no random number source available.\n" );
+               exit( 3 );
+       }
+       if (read_in_full( fd, dat, 128 ) != 128) {
+               fprintf( stderr, "Fatal: cannot read random number source.\n" );
+               exit( 3 );
+       }
+       close( fd );
+
+       for (i = 0; i < 256; i++)
+               rs.s[i] = i;
+       for (i = j = 0; i < 256; i++) {
+               si = rs.s[i];
+               j += si + dat[i & 127];
+               rs.s[i] = rs.s[j];
+               rs.s[j] = si;
+       }
+       rs.i = rs.j = 0;
+
+       for (i = 0; i < 256; i++)
+               arc4_getbyte();
+}
+
+static unsigned char
+arc4_getbyte( void )
+{
+       unsigned char si, sj;
+
+       rs.i++;
+       si = rs.s[rs.i];
+       rs.j += si;
+       sj = rs.s[rs.j];
+       rs.s[rs.i] = sj;
+       rs.s[rs.j] = si;
+       return rs.s[(si + sj) & 0xff];
+}
+
+static struct imap_cmd *
+v_issue_imap_cmd( imap_store_t *ctx, struct imap_cmd_cb *cb,
+                 const char *fmt, va_list ap )
+{
+       imap_t *imap = ctx->imap;
+       struct imap_cmd *cmd;
+       int n, bufl;
+       char buf[1024];
+
+       cmd = xmalloc( sizeof(struct imap_cmd) );
+       nfvasprintf( &cmd->cmd, fmt, ap );
+       cmd->tag = ++imap->nexttag;
+
+       if (cb)
+               cmd->cb = *cb;
+       else
+               memset( &cmd->cb, 0, sizeof(cmd->cb) );
+
+       while (imap->literal_pending)
+               get_cmd_result( ctx, NULL );
+
+       bufl = nfsnprintf( buf, sizeof(buf), cmd->cb.data ? CAP(LITERALPLUS) ?
+                          "%d %s{%d+}\r\n" : "%d %s{%d}\r\n" : "%d %s\r\n",
+                          cmd->tag, cmd->cmd, cmd->cb.dlen );
+       if (Verbose) {
+               if (imap->num_in_progress)
+                       printf( "(%d in progress) ", imap->num_in_progress );
+               if (memcmp( cmd->cmd, "LOGIN", 5 ))
+                       printf( ">>> %s", buf );
+               else
+                       printf( ">>> %d LOGIN <user> <pass>\n", cmd->tag );
+       }
+       if (socket_write( &imap->buf.sock, buf, bufl ) != bufl) {
+               free( cmd->cmd );
+               free( cmd );
+               if (cb && cb->data)
+                       free( cb->data );
+               return NULL;
+       }
+       if (cmd->cb.data) {
+               if (CAP(LITERALPLUS)) {
+                       n = socket_write( &imap->buf.sock, cmd->cb.data, cmd->cb.dlen );
+                       free( cmd->cb.data );
+                       if (n != cmd->cb.dlen ||
+                           (n = socket_write( &imap->buf.sock, "\r\n", 2 )) != 2)
+                       {
+                               free( cmd->cmd );
+                               free( cmd );
+                               return NULL;
+                       }
+                       cmd->cb.data = NULL;
+               } else
+                       imap->literal_pending = 1;
+       } else if (cmd->cb.cont)
+               imap->literal_pending = 1;
+       cmd->next = NULL;
+       *imap->in_progress_append = cmd;
+       imap->in_progress_append = &cmd->next;
+       imap->num_in_progress++;
+       return cmd;
+}
+
+static struct imap_cmd *
+issue_imap_cmd( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... )
+{
+       struct imap_cmd *ret;
+       va_list ap;
+
+       va_start( ap, fmt );
+       ret = v_issue_imap_cmd( ctx, cb, fmt, ap );
+       va_end( ap );
+       return ret;
+}
+
+static int
+imap_exec( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... )
+{
+       va_list ap;
+       struct imap_cmd *cmdp;
+
+       va_start( ap, fmt );
+       cmdp = v_issue_imap_cmd( ctx, cb, fmt, ap );
+       va_end( ap );
+       if (!cmdp)
+               return RESP_BAD;
+
+       return get_cmd_result( ctx, cmdp );
+}
+
+static int
+imap_exec_m( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... )
+{
+       va_list ap;
+       struct imap_cmd *cmdp;
+
+       va_start( ap, fmt );
+       cmdp = v_issue_imap_cmd( ctx, cb, fmt, ap );
+       va_end( ap );
+       if (!cmdp)
+               return DRV_STORE_BAD;
+
+       switch (get_cmd_result( ctx, cmdp )) {
+       case RESP_BAD: return DRV_STORE_BAD;
+       case RESP_NO: return DRV_MSG_BAD;
+       default: return DRV_OK;
+       }
+}
+
+static int
+is_atom( list_t *list )
+{
+       return list && list->val && list->val != NIL && list->val != LIST;
+}
+
+static int
+is_list( list_t *list )
+{
+       return list && list->val == LIST;
+}
+
+static void
+free_list( list_t *list )
+{
+       list_t *tmp;
+
+       for (; list; list = tmp) {
+               tmp = list->next;
+               if (is_list( list ))
+                       free_list( list->child );
+               else if (is_atom( list ))
+                       free( list->val );
+               free( list );
+       }
+}
+
+static int
+parse_imap_list_l( imap_t *imap, char **sp, list_t **curp, int level )
+{
+       list_t *cur;
+       char *s = *sp, *p;
+       int n, bytes;
+
+       for (;;) {
+               while (isspace( (unsigned char)*s ))
+                       s++;
+               if (level && *s == ')') {
+                       s++;
+                       break;
+               }
+               *curp = cur = xmalloc( sizeof(*cur) );
+               curp = &cur->next;
+               cur->val = NULL; /* for clean bail */
+               if (*s == '(') {
+                       /* sublist */
+                       s++;
+                       cur->val = LIST;
+                       if (parse_imap_list_l( imap, &s, &cur->child, level + 1 ))
+                               goto bail;
+               } else if (imap && *s == '{') {
+                       /* literal */
+                       bytes = cur->len = strtol( s + 1, &s, 10 );
+                       if (*s != '}')
+                               goto bail;
+
+                       s = cur->val = xmalloc( cur->len );
+
+                       /* dump whats left over in the input buffer */
+                       n = imap->buf.bytes - imap->buf.offset;
+
+                       if (n > bytes)
+                               /* the entire message fit in the buffer */
+                               n = bytes;
+
+                       memcpy( s, imap->buf.buf + imap->buf.offset, n );
+                       s += n;
+                       bytes -= n;
+
+                       /* mark that we used part of the buffer */
+                       imap->buf.offset += n;
+
+                       /* now read the rest of the message */
+                       while (bytes > 0) {
+                               if ((n = socket_read (&imap->buf.sock, s, bytes)) <= 0)
+                                       goto bail;
+                               s += n;
+                               bytes -= n;
+                       }
+
+                       if (buffer_gets( &imap->buf, &s ))
+                               goto bail;
+               } else if (*s == '"') {
+                       /* quoted string */
+                       s++;
+                       p = s;
+                       for (; *s != '"'; s++)
+                               if (!*s)
+                                       goto bail;
+                       cur->len = s - p;
+                       s++;
+                       cur->val = xmemdupz(p, cur->len);
+               } else {
+                       /* atom */
+                       p = s;
+                       for (; *s && !isspace( (unsigned char)*s ); s++)
+                               if (level && *s == ')')
+                                       break;
+                       cur->len = s - p;
+                       if (cur->len == 3 && !memcmp ("NIL", p, 3)) {
+                               cur->val = NIL;
+                       } else {
+                               cur->val = xmemdupz(p, cur->len);
+                       }
+               }
+
+               if (!level)
+                       break;
+               if (!*s)
+                       goto bail;
+       }
+       *sp = s;
+       *curp = NULL;
+       return 0;
+
+  bail:
+       *curp = NULL;
+       return -1;
+}
+
+static list_t *
+parse_imap_list( imap_t *imap, char **sp )
+{
+       list_t *head;
+
+       if (!parse_imap_list_l( imap, sp, &head, 0 ))
+               return head;
+       free_list( head );
+       return NULL;
+}
+
+static list_t *
+parse_list( char **sp )
+{
+       return parse_imap_list( NULL, sp );
+}
+
+static void
+parse_capability( imap_t *imap, char *cmd )
+{
+       char *arg;
+       unsigned i;
+
+       imap->caps = 0x80000000;
+       while ((arg = next_arg( &cmd )))
+               for (i = 0; i < ARRAY_SIZE(cap_list); i++)
+                       if (!strcmp( cap_list[i], arg ))
+                               imap->caps |= 1 << i;
+       imap->rcaps = imap->caps;
+}
+
+static int
+parse_response_code( imap_store_t *ctx, struct imap_cmd_cb *cb, char *s )
+{
+       imap_t *imap = ctx->imap;
+       char *arg, *p;
+
+       if (*s != '[')
+               return RESP_OK;         /* no response code */
+       s++;
+       if (!(p = strchr( s, ']' ))) {
+               fprintf( stderr, "IMAP error: malformed response code\n" );
+               return RESP_BAD;
+       }
+       *p++ = 0;
+       arg = next_arg( &s );
+       if (!strcmp( "UIDVALIDITY", arg )) {
+               if (!(arg = next_arg( &s )) || !(ctx->gen.uidvalidity = atoi( arg ))) {
+                       fprintf( stderr, "IMAP error: malformed UIDVALIDITY status\n" );
+                       return RESP_BAD;
+               }
+       } else if (!strcmp( "UIDNEXT", arg )) {
+               if (!(arg = next_arg( &s )) || !(imap->uidnext = atoi( arg ))) {
+                       fprintf( stderr, "IMAP error: malformed NEXTUID status\n" );
+                       return RESP_BAD;
+               }
+       } else if (!strcmp( "CAPABILITY", arg )) {
+               parse_capability( imap, s );
+       } else if (!strcmp( "ALERT", arg )) {
+               /* RFC2060 says that these messages MUST be displayed
+                * to the user
+                */
+               for (; isspace( (unsigned char)*p ); p++);
+               fprintf( stderr, "*** IMAP ALERT *** %s\n", p );
+       } else if (cb && cb->ctx && !strcmp( "APPENDUID", arg )) {
+               if (!(arg = next_arg( &s )) || !(ctx->gen.uidvalidity = atoi( arg )) ||
+                   !(arg = next_arg( &s )) || !(*(int *)cb->ctx = atoi( arg )))
+               {
+                       fprintf( stderr, "IMAP error: malformed APPENDUID status\n" );
+                       return RESP_BAD;
+               }
+       }
+       return RESP_OK;
+}
+
+static int
+get_cmd_result( imap_store_t *ctx, struct imap_cmd *tcmd )
+{
+       imap_t *imap = ctx->imap;
+       struct imap_cmd *cmdp, **pcmdp, *ncmdp;
+       char *cmd, *arg, *arg1, *p;
+       int n, resp, resp2, tag;
+
+       for (;;) {
+               if (buffer_gets( &imap->buf, &cmd ))
+                       return RESP_BAD;
+
+               arg = next_arg( &cmd );
+               if (*arg == '*') {
+                       arg = next_arg( &cmd );
+                       if (!arg) {
+                               fprintf( stderr, "IMAP error: unable to parse untagged response\n" );
+                               return RESP_BAD;
+                       }
+
+                       if (!strcmp( "NAMESPACE", arg )) {
+                               imap->ns_personal = parse_list( &cmd );
+                               imap->ns_other = parse_list( &cmd );
+                               imap->ns_shared = parse_list( &cmd );
+                       } else if (!strcmp( "OK", arg ) || !strcmp( "BAD", arg ) ||
+                                  !strcmp( "NO", arg ) || !strcmp( "BYE", arg )) {
+                               if ((resp = parse_response_code( ctx, NULL, cmd )) != RESP_OK)
+                                       return resp;
+                       } else if (!strcmp( "CAPABILITY", arg ))
+                               parse_capability( imap, cmd );
+                       else if ((arg1 = next_arg( &cmd ))) {
+                               if (!strcmp( "EXISTS", arg1 ))
+                                       ctx->gen.count = atoi( arg );
+                               else if (!strcmp( "RECENT", arg1 ))
+                                       ctx->gen.recent = atoi( arg );
+                       } else {
+                               fprintf( stderr, "IMAP error: unable to parse untagged response\n" );
+                               return RESP_BAD;
+                       }
+               } else if (!imap->in_progress) {
+                       fprintf( stderr, "IMAP error: unexpected reply: %s %s\n", arg, cmd ? cmd : "" );
+                       return RESP_BAD;
+               } else if (*arg == '+') {
+                       /* This can happen only with the last command underway, as
+                          it enforces a round-trip. */
+                       cmdp = (struct imap_cmd *)((char *)imap->in_progress_append -
+                              offsetof(struct imap_cmd, next));
+                       if (cmdp->cb.data) {
+                               n = socket_write( &imap->buf.sock, cmdp->cb.data, cmdp->cb.dlen );
+                               free( cmdp->cb.data );
+                               cmdp->cb.data = NULL;
+                               if (n != (int)cmdp->cb.dlen)
+                                       return RESP_BAD;
+                       } else if (cmdp->cb.cont) {
+                               if (cmdp->cb.cont( ctx, cmdp, cmd ))
+                                       return RESP_BAD;
+                       } else {
+                               fprintf( stderr, "IMAP error: unexpected command continuation request\n" );
+                               return RESP_BAD;
+                       }
+                       if (socket_write( &imap->buf.sock, "\r\n", 2 ) != 2)
+                               return RESP_BAD;
+                       if (!cmdp->cb.cont)
+                               imap->literal_pending = 0;
+                       if (!tcmd)
+                               return DRV_OK;
+               } else {
+                       tag = atoi( arg );
+                       for (pcmdp = &imap->in_progress; (cmdp = *pcmdp); pcmdp = &cmdp->next)
+                               if (cmdp->tag == tag)
+                                       goto gottag;
+                       fprintf( stderr, "IMAP error: unexpected tag %s\n", arg );
+                       return RESP_BAD;
+                 gottag:
+                       if (!(*pcmdp = cmdp->next))
+                               imap->in_progress_append = pcmdp;
+                       imap->num_in_progress--;
+                       if (cmdp->cb.cont || cmdp->cb.data)
+                               imap->literal_pending = 0;
+                       arg = next_arg( &cmd );
+                       if (!strcmp( "OK", arg ))
+                               resp = DRV_OK;
+                       else {
+                               if (!strcmp( "NO", arg )) {
+                                       if (cmdp->cb.create && cmd && (cmdp->cb.trycreate || !memcmp( cmd, "[TRYCREATE]", 11 ))) { /* SELECT, APPEND or UID COPY */
+                                               p = strchr( cmdp->cmd, '"' );
+                                               if (!issue_imap_cmd( ctx, NULL, "CREATE \"%.*s\"", strchr( p + 1, '"' ) - p + 1, p )) {
+                                                       resp = RESP_BAD;
+                                                       goto normal;
+                                               }
+                                               /* not waiting here violates the spec, but a server that does not
+                                                  grok this nonetheless violates it too. */
+                                               cmdp->cb.create = 0;
+                                               if (!(ncmdp = issue_imap_cmd( ctx, &cmdp->cb, "%s", cmdp->cmd ))) {
+                                                       resp = RESP_BAD;
+                                                       goto normal;
+                                               }
+                                               free( cmdp->cmd );
+                                               free( cmdp );
+                                               if (!tcmd)
+                                                       return 0;       /* ignored */
+                                               if (cmdp == tcmd)
+                                                       tcmd = ncmdp;
+                                               continue;
+                                       }
+                                       resp = RESP_NO;
+                               } else /*if (!strcmp( "BAD", arg ))*/
+                                       resp = RESP_BAD;
+                               fprintf( stderr, "IMAP command '%s' returned response (%s) - %s\n",
+                                        memcmp (cmdp->cmd, "LOGIN", 5) ?
+                                                       cmdp->cmd : "LOGIN <user> <pass>",
+                                                       arg, cmd ? cmd : "");
+                       }
+                       if ((resp2 = parse_response_code( ctx, &cmdp->cb, cmd )) > resp)
+                               resp = resp2;
+                 normal:
+                       if (cmdp->cb.done)
+                               cmdp->cb.done( ctx, cmdp, resp );
+                       if (cmdp->cb.data)
+                               free( cmdp->cb.data );
+                       free( cmdp->cmd );
+                       free( cmdp );
+                       if (!tcmd || tcmd == cmdp)
+                               return resp;
+               }
+       }
+       /* not reached */
+}
+
+static void
+imap_close_server( imap_store_t *ictx )
+{
+       imap_t *imap = ictx->imap;
+
+       if (imap->buf.sock.fd != -1) {
+               imap_exec( ictx, NULL, "LOGOUT" );
+               close( imap->buf.sock.fd );
+       }
+       free_list( imap->ns_personal );
+       free_list( imap->ns_other );
+       free_list( imap->ns_shared );
+       free( imap );
+}
+
+static void
+imap_close_store( store_t *ctx )
+{
+       imap_close_server( (imap_store_t *)ctx );
+       free_generic_messages( ctx->msgs );
+       free( ctx );
+}
+
+static store_t *
+imap_open_store( imap_server_conf_t *srvc )
+{
+       imap_store_t *ctx;
+       imap_t *imap;
+       char *arg, *rsp;
+       struct hostent *he;
+       struct sockaddr_in addr;
+       int s, a[2], preauth;
+       pid_t pid;
+
+       ctx = xcalloc( sizeof(*ctx), 1 );
+
+       ctx->imap = imap = xcalloc( sizeof(*imap), 1 );
+       imap->buf.sock.fd = -1;
+       imap->in_progress_append = &imap->in_progress;
+
+       /* open connection to IMAP server */
+
+       if (srvc->tunnel) {
+               imap_info( "Starting tunnel '%s'... ", srvc->tunnel );
+
+               if (socketpair( PF_UNIX, SOCK_STREAM, 0, a )) {
+                       perror( "socketpair" );
+                       exit( 1 );
+               }
+
+               pid = fork();
+               if (pid < 0)
+                       _exit( 127 );
+               if (!pid) {
+                       if (dup2( a[0], 0 ) == -1 || dup2( a[0], 1 ) == -1)
+                               _exit( 127 );
+                       close( a[0] );
+                       close( a[1] );
+                       execl( "/bin/sh", "sh", "-c", srvc->tunnel, NULL );
+                       _exit( 127 );
+               }
+
+               close (a[0]);
+
+               imap->buf.sock.fd = a[1];
+
+               imap_info( "ok\n" );
+       } else {
+               memset( &addr, 0, sizeof(addr) );
+               addr.sin_port = htons( srvc->port );
+               addr.sin_family = AF_INET;
+
+               imap_info( "Resolving %s... ", srvc->host );
+               he = gethostbyname( srvc->host );
+               if (!he) {
+                       perror( "gethostbyname" );
+                       goto bail;
+               }
+               imap_info( "ok\n" );
+
+               addr.sin_addr.s_addr = *((int *) he->h_addr_list[0]);
+
+               s = socket( PF_INET, SOCK_STREAM, 0 );
+
+               imap_info( "Connecting to %s:%hu... ", inet_ntoa( addr.sin_addr ), ntohs( addr.sin_port ) );
+               if (connect( s, (struct sockaddr *)&addr, sizeof(addr) )) {
+                       close( s );
+                       perror( "connect" );
+                       goto bail;
+               }
+               imap_info( "ok\n" );
+
+               imap->buf.sock.fd = s;
+
+       }
+
+       /* read the greeting string */
+       if (buffer_gets( &imap->buf, &rsp )) {
+               fprintf( stderr, "IMAP error: no greeting response\n" );
+               goto bail;
+       }
+       arg = next_arg( &rsp );
+       if (!arg || *arg != '*' || (arg = next_arg( &rsp )) == NULL) {
+               fprintf( stderr, "IMAP error: invalid greeting response\n" );
+               goto bail;
+       }
+       preauth = 0;
+       if (!strcmp( "PREAUTH", arg ))
+               preauth = 1;
+       else if (strcmp( "OK", arg ) != 0) {
+               fprintf( stderr, "IMAP error: unknown greeting response\n" );
+               goto bail;
+       }
+       parse_response_code( ctx, NULL, rsp );
+       if (!imap->caps && imap_exec( ctx, NULL, "CAPABILITY" ) != RESP_OK)
+               goto bail;
+
+       if (!preauth) {
+
+               imap_info ("Logging in...\n");
+               if (!srvc->user) {
+                       fprintf( stderr, "Skipping server %s, no user\n", srvc->host );
+                       goto bail;
+               }
+               if (!srvc->pass) {
+                       char prompt[80];
+                       sprintf( prompt, "Password (%s@%s): ", srvc->user, srvc->host );
+                       arg = getpass( prompt );
+                       if (!arg) {
+                               perror( "getpass" );
+                               exit( 1 );
+                       }
+                       if (!*arg) {
+                               fprintf( stderr, "Skipping account %s@%s, no password\n", srvc->user, srvc->host );
+                               goto bail;
+                       }
+                       /*
+                        * getpass() returns a pointer to a static buffer.  make a copy
+                        * for long term storage.
+                        */
+                       srvc->pass = xstrdup( arg );
+               }
+               if (CAP(NOLOGIN)) {
+                       fprintf( stderr, "Skipping account %s@%s, server forbids LOGIN\n", srvc->user, srvc->host );
+                       goto bail;
+               }
+               imap_warn( "*** IMAP Warning *** Password is being sent in the clear\n" );
+               if (imap_exec( ctx, NULL, "LOGIN \"%s\" \"%s\"", srvc->user, srvc->pass ) != RESP_OK) {
+                       fprintf( stderr, "IMAP error: LOGIN failed\n" );
+                       goto bail;
+               }
+       } /* !preauth */
+
+       ctx->prefix = "";
+       ctx->trashnc = 1;
+       return (store_t *)ctx;
+
+  bail:
+       imap_close_store( &ctx->gen );
+       return NULL;
+}
+
+static int
+imap_make_flags( int flags, char *buf )
+{
+       const char *s;
+       unsigned i, d;
+
+       for (i = d = 0; i < ARRAY_SIZE(Flags); i++)
+               if (flags & (1 << i)) {
+                       buf[d++] = ' ';
+                       buf[d++] = '\\';
+                       for (s = Flags[i]; *s; s++)
+                               buf[d++] = *s;
+               }
+       buf[0] = '(';
+       buf[d++] = ')';
+       return d;
+}
+
+#define TUIDL 8
+
+static int
+imap_store_msg( store_t *gctx, msg_data_t *data, int *uid )
+{
+       imap_store_t *ctx = (imap_store_t *)gctx;
+       imap_t *imap = ctx->imap;
+       struct imap_cmd_cb cb;
+       char *fmap, *buf;
+       const char *prefix, *box;
+       int ret, i, j, d, len, extra, nocr;
+       int start, sbreak = 0, ebreak = 0;
+       char flagstr[128], tuid[TUIDL * 2 + 1];
+
+       memset( &cb, 0, sizeof(cb) );
+
+       fmap = data->data;
+       len = data->len;
+       nocr = !data->crlf;
+       extra = 0, i = 0;
+       if (!CAP(UIDPLUS) && uid) {
+         nloop:
+               start = i;
+               while (i < len)
+                       if (fmap[i++] == '\n') {
+                               extra += nocr;
+                               if (i - 2 + nocr == start) {
+                                       sbreak = ebreak = i - 2 + nocr;
+                                       goto mktid;
+                               }
+                               if (!memcmp( fmap + start, "X-TUID: ", 8 )) {
+                                       extra -= (ebreak = i) - (sbreak = start) + nocr;
+                                       goto mktid;
+                               }
+                               goto nloop;
+                       }
+               /* invalid message */
+               free( fmap );
+               return DRV_MSG_BAD;
+        mktid:
+               for (j = 0; j < TUIDL; j++)
+                       sprintf( tuid + j * 2, "%02x", arc4_getbyte() );
+               extra += 8 + TUIDL * 2 + 2;
+       }
+       if (nocr)
+               for (; i < len; i++)
+                       if (fmap[i] == '\n')
+                               extra++;
+
+       cb.dlen = len + extra;
+       buf = cb.data = xmalloc( cb.dlen );
+       i = 0;
+       if (!CAP(UIDPLUS) && uid) {
+               if (nocr) {
+                       for (; i < sbreak; i++)
+                               if (fmap[i] == '\n') {
+                                       *buf++ = '\r';
+                                       *buf++ = '\n';
+                               } else
+                                       *buf++ = fmap[i];
+               } else {
+                       memcpy( buf, fmap, sbreak );
+                       buf += sbreak;
+               }
+               memcpy( buf, "X-TUID: ", 8 );
+               buf += 8;
+               memcpy( buf, tuid, TUIDL * 2 );
+               buf += TUIDL * 2;
+               *buf++ = '\r';
+               *buf++ = '\n';
+               i = ebreak;
+       }
+       if (nocr) {
+               for (; i < len; i++)
+                       if (fmap[i] == '\n') {
+                               *buf++ = '\r';
+                               *buf++ = '\n';
+                       } else
+                               *buf++ = fmap[i];
+       } else
+               memcpy( buf, fmap + i, len - i );
+
+       free( fmap );
+
+       d = 0;
+       if (data->flags) {
+               d = imap_make_flags( data->flags, flagstr );
+               flagstr[d++] = ' ';
+       }
+       flagstr[d] = 0;
+
+       if (!uid) {
+               box = gctx->conf->trash;
+               prefix = ctx->prefix;
+               cb.create = 1;
+               if (ctx->trashnc)
+                       imap->caps = imap->rcaps & ~(1 << LITERALPLUS);
+       } else {
+               box = gctx->name;
+               prefix = !strcmp( box, "INBOX" ) ? "" : ctx->prefix;
+               cb.create = 0;
+       }
+       cb.ctx = uid;
+       ret = imap_exec_m( ctx, &cb, "APPEND \"%s%s\" %s", prefix, box, flagstr );
+       imap->caps = imap->rcaps;
+       if (ret != DRV_OK)
+               return ret;
+       if (!uid)
+               ctx->trashnc = 0;
+       else
+               gctx->count++;
+
+       return DRV_OK;
+}
+
+#define CHUNKSIZE 0x1000
+
+static int
+read_message( FILE *f, msg_data_t *msg )
+{
+       struct strbuf buf;
+
+       memset(msg, 0, sizeof(*msg));
+       strbuf_init(&buf, 0);
+
+       do {
+               if (strbuf_fread(&buf, CHUNKSIZE, f) <= 0)
+                       break;
+       } while (!feof(f));
+
+       msg->len  = buf.len;
+       msg->data = strbuf_detach(&buf, NULL);
+       return msg->len;
+}
+
+static int
+count_messages( msg_data_t *msg )
+{
+       int count = 0;
+       char *p = msg->data;
+
+       while (1) {
+               if (!prefixcmp(p, "From ")) {
+                       count++;
+                       p += 5;
+               }
+               p = strstr( p+5, "\nFrom ");
+               if (!p)
+                       break;
+               p++;
+       }
+       return count;
+}
+
+static int
+split_msg( msg_data_t *all_msgs, msg_data_t *msg, int *ofs )
+{
+       char *p, *data;
+
+       memset( msg, 0, sizeof *msg );
+       if (*ofs >= all_msgs->len)
+               return 0;
+
+       data = &all_msgs->data[ *ofs ];
+       msg->len = all_msgs->len - *ofs;
+
+       if (msg->len < 5 || prefixcmp(data, "From "))
+               return 0;
+
+       p = strchr( data, '\n' );
+       if (p) {
+               p = &p[1];
+               msg->len -= p-data;
+               *ofs += p-data;
+               data = p;
+       }
+
+       p = strstr( data, "\nFrom " );
+       if (p)
+               msg->len = &p[1] - data;
+
+       msg->data = xmemdupz(data, msg->len);
+       *ofs += msg->len;
+       return 1;
+}
+
+static imap_server_conf_t server =
+{
+       NULL,   /* name */
+       NULL,   /* tunnel */
+       NULL,   /* host */
+       0,      /* port */
+       NULL,   /* user */
+       NULL,   /* pass */
+};
+
+static char *imap_folder;
+
+static int
+git_imap_config(const char *key, const char *val)
+{
+       char imap_key[] = "imap.";
+
+       if (strncmp( key, imap_key, sizeof imap_key - 1 ))
+               return 0;
+       key += sizeof imap_key - 1;
+
+       if (!strcmp( "folder", key )) {
+               imap_folder = xstrdup( val );
+       } else if (!strcmp( "host", key )) {
+               {
+                       if (!prefixcmp(val, "imap:"))
+                               val += 5;
+                       if (!server.port)
+                               server.port = 143;
+               }
+               if (!prefixcmp(val, "//"))
+                       val += 2;
+               server.host = xstrdup( val );
+       }
+       else if (!strcmp( "user", key ))
+               server.user = xstrdup( val );
+       else if (!strcmp( "pass", key ))
+               server.pass = xstrdup( val );
+       else if (!strcmp( "port", key ))
+               server.port = git_config_int( key, val );
+       else if (!strcmp( "tunnel", key ))
+               server.tunnel = xstrdup( val );
+       return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+       msg_data_t all_msgs, msg;
+       store_t *ctx = NULL;
+       int uid = 0;
+       int ofs = 0;
+       int r;
+       int total, n = 0;
+
+       /* init the random number generator */
+       arc4_init();
+
+       git_config( git_imap_config );
+
+       if (!imap_folder) {
+               fprintf( stderr, "no imap store specified\n" );
+               return 1;
+       }
+
+       /* read the messages */
+       if (!read_message( stdin, &all_msgs )) {
+               fprintf(stderr,"nothing to send\n");
+               return 1;
+       }
+
+       total = count_messages( &all_msgs );
+       if (!total) {
+               fprintf(stderr,"no messages to send\n");
+               return 1;
+       }
+
+       /* write it to the imap server */
+       ctx = imap_open_store( &server );
+       if (!ctx) {
+               fprintf( stderr,"failed to open store\n");
+               return 1;
+       }
+
+       fprintf( stderr, "sending %d message%s\n", total, (total!=1)?"s":"" );
+       ctx->name = imap_folder;
+       while (1) {
+               unsigned percent = n * 100 / total;
+               fprintf( stderr, "%4u%% (%d/%d) done\r", percent, n, total );
+               if (!split_msg( &all_msgs, &msg, &ofs ))
+                       break;
+               r = imap_store_msg( ctx, &msg, &uid );
+               if (r != DRV_OK) break;
+               n++;
+       }
+       fprintf( stderr,"\n" );
+
+       imap_close_store( ctx );
+
+       return 0;
+}
diff --git a/index-pack.c b/index-pack.c
new file mode 100644 (file)
index 0000000..9fd6982
--- /dev/null
@@ -0,0 +1,835 @@
+#include "cache.h"
+#include "delta.h"
+#include "pack.h"
+#include "csum-file.h"
+#include "blob.h"
+#include "commit.h"
+#include "tag.h"
+#include "tree.h"
+#include "progress.h"
+
+static const char index_pack_usage[] =
+"git-index-pack [-v] [-o <index-file>] [{ ---keep | --keep=<msg> }] { <pack-file> | --stdin [--fix-thin] [<pack-file>] }";
+
+struct object_entry
+{
+       struct pack_idx_entry idx;
+       unsigned long size;
+       unsigned int hdr_size;
+       enum object_type type;
+       enum object_type real_type;
+};
+
+union delta_base {
+       unsigned char sha1[20];
+       off_t offset;
+};
+
+/*
+ * Even if sizeof(union delta_base) == 24 on 64-bit archs, we really want
+ * to memcmp() only the first 20 bytes.
+ */
+#define UNION_BASE_SZ  20
+
+struct delta_entry
+{
+       union delta_base base;
+       int obj_no;
+};
+
+static struct object_entry *objects;
+static struct delta_entry *deltas;
+static int nr_objects;
+static int nr_deltas;
+static int nr_resolved_deltas;
+
+static int from_stdin;
+static int verbose;
+
+static struct progress *progress;
+
+/* We always read in 4kB chunks. */
+static unsigned char input_buffer[4096];
+static unsigned int input_offset, input_len;
+static off_t consumed_bytes;
+static SHA_CTX input_ctx;
+static uint32_t input_crc32;
+static int input_fd, output_fd, pack_fd;
+
+/* Discard current buffer used content. */
+static void flush(void)
+{
+       if (input_offset) {
+               if (output_fd >= 0)
+                       write_or_die(output_fd, input_buffer, input_offset);
+               SHA1_Update(&input_ctx, input_buffer, input_offset);
+               memmove(input_buffer, input_buffer + input_offset, input_len);
+               input_offset = 0;
+       }
+}
+
+/*
+ * Make sure at least "min" bytes are available in the buffer, and
+ * return the pointer to the buffer.
+ */
+static void *fill(int min)
+{
+       if (min <= input_len)
+               return input_buffer + input_offset;
+       if (min > sizeof(input_buffer))
+               die("cannot fill %d bytes", min);
+       flush();
+       do {
+               ssize_t ret = xread(input_fd, input_buffer + input_len,
+                               sizeof(input_buffer) - input_len);
+               if (ret <= 0) {
+                       if (!ret)
+                               die("early EOF");
+                       die("read error on input: %s", strerror(errno));
+               }
+               input_len += ret;
+               if (from_stdin)
+                       display_throughput(progress, consumed_bytes + input_len);
+       } while (input_len < min);
+       return input_buffer;
+}
+
+static void use(int bytes)
+{
+       if (bytes > input_len)
+               die("used more bytes than were available");
+       input_crc32 = crc32(input_crc32, input_buffer + input_offset, bytes);
+       input_len -= bytes;
+       input_offset += bytes;
+
+       /* make sure off_t is sufficiently large not to wrap */
+       if (consumed_bytes > consumed_bytes + bytes)
+               die("pack too large for current definition of off_t");
+       consumed_bytes += bytes;
+}
+
+static char *open_pack_file(char *pack_name)
+{
+       if (from_stdin) {
+               input_fd = 0;
+               if (!pack_name) {
+                       static char tmpfile[PATH_MAX];
+                       snprintf(tmpfile, sizeof(tmpfile),
+                                "%s/tmp_pack_XXXXXX", get_object_directory());
+                       output_fd = xmkstemp(tmpfile);
+                       pack_name = xstrdup(tmpfile);
+               } else
+                       output_fd = open(pack_name, O_CREAT|O_EXCL|O_RDWR, 0600);
+               if (output_fd < 0)
+                       die("unable to create %s: %s\n", pack_name, strerror(errno));
+               pack_fd = output_fd;
+       } else {
+               input_fd = open(pack_name, O_RDONLY);
+               if (input_fd < 0)
+                       die("cannot open packfile '%s': %s",
+                           pack_name, strerror(errno));
+               output_fd = -1;
+               pack_fd = input_fd;
+       }
+       SHA1_Init(&input_ctx);
+       return pack_name;
+}
+
+static void parse_pack_header(void)
+{
+       struct pack_header *hdr = fill(sizeof(struct pack_header));
+
+       /* Header consistency check */
+       if (hdr->hdr_signature != htonl(PACK_SIGNATURE))
+               die("pack signature mismatch");
+       if (!pack_version_ok(hdr->hdr_version))
+               die("pack version %d unsupported", ntohl(hdr->hdr_version));
+
+       nr_objects = ntohl(hdr->hdr_entries);
+       use(sizeof(struct pack_header));
+}
+
+static void bad_object(unsigned long offset, const char *format,
+                      ...) NORETURN __attribute__((format (printf, 2, 3)));
+
+static void bad_object(unsigned long offset, const char *format, ...)
+{
+       va_list params;
+       char buf[1024];
+
+       va_start(params, format);
+       vsnprintf(buf, sizeof(buf), format, params);
+       va_end(params);
+       die("pack has bad object at offset %lu: %s", offset, buf);
+}
+
+static void *unpack_entry_data(unsigned long offset, unsigned long size)
+{
+       z_stream stream;
+       void *buf = xmalloc(size);
+
+       memset(&stream, 0, sizeof(stream));
+       stream.next_out = buf;
+       stream.avail_out = size;
+       stream.next_in = fill(1);
+       stream.avail_in = input_len;
+       inflateInit(&stream);
+
+       for (;;) {
+               int ret = inflate(&stream, 0);
+               use(input_len - stream.avail_in);
+               if (stream.total_out == size && ret == Z_STREAM_END)
+                       break;
+               if (ret != Z_OK)
+                       bad_object(offset, "inflate returned %d", ret);
+               stream.next_in = fill(1);
+               stream.avail_in = input_len;
+       }
+       inflateEnd(&stream);
+       return buf;
+}
+
+static void *unpack_raw_entry(struct object_entry *obj, union delta_base *delta_base)
+{
+       unsigned char *p, c;
+       unsigned long size;
+       off_t base_offset;
+       unsigned shift;
+       void *data;
+
+       obj->idx.offset = consumed_bytes;
+       input_crc32 = crc32(0, Z_NULL, 0);
+
+       p = fill(1);
+       c = *p;
+       use(1);
+       obj->type = (c >> 4) & 7;
+       size = (c & 15);
+       shift = 4;
+       while (c & 0x80) {
+               p = fill(1);
+               c = *p;
+               use(1);
+               size += (c & 0x7fUL) << shift;
+               shift += 7;
+       }
+       obj->size = size;
+
+       switch (obj->type) {
+       case OBJ_REF_DELTA:
+               hashcpy(delta_base->sha1, fill(20));
+               use(20);
+               break;
+       case OBJ_OFS_DELTA:
+               memset(delta_base, 0, sizeof(*delta_base));
+               p = fill(1);
+               c = *p;
+               use(1);
+               base_offset = c & 127;
+               while (c & 128) {
+                       base_offset += 1;
+                       if (!base_offset || MSB(base_offset, 7))
+                               bad_object(obj->idx.offset, "offset value overflow for delta base object");
+                       p = fill(1);
+                       c = *p;
+                       use(1);
+                       base_offset = (base_offset << 7) + (c & 127);
+               }
+               delta_base->offset = obj->idx.offset - base_offset;
+               if (delta_base->offset >= obj->idx.offset)
+                       bad_object(obj->idx.offset, "delta base offset is out of bound");
+               break;
+       case OBJ_COMMIT:
+       case OBJ_TREE:
+       case OBJ_BLOB:
+       case OBJ_TAG:
+               break;
+       default:
+               bad_object(obj->idx.offset, "unknown object type %d", obj->type);
+       }
+       obj->hdr_size = consumed_bytes - obj->idx.offset;
+
+       data = unpack_entry_data(obj->idx.offset, obj->size);
+       obj->idx.crc32 = input_crc32;
+       return data;
+}
+
+static void *get_data_from_pack(struct object_entry *obj)
+{
+       off_t from = obj[0].idx.offset + obj[0].hdr_size;
+       unsigned long len = obj[1].idx.offset - from;
+       unsigned long rdy = 0;
+       unsigned char *src, *data;
+       z_stream stream;
+       int st;
+
+       src = xmalloc(len);
+       data = src;
+       do {
+               ssize_t n = pread(pack_fd, data + rdy, len - rdy, from + rdy);
+               if (n <= 0)
+                       die("cannot pread pack file: %s", strerror(errno));
+               rdy += n;
+       } while (rdy < len);
+       data = xmalloc(obj->size);
+       memset(&stream, 0, sizeof(stream));
+       stream.next_out = data;
+       stream.avail_out = obj->size;
+       stream.next_in = src;
+       stream.avail_in = len;
+       inflateInit(&stream);
+       while ((st = inflate(&stream, Z_FINISH)) == Z_OK);
+       inflateEnd(&stream);
+       if (st != Z_STREAM_END || stream.total_out != obj->size)
+               die("serious inflate inconsistency");
+       free(src);
+       return data;
+}
+
+static int find_delta(const union delta_base *base)
+{
+       int first = 0, last = nr_deltas;
+
+        while (first < last) {
+                int next = (first + last) / 2;
+                struct delta_entry *delta = &deltas[next];
+                int cmp;
+
+                cmp = memcmp(base, &delta->base, UNION_BASE_SZ);
+                if (!cmp)
+                        return next;
+                if (cmp < 0) {
+                        last = next;
+                        continue;
+                }
+                first = next+1;
+        }
+        return -first-1;
+}
+
+static int find_delta_children(const union delta_base *base,
+                              int *first_index, int *last_index)
+{
+       int first = find_delta(base);
+       int last = first;
+       int end = nr_deltas - 1;
+
+       if (first < 0)
+               return -1;
+       while (first > 0 && !memcmp(&deltas[first - 1].base, base, UNION_BASE_SZ))
+               --first;
+       while (last < end && !memcmp(&deltas[last + 1].base, base, UNION_BASE_SZ))
+               ++last;
+       *first_index = first;
+       *last_index = last;
+       return 0;
+}
+
+static void sha1_object(const void *data, unsigned long size,
+                       enum object_type type, unsigned char *sha1)
+{
+       hash_sha1_file(data, size, typename(type), sha1);
+       if (has_sha1_file(sha1)) {
+               void *has_data;
+               enum object_type has_type;
+               unsigned long has_size;
+               has_data = read_sha1_file(sha1, &has_type, &has_size);
+               if (!has_data)
+                       die("cannot read existing object %s", sha1_to_hex(sha1));
+               if (size != has_size || type != has_type ||
+                   memcmp(data, has_data, size) != 0)
+                       die("SHA1 COLLISION FOUND WITH %s !", sha1_to_hex(sha1));
+               free(has_data);
+       }
+}
+
+static void resolve_delta(struct object_entry *delta_obj, void *base_data,
+                         unsigned long base_size, enum object_type type)
+{
+       void *delta_data;
+       unsigned long delta_size;
+       void *result;
+       unsigned long result_size;
+       union delta_base delta_base;
+       int j, first, last;
+
+       delta_obj->real_type = type;
+       delta_data = get_data_from_pack(delta_obj);
+       delta_size = delta_obj->size;
+       result = patch_delta(base_data, base_size, delta_data, delta_size,
+                            &result_size);
+       free(delta_data);
+       if (!result)
+               bad_object(delta_obj->idx.offset, "failed to apply delta");
+       sha1_object(result, result_size, type, delta_obj->idx.sha1);
+       nr_resolved_deltas++;
+
+       hashcpy(delta_base.sha1, delta_obj->idx.sha1);
+       if (!find_delta_children(&delta_base, &first, &last)) {
+               for (j = first; j <= last; j++) {
+                       struct object_entry *child = objects + deltas[j].obj_no;
+                       if (child->real_type == OBJ_REF_DELTA)
+                               resolve_delta(child, result, result_size, type);
+               }
+       }
+
+       memset(&delta_base, 0, sizeof(delta_base));
+       delta_base.offset = delta_obj->idx.offset;
+       if (!find_delta_children(&delta_base, &first, &last)) {
+               for (j = first; j <= last; j++) {
+                       struct object_entry *child = objects + deltas[j].obj_no;
+                       if (child->real_type == OBJ_OFS_DELTA)
+                               resolve_delta(child, result, result_size, type);
+               }
+       }
+
+       free(result);
+}
+
+static int compare_delta_entry(const void *a, const void *b)
+{
+       const struct delta_entry *delta_a = a;
+       const struct delta_entry *delta_b = b;
+       return memcmp(&delta_a->base, &delta_b->base, UNION_BASE_SZ);
+}
+
+/* Parse all objects and return the pack content SHA1 hash */
+static void parse_pack_objects(unsigned char *sha1)
+{
+       int i;
+       struct delta_entry *delta = deltas;
+       void *data;
+       struct stat st;
+
+       /*
+        * First pass:
+        * - find locations of all objects;
+        * - calculate SHA1 of all non-delta objects;
+        * - remember base (SHA1 or offset) for all deltas.
+        */
+       if (verbose)
+               progress = start_progress(
+                               from_stdin ? "Receiving objects" : "Indexing objects",
+                               nr_objects);
+       for (i = 0; i < nr_objects; i++) {
+               struct object_entry *obj = &objects[i];
+               data = unpack_raw_entry(obj, &delta->base);
+               obj->real_type = obj->type;
+               if (obj->type == OBJ_REF_DELTA || obj->type == OBJ_OFS_DELTA) {
+                       nr_deltas++;
+                       delta->obj_no = i;
+                       delta++;
+               } else
+                       sha1_object(data, obj->size, obj->type, obj->idx.sha1);
+               free(data);
+               display_progress(progress, i+1);
+       }
+       objects[i].idx.offset = consumed_bytes;
+       stop_progress(&progress);
+
+       /* Check pack integrity */
+       flush();
+       SHA1_Final(sha1, &input_ctx);
+       if (hashcmp(fill(20), sha1))
+               die("pack is corrupted (SHA1 mismatch)");
+       use(20);
+
+       /* If input_fd is a file, we should have reached its end now. */
+       if (fstat(input_fd, &st))
+               die("cannot fstat packfile: %s", strerror(errno));
+       if (S_ISREG(st.st_mode) &&
+                       lseek(input_fd, 0, SEEK_CUR) - input_len != st.st_size)
+               die("pack has junk at the end");
+
+       if (!nr_deltas)
+               return;
+
+       /* Sort deltas by base SHA1/offset for fast searching */
+       qsort(deltas, nr_deltas, sizeof(struct delta_entry),
+             compare_delta_entry);
+
+       /*
+        * Second pass:
+        * - for all non-delta objects, look if it is used as a base for
+        *   deltas;
+        * - if used as a base, uncompress the object and apply all deltas,
+        *   recursively checking if the resulting object is used as a base
+        *   for some more deltas.
+        */
+       if (verbose)
+               progress = start_progress("Resolving deltas", nr_deltas);
+       for (i = 0; i < nr_objects; i++) {
+               struct object_entry *obj = &objects[i];
+               union delta_base base;
+               int j, ref, ref_first, ref_last, ofs, ofs_first, ofs_last;
+
+               if (obj->type == OBJ_REF_DELTA || obj->type == OBJ_OFS_DELTA)
+                       continue;
+               hashcpy(base.sha1, obj->idx.sha1);
+               ref = !find_delta_children(&base, &ref_first, &ref_last);
+               memset(&base, 0, sizeof(base));
+               base.offset = obj->idx.offset;
+               ofs = !find_delta_children(&base, &ofs_first, &ofs_last);
+               if (!ref && !ofs)
+                       continue;
+               data = get_data_from_pack(obj);
+               if (ref)
+                       for (j = ref_first; j <= ref_last; j++) {
+                               struct object_entry *child = objects + deltas[j].obj_no;
+                               if (child->real_type == OBJ_REF_DELTA)
+                                       resolve_delta(child, data,
+                                                     obj->size, obj->type);
+                       }
+               if (ofs)
+                       for (j = ofs_first; j <= ofs_last; j++) {
+                               struct object_entry *child = objects + deltas[j].obj_no;
+                               if (child->real_type == OBJ_OFS_DELTA)
+                                       resolve_delta(child, data,
+                                                     obj->size, obj->type);
+                       }
+               free(data);
+               display_progress(progress, nr_resolved_deltas);
+       }
+}
+
+static int write_compressed(int fd, void *in, unsigned int size, uint32_t *obj_crc)
+{
+       z_stream stream;
+       unsigned long maxsize;
+       void *out;
+
+       memset(&stream, 0, sizeof(stream));
+       deflateInit(&stream, zlib_compression_level);
+       maxsize = deflateBound(&stream, size);
+       out = xmalloc(maxsize);
+
+       /* Compress it */
+       stream.next_in = in;
+       stream.avail_in = size;
+       stream.next_out = out;
+       stream.avail_out = maxsize;
+       while (deflate(&stream, Z_FINISH) == Z_OK);
+       deflateEnd(&stream);
+
+       size = stream.total_out;
+       write_or_die(fd, out, size);
+       *obj_crc = crc32(*obj_crc, out, size);
+       free(out);
+       return size;
+}
+
+static void append_obj_to_pack(const unsigned char *sha1, void *buf,
+                              unsigned long size, enum object_type type)
+{
+       struct object_entry *obj = &objects[nr_objects++];
+       unsigned char header[10];
+       unsigned long s = size;
+       int n = 0;
+       unsigned char c = (type << 4) | (s & 15);
+       s >>= 4;
+       while (s) {
+               header[n++] = c | 0x80;
+               c = s & 0x7f;
+               s >>= 7;
+       }
+       header[n++] = c;
+       write_or_die(output_fd, header, n);
+       obj[0].idx.crc32 = crc32(0, Z_NULL, 0);
+       obj[0].idx.crc32 = crc32(obj[0].idx.crc32, header, n);
+       obj[1].idx.offset = obj[0].idx.offset + n;
+       obj[1].idx.offset += write_compressed(output_fd, buf, size, &obj[0].idx.crc32);
+       hashcpy(obj->idx.sha1, sha1);
+}
+
+static int delta_pos_compare(const void *_a, const void *_b)
+{
+       struct delta_entry *a = *(struct delta_entry **)_a;
+       struct delta_entry *b = *(struct delta_entry **)_b;
+       return a->obj_no - b->obj_no;
+}
+
+static void fix_unresolved_deltas(int nr_unresolved)
+{
+       struct delta_entry **sorted_by_pos;
+       int i, n = 0;
+
+       /*
+        * Since many unresolved deltas may well be themselves base objects
+        * for more unresolved deltas, we really want to include the
+        * smallest number of base objects that would cover as much delta
+        * as possible by picking the
+        * trunc deltas first, allowing for other deltas to resolve without
+        * additional base objects.  Since most base objects are to be found
+        * before deltas depending on them, a good heuristic is to start
+        * resolving deltas in the same order as their position in the pack.
+        */
+       sorted_by_pos = xmalloc(nr_unresolved * sizeof(*sorted_by_pos));
+       for (i = 0; i < nr_deltas; i++) {
+               if (objects[deltas[i].obj_no].real_type != OBJ_REF_DELTA)
+                       continue;
+               sorted_by_pos[n++] = &deltas[i];
+       }
+       qsort(sorted_by_pos, n, sizeof(*sorted_by_pos), delta_pos_compare);
+
+       for (i = 0; i < n; i++) {
+               struct delta_entry *d = sorted_by_pos[i];
+               void *data;
+               unsigned long size;
+               enum object_type type;
+               int j, first, last;
+
+               if (objects[d->obj_no].real_type != OBJ_REF_DELTA)
+                       continue;
+               data = read_sha1_file(d->base.sha1, &type, &size);
+               if (!data)
+                       continue;
+
+               find_delta_children(&d->base, &first, &last);
+               for (j = first; j <= last; j++) {
+                       struct object_entry *child = objects + deltas[j].obj_no;
+                       if (child->real_type == OBJ_REF_DELTA)
+                               resolve_delta(child, data, size, type);
+               }
+
+               if (check_sha1_signature(d->base.sha1, data, size, typename(type)))
+                       die("local object %s is corrupt", sha1_to_hex(d->base.sha1));
+               append_obj_to_pack(d->base.sha1, data, size, type);
+               free(data);
+               display_progress(progress, nr_resolved_deltas);
+       }
+       free(sorted_by_pos);
+}
+
+static void final(const char *final_pack_name, const char *curr_pack_name,
+                 const char *final_index_name, const char *curr_index_name,
+                 const char *keep_name, const char *keep_msg,
+                 unsigned char *sha1)
+{
+       const char *report = "pack";
+       char name[PATH_MAX];
+       int err;
+
+       if (!from_stdin) {
+               close(input_fd);
+       } else {
+               err = close(output_fd);
+               if (err)
+                       die("error while closing pack file: %s", strerror(errno));
+               chmod(curr_pack_name, 0444);
+       }
+
+       if (keep_msg) {
+               int keep_fd, keep_msg_len = strlen(keep_msg);
+               if (!keep_name) {
+                       snprintf(name, sizeof(name), "%s/pack/pack-%s.keep",
+                                get_object_directory(), sha1_to_hex(sha1));
+                       keep_name = name;
+               }
+               keep_fd = open(keep_name, O_RDWR|O_CREAT|O_EXCL, 0600);
+               if (keep_fd < 0) {
+                       if (errno != EEXIST)
+                               die("cannot write keep file");
+               } else {
+                       if (keep_msg_len > 0) {
+                               write_or_die(keep_fd, keep_msg, keep_msg_len);
+                               write_or_die(keep_fd, "\n", 1);
+                       }
+                       if (close(keep_fd) != 0)
+                               die("cannot write keep file");
+                       report = "keep";
+               }
+       }
+
+       if (final_pack_name != curr_pack_name) {
+               if (!final_pack_name) {
+                       snprintf(name, sizeof(name), "%s/pack/pack-%s.pack",
+                                get_object_directory(), sha1_to_hex(sha1));
+                       final_pack_name = name;
+               }
+               if (move_temp_to_file(curr_pack_name, final_pack_name))
+                       die("cannot store pack file");
+       }
+
+       chmod(curr_index_name, 0444);
+       if (final_index_name != curr_index_name) {
+               if (!final_index_name) {
+                       snprintf(name, sizeof(name), "%s/pack/pack-%s.idx",
+                                get_object_directory(), sha1_to_hex(sha1));
+                       final_index_name = name;
+               }
+               if (move_temp_to_file(curr_index_name, final_index_name))
+                       die("cannot store index file");
+       }
+
+       if (!from_stdin) {
+               printf("%s\n", sha1_to_hex(sha1));
+       } else {
+               char buf[48];
+               int len = snprintf(buf, sizeof(buf), "%s\t%s\n",
+                                  report, sha1_to_hex(sha1));
+               write_or_die(1, buf, len);
+
+               /*
+                * Let's just mimic git-unpack-objects here and write
+                * the last part of the input buffer to stdout.
+                */
+               while (input_len) {
+                       err = xwrite(1, input_buffer + input_offset, input_len);
+                       if (err <= 0)
+                               break;
+                       input_len -= err;
+                       input_offset += err;
+               }
+       }
+}
+
+static int git_index_pack_config(const char *k, const char *v)
+{
+       if (!strcmp(k, "pack.indexversion")) {
+               pack_idx_default_version = git_config_int(k, v);
+               if (pack_idx_default_version > 2)
+                       die("bad pack.indexversion=%d", pack_idx_default_version);
+               return 0;
+       }
+       return git_default_config(k, v);
+}
+
+int main(int argc, char **argv)
+{
+       int i, fix_thin_pack = 0;
+       char *curr_pack, *pack_name = NULL;
+       char *curr_index, *index_name = NULL;
+       const char *keep_name = NULL, *keep_msg = NULL;
+       char *index_name_buf = NULL, *keep_name_buf = NULL;
+       struct pack_idx_entry **idx_objects;
+       unsigned char sha1[20];
+
+       git_config(git_index_pack_config);
+
+       for (i = 1; i < argc; i++) {
+               char *arg = argv[i];
+
+               if (*arg == '-') {
+                       if (!strcmp(arg, "--stdin")) {
+                               from_stdin = 1;
+                       } else if (!strcmp(arg, "--fix-thin")) {
+                               fix_thin_pack = 1;
+                       } else if (!strcmp(arg, "--keep")) {
+                               keep_msg = "";
+                       } else if (!prefixcmp(arg, "--keep=")) {
+                               keep_msg = arg + 7;
+                       } else if (!prefixcmp(arg, "--pack_header=")) {
+                               struct pack_header *hdr;
+                               char *c;
+
+                               hdr = (struct pack_header *)input_buffer;
+                               hdr->hdr_signature = htonl(PACK_SIGNATURE);
+                               hdr->hdr_version = htonl(strtoul(arg + 14, &c, 10));
+                               if (*c != ',')
+                                       die("bad %s", arg);
+                               hdr->hdr_entries = htonl(strtoul(c + 1, &c, 10));
+                               if (*c)
+                                       die("bad %s", arg);
+                               input_len = sizeof(*hdr);
+                       } else if (!strcmp(arg, "-v")) {
+                               verbose = 1;
+                       } else if (!strcmp(arg, "-o")) {
+                               if (index_name || (i+1) >= argc)
+                                       usage(index_pack_usage);
+                               index_name = argv[++i];
+                       } else if (!prefixcmp(arg, "--index-version=")) {
+                               char *c;
+                               pack_idx_default_version = strtoul(arg + 16, &c, 10);
+                               if (pack_idx_default_version > 2)
+                                       die("bad %s", arg);
+                               if (*c == ',')
+                                       pack_idx_off32_limit = strtoul(c+1, &c, 0);
+                               if (*c || pack_idx_off32_limit & 0x80000000)
+                                       die("bad %s", arg);
+                       } else
+                               usage(index_pack_usage);
+                       continue;
+               }
+
+               if (pack_name)
+                       usage(index_pack_usage);
+               pack_name = arg;
+       }
+
+       if (!pack_name && !from_stdin)
+               usage(index_pack_usage);
+       if (fix_thin_pack && !from_stdin)
+               die("--fix-thin cannot be used without --stdin");
+       if (!index_name && pack_name) {
+               int len = strlen(pack_name);
+               if (!has_extension(pack_name, ".pack"))
+                       die("packfile name '%s' does not end with '.pack'",
+                           pack_name);
+               index_name_buf = xmalloc(len);
+               memcpy(index_name_buf, pack_name, len - 5);
+               strcpy(index_name_buf + len - 5, ".idx");
+               index_name = index_name_buf;
+       }
+       if (keep_msg && !keep_name && pack_name) {
+               int len = strlen(pack_name);
+               if (!has_extension(pack_name, ".pack"))
+                       die("packfile name '%s' does not end with '.pack'",
+                           pack_name);
+               keep_name_buf = xmalloc(len);
+               memcpy(keep_name_buf, pack_name, len - 5);
+               strcpy(keep_name_buf + len - 5, ".keep");
+               keep_name = keep_name_buf;
+       }
+
+       curr_pack = open_pack_file(pack_name);
+       parse_pack_header();
+       objects = xmalloc((nr_objects + 1) * sizeof(struct object_entry));
+       deltas = xmalloc(nr_objects * sizeof(struct delta_entry));
+       parse_pack_objects(sha1);
+       if (nr_deltas == nr_resolved_deltas) {
+               stop_progress(&progress);
+               /* Flush remaining pack final 20-byte SHA1. */
+               flush();
+       } else {
+               if (fix_thin_pack) {
+                       char msg[48];
+                       int nr_unresolved = nr_deltas - nr_resolved_deltas;
+                       int nr_objects_initial = nr_objects;
+                       if (nr_unresolved <= 0)
+                               die("confusion beyond insanity");
+                       objects = xrealloc(objects,
+                                          (nr_objects + nr_unresolved + 1)
+                                          * sizeof(*objects));
+                       fix_unresolved_deltas(nr_unresolved);
+                       sprintf(msg, "completed with %d local objects",
+                               nr_objects - nr_objects_initial);
+                       stop_progress_msg(&progress, msg);
+                       fixup_pack_header_footer(output_fd, sha1,
+                                                curr_pack, nr_objects);
+               }
+               if (nr_deltas != nr_resolved_deltas)
+                       die("pack has %d unresolved deltas",
+                           nr_deltas - nr_resolved_deltas);
+       }
+       free(deltas);
+
+       idx_objects = xmalloc((nr_objects) * sizeof(struct pack_idx_entry *));
+       for (i = 0; i < nr_objects; i++)
+               idx_objects[i] = &objects[i].idx;
+       curr_index = write_idx_file(index_name, idx_objects, nr_objects, sha1);
+       free(idx_objects);
+
+       final(pack_name, curr_pack,
+               index_name, curr_index,
+               keep_name, keep_msg,
+               sha1);
+       free(objects);
+       free(index_name_buf);
+       free(keep_name_buf);
+       if (pack_name == NULL)
+               free(curr_pack);
+       if (index_name == NULL)
+               free(curr_index);
+
+       return 0;
+}
diff --git a/interpolate.c b/interpolate.c
new file mode 100644 (file)
index 0000000..6ef53f2
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2006 Jon Loeliger
+ */
+
+#include "git-compat-util.h"
+#include "interpolate.h"
+
+
+void interp_set_entry(struct interp *table, int slot, const char *value)
+{
+       char *oldval = table[slot].value;
+       char *newval = NULL;
+
+       if (oldval)
+               free(oldval);
+
+       if (value)
+               newval = xstrdup(value);
+
+       table[slot].value = newval;
+}
+
+
+void interp_clear_table(struct interp *table, int ninterps)
+{
+       int i;
+
+       for (i = 0; i < ninterps; i++) {
+               interp_set_entry(table, i, NULL);
+       }
+}
+
+
+/*
+ * Convert a NUL-terminated string in buffer orig
+ * into the supplied buffer, result, whose length is reslen,
+ * performing substitutions on %-named sub-strings from
+ * the table, interps, with ninterps entries.
+ *
+ * Example interps:
+ *    {
+ *        { "%H", "example.org"},
+ *        { "%port", "123"},
+ *        { "%%", "%"},
+ *    }
+ *
+ * Returns the length of the substituted string (not including the final \0).
+ * Like with snprintf, if the result is >= reslen, then it overflowed.
+ */
+
+unsigned long interpolate(char *result, unsigned long reslen,
+               const char *orig,
+               const struct interp *interps, int ninterps)
+{
+       const char *src = orig;
+       char *dest = result;
+       unsigned long newlen = 0;
+       const char *name, *value;
+       unsigned long namelen, valuelen;
+       int i;
+       char c;
+
+       while ((c = *src)) {
+               if (c == '%') {
+                       /* Try to match an interpolation string. */
+                       for (i = 0; i < ninterps; i++) {
+                               name = interps[i].name;
+                               namelen = strlen(name);
+                               if (strncmp(src, name, namelen) == 0)
+                                       break;
+                       }
+
+                       /* Check for valid interpolation. */
+                       if (i < ninterps) {
+                               value = interps[i].value;
+                               if (!value) {
+                                       src += namelen;
+                                       continue;
+                               }
+
+                               valuelen = strlen(value);
+                               if (newlen + valuelen < reslen) {
+                                       /* Substitute. */
+                                       memcpy(dest, value, valuelen);
+                                       dest += valuelen;
+                               }
+                               newlen += valuelen;
+                               src += namelen;
+                               continue;
+                       }
+               }
+               /* Straight copy one non-interpolation character. */
+               if (newlen + 1 < reslen)
+                       *dest++ = *src;
+               src++;
+               newlen++;
+       }
+
+       /* XXX: the previous loop always keep room for the ending NUL,
+          we just need to check if there was room for a NUL in the first place */
+       if (reslen > 0)
+               *dest = '\0';
+       return newlen;
+}
diff --git a/interpolate.h b/interpolate.h
new file mode 100644 (file)
index 0000000..77407e6
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2006 Jon Loeliger
+ */
+
+#ifndef INTERPOLATE_H
+#define INTERPOLATE_H
+
+/*
+ * Convert a NUL-terminated string in buffer orig,
+ * performing substitutions on %-named sub-strings from
+ * the interpretation table.
+ */
+
+struct interp {
+       const char *name;
+       char *value;
+};
+
+extern void interp_set_entry(struct interp *table, int slot, const char *value);
+extern void interp_clear_table(struct interp *table, int ninterps);
+
+extern unsigned long interpolate(char *result, unsigned long reslen,
+                                const char *orig,
+                                const struct interp *interps, int ninterps);
+
+#endif /* INTERPOLATE_H */
diff --git a/lib/about.tcl b/lib/about.tcl
deleted file mode 100644 (file)
index 719fc54..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-# git-gui about git-gui dialog
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc do_about {} {
-       global appvers copyright oguilib
-       global tcl_patchLevel tk_patchLevel
-
-       set w .about_dialog
-       toplevel $w
-       wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
-
-       pack [git_logo $w.git_logo] -side left -fill y -padx 10 -pady 10
-       label $w.header -text [mc "About %s" [appname]] \
-               -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       button $w.buttons.close -text {Close} \
-               -default active \
-               -command [list destroy $w]
-       pack $w.buttons.close -side right
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       label $w.desc \
-               -text "[mc "git-gui - a graphical user interface for Git."]\n$copyright" \
-               -padx 5 -pady 5 \
-               -justify left \
-               -anchor w \
-               -borderwidth 1 \
-               -relief solid
-       pack $w.desc -side top -fill x -padx 5 -pady 5
-
-       set v {}
-       append v "git-gui version $appvers\n"
-       append v "[git version]\n"
-       append v "\n"
-       if {$tcl_patchLevel eq $tk_patchLevel} {
-               append v "Tcl/Tk version $tcl_patchLevel"
-       } else {
-               append v "Tcl version $tcl_patchLevel"
-               append v ", Tk version $tk_patchLevel"
-       }
-
-       set d {}
-       append d "git wrapper: $::_git\n"
-       append d "git exec dir: [gitexec]\n"
-       append d "git-gui lib: $oguilib"
-
-       label $w.vers \
-               -text $v \
-               -padx 5 -pady 5 \
-               -justify left \
-               -anchor w \
-               -borderwidth 1 \
-               -relief solid
-       pack $w.vers -side top -fill x -padx 5 -pady 5
-
-       label $w.dirs \
-               -text $d \
-               -padx 5 -pady 5 \
-               -justify left \
-               -anchor w \
-               -borderwidth 1 \
-               -relief solid
-       pack $w.dirs -side top -fill x -padx 5 -pady 5
-
-       menu $w.ctxm -tearoff 0
-       $w.ctxm add command \
-               -label {Copy} \
-               -command "
-               clipboard clear
-               clipboard append -format STRING -type STRING -- \[$w.vers cget -text\]
-       "
-
-       bind $w <Visibility> "grab $w; focus $w.buttons.close"
-       bind $w <Key-Escape> "destroy $w"
-       bind $w <Key-Return> "destroy $w"
-       bind_button3 $w.vers "tk_popup $w.ctxm %X %Y; grab $w; focus $w"
-       wm title $w "About [appname]"
-       tkwait window $w
-}
diff --git a/lib/blame.tcl b/lib/blame.tcl
deleted file mode 100644 (file)
index 00ecf21..0000000
+++ /dev/null
@@ -1,980 +0,0 @@
-# git-gui blame viewer
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-class blame {
-
-image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
-
-# Persistant data (survives loads)
-#
-field history {}; # viewer history: {commit path}
-field header    ; # array commit,key -> header field
-
-# Tk UI control paths
-#
-field w          ; # top window in this viewer
-field w_back     ; # our back button
-field w_path     ; # label showing the current file path
-field w_columns  ; # list of all column widgets in the viewer
-field w_line     ; # text column: all line numbers
-field w_amov     ; # text column: annotations + move tracking
-field w_asim     ; # text column: annotations (simple computation)
-field w_file     ; # text column: actual file data
-field w_cviewer  ; # pane showing commit message
-field status     ; # status mega-widget instance
-field old_height ; # last known height of $w.file_pane
-
-# Tk UI colors
-#
-variable active_color #c0edc5
-variable group_colors {
-       #d6d6d6
-       #e1e1e1
-       #ececec
-}
-
-# Switches for original location detection
-#
-variable original_options [list -C -C]
-if {[git-version >= 1.5.3]} {
-       lappend original_options -w ; # ignore indentation changes
-}
-
-# Current blame data; cleared/reset on each load
-#
-field commit               ; # input commit to blame
-field path                 ; # input filename to view in $commit
-
-field current_fd        {} ; # background process running
-field highlight_line    -1 ; # current line selected
-field highlight_column  {} ; # current commit column selected
-field highlight_commit  {} ; # sha1 of commit selected
-
-field total_lines       0  ; # total length of file
-field blame_lines       0  ; # number of lines computed
-field amov_data            ; # list of {commit origfile origline}
-field asim_data            ; # list of {commit origfile origline}
-
-field r_commit             ; # commit currently being parsed
-field r_orig_line          ; # original line number
-field r_final_line         ; # final line number
-field r_line_count         ; # lines in this region
-
-field tooltip_wm        {} ; # Current tooltip toplevel, if open
-field tooltip_t         {} ; # Text widget in $tooltip_wm
-field tooltip_timer     {} ; # Current timer event for our tooltip
-field tooltip_commit    {} ; # Commit(s) in tooltip
-
-constructor new {i_commit i_path} {
-       global cursor_ptr
-       variable active_color
-       variable group_colors
-
-       set commit $i_commit
-       set path   $i_path
-
-       make_toplevel top w
-       wm title $top [append "[appname] ([reponame]): " [mc "File Viewer"]]
-
-       frame $w.header -background gold
-       label $w.header.commit_l \
-               -text [mc "Commit:"] \
-               -background gold \
-               -anchor w \
-               -justify left
-       set w_back $w.header.commit_b
-       label $w_back \
-               -image ::blame::img_back_arrow \
-               -borderwidth 0 \
-               -relief flat \
-               -state disabled \
-               -background gold \
-               -activebackground gold
-       bind $w_back <Button-1> "
-               if {\[$w_back cget -state\] eq {normal}} {
-                       [cb _history_menu]
-               }
-               "
-       label $w.header.commit \
-               -textvariable @commit \
-               -background gold \
-               -anchor w \
-               -justify left
-       label $w.header.path_l \
-               -text [mc "File:"] \
-               -background gold \
-               -anchor w \
-               -justify left
-       set w_path $w.header.path
-       label $w_path \
-               -background gold \
-               -anchor w \
-               -justify left
-       pack $w.header.commit_l -side left
-       pack $w_back -side left
-       pack $w.header.commit -side left
-       pack $w_path -fill x -side right
-       pack $w.header.path_l -side right
-
-       panedwindow $w.file_pane -orient vertical
-       frame $w.file_pane.out
-       frame $w.file_pane.cm
-       $w.file_pane add $w.file_pane.out \
-               -sticky nsew \
-               -minsize 100 \
-               -height 100 \
-               -width 100
-       $w.file_pane add $w.file_pane.cm \
-               -sticky nsew \
-               -minsize 25 \
-               -height 25 \
-               -width 100
-
-       set w_line $w.file_pane.out.linenumber_t
-       text $w_line \
-               -takefocus 0 \
-               -highlightthickness 0 \
-               -padx 0 -pady 0 \
-               -background white -borderwidth 0 \
-               -state disabled \
-               -wrap none \
-               -height 40 \
-               -width 6 \
-               -font font_diff
-       $w_line tag conf linenumber -justify right -rmargin 5
-
-       set w_amov $w.file_pane.out.amove_t
-       text $w_amov \
-               -takefocus 0 \
-               -highlightthickness 0 \
-               -padx 0 -pady 0 \
-               -background white -borderwidth 0 \
-               -state disabled \
-               -wrap none \
-               -height 40 \
-               -width 5 \
-               -font font_diff
-       $w_amov tag conf author_abbr -justify right -rmargin 5
-       $w_amov tag conf curr_commit
-       $w_amov tag conf prior_commit -foreground blue -underline 1
-       $w_amov tag bind prior_commit \
-               <Button-1> \
-               "[cb _load_commit $w_amov @amov_data @%x,%y];break"
-
-       set w_asim $w.file_pane.out.asimple_t
-       text $w_asim \
-               -takefocus 0 \
-               -highlightthickness 0 \
-               -padx 0 -pady 0 \
-               -background white -borderwidth 0 \
-               -state disabled \
-               -wrap none \
-               -height 40 \
-               -width 4 \
-               -font font_diff
-       $w_asim tag conf author_abbr -justify right
-       $w_asim tag conf curr_commit
-       $w_asim tag conf prior_commit -foreground blue -underline 1
-       $w_asim tag bind prior_commit \
-               <Button-1> \
-               "[cb _load_commit $w_asim @asim_data @%x,%y];break"
-
-       set w_file $w.file_pane.out.file_t
-       text $w_file \
-               -takefocus 0 \
-               -highlightthickness 0 \
-               -padx 0 -pady 0 \
-               -background white -borderwidth 0 \
-               -state disabled \
-               -wrap none \
-               -height 40 \
-               -width 80 \
-               -xscrollcommand [list $w.file_pane.out.sbx set] \
-               -font font_diff
-
-       set w_columns [list $w_amov $w_asim $w_line $w_file]
-
-       scrollbar $w.file_pane.out.sbx \
-               -orient h \
-               -command [list $w_file xview]
-       scrollbar $w.file_pane.out.sby \
-               -orient v \
-               -command [list scrollbar2many $w_columns yview]
-       eval grid $w_columns $w.file_pane.out.sby -sticky nsew
-       grid conf \
-               $w.file_pane.out.sbx \
-               -column [expr {[llength $w_columns] - 1}] \
-               -sticky we
-       grid columnconfigure \
-               $w.file_pane.out \
-               [expr {[llength $w_columns] - 1}] \
-               -weight 1
-       grid rowconfigure $w.file_pane.out 0 -weight 1
-
-       set w_cviewer $w.file_pane.cm.t
-       text $w_cviewer \
-               -background white -borderwidth 0 \
-               -state disabled \
-               -wrap none \
-               -height 10 \
-               -width 80 \
-               -xscrollcommand [list $w.file_pane.cm.sbx set] \
-               -yscrollcommand [list $w.file_pane.cm.sby set] \
-               -font font_diff
-       $w_cviewer tag conf still_loading \
-               -font font_uiitalic \
-               -justify center
-       $w_cviewer tag conf header_key \
-               -tabs {3c} \
-               -background $active_color \
-               -font font_uibold
-       $w_cviewer tag conf header_val \
-               -background $active_color \
-               -font font_ui
-       $w_cviewer tag raise sel
-       scrollbar $w.file_pane.cm.sbx \
-               -orient h \
-               -command [list $w_cviewer xview]
-       scrollbar $w.file_pane.cm.sby \
-               -orient v \
-               -command [list $w_cviewer yview]
-       pack $w.file_pane.cm.sby -side right -fill y
-       pack $w.file_pane.cm.sbx -side bottom -fill x
-       pack $w_cviewer -expand 1 -fill both
-
-       set status [::status_bar::new $w.status]
-
-       menu $w.ctxm -tearoff 0
-       $w.ctxm add command \
-               -label [mc "Copy Commit"] \
-               -command [cb _copycommit]
-
-       foreach i $w_columns {
-               for {set g 0} {$g < [llength $group_colors]} {incr g} {
-                       $i tag conf color$g -background [lindex $group_colors $g]
-               }
-
-               $i conf -cursor $cursor_ptr
-               $i conf -yscrollcommand [list many2scrollbar \
-                       $w_columns yview $w.file_pane.out.sby]
-               bind $i <Button-1> "
-                       [cb _hide_tooltip]
-                       [cb _click $i @%x,%y]
-                       focus $i
-               "
-               bind $i <Any-Motion>  [cb _show_tooltip $i @%x,%y]
-               bind $i <Any-Enter>   [cb _hide_tooltip]
-               bind $i <Any-Leave>   [cb _hide_tooltip]
-               bind_button3 $i "
-                       [cb _hide_tooltip]
-                       set cursorX %x
-                       set cursorY %y
-                       set cursorW %W
-                       tk_popup $w.ctxm %X %Y
-               "
-               bind $i <Shift-Tab> "[list focus $w_cviewer];break"
-               bind $i <Tab>       "[list focus $w_cviewer];break"
-       }
-
-       foreach i [concat $w_columns $w_cviewer] {
-               bind $i <Key-Up>        {catch {%W yview scroll -1 units};break}
-               bind $i <Key-Down>      {catch {%W yview scroll  1 units};break}
-               bind $i <Key-Left>      {catch {%W xview scroll -1 units};break}
-               bind $i <Key-Right>     {catch {%W xview scroll  1 units};break}
-               bind $i <Key-k>         {catch {%W yview scroll -1 units};break}
-               bind $i <Key-j>         {catch {%W yview scroll  1 units};break}
-               bind $i <Key-h>         {catch {%W xview scroll -1 units};break}
-               bind $i <Key-l>         {catch {%W xview scroll  1 units};break}
-               bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break}
-               bind $i <Control-Key-f> {catch {%W yview scroll  1 pages};break}
-       }
-
-       bind $w_cviewer <Shift-Tab> "[list focus $w_file];break"
-       bind $w_cviewer <Tab>       "[list focus $w_file];break"
-       bind $w_cviewer <Button-1> [list focus $w_cviewer]
-       bind $w_file    <Visibility> [list focus $w_file]
-
-       grid configure $w.header -sticky ew
-       grid configure $w.file_pane -sticky nsew
-       grid configure $w.status -sticky ew
-       grid columnconfigure $top 0 -weight 1
-       grid rowconfigure $top 0 -weight 0
-       grid rowconfigure $top 1 -weight 1
-       grid rowconfigure $top 2 -weight 0
-
-       set req_w [winfo reqwidth  $top]
-       set req_h [winfo reqheight $top]
-       set scr_h [expr {[winfo screenheight $top] - 100}]
-       if {$req_w < 600} {set req_w 600}
-       if {$req_h < $scr_h} {set req_h $scr_h}
-       set g "${req_w}x${req_h}"
-       wm geometry $top $g
-       update
-
-       set old_height [winfo height $w.file_pane]
-       $w.file_pane sash place 0 \
-               [lindex [$w.file_pane sash coord 0] 0] \
-               [expr {int($old_height * 0.70)}]
-       bind $w.file_pane <Configure> \
-       "if {{$w.file_pane} eq {%W}} {[cb _resize %h]}"
-
-       _load $this {}
-}
-
-method _load {jump} {
-       variable group_colors
-
-       _hide_tooltip $this
-
-       if {$total_lines != 0 || $current_fd ne {}} {
-               if {$current_fd ne {}} {
-                       catch {close $current_fd}
-                       set current_fd {}
-               }
-
-               foreach i $w_columns {
-                       $i conf -state normal
-                       $i delete 0.0 end
-                       foreach g [$i tag names] {
-                               if {[regexp {^g[0-9a-f]{40}$} $g]} {
-                                       $i tag delete $g
-                               }
-                       }
-                       $i conf -state disabled
-               }
-
-               $w_cviewer conf -state normal
-               $w_cviewer delete 0.0 end
-               $w_cviewer conf -state disabled
-
-               set highlight_line -1
-               set highlight_column {}
-               set highlight_commit {}
-               set total_lines 0
-       }
-
-       if {$history eq {}} {
-               $w_back conf -state disabled
-       } else {
-               $w_back conf -state normal
-       }
-
-       # Index 0 is always empty.  There is never line 0 as
-       # we use only 1 based lines, as that matches both with
-       # git-blame output and with Tk's text widget.
-       #
-       set amov_data [list [list]]
-       set asim_data [list [list]]
-
-       $status show [mc "Reading %s..." "$commit:[escape_path $path]"]
-       $w_path conf -text [escape_path $path]
-       if {$commit eq {}} {
-               set fd [open $path r]
-               fconfigure $fd -eofchar {}
-       } else {
-               set fd [git_read cat-file blob "$commit:$path"]
-       }
-       fconfigure $fd -blocking 0 -translation lf -encoding binary
-       fileevent $fd readable [cb _read_file $fd $jump]
-       set current_fd $fd
-}
-
-method _history_menu {} {
-       set m $w.backmenu
-       if {[winfo exists $m]} {
-               $m delete 0 end
-       } else {
-               menu $m -tearoff 0
-       }
-
-       for {set i [expr {[llength $history] - 1}]
-               } {$i >= 0} {incr i -1} {
-               set e [lindex $history $i]
-               set c [lindex $e 0]
-               set f [lindex $e 1]
-
-               if {[regexp {^[0-9a-f]{40}$} $c]} {
-                       set t [string range $c 0 8]...
-               } elseif {$c eq {}} {
-                       set t {Working Directory}
-               } else {
-                       set t $c
-               }
-               if {![catch {set summary $header($c,summary)}]} {
-                       append t " $summary"
-                       if {[string length $t] > 70} {
-                               set t [string range $t 0 66]...
-                       }
-               }
-
-               $m add command -label $t -command [cb _goback $i]
-       }
-       set X [winfo rootx $w_back]
-       set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}]
-       tk_popup $m $X $Y
-}
-
-method _goback {i} {
-       set dat [lindex $history $i]
-       set history [lrange $history 0 [expr {$i - 1}]]
-       set commit [lindex $dat 0]
-       set path [lindex $dat 1]
-       _load $this [lrange $dat 2 5]
-}
-
-method _read_file {fd jump} {
-       if {$fd ne $current_fd} {
-               catch {close $fd}
-               return
-       }
-
-       foreach i $w_columns {$i conf -state normal}
-       while {[gets $fd line] >= 0} {
-               regsub "\r\$" $line {} line
-               incr total_lines
-               lappend amov_data {}
-               lappend asim_data {}
-
-               if {$total_lines > 1} {
-                       foreach i $w_columns {$i insert end "\n"}
-               }
-
-               $w_line insert end "$total_lines" linenumber
-               $w_file insert end "$line"
-       }
-
-       set ln_wc [expr {[string length $total_lines] + 2}]
-       if {[$w_line cget -width] < $ln_wc} {
-               $w_line conf -width $ln_wc
-       }
-
-       foreach i $w_columns {$i conf -state disabled}
-
-       if {[eof $fd]} {
-               close $fd
-
-               # If we don't force Tk to update the widgets *right now*
-               # none of our jump commands will cause a change in the UI.
-               #
-               update
-
-               if {[llength $jump] == 1} {
-                       set highlight_line [lindex $jump 0]
-                       $w_file see "$highlight_line.0"
-               } elseif {[llength $jump] == 4} {
-                       set highlight_column [lindex $jump 0]
-                       set highlight_line [lindex $jump 1]
-                       $w_file xview moveto [lindex $jump 2]
-                       $w_file yview moveto [lindex $jump 3]
-               }
-
-               _exec_blame $this $w_asim @asim_data \
-                       [list] \
-                       [mc "Loading copy/move tracking annotations..."]
-       }
-} ifdeleted { catch {close $fd} }
-
-method _exec_blame {cur_w cur_d options cur_s} {
-       lappend options --incremental
-       if {$commit eq {}} {
-               lappend options --contents $path
-       } else {
-               lappend options $commit
-       }
-       lappend options -- $path
-       set fd [eval git_read --nice blame $options]
-       fconfigure $fd -blocking 0 -translation lf -encoding binary
-       fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]
-       set current_fd $fd
-       set blame_lines 0
-
-       $status start \
-               $cur_s \
-               [mc "lines annotated"]
-}
-
-method _read_blame {fd cur_w cur_d} {
-       upvar #0 $cur_d line_data
-       variable group_colors
-       variable original_options
-
-       if {$fd ne $current_fd} {
-               catch {close $fd}
-               return
-       }
-
-       $cur_w conf -state normal
-       while {[gets $fd line] >= 0} {
-               if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
-                       cmit original_line final_line line_count]} {
-                       set r_commit     $cmit
-                       set r_orig_line  $original_line
-                       set r_final_line $final_line
-                       set r_line_count $line_count
-               } elseif {[string match {filename *} $line]} {
-                       set file [string range $line 9 end]
-                       set n    $r_line_count
-                       set lno  $r_final_line
-                       set oln  $r_orig_line
-                       set cmit $r_commit
-
-                       if {[regexp {^0{40}$} $cmit]} {
-                               set commit_abbr work
-                               set commit_type curr_commit
-                       } elseif {$cmit eq $commit} {
-                               set commit_abbr this
-                               set commit_type curr_commit
-                       } else {
-                               set commit_type prior_commit
-                               set commit_abbr [string range $cmit 0 3]
-                       }
-
-                       set author_abbr {}
-                       set a_name {}
-                       catch {set a_name $header($cmit,author)}
-                       while {$a_name ne {}} {
-                               if {$author_abbr ne {}
-                                       && [string index $a_name 0] eq {'}} {
-                                       regsub {^'[^']+'\s+} $a_name {} a_name
-                               }
-                               if {![regexp {^([[:upper:]])} $a_name _a]} break
-                               append author_abbr $_a
-                               unset _a
-                               if {![regsub \
-                                       {^[[:upper:]][^\s]*\s+} \
-                                       $a_name {} a_name ]} break
-                       }
-                       if {$author_abbr eq {}} {
-                               set author_abbr { |}
-                       } else {
-                               set author_abbr [string range $author_abbr 0 3]
-                       }
-                       unset a_name
-
-                       set first_lno $lno
-                       while {
-                          $first_lno > 1
-                       && $cmit eq [lindex $line_data [expr {$first_lno - 1}] 0]
-                       && $file eq [lindex $line_data [expr {$first_lno - 1}] 1]
-                       } {
-                               incr first_lno -1
-                       }
-
-                       set color {}
-                       if {$first_lno < $lno} {
-                               foreach g [$w_file tag names $first_lno.0] {
-                                       if {[regexp {^color[0-9]+$} $g]} {
-                                               set color $g
-                                               break
-                                       }
-                               }
-                       } else {
-                               set i [lsort [concat \
-                                       [$w_file tag names "[expr {$first_lno - 1}].0"] \
-                                       [$w_file tag names "[expr {$lno + $n}].0"] \
-                                       ]]
-                               for {set g 0} {$g < [llength $group_colors]} {incr g} {
-                                       if {[lsearch -sorted -exact $i color$g] == -1} {
-                                               set color color$g
-                                               break
-                                       }
-                               }
-                       }
-                       if {$color eq {}} {
-                               set color color0
-                       }
-
-                       while {$n > 0} {
-                               set lno_e "$lno.0 lineend + 1c"
-                               if {[lindex $line_data $lno] ne {}} {
-                                       set g [lindex $line_data $lno 0]
-                                       foreach i $w_columns {
-                                               $i tag remove g$g $lno.0 $lno_e
-                                       }
-                               }
-                               lset line_data $lno [list $cmit $file $oln]
-
-                               $cur_w delete $lno.0 "$lno.0 lineend"
-                               if {$lno == $first_lno} {
-                                       $cur_w insert $lno.0 $commit_abbr $commit_type
-                               } elseif {$lno == [expr {$first_lno + 1}]} {
-                                       $cur_w insert $lno.0 $author_abbr author_abbr
-                               } else {
-                                       $cur_w insert $lno.0 { |}
-                               }
-
-                               foreach i $w_columns {
-                                       if {$cur_w eq $w_amov} {
-                                               for {set g 0} \
-                                                       {$g < [llength $group_colors]} \
-                                                       {incr g} {
-                                                       $i tag remove color$g $lno.0 $lno_e
-                                               }
-                                               $i tag add $color $lno.0 $lno_e
-                                       }
-                                       $i tag add g$cmit $lno.0 $lno_e
-                               }
-
-                               if {$highlight_column eq $cur_w} {
-                                       if {$highlight_line == -1
-                                        && [lindex [$w_file yview] 0] == 0} {
-                                               $w_file see $lno.0
-                                               set highlight_line $lno
-                                       }
-                                       if {$highlight_line == $lno} {
-                                               _showcommit $this $cur_w $lno
-                                       }
-                               }
-
-                               incr n -1
-                               incr lno
-                               incr oln
-                               incr blame_lines
-                       }
-
-                       while {
-                          $cmit eq [lindex $line_data $lno 0]
-                       && $file eq [lindex $line_data $lno 1]
-                       } {
-                               $cur_w delete $lno.0 "$lno.0 lineend"
-
-                               if {$lno == $first_lno} {
-                                       $cur_w insert $lno.0 $commit_abbr $commit_type
-                               } elseif {$lno == [expr {$first_lno + 1}]} {
-                                       $cur_w insert $lno.0 $author_abbr author_abbr
-                               } else {
-                                       $cur_w insert $lno.0 { |}
-                               }
-
-                               if {$cur_w eq $w_amov} {
-                                       foreach i $w_columns {
-                                               for {set g 0} \
-                                                       {$g < [llength $group_colors]} \
-                                                       {incr g} {
-                                                       $i tag remove color$g $lno.0 $lno_e
-                                               }
-                                               $i tag add $color $lno.0 $lno_e
-                                       }
-                               }
-
-                               incr lno
-                       }
-
-               } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} {
-                       set header($r_commit,$key) $data
-               }
-       }
-       $cur_w conf -state disabled
-
-       if {[eof $fd]} {
-               close $fd
-               if {$cur_w eq $w_asim} {
-                       _exec_blame $this $w_amov @amov_data \
-                               $original_options \
-                               [mc "Loading original location annotations..."]
-               } else {
-                       set current_fd {}
-                       $status stop [mc "Annotation complete."]
-               }
-       } else {
-               $status update $blame_lines $total_lines
-       }
-} ifdeleted { catch {close $fd} }
-
-method _click {cur_w pos} {
-       set lno [lindex [split [$cur_w index $pos] .] 0]
-       _showcommit $this $cur_w $lno
-}
-
-method _load_commit {cur_w cur_d pos} {
-       upvar #0 $cur_d line_data
-       set lno [lindex [split [$cur_w index $pos] .] 0]
-       set dat [lindex $line_data $lno]
-       if {$dat ne {}} {
-               lappend history [list \
-                       $commit $path \
-                       $highlight_column \
-                       $highlight_line \
-                       [lindex [$w_file xview] 0] \
-                       [lindex [$w_file yview] 0] \
-                       ]
-               set commit [lindex $dat 0]
-               set path   [lindex $dat 1]
-               _load $this [list [lindex $dat 2]]
-       }
-}
-
-method _showcommit {cur_w lno} {
-       global repo_config
-       variable active_color
-
-       if {$highlight_commit ne {}} {
-               foreach i $w_columns {
-                       $i tag conf g$highlight_commit -background {}
-                       $i tag lower g$highlight_commit
-               }
-       }
-
-       if {$cur_w eq $w_asim} {
-               set dat [lindex $asim_data $lno]
-               set highlight_column $w_asim
-       } else {
-               set dat [lindex $amov_data $lno]
-               set highlight_column $w_amov
-       }
-
-       $w_cviewer conf -state normal
-       $w_cviewer delete 0.0 end
-
-       if {$dat eq {}} {
-               set cmit {}
-               $w_cviewer insert end [mc "Loading annotation..."] still_loading
-       } else {
-               set cmit [lindex $dat 0]
-               set file [lindex $dat 1]
-
-               foreach i $w_columns {
-                       $i tag conf g$cmit -background $active_color
-                       $i tag raise g$cmit
-               }
-
-               set author_name {}
-               set author_email {}
-               set author_time {}
-               catch {set author_name $header($cmit,author)}
-               catch {set author_email $header($cmit,author-mail)}
-               catch {set author_time [format_date $header($cmit,author-time)]}
-
-               set committer_name {}
-               set committer_email {}
-               set committer_time {}
-               catch {set committer_name $header($cmit,committer)}
-               catch {set committer_email $header($cmit,committer-mail)}
-               catch {set committer_time [format_date $header($cmit,committer-time)]}
-
-               if {[catch {set msg $header($cmit,message)}]} {
-                       set msg {}
-                       catch {
-                               set fd [git_read cat-file commit $cmit]
-                               fconfigure $fd -encoding binary -translation lf
-                               if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
-                                       set enc utf-8
-                               }
-                               while {[gets $fd line] > 0} {
-                                       if {[string match {encoding *} $line]} {
-                                               set enc [string tolower [string range $line 9 end]]
-                                       }
-                               }
-                               set msg [read $fd]
-                               close $fd
-
-                               set enc [tcl_encoding $enc]
-                               if {$enc ne {}} {
-                                       set msg [encoding convertfrom $enc $msg]
-                                       set author_name [encoding convertfrom $enc $author_name]
-                                       set committer_name [encoding convertfrom $enc $committer_name]
-                                       set header($cmit,author) $author_name
-                                       set header($cmit,committer) $committer_name
-                                       set header($cmit,summary) \
-                                       [encoding convertfrom $enc $header($cmit,summary)]
-                               }
-                               set msg [string trim $msg]
-                       }
-                       set header($cmit,message) $msg
-               }
-
-               $w_cviewer insert end "commit $cmit\n" header_key
-               $w_cviewer insert end [strcat [mc "Author:"] "\t"] header_key
-               $w_cviewer insert end "$author_name $author_email" header_val
-               $w_cviewer insert end "  $author_time\n" header_val
-
-               $w_cviewer insert end [strcat [mc "Committer:"] "\t"] header_key
-               $w_cviewer insert end "$committer_name $committer_email" header_val
-               $w_cviewer insert end "  $committer_time\n" header_val
-
-               if {$file ne $path} {
-                       $w_cviewer insert end [strcat [mc "Original File:"] "\t"] header_key
-                       $w_cviewer insert end "[escape_path $file]\n" header_val
-               }
-
-               $w_cviewer insert end "\n$msg"
-       }
-       $w_cviewer conf -state disabled
-
-       set highlight_line $lno
-       set highlight_commit $cmit
-
-       if {[lsearch -exact $tooltip_commit $highlight_commit] != -1} {
-               _hide_tooltip $this
-       }
-}
-
-method _copycommit {} {
-       set pos @$::cursorX,$::cursorY
-       set lno [lindex [split [$::cursorW index $pos] .] 0]
-       set dat [lindex $amov_data $lno]
-       if {$dat ne {}} {
-               clipboard clear
-               clipboard append \
-                       -format STRING \
-                       -type STRING \
-                       -- [lindex $dat 0]
-       }
-}
-
-method _show_tooltip {cur_w pos} {
-       if {$tooltip_wm ne {}} {
-               _open_tooltip $this $cur_w
-       } elseif {$tooltip_timer eq {}} {
-               set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]]
-       }
-}
-
-method _open_tooltip {cur_w} {
-       set tooltip_timer {}
-       set pos_x [winfo pointerx $cur_w]
-       set pos_y [winfo pointery $cur_w]
-       if {[winfo containing $pos_x $pos_y] ne $cur_w} {
-               _hide_tooltip $this
-               return
-       }
-
-       if {$tooltip_wm ne "$cur_w.tooltip"} {
-               _hide_tooltip $this
-
-               set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1]
-               wm overrideredirect $tooltip_wm 1
-               wm transient $tooltip_wm [winfo toplevel $cur_w]
-               set tooltip_t $tooltip_wm.label
-               text $tooltip_t \
-                       -takefocus 0 \
-                       -highlightthickness 0 \
-                       -relief flat \
-                       -borderwidth 0 \
-                       -wrap none \
-                       -background lightyellow \
-                       -foreground black
-               $tooltip_t tag conf section_header -font font_uibold
-               pack $tooltip_t
-       } else {
-               $tooltip_t conf -state normal
-               $tooltip_t delete 0.0 end
-       }
-
-       set pos @[join [list \
-               [expr {$pos_x - [winfo rootx $cur_w]}] \
-               [expr {$pos_y - [winfo rooty $cur_w]}]] ,]
-       set lno [lindex [split [$cur_w index $pos] .] 0]
-       if {$cur_w eq $w_amov} {
-               set dat [lindex $amov_data $lno]
-               set org {}
-       } else {
-               set dat [lindex $asim_data $lno]
-               set org [lindex $amov_data $lno]
-       }
-
-       if {$dat eq {}} {
-               _hide_tooltip $this
-               return
-       }
-
-       set cmit [lindex $dat 0]
-       set tooltip_commit [list $cmit]
-
-       set author_name {}
-       set summary     {}
-       set author_time {}
-       catch {set author_name $header($cmit,author)}
-       catch {set summary     $header($cmit,summary)}
-       catch {set author_time [format_date $header($cmit,author-time)]}
-
-       $tooltip_t insert end "commit $cmit\n"
-       $tooltip_t insert end "$author_name  $author_time\n"
-       $tooltip_t insert end "$summary"
-
-       if {$org ne {} && [lindex $org 0] ne $cmit} {
-               set save [$tooltip_t get 0.0 end]
-               $tooltip_t delete 0.0 end
-
-               set cmit [lindex $org 0]
-               set file [lindex $org 1]
-               lappend tooltip_commit $cmit
-
-               set author_name {}
-               set summary     {}
-               set author_time {}
-               catch {set author_name $header($cmit,author)}
-               catch {set summary     $header($cmit,summary)}
-               catch {set author_time [format_date $header($cmit,author-time)]}
-
-               $tooltip_t insert end [strcat [mc "Originally By:"] "\n"] section_header
-               $tooltip_t insert end "commit $cmit\n"
-               $tooltip_t insert end "$author_name  $author_time\n"
-               $tooltip_t insert end "$summary\n"
-
-               if {$file ne $path} {
-                       $tooltip_t insert end [strcat [mc "In File:"] " "] section_header
-                       $tooltip_t insert end "$file\n"
-               }
-
-               $tooltip_t insert end "\n"
-               $tooltip_t insert end [strcat [mc "Copied Or Moved Here By:"] "\n"] section_header
-               $tooltip_t insert end $save
-       }
-
-       $tooltip_t conf -state disabled
-       _position_tooltip $this
-}
-
-method _position_tooltip {} {
-       set max_h [lindex [split [$tooltip_t index end] .] 0]
-       set max_w 0
-       for {set i 1} {$i <= $max_h} {incr i} {
-               set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1]
-               if {$c > $max_w} {set max_w $c}
-       }
-       $tooltip_t conf -width $max_w -height $max_h
-
-       set req_w [winfo reqwidth  $tooltip_t]
-       set req_h [winfo reqheight $tooltip_t]
-       set pos_x [expr {[winfo pointerx .] +  5}]
-       set pos_y [expr {[winfo pointery .] + 10}]
-
-       set g "${req_w}x${req_h}"
-       if {$pos_x >= 0} {append g +}
-       append g $pos_x
-       if {$pos_y >= 0} {append g +}
-       append g $pos_y
-
-       wm geometry $tooltip_wm $g
-       raise $tooltip_wm
-}
-
-method _hide_tooltip {} {
-       if {$tooltip_wm ne {}} {
-               destroy $tooltip_wm
-               set tooltip_wm {}
-               set tooltip_commit {}
-       }
-       if {$tooltip_timer ne {}} {
-               after cancel $tooltip_timer
-               set tooltip_timer {}
-       }
-}
-
-method _resize {new_height} {
-       set diff [expr {$new_height - $old_height}]
-       if {$diff == 0} return
-
-       set my [expr {[winfo height $w.file_pane] - 25}]
-       set o [$w.file_pane sash coord 0]
-       set ox [lindex $o 0]
-       set oy [expr {[lindex $o 1] + $diff}]
-       if {$oy < 0}   {set oy 0}
-       if {$oy > $my} {set oy $my}
-       $w.file_pane sash place 0 $ox $oy
-
-       set old_height $new_height
-}
-
-}
diff --git a/lib/branch.tcl b/lib/branch.tcl
deleted file mode 100644 (file)
index 777eeb7..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-# git-gui branch (create/delete) support
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc load_all_heads {} {
-       global some_heads_tracking
-
-       set rh refs/heads
-       set rh_len [expr {[string length $rh] + 1}]
-       set all_heads [list]
-       set fd [git_read for-each-ref --format=%(refname) $rh]
-       while {[gets $fd line] > 0} {
-               if {!$some_heads_tracking || ![is_tracking_branch $line]} {
-                       lappend all_heads [string range $line $rh_len end]
-               }
-       }
-       close $fd
-
-       return [lsort $all_heads]
-}
-
-proc load_all_tags {} {
-       set all_tags [list]
-       set fd [git_read for-each-ref \
-               --sort=-taggerdate \
-               --format=%(refname) \
-               refs/tags]
-       while {[gets $fd line] > 0} {
-               if {![regsub ^refs/tags/ $line {} name]} continue
-               lappend all_tags $name
-       }
-       close $fd
-       return $all_tags
-}
-
-proc radio_selector {varname value args} {
-       upvar #0 $varname var
-       set var $value
-}
diff --git a/lib/branch_checkout.tcl b/lib/branch_checkout.tcl
deleted file mode 100644 (file)
index 6603703..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-# git-gui branch checkout support
-# Copyright (C) 2007 Shawn Pearce
-
-class branch_checkout {
-
-field w              ; # widget path
-field w_rev          ; # mega-widget to pick the initial revision
-
-field opt_fetch     1; # refetch tracking branch if used?
-field opt_detach    0; # force a detached head case?
-
-constructor dialog {} {
-       make_toplevel top w
-       wm title $top [append "[appname] ([reponame]): " [mc "Checkout Branch"]]
-       if {$top ne {.}} {
-               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
-       }
-
-       label $w.header -text [mc "Checkout Branch"] -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       button $w.buttons.create -text [mc Checkout] \
-               -default active \
-               -command [cb _checkout]
-       pack $w.buttons.create -side right
-       button $w.buttons.cancel -text [mc Cancel] \
-               -command [list destroy $w]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       set w_rev [::choose_rev::new $w.rev [mc Revision]]
-       $w_rev bind_listbox <Double-Button-1> [cb _checkout]
-       pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5
-
-       labelframe $w.options -text [mc Options]
-
-       checkbutton $w.options.fetch \
-               -text [mc "Fetch Tracking Branch"] \
-               -variable @opt_fetch
-       pack $w.options.fetch -anchor nw
-
-       checkbutton $w.options.detach \
-               -text [mc "Detach From Local Branch"] \
-               -variable @opt_detach
-       pack $w.options.detach -anchor nw
-
-       pack $w.options -anchor nw -fill x -pady 5 -padx 5
-
-       bind $w <Visibility> [cb _visible]
-       bind $w <Key-Escape> [list destroy $w]
-       bind $w <Key-Return> [cb _checkout]\;break
-       tkwait window $w
-}
-
-method _checkout {} {
-       set spec [$w_rev get_tracking_branch]
-       if {$spec ne {} && $opt_fetch} {
-               set new {}
-       } elseif {[catch {set new [$w_rev commit_or_die]}]} {
-               return
-       }
-
-       if {$opt_detach} {
-               set ref {}
-       } else {
-               set ref [$w_rev get_local_branch]
-       }
-
-       set co [::checkout_op::new [$w_rev get] $new $ref]
-       $co parent $w
-       $co enable_checkout 1
-       if {$spec ne {} && $opt_fetch} {
-               $co enable_fetch $spec
-       }
-
-       if {[$co run]} {
-               destroy $w
-       } else {
-               $w_rev focus_filter
-       }
-}
-
-method _visible {} {
-       grab $w
-       $w_rev focus_filter
-}
-
-}
diff --git a/lib/branch_create.tcl b/lib/branch_create.tcl
deleted file mode 100644 (file)
index 53dfb4c..0000000
+++ /dev/null
@@ -1,220 +0,0 @@
-# git-gui branch create support
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-class branch_create {
-
-field w              ; # widget path
-field w_rev          ; # mega-widget to pick the initial revision
-field w_name         ; # new branch name widget
-
-field name         {}; # name of the branch the user has chosen
-field name_type  user; # type of branch name to use
-
-field opt_merge    ff; # type of merge to apply to existing branch
-field opt_checkout  1; # automatically checkout the new branch?
-field opt_fetch     1; # refetch tracking branch if used?
-field reset_ok      0; # did the user agree to reset?
-
-constructor dialog {} {
-       global repo_config
-
-       make_toplevel top w
-       wm title $top [append "[appname] ([reponame]): " [mc "Create Branch"]]
-       if {$top ne {.}} {
-               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
-       }
-
-       label $w.header -text [mc "Create New Branch"] -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       button $w.buttons.create -text [mc Create] \
-               -default active \
-               -command [cb _create]
-       pack $w.buttons.create -side right
-       button $w.buttons.cancel -text [mc Cancel] \
-               -command [list destroy $w]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       labelframe $w.desc -text [mc "Branch Name"]
-       radiobutton $w.desc.name_r \
-               -anchor w \
-               -text [mc "Name:"] \
-               -value user \
-               -variable @name_type
-       set w_name $w.desc.name_t
-       entry $w_name \
-               -borderwidth 1 \
-               -relief sunken \
-               -width 40 \
-               -textvariable @name \
-               -validate key \
-               -validatecommand [cb _validate %d %S]
-       grid $w.desc.name_r $w_name -sticky we -padx {0 5}
-
-       radiobutton $w.desc.match_r \
-               -anchor w \
-               -text [mc "Match Tracking Branch Name"] \
-               -value match \
-               -variable @name_type
-       grid $w.desc.match_r -sticky we -padx {0 5} -columnspan 2
-
-       grid columnconfigure $w.desc 1 -weight 1
-       pack $w.desc -anchor nw -fill x -pady 5 -padx 5
-
-       set w_rev [::choose_rev::new $w.rev [mc "Starting Revision"]]
-       pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5
-
-       labelframe $w.options -text [mc Options]
-
-       frame $w.options.merge
-       label $w.options.merge.l -text [mc "Update Existing Branch:"]
-       pack $w.options.merge.l -side left
-       radiobutton $w.options.merge.no \
-               -text [mc No] \
-               -value none \
-               -variable @opt_merge
-       pack $w.options.merge.no -side left
-       radiobutton $w.options.merge.ff \
-               -text [mc "Fast Forward Only"] \
-               -value ff \
-               -variable @opt_merge
-       pack $w.options.merge.ff -side left
-       radiobutton $w.options.merge.reset \
-               -text [mc Reset] \
-               -value reset \
-               -variable @opt_merge
-       pack $w.options.merge.reset -side left
-       pack $w.options.merge -anchor nw
-
-       checkbutton $w.options.fetch \
-               -text [mc "Fetch Tracking Branch"] \
-               -variable @opt_fetch
-       pack $w.options.fetch -anchor nw
-
-       checkbutton $w.options.checkout \
-               -text [mc "Checkout After Creation"] \
-               -variable @opt_checkout
-       pack $w.options.checkout -anchor nw
-       pack $w.options -anchor nw -fill x -pady 5 -padx 5
-
-       trace add variable @name_type write [cb _select]
-
-       set name $repo_config(gui.newbranchtemplate)
-       if {[is_config_true gui.matchtrackingbranch]} {
-               set name_type match
-       }
-
-       bind $w <Visibility> [cb _visible]
-       bind $w <Key-Escape> [list destroy $w]
-       bind $w <Key-Return> [cb _create]\;break
-       tkwait window $w
-}
-
-method _create {} {
-       global repo_config
-       global M1B
-
-       set spec [$w_rev get_tracking_branch]
-       switch -- $name_type {
-       user {
-               set newbranch $name
-       }
-       match {
-               if {$spec eq {}} {
-                       tk_messageBox \
-                               -icon error \
-                               -type ok \
-                               -title [wm title $w] \
-                               -parent $w \
-                               -message [mc "Please select a tracking branch."]
-                       return
-               }
-               if {![regsub ^refs/heads/ [lindex $spec 2] {} newbranch]} {
-                       tk_messageBox \
-                               -icon error \
-                               -type ok \
-                               -title [wm title $w] \
-                               -parent $w \
-                               -message [mc "Tracking branch %s is not a branch in the remote repository." [$w get]]
-                       return
-               }
-       }
-       }
-
-       if {$newbranch eq {}
-               || $newbranch eq $repo_config(gui.newbranchtemplate)} {
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message [mc "Please supply a branch name."]
-               focus $w_name
-               return
-       }
-
-       if {[catch {git check-ref-format "heads/$newbranch"}]} {
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message [mc "'%s' is not an acceptable branch name." $newbranch]
-               focus $w_name
-               return
-       }
-
-       if {$spec ne {} && $opt_fetch} {
-               set new {}
-       } elseif {[catch {set new [$w_rev commit_or_die]}]} {
-               return
-       }
-
-       set co [::checkout_op::new \
-               [$w_rev get] \
-               $new \
-               refs/heads/$newbranch]
-       $co parent $w
-       $co enable_create   1
-       $co enable_merge    $opt_merge
-       $co enable_checkout $opt_checkout
-       if {$spec ne {} && $opt_fetch} {
-               $co enable_fetch $spec
-       }
-
-       if {[$co run]} {
-               destroy $w
-       } else {
-               focus $w_name
-       }
-}
-
-method _validate {d S} {
-       if {$d == 1} {
-               if {[regexp {[~^:?*\[\0- ]} $S]} {
-                       return 0
-               }
-               if {[string length $S] > 0} {
-                       set name_type user
-               }
-       }
-       return 1
-}
-
-method _select {args} {
-       if {$name_type eq {match}} {
-               $w_rev pick_tracking_branch
-       }
-}
-
-method _visible {} {
-       grab $w
-       if {$name_type eq {user}} {
-               $w_name icursor end
-               focus $w_name
-       }
-}
-
-}
diff --git a/lib/branch_delete.tcl b/lib/branch_delete.tcl
deleted file mode 100644 (file)
index 86c4f73..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-# git-gui branch delete support
-# Copyright (C) 2007 Shawn Pearce
-
-class branch_delete {
-
-field w               ; # widget path
-field w_heads         ; # listbox of local head names
-field w_check         ; # revision picker for merge test
-field w_delete        ; # delete button
-
-constructor dialog {} {
-       global current_branch
-
-       make_toplevel top w
-       wm title $top [append "[appname] ([reponame]): " [mc "Delete Branch"]]
-       if {$top ne {.}} {
-               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
-       }
-
-       label $w.header -text [mc "Delete Local Branch"] -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       set w_delete $w.buttons.delete
-       button $w_delete \
-               -text [mc Delete] \
-               -default active \
-               -state disabled \
-               -command [cb _delete]
-       pack $w_delete -side right
-       button $w.buttons.cancel \
-               -text [mc Cancel] \
-               -command [list destroy $w]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       labelframe $w.list -text [mc "Local Branches"]
-       set w_heads $w.list.l
-       listbox $w_heads \
-               -height 10 \
-               -width 70 \
-               -selectmode extended \
-               -exportselection false \
-               -yscrollcommand [list $w.list.sby set]
-       scrollbar $w.list.sby -command [list $w.list.l yview]
-       pack $w.list.sby -side right -fill y
-       pack $w.list.l -side left -fill both -expand 1
-       pack $w.list -fill both -expand 1 -pady 5 -padx 5
-
-       set w_check [choose_rev::new \
-               $w.check \
-               [mc "Delete Only If Merged Into"] \
-               ]
-       $w_check none [mc "Always (Do not perform merge test.)"]
-       pack $w.check -anchor nw -fill x -pady 5 -padx 5
-
-       foreach h [load_all_heads] {
-               if {$h ne $current_branch} {
-                       $w_heads insert end $h
-               }
-       }
-
-       bind $w_heads <<ListboxSelect>> [cb _select]
-       bind $w <Visibility> "
-               grab $w
-               focus $w
-       "
-       bind $w <Key-Escape> [list destroy $w]
-       bind $w <Key-Return> [cb _delete]\;break
-       tkwait window $w
-}
-
-method _select {} {
-       if {[$w_heads curselection] eq {}} {
-               $w_delete configure -state disabled
-       } else {
-               $w_delete configure -state normal
-       }
-}
-
-method _delete {} {
-       if {[catch {set check_cmt [$w_check commit_or_die]}]} {
-               return
-       }
-
-       set to_delete [list]
-       set not_merged [list]
-       foreach i [$w_heads curselection] {
-               set b [$w_heads get $i]
-               if {[catch {
-                       set o [git rev-parse --verify "refs/heads/$b"]
-               }]} continue
-               if {$check_cmt ne {}} {
-                       if {[catch {set m [git merge-base $o $check_cmt]}]} continue
-                       if {$o ne $m} {
-                               lappend not_merged $b
-                               continue
-                       }
-               }
-               lappend to_delete [list $b $o]
-       }
-       if {$not_merged ne {}} {
-               set msg "[mc "The following branches are not completely merged into %s:" [$w_check get]]
-
- - [join $not_merged "\n - "]"
-               tk_messageBox \
-                       -icon info \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message $msg
-       }
-       if {$to_delete eq {}} return
-       if {$check_cmt eq {}} {
-               set msg [mc "Recovering deleted branches is difficult. \n\n Delete the selected branches?"]
-               if {[tk_messageBox \
-                       -icon warning \
-                       -type yesno \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message $msg] ne yes} {
-                       return
-               }
-       }
-
-       set failed {}
-       foreach i $to_delete {
-               set b [lindex $i 0]
-               set o [lindex $i 1]
-               if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
-                       append failed " - $b: $err\n"
-               }
-       }
-
-       if {$failed ne {}} {
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message [mc "Failed to delete branches:\n%s" $failed]
-       }
-
-       destroy $w
-}
-
-}
diff --git a/lib/branch_rename.tcl b/lib/branch_rename.tcl
deleted file mode 100644 (file)
index 1665388..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-# git-gui branch rename support
-# Copyright (C) 2007 Shawn Pearce
-
-class branch_rename {
-
-field w
-field oldname
-field newname
-
-constructor dialog {} {
-       global current_branch
-
-       make_toplevel top w
-       wm title $top [append "[appname] ([reponame]): " [mc "Rename Branch"]]
-       if {$top ne {.}} {
-               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
-       }
-
-       set oldname $current_branch
-       set newname [get_config gui.newbranchtemplate]
-
-       label $w.header -text [mc "Rename Branch"] -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       button $w.buttons.rename -text [mc Rename] \
-               -default active \
-               -command [cb _rename]
-       pack $w.buttons.rename -side right
-       button $w.buttons.cancel -text [mc Cancel] \
-               -command [list destroy $w]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       frame $w.rename
-       label $w.rename.oldname_l -text [mc "Branch:"]
-       eval tk_optionMenu $w.rename.oldname_m @oldname [load_all_heads]
-
-       label $w.rename.newname_l -text [mc "New Name:"]
-       entry $w.rename.newname_t \
-               -borderwidth 1 \
-               -relief sunken \
-               -width 40 \
-               -textvariable @newname \
-               -validate key \
-               -validatecommand {
-                       if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0}
-                       return 1
-               }
-
-       grid $w.rename.oldname_l $w.rename.oldname_m -sticky w  -padx {0 5}
-       grid $w.rename.newname_l $w.rename.newname_t -sticky we -padx {0 5}
-       grid columnconfigure $w.rename 1 -weight 1
-       pack $w.rename -anchor nw -fill x -pady 5 -padx 5
-
-       bind $w <Key-Return> [cb _rename]
-       bind $w <Key-Escape> [list destroy $w]
-       bind $w <Visibility> "
-               grab $w
-               $w.rename.newname_t icursor end
-               focus $w.rename.newname_t
-       "
-       tkwait window $w
-}
-
-method _rename {} {
-       global current_branch
-
-       if {$oldname eq {}} {
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message [mc "Please select a branch to rename."]
-               focus $w.rename.oldname_m
-               return
-       }
-       if {$newname eq {}
-               || $newname eq [get_config gui.newbranchtemplate]} {
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message [mc "Please supply a branch name."]
-               focus $w.rename.newname_t
-               return
-       }
-       if {![catch {git show-ref --verify -- "refs/heads/$newname"}]} {
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message [mc "Branch '%s' already exists." $newname]
-               focus $w.rename.newname_t
-               return
-       }
-       if {[catch {git check-ref-format "heads/$newname"}]} {
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message [mc "'%s' is not an acceptable branch name." $newname]
-               focus $w.rename.newname_t
-               return
-       }
-
-       if {[catch {git branch -m $oldname $newname} err]} {
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message [strcat [mc "Failed to rename '%s'." $oldname] "\n\n$err"]
-               return
-       }
-
-       if {$current_branch eq $oldname} {
-               set current_branch $newname
-       }
-
-       destroy $w
-}
-
-}
diff --git a/lib/browser.tcl b/lib/browser.tcl
deleted file mode 100644 (file)
index 53d5a62..0000000
+++ /dev/null
@@ -1,310 +0,0 @@
-# git-gui tree browser
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-class browser {
-
-image create photo ::browser::img_parent  -data {R0lGODlhEAAQAIUAAPwCBBxSHBxOHMTSzNzu3KzCtBRGHCSKFIzCjLzSxBQ2FAxGHDzCLCyeHBQ+FHSmfAwuFBxKLDSCNMzizISyjJzOnDSyLAw+FAQSDAQeDBxWJAwmDAQOBKzWrDymNAQaDAQODAwaDDyKTFSyXFTGTEy6TAQCBAQKDAwiFBQyHAwSFAwmHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAZ1QIBwSCwaj0hiQCBICpcDQsFgGAaIguhhi0gohIsrQEDYMhiNrRfgeAQC5fMCAolIDhD2hFI5WC4YRBkaBxsOE2l/RxsHHA4dHmkfRyAbIQ4iIyQlB5NFGCAACiakpSZEJyinTgAcKSesACorgU4mJ6uxR35BACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=}
-image create photo ::browser::img_rblob   -data {R0lGODlhEAAQAIUAAPwCBFxaXNze3Ly2rJSWjPz+/Ozq7GxqbJyanPT29HRydMzOzDQyNIyKjERCROTi3Pz69PTy7Pzy7PTu5Ozm3LyqlJyWlJSSjJSOhOzi1LyulPz27PTq3PTm1OzezLyqjIyKhJSKfOzaxPz29OzizLyidIyGdIyCdOTOpLymhOzavOTStMTCtMS+rMS6pMSynMSulLyedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAaQQIAQECgajcNkQMBkDgKEQFK4LFgLhkMBIVUKroWEYlEgMLxbBKLQUBwc52HgAQ4LBo049atWQyIPA3pEdFcQEhMUFYNVagQWFxgZGoxfYRsTHB0eH5UJCJAYICEinUoPIxIcHCQkIiIllQYEGCEhJicoKYwPmiQeKisrKLFKLCwtLi8wHyUlMYwM0tPUDH5BACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=}
-image create photo ::browser::img_xblob   -data {R0lGODlhEAAQAIYAAPwCBFRWVFxaXNza3OTi3Nze3Ly2tJyanPz+/Ozq7GxubNzSxMzOzMTGxHRybDQyNLy+vHRydHx6fKSipISChIyKjGxqbERCRCwuLLy6vGRiZExKTCQiJAwKDLSytLy2rJSSlHx+fDw6PKyqrBQWFPTu5Ozm3LyulLS2tCQmJAQCBPTq3Ozi1MSynCwqLAQGBOTazOzizOzezLyqjBweHNzSvOzaxKyurHRuZNzOtLymhDw+PIyCdOzWvOTOpLyidNzKtOTStLyifMTCtMS+rLyedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAfZgACCAAEChYeGg4oCAwQFjgYBBwGKggEECJkICQoIkwADCwwNDY2mDA4Lng8QDhESsLARExQVDhYXGBkWExIaGw8cHR4SCQQfFQ8eFgUgIQEiwiMSBMYfGB4atwEXDyQd0wQlJicPKAHoFyIpJCoeDgMrLC0YKBsX6i4kL+4OMDEyZijr5oLGNxUqUCioEcPGDAwjPNyI6MEDChQjcOSwsUDHgw07RIgI4KCkAgs8cvTw8eOBogAxQtXIASTISiEuBwUYMoRIixYnZggpUgTDywdIkWJIitRPIAAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
-image create photo ::browser::img_tree    -data {R0lGODlhEAAQAIYAAPwCBAQCBExKTBwWHMzKzOzq7ERCRExGTCwqLARqnAQ+ZHR2dKyqrNTOzHx2fCQiJMTi9NTu9HzC3AxmnAQ+XPTm7Dy67DymzITC3IzG5AxypHRydKymrMzOzOzu7BweHByy9AyGtFyy1IzG3NTu/ARupFRSVByazBR6rAyGvFyuzJTK3MTm9BR+tAxWhHS61MTi7Pz+/IymvCxulBRelAx2rHS63Pz6/PTy9PTu9Nza3ISitBRupFSixNTS1CxqnDQyNMzGzOTi5MTCxMTGxGxubGxqbLy2vLSutGRiZLy6vLSytKyurDQuNFxaXKSipDw6PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAfDgACCAAECg4eIAAMEBQYHCImDBgkKCwwNBQIBBw4Bhw8QERITFJYEFQUFnoIPFhcYoRkaFBscHR4Ggh8gIRciEiMQJBkltCa6JyUoKSkXKhIrLCQYuQAPLS4TEyUhKb0qLzDVAjEFMjMuNBMoNcw21QY3ODkFOjs82RM1PfDzFRU3fOggcM7Fj2pAgggRokOHDx9DhhAZUqQaISBGhjwMEvEIkiIHEgUAkgSJkiNLmFSMJChAEydPGBSBwvJQgAc0/QQCACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=}
-image create photo ::browser::img_symlink -data {R0lGODlhEAAQAIQAAPwCBCwqLLSytLy+vERGRFRWVDQ2NKSmpAQCBKyurMTGxISChJyanHR2dIyKjGxubHRydGRmZIyOjFxeXHx6fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAVbICACwWieY1CibCCsrBkMb0zchSEcNYskCtqBBzshFkOGQFk0IRqOxqPBODRHCMhCQKteRc9FI/KQWGOIyFYgkDC+gPR4snCcfRGKOIKIgSMQE31+f4OEYCZ+IQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
-image create photo ::browser::img_unknown -data {R0lGODlhEAAQAIUAAPwCBFxaXIyKjNTW1Nze3LS2tJyanER2RGS+VPz+/PTu5GxqbPz69BQ6BCxeLFSqRPT29HRydMzOzDQyNERmPKSypCRWHIyKhERCRDyGPKz2nESiLBxGHCyCHGxubPz6/PTy7Ozi1Ly2rKSipOzm3LyqlKSWhCRyFOzizLymhNTKtNzOvOzaxOTStPz27OzWvOTOpLSupLyedMS+rMS6pMSulLyqjLymfLyifAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAamQIAQECgajcOkYEBoDgoBQyAJOCCuiENCsWBIh9aGw9F4HCARiXciRDQoBUnlYRlcIgsMG5CxXAgMGhscBRAEBRd7AB0eBBoIgxUfICEiikSPgyMMIAokJZcBkBybJgomIaBJAZoMpyCmqkMBFCcVCrgKKAwpoSorKqchKCwtvasIFBIhLiYvLzDHsxQNMcMKLDAwMqEz3jQ1NTY3ONyrE+jp6hN+QQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
-
-field w
-field browser_commit
-field browser_path
-field browser_files  {}
-field browser_status [mc "Starting..."]
-field browser_stack  {}
-field browser_busy   1
-
-field ls_buf     {}; # Buffered record output from ls-tree
-
-constructor new {commit {path {}}} {
-       global cursor_ptr M1B
-       make_toplevel top w
-       wm title $top [append "[appname] ([reponame]): " [mc "File Browser"]]
-
-       set browser_commit $commit
-       set browser_path $browser_commit:$path
-
-       label $w.path \
-               -textvariable @browser_path \
-               -anchor w \
-               -justify left \
-               -borderwidth 1 \
-               -relief sunken \
-               -font font_uibold
-       pack $w.path -anchor w -side top -fill x
-
-       frame $w.list
-       set w_list $w.list.l
-       text $w_list -background white -borderwidth 0 \
-               -cursor $cursor_ptr \
-               -state disabled \
-               -wrap none \
-               -height 20 \
-               -width 70 \
-               -xscrollcommand [list $w.list.sbx set] \
-               -yscrollcommand [list $w.list.sby set]
-       rmsel_tag $w_list
-       scrollbar $w.list.sbx -orient h -command [list $w_list xview]
-       scrollbar $w.list.sby -orient v -command [list $w_list yview]
-       pack $w.list.sbx -side bottom -fill x
-       pack $w.list.sby -side right -fill y
-       pack $w_list -side left -fill both -expand 1
-       pack $w.list -side top -fill both -expand 1
-
-       label $w.status \
-               -textvariable @browser_status \
-               -anchor w \
-               -justify left \
-               -borderwidth 1 \
-               -relief sunken
-       pack $w.status -anchor w -side bottom -fill x
-
-       bind $w_list <Button-1>        "[cb _click 0 @%x,%y];break"
-       bind $w_list <Double-Button-1> "[cb _click 1 @%x,%y];break"
-       bind $w_list <$M1B-Up>         "[cb _parent]        ;break"
-       bind $w_list <$M1B-Left>       "[cb _parent]        ;break"
-       bind $w_list <Up>              "[cb _move -1]       ;break"
-       bind $w_list <Down>            "[cb _move  1]       ;break"
-       bind $w_list <$M1B-Right>      "[cb _enter]         ;break"
-       bind $w_list <Return>          "[cb _enter]         ;break"
-       bind $w_list <Prior>           "[cb _page -1]       ;break"
-       bind $w_list <Next>            "[cb _page  1]       ;break"
-       bind $w_list <Left>            break
-       bind $w_list <Right>           break
-
-       bind $w_list <Visibility> [list focus $w_list]
-       set w $w_list
-       if {$path ne {}} {
-               _ls $this $browser_commit:$path $path
-       } else {
-               _ls $this $browser_commit $path
-       }
-       return $this
-}
-
-method _move {dir} {
-       if {$browser_busy} return
-       set lno [lindex [split [$w index in_sel.first] .] 0]
-       incr lno $dir
-       if {[lindex $browser_files [expr {$lno - 1}]] ne {}} {
-               $w tag remove in_sel 0.0 end
-               $w tag add in_sel $lno.0 [expr {$lno + 1}].0
-               $w see $lno.0
-       }
-}
-
-method _page {dir} {
-       if {$browser_busy} return
-       $w yview scroll $dir pages
-       set lno [expr {int(
-                 [lindex [$w yview] 0]
-               * [llength $browser_files]
-               + 1)}]
-       if {[lindex $browser_files [expr {$lno - 1}]] ne {}} {
-               $w tag remove in_sel 0.0 end
-               $w tag add in_sel $lno.0 [expr {$lno + 1}].0
-               $w see $lno.0
-       }
-}
-
-method _parent {} {
-       if {$browser_busy} return
-       set info [lindex $browser_files 0]
-       if {[lindex $info 0] eq {parent}} {
-               set parent [lindex $browser_stack end-1]
-               set browser_stack [lrange $browser_stack 0 end-2]
-               if {$browser_stack eq {}} {
-                       regsub {:.*$} $browser_path {:} browser_path
-               } else {
-                       regsub {/[^/]+$} $browser_path {} browser_path
-               }
-               set browser_status [mc "Loading %s..." $browser_path]
-               _ls $this [lindex $parent 0] [lindex $parent 1]
-       }
-}
-
-method _enter {} {
-       if {$browser_busy} return
-       set lno [lindex [split [$w index in_sel.first] .] 0]
-       set info [lindex $browser_files [expr {$lno - 1}]]
-       if {$info ne {}} {
-               switch -- [lindex $info 0] {
-               parent {
-                       _parent $this
-               }
-               tree {
-                       set name [lindex $info 2]
-                       set escn [escape_path $name]
-                       set browser_status [mc "Loading %s..." $escn]
-                       append browser_path $escn
-                       _ls $this [lindex $info 1] $name
-               }
-               blob {
-                       set name [lindex $info 2]
-                       set p {}
-                       foreach n $browser_stack {
-                               append p [lindex $n 1]
-                       }
-                       append p $name
-                       blame::new $browser_commit $p
-               }
-               }
-       }
-}
-
-method _click {was_double_click pos} {
-       if {$browser_busy} return
-       set lno [lindex [split [$w index $pos] .] 0]
-       focus $w
-
-       if {[lindex $browser_files [expr {$lno - 1}]] ne {}} {
-               $w tag remove in_sel 0.0 end
-               $w tag add in_sel $lno.0 [expr {$lno + 1}].0
-               if {$was_double_click} {
-                       _enter $this
-               }
-       }
-}
-
-method _ls {tree_id {name {}}} {
-       set ls_buf {}
-       set browser_files {}
-       set browser_busy 1
-
-       $w conf -state normal
-       $w tag remove in_sel 0.0 end
-       $w delete 0.0 end
-       if {$browser_stack ne {}} {
-               $w image create end \
-                       -align center -padx 5 -pady 1 \
-                       -name icon0 \
-                       -image ::browser::img_parent
-               $w insert end [mc "\[Up To Parent\]"]
-               lappend browser_files parent
-       }
-       lappend browser_stack [list $tree_id $name]
-       $w conf -state disabled
-
-       set fd [git_read ls-tree -z $tree_id]
-       fconfigure $fd -blocking 0 -translation binary -encoding binary
-       fileevent $fd readable [cb _read $fd]
-}
-
-method _read {fd} {
-       append ls_buf [read $fd]
-       set pck [split $ls_buf "\0"]
-       set ls_buf [lindex $pck end]
-
-       set n [llength $browser_files]
-       $w conf -state normal
-       foreach p [lrange $pck 0 end-1] {
-               set tab [string first "\t" $p]
-               if {$tab == -1} continue
-
-               set info [split [string range $p 0 [expr {$tab - 1}]] { }]
-               set path [string range $p [expr {$tab + 1}] end]
-               set type   [lindex $info 1]
-               set object [lindex $info 2]
-
-               switch -- $type {
-               blob {
-                       scan [lindex $info 0] %o mode
-                       if {$mode == 0120000} {
-                               set image ::browser::img_symlink
-                       } elseif {($mode & 0100) != 0} {
-                               set image ::browser::img_xblob
-                       } else {
-                               set image ::browser::img_rblob
-                       }
-               }
-               tree {
-                       set image ::browser::img_tree
-                       append path /
-               }
-               default {
-                       set image ::browser::img_unknown
-               }
-               }
-
-               if {$n > 0} {$w insert end "\n"}
-               $w image create end \
-                       -align center -padx 5 -pady 1 \
-                       -name icon[incr n] \
-                       -image $image
-               $w insert end [escape_path $path]
-               lappend browser_files [list $type $object $path]
-       }
-       $w conf -state disabled
-
-       if {[eof $fd]} {
-               close $fd
-               set browser_status [mc "Ready."]
-               set browser_busy 0
-               set ls_buf {}
-               if {$n > 0} {
-                       $w tag add in_sel 1.0 2.0
-                       focus -force $w
-               }
-       }
-} ifdeleted {
-       catch {close $fd}
-}
-
-}
-
-class browser_open {
-
-field w              ; # widget path
-field w_rev          ; # mega-widget to pick the initial revision
-
-constructor dialog {} {
-       make_toplevel top w
-       wm title $top [append "[appname] ([reponame]): " [mc "Browse Branch Files"]]
-       if {$top ne {.}} {
-               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
-       }
-
-       label $w.header \
-               -text [mc "Browse Branch Files"] \
-               -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       button $w.buttons.browse -text [mc Browse] \
-               -default active \
-               -command [cb _open]
-       pack $w.buttons.browse -side right
-       button $w.buttons.cancel -text [mc Cancel] \
-               -command [list destroy $w]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       set w_rev [::choose_rev::new $w.rev [mc Revision]]
-       $w_rev bind_listbox <Double-Button-1> [cb _open]
-       pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5
-
-       bind $w <Visibility> [cb _visible]
-       bind $w <Key-Escape> [list destroy $w]
-       bind $w <Key-Return> [cb _open]\;break
-       tkwait window $w
-}
-
-method _open {} {
-       if {[catch {$w_rev commit_or_die} err]} {
-               return
-       }
-       set name [$w_rev get]
-       destroy $w
-       browser::new $name
-}
-
-method _visible {} {
-       grab $w
-       $w_rev focus_filter
-}
-
-}
diff --git a/lib/checkout_op.tcl b/lib/checkout_op.tcl
deleted file mode 100644 (file)
index f243966..0000000
+++ /dev/null
@@ -1,588 +0,0 @@
-# git-gui commit checkout support
-# Copyright (C) 2007 Shawn Pearce
-
-class checkout_op {
-
-field w        {}; # our window (if we have one)
-field w_cons   {}; # embedded console window object
-
-field new_expr   ; # expression the user saw/thinks this is
-field new_hash   ; # commit SHA-1 we are switching to
-field new_ref    ; # ref we are updating/creating
-
-field parent_w      .; # window that started us
-field merge_type none; # type of merge to apply to existing branch
-field merge_base   {}; # merge base if we have another ref involved
-field fetch_spec   {}; # refetch tracking branch if used?
-field checkout      1; # actually checkout the branch?
-field create        0; # create the branch if it doesn't exist?
-
-field reset_ok      0; # did the user agree to reset?
-field fetch_ok      0; # did the fetch succeed?
-
-field readtree_d   {}; # buffered output from read-tree
-field update_old   {}; # was the update-ref call deferred?
-field reflog_msg   {}; # log message for the update-ref call
-
-constructor new {expr hash {ref {}}} {
-       set new_expr $expr
-       set new_hash $hash
-       set new_ref  $ref
-
-       return $this
-}
-
-method parent {path} {
-       set parent_w [winfo toplevel $path]
-}
-
-method enable_merge {type} {
-       set merge_type $type
-}
-
-method enable_fetch {spec} {
-       set fetch_spec $spec
-}
-
-method enable_checkout {co} {
-       set checkout $co
-}
-
-method enable_create {co} {
-       set create $co
-}
-
-method run {} {
-       if {$fetch_spec ne {}} {
-               global M1B
-
-               # We were asked to refresh a single tracking branch
-               # before we get to work.  We should do that before we
-               # consider any ref updating.
-               #
-               set fetch_ok 0
-               set l_trck [lindex $fetch_spec 0]
-               set remote [lindex $fetch_spec 1]
-               set r_head [lindex $fetch_spec 2]
-               regsub ^refs/heads/ $r_head {} r_name
-
-               set cmd [list git fetch $remote]
-               if {$l_trck ne {}} {
-                       lappend cmd +$r_head:$l_trck
-               } else {
-                       lappend cmd $r_head
-               }
-
-               _toplevel $this {Refreshing Tracking Branch}
-               set w_cons [::console::embed \
-                       $w.console \
-                       [mc "Fetching %s from %s" $r_name $remote]]
-               pack $w.console -fill both -expand 1
-               $w_cons exec $cmd [cb _finish_fetch]
-
-               bind $w <$M1B-Key-w> break
-               bind $w <$M1B-Key-W> break
-               bind $w <Visibility> "
-                       [list grab $w]
-                       [list focus $w]
-               "
-               wm protocol $w WM_DELETE_WINDOW [cb _noop]
-               tkwait window $w
-
-               if {!$fetch_ok} {
-                       delete_this
-                       return 0
-               }
-       }
-
-       if {$new_ref ne {}} {
-               # If we have a ref we need to update it before we can
-               # proceed with a checkout (if one was enabled).
-               #
-               if {![_update_ref $this]} {
-                       delete_this
-                       return 0
-               }
-       }
-
-       if {$checkout} {
-               _checkout $this
-               return 1
-       }
-
-       delete_this
-       return 1
-}
-
-method _noop {} {}
-
-method _finish_fetch {ok} {
-       if {$ok} {
-               set l_trck [lindex $fetch_spec 0]
-               if {$l_trck eq {}} {
-                       set l_trck FETCH_HEAD
-               }
-               if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
-                       set ok 0
-                       $w_cons insert [mc "fatal: Cannot resolve %s" $l_trck]
-                       $w_cons insert $err
-               }
-       }
-
-       $w_cons done $ok
-       set w_cons {}
-       wm protocol $w WM_DELETE_WINDOW {}
-
-       if {$ok} {
-               destroy $w
-               set w {}
-       } else {
-               button $w.close -text [mc Close] -command [list destroy $w]
-               pack $w.close -side bottom -anchor e -padx 10 -pady 10
-       }
-
-       set fetch_ok $ok
-}
-
-method _update_ref {} {
-       global null_sha1 current_branch
-
-       set ref $new_ref
-       set new $new_hash
-
-       set is_current 0
-       set rh refs/heads/
-       set rn [string length $rh]
-       if {[string equal -length $rn $rh $ref]} {
-               set newbranch [string range $ref $rn end]
-               if {$current_branch eq $newbranch} {
-                       set is_current 1
-               }
-       } else {
-               set newbranch $ref
-       }
-
-       if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
-               # Assume it does not exist, and that is what the error was.
-               #
-               if {!$create} {
-                       _error $this [mc "Branch '%s' does not exist." $newbranch]
-                       return 0
-               }
-
-               set reflog_msg "branch: Created from $new_expr"
-               set cur $null_sha1
-       } elseif {$create && $merge_type eq {none}} {
-               # We were told to create it, but not do a merge.
-               # Bad.  Name shouldn't have existed.
-               #
-               _error $this [mc "Branch '%s' already exists." $newbranch]
-               return 0
-       } elseif {!$create && $merge_type eq {none}} {
-               # We aren't creating, it exists and we don't merge.
-               # We are probably just a simple branch switch.
-               # Use whatever value we just read.
-               #
-               set new      $cur
-               set new_hash $cur
-       } elseif {$new eq $cur} {
-               # No merge would be required, don't compute anything.
-               #
-       } else {
-               catch {set merge_base [git merge-base $new $cur]}
-               if {$merge_base eq $cur} {
-                       # The current branch is older.
-                       #
-                       set reflog_msg "merge $new_expr: Fast-forward"
-               } else {
-                       switch -- $merge_type {
-                       ff {
-                               if {$merge_base eq $new} {
-                                       # The current branch is actually newer.
-                                       #
-                                       set new $cur
-                                       set new_hash $cur
-                               } else {
-                                       _error $this [mc "Branch '%s' already exists.\n\nIt cannot fast-forward to %s.\nA merge is required." $newbranch $new_expr]
-                                       return 0
-                               }
-                       }
-                       reset {
-                               # The current branch will lose things.
-                               #
-                               if {[_confirm_reset $this $cur]} {
-                                       set reflog_msg "reset $new_expr"
-                               } else {
-                                       return 0
-                               }
-                       }
-                       default {
-                               _error $this [mc "Merge strategy '%s' not supported." $merge_type]
-                               return 0
-                       }
-                       }
-               }
-       }
-
-       if {$new ne $cur} {
-               if {$is_current} {
-                       # No so fast.  We should defer this in case
-                       # we cannot update the working directory.
-                       #
-                       set update_old $cur
-                       return 1
-               }
-
-               if {[catch {
-                               git update-ref -m $reflog_msg $ref $new $cur
-                       } err]} {
-                       _error $this [strcat [mc "Failed to update '%s'." $newbranch] "\n\n$err"]
-                       return 0
-               }
-       }
-
-       return 1
-}
-
-method _checkout {} {
-       if {[lock_index checkout_op]} {
-               after idle [cb _start_checkout]
-       } else {
-               _error $this [mc "Staging area (index) is already locked."]
-               delete_this
-       }
-}
-
-method _start_checkout {} {
-       global HEAD commit_type
-
-       # -- Our in memory state should match the repository.
-       #
-       repository_state curType curHEAD curMERGE_HEAD
-       if {[string match amend* $commit_type]
-               && $curType eq {normal}
-               && $curHEAD eq $HEAD} {
-       } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
-               info_popup [mc "Last scanned state does not match repository state.
-
-Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
-
-The rescan will be automatically started now.
-"]
-               unlock_index
-               rescan ui_ready
-               delete_this
-               return
-       }
-
-       if {$curHEAD eq $new_hash} {
-               _after_readtree $this
-       } elseif {[is_config_true gui.trustmtime]} {
-               _readtree $this
-       } else {
-               ui_status {Refreshing file status...}
-               set fd [git_read update-index \
-                       -q \
-                       --unmerged \
-                       --ignore-missing \
-                       --refresh \
-                       ]
-               fconfigure $fd -blocking 0 -translation binary
-               fileevent $fd readable [cb _refresh_wait $fd]
-       }
-}
-
-method _refresh_wait {fd} {
-       read $fd
-       if {[eof $fd]} {
-               close $fd
-               _readtree $this
-       }
-}
-
-method _name {} {
-       if {$new_ref eq {}} {
-               return [string range $new_hash 0 7]
-       }
-
-       set rh refs/heads/
-       set rn [string length $rh]
-       if {[string equal -length $rn $rh $new_ref]} {
-               return [string range $new_ref $rn end]
-       } else {
-               return $new_ref
-       }
-}
-
-method _readtree {} {
-       global HEAD
-
-       set readtree_d {}
-       $::main_status start \
-               [mc "Updating working directory to '%s'..." [_name $this]] \
-               {files checked out}
-
-       set fd [git_read --stderr read-tree \
-               -m \
-               -u \
-               -v \
-               --exclude-per-directory=.gitignore \
-               $HEAD \
-               $new_hash \
-               ]
-       fconfigure $fd -blocking 0 -translation binary
-       fileevent $fd readable [cb _readtree_wait $fd]
-}
-
-method _readtree_wait {fd} {
-       global current_branch
-
-       set buf [read $fd]
-       $::main_status update_meter $buf
-       append readtree_d $buf
-
-       fconfigure $fd -blocking 1
-       if {![eof $fd]} {
-               fconfigure $fd -blocking 0
-               return
-       }
-
-       if {[catch {close $fd}]} {
-               set err $readtree_d
-               regsub {^fatal: } $err {} err
-               $::main_status stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]]
-               warn_popup [strcat [mc "File level merge required."] "
-
-$err
-
-" [mc "Staying on branch '%s'." $current_branch]]
-               unlock_index
-               delete_this
-               return
-       }
-
-       $::main_status stop
-       _after_readtree $this
-}
-
-method _after_readtree {} {
-       global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
-       global current_branch is_detached
-       global ui_comm
-
-       set name [_name $this]
-       set log "checkout: moving"
-       if {!$is_detached} {
-               append log " from $current_branch"
-       }
-
-       # -- Move/create HEAD as a symbolic ref.  Core git does not
-       #    even check for failure here, it Just Works(tm).  If it
-       #    doesn't we are in some really ugly state that is difficult
-       #    to recover from within git-gui.
-       #
-       set rh refs/heads/
-       set rn [string length $rh]
-       if {[string equal -length $rn $rh $new_ref]} {
-               set new_branch [string range $new_ref $rn end]
-               if {$is_detached || $current_branch ne $new_branch} {
-                       append log " to $new_branch"
-                       if {[catch {
-                                       git symbolic-ref -m $log HEAD $new_ref
-                               } err]} {
-                               _fatal $this $err
-                       }
-                       set current_branch $new_branch
-                       set is_detached 0
-               }
-       } else {
-               if {!$is_detached || $new_hash ne $HEAD} {
-                       append log " to $new_expr"
-                       if {[catch {
-                                       _detach_HEAD $log $new_hash
-                               } err]} {
-                               _fatal $this $err
-                       }
-               }
-               set current_branch HEAD
-               set is_detached 1
-       }
-
-       # -- We had to defer updating the branch itself until we
-       #    knew the working directory would update.  So now we
-       #    need to finish that work.  If it fails we're in big
-       #    trouble.
-       #
-       if {$update_old ne {}} {
-               if {[catch {
-                               git update-ref \
-                                       -m $reflog_msg \
-                                       $new_ref \
-                                       $new_hash \
-                                       $update_old
-                       } err]} {
-                       _fatal $this $err
-               }
-       }
-
-       if {$is_detached} {
-               info_popup [mc "You are no longer on a local branch.
-
-If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."]
-       }
-
-       # -- Update our repository state.  If we were previously in
-       #    amend mode we need to toss the current buffer and do a
-       #    full rescan to update our file lists.  If we weren't in
-       #    amend mode our file lists are accurate and we can avoid
-       #    the rescan.
-       #
-       unlock_index
-       set selected_commit_type new
-       if {[string match amend* $commit_type]} {
-               $ui_comm delete 0.0 end
-               $ui_comm edit reset
-               $ui_comm edit modified false
-               rescan [list ui_status [mc "Checked out '%s'." $name]]
-       } else {
-               repository_state commit_type HEAD MERGE_HEAD
-               set PARENT $HEAD
-               ui_status "Checked out '$name'."
-       }
-       delete_this
-}
-
-git-version proc _detach_HEAD {log new} {
-       >= 1.5.3 {
-               git update-ref --no-deref -m $log HEAD $new
-       }
-       default {
-               set p [gitdir HEAD]
-               file delete $p
-               set fd [open $p w]
-               fconfigure $fd -translation lf -encoding utf-8
-               puts $fd $new
-               close $fd
-       }
-}
-
-method _confirm_reset {cur} {
-       set reset_ok 0
-       set name [_name $this]
-       set gitk [list do_gitk [list $cur ^$new_hash]]
-
-       _toplevel $this {Confirm Branch Reset}
-       pack [label $w.msg1 \
-               -anchor w \
-               -justify left \
-               -text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\
-               ] -anchor w
-
-       set list $w.list.l
-       frame $w.list
-       text $list \
-               -font font_diff \
-               -width 80 \
-               -height 10 \
-               -wrap none \
-               -xscrollcommand [list $w.list.sbx set] \
-               -yscrollcommand [list $w.list.sby set]
-       scrollbar $w.list.sbx -orient h -command [list $list xview]
-       scrollbar $w.list.sby -orient v -command [list $list yview]
-       pack $w.list.sbx -fill x -side bottom
-       pack $w.list.sby -fill y -side right
-       pack $list -fill both -expand 1
-       pack $w.list -fill both -expand 1 -padx 5 -pady 5
-
-       pack [label $w.msg2 \
-               -anchor w \
-               -justify left \
-               -text [mc "Recovering lost commits may not be easy."] \
-               ]
-       pack [label $w.msg3 \
-               -anchor w \
-               -justify left \
-               -text [mc "Reset '%s'?" $name] \
-               ]
-
-       frame $w.buttons
-       button $w.buttons.visualize \
-               -text [mc Visualize] \
-               -command $gitk
-       pack $w.buttons.visualize -side left
-       button $w.buttons.reset \
-               -text [mc Reset] \
-               -command "
-                       set @reset_ok 1
-                       destroy $w
-               "
-       pack $w.buttons.reset -side right
-       button $w.buttons.cancel \
-               -default active \
-               -text [mc Cancel] \
-               -command [list destroy $w]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
-       while {[gets $fd line] > 0} {
-               set abbr [string range $line 0 7]
-               set subj [string range $line 41 end]
-               $list insert end "$abbr  $subj\n"
-       }
-       close $fd
-       $list configure -state disabled
-
-       bind $w    <Key-v> $gitk
-       bind $w <Visibility> "
-               grab $w
-               focus $w.buttons.cancel
-       "
-       bind $w <Key-Return> [list destroy $w]
-       bind $w <Key-Escape> [list destroy $w]
-       tkwait window $w
-       return $reset_ok
-}
-
-method _error {msg} {
-       if {[winfo ismapped $parent_w]} {
-               set p $parent_w
-       } else {
-               set p .
-       }
-
-       tk_messageBox \
-               -icon error \
-               -type ok \
-               -title [wm title $p] \
-               -parent $p \
-               -message $msg
-}
-
-method _toplevel {title} {
-       regsub -all {::} $this {__} w
-       set w .$w
-
-       if {[winfo ismapped $parent_w]} {
-               set p $parent_w
-       } else {
-               set p .
-       }
-
-       toplevel $w
-       wm title $w $title
-       wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
-}
-
-method _fatal {err} {
-       error_popup [strcat [mc "Failed to set current branch.
-
-This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
-
-This should not have occurred.  %s will now close and give up." [appname]] "
-
-$err"]
-       exit 1
-}
-
-}
diff --git a/lib/choose_font.tcl b/lib/choose_font.tcl
deleted file mode 100644 (file)
index 0c4051b..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-# git-gui font chooser
-# Copyright (C) 2007 Shawn Pearce
-
-class choose_font {
-
-field w
-field w_family    ; # UI widget of all known family names
-field w_example   ; # Example to showcase the chosen font
-
-field f_family    ; # Currently chosen family name
-field f_size      ; # Currently chosen point size
-
-field v_family    ; # Name of global variable for family
-field v_size      ; # Name of global variable for size
-
-variable all_families [list]  ; # All fonts known to Tk
-
-constructor pick {path title a_family a_size} {
-       variable all_families
-
-       set v_family $a_family
-       set v_size $a_size
-
-       upvar #0 $v_family pv_family
-       upvar #0 $v_size pv_size
-
-       set f_family $pv_family
-       set f_size $pv_size
-
-       make_toplevel top w
-       wm title $top "[appname] ([reponame]): $title"
-       wm geometry $top "+[winfo rootx $path]+[winfo rooty $path]"
-
-       label $w.header -text $title -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       button $w.buttons.select \
-               -text [mc Select] \
-               -default active \
-               -command [cb _select]
-       button $w.buttons.cancel \
-               -text [mc Cancel] \
-               -command [list destroy $w]
-       pack $w.buttons.select -side right
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       frame $w.inner
-
-       frame $w.inner.family
-       label $w.inner.family.l \
-               -text [mc "Font Family"] \
-               -anchor w
-       set w_family $w.inner.family.v
-       text $w_family \
-               -background white \
-               -borderwidth 1 \
-               -relief sunken \
-               -cursor $::cursor_ptr \
-               -wrap none \
-               -width 30 \
-               -height 10 \
-               -yscrollcommand [list $w.inner.family.sby set]
-       rmsel_tag $w_family
-       scrollbar $w.inner.family.sby -command [list $w_family yview]
-       pack $w.inner.family.l -side top -fill x
-       pack $w.inner.family.sby -side right -fill y
-       pack $w_family -fill both -expand 1
-
-       frame $w.inner.size
-       label $w.inner.size.l \
-               -text [mc "Font Size"] \
-               -anchor w
-       spinbox $w.inner.size.v \
-               -textvariable @f_size \
-               -from 2 -to 80 -increment 1 \
-               -width 3
-       bind $w.inner.size.v <FocusIn> {%W selection range 0 end}
-       pack $w.inner.size.l -fill x -side top
-       pack $w.inner.size.v -fill x -padx 2
-
-       grid configure $w.inner.family $w.inner.size -sticky nsew
-       grid rowconfigure $w.inner 0 -weight 1
-       grid columnconfigure $w.inner 0 -weight 1
-       pack $w.inner -fill both -expand 1 -padx 5 -pady 5
-
-       frame $w.example
-       label $w.example.l \
-               -text [mc "Font Example"] \
-               -anchor w
-       set w_example $w.example.t
-       text $w_example \
-               -background white \
-               -borderwidth 1 \
-               -relief sunken \
-               -height 3 \
-               -width 40
-       rmsel_tag $w_example
-       $w_example tag conf example -justify center
-       $w_example insert end [mc "This is example text.\nIf you like this text, it can be your font."] example
-       $w_example conf -state disabled
-       pack $w.example.l -fill x
-       pack $w_example -fill x
-       pack $w.example -fill x -padx 5
-
-       if {$all_families eq {}} {
-               set all_families [lsort [font families]]
-       }
-
-       $w_family tag conf pick
-       $w_family tag bind pick <Button-1> [cb _pick_family %x %y]\;break
-       foreach f $all_families {
-               set sel [list pick]
-               if {$f eq $f_family} {
-                       lappend sel in_sel
-               }
-               $w_family insert end "$f\n" $sel
-       }
-       $w_family conf -state disabled
-       _update $this
-
-       trace add variable @f_size write [cb _update]
-       bind $w <Key-Escape> [list destroy $w]
-       bind $w <Key-Return> [cb _select]\;break
-       bind $w <Visibility> "
-               grab $w
-               focus $w
-       "
-       tkwait window $w
-}
-
-method _select {} {
-       upvar #0 $v_family pv_family
-       upvar #0 $v_size pv_size
-
-       set pv_family $f_family
-       set pv_size $f_size
-
-       destroy $w
-}
-
-method _pick_family {x y} {
-       variable all_families
-
-       set i [lindex [split [$w_family index @$x,$y] .] 0]
-       set n [lindex $all_families [expr {$i - 1}]]
-       if {$n ne {}} {
-               $w_family tag remove in_sel 0.0 end
-               $w_family tag add in_sel $i.0 [expr {$i + 1}].0
-               set f_family $n
-               _update $this
-       }
-}
-
-method _update {args} {
-       variable all_families
-
-       set i [lsearch -exact $all_families $f_family]
-       if {$i < 0} return
-
-       $w_example tag conf example -font [list $f_family $f_size]
-       $w_family see [expr {$i + 1}].0
-}
-
-}
diff --git a/lib/choose_repository.tcl b/lib/choose_repository.tcl
deleted file mode 100644 (file)
index 2bac50e..0000000
+++ /dev/null
@@ -1,1044 +0,0 @@
-# git-gui Git repository chooser
-# Copyright (C) 2007 Shawn Pearce
-
-class choose_repository {
-
-field top
-field w
-field w_body      ; # Widget holding the center content
-field w_next      ; # Next button
-field w_quit      ; # Quit button
-field o_cons      ; # Console object (if active)
-field w_types     ; # List of type buttons in clone
-field w_recentlist ; # Listbox containing recent repositories
-
-field done              0 ; # Finished picking the repository?
-field local_path       {} ; # Where this repository is locally
-field origin_url       {} ; # Where we are cloning from
-field origin_name  origin ; # What we shall call 'origin'
-field clone_type hardlink ; # Type of clone to construct
-field readtree_err        ; # Error output from read-tree (if any)
-field sorted_recent       ; # recent repositories (sorted)
-
-constructor pick {} {
-       global M1T M1B
-
-       make_toplevel top w
-       wm title $top [mc "Git Gui"]
-
-       if {$top eq {.}} {
-               menu $w.mbar -tearoff 0
-               $top configure -menu $w.mbar
-
-               set m_repo $w.mbar.repository
-               $w.mbar add cascade \
-                       -label [mc Repository] \
-                       -menu $m_repo
-               menu $m_repo
-
-               if {[is_MacOSX]} {
-                       $w.mbar add cascade -label [mc Apple] -menu .mbar.apple
-                       menu $w.mbar.apple
-                       $w.mbar.apple add command \
-                               -label [mc "About %s" [appname]] \
-                               -command do_about
-               } else {
-                       $w.mbar add cascade -label [mc Help] -menu $w.mbar.help
-                       menu $w.mbar.help
-                       $w.mbar.help add command \
-                               -label [mc "About %s" [appname]] \
-                               -command do_about
-               }
-
-               wm protocol $top WM_DELETE_WINDOW exit
-               bind $top <$M1B-q> exit
-               bind $top <$M1B-Q> exit
-               bind $top <Key-Escape> exit
-       } else {
-               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
-               bind $top <Key-Escape> [list destroy $top]
-               set m_repo {}
-       }
-
-       pack [git_logo $w.git_logo] -side left -fill y -padx 10 -pady 10
-
-       set w_body $w.body
-       set opts $w_body.options
-       frame $w_body
-       text $opts \
-               -cursor $::cursor_ptr \
-               -relief flat \
-               -background [$w_body cget -background] \
-               -wrap none \
-               -spacing1 5 \
-               -width 50 \
-               -height 3
-       pack $opts -anchor w -fill x
-
-       $opts tag conf link_new -foreground blue -underline 1
-       $opts tag bind link_new <1> [cb _next new]
-       $opts insert end [mc "Create New Repository"] link_new
-       $opts insert end "\n"
-       if {$m_repo ne {}} {
-               $m_repo add command \
-                       -command [cb _next new] \
-                       -accelerator $M1T-N \
-                       -label [mc "New..."]
-               bind $top <$M1B-n> [cb _next new]
-               bind $top <$M1B-N> [cb _next new]
-       }
-
-       $opts tag conf link_clone -foreground blue -underline 1
-       $opts tag bind link_clone <1> [cb _next clone]
-       $opts insert end [mc "Clone Existing Repository"] link_clone
-       $opts insert end "\n"
-       if {$m_repo ne {}} {
-               $m_repo add command \
-                       -command [cb _next clone] \
-                       -accelerator $M1T-C \
-                       -label [mc "Clone..."]
-               bind $top <$M1B-c> [cb _next clone]
-               bind $top <$M1B-C> [cb _next clone]
-       }
-
-       $opts tag conf link_open -foreground blue -underline 1
-       $opts tag bind link_open <1> [cb _next open]
-       $opts insert end [mc "Open Existing Repository"] link_open
-       $opts insert end "\n"
-       if {$m_repo ne {}} {
-               $m_repo add command \
-                       -command [cb _next open] \
-                       -accelerator $M1T-O \
-                       -label [mc "Open..."]
-               bind $top <$M1B-o> [cb _next open]
-               bind $top <$M1B-O> [cb _next open]
-       }
-
-       $opts conf -state disabled
-
-       set sorted_recent [_get_recentrepos]
-       if {[llength $sorted_recent] > 0} {
-               if {$m_repo ne {}} {
-                       $m_repo add separator
-                       $m_repo add command \
-                               -state disabled \
-                               -label [mc "Recent Repositories"]
-               }
-
-               label $w_body.space
-               label $w_body.recentlabel \
-                       -anchor w \
-                       -text [mc "Open Recent Repository:"]
-               set w_recentlist $w_body.recentlist
-               text $w_recentlist \
-                       -cursor $::cursor_ptr \
-                       -relief flat \
-                       -background [$w_body.recentlabel cget -background] \
-                       -wrap none \
-                       -width 50 \
-                       -height 10
-               $w_recentlist tag conf link \
-                       -foreground blue \
-                       -underline 1
-               set home $::env(HOME)
-               if {[is_Cygwin]} {
-                       set home [exec cygpath --windows --absolute $home]
-               }
-               set home "[file normalize $home]/"
-               set hlen [string length $home]
-               foreach p $sorted_recent {
-                       set path $p
-                       if {[string equal -length $hlen $home $p]} {
-                               set p "~/[string range $p $hlen end]"
-                       }
-                       regsub -all "\n" $p "\\n" p
-                       $w_recentlist insert end $p link
-                       $w_recentlist insert end "\n"
-
-                       if {$m_repo ne {}} {
-                               $m_repo add command \
-                                       -command [cb _open_recent_path $path] \
-                                       -label "    $p"
-                       }
-               }
-               $w_recentlist conf -state disabled
-               $w_recentlist tag bind link <1> [cb _open_recent %x,%y]
-               pack $w_body.space -anchor w -fill x
-               pack $w_body.recentlabel -anchor w -fill x
-               pack $w_recentlist -anchor w -fill x
-       }
-       pack $w_body -fill x -padx 10 -pady 10
-
-       frame $w.buttons
-       set w_next $w.buttons.next
-       set w_quit $w.buttons.quit
-       button $w_quit \
-               -text [mc "Quit"] \
-               -command exit
-       pack $w_quit -side right -padx 5
-       pack $w.buttons -side bottom -fill x -padx 10 -pady 10
-
-       if {$m_repo ne {}} {
-               $m_repo add separator
-               $m_repo add command \
-                       -label [mc Quit] \
-                       -command exit \
-                       -accelerator $M1T-Q
-       }
-
-       bind $top <Return> [cb _invoke_next]
-       bind $top <Visibility> "
-               [cb _center]
-               grab $top
-               focus $top
-               bind $top <Visibility> {}
-       "
-       wm deiconify $top
-       tkwait variable @done
-
-       if {$top eq {.}} {
-               eval destroy [winfo children $top]
-       }
-}
-
-proc _home {} {
-       if {[catch {set h $::env(HOME)}]
-               || ![file isdirectory $h]} {
-               set h .
-       }
-       return $h
-}
-
-method _center {} {
-       set nx [winfo reqwidth $top]
-       set ny [winfo reqheight $top]
-       set rx [expr {([winfo screenwidth  $top] - $nx) / 3}]
-       set ry [expr {([winfo screenheight $top] - $ny) / 3}]
-       wm geometry $top [format {+%d+%d} $rx $ry]
-}
-
-method _invoke_next {} {
-       if {[winfo exists $w_next]} {
-               uplevel #0 [$w_next cget -command]
-       }
-}
-
-proc _get_recentrepos {} {
-       set recent [list]
-       foreach p [get_config gui.recentrepo] {
-               if {[_is_git [file join $p .git]]} {
-                       lappend recent $p
-               }
-       }
-       return [lsort $recent]
-}
-
-proc _unset_recentrepo {p} {
-       regsub -all -- {([()\[\]{}\.^$+*?\\])} $p {\\\1} p
-       git config --global --unset gui.recentrepo "^$p\$"
-}
-
-proc _append_recentrepos {path} {
-       set path [file normalize $path]
-       set recent [get_config gui.recentrepo]
-
-       if {[lindex $recent end] eq $path} {
-               return
-       }
-
-       set i [lsearch $recent $path]
-       if {$i >= 0} {
-               _unset_recentrepo $path
-               set recent [lreplace $recent $i $i]
-       }
-
-       lappend recent $path
-       git config --global --add gui.recentrepo $path
-
-       while {[llength $recent] > 10} {
-               _unset_recentrepo [lindex $recent 0]
-               set recent [lrange $recent 1 end]
-       }
-}
-
-method _open_recent {xy} {
-       set id [lindex [split [$w_recentlist index @$xy] .] 0]
-       set local_path [lindex $sorted_recent [expr {$id - 1}]]
-       _do_open2 $this
-}
-
-method _open_recent_path {p} {
-       set local_path $p
-       _do_open2 $this
-}
-
-method _next {action} {
-       destroy $w_body
-       if {![winfo exists $w_next]} {
-               button $w_next -default active
-               pack $w_next -side right -padx 5 -before $w_quit
-       }
-       _do_$action $this
-}
-
-method _write_local_path {args} {
-       if {$local_path eq {}} {
-               $w_next conf -state disabled
-       } else {
-               $w_next conf -state normal
-       }
-}
-
-method _git_init {} {
-       if {[file exists $local_path]} {
-               error_popup [mc "Location %s already exists." $local_path]
-               return 0
-       }
-
-       if {[catch {file mkdir $local_path} err]} {
-               error_popup [strcat \
-                       [mc "Failed to create repository %s:" $local_path] \
-                       "\n\n$err"]
-               return 0
-       }
-
-       if {[catch {cd $local_path} err]} {
-               error_popup [strcat \
-                       [mc "Failed to create repository %s:" $local_path] \
-                       "\n\n$err"]
-               return 0
-       }
-
-       if {[catch {git init} err]} {
-               error_popup [strcat \
-                       [mc "Failed to create repository %s:" $local_path] \
-                       "\n\n$err"]
-               return 0
-       }
-
-       _append_recentrepos [pwd]
-       set ::_gitdir .git
-       set ::_prefix {}
-       return 1
-}
-
-proc _is_git {path} {
-       if {[file exists [file join $path HEAD]]
-        && [file exists [file join $path objects]]
-        && [file exists [file join $path config]]} {
-               return 1
-       }
-       if {[is_Cygwin]} {
-               if {[file exists [file join $path HEAD]]
-                && [file exists [file join $path objects.lnk]]
-                && [file exists [file join $path config.lnk]]} {
-                       return 1
-               }
-       }
-       return 0
-}
-
-proc _objdir {path} {
-       set objdir [file join $path .git objects]
-       if {[file isdirectory $objdir]} {
-               return $objdir
-       }
-
-       set objdir [file join $path objects]
-       if {[file isdirectory $objdir]} {
-               return $objdir
-       }
-
-       if {[is_Cygwin]} {
-               set objdir [file join $path .git objects.lnk]
-               if {[file isfile $objdir]} {
-                       return [win32_read_lnk $objdir]
-               }
-
-               set objdir [file join $path objects.lnk]
-               if {[file isfile $objdir]} {
-                       return [win32_read_lnk $objdir]
-               }
-       }
-
-       return {}
-}
-
-######################################################################
-##
-## Create New Repository
-
-method _do_new {} {
-       $w_next conf \
-               -state disabled \
-               -command [cb _do_new2] \
-               -text [mc "Create"]
-
-       frame $w_body
-       label $w_body.h \
-               -font font_uibold \
-               -text [mc "Create New Repository"]
-       pack $w_body.h -side top -fill x -pady 10
-       pack $w_body -fill x -padx 10
-
-       frame $w_body.where
-       label $w_body.where.l -text [mc "Directory:"]
-       entry $w_body.where.t \
-               -textvariable @local_path \
-               -font font_diff \
-               -width 50
-       button $w_body.where.b \
-               -text [mc "Browse"] \
-               -command [cb _new_local_path]
-
-       pack $w_body.where.b -side right
-       pack $w_body.where.l -side left
-       pack $w_body.where.t -fill x
-       pack $w_body.where -fill x
-
-       trace add variable @local_path write [cb _write_local_path]
-       bind $w_body.h <Destroy> [list trace remove variable @local_path write [cb _write_local_path]]
-       update
-       focus $w_body.where.t
-}
-
-method _new_local_path {} {
-       if {$local_path ne {}} {
-               set p [file dirname $local_path]
-       } else {
-               set p [_home]
-       }
-
-       set p [tk_chooseDirectory \
-               -initialdir $p \
-               -parent $top \
-               -title [mc "Git Repository"] \
-               -mustexist false]
-       if {$p eq {}} return
-
-       set p [file normalize $p]
-       if {[file isdirectory $p]} {
-               foreach i [glob \
-                       -directory $p \
-                       -tails \
-                       -nocomplain \
-                       * .*] {
-                       switch -- $i {
-                        . continue
-                       .. continue
-                       default {
-                               error_popup [mc "Directory %s already exists." $p]
-                               return
-                       }
-                       }
-               }
-               if {[catch {file delete $p} err]} {
-                       error_popup [strcat \
-                               [mc "Directory %s already exists." $p] \
-                               "\n\n$err"]
-                       return
-               }
-       } elseif {[file exists $p]} {
-               error_popup [mc "File %s already exists." $p]
-               return
-       }
-       set local_path $p
-}
-
-method _do_new2 {} {
-       if {![_git_init $this]} {
-               return
-       }
-       set done 1
-}
-
-######################################################################
-##
-## Clone Existing Repository
-
-method _do_clone {} {
-       $w_next conf \
-               -state disabled \
-               -command [cb _do_clone2] \
-               -text [mc "Clone"]
-
-       frame $w_body
-       label $w_body.h \
-               -font font_uibold \
-               -text [mc "Clone Existing Repository"]
-       pack $w_body.h -side top -fill x -pady 10
-       pack $w_body -fill x -padx 10
-
-       set args $w_body.args
-       frame $w_body.args
-       pack $args -fill both
-
-       label $args.origin_l -text [mc "URL:"]
-       entry $args.origin_t \
-               -textvariable @origin_url \
-               -font font_diff \
-               -width 50
-       button $args.origin_b \
-               -text [mc "Browse"] \
-               -command [cb _open_origin]
-       grid $args.origin_l $args.origin_t $args.origin_b -sticky ew
-
-       label $args.where_l -text [mc "Directory:"]
-       entry $args.where_t \
-               -textvariable @local_path \
-               -font font_diff \
-               -width 50
-       button $args.where_b \
-               -text [mc "Browse"] \
-               -command [cb _new_local_path]
-       grid $args.where_l $args.where_t $args.where_b -sticky ew
-
-       label $args.type_l -text [mc "Clone Type:"]
-       frame $args.type_f
-       set w_types [list]
-       lappend w_types [radiobutton $args.type_f.hardlink \
-               -state disabled \
-               -anchor w \
-               -text [mc "Standard (Fast, Semi-Redundant, Hardlinks)"] \
-               -variable @clone_type \
-               -value hardlink]
-       lappend w_types [radiobutton $args.type_f.full \
-               -state disabled \
-               -anchor w \
-               -text [mc "Full Copy (Slower, Redundant Backup)"] \
-               -variable @clone_type \
-               -value full]
-       lappend w_types [radiobutton $args.type_f.shared \
-               -state disabled \
-               -anchor w \
-               -text [mc "Shared (Fastest, Not Recommended, No Backup)"] \
-               -variable @clone_type \
-               -value shared]
-       foreach r $w_types {
-               pack $r -anchor w
-       }
-       grid $args.type_l $args.type_f -sticky new
-
-       grid columnconfigure $args 1 -weight 1
-
-       trace add variable @local_path write [cb _update_clone]
-       trace add variable @origin_url write [cb _update_clone]
-       bind $w_body.h <Destroy> "
-               [list trace remove variable @local_path write [cb _update_clone]]
-               [list trace remove variable @origin_url write [cb _update_clone]]
-       "
-       update
-       focus $args.origin_t
-}
-
-method _open_origin {} {
-       if {$origin_url ne {} && [file isdirectory $origin_url]} {
-               set p $origin_url
-       } else {
-               set p [_home]
-       }
-
-       set p [tk_chooseDirectory \
-               -initialdir $p \
-               -parent $top \
-               -title [mc "Git Repository"] \
-               -mustexist true]
-       if {$p eq {}} return
-
-       set p [file normalize $p]
-       if {![_is_git [file join $p .git]] && ![_is_git $p]} {
-               error_popup [mc "Not a Git repository: %s" [file tail $p]]
-               return
-       }
-       set origin_url $p
-}
-
-method _update_clone {args} {
-       if {$local_path ne {} && $origin_url ne {}} {
-               $w_next conf -state normal
-       } else {
-               $w_next conf -state disabled
-       }
-
-       if {$origin_url ne {} &&
-               (  [_is_git [file join $origin_url .git]]
-               || [_is_git $origin_url])} {
-               set e normal
-               if {[[lindex $w_types 0] cget -state] eq {disabled}} {
-                       set clone_type hardlink
-               }
-       } else {
-               set e disabled
-               set clone_type full
-       }
-
-       foreach r $w_types {
-               $r conf -state $e
-       }
-}
-
-method _do_clone2 {} {
-       if {[file isdirectory $origin_url]} {
-               set origin_url [file normalize $origin_url]
-       }
-
-       if {$clone_type eq {hardlink} && ![file isdirectory $origin_url]} {
-               error_popup [mc "Standard only available for local repository."]
-               return
-       }
-       if {$clone_type eq {shared} && ![file isdirectory $origin_url]} {
-               error_popup [mc "Shared only available for local repository."]
-               return
-       }
-
-       if {$clone_type eq {hardlink} || $clone_type eq {shared}} {
-               set objdir [_objdir $origin_url]
-               if {$objdir eq {}} {
-                       error_popup [mc "Not a Git repository: %s" [file tail $origin_url]]
-                       return
-               }
-       }
-
-       set giturl $origin_url
-       if {[is_Cygwin] && [file isdirectory $giturl]} {
-               set giturl [exec cygpath --unix --absolute $giturl]
-               if {$clone_type eq {shared}} {
-                       set objdir [exec cygpath --unix --absolute $objdir]
-               }
-       }
-
-       if {![_git_init $this]} return
-       set local_path [pwd]
-
-       if {[catch {
-                       git config remote.$origin_name.url $giturl
-                       git config remote.$origin_name.fetch +refs/heads/*:refs/remotes/$origin_name/*
-               } err]} {
-               error_popup [strcat [mc "Failed to configure origin"] "\n\n$err"]
-               return
-       }
-
-       destroy $w_body $w_next
-
-       switch -exact -- $clone_type {
-       hardlink {
-               set o_cons [status_bar::two_line $w_body]
-               pack $w_body -fill x -padx 10 -pady 10
-
-               $o_cons start \
-                       [mc "Counting objects"] \
-                       [mc "buckets"]
-               update
-
-               if {[file exists [file join $objdir info alternates]]} {
-                       set pwd [pwd]
-                       if {[catch {
-                               file mkdir [gitdir objects info]
-                               set f_in [open [file join $objdir info alternates] r]
-                               set f_cp [open [gitdir objects info alternates] w]
-                               fconfigure $f_in -translation binary -encoding binary
-                               fconfigure $f_cp -translation binary -encoding binary
-                               cd $objdir
-                               while {[gets $f_in line] >= 0} {
-                                       if {[is_Cygwin]} {
-                                               puts $f_cp [exec cygpath --unix --absolute $line]
-                                       } else {
-                                               puts $f_cp [file normalize $line]
-                                       }
-                               }
-                               close $f_in
-                               close $f_cp
-                               cd $pwd
-                       } err]} {
-                               catch {cd $pwd}
-                               _clone_failed $this [mc "Unable to copy objects/info/alternates: %s" $err]
-                               return
-                       }
-               }
-
-               set tolink  [list]
-               set buckets [glob \
-                       -tails \
-                       -nocomplain \
-                       -directory [file join $objdir] ??]
-               set bcnt [expr {[llength $buckets] + 2}]
-               set bcur 1
-               $o_cons update $bcur $bcnt
-               update
-
-               file mkdir [file join .git objects pack]
-               foreach i [glob -tails -nocomplain \
-                       -directory [file join $objdir pack] *] {
-                       lappend tolink [file join pack $i]
-               }
-               $o_cons update [incr bcur] $bcnt
-               update
-
-               foreach i $buckets {
-                       file mkdir [file join .git objects $i]
-                       foreach j [glob -tails -nocomplain \
-                               -directory [file join $objdir $i] *] {
-                               lappend tolink [file join $i $j]
-                       }
-                       $o_cons update [incr bcur] $bcnt
-                       update
-               }
-               $o_cons stop
-
-               if {$tolink eq {}} {
-                       info_popup [strcat \
-                               [mc "Nothing to clone from %s." $origin_url] \
-                               "\n" \
-                               [mc "The 'master' branch has not been initialized."] \
-                               ]
-                       destroy $w_body
-                       set done 1
-                       return
-               }
-
-               set i [lindex $tolink 0]
-               if {[catch {
-                               file link -hard \
-                                       [file join .git objects $i] \
-                                       [file join $objdir $i]
-                       } err]} {
-                       info_popup [mc "Hardlinks are unavailable.  Falling back to copying."]
-                       set i [_copy_files $this $objdir $tolink]
-               } else {
-                       set i [_link_files $this $objdir [lrange $tolink 1 end]]
-               }
-               if {!$i} return
-
-               destroy $w_body
-       }
-       full {
-               set o_cons [console::embed \
-                       $w_body \
-                       [mc "Cloning from %s" $origin_url]]
-               pack $w_body -fill both -expand 1 -padx 10
-               $o_cons exec \
-                       [list git fetch --no-tags -k $origin_name] \
-                       [cb _do_clone_tags]
-       }
-       shared {
-               set fd [open [gitdir objects info alternates] w]
-               fconfigure $fd -translation binary
-               puts $fd $objdir
-               close $fd
-       }
-       }
-
-       if {$clone_type eq {hardlink} || $clone_type eq {shared}} {
-               if {![_clone_refs $this]} return
-               set pwd [pwd]
-               if {[catch {
-                               cd $origin_url
-                               set HEAD [git rev-parse --verify HEAD^0]
-                       } err]} {
-                       _clone_failed $this [mc "Not a Git repository: %s" [file tail $origin_url]]
-                       return 0
-               }
-               cd $pwd
-               _do_clone_checkout $this $HEAD
-       }
-}
-
-method _copy_files {objdir tocopy} {
-       $o_cons start \
-               [mc "Copying objects"] \
-               [mc "KiB"]
-       set tot 0
-       set cmp 0
-       foreach p $tocopy {
-               incr tot [file size [file join $objdir $p]]
-       }
-       foreach p $tocopy {
-               if {[catch {
-                               set f_in [open [file join $objdir $p] r]
-                               set f_cp [open [file join .git objects $p] w]
-                               fconfigure $f_in -translation binary -encoding binary
-                               fconfigure $f_cp -translation binary -encoding binary
-
-                               while {![eof $f_in]} {
-                                       incr cmp [fcopy $f_in $f_cp -size 16384]
-                                       $o_cons update \
-                                               [expr {$cmp / 1024}] \
-                                               [expr {$tot / 1024}]
-                                       update
-                               }
-
-                               close $f_in
-                               close $f_cp
-                       } err]} {
-                       _clone_failed $this [mc "Unable to copy object: %s" $err]
-                       return 0
-               }
-       }
-       return 1
-}
-
-method _link_files {objdir tolink} {
-       set total [llength $tolink]
-       $o_cons start \
-               [mc "Linking objects"] \
-               [mc "objects"]
-       for {set i 0} {$i < $total} {} {
-               set p [lindex $tolink $i]
-               if {[catch {
-                               file link -hard \
-                                       [file join .git objects $p] \
-                                       [file join $objdir $p]
-                       } err]} {
-                       _clone_failed $this [mc "Unable to hardlink object: %s" $err]
-                       return 0
-               }
-
-               incr i
-               if {$i % 5 == 0} {
-                       $o_cons update $i $total
-                       update
-               }
-       }
-       return 1
-}
-
-method _clone_refs {} {
-       set pwd [pwd]
-       if {[catch {cd $origin_url} err]} {
-               error_popup [mc "Not a Git repository: %s" [file tail $origin_url]]
-               return 0
-       }
-       set fd_in [git_read for-each-ref \
-               --tcl \
-               {--format=list %(refname) %(objectname) %(*objectname)}]
-       cd $pwd
-
-       set fd [open [gitdir packed-refs] w]
-       fconfigure $fd -translation binary
-       puts $fd "# pack-refs with: peeled"
-       while {[gets $fd_in line] >= 0} {
-               set line [eval $line]
-               set refn [lindex $line 0]
-               set robj [lindex $line 1]
-               set tobj [lindex $line 2]
-
-               if {[regsub ^refs/heads/ $refn \
-                       "refs/remotes/$origin_name/" refn]} {
-                       puts $fd "$robj $refn"
-               } elseif {[string match refs/tags/* $refn]} {
-                       puts $fd "$robj $refn"
-                       if {$tobj ne {}} {
-                               puts $fd "^$tobj"
-                       }
-               }
-       }
-       close $fd_in
-       close $fd
-       return 1
-}
-
-method _do_clone_tags {ok} {
-       if {$ok} {
-               $o_cons exec \
-                       [list git fetch --tags -k $origin_name] \
-                       [cb _do_clone_HEAD]
-       } else {
-               $o_cons done $ok
-               _clone_failed $this [mc "Cannot fetch branches and objects.  See console output for details."]
-       }
-}
-
-method _do_clone_HEAD {ok} {
-       if {$ok} {
-               $o_cons exec \
-                       [list git fetch $origin_name HEAD] \
-                       [cb _do_clone_full_end]
-       } else {
-               $o_cons done $ok
-               _clone_failed $this [mc "Cannot fetch tags.  See console output for details."]
-       }
-}
-
-method _do_clone_full_end {ok} {
-       $o_cons done $ok
-
-       if {$ok} {
-               destroy $w_body
-
-               set HEAD {}
-               if {[file exists [gitdir FETCH_HEAD]]} {
-                       set fd [open [gitdir FETCH_HEAD] r]
-                       while {[gets $fd line] >= 0} {
-                               if {[regexp "^(.{40})\t\t" $line line HEAD]} {
-                                       break
-                               }
-                       }
-                       close $fd
-               }
-
-               catch {git pack-refs}
-               _do_clone_checkout $this $HEAD
-       } else {
-               _clone_failed $this [mc "Cannot determine HEAD.  See console output for details."]
-       }
-}
-
-method _clone_failed {{why {}}} {
-       if {[catch {file delete -force $local_path} err]} {
-               set why [strcat \
-                       $why \
-                       "\n\n" \
-                       [mc "Unable to cleanup %s" $local_path] \
-                       "\n\n" \
-                       $err]
-       }
-       if {$why ne {}} {
-               update
-               error_popup [strcat [mc "Clone failed."] "\n" $why]
-       }
-}
-
-method _do_clone_checkout {HEAD} {
-       if {$HEAD eq {}} {
-               info_popup [strcat \
-                       [mc "No default branch obtained."] \
-                       "\n" \
-                       [mc "The 'master' branch has not been initialized."] \
-                       ]
-               set done 1
-               return
-       }
-       if {[catch {
-                       git update-ref HEAD $HEAD^0
-               } err]} {
-               info_popup [strcat \
-                       [mc "Cannot resolve %s as a commit." $HEAD^0] \
-                       "\n  $err" \
-                       "\n" \
-                       [mc "The 'master' branch has not been initialized."] \
-                       ]
-               set done 1
-               return
-       }
-
-       set o_cons [status_bar::two_line $w_body]
-       pack $w_body -fill x -padx 10 -pady 10
-       $o_cons start \
-               [mc "Creating working directory"] \
-               [mc "files"]
-
-       set readtree_err {}
-       set fd [git_read --stderr read-tree \
-               -m \
-               -u \
-               -v \
-               HEAD \
-               HEAD \
-               ]
-       fconfigure $fd -blocking 0 -translation binary
-       fileevent $fd readable [cb _readtree_wait $fd]
-}
-
-method _readtree_wait {fd} {
-       set buf [read $fd]
-       $o_cons update_meter $buf
-       append readtree_err $buf
-
-       fconfigure $fd -blocking 1
-       if {![eof $fd]} {
-               fconfigure $fd -blocking 0
-               return
-       }
-
-       if {[catch {close $fd}]} {
-               set err $readtree_err
-               regsub {^fatal: } $err {} err
-               error_popup [strcat \
-                       [mc "Initial file checkout failed."] \
-                       "\n\n$err"]
-               return
-       }
-
-       set done 1
-}
-
-######################################################################
-##
-## Open Existing Repository
-
-method _do_open {} {
-       $w_next conf \
-               -state disabled \
-               -command [cb _do_open2] \
-               -text [mc "Open"]
-
-       frame $w_body
-       label $w_body.h \
-               -font font_uibold \
-               -text [mc "Open Existing Repository"]
-       pack $w_body.h -side top -fill x -pady 10
-       pack $w_body -fill x -padx 10
-
-       frame $w_body.where
-       label $w_body.where.l -text [mc "Repository:"]
-       entry $w_body.where.t \
-               -textvariable @local_path \
-               -font font_diff \
-               -width 50
-       button $w_body.where.b \
-               -text [mc "Browse"] \
-               -command [cb _open_local_path]
-
-       pack $w_body.where.b -side right
-       pack $w_body.where.l -side left
-       pack $w_body.where.t -fill x
-       pack $w_body.where -fill x
-
-       trace add variable @local_path write [cb _write_local_path]
-       bind $w_body.h <Destroy> [list trace remove variable @local_path write [cb _write_local_path]]
-       update
-       focus $w_body.where.t
-}
-
-method _open_local_path {} {
-       if {$local_path ne {}} {
-               set p $local_path
-       } else {
-               set p [_home]
-       }
-
-       set p [tk_chooseDirectory \
-               -initialdir $p \
-               -parent $top \
-               -title [mc "Git Repository"] \
-               -mustexist true]
-       if {$p eq {}} return
-
-       set p [file normalize $p]
-       if {![_is_git [file join $p .git]]} {
-               error_popup [mc "Not a Git repository: %s" [file tail $p]]
-               return
-       }
-       set local_path $p
-}
-
-method _do_open2 {} {
-       if {![_is_git [file join $local_path .git]]} {
-               error_popup [mc "Not a Git repository: %s" [file tail $local_path]]
-               return
-       }
-
-       if {[catch {cd $local_path} err]} {
-               error_popup [strcat \
-                       [mc "Failed to open repository %s:" $local_path] \
-                       "\n\n$err"]
-               return
-       }
-
-       _append_recentrepos [pwd]
-       set ::_gitdir .git
-       set ::_prefix {}
-       set done 1
-}
-
-}
diff --git a/lib/choose_rev.tcl b/lib/choose_rev.tcl
deleted file mode 100644 (file)
index a063c5b..0000000
+++ /dev/null
@@ -1,627 +0,0 @@
-# git-gui revision chooser
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-class choose_rev {
-
-image create photo ::choose_rev::img_find -data {R0lGODlhEAAQAIYAAPwCBCQmJDw+PBQSFAQCBMza3NTm5MTW1HyChOT29Ozq7MTq7Kze5Kzm7Oz6/NTy9Iza5GzGzKzS1Nzy9Nz29Kzq9HTGzHTK1Lza3AwKDLzu9JTi7HTW5GTCzITO1Mzq7Hza5FTK1ESyvHzKzKzW3DQyNDyqtDw6PIzW5HzGzAT+/Dw+RKyurNTOzMTGxMS+tJSGdATCxHRydLSqpLymnLSijBweHERCRNze3Pz69PTy9Oze1OTSxOTGrMSqlLy+vPTu5OzSvMymjNTGvNS+tMy2pMyunMSefAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAe4gACCAAECA4OIiAIEBQYHBAKJgwIICQoLDA0IkZIECQ4PCxARCwSSAxITFA8VEBYXGBmJAQYLGhUbHB0eH7KIGRIMEBAgISIjJKaIJQQLFxERIialkieUGigpKRoIBCqJKyyLBwvJAioEyoICLS4v6QQwMQQyLuqLli8zNDU2BCf1lN3AkUPHDh49fAQAAEnGD1MCCALZEaSHkIUMBQS8wWMIkSJGhBzBmFEGgRsBUqpMiSgdAD+BAAAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
-
-field w               ; # our megawidget path
-field w_list          ; # list of currently filtered specs
-field w_filter        ; # filter entry for $w_list
-
-field c_expr        {}; # current revision expression
-field filter          ; # current filter string
-field revtype     head; # type of revision chosen
-field cur_specs [list]; # list of specs for $revtype
-field spec_head       ; # list of all head specs
-field spec_trck       ; # list of all tracking branch specs
-field spec_tag        ; # list of all tag specs
-field tip_data        ; # array of tip commit info by refname
-field log_last        ; # array of reflog date by refname
-
-field tooltip_wm        {} ; # Current tooltip toplevel, if open
-field tooltip_t         {} ; # Text widget in $tooltip_wm
-field tooltip_timer     {} ; # Current timer event for our tooltip
-
-proc new {path {title {}}} {
-       return [_new $path 0 $title]
-}
-
-proc new_unmerged {path {title {}}} {
-       return [_new $path 1 $title]
-}
-
-constructor _new {path unmerged_only title} {
-       global current_branch is_detached
-
-       if {![info exists ::all_remotes]} {
-               load_all_remotes
-       }
-
-       set w $path
-
-       if {$title ne {}} {
-               labelframe $w -text $title
-       } else {
-               frame $w
-       }
-       bind $w <Destroy> [cb _delete %W]
-
-       if {$is_detached} {
-               radiobutton $w.detachedhead_r \
-                       -anchor w \
-                       -text [mc "This Detached Checkout"] \
-                       -value HEAD \
-                       -variable @revtype
-               grid $w.detachedhead_r -sticky we -padx {0 5} -columnspan 2
-       }
-
-       radiobutton $w.expr_r \
-               -text [mc "Revision Expression:"] \
-               -value expr \
-               -variable @revtype
-       entry $w.expr_t \
-               -borderwidth 1 \
-               -relief sunken \
-               -width 50 \
-               -textvariable @c_expr \
-               -validate key \
-               -validatecommand [cb _validate %d %S]
-       grid $w.expr_r $w.expr_t -sticky we -padx {0 5}
-
-       frame $w.types
-       radiobutton $w.types.head_r \
-               -text [mc "Local Branch"] \
-               -value head \
-               -variable @revtype
-       pack $w.types.head_r -side left
-       radiobutton $w.types.trck_r \
-               -text [mc "Tracking Branch"] \
-               -value trck \
-               -variable @revtype
-       pack $w.types.trck_r -side left
-       radiobutton $w.types.tag_r \
-               -text [mc "Tag"] \
-               -value tag \
-               -variable @revtype
-       pack $w.types.tag_r -side left
-       set w_filter $w.types.filter
-       entry $w_filter \
-               -borderwidth 1 \
-               -relief sunken \
-               -width 12 \
-               -textvariable @filter \
-               -validate key \
-               -validatecommand [cb _filter %P]
-       pack $w_filter -side right
-       pack [label $w.types.filter_icon \
-               -image ::choose_rev::img_find \
-               ] -side right
-       grid $w.types -sticky we -padx {0 5} -columnspan 2
-
-       frame $w.list
-       set w_list $w.list.l
-       listbox $w_list \
-               -font font_diff \
-               -width 50 \
-               -height 10 \
-               -selectmode browse \
-               -exportselection false \
-               -xscrollcommand [cb _sb_set $w.list.sbx h] \
-               -yscrollcommand [cb _sb_set $w.list.sby v]
-       pack $w_list -fill both -expand 1
-       grid $w.list -sticky nswe -padx {20 5} -columnspan 2
-       bind $w_list <Any-Motion>  [cb _show_tooltip @%x,%y]
-       bind $w_list <Any-Enter>   [cb _hide_tooltip]
-       bind $w_list <Any-Leave>   [cb _hide_tooltip]
-       bind $w_list <Destroy>     [cb _hide_tooltip]
-
-       grid columnconfigure $w 1 -weight 1
-       if {$is_detached} {
-               grid rowconfigure $w 3 -weight 1
-       } else {
-               grid rowconfigure $w 2 -weight 1
-       }
-
-       trace add variable @revtype write [cb _select]
-       bind $w_filter <Key-Return> [list focus $w_list]\;break
-       bind $w_filter <Key-Down>   [list focus $w_list]
-
-       set fmt list
-       append fmt { %(refname)}
-       append fmt { [list}
-       append fmt { %(objecttype)}
-       append fmt { %(objectname)}
-       append fmt { [concat %(taggername) %(authorname)]}
-       append fmt { [reformat_date [concat %(taggerdate) %(authordate)]]}
-       append fmt { %(subject)}
-       append fmt {] [list}
-       append fmt { %(*objecttype)}
-       append fmt { %(*objectname)}
-       append fmt { %(*authorname)}
-       append fmt { [reformat_date %(*authordate)]}
-       append fmt { %(*subject)}
-       append fmt {]}
-       set all_refn [list]
-       set fr_fd [git_read for-each-ref \
-               --tcl \
-               --sort=-taggerdate \
-               --format=$fmt \
-               refs/heads \
-               refs/remotes \
-               refs/tags \
-               ]
-       fconfigure $fr_fd -translation lf -encoding utf-8
-       while {[gets $fr_fd line] > 0} {
-               set line [eval $line]
-               if {[lindex $line 1 0] eq {tag}} {
-                       if {[lindex $line 2 0] eq {commit}} {
-                               set sha1 [lindex $line 2 1]
-                       } else {
-                               continue
-                       }
-               } elseif {[lindex $line 1 0] eq {commit}} {
-                       set sha1 [lindex $line 1 1]
-               } else {
-                       continue
-               }
-               set refn [lindex $line 0]
-               set tip_data($refn) [lrange $line 1 end]
-               lappend cmt_refn($sha1) $refn
-               lappend all_refn $refn
-       }
-       close $fr_fd
-
-       if {$unmerged_only} {
-               set fr_fd [git_read rev-list --all ^$::HEAD]
-               while {[gets $fr_fd sha1] > 0} {
-                       if {[catch {set rlst $cmt_refn($sha1)}]} continue
-                       foreach refn $rlst {
-                               set inc($refn) 1
-                       }
-               }
-               close $fr_fd
-       } else {
-               foreach refn $all_refn {
-                       set inc($refn) 1
-               }
-       }
-
-       set spec_head [list]
-       foreach name [load_all_heads] {
-               set refn refs/heads/$name
-               if {[info exists inc($refn)]} {
-                       lappend spec_head [list $name $refn]
-               }
-       }
-
-       set spec_trck [list]
-       foreach spec [all_tracking_branches] {
-               set refn [lindex $spec 0]
-               if {[info exists inc($refn)]} {
-                       regsub ^refs/(heads|remotes)/ $refn {} name
-                       lappend spec_trck [concat $name $spec]
-               }
-       }
-
-       set spec_tag [list]
-       foreach name [load_all_tags] {
-               set refn refs/tags/$name
-               if {[info exists inc($refn)]} {
-                       lappend spec_tag [list $name $refn]
-               }
-       }
-
-                 if {$is_detached}             { set revtype HEAD
-       } elseif {[llength $spec_head] > 0} { set revtype head
-       } elseif {[llength $spec_trck] > 0} { set revtype trck
-       } elseif {[llength $spec_tag ] > 0} { set revtype tag
-       } else {                              set revtype expr
-       }
-
-       if {$revtype eq {head} && $current_branch ne {}} {
-               set i 0
-               foreach spec $spec_head {
-                       if {[lindex $spec 0] eq $current_branch} {
-                               $w_list selection clear 0 end
-                               $w_list selection set $i
-                               break
-                       }
-                       incr i
-               }
-       }
-
-       return $this
-}
-
-method none {text} {
-       if {![winfo exists $w.none_r]} {
-               radiobutton $w.none_r \
-                       -anchor w \
-                       -value none \
-                       -variable @revtype
-               grid $w.none_r -sticky we -padx {0 5} -columnspan 2
-       }
-       $w.none_r configure -text $text
-}
-
-method get {} {
-       switch -- $revtype {
-       head -
-       trck -
-       tag  {
-               set i [$w_list curselection]
-               if {$i ne {}} {
-                       return [lindex $cur_specs $i 0]
-               } else {
-                       return {}
-               }
-       }
-
-       HEAD { return HEAD                     }
-       expr { return $c_expr                  }
-       none { return {}                       }
-       default { error "unknown type of revision" }
-       }
-}
-
-method pick_tracking_branch {} {
-       set revtype trck
-}
-
-method focus_filter {} {
-       if {[$w_filter cget -state] eq {normal}} {
-               focus $w_filter
-       }
-}
-
-method bind_listbox {event script}  {
-       bind $w_list $event $script
-}
-
-method get_local_branch {} {
-       if {$revtype eq {head}} {
-               return [_expr $this]
-       } else {
-               return {}
-       }
-}
-
-method get_tracking_branch {} {
-       set i [$w_list curselection]
-       if {$i eq {} || $revtype ne {trck}} {
-               return {}
-       }
-       return [lrange [lindex $cur_specs $i] 1 end]
-}
-
-method get_commit {} {
-       set e [_expr $this]
-       if {$e eq {}} {
-               return {}
-       }
-       return [git rev-parse --verify "$e^0"]
-}
-
-method commit_or_die {} {
-       if {[catch {set new [get_commit $this]} err]} {
-
-               # Cleanup the not-so-friendly error from rev-parse.
-               #
-               regsub {^fatal:\s*} $err {} err
-               if {$err eq {Needed a single revision}} {
-                       set err {}
-               }
-
-               set top [winfo toplevel $w]
-               set msg [strcat [mc "Invalid revision: %s" [get $this]] "\n\n$err"]
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $top] \
-                       -parent $top \
-                       -message $msg
-               error $msg
-       }
-       return $new
-}
-
-method _expr {} {
-       switch -- $revtype {
-       head -
-       trck -
-       tag  {
-               set i [$w_list curselection]
-               if {$i ne {}} {
-                       return [lindex $cur_specs $i 1]
-               } else {
-                       error [mc "No revision selected."]
-               }
-       }
-
-       expr {
-               if {$c_expr ne {}} {
-                       return $c_expr
-               } else {
-                       error [mc "Revision expression is empty."]
-               }
-       }
-       HEAD { return HEAD                     }
-       none { return {}                       }
-       default { error "unknown type of revision"      }
-       }
-}
-
-method _validate {d S} {
-       if {$d == 1} {
-               if {[regexp {\s} $S]} {
-                       return 0
-               }
-               if {[string length $S] > 0} {
-                       set revtype expr
-               }
-       }
-       return 1
-}
-
-method _filter {P} {
-       if {[regexp {\s} $P]} {
-               return 0
-       }
-       _rebuild $this $P
-       return 1
-}
-
-method _select {args} {
-       _rebuild $this $filter
-       focus_filter $this
-}
-
-method _rebuild {pat} {
-       set ste normal
-       switch -- $revtype {
-       head { set new $spec_head }
-       trck { set new $spec_trck }
-       tag  { set new $spec_tag  }
-       expr -
-       HEAD -
-       none {
-               set new [list]
-               set ste disabled
-       }
-       }
-
-       if {[$w_list cget -state] eq {disabled}} {
-               $w_list configure -state normal
-       }
-       $w_list delete 0 end
-
-       if {$pat ne {}} {
-               set pat *${pat}*
-       }
-       set cur_specs [list]
-       foreach spec $new {
-               set txt [lindex $spec 0]
-               if {$pat eq {} || [string match $pat $txt]} {
-                       lappend cur_specs $spec
-                       $w_list insert end $txt
-               }
-       }
-       if {$cur_specs ne {}} {
-               $w_list selection clear 0 end
-               $w_list selection set 0
-       }
-
-       if {[$w_filter cget -state] ne $ste} {
-               $w_list   configure -state $ste
-               $w_filter configure -state $ste
-       }
-}
-
-method _delete {current} {
-       if {$current eq $w} {
-               delete_this
-       }
-}
-
-method _sb_set {sb orient first last} {
-       set old_focus [focus -lastfor $w]
-
-       if {$first == 0 && $last == 1} {
-               if {[winfo exists $sb]} {
-                       destroy $sb
-                       if {$old_focus ne {}} {
-                               update
-                               focus $old_focus
-                       }
-               }
-               return
-       }
-
-       if {![winfo exists $sb]} {
-               if {$orient eq {h}} {
-                       scrollbar $sb -orient h -command [list $w_list xview]
-                       pack $sb -fill x -side bottom -before $w_list
-               } else {
-                       scrollbar $sb -orient v -command [list $w_list yview]
-                       pack $sb -fill y -side right -before $w_list
-               }
-               if {$old_focus ne {}} {
-                       update
-                       focus $old_focus
-               }
-       }
-       $sb set $first $last
-}
-
-method _show_tooltip {pos} {
-       if {$tooltip_wm ne {}} {
-               _open_tooltip $this
-       } elseif {$tooltip_timer eq {}} {
-               set tooltip_timer [after 1000 [cb _open_tooltip]]
-       }
-}
-
-method _open_tooltip {} {
-       global remote_url
-
-       set tooltip_timer {}
-       set pos_x [winfo pointerx $w_list]
-       set pos_y [winfo pointery $w_list]
-       if {[winfo containing $pos_x $pos_y] ne $w_list} {
-               _hide_tooltip $this
-               return
-       }
-
-       set pos @[join [list \
-               [expr {$pos_x - [winfo rootx $w_list]}] \
-               [expr {$pos_y - [winfo rooty $w_list]}]] ,]
-       set lno [$w_list index $pos]
-       if {$lno eq {}} {
-               _hide_tooltip $this
-               return
-       }
-
-       set spec [lindex $cur_specs $lno]
-       set refn [lindex $spec 1]
-       if {$refn eq {}} {
-               _hide_tooltip $this
-               return
-       }
-
-       if {$tooltip_wm eq {}} {
-               set tooltip_wm [toplevel $w_list.tooltip -borderwidth 1]
-               wm overrideredirect $tooltip_wm 1
-               wm transient $tooltip_wm [winfo toplevel $w_list]
-               set tooltip_t $tooltip_wm.label
-               text $tooltip_t \
-                       -takefocus 0 \
-                       -highlightthickness 0 \
-                       -relief flat \
-                       -borderwidth 0 \
-                       -wrap none \
-                       -background lightyellow \
-                       -foreground black
-               $tooltip_t tag conf section_header -font font_uibold
-               bind $tooltip_wm <Escape> [cb _hide_tooltip]
-               pack $tooltip_t
-       } else {
-               $tooltip_t conf -state normal
-               $tooltip_t delete 0.0 end
-       }
-
-       set data $tip_data($refn)
-       if {[lindex $data 0 0] eq {tag}} {
-               set tag  [lindex $data 0]
-               if {[lindex $data 1 0] eq {commit}} {
-                       set cmit [lindex $data 1]
-               } else {
-                       set cmit {}
-               }
-       } elseif {[lindex $data 0 0] eq {commit}} {
-               set tag  {}
-               set cmit [lindex $data 0]
-       }
-
-       $tooltip_t insert end [lindex $spec 0]
-       set last [_reflog_last $this [lindex $spec 1]]
-       if {$last ne {}} {
-               $tooltip_t insert end "\n"
-               $tooltip_t insert end [mc "Updated"]
-               $tooltip_t insert end " $last"
-       }
-       $tooltip_t insert end "\n"
-
-       if {$tag ne {}} {
-               $tooltip_t insert end "\n"
-               $tooltip_t insert end [mc "Tag"] section_header
-               $tooltip_t insert end "  [lindex $tag 1]\n"
-               $tooltip_t insert end [lindex $tag 2]
-               $tooltip_t insert end " ([lindex $tag 3])\n"
-               $tooltip_t insert end [lindex $tag 4]
-               $tooltip_t insert end "\n"
-       }
-
-       if {$cmit ne {}} {
-               $tooltip_t insert end "\n"
-               $tooltip_t insert end [mc "Commit@@noun"] section_header
-               $tooltip_t insert end "  [lindex $cmit 1]\n"
-               $tooltip_t insert end [lindex $cmit 2]
-               $tooltip_t insert end " ([lindex $cmit 3])\n"
-               $tooltip_t insert end [lindex $cmit 4]
-       }
-
-       if {[llength $spec] > 2} {
-               $tooltip_t insert end "\n"
-               $tooltip_t insert end [mc "Remote"] section_header
-               $tooltip_t insert end "  [lindex $spec 2]\n"
-               $tooltip_t insert end [mc "URL"]
-               $tooltip_t insert end " $remote_url([lindex $spec 2])\n"
-               $tooltip_t insert end [mc "Branch"]
-               $tooltip_t insert end " [lindex $spec 3]"
-       }
-
-       $tooltip_t conf -state disabled
-       _position_tooltip $this
-}
-
-method _reflog_last {name} {
-       if {[info exists reflog_last($name)]} {
-               return reflog_last($name)
-       }
-
-       set last {}
-       if {[catch {set last [file mtime [gitdir $name]]}]
-       && ![catch {set g [open [gitdir logs $name] r]}]} {
-               fconfigure $g -translation binary
-               while {[gets $g line] >= 0} {
-                       if {[regexp {> ([1-9][0-9]*) } $line line when]} {
-                               set last $when
-                       }
-               }
-               close $g
-       }
-
-       if {$last ne {}} {
-               set last [format_date $last]
-       }
-       set reflog_last($name) $last
-       return $last
-}
-
-method _position_tooltip {} {
-       set max_h [lindex [split [$tooltip_t index end] .] 0]
-       set max_w 0
-       for {set i 1} {$i <= $max_h} {incr i} {
-               set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1]
-               if {$c > $max_w} {set max_w $c}
-       }
-       $tooltip_t conf -width $max_w -height $max_h
-
-       set req_w [winfo reqwidth  $tooltip_t]
-       set req_h [winfo reqheight $tooltip_t]
-       set pos_x [expr {[winfo pointerx .] +  5}]
-       set pos_y [expr {[winfo pointery .] + 10}]
-
-       set g "${req_w}x${req_h}"
-       if {$pos_x >= 0} {append g +}
-       append g $pos_x
-       if {$pos_y >= 0} {append g +}
-       append g $pos_y
-
-       wm geometry $tooltip_wm $g
-       raise $tooltip_wm
-}
-
-method _hide_tooltip {} {
-       if {$tooltip_wm ne {}} {
-               destroy $tooltip_wm
-               set tooltip_wm {}
-       }
-       if {$tooltip_timer ne {}} {
-               after cancel $tooltip_timer
-               set tooltip_timer {}
-       }
-}
-
-}
diff --git a/lib/class.tcl b/lib/class.tcl
deleted file mode 100644 (file)
index dc21411..0000000
+++ /dev/null
@@ -1,186 +0,0 @@
-# git-gui simple class/object fake-alike
-# Copyright (C) 2007 Shawn Pearce
-
-proc class {class body} {
-       if {[namespace exists $class]} {
-               error "class $class already declared"
-       }
-       namespace eval $class "
-               variable __nextid     0
-               variable __sealed     0
-               variable __field_list {}
-               variable __field_array
-
-               proc cb {name args} {
-                       upvar this this
-                       concat \[list ${class}::\$name \$this\] \$args
-               }
-       "
-       namespace eval $class $body
-}
-
-proc field {name args} {
-       set class [uplevel {namespace current}]
-       variable ${class}::__sealed
-       variable ${class}::__field_array
-
-       switch [llength $args] {
-       0 { set new [list $name] }
-       1 { set new [list $name [lindex $args 0]] }
-       default { error "wrong # args: field name value?" }
-       }
-
-       if {$__sealed} {
-               error "class $class is sealed (cannot add new fields)"
-       }
-
-       if {[catch {set old $__field_array($name)}]} {
-               variable ${class}::__field_list
-               lappend __field_list $new
-               set __field_array($name) 1
-       } else {
-               error "field $name already declared"
-       }
-}
-
-proc constructor {name params body} {
-       set class [uplevel {namespace current}]
-       set ${class}::__sealed 1
-       variable ${class}::__field_list
-       set mbodyc {}
-
-       append mbodyc {set this } $class
-       append mbodyc {::__o[incr } $class {::__nextid]::__d} \;
-       append mbodyc {create_this } $class \;
-       append mbodyc {set __this [namespace qualifiers $this]} \;
-
-       if {$__field_list ne {}} {
-               append mbodyc {upvar #0}
-               foreach n $__field_list {
-                       set n [lindex $n 0]
-                       append mbodyc { ${__this}::} $n { } $n
-                       regsub -all @$n\\M $body "\${__this}::$n" body
-               }
-               append mbodyc \;
-               foreach n $__field_list {
-                       if {[llength $n] == 2} {
-                               append mbodyc \
-                               {set } [lindex $n 0] { } [list [lindex $n 1]] \;
-                       }
-               }
-       }
-       append mbodyc $body
-       namespace eval $class [list proc $name $params $mbodyc]
-}
-
-proc method {name params body {deleted {}} {del_body {}}} {
-       set class [uplevel {namespace current}]
-       set ${class}::__sealed 1
-       variable ${class}::__field_list
-       set params [linsert $params 0 this]
-       set mbodyc {}
-
-       append mbodyc {set __this [namespace qualifiers $this]} \;
-
-       switch $deleted {
-       {} {}
-       ifdeleted {
-               append mbodyc {if {![namespace exists $__this]} }
-               append mbodyc \{ $del_body \; return \} \;
-       }
-       default {
-               error "wrong # args: method name args body (ifdeleted body)?"
-       }
-       }
-
-       set decl {}
-       foreach n $__field_list {
-               set n [lindex $n 0]
-               if {[regexp -- $n\\M $body]} {
-                       if {   [regexp -all -- $n\\M $body] == 1
-                               && [regexp -all -- \\\$$n\\M $body] == 1
-                               && [regexp -all -- \\\$$n\\( $body] == 0} {
-                               regsub -all \
-                                       \\\$$n\\M $body \
-                                       "\[set \${__this}::$n\]" body
-                       } else {
-                               append decl { ${__this}::} $n { } $n
-                               regsub -all @$n\\M $body "\${__this}::$n" body
-                       }
-               }
-       }
-       if {$decl ne {}} {
-               append mbodyc {upvar #0} $decl \;
-       }
-       append mbodyc $body
-       namespace eval $class [list proc $name $params $mbodyc]
-}
-
-proc create_this {class} {
-       upvar this this
-       namespace eval [namespace qualifiers $this] [list proc \
-               [namespace tail $this] \
-               [list name args] \
-               "eval \[list ${class}::\$name $this\] \$args" \
-       ]
-}
-
-proc delete_this {{t {}}} {
-       if {$t eq {}} {
-               upvar this this
-               set t $this
-       }
-       set t [namespace qualifiers $t]
-       if {[namespace exists $t]} {namespace delete $t}
-}
-
-proc make_toplevel {t w args} {
-       upvar $t top $w pfx this this
-
-       if {[llength $args] % 2} {
-               error "make_toplevel topvar winvar {options}"
-       }
-       set autodelete 1
-       foreach {name value} $args {
-               switch -exact -- $name {
-               -autodelete {set autodelete $value}
-               default     {error "unsupported option $name"}
-               }
-       }
-
-       if {$::root_exists || [winfo ismapped .]} {
-               regsub -all {::} $this {__} w
-               set top .$w
-               set pfx $top
-               toplevel $top
-               set ::root_exists 1
-       } else {
-               set top .
-               set pfx {}
-       }
-
-       if {$autodelete} {
-               wm protocol $top WM_DELETE_WINDOW "
-                       [list delete_this $this]
-                       [list destroy $top]
-               "
-       }
-}
-
-
-## auto_mkindex support for class/constructor/method
-##
-auto_mkindex_parser::command class {name body} {
-       variable parser
-       variable contextStack
-       set contextStack [linsert $contextStack 0 $name]
-       $parser eval [list _%@namespace eval $name] $body
-       set contextStack [lrange $contextStack 1 end]
-}
-auto_mkindex_parser::command constructor {name args} {
-       variable index
-       variable scriptFile
-       append index [list set auto_index([fullname $name])] \
-               [format { [list source [file join $dir %s]]} \
-               [file split $scriptFile]] "\n"
-}
diff --git a/lib/commit.tcl b/lib/commit.tcl
deleted file mode 100644 (file)
index b2d2d53..0000000
+++ /dev/null
@@ -1,431 +0,0 @@
-# git-gui misc. commit reading/writing support
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc load_last_commit {} {
-       global HEAD PARENT MERGE_HEAD commit_type ui_comm
-       global repo_config
-
-       if {[llength $PARENT] == 0} {
-               error_popup [mc "There is nothing to amend.
-
-You are about to create the initial commit.  There is no commit before this to amend.
-"]
-               return
-       }
-
-       repository_state curType curHEAD curMERGE_HEAD
-       if {$curType eq {merge}} {
-               error_popup [mc "Cannot amend while merging.
-
-You are currently in the middle of a merge that has not been fully completed.  You cannot amend the prior commit unless you first abort the current merge activity.
-"]
-               return
-       }
-
-       set msg {}
-       set parents [list]
-       if {[catch {
-                       set fd [git_read cat-file commit $curHEAD]
-                       fconfigure $fd -encoding binary -translation lf
-                       if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
-                               set enc utf-8
-                       }
-                       while {[gets $fd line] > 0} {
-                               if {[string match {parent *} $line]} {
-                                       lappend parents [string range $line 7 end]
-                               } elseif {[string match {encoding *} $line]} {
-                                       set enc [string tolower [string range $line 9 end]]
-                               }
-                       }
-                       set msg [read $fd]
-                       close $fd
-
-                       set enc [tcl_encoding $enc]
-                       if {$enc ne {}} {
-                               set msg [encoding convertfrom $enc $msg]
-                       }
-                       set msg [string trim $msg]
-               } err]} {
-               error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
-               return
-       }
-
-       set HEAD $curHEAD
-       set PARENT $parents
-       set MERGE_HEAD [list]
-       switch -- [llength $parents] {
-       0       {set commit_type amend-initial}
-       1       {set commit_type amend}
-       default {set commit_type amend-merge}
-       }
-
-       $ui_comm delete 0.0 end
-       $ui_comm insert end $msg
-       $ui_comm edit reset
-       $ui_comm edit modified false
-       rescan ui_ready
-}
-
-set GIT_COMMITTER_IDENT {}
-
-proc committer_ident {} {
-       global GIT_COMMITTER_IDENT
-
-       if {$GIT_COMMITTER_IDENT eq {}} {
-               if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} {
-                       error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"]
-                       return {}
-               }
-               if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
-                       $me me GIT_COMMITTER_IDENT]} {
-                       error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"]
-                       return {}
-               }
-       }
-
-       return $GIT_COMMITTER_IDENT
-}
-
-proc do_signoff {} {
-       global ui_comm
-
-       set me [committer_ident]
-       if {$me eq {}} return
-
-       set sob "Signed-off-by: $me"
-       set last [$ui_comm get {end -1c linestart} {end -1c}]
-       if {$last ne $sob} {
-               $ui_comm edit separator
-               if {$last ne {}
-                       && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
-                       $ui_comm insert end "\n"
-               }
-               $ui_comm insert end "\n$sob"
-               $ui_comm edit separator
-               $ui_comm see end
-       }
-}
-
-proc create_new_commit {} {
-       global commit_type ui_comm
-
-       set commit_type normal
-       $ui_comm delete 0.0 end
-       $ui_comm edit reset
-       $ui_comm edit modified false
-       rescan ui_ready
-}
-
-proc commit_tree {} {
-       global HEAD commit_type file_states ui_comm repo_config
-       global pch_error
-
-       if {[committer_ident] eq {}} return
-       if {![lock_index update]} return
-
-       # -- Our in memory state should match the repository.
-       #
-       repository_state curType curHEAD curMERGE_HEAD
-       if {[string match amend* $commit_type]
-               && $curType eq {normal}
-               && $curHEAD eq $HEAD} {
-       } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
-               info_popup [mc "Last scanned state does not match repository state.
-
-Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
-
-The rescan will be automatically started now.
-"]
-               unlock_index
-               rescan ui_ready
-               return
-       }
-
-       # -- At least one file should differ in the index.
-       #
-       set files_ready 0
-       foreach path [array names file_states] {
-               switch -glob -- [lindex $file_states($path) 0] {
-               _? {continue}
-               A? -
-               D? -
-               M? {set files_ready 1}
-               U? {
-                       error_popup [mc "Unmerged files cannot be committed.
-
-File %s has merge conflicts.  You must resolve them and stage the file before committing.
-" [short_path $path]]
-                       unlock_index
-                       return
-               }
-               default {
-                       error_popup [mc "Unknown file state %s detected.
-
-File %s cannot be committed by this program.
-" [lindex $s 0] [short_path $path]]
-               }
-               }
-       }
-       if {!$files_ready && ![string match *merge $curType]} {
-               info_popup [mc "No changes to commit.
-
-You must stage at least 1 file before you can commit.
-"]
-               unlock_index
-               return
-       }
-
-       # -- A message is required.
-       #
-       set msg [string trim [$ui_comm get 1.0 end]]
-       regsub -all -line {[ \t\r]+$} $msg {} msg
-       if {$msg eq {}} {
-               error_popup [mc "Please supply a commit message.
-
-A good commit message has the following format:
-
-- First line: Describe in one sentence what you did.
-- Second line: Blank
-- Remaining lines: Describe why this change is good.
-"]
-               unlock_index
-               return
-       }
-
-       # -- Run the pre-commit hook.
-       #
-       set pchook [gitdir hooks pre-commit]
-
-       # On Cygwin [file executable] might lie so we need to ask
-       # the shell if the hook is executable.  Yes that's annoying.
-       #
-       if {[is_Cygwin] && [file isfile $pchook]} {
-               set pchook [list sh -c [concat \
-                       "if test -x \"$pchook\";" \
-                       "then exec \"$pchook\" 2>&1;" \
-                       "fi"]]
-       } elseif {[file executable $pchook]} {
-               set pchook [list $pchook |& cat]
-       } else {
-               commit_writetree $curHEAD $msg
-               return
-       }
-
-       ui_status {Calling pre-commit hook...}
-       set pch_error {}
-       set fd_ph [open "| $pchook" r]
-       fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
-       fileevent $fd_ph readable \
-               [list commit_prehook_wait $fd_ph $curHEAD $msg]
-}
-
-proc commit_prehook_wait {fd_ph curHEAD msg} {
-       global pch_error
-
-       append pch_error [read $fd_ph]
-       fconfigure $fd_ph -blocking 1
-       if {[eof $fd_ph]} {
-               if {[catch {close $fd_ph}]} {
-                       ui_status {Commit declined by pre-commit hook.}
-                       hook_failed_popup pre-commit $pch_error
-                       unlock_index
-               } else {
-                       commit_writetree $curHEAD $msg
-               }
-               set pch_error {}
-               return
-       }
-       fconfigure $fd_ph -blocking 0
-}
-
-proc commit_writetree {curHEAD msg} {
-       ui_status {Committing changes...}
-       set fd_wt [git_read write-tree]
-       fileevent $fd_wt readable \
-               [list commit_committree $fd_wt $curHEAD $msg]
-}
-
-proc commit_committree {fd_wt curHEAD msg} {
-       global HEAD PARENT MERGE_HEAD commit_type
-       global current_branch
-       global ui_comm selected_commit_type
-       global file_states selected_paths rescan_active
-       global repo_config
-
-       gets $fd_wt tree_id
-       if {[catch {close $fd_wt} err]} {
-               error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
-               ui_status {Commit failed.}
-               unlock_index
-               return
-       }
-
-       # -- Verify this wasn't an empty change.
-       #
-       if {$commit_type eq {normal}} {
-               set fd_ot [git_read cat-file commit $PARENT]
-               fconfigure $fd_ot -encoding binary -translation lf
-               set old_tree [gets $fd_ot]
-               close $fd_ot
-
-               if {[string equal -length 5 {tree } $old_tree]
-                       && [string length $old_tree] == 45} {
-                       set old_tree [string range $old_tree 5 end]
-               } else {
-                       error [mc "Commit %s appears to be corrupt" $PARENT]
-               }
-
-               if {$tree_id eq $old_tree} {
-                       info_popup [mc "No changes to commit.
-
-No files were modified by this commit and it was not a merge commit.
-
-A rescan will be automatically started now.
-"]
-                       unlock_index
-                       rescan {ui_status [mc "No changes to commit."]}
-                       return
-               }
-       }
-
-       # -- Build the message.
-       #
-       set msg_p [gitdir COMMIT_EDITMSG]
-       set msg_wt [open $msg_p w]
-       fconfigure $msg_wt -translation lf
-       if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
-               set enc utf-8
-       }
-       set use_enc [tcl_encoding $enc]
-       if {$use_enc ne {}} {
-               fconfigure $msg_wt -encoding $use_enc
-       } else {
-               puts stderr [mc "warning: Tcl does not support encoding '%s'." $enc]
-               fconfigure $msg_wt -encoding utf-8
-       }
-       puts -nonewline $msg_wt $msg
-       close $msg_wt
-
-       # -- Create the commit.
-       #
-       set cmd [list commit-tree $tree_id]
-       foreach p [concat $PARENT $MERGE_HEAD] {
-               lappend cmd -p $p
-       }
-       lappend cmd <$msg_p
-       if {[catch {set cmt_id [eval git $cmd]} err]} {
-               error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
-               ui_status {Commit failed.}
-               unlock_index
-               return
-       }
-
-       # -- Update the HEAD ref.
-       #
-       set reflogm commit
-       if {$commit_type ne {normal}} {
-               append reflogm " ($commit_type)"
-       }
-       set i [string first "\n" $msg]
-       if {$i >= 0} {
-               set subject [string range $msg 0 [expr {$i - 1}]]
-       } else {
-               set subject $msg
-       }
-       append reflogm {: } $subject
-       if {[catch {
-                       git update-ref -m $reflogm HEAD $cmt_id $curHEAD
-               } err]} {
-               error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
-               ui_status {Commit failed.}
-               unlock_index
-               return
-       }
-
-       # -- Cleanup after ourselves.
-       #
-       catch {file delete $msg_p}
-       catch {file delete [gitdir MERGE_HEAD]}
-       catch {file delete [gitdir MERGE_MSG]}
-       catch {file delete [gitdir SQUASH_MSG]}
-       catch {file delete [gitdir GITGUI_MSG]}
-
-       # -- Let rerere do its thing.
-       #
-       if {[get_config rerere.enabled] eq {}} {
-               set rerere [file isdirectory [gitdir rr-cache]]
-       } else {
-               set rerere [is_config_true rerere.enabled]
-       }
-       if {$rerere} {
-               catch {git rerere}
-       }
-
-       # -- Run the post-commit hook.
-       #
-       set pchook [gitdir hooks post-commit]
-       if {[is_Cygwin] && [file isfile $pchook]} {
-               set pchook [list sh -c [concat \
-                       "if test -x \"$pchook\";" \
-                       "then exec \"$pchook\";" \
-                       "fi"]]
-       } elseif {![file executable $pchook]} {
-               set pchook {}
-       }
-       if {$pchook ne {}} {
-               catch {exec $pchook &}
-       }
-
-       $ui_comm delete 0.0 end
-       $ui_comm edit reset
-       $ui_comm edit modified false
-       if {$::GITGUI_BCK_exists} {
-               catch {file delete [gitdir GITGUI_BCK]}
-               set ::GITGUI_BCK_exists 0
-       }
-
-       if {[is_enabled singlecommit]} do_quit
-
-       # -- Update in memory status
-       #
-       set selected_commit_type new
-       set commit_type normal
-       set HEAD $cmt_id
-       set PARENT $cmt_id
-       set MERGE_HEAD [list]
-
-       foreach path [array names file_states] {
-               set s $file_states($path)
-               set m [lindex $s 0]
-               switch -glob -- $m {
-               _O -
-               _M -
-               _D {continue}
-               __ -
-               A_ -
-               M_ -
-               D_ {
-                       unset file_states($path)
-                       catch {unset selected_paths($path)}
-               }
-               DO {
-                       set file_states($path) [list _O [lindex $s 1] {} {}]
-               }
-               AM -
-               AD -
-               MM -
-               MD {
-                       set file_states($path) [list \
-                               _[string index $m 1] \
-                               [lindex $s 1] \
-                               [lindex $s 3] \
-                               {}]
-               }
-               }
-       }
-
-       display_all_files
-       unlock_index
-       reshow_diff
-       ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
-}
diff --git a/lib/console.tcl b/lib/console.tcl
deleted file mode 100644 (file)
index 5597188..0000000
+++ /dev/null
@@ -1,218 +0,0 @@
-# git-gui console support
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-class console {
-
-field t_short
-field t_long
-field w
-field w_t
-field console_cr
-field is_toplevel    1; # are we our own window?
-
-constructor new {short_title long_title} {
-       set t_short $short_title
-       set t_long $long_title
-       _init $this
-       return $this
-}
-
-constructor embed {path title} {
-       set t_short {}
-       set t_long $title
-       set w $path
-       set is_toplevel 0
-       _init $this
-       return $this
-}
-
-method _init {} {
-       global M1B
-
-       if {$is_toplevel} {
-               make_toplevel top w -autodelete 0
-               wm title $top "[appname] ([reponame]): $t_short"
-       } else {
-               frame $w
-       }
-
-       set console_cr 1.0
-       set w_t $w.m.t
-
-       frame $w.m
-       label $w.m.l1 \
-               -textvariable @t_long  \
-               -anchor w \
-               -justify left \
-               -font font_uibold
-       text $w_t \
-               -background white -borderwidth 1 \
-               -relief sunken \
-               -width 80 -height 10 \
-               -wrap none \
-               -font font_diff \
-               -state disabled \
-               -xscrollcommand [cb _sb_set $w.m.sbx h] \
-               -yscrollcommand [cb _sb_set $w.m.sby v]
-       label $w.m.s -text [mc "Working... please wait..."] \
-               -anchor w \
-               -justify left \
-               -font font_uibold
-       pack $w.m.l1 -side top -fill x
-       pack $w.m.s -side bottom -fill x
-       pack $w_t -side left -fill both -expand 1
-       pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
-
-       menu $w.ctxm -tearoff 0
-       $w.ctxm add command -label [mc "Copy"] \
-               -command "tk_textCopy $w_t"
-       $w.ctxm add command -label [mc "Select All"] \
-               -command "focus $w_t;$w_t tag add sel 0.0 end"
-       $w.ctxm add command -label [mc "Copy All"] \
-               -command "
-                       $w_t tag add sel 0.0 end
-                       tk_textCopy $w_t
-                       $w_t tag remove sel 0.0 end
-               "
-
-       if {$is_toplevel} {
-               button $w.ok -text [mc "Close"] \
-                       -state disabled \
-                       -command [list destroy $w]
-               pack $w.ok -side bottom -anchor e -pady 10 -padx 10
-               bind $w <Visibility> [list focus $w]
-       }
-
-       bind_button3 $w_t "tk_popup $w.ctxm %X %Y"
-       bind $w_t <$M1B-Key-a> "$w_t tag add sel 0.0 end;break"
-       bind $w_t <$M1B-Key-A> "$w_t tag add sel 0.0 end;break"
-}
-
-method exec {cmd {after {}}} {
-       if {[lindex $cmd 0] eq {git}} {
-               set fd_f [eval git_read --stderr [lrange $cmd 1 end]]
-       } else {
-               lappend cmd 2>@1
-               set fd_f [_open_stdout_stderr $cmd]
-       }
-       fconfigure $fd_f -blocking 0 -translation binary
-       fileevent $fd_f readable [cb _read $fd_f $after]
-}
-
-method _read {fd after} {
-       set buf [read $fd]
-       if {$buf ne {}} {
-               if {![winfo exists $w_t]} {_init $this}
-               $w_t conf -state normal
-               set c 0
-               set n [string length $buf]
-               while {$c < $n} {
-                       set cr [string first "\r" $buf $c]
-                       set lf [string first "\n" $buf $c]
-                       if {$cr < 0} {set cr [expr {$n + 1}]}
-                       if {$lf < 0} {set lf [expr {$n + 1}]}
-
-                       if {$lf < $cr} {
-                               $w_t insert end [string range $buf $c $lf]
-                               set console_cr [$w_t index {end -1c}]
-                               set c $lf
-                               incr c
-                       } else {
-                               $w_t delete $console_cr end
-                               $w_t insert end "\n"
-                               $w_t insert end [string range $buf $c [expr {$cr - 1}]]
-                               set c $cr
-                               incr c
-                       }
-               }
-               $w_t conf -state disabled
-               $w_t see end
-       }
-
-       fconfigure $fd -blocking 1
-       if {[eof $fd]} {
-               if {[catch {close $fd}]} {
-                       set ok 0
-               } else {
-                       set ok 1
-               }
-               if {$after ne {}} {
-                       uplevel #0 $after $ok
-               } else {
-                       done $this $ok
-               }
-               return
-       }
-       fconfigure $fd -blocking 0
-}
-
-method chain {cmdlist {ok 1}} {
-       if {$ok} {
-               if {[llength $cmdlist] == 0} {
-                       done $this $ok
-                       return
-               }
-
-               set cmd [lindex $cmdlist 0]
-               set cmdlist [lrange $cmdlist 1 end]
-
-               if {[lindex $cmd 0] eq {exec}} {
-                       exec $this \
-                               [lrange $cmd 1 end] \
-                               [cb chain $cmdlist]
-               } else {
-                       uplevel #0 $cmd [cb chain $cmdlist]
-               }
-       } else {
-               done $this $ok
-       }
-}
-
-method insert {txt} {
-       if {![winfo exists $w_t]} {_init $this}
-       $w_t conf -state normal
-       $w_t insert end "$txt\n"
-       set console_cr [$w_t index {end -1c}]
-       $w_t conf -state disabled
-}
-
-method done {ok} {
-       if {$ok} {
-               if {[winfo exists $w.m.s]} {
-                       bind $w.m.s <Destroy> [list delete_this $this]
-                       $w.m.s conf -background green -text [mc "Success"]
-                       if {$is_toplevel} {
-                               $w.ok conf -state normal
-                               focus $w.ok
-                       }
-               } else {
-                       delete_this
-               }
-       } else {
-               if {![winfo exists $w.m.s]} {
-                       _init $this
-               }
-               bind $w.m.s <Destroy> [list delete_this $this]
-               $w.m.s conf -background red -text [mc "Error: Command Failed"]
-               if {$is_toplevel} {
-                       $w.ok conf -state normal
-                       focus $w.ok
-               }
-       }
-}
-
-method _sb_set {sb orient first last} {
-       if {![winfo exists $sb]} {
-               if {$first == $last || ($first == 0 && $last == 1)} return
-               if {$orient eq {h}} {
-                       scrollbar $sb -orient h -command [list $w_t xview]
-                       pack $sb -fill x -side bottom -before $w_t
-               } else {
-                       scrollbar $sb -orient v -command [list $w_t yview]
-                       pack $sb -fill y -side right -before $w_t
-               }
-       }
-       $sb set $first $last
-}
-
-}
diff --git a/lib/database.tcl b/lib/database.tcl
deleted file mode 100644 (file)
index d66aa3f..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-# git-gui object database management support
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc do_stats {} {
-       set fd [git_read count-objects -v]
-       while {[gets $fd line] > 0} {
-               if {[regexp {^([^:]+): (\d+)$} $line _ name value]} {
-                       set stats($name) $value
-               }
-       }
-       close $fd
-
-       set packed_sz 0
-       foreach p [glob -directory [gitdir objects pack] \
-               -type f \
-               -nocomplain -- *] {
-               incr packed_sz [file size $p]
-       }
-       if {$packed_sz > 0} {
-               set stats(size-pack) [expr {$packed_sz / 1024}]
-       }
-
-       set w .stats_view
-       toplevel $w
-       wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
-
-       label $w.header -text [mc "Database Statistics"]
-       pack $w.header -side top -fill x
-
-       frame $w.buttons -border 1
-       button $w.buttons.close -text [mc Close] \
-               -default active \
-               -command [list destroy $w]
-       button $w.buttons.gc -text [mc "Compress Database"] \
-               -default normal \
-               -command "destroy $w;do_gc"
-       pack $w.buttons.close -side right
-       pack $w.buttons.gc -side left
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       frame $w.stat -borderwidth 1 -relief solid
-       foreach s {
-               {count           {mc "Number of loose objects"}}
-               {size            {mc "Disk space used by loose objects"} { KiB}}
-               {in-pack         {mc "Number of packed objects"}}
-               {packs           {mc "Number of packs"}}
-               {size-pack       {mc "Disk space used by packed objects"} { KiB}}
-               {prune-packable  {mc "Packed objects waiting for pruning"}}
-               {garbage         {mc "Garbage files"}}
-               } {
-               set name [lindex $s 0]
-               set label [eval [lindex $s 1]]
-               if {[catch {set value $stats($name)}]} continue
-               if {[llength $s] > 2} {
-                       set value "$value[lindex $s 2]"
-               }
-
-               label $w.stat.l_$name -text "$label:" -anchor w
-               label $w.stat.v_$name -text $value -anchor w
-               grid $w.stat.l_$name $w.stat.v_$name -sticky we -padx {0 5}
-       }
-       pack $w.stat -pady 10 -padx 10
-
-       bind $w <Visibility> "grab $w; focus $w.buttons.close"
-       bind $w <Key-Escape> [list destroy $w]
-       bind $w <Key-Return> [list destroy $w]
-       wm title $w [append "[appname] ([reponame]): " [mc "Database Statistics"]]
-       tkwait window $w
-}
-
-proc do_gc {} {
-       set w [console::new {gc} [mc "Compressing the object database"]]
-       console::chain $w {
-               {exec git pack-refs --prune}
-               {exec git reflog expire --all}
-               {exec git repack -a -d -l}
-               {exec git rerere gc}
-       }
-}
-
-proc do_fsck_objects {} {
-       set w [console::new {fsck-objects} \
-               [mc "Verifying the object database with fsck-objects"]]
-       set cmd [list git fsck-objects]
-       lappend cmd --full
-       lappend cmd --cache
-       lappend cmd --strict
-       console::exec $w $cmd
-}
-
-proc hint_gc {} {
-       set object_limit 8
-       if {[is_Windows]} {
-               set object_limit 1
-       }
-
-       set objects_current [llength [glob \
-               -directory [gitdir objects 42] \
-               -nocomplain \
-               -tails \
-               -- \
-               *]]
-
-       if {$objects_current >= $object_limit} {
-               set objects_current [expr {$objects_current * 256}]
-               set object_limit    [expr {$object_limit    * 256}]
-               if {[ask_popup \
-                       [mc "This repository currently has approximately %i loose objects.
-
-To maintain optimal performance it is strongly recommended that you compress the database when more than %i loose objects exist.
-
-Compress the database now?" $objects_current $object_limit]] eq yes} {
-                       do_gc
-               }
-       }
-}
diff --git a/lib/date.tcl b/lib/date.tcl
deleted file mode 100644 (file)
index abe8299..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-# git-gui date processing support
-# Copyright (C) 2007 Shawn Pearce
-
-set git_month(Jan)  1
-set git_month(Feb)  2
-set git_month(Mar)  3
-set git_month(Apr)  4
-set git_month(May)  5
-set git_month(Jun)  6
-set git_month(Jul)  7
-set git_month(Aug)  8
-set git_month(Sep)  9
-set git_month(Oct) 10
-set git_month(Nov) 11
-set git_month(Dec) 12
-
-proc parse_git_date {s} {
-       if {$s eq {}} {
-               return {}
-       }
-
-       if {![regexp \
-               {^... (...) (\d{1,2}) (\d\d):(\d\d):(\d\d) (\d{4}) ([+-]?)(\d\d)(\d\d)$} $s s \
-               month day hr mm ss yr ew tz_h tz_m]} {
-               error [mc "Invalid date from Git: %s" $s]
-       }
-
-       set s [clock scan [format {%4.4i%2.2i%2.2iT%2s%2s%2s} \
-                       $yr $::git_month($month) $day \
-                       $hr $mm $ss] \
-                       -gmt 1]
-
-       regsub ^0 $tz_h {} tz_h
-       regsub ^0 $tz_m {} tz_m
-       switch -- $ew {
-       -  {set ew +}
-       +  {set ew -}
-       {} {set ew -}
-       }
-
-       return [expr "$s $ew ($tz_h * 3600 + $tz_m * 60)"]
-}
-
-proc format_date {s} {
-       if {$s eq {}} {
-               return {}
-       }
-       return [clock format $s -format {%a %b %e %H:%M:%S %Y}]
-}
-
-proc reformat_date {s} {
-       return [format_date [parse_git_date $s]]
-}
diff --git a/lib/diff.tcl b/lib/diff.tcl
deleted file mode 100644 (file)
index 43565e4..0000000
+++ /dev/null
@@ -1,361 +0,0 @@
-# git-gui diff viewer
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc clear_diff {} {
-       global ui_diff current_diff_path current_diff_header
-       global ui_index ui_workdir
-
-       $ui_diff conf -state normal
-       $ui_diff delete 0.0 end
-       $ui_diff conf -state disabled
-
-       set current_diff_path {}
-       set current_diff_header {}
-
-       $ui_index tag remove in_diff 0.0 end
-       $ui_workdir tag remove in_diff 0.0 end
-}
-
-proc reshow_diff {} {
-       global file_states file_lists
-       global current_diff_path current_diff_side
-
-       set p $current_diff_path
-       if {$p eq {}} {
-               # No diff is being shown.
-       } elseif {$current_diff_side eq {}
-               || [catch {set s $file_states($p)}]
-               || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
-               clear_diff
-       } else {
-               show_diff $p $current_diff_side
-       }
-}
-
-proc handle_empty_diff {} {
-       global current_diff_path file_states file_lists
-
-       set path $current_diff_path
-       set s $file_states($path)
-       if {[lindex $s 0] ne {_M}} return
-
-       info_popup [mc "No differences detected.
-
-%s has no changes.
-
-The modification date of this file was updated by another application, but the content within the file was not changed.
-
-A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
-
-       clear_diff
-       display_file $path __
-       rescan ui_ready 0
-}
-
-proc show_diff {path w {lno {}}} {
-       global file_states file_lists
-       global is_3way_diff diff_active repo_config
-       global ui_diff ui_index ui_workdir
-       global current_diff_path current_diff_side current_diff_header
-
-       if {$diff_active || ![lock_index read]} return
-
-       clear_diff
-       if {$lno == {}} {
-               set lno [lsearch -sorted -exact $file_lists($w) $path]
-               if {$lno >= 0} {
-                       incr lno
-               }
-       }
-       if {$lno >= 1} {
-               $w tag add in_diff $lno.0 [expr {$lno + 1}].0
-       }
-
-       set s $file_states($path)
-       set m [lindex $s 0]
-       set is_3way_diff 0
-       set diff_active 1
-       set current_diff_path $path
-       set current_diff_side $w
-       set current_diff_header {}
-       ui_status [mc "Loading diff of %s..." [escape_path $path]]
-
-       # - Git won't give us the diff, there's nothing to compare to!
-       #
-       if {$m eq {_O}} {
-               set max_sz [expr {128 * 1024}]
-               set type unknown
-               if {[catch {
-                               set type [file type $path]
-                               switch -- $type {
-                               directory {
-                                       set type submodule
-                                       set content {}
-                                       set sz 0
-                               }
-                               link {
-                                       set content [file readlink $path]
-                                       set sz [string length $content]
-                               }
-                               file {
-                                       set fd [open $path r]
-                                       fconfigure $fd -eofchar {}
-                                       set content [read $fd $max_sz]
-                                       close $fd
-                                       set sz [file size $path]
-                               }
-                               default {
-                                       error "'$type' not supported"
-                               }
-                               }
-                       } err ]} {
-                       set diff_active 0
-                       unlock_index
-                       ui_status [mc "Unable to display %s" [escape_path $path]]
-                       error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
-                       return
-               }
-               $ui_diff conf -state normal
-               if {$type eq {submodule}} {
-                       $ui_diff insert end [append \
-                               "* " \
-                               [mc "Git Repository (subproject)"] \
-                               "\n"] d_@
-               } elseif {![catch {set type [exec file $path]}]} {
-                       set n [string length $path]
-                       if {[string equal -length $n $path $type]} {
-                               set type [string range $type $n end]
-                               regsub {^:?\s*} $type {} type
-                       }
-                       $ui_diff insert end "* $type\n" d_@
-               }
-               if {[string first "\0" $content] != -1} {
-                       $ui_diff insert end \
-                               [mc "* Binary file (not showing content)."] \
-                               d_@
-               } else {
-                       if {$sz > $max_sz} {
-                               $ui_diff insert end \
-"* Untracked file is $sz bytes.
-* Showing only first $max_sz bytes.
-" d_@
-                       }
-                       $ui_diff insert end $content
-                       if {$sz > $max_sz} {
-                               $ui_diff insert end "
-* Untracked file clipped here by [appname].
-* To see the entire file, use an external editor.
-" d_@
-                       }
-               }
-               $ui_diff conf -state disabled
-               set diff_active 0
-               unlock_index
-               ui_ready
-               return
-       }
-
-       set cmd [list]
-       if {$w eq $ui_index} {
-               lappend cmd diff-index
-               lappend cmd --cached
-       } elseif {$w eq $ui_workdir} {
-               if {[string index $m 0] eq {U}} {
-                       lappend cmd diff
-               } else {
-                       lappend cmd diff-files
-               }
-       }
-
-       lappend cmd -p
-       lappend cmd --no-color
-       if {$repo_config(gui.diffcontext) >= 0} {
-               lappend cmd "-U$repo_config(gui.diffcontext)"
-       }
-       if {$w eq $ui_index} {
-               lappend cmd [PARENT]
-       }
-       lappend cmd --
-       lappend cmd $path
-
-       if {[catch {set fd [eval git_read --nice $cmd]} err]} {
-               set diff_active 0
-               unlock_index
-               ui_status [mc "Unable to display %s" [escape_path $path]]
-               error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
-               return
-       }
-
-       fconfigure $fd \
-               -blocking 0 \
-               -encoding binary \
-               -translation binary
-       fileevent $fd readable [list read_diff $fd]
-}
-
-proc read_diff {fd} {
-       global ui_diff diff_active
-       global is_3way_diff current_diff_header
-
-       $ui_diff conf -state normal
-       while {[gets $fd line] >= 0} {
-               # -- Cleanup uninteresting diff header lines.
-               #
-               if {   [string match {diff --git *}      $line]
-                       || [string match {diff --cc *}       $line]
-                       || [string match {diff --combined *} $line]
-                       || [string match {--- *}             $line]
-                       || [string match {+++ *}             $line]} {
-                       append current_diff_header $line "\n"
-                       continue
-               }
-               if {[string match {index *} $line]} continue
-               if {$line eq {deleted file mode 120000}} {
-                       set line "deleted symlink"
-               }
-
-               # -- Automatically detect if this is a 3 way diff.
-               #
-               if {[string match {@@@ *} $line]} {set is_3way_diff 1}
-
-               if {[string match {mode *} $line]
-                       || [string match {new file *} $line]
-                       || [string match {deleted file *} $line]
-                       || [string match {deleted symlink} $line]
-                       || [string match {Binary files * and * differ} $line]
-                       || $line eq {\ No newline at end of file}
-                       || [regexp {^\* Unmerged path } $line]} {
-                       set tags {}
-               } elseif {$is_3way_diff} {
-                       set op [string range $line 0 1]
-                       switch -- $op {
-                       {  } {set tags {}}
-                       {@@} {set tags d_@}
-                       { +} {set tags d_s+}
-                       { -} {set tags d_s-}
-                       {+ } {set tags d_+s}
-                       {- } {set tags d_-s}
-                       {--} {set tags d_--}
-                       {++} {
-                               if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
-                                       set line [string replace $line 0 1 {  }]
-                                       set tags d$op
-                               } else {
-                                       set tags d_++
-                               }
-                       }
-                       default {
-                               puts "error: Unhandled 3 way diff marker: {$op}"
-                               set tags {}
-                       }
-                       }
-               } else {
-                       set op [string index $line 0]
-                       switch -- $op {
-                       { } {set tags {}}
-                       {@} {set tags d_@}
-                       {-} {set tags d_-}
-                       {+} {
-                               if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
-                                       set line [string replace $line 0 0 { }]
-                                       set tags d$op
-                               } else {
-                                       set tags d_+
-                               }
-                       }
-                       default {
-                               puts "error: Unhandled 2 way diff marker: {$op}"
-                               set tags {}
-                       }
-                       }
-               }
-               $ui_diff insert end $line $tags
-               if {[string index $line end] eq "\r"} {
-                       $ui_diff tag add d_cr {end - 2c}
-               }
-               $ui_diff insert end "\n" $tags
-       }
-       $ui_diff conf -state disabled
-
-       if {[eof $fd]} {
-               close $fd
-               set diff_active 0
-               unlock_index
-               ui_ready
-
-               if {[$ui_diff index end] eq {2.0}} {
-                       handle_empty_diff
-               }
-       }
-}
-
-proc apply_hunk {x y} {
-       global current_diff_path current_diff_header current_diff_side
-       global ui_diff ui_index file_states
-
-       if {$current_diff_path eq {} || $current_diff_header eq {}} return
-       if {![lock_index apply_hunk]} return
-
-       set apply_cmd {apply --cached --whitespace=nowarn}
-       set mi [lindex $file_states($current_diff_path) 0]
-       if {$current_diff_side eq $ui_index} {
-               set failed_msg [mc "Failed to unstage selected hunk."]
-               lappend apply_cmd --reverse
-               if {[string index $mi 0] ne {M}} {
-                       unlock_index
-                       return
-               }
-       } else {
-               set failed_msg [mc "Failed to stage selected hunk."]
-               if {[string index $mi 1] ne {M}} {
-                       unlock_index
-                       return
-               }
-       }
-
-       set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
-       set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
-       if {$s_lno eq {}} {
-               unlock_index
-               return
-       }
-
-       set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
-       if {$e_lno eq {}} {
-               set e_lno end
-       }
-
-       if {[catch {
-               set p [eval git_write $apply_cmd]
-               fconfigure $p -translation binary -encoding binary
-               puts -nonewline $p $current_diff_header
-               puts -nonewline $p [$ui_diff get $s_lno $e_lno]
-               close $p} err]} {
-               error_popup [append $failed_msg "\n\n$err"]
-               unlock_index
-               return
-       }
-
-       $ui_diff conf -state normal
-       $ui_diff delete $s_lno $e_lno
-       $ui_diff conf -state disabled
-
-       if {[$ui_diff get 1.0 end] eq "\n"} {
-               set o _
-       } else {
-               set o ?
-       }
-
-       if {$current_diff_side eq $ui_index} {
-               set mi ${o}M
-       } elseif {[string index $mi 0] eq {_}} {
-               set mi M$o
-       } else {
-               set mi ?$o
-       }
-       unlock_index
-       display_file $current_diff_path $mi
-       if {$o eq {_}} {
-               clear_diff
-       }
-}
diff --git a/lib/encoding.tcl b/lib/encoding.tcl
deleted file mode 100644 (file)
index 7f06b0d..0000000
+++ /dev/null
@@ -1,276 +0,0 @@
-# git-gui encoding support
-# Copyright (C) 2005 Paul Mackerras <paulus@samba.org>
-# (Copied from gitk, commit fd8ccbec4f0161)
-
-# This list of encoding names and aliases is distilled from
-# http://www.iana.org/assignments/character-sets.
-# Not all of them are supported by Tcl.
-set encoding_aliases {
-    { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII
-      ISO646-US US-ASCII us IBM367 cp367 csASCII }
-    { ISO-10646-UTF-1 csISO10646UTF1 }
-    { ISO_646.basic:1983 ref csISO646basic1983 }
-    { INVARIANT csINVARIANT }
-    { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion }
-    { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom }
-    { NATS-SEFI iso-ir-8-1 csNATSSEFI }
-    { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD }
-    { NATS-DANO iso-ir-9-1 csNATSDANO }
-    { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD }
-    { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish }
-    { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames }
-    { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 }
-    { ISO-2022-KR csISO2022KR }
-    { EUC-KR csEUCKR }
-    { ISO-2022-JP csISO2022JP }
-    { ISO-2022-JP-2 csISO2022JP2 }
-    { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7
-      csISO13JISC6220jp }
-    { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro }
-    { IT iso-ir-15 ISO646-IT csISO15Italian }
-    { PT iso-ir-16 ISO646-PT csISO16Portuguese }
-    { ES iso-ir-17 ISO646-ES csISO17Spanish }
-    { greek7-old iso-ir-18 csISO18Greek7Old }
-    { latin-greek iso-ir-19 csISO19LatinGreek }
-    { DIN_66003 iso-ir-21 de ISO646-DE csISO21German }
-    { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French }
-    { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 }
-    { ISO_5427 iso-ir-37 csISO5427Cyrillic }
-    { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 }
-    { BS_viewdata iso-ir-47 csISO47BSViewdata }
-    { INIS iso-ir-49 csISO49INIS }
-    { INIS-8 iso-ir-50 csISO50INIS8 }
-    { INIS-cyrillic iso-ir-51 csISO51INISCyrillic }
-    { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 }
-    { ISO_5428:1980 iso-ir-55 csISO5428Greek }
-    { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 }
-    { GB_2312-80 iso-ir-58 chinese csISO58GB231280 }
-    { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian
-      csISO60Norwegian1 }
-    { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 }
-    { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French }
-    { videotex-suppl iso-ir-70 csISO70VideotexSupp1 }
-    { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 }
-    { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 }
-    { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian }
-    { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 }
-    { greek7 iso-ir-88 csISO88Greek7 }
-    { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 }
-    { iso-ir-90 csISO90 }
-    { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a }
-    { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b
-      csISO92JISC62991984b }
-    { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd }
-    { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand }
-    { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add
-      csISO95JIS62291984handadd }
-    { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana }
-    { ISO_2033-1983 iso-ir-98 e13b csISO2033 }
-    { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS }
-    { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819
-      CP819 csISOLatin1 }
-    { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 }
-    { T.61-7bit iso-ir-102 csISO102T617bit }
-    { T.61-8bit T.61 iso-ir-103 csISO103T618bit }
-    { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 }
-    { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 }
-    { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic }
-    { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 }
-    { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 }
-    { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr }
-    { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708
-      arabic csISOLatinArabic }
-    { ISO_8859-6-E csISO88596E ISO-8859-6-E }
-    { ISO_8859-6-I csISO88596I ISO-8859-6-I }
-    { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118
-      greek greek8 csISOLatinGreek }
-    { T.101-G2 iso-ir-128 csISO128T101G2 }
-    { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew
-      csISOLatinHebrew }
-    { ISO_8859-8-E csISO88598E ISO-8859-8-E }
-    { ISO_8859-8-I csISO88598I ISO-8859-8-I }
-    { CSN_369103 iso-ir-139 csISO139CSN369103 }
-    { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 }
-    { ISO_6937-2-add iso-ir-142 csISOTextComm }
-    { IEC_P27-1 iso-ir-143 csISO143IECP271 }
-    { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic
-      csISOLatinCyrillic }
-    { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian }
-    { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian }
-    { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 }
-    { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT }
-    { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba }
-    { ISO_6937-2-25 iso-ir-152 csISO6937Add }
-    { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 }
-    { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp }
-    { ISO_10367-box iso-ir-155 csISO10367Box }
-    { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 }
-    { latin-lap lap iso-ir-158 csISO158Lap }
-    { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 }
-    { DS_2089 DS2089 ISO646-DK dk csISO646Danish }
-    { us-dk csUSDK }
-    { dk-us csDKUS }
-    { JIS_X0201 X0201 csHalfWidthKatakana }
-    { KSC5636 ISO646-KR csKSC5636 }
-    { ISO-10646-UCS-2 csUnicode }
-    { ISO-10646-UCS-4 csUCS4 }
-    { DEC-MCS dec csDECMCS }
-    { hp-roman8 roman8 r8 csHPRoman8 }
-    { macintosh mac csMacintosh }
-    { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl
-      csIBM037 }
-    { IBM038 EBCDIC-INT cp038 csIBM038 }
-    { IBM273 CP273 csIBM273 }
-    { IBM274 EBCDIC-BE CP274 csIBM274 }
-    { IBM275 EBCDIC-BR cp275 csIBM275 }
-    { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 }
-    { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 }
-    { IBM280 CP280 ebcdic-cp-it csIBM280 }
-    { IBM281 EBCDIC-JP-E cp281 csIBM281 }
-    { IBM284 CP284 ebcdic-cp-es csIBM284 }
-    { IBM285 CP285 ebcdic-cp-gb csIBM285 }
-    { IBM290 cp290 EBCDIC-JP-kana csIBM290 }
-    { IBM297 cp297 ebcdic-cp-fr csIBM297 }
-    { IBM420 cp420 ebcdic-cp-ar1 csIBM420 }
-    { IBM423 cp423 ebcdic-cp-gr csIBM423 }
-    { IBM424 cp424 ebcdic-cp-he csIBM424 }
-    { IBM437 cp437 437 csPC8CodePage437 }
-    { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 }
-    { IBM775 cp775 csPC775Baltic }
-    { IBM850 cp850 850 csPC850Multilingual }
-    { IBM851 cp851 851 csIBM851 }
-    { IBM852 cp852 852 csPCp852 }
-    { IBM855 cp855 855 csIBM855 }
-    { IBM857 cp857 857 csIBM857 }
-    { IBM860 cp860 860 csIBM860 }
-    { IBM861 cp861 861 cp-is csIBM861 }
-    { IBM862 cp862 862 csPC862LatinHebrew }
-    { IBM863 cp863 863 csIBM863 }
-    { IBM864 cp864 csIBM864 }
-    { IBM865 cp865 865 csIBM865 }
-    { IBM866 cp866 866 csIBM866 }
-    { IBM868 CP868 cp-ar csIBM868 }
-    { IBM869 cp869 869 cp-gr csIBM869 }
-    { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 }
-    { IBM871 CP871 ebcdic-cp-is csIBM871 }
-    { IBM880 cp880 EBCDIC-Cyrillic csIBM880 }
-    { IBM891 cp891 csIBM891 }
-    { IBM903 cp903 csIBM903 }
-    { IBM904 cp904 904 csIBBM904 }
-    { IBM905 CP905 ebcdic-cp-tr csIBM905 }
-    { IBM918 CP918 ebcdic-cp-ar2 csIBM918 }
-    { IBM1026 CP1026 csIBM1026 }
-    { EBCDIC-AT-DE csIBMEBCDICATDE }
-    { EBCDIC-AT-DE-A csEBCDICATDEA }
-    { EBCDIC-CA-FR csEBCDICCAFR }
-    { EBCDIC-DK-NO csEBCDICDKNO }
-    { EBCDIC-DK-NO-A csEBCDICDKNOA }
-    { EBCDIC-FI-SE csEBCDICFISE }
-    { EBCDIC-FI-SE-A csEBCDICFISEA }
-    { EBCDIC-FR csEBCDICFR }
-    { EBCDIC-IT csEBCDICIT }
-    { EBCDIC-PT csEBCDICPT }
-    { EBCDIC-ES csEBCDICES }
-    { EBCDIC-ES-A csEBCDICESA }
-    { EBCDIC-ES-S csEBCDICESS }
-    { EBCDIC-UK csEBCDICUK }
-    { EBCDIC-US csEBCDICUS }
-    { UNKNOWN-8BIT csUnknown8BiT }
-    { MNEMONIC csMnemonic }
-    { MNEM csMnem }
-    { VISCII csVISCII }
-    { VIQR csVIQR }
-    { KOI8-R csKOI8R }
-    { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro }
-    { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro }
-    { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro }
-    { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro }
-    { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro }
-    { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro }
-    { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro }
-    { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro }
-    { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro }
-    { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro }
-    { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro }
-    { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro }
-    { IBM1047 IBM-1047 }
-    { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian }
-    { Amiga-1251 Ami1251 Amiga1251 Ami-1251 }
-    { UNICODE-1-1 csUnicode11 }
-    { CESU-8 csCESU-8 }
-    { BOCU-1 csBOCU-1 }
-    { UNICODE-1-1-UTF-7 csUnicode11UTF7 }
-    { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic
-      l8 }
-    { ISO-8859-15 ISO_8859-15 Latin-9 }
-    { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 }
-    { GBK CP936 MS936 windows-936 }
-    { JIS_Encoding csJISEncoding }
-    { Shift_JIS MS_Kanji csShiftJIS }
-    { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese
-      EUC-JP }
-    { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese }
-    { ISO-10646-UCS-Basic csUnicodeASCII }
-    { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 }
-    { ISO-Unicode-IBM-1261 csUnicodeIBM1261 }
-    { ISO-Unicode-IBM-1268 csUnicodeIBM1268 }
-    { ISO-Unicode-IBM-1276 csUnicodeIBM1276 }
-    { ISO-Unicode-IBM-1264 csUnicodeIBM1264 }
-    { ISO-Unicode-IBM-1265 csUnicodeIBM1265 }
-    { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 }
-    { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 }
-    { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 }
-    { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 }
-    { Adobe-Standard-Encoding csAdobeStandardEncoding }
-    { Ventura-US csVenturaUS }
-    { Ventura-International csVenturaInternational }
-    { PC8-Danish-Norwegian csPC8DanishNorwegian }
-    { PC8-Turkish csPC8Turkish }
-    { IBM-Symbols csIBMSymbols }
-    { IBM-Thai csIBMThai }
-    { HP-Legal csHPLegal }
-    { HP-Pi-font csHPPiFont }
-    { HP-Math8 csHPMath8 }
-    { Adobe-Symbol-Encoding csHPPSMath }
-    { HP-DeskTop csHPDesktop }
-    { Ventura-Math csVenturaMath }
-    { Microsoft-Publishing csMicrosoftPublishing }
-    { Windows-31J csWindows31J }
-    { GB2312 csGB2312 }
-    { Big5 csBig5 }
-}
-
-proc tcl_encoding {enc} {
-    global encoding_aliases
-    set names [encoding names]
-    set lcnames [string tolower $names]
-    set enc [string tolower $enc]
-    set i [lsearch -exact $lcnames $enc]
-    if {$i < 0} {
-       # look for "isonnn" instead of "iso-nnn" or "iso_nnn"
-       if {[regsub {^iso[-_]} $enc iso encx]} {
-           set i [lsearch -exact $lcnames $encx]
-       }
-    }
-    if {$i < 0} {
-       foreach l $encoding_aliases {
-           set ll [string tolower $l]
-           if {[lsearch -exact $ll $enc] < 0} continue
-           # look through the aliases for one that tcl knows about
-           foreach e $ll {
-               set i [lsearch -exact $lcnames $e]
-               if {$i < 0} {
-                   if {[regsub {^iso[-_]} $e iso ex]} {
-                       set i [lsearch -exact $lcnames $ex]
-                   }
-               }
-               if {$i >= 0} break
-           }
-           break
-       }
-    }
-    if {$i >= 0} {
-       return [lindex $names $i]
-    }
-    return {}
-}
diff --git a/lib/error.tcl b/lib/error.tcl
deleted file mode 100644 (file)
index 13565b7..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-# git-gui branch (create/delete) support
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc error_popup {msg} {
-       set title [appname]
-       if {[reponame] ne {}} {
-               append title " ([reponame])"
-       }
-       set cmd [list tk_messageBox \
-               -icon error \
-               -type ok \
-               -title [append "$title: " [mc "error"]] \
-               -message $msg]
-       if {[winfo ismapped .]} {
-               lappend cmd -parent .
-       }
-       eval $cmd
-}
-
-proc warn_popup {msg} {
-       set title [appname]
-       if {[reponame] ne {}} {
-               append title " ([reponame])"
-       }
-       set cmd [list tk_messageBox \
-               -icon warning \
-               -type ok \
-               -title [append "$title: " [mc "warning"]] \
-               -message $msg]
-       if {[winfo ismapped .]} {
-               lappend cmd -parent .
-       }
-       eval $cmd
-}
-
-proc info_popup {msg {parent .}} {
-       set title [appname]
-       if {[reponame] ne {}} {
-               append title " ([reponame])"
-       }
-       tk_messageBox \
-               -parent $parent \
-               -icon info \
-               -type ok \
-               -title $title \
-               -message $msg
-}
-
-proc ask_popup {msg} {
-       set title [appname]
-       if {[reponame] ne {}} {
-               append title " ([reponame])"
-       }
-       set cmd [list tk_messageBox \
-               -icon question \
-               -type yesno \
-               -title $title \
-               -message $msg]
-       if {[winfo ismapped .]} {
-               lappend cmd -parent .
-       }
-       eval $cmd
-}
-
-proc hook_failed_popup {hook msg} {
-       set w .hookfail
-       toplevel $w
-
-       frame $w.m
-       label $w.m.l1 -text "$hook hook failed:" \
-               -anchor w \
-               -justify left \
-               -font font_uibold
-       text $w.m.t \
-               -background white -borderwidth 1 \
-               -relief sunken \
-               -width 80 -height 10 \
-               -font font_diff \
-               -yscrollcommand [list $w.m.sby set]
-       label $w.m.l2 \
-               -text [mc "You must correct the above errors before committing."] \
-               -anchor w \
-               -justify left \
-               -font font_uibold
-       scrollbar $w.m.sby -command [list $w.m.t yview]
-       pack $w.m.l1 -side top -fill x
-       pack $w.m.l2 -side bottom -fill x
-       pack $w.m.sby -side right -fill y
-       pack $w.m.t -side left -fill both -expand 1
-       pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
-
-       $w.m.t insert 1.0 $msg
-       $w.m.t conf -state disabled
-
-       button $w.ok -text OK \
-               -width 15 \
-               -command "destroy $w"
-       pack $w.ok -side bottom -anchor e -pady 10 -padx 10
-
-       bind $w <Visibility> "grab $w; focus $w"
-       bind $w <Key-Return> "destroy $w"
-       wm title $w [append "[appname] ([reponame]): " [mc "error"]]
-       tkwait window $w
-}
diff --git a/lib/git-gui.ico b/lib/git-gui.ico
deleted file mode 100644 (file)
index 334cfa5..0000000
Binary files a/lib/git-gui.ico and /dev/null differ
diff --git a/lib/index.tcl b/lib/index.tcl
deleted file mode 100644 (file)
index a0b22f2..0000000
+++ /dev/null
@@ -1,435 +0,0 @@
-# git-gui index (add/remove) support
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc _delete_indexlock {} {
-       if {[catch {file delete -- [gitdir index.lock]} err]} {
-               error_popup [strcat [mc "Unable to unlock the index."] "\n\n$err"]
-       }
-}
-
-proc _close_updateindex {fd after} {
-       fconfigure $fd -blocking 1
-       if {[catch {close $fd} err]} {
-               set w .indexfried
-               toplevel $w
-               wm title $w [strcat "[appname] ([reponame]): " [mc "Index Error"]]
-               wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
-               pack [label $w.msg \
-                       -justify left \
-                       -anchor w \
-                       -text [strcat \
-                               [mc "Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui."] \
-                               "\n\n$err"] \
-                       ] -anchor w
-
-               frame $w.buttons
-               button $w.buttons.continue \
-                       -text [mc "Continue"] \
-                       -command [list destroy $w]
-               pack $w.buttons.continue -side right -padx 5
-               button $w.buttons.unlock \
-                       -text [mc "Unlock Index"] \
-                       -command "destroy $w; _delete_indexlock"
-               pack $w.buttons.unlock -side right
-               pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-               wm protocol $w WM_DELETE_WINDOW update
-               bind $w.buttons.continue <Visibility> "
-                       grab $w
-                       focus $w.buttons.continue
-               "
-               tkwait window $w
-
-               $::main_status stop
-               unlock_index
-               rescan $after 0
-               return
-       }
-
-       $::main_status stop
-       unlock_index
-       uplevel #0 $after
-}
-
-proc update_indexinfo {msg pathList after} {
-       global update_index_cp
-
-       if {![lock_index update]} return
-
-       set update_index_cp 0
-       set pathList [lsort $pathList]
-       set totalCnt [llength $pathList]
-       set batch [expr {int($totalCnt * .01) + 1}]
-       if {$batch > 25} {set batch 25}
-
-       $::main_status start $msg [mc "files"]
-       set fd [git_write update-index -z --index-info]
-       fconfigure $fd \
-               -blocking 0 \
-               -buffering full \
-               -buffersize 512 \
-               -encoding binary \
-               -translation binary
-       fileevent $fd writable [list \
-               write_update_indexinfo \
-               $fd \
-               $pathList \
-               $totalCnt \
-               $batch \
-               $after \
-               ]
-}
-
-proc write_update_indexinfo {fd pathList totalCnt batch after} {
-       global update_index_cp
-       global file_states current_diff_path
-
-       if {$update_index_cp >= $totalCnt} {
-               _close_updateindex $fd $after
-               return
-       }
-
-       for {set i $batch} \
-               {$update_index_cp < $totalCnt && $i > 0} \
-               {incr i -1} {
-               set path [lindex $pathList $update_index_cp]
-               incr update_index_cp
-
-               set s $file_states($path)
-               switch -glob -- [lindex $s 0] {
-               A? {set new _O}
-               M? {set new _M}
-               D_ {set new _D}
-               D? {set new _?}
-               ?? {continue}
-               }
-               set info [lindex $s 2]
-               if {$info eq {}} continue
-
-               puts -nonewline $fd "$info\t[encoding convertto $path]\0"
-               display_file $path $new
-       }
-
-       $::main_status update $update_index_cp $totalCnt
-}
-
-proc update_index {msg pathList after} {
-       global update_index_cp
-
-       if {![lock_index update]} return
-
-       set update_index_cp 0
-       set pathList [lsort $pathList]
-       set totalCnt [llength $pathList]
-       set batch [expr {int($totalCnt * .01) + 1}]
-       if {$batch > 25} {set batch 25}
-
-       $::main_status start $msg [mc "files"]
-       set fd [git_write update-index --add --remove -z --stdin]
-       fconfigure $fd \
-               -blocking 0 \
-               -buffering full \
-               -buffersize 512 \
-               -encoding binary \
-               -translation binary
-       fileevent $fd writable [list \
-               write_update_index \
-               $fd \
-               $pathList \
-               $totalCnt \
-               $batch \
-               $after \
-               ]
-}
-
-proc write_update_index {fd pathList totalCnt batch after} {
-       global update_index_cp
-       global file_states current_diff_path
-
-       if {$update_index_cp >= $totalCnt} {
-               _close_updateindex $fd $after
-               return
-       }
-
-       for {set i $batch} \
-               {$update_index_cp < $totalCnt && $i > 0} \
-               {incr i -1} {
-               set path [lindex $pathList $update_index_cp]
-               incr update_index_cp
-
-               switch -glob -- [lindex $file_states($path) 0] {
-               AD {set new __}
-               ?D {set new D_}
-               _O -
-               AM {set new A_}
-               U? {
-                       if {[file exists $path]} {
-                               set new M_
-                       } else {
-                               set new D_
-                       }
-               }
-               ?M {set new M_}
-               ?? {continue}
-               }
-               puts -nonewline $fd "[encoding convertto $path]\0"
-               display_file $path $new
-       }
-
-       $::main_status update $update_index_cp $totalCnt
-}
-
-proc checkout_index {msg pathList after} {
-       global update_index_cp
-
-       if {![lock_index update]} return
-
-       set update_index_cp 0
-       set pathList [lsort $pathList]
-       set totalCnt [llength $pathList]
-       set batch [expr {int($totalCnt * .01) + 1}]
-       if {$batch > 25} {set batch 25}
-
-       $::main_status start $msg [mc "files"]
-       set fd [git_write checkout-index \
-               --index \
-               --quiet \
-               --force \
-               -z \
-               --stdin \
-               ]
-       fconfigure $fd \
-               -blocking 0 \
-               -buffering full \
-               -buffersize 512 \
-               -encoding binary \
-               -translation binary
-       fileevent $fd writable [list \
-               write_checkout_index \
-               $fd \
-               $pathList \
-               $totalCnt \
-               $batch \
-               $after \
-               ]
-}
-
-proc write_checkout_index {fd pathList totalCnt batch after} {
-       global update_index_cp
-       global file_states current_diff_path
-
-       if {$update_index_cp >= $totalCnt} {
-               _close_updateindex $fd $after
-               return
-       }
-
-       for {set i $batch} \
-               {$update_index_cp < $totalCnt && $i > 0} \
-               {incr i -1} {
-               set path [lindex $pathList $update_index_cp]
-               incr update_index_cp
-               switch -glob -- [lindex $file_states($path) 0] {
-               U? {continue}
-               ?M -
-               ?D {
-                       puts -nonewline $fd "[encoding convertto $path]\0"
-                       display_file $path ?_
-               }
-               }
-       }
-
-       $::main_status update $update_index_cp $totalCnt
-}
-
-proc unstage_helper {txt paths} {
-       global file_states current_diff_path
-
-       if {![lock_index begin-update]} return
-
-       set pathList [list]
-       set after {}
-       foreach path $paths {
-               switch -glob -- [lindex $file_states($path) 0] {
-               A? -
-               M? -
-               D? {
-                       lappend pathList $path
-                       if {$path eq $current_diff_path} {
-                               set after {reshow_diff;}
-                       }
-               }
-               }
-       }
-       if {$pathList eq {}} {
-               unlock_index
-       } else {
-               update_indexinfo \
-                       $txt \
-                       $pathList \
-                       [concat $after [list ui_ready]]
-       }
-}
-
-proc do_unstage_selection {} {
-       global current_diff_path selected_paths
-
-       if {[array size selected_paths] > 0} {
-               unstage_helper \
-                       {Unstaging selected files from commit} \
-                       [array names selected_paths]
-       } elseif {$current_diff_path ne {}} {
-               unstage_helper \
-                       [mc "Unstaging %s from commit" [short_path $current_diff_path]] \
-                       [list $current_diff_path]
-       }
-}
-
-proc add_helper {txt paths} {
-       global file_states current_diff_path
-
-       if {![lock_index begin-update]} return
-
-       set pathList [list]
-       set after {}
-       foreach path $paths {
-               switch -glob -- [lindex $file_states($path) 0] {
-               _O -
-               ?M -
-               ?D -
-               U? {
-                       lappend pathList $path
-                       if {$path eq $current_diff_path} {
-                               set after {reshow_diff;}
-                       }
-               }
-               }
-       }
-       if {$pathList eq {}} {
-               unlock_index
-       } else {
-               update_index \
-                       $txt \
-                       $pathList \
-                       [concat $after {ui_status {Ready to commit.}}]
-       }
-}
-
-proc do_add_selection {} {
-       global current_diff_path selected_paths
-
-       if {[array size selected_paths] > 0} {
-               add_helper \
-                       {Adding selected files} \
-                       [array names selected_paths]
-       } elseif {$current_diff_path ne {}} {
-               add_helper \
-                       [mc "Adding %s" [short_path $current_diff_path]] \
-                       [list $current_diff_path]
-       }
-}
-
-proc do_add_all {} {
-       global file_states
-
-       set paths [list]
-       foreach path [array names file_states] {
-               switch -glob -- [lindex $file_states($path) 0] {
-               U? {continue}
-               ?M -
-               ?D {lappend paths $path}
-               }
-       }
-       add_helper {Adding all changed files} $paths
-}
-
-proc revert_helper {txt paths} {
-       global file_states current_diff_path
-
-       if {![lock_index begin-update]} return
-
-       set pathList [list]
-       set after {}
-       foreach path $paths {
-               switch -glob -- [lindex $file_states($path) 0] {
-               U? {continue}
-               ?M -
-               ?D {
-                       lappend pathList $path
-                       if {$path eq $current_diff_path} {
-                               set after {reshow_diff;}
-                       }
-               }
-               }
-       }
-
-
-       # Split question between singular and plural cases, because
-       # such distinction is needed in some languages. Previously, the
-       # code used "Revert changes in" for both, but that can't work
-       # in languages where 'in' must be combined with word from
-       # rest of string (in diffrent way for both cases of course).
-       #
-       # FIXME: Unfortunately, even that isn't enough in some languages
-       # as they have quite complex plural-form rules. Unfortunately,
-       # msgcat doesn't seem to support that kind of string translation.
-       #
-       set n [llength $pathList]
-       if {$n == 0} {
-               unlock_index
-               return
-       } elseif {$n == 1} {
-               set query [mc "Revert changes in file %s?" [short_path [lindex $pathList]]]
-       } else {
-               set query [mc "Revert changes in these %i files?" $n]
-       }
-
-       set reply [tk_dialog \
-               .confirm_revert \
-               "[appname] ([reponame])" \
-               [mc "Any unstaged changes will be permanently lost by the revert."] \
-               question \
-               1 \
-               [mc "Do Nothing"] \
-               [mc "Revert Changes"] \
-               ]
-       if {$reply == 1} {
-               checkout_index \
-                       $txt \
-                       $pathList \
-                       [concat $after [list ui_ready]]
-       } else {
-               unlock_index
-       }
-}
-
-proc do_revert_selection {} {
-       global current_diff_path selected_paths
-
-       if {[array size selected_paths] > 0} {
-               revert_helper \
-                       {Reverting selected files} \
-                       [array names selected_paths]
-       } elseif {$current_diff_path ne {}} {
-               revert_helper \
-                       "Reverting [short_path $current_diff_path]" \
-                       [list $current_diff_path]
-       }
-}
-
-proc do_select_commit_type {} {
-       global commit_type selected_commit_type
-
-       if {$selected_commit_type eq {new}
-               && [string match amend* $commit_type]} {
-               create_new_commit
-       } elseif {$selected_commit_type eq {amend}
-               && ![string match amend* $commit_type]} {
-               load_last_commit
-
-               # The amend request was rejected...
-               #
-               if {![string match amend* $commit_type]} {
-                       set selected_commit_type new
-               }
-       }
-}
diff --git a/lib/logo.tcl b/lib/logo.tcl
deleted file mode 100644 (file)
index 5ff7669..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-# git-gui Git Gui logo
-# Copyright (C) 2007 Shawn Pearce
-
-# Henrik Nyh's alternative Git logo, from his blog post
-# http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon
-#
-image create photo ::git_logo_data -data {
-R0lGODdhYQC8AIQbAGZmZtg4LW9vb3l5eYKCgoyMjEC/TOJpYZWVlZ+fn2/PeKmpqbKysry8vMXF
-xZ/fpc/Pz7fnvPXNytnZ2eLi4s/v0vja1+zs7Of36fX19f3z8v///////////////////ywAAAAA
-YQC8AAAF/uAmjmRpnmiqrmzrvq4hz3RtGw+s7zx5/7dcb0hUAY8zYXHJRCKVzGjPeYRKry8q0Irt
-GrVBr3gFDo/PprKNix6ra+y2902Ly7H05L2dl9n3UX04gGeCf4RFhohiiotdjY5XkJGBfYeUOpOY
-iZablXmXURgPpKWmp6ipqYIKqq6vqREjFYK1trUKs7e7vFq5IrS9wsM0vxvBxMm8xsjKzqy6z9J5
-zNPWatXX2k7Z29433d/iMuHj3+Xm2+jp1+vs0+7vz/HyyvT1xPf4wvr7y9H+pBkbBasgLFYGE8ba
-o8nTlE4OOYGKKJFOKIopGmLMAnHjDo0eWYAM+WUiSRgj/k+eSKmyBMuWI17C3CATZs2WN1XmPLmT
-ZM+QPz0G3VihqNGjSJNWwDCzqdOnUKPu0SChqtWrWLNq3cq1q9evYCVYGCEhgNmzaNOqXcu2rdu3
-cOMGOEBWrt27ePPCpSuirN6/gAO35bvBr+DDiPMSNpy4sWO2ix9Lnmw2MuXLiS1j3gxYM+fPdz2D
-Hv1WNOnTak2jXj23LuvXlV3DZq16Nujatjnjzo15N2/Kvn9LDi7cMfHimaUqX868ufPn0KPPpOCA
-AQMWCQBo3869u/fv4MNrd3DlQoMC3QlkSJFdvPv38LVDWJLBAYHwE1LE38+/+/UhGTAggHv5odDf
-gfv9/seDgPAVeAKCELqnIAwU3BefgyZEqOF3E7rAQH8YlrDhiNt1uEIG6IGoH4kjmpjCBRaqaCCL
-G7p4AgUDIhgiCTTW2AKOEe44Qo8a2khCBgNoKKQIREZopAgZxAjhkhs0CeGTG7Sn5IpW9vekAyRS
-2eWBRl6Q44ZijhlfAQlQmeKIaarpHZsMTHABCxDQGKec3JH3QpIs7snndn6yAKaeXA7aZwuABppo
-fAws0GiEhaKQJ40F3DkjfwVC8CaCAlCgAgIkJjDfCgdiOMGn/Q2w3gkZtPgqC6ma0ECECaBwa4QE
-aOpCrSYAqeMJpEKYqw7ABnsmfwQ8aCwPySqLYKUb/kwAYbPQyoiCtQcOUMKHBwrgK7LaogBuuaxC
-OkS0KEwa37EiLBufALPuwO4Jh/InwAixkknEvSe4C9+p3PY3rr3lpnDufguIcCmzRQAc7IHYLhxf
-w/8mnILA74lg8cARa4xCsZxusMCBomZccgsfv0deuh2HvLKh/sLs3hJSvieuCwUzvIHN4tGXc3ih
-vtDzmj8fSNLR8BWQdH9LH+g00OFF3d/UBx4cUcvuOc21eFRiouV+Xvvr0dDvlX21R/2uzTR89TqU
-L3+5UoBgAxtRHd5/CHpLkd13i4D2e3hHRLKMY+9Hr0Nvx/fq3Pw57cng7/m9wQVObnIyhAiQwHF8
-/tQS8nDgI2wOYeh3CAvhuIBHiDEgqvdtwudkaz3GBPKaTcKuGgqAJRMZmK6h1hnk3ncDcUvhgPFS
-o5B476ZKQcECzCN4qgmYN4lAncmzcAEEkhJp+QlfkyhAAdtbN8H67FvHQAF6b4g6v9UryqfkKkBu
-v/0prxD//kR63YnqB8AeqcdoBRxU/1zAuwRaaX4reJ4DSSRAHUhwgrgqwgUx2B94EWGDHISPBzUY
-QgSNcAn6K6F4fscDCtBOhdoRwPW6kIHDwZA7vWoDBF44Qd/tIUAEBCACbIeG4AXxfmFrQ4B4OCYE
-JBEQELChmgbAACJioj4JOCKCCLCABZ6EAg1IHwDlyLYAB1gRJhSYgHUQAD9WnQ9+CWBAA+wknTpC
-JwQAOw==
-}
-
-proc git_logo {w} {
-       label $w \
-               -borderwidth 1 \
-               -relief sunken \
-               -background white \
-               -image ::git_logo_data
-       return $w
-}
diff --git a/lib/merge.tcl b/lib/merge.tcl
deleted file mode 100644 (file)
index 63e1427..0000000
+++ /dev/null
@@ -1,274 +0,0 @@
-# git-gui branch merge support
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-class merge {
-
-field w         ; # top level window
-field w_rev     ; # mega-widget to pick the revision to merge
-
-method _can_merge {} {
-       global HEAD commit_type file_states
-
-       if {[string match amend* $commit_type]} {
-               info_popup [mc "Cannot merge while amending.
-
-You must finish amending this commit before starting any type of merge.
-"]
-               return 0
-       }
-
-       if {[committer_ident] eq {}} {return 0}
-       if {![lock_index merge]} {return 0}
-
-       # -- Our in memory state should match the repository.
-       #
-       repository_state curType curHEAD curMERGE_HEAD
-       if {$commit_type ne $curType || $HEAD ne $curHEAD} {
-               info_popup [mc "Last scanned state does not match repository state.
-
-Another Git program has modified this repository since the last scan.  A rescan must be performed before a merge can be performed.
-
-The rescan will be automatically started now.
-"]
-               unlock_index
-               rescan ui_ready
-               return 0
-       }
-
-       foreach path [array names file_states] {
-               switch -glob -- [lindex $file_states($path) 0] {
-               _O {
-                       continue; # and pray it works!
-               }
-               U? {
-                       error_popup [mc "You are in the middle of a conflicted merge.
-
-File %s has merge conflicts.
-
-You must resolve them, stage the file, and commit to complete the current merge.  Only then can you begin another merge.
-" [short_path $path]]
-                       unlock_index
-                       return 0
-               }
-               ?? {
-                       error_popup [mc "You are in the middle of a change.
-
-File %s is modified.
-
-You should complete the current commit before starting a merge.  Doing so will help you abort a failed merge, should the need arise.
-" [short_path $path]]
-                       unlock_index
-                       return 0
-               }
-               }
-       }
-
-       return 1
-}
-
-method _rev {} {
-       if {[catch {$w_rev commit_or_die}]} {
-               return {}
-       }
-       return [$w_rev get]
-}
-
-method _visualize {} {
-       set rev [_rev $this]
-       if {$rev ne {}} {
-               do_gitk [list $rev --not HEAD]
-       }
-}
-
-method _start {} {
-       global HEAD current_branch remote_url
-
-       set name [_rev $this]
-       if {$name eq {}} {
-               return
-       }
-
-       set spec [$w_rev get_tracking_branch]
-       set cmit [$w_rev get_commit]
-
-       set fh [open [gitdir FETCH_HEAD] w]
-       fconfigure $fh -translation lf
-       if {$spec eq {}} {
-               set remote .
-               set branch $name
-               set stitle $branch
-       } else {
-               set remote $remote_url([lindex $spec 1])
-               if {[regexp {^[^:@]*@[^:]*:/} $remote]} {
-                       regsub {^[^:@]*@} $remote {} remote
-               }
-               set branch [lindex $spec 2]
-               set stitle [mc "%s of %s" $branch $remote]
-       }
-       regsub ^refs/heads/ $branch {} branch
-       puts $fh "$cmit\t\tbranch '$branch' of $remote"
-       close $fh
-
-       set cmd [list git]
-       lappend cmd merge
-       lappend cmd --strategy=recursive
-       lappend cmd [git fmt-merge-msg <[gitdir FETCH_HEAD]]
-       lappend cmd HEAD
-       lappend cmd $name
-
-       set msg [mc "Merging %s and %s" $current_branch $stitle]
-       ui_status "$msg..."
-       set cons [console::new [mc "Merge"] "merge $stitle"]
-       console::exec $cons $cmd [cb _finish $cons]
-
-       wm protocol $w WM_DELETE_WINDOW {}
-       destroy $w
-}
-
-method _finish {cons ok} {
-       console::done $cons $ok
-       if {$ok} {
-               set msg [mc "Merge completed successfully."]
-       } else {
-               set msg [mc "Merge failed.  Conflict resolution is required."]
-       }
-       unlock_index
-       rescan [list ui_status $msg]
-       delete_this
-}
-
-constructor dialog {} {
-       global current_branch
-       global M1B
-
-       if {![_can_merge $this]} {
-               delete_this
-               return
-       }
-
-       make_toplevel top w
-       wm title $top [append "[appname] ([reponame]): " [mc "Merge"]]
-       if {$top ne {.}} {
-               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
-       }
-
-       set _start [cb _start]
-
-       label $w.header \
-               -text [mc "Merge Into %s" $current_branch] \
-               -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       button $w.buttons.visualize \
-               -text [mc Visualize] \
-               -command [cb _visualize]
-       pack $w.buttons.visualize -side left
-       button $w.buttons.merge \
-               -text [mc Merge] \
-               -command $_start
-       pack $w.buttons.merge -side right
-       button $w.buttons.cancel \
-               -text [mc "Cancel"] \
-               -command [cb _cancel]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       set w_rev [::choose_rev::new_unmerged $w.rev [mc "Revision To Merge"]]
-       pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5
-
-       bind $w <$M1B-Key-Return> $_start
-       bind $w <Key-Return> $_start
-       bind $w <Key-Escape> [cb _cancel]
-       wm protocol $w WM_DELETE_WINDOW [cb _cancel]
-
-       bind $w.buttons.merge <Visibility> [cb _visible]
-       tkwait window $w
-}
-
-method _visible {} {
-       grab $w
-       if {[is_config_true gui.matchtrackingbranch]} {
-               $w_rev pick_tracking_branch
-       }
-       $w_rev focus_filter
-}
-
-method _cancel {} {
-       wm protocol $w WM_DELETE_WINDOW {}
-       unlock_index
-       destroy $w
-       delete_this
-}
-
-}
-
-namespace eval merge {
-
-proc reset_hard {} {
-       global HEAD commit_type file_states
-
-       if {[string match amend* $commit_type]} {
-               info_popup [mc "Cannot abort while amending.
-
-You must finish amending this commit.
-"]
-               return
-       }
-
-       if {![lock_index abort]} return
-
-       if {[string match *merge* $commit_type]} {
-               set op_question [mc "Abort merge?
-
-Aborting the current merge will cause *ALL* uncommitted changes to be lost.
-
-Continue with aborting the current merge?"]
-       } else {
-               set op_question [mc "Reset changes?
-
-Resetting the changes will cause *ALL* uncommitted changes to be lost.
-
-Continue with resetting the current changes?"]
-       }
-
-       if {[ask_popup $op_question] eq {yes}} {
-               set fd [git_read --stderr read-tree --reset -u -v HEAD]
-               fconfigure $fd -blocking 0 -translation binary
-               fileevent $fd readable [namespace code [list _reset_wait $fd]]
-               $::main_status start [mc "Aborting"] {files reset}
-       } else {
-               unlock_index
-       }
-}
-
-proc _reset_wait {fd} {
-       global ui_comm
-
-       $::main_status update_meter [read $fd]
-
-       fconfigure $fd -blocking 1
-       if {[eof $fd]} {
-               set fail [catch {close $fd} err]
-               $::main_status stop
-               unlock_index
-
-               $ui_comm delete 0.0 end
-               $ui_comm edit modified false
-
-               catch {file delete [gitdir MERGE_HEAD]}
-               catch {file delete [gitdir rr-cache MERGE_RR]}
-               catch {file delete [gitdir SQUASH_MSG]}
-               catch {file delete [gitdir MERGE_MSG]}
-               catch {file delete [gitdir GITGUI_MSG]}
-
-               if {$fail} {
-                       warn_popup "[mc "Abort failed."]\n\n$err"
-               }
-               rescan {ui_status [mc "Abort completed.  Ready."]}
-       } else {
-               fconfigure $fd -blocking 0
-       }
-}
-
-}
diff --git a/lib/option.tcl b/lib/option.tcl
deleted file mode 100644 (file)
index f812e5e..0000000
+++ /dev/null
@@ -1,239 +0,0 @@
-# git-gui options editor
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc save_config {} {
-       global default_config font_descs
-       global repo_config global_config
-       global repo_config_new global_config_new
-
-       foreach option $font_descs {
-               set name [lindex $option 0]
-               set font [lindex $option 1]
-               font configure $font \
-                       -family $global_config_new(gui.$font^^family) \
-                       -size $global_config_new(gui.$font^^size)
-               font configure ${font}bold \
-                       -family $global_config_new(gui.$font^^family) \
-                       -size $global_config_new(gui.$font^^size)
-               font configure ${font}italic \
-                       -family $global_config_new(gui.$font^^family) \
-                       -size $global_config_new(gui.$font^^size)
-               set global_config_new(gui.$name) [font configure $font]
-               unset global_config_new(gui.$font^^family)
-               unset global_config_new(gui.$font^^size)
-       }
-
-       foreach name [array names default_config] {
-               set value $global_config_new($name)
-               if {$value ne $global_config($name)} {
-                       if {$value eq $default_config($name)} {
-                               catch {git config --global --unset $name}
-                       } else {
-                               regsub -all "\[{}\]" $value {"} value
-                               git config --global $name $value
-                       }
-                       set global_config($name) $value
-                       if {$value eq $repo_config($name)} {
-                               catch {git config --unset $name}
-                               set repo_config($name) $value
-                       }
-               }
-       }
-
-       foreach name [array names default_config] {
-               set value $repo_config_new($name)
-               if {$value ne $repo_config($name)} {
-                       if {$value eq $global_config($name)} {
-                               catch {git config --unset $name}
-                       } else {
-                               regsub -all "\[{}\]" $value {"} value
-                               git config $name $value
-                       }
-                       set repo_config($name) $value
-               }
-       }
-}
-
-proc do_options {} {
-       global repo_config global_config font_descs
-       global repo_config_new global_config_new
-
-       array unset repo_config_new
-       array unset global_config_new
-       foreach name [array names repo_config] {
-               set repo_config_new($name) $repo_config($name)
-       }
-       load_config 1
-       foreach name [array names repo_config] {
-               switch -- $name {
-               gui.diffcontext {continue}
-               }
-               set repo_config_new($name) $repo_config($name)
-       }
-       foreach name [array names global_config] {
-               set global_config_new($name) $global_config($name)
-       }
-
-       set w .options_editor
-       toplevel $w
-       wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
-
-       frame $w.buttons
-       button $w.buttons.restore -text [mc "Restore Defaults"] \
-               -default normal \
-               -command do_restore_defaults
-       pack $w.buttons.restore -side left
-       button $w.buttons.save -text [mc Save] \
-               -default active \
-               -command [list do_save_config $w]
-       pack $w.buttons.save -side right
-       button $w.buttons.cancel -text [mc "Cancel"] \
-               -default normal \
-               -command [list destroy $w]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       labelframe $w.repo -text [mc "%s Repository" [reponame]]
-       labelframe $w.global -text [mc "Global (All Repositories)"]
-       pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
-       pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
-
-       set optid 0
-       foreach option {
-               {t user.name {mc "User Name"}}
-               {t user.email {mc "Email Address"}}
-
-               {b merge.summary {mc "Summarize Merge Commits"}}
-               {i-1..5 merge.verbosity {mc "Merge Verbosity"}}
-               {b merge.diffstat {mc "Show Diffstat After Merge"}}
-
-               {b gui.trustmtime  {mc "Trust File Modification Timestamps"}}
-               {b gui.pruneduringfetch {mc "Prune Tracking Branches During Fetch"}}
-               {b gui.matchtrackingbranch {mc "Match Tracking Branches"}}
-               {i-0..99 gui.diffcontext {mc "Number of Diff Context Lines"}}
-               {t gui.newbranchtemplate {mc "New Branch Name Template"}}
-               } {
-               set type [lindex $option 0]
-               set name [lindex $option 1]
-               set text [eval [lindex $option 2]]
-               incr optid
-               foreach f {repo global} {
-                       switch -glob -- $type {
-                       b {
-                               checkbutton $w.$f.$optid -text $text \
-                                       -variable ${f}_config_new($name) \
-                                       -onvalue true \
-                                       -offvalue false
-                               pack $w.$f.$optid -side top -anchor w
-                       }
-                       i-* {
-                               regexp -- {-(\d+)\.\.(\d+)$} $type _junk min max
-                               frame $w.$f.$optid
-                               label $w.$f.$optid.l -text "$text:"
-                               pack $w.$f.$optid.l -side left -anchor w -fill x
-                               spinbox $w.$f.$optid.v \
-                                       -textvariable ${f}_config_new($name) \
-                                       -from $min \
-                                       -to $max \
-                                       -increment 1 \
-                                       -width [expr {1 + [string length $max]}]
-                               bind $w.$f.$optid.v <FocusIn> {%W selection range 0 end}
-                               pack $w.$f.$optid.v -side right -anchor e -padx 5
-                               pack $w.$f.$optid -side top -anchor w -fill x
-                       }
-                       t {
-                               frame $w.$f.$optid
-                               label $w.$f.$optid.l -text "$text:"
-                               entry $w.$f.$optid.v \
-                                       -borderwidth 1 \
-                                       -relief sunken \
-                                       -width 20 \
-                                       -textvariable ${f}_config_new($name)
-                               pack $w.$f.$optid.l -side left -anchor w
-                               pack $w.$f.$optid.v -side left -anchor w \
-                                       -fill x -expand 1 \
-                                       -padx 5
-                               pack $w.$f.$optid -side top -anchor w -fill x
-                       }
-                       }
-               }
-       }
-
-       set all_fonts [lsort [font families]]
-       foreach option $font_descs {
-               set name [lindex $option 0]
-               set font [lindex $option 1]
-               set text [eval [lindex $option 2]]
-
-               set global_config_new(gui.$font^^family) \
-                       [font configure $font -family]
-               set global_config_new(gui.$font^^size) \
-                       [font configure $font -size]
-
-               frame $w.global.$name
-               label $w.global.$name.l -text "$text:"
-               button $w.global.$name.b \
-                       -text [mc "Change Font"] \
-                       -command [list \
-                               choose_font::pick \
-                               $w \
-                               [mc "Choose %s" $text] \
-                               global_config_new(gui.$font^^family) \
-                               global_config_new(gui.$font^^size) \
-                               ]
-               label $w.global.$name.f -textvariable global_config_new(gui.$font^^family)
-               label $w.global.$name.s -textvariable global_config_new(gui.$font^^size)
-               label $w.global.$name.pt -text [mc "pt."]
-               pack $w.global.$name.l -side left -anchor w
-               pack $w.global.$name.b -side right -anchor e
-               pack $w.global.$name.pt -side right -anchor w
-               pack $w.global.$name.s -side right -anchor w
-               pack $w.global.$name.f -side right -anchor w
-               pack $w.global.$name -side top -anchor w -fill x
-       }
-
-       bind $w <Visibility> "grab $w; focus $w.buttons.save"
-       bind $w <Key-Escape> "destroy $w"
-       bind $w <Key-Return> [list do_save_config $w]
-
-       if {[is_MacOSX]} {
-               set t [mc "Preferences"]
-       } else {
-               set t [mc "Options"]
-       }
-       wm title $w "[appname] ([reponame]): $t"
-       tkwait window $w
-}
-
-proc do_restore_defaults {} {
-       global font_descs default_config repo_config
-       global repo_config_new global_config_new
-
-       foreach name [array names default_config] {
-               set repo_config_new($name) $default_config($name)
-               set global_config_new($name) $default_config($name)
-       }
-
-       foreach option $font_descs {
-               set name [lindex $option 0]
-               set repo_config(gui.$name) $default_config(gui.$name)
-       }
-       apply_config
-
-       foreach option $font_descs {
-               set name [lindex $option 0]
-               set font [lindex $option 1]
-               set global_config_new(gui.$font^^family) \
-                       [font configure $font -family]
-               set global_config_new(gui.$font^^size) \
-                       [font configure $font -size]
-       }
-}
-
-proc do_save_config {w} {
-       if {[catch {save_config} err]} {
-               error_popup [strcat [mc "Failed to completely save options:"] "\n\n$err"]
-       }
-       reshow_diff
-       destroy $w
-}
diff --git a/lib/remote.tcl b/lib/remote.tcl
deleted file mode 100644 (file)
index 0e86dda..0000000
+++ /dev/null
@@ -1,222 +0,0 @@
-# git-gui remote management
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-set some_heads_tracking 0;  # assume not
-
-proc is_tracking_branch {name} {
-       global tracking_branches
-       foreach spec $tracking_branches {
-               set t [lindex $spec 0]
-               if {$t eq $name || [string match $t $name]} {
-                       return 1
-               }
-       }
-       return 0
-}
-
-proc all_tracking_branches {} {
-       global tracking_branches
-
-       set all [list]
-       set pat [list]
-       set cmd [list]
-
-       foreach spec $tracking_branches {
-               set dst [lindex $spec 0]
-               if {[string range $dst end-1 end] eq {/*}} {
-                       lappend pat $spec
-                       lappend cmd [string range $dst 0 end-2]
-               } else {
-                       lappend all $spec
-               }
-       }
-
-       if {$pat ne {}} {
-               set fd [eval git_read for-each-ref --format=%(refname) $cmd]
-               while {[gets $fd n] > 0} {
-                       foreach spec $pat {
-                               set dst [string range [lindex $spec 0] 0 end-2]
-                               set len [string length $dst]
-                               if {[string equal -length $len $dst $n]} {
-                                       set src [string range [lindex $spec 2] 0 end-2]
-                                       set spec [list \
-                                               $n \
-                                               [lindex $spec 1] \
-                                               $src[string range $n $len end] \
-                                               ]
-                                       lappend all $spec
-                               }
-                       }
-               }
-               close $fd
-       }
-
-       return [lsort -index 0 -unique $all]
-}
-
-proc load_all_remotes {} {
-       global repo_config
-       global all_remotes tracking_branches some_heads_tracking
-       global remote_url
-
-       set some_heads_tracking 0
-       set all_remotes [list]
-       set trck [list]
-
-       set rh_str refs/heads/
-       set rh_len [string length $rh_str]
-       set rm_dir [gitdir remotes]
-       if {[file isdirectory $rm_dir]} {
-               set all_remotes [glob \
-                       -types f \
-                       -tails \
-                       -nocomplain \
-                       -directory $rm_dir *]
-
-               foreach name $all_remotes {
-                       catch {
-                               set fd [open [file join $rm_dir $name] r]
-                               while {[gets $fd line] >= 0} {
-                                       if {[regexp {^URL:[     ]*(.+)$} $line line url]} {
-                                               set remote_url($name) $url
-                                               continue
-                                       }
-                                       if {![regexp {^Pull:[   ]*([^:]+):(.+)$} \
-                                               $line line src dst]} continue
-                                       if {[string index $src 0] eq {+}} {
-                                               set src [string range $src 1 end]
-                                       }
-                                       if {![string equal -length 5 refs/ $src]} {
-                                               set src $rh_str$src
-                                       }
-                                       if {![string equal -length 5 refs/ $dst]} {
-                                               set dst $rh_str$dst
-                                       }
-                                       if {[string equal -length $rh_len $rh_str $dst]} {
-                                               set some_heads_tracking 1
-                                       }
-                                       lappend trck [list $dst $name $src]
-                               }
-                               close $fd
-                       }
-               }
-       }
-
-       foreach line [array names repo_config remote.*.url] {
-               if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue
-               lappend all_remotes $name
-               set remote_url($name) $repo_config(remote.$name.url)
-
-               if {[catch {set fl $repo_config(remote.$name.fetch)}]} {
-                       set fl {}
-               }
-               foreach line $fl {
-                       if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue
-                       if {[string index $src 0] eq {+}} {
-                               set src [string range $src 1 end]
-                       }
-                       if {![string equal -length 5 refs/ $src]} {
-                               set src $rh_str$src
-                       }
-                       if {![string equal -length 5 refs/ $dst]} {
-                               set dst $rh_str$dst
-                       }
-                       if {[string equal -length $rh_len $rh_str $dst]} {
-                               set some_heads_tracking 1
-                       }
-                       lappend trck [list $dst $name $src]
-               }
-       }
-
-       set tracking_branches [lsort -index 0 -unique $trck]
-       set all_remotes [lsort -unique $all_remotes]
-}
-
-proc populate_fetch_menu {} {
-       global all_remotes repo_config
-
-       set remote_m .mbar.remote
-       set fetch_m $remote_m.fetch
-       set prune_m $remote_m.prune
-
-       foreach r $all_remotes {
-               set enable 0
-               if {![catch {set a $repo_config(remote.$r.url)}]} {
-                       if {![catch {set a $repo_config(remote.$r.fetch)}]} {
-                               set enable 1
-                       }
-               } else {
-                       catch {
-                               set fd [open [gitdir remotes $r] r]
-                               while {[gets $fd n] >= 0} {
-                                       if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
-                                               set enable 1
-                                               break
-                                       }
-                               }
-                               close $fd
-                       }
-               }
-
-               if {$enable} {
-                       if {![winfo exists $fetch_m]} {
-                               menu $prune_m
-                               $remote_m insert 0 cascade \
-                                       -label [mc "Prune from"] \
-                                       -menu $prune_m
-
-                               menu $fetch_m
-                               $remote_m insert 0 cascade \
-                                       -label [mc "Fetch from"] \
-                                       -menu $fetch_m
-                       }
-
-                       $fetch_m add command \
-                               -label $r \
-                               -command [list fetch_from $r]
-                       $prune_m add command \
-                               -label $r \
-                               -command [list prune_from $r]
-               }
-       }
-}
-
-proc populate_push_menu {} {
-       global all_remotes repo_config
-
-       set remote_m .mbar.remote
-       set push_m $remote_m.push
-
-       foreach r $all_remotes {
-               set enable 0
-               if {![catch {set a $repo_config(remote.$r.url)}]} {
-                       if {![catch {set a $repo_config(remote.$r.push)}]} {
-                               set enable 1
-                       }
-               } else {
-                       catch {
-                               set fd [open [gitdir remotes $r] r]
-                               while {[gets $fd n] >= 0} {
-                                       if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
-                                               set enable 1
-                                               break
-                                       }
-                               }
-                               close $fd
-                       }
-               }
-
-               if {$enable} {
-                       if {![winfo exists $push_m]} {
-                               menu $push_m
-                               $remote_m insert 0 cascade \
-                                       -label [mc "Push to"] \
-                                       -menu $push_m
-                       }
-
-                       $push_m add command \
-                               -label $r \
-                               -command [list push_to $r]
-               }
-       }
-}
diff --git a/lib/remote_branch_delete.tcl b/lib/remote_branch_delete.tcl
deleted file mode 100644 (file)
index c7b8148..0000000
+++ /dev/null
@@ -1,345 +0,0 @@
-# git-gui remote branch deleting support
-# Copyright (C) 2007 Shawn Pearce
-
-class remote_branch_delete {
-
-field w
-field head_m
-
-field urltype   {url}
-field remote    {}
-field url       {}
-
-field checktype  {head}
-field check_head {}
-
-field status    {}
-field idle_id   {}
-field full_list {}
-field head_list {}
-field active_ls {}
-field head_cache
-field full_cache
-field cached
-
-constructor dialog {} {
-       global all_remotes M1B
-
-       make_toplevel top w
-       wm title $top [append "[appname] ([reponame]): " [mc "Delete Remote Branch"]]
-       if {$top ne {.}} {
-               wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
-       }
-
-       label $w.header -text [mc "Delete Remote Branch"] -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       button $w.buttons.delete -text [mc Delete] \
-               -default active \
-               -command [cb _delete]
-       pack $w.buttons.delete -side right
-       button $w.buttons.cancel -text [mc "Cancel"] \
-               -command [list destroy $w]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       labelframe $w.dest -text [mc "From Repository"]
-       if {$all_remotes ne {}} {
-               radiobutton $w.dest.remote_r \
-                       -text [mc "Remote:"] \
-                       -value remote \
-                       -variable @urltype
-               eval tk_optionMenu $w.dest.remote_m @remote $all_remotes
-               grid $w.dest.remote_r $w.dest.remote_m -sticky w
-               if {[lsearch -sorted -exact $all_remotes origin] != -1} {
-                       set remote origin
-               } else {
-                       set remote [lindex $all_remotes 0]
-               }
-               set urltype remote
-               trace add variable @remote write [cb _write_remote]
-       } else {
-               set urltype url
-       }
-       radiobutton $w.dest.url_r \
-               -text [mc "Arbitrary URL:"] \
-               -value url \
-               -variable @urltype
-       entry $w.dest.url_t \
-               -borderwidth 1 \
-               -relief sunken \
-               -width 50 \
-               -textvariable @url \
-               -validate key \
-               -validatecommand {
-                       if {%d == 1 && [regexp {\s} %S]} {return 0}
-                       return 1
-               }
-       trace add variable @url write [cb _write_url]
-       grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5}
-       grid columnconfigure $w.dest 1 -weight 1
-       pack $w.dest -anchor nw -fill x -pady 5 -padx 5
-
-       labelframe $w.heads -text [mc "Branches"]
-       listbox $w.heads.l \
-               -height 10 \
-               -width 70 \
-               -listvariable @head_list \
-               -selectmode extended \
-               -yscrollcommand [list $w.heads.sby set]
-       scrollbar $w.heads.sby -command [list $w.heads.l yview]
-
-       frame $w.heads.footer
-       label $w.heads.footer.status \
-               -textvariable @status \
-               -anchor w \
-               -justify left
-       button $w.heads.footer.rescan \
-               -text [mc "Rescan"] \
-               -command [cb _rescan]
-       pack $w.heads.footer.status -side left -fill x
-       pack $w.heads.footer.rescan -side right
-
-       pack $w.heads.footer -side bottom -fill x
-       pack $w.heads.sby -side right -fill y
-       pack $w.heads.l -side left -fill both -expand 1
-       pack $w.heads -fill both -expand 1 -pady 5 -padx 5
-
-       labelframe $w.validate -text [mc "Delete Only If"]
-       radiobutton $w.validate.head_r \
-               -text [mc "Merged Into:"] \
-               -value head \
-               -variable @checktype
-       set head_m [tk_optionMenu $w.validate.head_m @check_head {}]
-       trace add variable @head_list write [cb _write_head_list]
-       trace add variable @check_head write [cb _write_check_head]
-       grid $w.validate.head_r $w.validate.head_m -sticky w
-       radiobutton $w.validate.always_r \
-               -text [mc "Always (Do not perform merge checks)"] \
-               -value always \
-               -variable @checktype
-       grid $w.validate.always_r -columnspan 2 -sticky w
-       grid columnconfigure $w.validate 1 -weight 1
-       pack $w.validate -anchor nw -fill x -pady 5 -padx 5
-
-       trace add variable @urltype write [cb _write_urltype]
-       _rescan $this
-
-       bind $w <Key-F5>     [cb _rescan]
-       bind $w <$M1B-Key-r> [cb _rescan]
-       bind $w <$M1B-Key-R> [cb _rescan]
-       bind $w <Key-Return> [cb _delete]
-       bind $w <Key-Escape> [list destroy $w]
-       return $w
-}
-
-method _delete {} {
-       switch $urltype {
-       remote {set uri $remote}
-       url    {set uri $url}
-       }
-
-       set cache $urltype:$uri
-       set crev {}
-       if {$checktype eq {head}} {
-               if {$check_head eq {}} {
-                       tk_messageBox \
-                               -icon error \
-                               -type ok \
-                               -title [wm title $w] \
-                               -parent $w \
-                               -message [mc "A branch is required for 'Merged Into'."]
-                       return
-               }
-               set crev $full_cache("$cache\nrefs/heads/$check_head")
-       }
-
-       set not_merged [list]
-       set need_fetch 0
-       set have_selection 0
-       set push_cmd [list git push]
-       lappend push_cmd -v
-       lappend push_cmd $uri
-
-       foreach i [$w.heads.l curselection] {
-               set ref [lindex $full_list $i]
-               if {$crev ne {}} {
-                       set obj $full_cache("$cache\n$ref")
-                       if {[catch {set m [git merge-base $obj $crev]}]} {
-                               set need_fetch 1
-                               set m {}
-                       }
-                       if {$obj ne $m} {
-                               lappend not_merged [lindex $head_list $i]
-                               continue
-                       }
-               }
-
-               lappend push_cmd :$ref
-               set have_selection 1
-       }
-
-       if {$not_merged ne {}} {
-               set msg [mc "The following branches are not completely merged into %s:
-
- - %s" $check_head [join $not_merged "\n - "]]
-
-               if {$need_fetch} {
-                       append msg "\n\n" [mc "One or more of the merge tests failed because you have not fetched the necessary commits.  Try fetching from %s first." $uri]
-               }
-
-               tk_messageBox \
-                       -icon info \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message $msg
-               if {!$have_selection} return
-       }
-
-       if {!$have_selection} {
-               tk_messageBox \
-                       -icon error \
-                       -type ok \
-                       -title [wm title $w] \
-                       -parent $w \
-                       -message [mc "Please select one or more branches to delete."]
-               return
-       }
-
-       if {[tk_messageBox \
-               -icon warning \
-               -type yesno \
-               -title [wm title $w] \
-               -parent $w \
-               -message [mc "Recovering deleted branches is difficult.
-
-Delete the selected branches?"]] ne yes} {
-               return
-       }
-
-       destroy $w
-
-       set cons [console::new \
-               "push $uri" \
-               [mc "Deleting branches from %s" $uri]]
-       console::exec $cons $push_cmd
-}
-
-method _rescan {{force 1}} {
-       switch $urltype {
-       remote {set uri $remote}
-       url    {set uri $url}
-       }
-
-       if {$force} {
-               unset -nocomplain cached($urltype:$uri)
-       }
-
-       if {$idle_id ne {}} {
-               after cancel $idle_id
-               set idle_id {}
-       }
-
-       _load $this $urltype:$uri $uri
-}
-
-method _write_remote     {args} { set urltype remote }
-method _write_url        {args} { set urltype url    }
-method _write_check_head {args} { set checktype head }
-
-method _write_head_list {args} {
-       $head_m delete 0 end
-       foreach abr $head_list {
-               $head_m insert end radiobutton \
-                       -label $abr \
-                       -value $abr \
-                       -variable @check_head
-       }
-       if {[lsearch -exact -sorted $head_list $check_head] < 0} {
-               set check_head {}
-       }
-}
-
-method _write_urltype {args} {
-       if {$urltype eq {url}} {
-               if {$idle_id ne {}} {
-                       after cancel $idle_id
-               }
-               _load $this none: {}
-               set idle_id [after 1000 [cb _rescan 0]]
-       } else {
-               _rescan $this 0
-       }
-}
-
-method _load {cache uri} {
-       if {$active_ls ne {}} {
-               catch {close $active_ls}
-       }
-
-       if {$uri eq {}} {
-               $w.heads.l conf -state disabled
-               set head_list [list]
-               set full_list [list]
-               set status [mc "No repository selected."]
-               return
-       }
-
-       if {[catch {set x $cached($cache)}]} {
-               set status [mc "Scanning %s..." $uri]
-               $w.heads.l conf -state disabled
-               set head_list [list]
-               set full_list [list]
-               set head_cache($cache) [list]
-               set full_cache($cache) [list]
-               set active_ls [git_read ls-remote $uri]
-               fconfigure $active_ls \
-                       -blocking 0 \
-                       -translation lf \
-                       -encoding utf-8
-               fileevent $active_ls readable [cb _read $cache $active_ls]
-       } else {
-               set status {}
-               set full_list $full_cache($cache)
-               set head_list $head_cache($cache)
-               $w.heads.l conf -state normal
-       }
-}
-
-method _read {cache fd} {
-       if {$fd ne $active_ls} {
-               catch {close $fd}
-               return
-       }
-
-       while {[gets $fd line] >= 0} {
-               if {[string match {*^{}} $line]} continue
-               if {[regexp {^([0-9a-f]{40})    (.*)$} $line _junk obj ref]} {
-                       if {[regsub ^refs/heads/ $ref {} abr]} {
-                               lappend head_list $abr
-                               lappend head_cache($cache) $abr
-                               lappend full_list $ref
-                               lappend full_cache($cache) $ref
-                               set full_cache("$cache\n$ref") $obj
-                       }
-               }
-       }
-
-       if {[eof $fd]} {
-               if {[catch {close $fd} err]} {
-                       set status $err
-                       set head_list [list]
-                       set full_list [list]
-               } else {
-                       set status {}
-                       set cached($cache) 1
-                       $w.heads.l conf -state normal
-               }
-       }
-} ifdeleted {
-       catch {close $fd}
-}
-
-}
diff --git a/lib/shortcut.tcl b/lib/shortcut.tcl
deleted file mode 100644 (file)
index 38c3151..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-# git-gui desktop icon creators
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc do_windows_shortcut {} {
-       set fn [tk_getSaveFile \
-               -parent . \
-               -title [append "[appname] ([reponame]): " [mc "Create Desktop Icon"]] \
-               -initialfile "Git [reponame].lnk"]
-       if {$fn != {}} {
-               if {[file extension $fn] ne {.lnk}} {
-                       set fn ${fn}.lnk
-               }
-               if {[catch {
-                               win32_create_lnk $fn [list \
-                                       [info nameofexecutable] \
-                                       [file normalize $::argv0] \
-                                       ] \
-                                       [file dirname [file normalize [gitdir]]]
-                       } err]} {
-                       error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"]
-               }
-       }
-}
-
-proc do_cygwin_shortcut {} {
-       global argv0
-
-       if {[catch {
-               set desktop [exec cygpath \
-                       --windows \
-                       --absolute \
-                       --long-name \
-                       --desktop]
-               }]} {
-                       set desktop .
-       }
-       set fn [tk_getSaveFile \
-               -parent . \
-               -title [append "[appname] ([reponame]): " [mc "Create Desktop Icon"]] \
-               -initialdir $desktop \
-               -initialfile "Git [reponame].lnk"]
-       if {$fn != {}} {
-               if {[file extension $fn] ne {.lnk}} {
-                       set fn ${fn}.lnk
-               }
-               if {[catch {
-                               set sh [exec cygpath \
-                                       --windows \
-                                       --absolute \
-                                       /bin/sh.exe]
-                               set me [exec cygpath \
-                                       --unix \
-                                       --absolute \
-                                       $argv0]
-                               win32_create_lnk $fn [list \
-                                       $sh -c \
-                                       "CHERE_INVOKING=1 source /etc/profile;[sq $me]" \
-                                       ] \
-                                       [file dirname [file normalize [gitdir]]]
-                       } err]} {
-                       error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"]
-               }
-       }
-}
-
-proc do_macosx_app {} {
-       global argv0 env
-
-       set fn [tk_getSaveFile \
-               -parent . \
-               -title [append "[appname] ([reponame]): " [mc "Create Desktop Icon"]] \
-               -initialdir [file join $env(HOME) Desktop] \
-               -initialfile "Git [reponame].app"]
-       if {$fn != {}} {
-               if {[file extension $fn] ne {.app}} {
-                       set fn ${fn}.app
-               }
-               if {[catch {
-                               set Contents [file join $fn Contents]
-                               set MacOS [file join $Contents MacOS]
-                               set exe [file join $MacOS git-gui]
-
-                               file mkdir $MacOS
-
-                               set fd [open [file join $Contents Info.plist] w]
-                               puts $fd {<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-       <key>CFBundleDevelopmentRegion</key>
-       <string>English</string>
-       <key>CFBundleExecutable</key>
-       <string>git-gui</string>
-       <key>CFBundleIdentifier</key>
-       <string>org.spearce.git-gui</string>
-       <key>CFBundleInfoDictionaryVersion</key>
-       <string>6.0</string>
-       <key>CFBundlePackageType</key>
-       <string>APPL</string>
-       <key>CFBundleSignature</key>
-       <string>????</string>
-       <key>CFBundleVersion</key>
-       <string>1.0</string>
-       <key>NSPrincipalClass</key>
-       <string>NSApplication</string>
-</dict>
-</plist>}
-                               close $fd
-
-                               set fd [open $exe w]
-                               puts $fd "#!/bin/sh"
-                               foreach name [lsort [array names env]] {
-                                       set value $env($name)
-                                       switch -- $name {
-                                       GIT_DIR { set value [file normalize [gitdir]] }
-                                       }
-
-                                       switch -glob -- $name {
-                                       SSH_* -
-                                       GIT_* {
-                                               puts $fd "if test \"z\$$name\" = z; then"
-                                               puts $fd "  export $name=[sq $value]"
-                                               puts $fd "fi &&"
-                                       }
-                                       }
-                               }
-                               puts $fd "export PATH=[sq [file dirname $::_git]]:\$PATH &&"
-                               puts $fd "cd [sq [file normalize [pwd]]] &&"
-                               puts $fd "exec \\"
-                               puts $fd " [sq [info nameofexecutable]] \\"
-                               puts $fd " [sq [file normalize $argv0]]"
-                               close $fd
-
-                               file attributes $exe -permissions u+x,g+x,o+x
-                       } err]} {
-                       error_popup [strcat [mc "Cannot write icon:"] "\n\n$err"]
-               }
-       }
-}
diff --git a/lib/status_bar.tcl b/lib/status_bar.tcl
deleted file mode 100644 (file)
index 51d4177..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-# git-gui status bar mega-widget
-# Copyright (C) 2007 Shawn Pearce
-
-class status_bar {
-
-field w         ; # our own window path
-field w_l       ; # text widget we draw messages into
-field w_c       ; # canvas we draw a progress bar into
-field c_pack    ; # script to pack the canvas with
-field status  {}; # single line of text we show
-field prefix  {}; # text we format into status
-field units   {}; # unit of progress
-field meter   {}; # current core git progress meter (if active)
-
-constructor new {path} {
-       set w $path
-       set w_l $w.l
-       set w_c $w.c
-
-       frame $w \
-               -borderwidth 1 \
-               -relief sunken
-       label $w_l \
-               -textvariable @status \
-               -anchor w \
-               -justify left
-       pack $w_l -side left
-       set c_pack [cb _oneline_pack]
-
-       bind $w <Destroy> [cb _delete %W]
-       return $this
-}
-
-method _oneline_pack {} {
-       $w_c conf -width 100
-       pack $w_c -side right
-}
-
-constructor two_line {path} {
-       set w $path
-       set w_l $w.l
-       set w_c $w.c
-
-       frame $w
-       label $w_l \
-               -textvariable @status \
-               -anchor w \
-               -justify left
-       pack $w_l -anchor w -fill x
-       set c_pack [list pack $w_c -fill x]
-
-       bind $w <Destroy> [cb _delete %W]
-       return $this
-}
-
-method start {msg uds} {
-       if {[winfo exists $w_c]} {
-               $w_c coords bar 0 0 0 20
-       } else {
-               canvas $w_c \
-                       -height [expr {int([winfo reqheight $w_l] * 0.6)}] \
-                       -borderwidth 1 \
-                       -relief groove \
-                       -highlightt 0
-               $w_c create rectangle 0 0 0 20 -tags bar -fill navy
-               eval $c_pack
-       }
-
-       set status $msg
-       set prefix $msg
-       set units  $uds
-       set meter  {}
-}
-
-method update {have total} {
-       set pdone 0
-       if {$total > 0} {
-               set pdone [expr {100 * $have / $total}]
-               set cdone [expr {[winfo width $w_c] * $have / $total}]
-       }
-
-       set prec [string length [format %i $total]]
-       set status [mc "%s ... %*i of %*i %s (%3i%%)" \
-               $prefix \
-               $prec $have \
-               $prec $total \
-               $units $pdone]
-       $w_c coords bar 0 0 $cdone 20
-}
-
-method update_meter {buf} {
-       append meter $buf
-       set r [string last "\r" $meter]
-       if {$r == -1} {
-               return
-       }
-
-       set prior [string range $meter 0 $r]
-       set meter [string range $meter [expr {$r + 1}] end]
-       set p "\\((\\d+)/(\\d+)\\)"
-       if {[regexp ":\\s*\\d+% $p\(?:, done.\\s*\n|\\s*\r)\$" $prior _j a b]} {
-               update $this $a $b
-       } elseif {[regexp "$p\\s+done\r\$" $prior _j a b]} {
-               update $this $a $b
-       }
-}
-
-method stop {{msg {}}} {
-       destroy $w_c
-       if {$msg ne {}} {
-               set status $msg
-       }
-}
-
-method show {msg {test {}}} {
-       if {$test eq {} || $status eq $test} {
-               set status $msg
-       }
-}
-
-method _delete {current} {
-       if {$current eq $w} {
-               delete_this
-       }
-}
-
-}
diff --git a/lib/transport.tcl b/lib/transport.tcl
deleted file mode 100644 (file)
index 8e6a9d0..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-# git-gui transport (fetch/push) support
-# Copyright (C) 2006, 2007 Shawn Pearce
-
-proc fetch_from {remote} {
-       set w [console::new \
-               [mc "fetch %s" $remote] \
-               [mc "Fetching new changes from %s" $remote]]
-       set cmds [list]
-       lappend cmds [list exec git fetch $remote]
-       if {[is_config_true gui.pruneduringfetch]} {
-               lappend cmds [list exec git remote prune $remote]
-       }
-       console::chain $w $cmds
-}
-
-proc prune_from {remote} {
-       set w [console::new \
-               [mc "remote prune %s" $remote] \
-               [mc "Pruning tracking branches deleted from %s" $remote]]
-       console::exec $w [list git remote prune $remote]
-}
-
-proc push_to {remote} {
-       set w [console::new \
-               [mc "push %s" $remote] \
-               [mc "Pushing changes to %s" $remote]]
-       set cmd [list git push]
-       lappend cmd -v
-       lappend cmd $remote
-       console::exec $w $cmd
-}
-
-proc start_push_anywhere_action {w} {
-       global push_urltype push_remote push_url push_thin push_tags
-       global push_force
-
-       set r_url {}
-       switch -- $push_urltype {
-       remote {set r_url $push_remote}
-       url {set r_url $push_url}
-       }
-       if {$r_url eq {}} return
-
-       set cmd [list git push]
-       lappend cmd -v
-       if {$push_thin} {
-               lappend cmd --thin
-       }
-       if {$push_force} {
-               lappend cmd --force
-       }
-       if {$push_tags} {
-               lappend cmd --tags
-       }
-       lappend cmd $r_url
-       set cnt 0
-       foreach i [$w.source.l curselection] {
-               set b [$w.source.l get $i]
-               lappend cmd "refs/heads/$b:refs/heads/$b"
-               incr cnt
-       }
-       if {$cnt == 0} {
-               return
-       } elseif {$cnt == 1} {
-               set unit branch
-       } else {
-               set unit branches
-       }
-
-       set cons [console::new \
-               [mc "push %s" $r_url] \
-               [mc "Pushing %s %s to %s" $cnt $unit $r_url]]
-       console::exec $cons $cmd
-       destroy $w
-}
-
-trace add variable push_remote write \
-       [list radio_selector push_urltype remote]
-
-proc do_push_anywhere {} {
-       global all_remotes current_branch
-       global push_urltype push_remote push_url push_thin push_tags
-       global push_force
-
-       set w .push_setup
-       toplevel $w
-       wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
-
-       label $w.header -text [mc "Push Branches"] -font font_uibold
-       pack $w.header -side top -fill x
-
-       frame $w.buttons
-       button $w.buttons.create -text [mc Push] \
-               -default active \
-               -command [list start_push_anywhere_action $w]
-       pack $w.buttons.create -side right
-       button $w.buttons.cancel -text [mc "Cancel"] \
-               -default normal \
-               -command [list destroy $w]
-       pack $w.buttons.cancel -side right -padx 5
-       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
-
-       labelframe $w.source -text [mc "Source Branches"]
-       listbox $w.source.l \
-               -height 10 \
-               -width 70 \
-               -selectmode extended \
-               -yscrollcommand [list $w.source.sby set]
-       foreach h [load_all_heads] {
-               $w.source.l insert end $h
-               if {$h eq $current_branch} {
-                       $w.source.l select set end
-               }
-       }
-       scrollbar $w.source.sby -command [list $w.source.l yview]
-       pack $w.source.sby -side right -fill y
-       pack $w.source.l -side left -fill both -expand 1
-       pack $w.source -fill both -expand 1 -pady 5 -padx 5
-
-       labelframe $w.dest -text [mc "Destination Repository"]
-       if {$all_remotes ne {}} {
-               radiobutton $w.dest.remote_r \
-                       -text [mc "Remote:"] \
-                       -value remote \
-                       -variable push_urltype
-               eval tk_optionMenu $w.dest.remote_m push_remote $all_remotes
-               grid $w.dest.remote_r $w.dest.remote_m -sticky w
-               if {[lsearch -sorted -exact $all_remotes origin] != -1} {
-                       set push_remote origin
-               } else {
-                       set push_remote [lindex $all_remotes 0]
-               }
-               set push_urltype remote
-       } else {
-               set push_urltype url
-       }
-       radiobutton $w.dest.url_r \
-               -text [mc "Arbitrary URL:"] \
-               -value url \
-               -variable push_urltype
-       entry $w.dest.url_t \
-               -borderwidth 1 \
-               -relief sunken \
-               -width 50 \
-               -textvariable push_url \
-               -validate key \
-               -validatecommand {
-                       if {%d == 1 && [regexp {\s} %S]} {return 0}
-                       if {%d == 1 && [string length %S] > 0} {
-                               set push_urltype url
-                       }
-                       return 1
-               }
-       grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5}
-       grid columnconfigure $w.dest 1 -weight 1
-       pack $w.dest -anchor nw -fill x -pady 5 -padx 5
-
-       labelframe $w.options -text [mc "Transfer Options"]
-       checkbutton $w.options.force \
-               -text [mc "Force overwrite existing branch (may discard changes)"] \
-               -variable push_force
-       grid $w.options.force -columnspan 2 -sticky w
-       checkbutton $w.options.thin \
-               -text [mc "Use thin pack (for slow network connections)"] \
-               -variable push_thin
-       grid $w.options.thin -columnspan 2 -sticky w
-       checkbutton $w.options.tags \
-               -text [mc "Include tags"] \
-               -variable push_tags
-       grid $w.options.tags -columnspan 2 -sticky w
-       grid columnconfigure $w.options 1 -weight 1
-       pack $w.options -anchor nw -fill x -pady 5 -padx 5
-
-       set push_url {}
-       set push_force 0
-       set push_thin 0
-       set push_tags 0
-
-       bind $w <Visibility> "grab $w; focus $w.buttons.create"
-       bind $w <Key-Escape> "destroy $w"
-       bind $w <Key-Return> [list start_push_anywhere_action $w]
-       wm title $w [append "[appname] ([reponame]): " [mc "Push"]]
-       tkwait window $w
-}
diff --git a/lib/win32.tcl b/lib/win32.tcl
deleted file mode 100644 (file)
index d7f93d0..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-# git-gui Misc. native Windows 32 support
-# Copyright (C) 2007 Shawn Pearce
-
-proc win32_read_lnk {lnk_path} {
-       return [exec cscript.exe \
-               /E:jscript \
-               /nologo \
-               [file join $::oguilib win32_shortcut.js] \
-               $lnk_path]
-}
-
-proc win32_create_lnk {lnk_path lnk_exec lnk_dir} {
-       global oguilib
-
-       set lnk_args [lrange $lnk_exec 1 end]
-       set lnk_exec [lindex $lnk_exec 0]
-
-       eval [list exec wscript.exe \
-               /E:jscript \
-               /nologo \
-               [file join $oguilib win32_shortcut.js] \
-               $lnk_path \
-               [file join $oguilib git-gui.ico] \
-               $lnk_dir \
-               $lnk_exec] $lnk_args
-}
diff --git a/lib/win32_shortcut.js b/lib/win32_shortcut.js
deleted file mode 100644 (file)
index 117923f..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-// git-gui Windows shortcut support
-// Copyright (C) 2007 Shawn Pearce
-
-var WshShell = WScript.CreateObject("WScript.Shell");
-var argv = WScript.Arguments;
-var argi = 0;
-var lnk_path = argv.item(argi++);
-var ico_path = argi < argv.length ? argv.item(argi++) : undefined;
-var dir_path = argi < argv.length ? argv.item(argi++) : undefined;
-var lnk_exec = argi < argv.length ? argv.item(argi++) : undefined;
-var lnk_args = '';
-while (argi < argv.length) {
-       var s = argv.item(argi++);
-       if (lnk_args != '')
-               lnk_args += ' ';
-       if (s.indexOf(' ') >= 0) {
-               lnk_args += '"';
-               lnk_args += s;
-               lnk_args += '"';
-       } else {
-               lnk_args += s;
-       }
-}
-
-var lnk = WshShell.CreateShortcut(lnk_path);
-if (argv.length == 1) {
-       WScript.echo(lnk.TargetPath);
-} else {
-       lnk.TargetPath = lnk_exec;
-       lnk.Arguments = lnk_args;
-       lnk.IconLocation = ico_path + ", 0";
-       lnk.WorkingDirectory = dir_path;
-       lnk.Save();
-}
diff --git a/list-objects.c b/list-objects.c
new file mode 100644 (file)
index 0000000..4ef58e7
--- /dev/null
@@ -0,0 +1,180 @@
+#include "cache.h"
+#include "tag.h"
+#include "commit.h"
+#include "tree.h"
+#include "blob.h"
+#include "diff.h"
+#include "tree-walk.h"
+#include "revision.h"
+#include "list-objects.h"
+
+static void process_blob(struct rev_info *revs,
+                        struct blob *blob,
+                        struct object_array *p,
+                        struct name_path *path,
+                        const char *name)
+{
+       struct object *obj = &blob->object;
+
+       if (!revs->blob_objects)
+               return;
+       if (obj->flags & (UNINTERESTING | SEEN))
+               return;
+       obj->flags |= SEEN;
+       name = xstrdup(name);
+       add_object(obj, p, path, name);
+}
+
+/*
+ * Processing a gitlink entry currently does nothing, since
+ * we do not recurse into the subproject.
+ *
+ * We *could* eventually add a flag that actually does that,
+ * which would involve:
+ *  - is the subproject actually checked out?
+ *  - if so, see if the subproject has already been added
+ *    to the alternates list, and add it if not.
+ *  - process the commit (or tag) the gitlink points to
+ *    recursively.
+ *
+ * However, it's unclear whether there is really ever any
+ * reason to see superprojects and subprojects as such a
+ * "unified" object pool (potentially resulting in a totally
+ * humongous pack - avoiding which was the whole point of
+ * having gitlinks in the first place!).
+ *
+ * So for now, there is just a note that we *could* follow
+ * the link, and how to do it. Whether it necessarily makes
+ * any sense what-so-ever to ever do that is another issue.
+ */
+static void process_gitlink(struct rev_info *revs,
+                           const unsigned char *sha1,
+                           struct object_array *p,
+                           struct name_path *path,
+                           const char *name)
+{
+       /* Nothing to do */
+}
+
+static void process_tree(struct rev_info *revs,
+                        struct tree *tree,
+                        struct object_array *p,
+                        struct name_path *path,
+                        const char *name)
+{
+       struct object *obj = &tree->object;
+       struct tree_desc desc;
+       struct name_entry entry;
+       struct name_path me;
+
+       if (!revs->tree_objects)
+               return;
+       if (obj->flags & (UNINTERESTING | SEEN))
+               return;
+       if (parse_tree(tree) < 0)
+               die("bad tree object %s", sha1_to_hex(obj->sha1));
+       obj->flags |= SEEN;
+       name = xstrdup(name);
+       add_object(obj, p, path, name);
+       me.up = path;
+       me.elem = name;
+       me.elem_len = strlen(name);
+
+       init_tree_desc(&desc, tree->buffer, tree->size);
+
+       while (tree_entry(&desc, &entry)) {
+               if (S_ISDIR(entry.mode))
+                       process_tree(revs,
+                                    lookup_tree(entry.sha1),
+                                    p, &me, entry.path);
+               else if (S_ISGITLINK(entry.mode))
+                       process_gitlink(revs, entry.sha1,
+                                       p, &me, entry.path);
+               else
+                       process_blob(revs,
+                                    lookup_blob(entry.sha1),
+                                    p, &me, entry.path);
+       }
+       free(tree->buffer);
+       tree->buffer = NULL;
+}
+
+static void mark_edge_parents_uninteresting(struct commit *commit,
+                                           struct rev_info *revs,
+                                           show_edge_fn show_edge)
+{
+       struct commit_list *parents;
+
+       for (parents = commit->parents; parents; parents = parents->next) {
+               struct commit *parent = parents->item;
+               if (!(parent->object.flags & UNINTERESTING))
+                       continue;
+               mark_tree_uninteresting(parent->tree);
+               if (revs->edge_hint && !(parent->object.flags & SHOWN)) {
+                       parent->object.flags |= SHOWN;
+                       show_edge(parent);
+               }
+       }
+}
+
+void mark_edges_uninteresting(struct commit_list *list,
+                             struct rev_info *revs,
+                             show_edge_fn show_edge)
+{
+       for ( ; list; list = list->next) {
+               struct commit *commit = list->item;
+
+               if (commit->object.flags & UNINTERESTING) {
+                       mark_tree_uninteresting(commit->tree);
+                       continue;
+               }
+               mark_edge_parents_uninteresting(commit, revs, show_edge);
+       }
+}
+
+void traverse_commit_list(struct rev_info *revs,
+                         void (*show_commit)(struct commit *),
+                         void (*show_object)(struct object_array_entry *))
+{
+       int i;
+       struct commit *commit;
+       struct object_array objects = { 0, 0, NULL };
+
+       while ((commit = get_revision(revs)) != NULL) {
+               process_tree(revs, commit->tree, &objects, NULL, "");
+               show_commit(commit);
+       }
+       for (i = 0; i < revs->pending.nr; i++) {
+               struct object_array_entry *pending = revs->pending.objects + i;
+               struct object *obj = pending->item;
+               const char *name = pending->name;
+               if (obj->flags & (UNINTERESTING | SEEN))
+                       continue;
+               if (obj->type == OBJ_TAG) {
+                       obj->flags |= SEEN;
+                       add_object_array(obj, name, &objects);
+                       continue;
+               }
+               if (obj->type == OBJ_TREE) {
+                       process_tree(revs, (struct tree *)obj, &objects,
+                                    NULL, name);
+                       continue;
+               }
+               if (obj->type == OBJ_BLOB) {
+                       process_blob(revs, (struct blob *)obj, &objects,
+                                    NULL, name);
+                       continue;
+               }
+               die("unknown pending object %s (%s)",
+                   sha1_to_hex(obj->sha1), name);
+       }
+       for (i = 0; i < objects.nr; i++)
+               show_object(&objects.objects[i]);
+       free(objects.objects);
+       if (revs->pending.nr) {
+               free(revs->pending.objects);
+               revs->pending.nr = 0;
+               revs->pending.alloc = 0;
+               revs->pending.objects = NULL;
+       }
+}
diff --git a/list-objects.h b/list-objects.h
new file mode 100644 (file)
index 0000000..0f41391
--- /dev/null
@@ -0,0 +1,12 @@
+#ifndef LIST_OBJECTS_H
+#define LIST_OBJECTS_H
+
+typedef void (*show_commit_fn)(struct commit *);
+typedef void (*show_object_fn)(struct object_array_entry *);
+typedef void (*show_edge_fn)(struct commit *);
+
+void traverse_commit_list(struct rev_info *revs, show_commit_fn, show_object_fn);
+
+void mark_edges_uninteresting(struct commit_list *, struct rev_info *, show_edge_fn);
+
+#endif
diff --git a/lockfile.c b/lockfile.c
new file mode 100644 (file)
index 0000000..f45d3ed
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2005, Junio C Hamano
+ */
+#include "cache.h"
+
+static struct lock_file *lock_file_list;
+static const char *alternate_index_output;
+
+static void remove_lock_file(void)
+{
+       pid_t me = getpid();
+
+       while (lock_file_list) {
+               if (lock_file_list->owner == me &&
+                   lock_file_list->filename[0]) {
+                       close(lock_file_list->fd);
+                       unlink(lock_file_list->filename);
+               }
+               lock_file_list = lock_file_list->next;
+       }
+}
+
+static void remove_lock_file_on_signal(int signo)
+{
+       remove_lock_file();
+       signal(SIGINT, SIG_DFL);
+       raise(signo);
+}
+
+/*
+ * p = absolute or relative path name
+ *
+ * Return a pointer into p showing the beginning of the last path name
+ * element.  If p is empty or the root directory ("/"), just return p.
+ */
+static char *last_path_elm(char *p)
+{
+       /* r starts pointing to null at the end of the string */
+       char *r = strchr(p, '\0');
+
+       if (r == p)
+               return p; /* just return empty string */
+
+       r--; /* back up to last non-null character */
+
+       /* back up past trailing slashes, if any */
+       while (r > p && *r == '/')
+               r--;
+
+       /*
+        * then go backwards until I hit a slash, or the beginning of
+        * the string
+        */
+       while (r > p && *(r-1) != '/')
+               r--;
+       return r;
+}
+
+
+/* We allow "recursive" symbolic links. Only within reason, though */
+#define MAXDEPTH 5
+
+/*
+ * p = path that may be a symlink
+ * s = full size of p
+ *
+ * If p is a symlink, attempt to overwrite p with a path to the real
+ * file or directory (which may or may not exist), following a chain of
+ * symlinks if necessary.  Otherwise, leave p unmodified.
+ *
+ * This is a best-effort routine.  If an error occurs, p will either be
+ * left unmodified or will name a different symlink in a symlink chain
+ * that started with p's initial contents.
+ *
+ * Always returns p.
+ */
+
+static char *resolve_symlink(char *p, size_t s)
+{
+       int depth = MAXDEPTH;
+
+       while (depth--) {
+               char link[PATH_MAX];
+               int link_len = readlink(p, link, sizeof(link));
+               if (link_len < 0) {
+                       /* not a symlink anymore */
+                       return p;
+               }
+               else if (link_len < sizeof(link))
+                       /* readlink() never null-terminates */
+                       link[link_len] = '\0';
+               else {
+                       warning("%s: symlink too long", p);
+                       return p;
+               }
+
+               if (is_absolute_path(link)) {
+                       /* absolute path simply replaces p */
+                       if (link_len < s)
+                               strcpy(p, link);
+                       else {
+                               warning("%s: symlink too long", p);
+                               return p;
+                       }
+               } else {
+                       /*
+                        * link is a relative path, so I must replace the
+                        * last element of p with it.
+                        */
+                       char *r = (char*)last_path_elm(p);
+                       if (r - p + link_len < s)
+                               strcpy(r, link);
+                       else {
+                               warning("%s: symlink too long", p);
+                               return p;
+                       }
+               }
+       }
+       return p;
+}
+
+
+static int lock_file(struct lock_file *lk, const char *path)
+{
+       if (strlen(path) >= sizeof(lk->filename)) return -1;
+       strcpy(lk->filename, path);
+       /*
+        * subtract 5 from size to make sure there's room for adding
+        * ".lock" for the lock file name
+        */
+       resolve_symlink(lk->filename, sizeof(lk->filename)-5);
+       strcat(lk->filename, ".lock");
+       lk->fd = open(lk->filename, O_RDWR | O_CREAT | O_EXCL, 0666);
+       if (0 <= lk->fd) {
+               if (!lock_file_list) {
+                       signal(SIGINT, remove_lock_file_on_signal);
+                       atexit(remove_lock_file);
+               }
+               lk->owner = getpid();
+               if (!lk->on_list) {
+                       lk->next = lock_file_list;
+                       lock_file_list = lk;
+                       lk->on_list = 1;
+               }
+               if (adjust_shared_perm(lk->filename))
+                       return error("cannot fix permission bits on %s",
+                                    lk->filename);
+       }
+       else
+               lk->filename[0] = 0;
+       return lk->fd;
+}
+
+int hold_lock_file_for_update(struct lock_file *lk, const char *path, int die_on_error)
+{
+       int fd = lock_file(lk, path);
+       if (fd < 0 && die_on_error)
+               die("unable to create '%s.lock': %s", path, strerror(errno));
+       return fd;
+}
+
+int commit_lock_file(struct lock_file *lk)
+{
+       char result_file[PATH_MAX];
+       int i;
+       close(lk->fd);
+       strcpy(result_file, lk->filename);
+       i = strlen(result_file) - 5; /* .lock */
+       result_file[i] = 0;
+       i = rename(lk->filename, result_file);
+       lk->filename[0] = 0;
+       return i;
+}
+
+int hold_locked_index(struct lock_file *lk, int die_on_error)
+{
+       return hold_lock_file_for_update(lk, get_index_file(), die_on_error);
+}
+
+void set_alternate_index_output(const char *name)
+{
+       alternate_index_output = name;
+}
+
+int commit_locked_index(struct lock_file *lk)
+{
+       if (alternate_index_output) {
+               int result = rename(lk->filename, alternate_index_output);
+               lk->filename[0] = 0;
+               return result;
+       }
+       else
+               return commit_lock_file(lk);
+}
+
+void rollback_lock_file(struct lock_file *lk)
+{
+       if (lk->filename[0]) {
+               close(lk->fd);
+               unlink(lk->filename);
+       }
+       lk->filename[0] = 0;
+}
diff --git a/log-tree.c b/log-tree.c
new file mode 100644 (file)
index 0000000..1f3fcf1
--- /dev/null
@@ -0,0 +1,411 @@
+#include "cache.h"
+#include "diff.h"
+#include "commit.h"
+#include "log-tree.h"
+#include "reflog-walk.h"
+
+struct decoration name_decoration = { "object names" };
+
+static void show_parents(struct commit *commit, int abbrev)
+{
+       struct commit_list *p;
+       for (p = commit->parents; p ; p = p->next) {
+               struct commit *parent = p->item;
+               printf(" %s", diff_unique_abbrev(parent->object.sha1, abbrev));
+       }
+}
+
+void show_decorations(struct commit *commit)
+{
+       const char *prefix;
+       struct name_decoration *decoration;
+
+       decoration = lookup_decoration(&name_decoration, &commit->object);
+       if (!decoration)
+               return;
+       prefix = " (";
+       while (decoration) {
+               printf("%s%s", prefix, decoration->name);
+               prefix = ", ";
+               decoration = decoration->next;
+       }
+       putchar(')');
+}
+
+/*
+ * Search for "^[-A-Za-z]+: [^@]+@" pattern. It usually matches
+ * Signed-off-by: and Acked-by: lines.
+ */
+static int detect_any_signoff(char *letter, int size)
+{
+       char ch, *cp;
+       int seen_colon = 0;
+       int seen_at = 0;
+       int seen_name = 0;
+       int seen_head = 0;
+
+       cp = letter + size;
+       while (letter <= --cp && (ch = *cp) == '\n')
+               continue;
+
+       while (letter <= cp) {
+               ch = *cp--;
+               if (ch == '\n')
+                       break;
+
+               if (!seen_at) {
+                       if (ch == '@')
+                               seen_at = 1;
+                       continue;
+               }
+               if (!seen_colon) {
+                       if (ch == '@')
+                               return 0;
+                       else if (ch == ':')
+                               seen_colon = 1;
+                       else
+                               seen_name = 1;
+                       continue;
+               }
+               if (('A' <= ch && ch <= 'Z') ||
+                   ('a' <= ch && ch <= 'z') ||
+                   ch == '-') {
+                       seen_head = 1;
+                       continue;
+               }
+               /* no empty last line doesn't match */
+               return 0;
+       }
+       return seen_head && seen_name;
+}
+
+static void append_signoff(struct strbuf *sb, const char *signoff)
+{
+       static const char signed_off_by[] = "Signed-off-by: ";
+       size_t signoff_len = strlen(signoff);
+       int has_signoff = 0;
+       char *cp;
+
+       cp = sb->buf;
+
+       /* First see if we already have the sign-off by the signer */
+       while ((cp = strstr(cp, signed_off_by))) {
+
+               has_signoff = 1;
+
+               cp += strlen(signed_off_by);
+               if (cp + signoff_len >= sb->buf + sb->len)
+                       break;
+               if (strncmp(cp, signoff, signoff_len))
+                       continue;
+               if (!isspace(cp[signoff_len]))
+                       continue;
+               /* we already have him */
+               return;
+       }
+
+       if (!has_signoff)
+               has_signoff = detect_any_signoff(sb->buf, sb->len);
+
+       if (!has_signoff)
+               strbuf_addch(sb, '\n');
+
+       strbuf_addstr(sb, signed_off_by);
+       strbuf_add(sb, signoff, signoff_len);
+       strbuf_addch(sb, '\n');
+}
+
+static unsigned int digits_in_number(unsigned int number)
+{
+       unsigned int i = 10, result = 1;
+       while (i <= number) {
+               i *= 10;
+               result++;
+       }
+       return result;
+}
+
+static int has_non_ascii(const char *s)
+{
+       int ch;
+       if (!s)
+               return 0;
+       while ((ch = *s++) != '\0') {
+               if (non_ascii(ch))
+                       return 1;
+       }
+       return 0;
+}
+
+void show_log(struct rev_info *opt, const char *sep)
+{
+       struct strbuf msgbuf;
+       struct log_info *log = opt->loginfo;
+       struct commit *commit = log->commit, *parent = log->parent;
+       int abbrev = opt->diffopt.abbrev;
+       int abbrev_commit = opt->abbrev_commit ? opt->abbrev : 40;
+       const char *extra;
+       const char *subject = NULL, *extra_headers = opt->extra_headers;
+
+       opt->loginfo = NULL;
+       if (!opt->verbose_header) {
+               if (opt->left_right) {
+                       if (commit->object.flags & BOUNDARY)
+                               putchar('-');
+                       else if (commit->object.flags & SYMMETRIC_LEFT)
+                               putchar('<');
+                       else
+                               putchar('>');
+               }
+               fputs(diff_unique_abbrev(commit->object.sha1, abbrev_commit), stdout);
+               if (opt->parents)
+                       show_parents(commit, abbrev_commit);
+               show_decorations(commit);
+               putchar(opt->diffopt.line_termination);
+               return;
+       }
+
+       /*
+        * The "oneline" format has several special cases:
+        *  - The pretty-printed commit lacks a newline at the end
+        *    of the buffer, but we do want to make sure that we
+        *    have a newline there. If the separator isn't already
+        *    a newline, add an extra one.
+        *  - unlike other log messages, the one-line format does
+        *    not have an empty line between entries.
+        */
+       extra = "";
+       if (*sep != '\n' && opt->commit_format == CMIT_FMT_ONELINE)
+               extra = "\n";
+       if (opt->shown_one && opt->commit_format != CMIT_FMT_ONELINE)
+               putchar(opt->diffopt.line_termination);
+       opt->shown_one = 1;
+
+       /*
+        * Print header line of header..
+        */
+
+       if (opt->commit_format == CMIT_FMT_EMAIL) {
+               char *sha1 = sha1_to_hex(commit->object.sha1);
+               if (opt->total > 0) {
+                       static char buffer[64];
+                       snprintf(buffer, sizeof(buffer),
+                                       "Subject: [%s %0*d/%d] ",
+                                       opt->subject_prefix,
+                                       digits_in_number(opt->total),
+                                       opt->nr, opt->total);
+                       subject = buffer;
+               } else if (opt->total == 0 && opt->subject_prefix && *opt->subject_prefix) {
+                       static char buffer[256];
+                       snprintf(buffer, sizeof(buffer),
+                                       "Subject: [%s] ",
+                                       opt->subject_prefix);
+                       subject = buffer;
+               } else {
+                       subject = "Subject: ";
+               }
+
+               printf("From %s Mon Sep 17 00:00:00 2001\n", sha1);
+               if (opt->message_id)
+                       printf("Message-Id: <%s>\n", opt->message_id);
+               if (opt->ref_message_id)
+                       printf("In-Reply-To: <%s>\nReferences: <%s>\n",
+                              opt->ref_message_id, opt->ref_message_id);
+               if (opt->mime_boundary) {
+                       static char subject_buffer[1024];
+                       static char buffer[1024];
+                       snprintf(subject_buffer, sizeof(subject_buffer) - 1,
+                                "%s"
+                                "MIME-Version: 1.0\n"
+                                "Content-Type: multipart/mixed;"
+                                " boundary=\"%s%s\"\n"
+                                "\n"
+                                "This is a multi-part message in MIME "
+                                "format.\n"
+                                "--%s%s\n"
+                                "Content-Type: text/plain; "
+                                "charset=UTF-8; format=fixed\n"
+                                "Content-Transfer-Encoding: 8bit\n\n",
+                                extra_headers ? extra_headers : "",
+                                mime_boundary_leader, opt->mime_boundary,
+                                mime_boundary_leader, opt->mime_boundary);
+                       extra_headers = subject_buffer;
+
+                       snprintf(buffer, sizeof(buffer) - 1,
+                                "--%s%s\n"
+                                "Content-Type: text/x-patch;"
+                                " name=\"%s.diff\"\n"
+                                "Content-Transfer-Encoding: 8bit\n"
+                                "Content-Disposition: %s;"
+                                " filename=\"%s.diff\"\n\n",
+                                mime_boundary_leader, opt->mime_boundary,
+                                sha1,
+                                opt->no_inline ? "attachment" : "inline",
+                                sha1);
+                       opt->diffopt.stat_sep = buffer;
+               }
+       } else if (opt->commit_format != CMIT_FMT_USERFORMAT) {
+               fputs(diff_get_color_opt(&opt->diffopt, DIFF_COMMIT), stdout);
+               if (opt->commit_format != CMIT_FMT_ONELINE)
+                       fputs("commit ", stdout);
+               if (commit->object.flags & BOUNDARY)
+                       putchar('-');
+               else if (opt->left_right) {
+                       if (commit->object.flags & SYMMETRIC_LEFT)
+                               putchar('<');
+                       else
+                               putchar('>');
+               }
+               fputs(diff_unique_abbrev(commit->object.sha1, abbrev_commit),
+                     stdout);
+               if (opt->parents)
+                       show_parents(commit, abbrev_commit);
+               if (parent)
+                       printf(" (from %s)",
+                              diff_unique_abbrev(parent->object.sha1,
+                                                 abbrev_commit));
+               show_decorations(commit);
+               printf("%s", diff_get_color_opt(&opt->diffopt, DIFF_RESET));
+               putchar(opt->commit_format == CMIT_FMT_ONELINE ? ' ' : '\n');
+               if (opt->reflog_info) {
+                       show_reflog_message(opt->reflog_info,
+                                   opt->commit_format == CMIT_FMT_ONELINE,
+                                   opt->date_mode);
+                       if (opt->commit_format == CMIT_FMT_ONELINE) {
+                               printf("%s", sep);
+                               return;
+                       }
+               }
+       }
+
+       /*
+        * And then the pretty-printed message itself
+        */
+       strbuf_init(&msgbuf, 0);
+       pretty_print_commit(opt->commit_format, commit, &msgbuf,
+                           abbrev, subject, extra_headers, opt->date_mode,
+                           has_non_ascii(opt->add_signoff));
+
+       if (opt->add_signoff)
+               append_signoff(&msgbuf, opt->add_signoff);
+       if (opt->show_log_size)
+               printf("log size %i\n", (int)msgbuf.len);
+
+       if (msgbuf.len)
+               printf("%s%s%s", msgbuf.buf, extra, sep);
+       strbuf_release(&msgbuf);
+}
+
+int log_tree_diff_flush(struct rev_info *opt)
+{
+       diffcore_std(&opt->diffopt);
+
+       if (diff_queue_is_empty()) {
+               int saved_fmt = opt->diffopt.output_format;
+               opt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT;
+               diff_flush(&opt->diffopt);
+               opt->diffopt.output_format = saved_fmt;
+               return 0;
+       }
+
+       if (opt->loginfo && !opt->no_commit_id) {
+               /* When showing a verbose header (i.e. log message),
+                * and not in --pretty=oneline format, we would want
+                * an extra newline between the end of log and the
+                * output for readability.
+                */
+               show_log(opt, opt->diffopt.msg_sep);
+               if ((opt->diffopt.output_format & ~DIFF_FORMAT_NO_OUTPUT) &&
+                   opt->verbose_header &&
+                   opt->commit_format != CMIT_FMT_ONELINE) {
+                       int pch = DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_PATCH;
+                       if ((pch & opt->diffopt.output_format) == pch)
+                               printf("---");
+                       putchar('\n');
+               }
+       }
+       diff_flush(&opt->diffopt);
+       return 1;
+}
+
+static int do_diff_combined(struct rev_info *opt, struct commit *commit)
+{
+       unsigned const char *sha1 = commit->object.sha1;
+
+       diff_tree_combined_merge(sha1, opt->dense_combined_merges, opt);
+       return !opt->loginfo;
+}
+
+/*
+ * Show the diff of a commit.
+ *
+ * Return true if we printed any log info messages
+ */
+static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log_info *log)
+{
+       int showed_log;
+       struct commit_list *parents;
+       unsigned const char *sha1 = commit->object.sha1;
+
+       if (!opt->diff)
+               return 0;
+
+       /* Root commit? */
+       parents = commit->parents;
+       if (!parents) {
+               if (opt->show_root_diff) {
+                       diff_root_tree_sha1(sha1, "", &opt->diffopt);
+                       log_tree_diff_flush(opt);
+               }
+               return !opt->loginfo;
+       }
+
+       /* More than one parent? */
+       if (parents && parents->next) {
+               if (opt->ignore_merges)
+                       return 0;
+               else if (opt->combine_merges)
+                       return do_diff_combined(opt, commit);
+
+               /* If we show individual diffs, show the parent info */
+               log->parent = parents->item;
+       }
+
+       showed_log = 0;
+       for (;;) {
+               struct commit *parent = parents->item;
+
+               diff_tree_sha1(parent->object.sha1, sha1, "", &opt->diffopt);
+               log_tree_diff_flush(opt);
+
+               showed_log |= !opt->loginfo;
+
+               /* Set up the log info for the next parent, if any.. */
+               parents = parents->next;
+               if (!parents)
+                       break;
+               log->parent = parents->item;
+               opt->loginfo = log;
+       }
+       return showed_log;
+}
+
+int log_tree_commit(struct rev_info *opt, struct commit *commit)
+{
+       struct log_info log;
+       int shown;
+
+       log.commit = commit;
+       log.parent = NULL;
+       opt->loginfo = &log;
+
+       shown = log_tree_diff(opt, commit, &log);
+       if (!shown && opt->loginfo && opt->always_show_header) {
+               log.parent = NULL;
+               show_log(opt, "");
+               shown = 1;
+       }
+       opt->loginfo = NULL;
+       maybe_flush_or_die(stdout, "stdout");
+       return shown;
+}
diff --git a/log-tree.h b/log-tree.h
new file mode 100644 (file)
index 0000000..b33f7cd
--- /dev/null
@@ -0,0 +1,17 @@
+#ifndef LOG_TREE_H
+#define LOG_TREE_H
+
+#include "revision.h"
+
+struct log_info {
+       struct commit *commit, *parent;
+};
+
+void init_log_tree_opt(struct rev_info *);
+int log_tree_diff_flush(struct rev_info *);
+int log_tree_commit(struct rev_info *, struct commit *);
+int log_tree_opt_parse(struct rev_info *, const char **, int);
+void show_log(struct rev_info *opt, const char *sep);
+void show_decorations(struct commit *commit);
+
+#endif
diff --git a/macosx/AppMain.tcl b/macosx/AppMain.tcl
deleted file mode 100644 (file)
index 41ca08e..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-set gitexecdir {@@gitexecdir@@}
-set gitguilib  {@@GITGUI_LIBDIR@@}
-set env(PATH) "$gitexecdir:$env(PATH)"
-
-if {[string first -psn [lindex $argv 0]] == 0} {
-       lset argv 0 [file join $gitexecdir git-gui]
-}
-
-if {[file tail [lindex $argv 0]] eq {gitk}} {
-       set argv0 [file join $gitexecdir gitk]
-       set AppMain_source $argv0
-} else {
-       set argv0 [file join $gitexecdir [file tail [lindex $argv 0]]]
-       set AppMain_source [file join $gitguilib git-gui.tcl]
-       if {[pwd] eq {/}} {
-               cd $env(HOME)
-       }
-}
-
-unset gitexecdir gitguilib
-set argv [lrange $argv 1 end]
-source $AppMain_source
diff --git a/macosx/Info.plist b/macosx/Info.plist
deleted file mode 100644 (file)
index 99913ec..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-       <key>CFBundleDevelopmentRegion</key>
-       <string>English</string>
-       <key>CFBundleExecutable</key>
-       <string>Wish</string>
-       <key>CFBundleGetInfoString</key>
-       <string>Git Gui @@GITGUI_VERSION@@ © 2006-2007 Shawn Pearce, et. al.</string>
-       <key>CFBundleIconFile</key>
-       <string>git-gui.icns</string>
-       <key>CFBundleIdentifier</key>
-       <string>cz.or.repo.git-gui</string>
-       <key>CFBundleInfoDictionaryVersion</key>
-       <string>6.0</string>
-       <key>CFBundleName</key>
-       <string>Git Gui</string>
-       <key>CFBundlePackageType</key>
-       <string>APPL</string>
-       <key>CFBundleShortVersionString</key>
-       <string>@@GITGUI_VERSION@@</string>
-       <key>CFBundleSignature</key>
-       <string>GITg</string>
-       <key>CFBundleVersion</key>
-       <string>@@GITGUI_VERSION@@</string>
-</dict>
-</plist>
diff --git a/macosx/git-gui.icns b/macosx/git-gui.icns
deleted file mode 100644 (file)
index 77d88a7..0000000
Binary files a/macosx/git-gui.icns and /dev/null differ
diff --git a/mailmap.c b/mailmap.c
new file mode 100644 (file)
index 0000000..8714167
--- /dev/null
+++ b/mailmap.c
@@ -0,0 +1,91 @@
+#include "cache.h"
+#include "path-list.h"
+#include "mailmap.h"
+
+int read_mailmap(struct path_list *map, const char *filename, char **repo_abbrev)
+{
+       char buffer[1024];
+       FILE *f = fopen(filename, "r");
+
+       if (f == NULL)
+               return 1;
+       while (fgets(buffer, sizeof(buffer), f) != NULL) {
+               char *end_of_name, *left_bracket, *right_bracket;
+               char *name, *email;
+               int i;
+               if (buffer[0] == '#') {
+                       static const char abbrev[] = "# repo-abbrev:";
+                       int abblen = sizeof(abbrev) - 1;
+                       int len = strlen(buffer);
+
+                       if (!repo_abbrev)
+                               continue;
+
+                       if (len && buffer[len - 1] == '\n')
+                               buffer[--len] = 0;
+                       if (!strncmp(buffer, abbrev, abblen)) {
+                               char *cp;
+
+                               if (repo_abbrev)
+                                       free(*repo_abbrev);
+                               *repo_abbrev = xmalloc(len);
+
+                               for (cp = buffer + abblen; isspace(*cp); cp++)
+                                       ; /* nothing */
+                               strcpy(*repo_abbrev, cp);
+                       }
+                       continue;
+               }
+               if ((left_bracket = strchr(buffer, '<')) == NULL)
+                       continue;
+               if ((right_bracket = strchr(left_bracket + 1, '>')) == NULL)
+                       continue;
+               if (right_bracket == left_bracket + 1)
+                       continue;
+               for (end_of_name = left_bracket; end_of_name != buffer
+                               && isspace(end_of_name[-1]); end_of_name--)
+                       /* keep on looking */
+               if (end_of_name == buffer)
+                       continue;
+               name = xmalloc(end_of_name - buffer + 1);
+               strlcpy(name, buffer, end_of_name - buffer + 1);
+               email = xmalloc(right_bracket - left_bracket);
+               for (i = 0; i < right_bracket - left_bracket - 1; i++)
+                       email[i] = tolower(left_bracket[i + 1]);
+               email[right_bracket - left_bracket - 1] = '\0';
+               path_list_insert(email, map)->util = name;
+       }
+       fclose(f);
+       return 0;
+}
+
+int map_email(struct path_list *map, const char *email, char *name, int maxlen)
+{
+       char *p;
+       struct path_list_item *item;
+       char buf[1024], *mailbuf;
+       int i;
+
+       /* autocomplete common developers */
+       p = strchr(email, '>');
+       if (!p)
+               return 0;
+       if (p - email + 1 < sizeof(buf))
+               mailbuf = buf;
+       else
+               mailbuf = xmalloc(p - email + 1);
+
+       /* downcase the email address */
+       for (i = 0; i < p - email; i++)
+               mailbuf[i] = tolower(email[i]);
+       mailbuf[i] = 0;
+       item = path_list_lookup(mailbuf, map);
+       if (mailbuf != buf)
+               free(mailbuf);
+       if (item != NULL) {
+               const char *realname = (const char *)item->util;
+               strlcpy(name, realname, maxlen);
+               return 1;
+       }
+       return 0;
+}
diff --git a/mailmap.h b/mailmap.h
new file mode 100644 (file)
index 0000000..3503fd2
--- /dev/null
+++ b/mailmap.h
@@ -0,0 +1,7 @@
+#ifndef MAILMAP_H
+#define MAILMAP_H
+
+int read_mailmap(struct path_list *map, const char *filename, char **repo_abbrev);
+int map_email(struct path_list *mailmap, const char *email, char *name, int maxlen);
+
+#endif
diff --git a/match-trees.c b/match-trees.c
new file mode 100644 (file)
index 0000000..0fd6df7
--- /dev/null
@@ -0,0 +1,303 @@
+#include "cache.h"
+#include "tree.h"
+#include "tree-walk.h"
+
+static int score_missing(unsigned mode, const char *path)
+{
+       int score;
+
+       if (S_ISDIR(mode))
+               score = -1000;
+       else if (S_ISLNK(mode))
+               score = -500;
+       else
+               score = -50;
+       return score;
+}
+
+static int score_differs(unsigned mode1, unsigned mode2, const char *path)
+{
+       int score;
+
+       if (S_ISDIR(mode1) != S_ISDIR(mode2))
+               score = -100;
+       else if (S_ISLNK(mode1) != S_ISLNK(mode2))
+               score = -50;
+       else
+               score = -5;
+       return score;
+}
+
+static int score_matches(unsigned mode1, unsigned mode2, const char *path)
+{
+       int score;
+
+       /* Heh, we found SHA-1 collisions between different kind of objects */
+       if (S_ISDIR(mode1) != S_ISDIR(mode2))
+               score = -100;
+       else if (S_ISLNK(mode1) != S_ISLNK(mode2))
+               score = -50;
+
+       else if (S_ISDIR(mode1))
+               score = 1000;
+       else if (S_ISLNK(mode1))
+               score = 500;
+       else
+               score = 250;
+       return score;
+}
+
+/*
+ * Inspect two trees, and give a score that tells how similar they are.
+ */
+static int score_trees(const unsigned char *hash1, const unsigned char *hash2)
+{
+       struct tree_desc one;
+       struct tree_desc two;
+       void *one_buf, *two_buf;
+       int score = 0;
+       enum object_type type;
+       unsigned long size;
+
+       one_buf = read_sha1_file(hash1, &type, &size);
+       if (!one_buf)
+               die("unable to read tree (%s)", sha1_to_hex(hash1));
+       if (type != OBJ_TREE)
+               die("%s is not a tree", sha1_to_hex(hash1));
+       init_tree_desc(&one, one_buf, size);
+       two_buf = read_sha1_file(hash2, &type, &size);
+       if (!two_buf)
+               die("unable to read tree (%s)", sha1_to_hex(hash2));
+       if (type != OBJ_TREE)
+               die("%s is not a tree", sha1_to_hex(hash2));
+       init_tree_desc(&two, two_buf, size);
+       while (one.size | two.size) {
+               const unsigned char *elem1 = elem1;
+               const unsigned char *elem2 = elem2;
+               const char *path1 = path1;
+               const char *path2 = path2;
+               unsigned mode1 = mode1;
+               unsigned mode2 = mode2;
+               int cmp;
+
+               if (one.size)
+                       elem1 = tree_entry_extract(&one, &path1, &mode1);
+               if (two.size)
+                       elem2 = tree_entry_extract(&two, &path2, &mode2);
+
+               if (!one.size) {
+                       /* two has more entries */
+                       score += score_missing(mode2, path2);
+                       update_tree_entry(&two);
+                       continue;
+               }
+               if (!two.size) {
+                       /* two lacks this entry */
+                       score += score_missing(mode1, path1);
+                       update_tree_entry(&one);
+                       continue;
+               }
+               cmp = base_name_compare(path1, strlen(path1), mode1,
+                                       path2, strlen(path2), mode2);
+               if (cmp < 0) {
+                       /* path1 does not appear in two */
+                       score += score_missing(mode1, path1);
+                       update_tree_entry(&one);
+                       continue;
+               }
+               else if (cmp > 0) {
+                       /* path2 does not appear in one */
+                       score += score_missing(mode2, path2);
+                       update_tree_entry(&two);
+                       continue;
+               }
+               else if (hashcmp(elem1, elem2))
+                       /* they are different */
+                       score += score_differs(mode1, mode2, path1);
+               else
+                       /* same subtree or blob */
+                       score += score_matches(mode1, mode2, path1);
+               update_tree_entry(&one);
+               update_tree_entry(&two);
+       }
+       free(one_buf);
+       free(two_buf);
+       return score;
+}
+
+/*
+ * Match one itself and its subtrees with two and pick the best match.
+ */
+static void match_trees(const unsigned char *hash1,
+                       const unsigned char *hash2,
+                       int *best_score,
+                       char **best_match,
+                       const char *base,
+                       int recurse_limit)
+{
+       struct tree_desc one;
+       void *one_buf;
+       enum object_type type;
+       unsigned long size;
+
+       one_buf = read_sha1_file(hash1, &type, &size);
+       if (!one_buf)
+               die("unable to read tree (%s)", sha1_to_hex(hash1));
+       if (type != OBJ_TREE)
+               die("%s is not a tree", sha1_to_hex(hash1));
+       init_tree_desc(&one, one_buf, size);
+
+       while (one.size) {
+               const char *path;
+               const unsigned char *elem;
+               unsigned mode;
+               int score;
+
+               elem = tree_entry_extract(&one, &path, &mode);
+               if (!S_ISDIR(mode))
+                       goto next;
+               score = score_trees(elem, hash2);
+               if (*best_score < score) {
+                       char *newpath;
+                       newpath = xmalloc(strlen(base) + strlen(path) + 1);
+                       sprintf(newpath, "%s%s", base, path);
+                       free(*best_match);
+                       *best_match = newpath;
+                       *best_score = score;
+               }
+               if (recurse_limit) {
+                       char *newbase;
+                       newbase = xmalloc(strlen(base) + strlen(path) + 2);
+                       sprintf(newbase, "%s%s/", base, path);
+                       match_trees(elem, hash2, best_score, best_match,
+                                   newbase, recurse_limit - 1);
+                       free(newbase);
+               }
+
+       next:
+               update_tree_entry(&one);
+       }
+       free(one_buf);
+}
+
+/*
+ * A tree "hash1" has a subdirectory at "prefix".  Come up with a
+ * tree object by replacing it with another tree "hash2".
+ */
+static int splice_tree(const unsigned char *hash1,
+                      char *prefix,
+                      const unsigned char *hash2,
+                      unsigned char *result)
+{
+       char *subpath;
+       int toplen;
+       char *buf;
+       unsigned long sz;
+       struct tree_desc desc;
+       unsigned char *rewrite_here;
+       const unsigned char *rewrite_with;
+       unsigned char subtree[20];
+       enum object_type type;
+       int status;
+
+       subpath = strchr(prefix, '/');
+       if (!subpath)
+               toplen = strlen(prefix);
+       else {
+               toplen = subpath - prefix;
+               subpath++;
+       }
+
+       buf = read_sha1_file(hash1, &type, &sz);
+       if (!buf)
+               die("cannot read tree %s", sha1_to_hex(hash1));
+       init_tree_desc(&desc, buf, sz);
+
+       rewrite_here = NULL;
+       while (desc.size) {
+               const char *name;
+               unsigned mode;
+               const unsigned char *sha1;
+
+               sha1 = tree_entry_extract(&desc, &name, &mode);
+               if (strlen(name) == toplen &&
+                   !memcmp(name, prefix, toplen)) {
+                       if (!S_ISDIR(mode))
+                               die("entry %s in tree %s is not a tree",
+                                   name, sha1_to_hex(hash1));
+                       rewrite_here = (unsigned char *) sha1;
+                       break;
+               }
+               update_tree_entry(&desc);
+       }
+       if (!rewrite_here)
+               die("entry %.*s not found in tree %s",
+                   toplen, prefix, sha1_to_hex(hash1));
+       if (subpath) {
+               status = splice_tree(rewrite_here, subpath, hash2, subtree);
+               if (status)
+                       return status;
+               rewrite_with = subtree;
+       }
+       else
+               rewrite_with = hash2;
+       hashcpy(rewrite_here, rewrite_with);
+       status = write_sha1_file(buf, sz, tree_type, result);
+       free(buf);
+       return status;
+}
+
+/*
+ * We are trying to come up with a merge between one and two that
+ * results in a tree shape similar to one.  The tree two might
+ * correspond to a subtree of one, in which case it needs to be
+ * shifted down by prefixing otherwise empty directories.  On the
+ * other hand, it could cover tree one and we might need to pick a
+ * subtree of it.
+ */
+void shift_tree(const unsigned char *hash1,
+               const unsigned char *hash2,
+               unsigned char *shifted,
+               int depth_limit)
+{
+       char *add_prefix;
+       char *del_prefix;
+       int add_score, del_score;
+
+       add_score = del_score = score_trees(hash1, hash2);
+       add_prefix = xcalloc(1, 1);
+       del_prefix = xcalloc(1, 1);
+
+       /*
+        * See if one's subtree resembles two; if so we need to prefix
+        * two with a few fake trees to match the prefix.
+        */
+       match_trees(hash1, hash2, &add_score, &add_prefix, "", depth_limit);
+
+       /*
+        * See if two's subtree resembles one; if so we need to
+        * pick only subtree of two.
+        */
+       match_trees(hash2, hash1, &del_score, &del_prefix, "", depth_limit);
+
+       /* Assume we do not have to do any shifting */
+       hashcpy(shifted, hash2);
+
+       if (add_score < del_score) {
+               /* We need to pick a subtree of two */
+               unsigned mode;
+
+               if (!*del_prefix)
+                       return;
+
+               if (get_tree_entry(hash2, del_prefix, shifted, &mode))
+                       die("cannot find path %s in tree %s",
+                           del_prefix, sha1_to_hex(hash2));
+               return;
+       }
+
+       if (!*add_prefix)
+               return;
+
+       splice_tree(hash1, add_prefix, hash2, shifted);
+}
diff --git a/merge-file.c b/merge-file.c
new file mode 100644 (file)
index 0000000..1e031ea
--- /dev/null
@@ -0,0 +1,118 @@
+#include "cache.h"
+#include "run-command.h"
+#include "xdiff-interface.h"
+#include "blob.h"
+
+static int fill_mmfile_blob(mmfile_t *f, struct blob *obj)
+{
+       void *buf;
+       unsigned long size;
+       enum object_type type;
+
+       buf = read_sha1_file(obj->object.sha1, &type, &size);
+       if (!buf)
+               return -1;
+       if (type != OBJ_BLOB)
+               return -1;
+       f->ptr = buf;
+       f->size = size;
+       return 0;
+}
+
+static void free_mmfile(mmfile_t *f)
+{
+       free(f->ptr);
+}
+
+static void *three_way_filemerge(mmfile_t *base, mmfile_t *our, mmfile_t *their, unsigned long *size)
+{
+       mmbuffer_t res;
+       xpparam_t xpp;
+       int merge_status;
+
+       memset(&xpp, 0, sizeof(xpp));
+       merge_status = xdl_merge(base, our, ".our", their, ".their",
+               &xpp, XDL_MERGE_ZEALOUS, &res);
+
+       if (merge_status < 0)
+               return NULL;
+
+       *size = res.size;
+       return res.ptr;
+}
+
+static int common_outf(void *priv_, mmbuffer_t *mb, int nbuf)
+{
+       int i;
+       mmfile_t *dst = priv_;
+
+       for (i = 0; i < nbuf; i++) {
+               memcpy(dst->ptr + dst->size, mb[i].ptr, mb[i].size);
+               dst->size += mb[i].size;
+       }
+       return 0;
+}
+
+static int generate_common_file(mmfile_t *res, mmfile_t *f1, mmfile_t *f2)
+{
+       unsigned long size = f1->size < f2->size ? f1->size : f2->size;
+       void *ptr = xmalloc(size);
+       xpparam_t xpp;
+       xdemitconf_t xecfg;
+       xdemitcb_t ecb;
+
+       xpp.flags = XDF_NEED_MINIMAL;
+       memset(&xecfg, 0, sizeof(xecfg));
+       xecfg.ctxlen = 3;
+       xecfg.flags = XDL_EMIT_COMMON;
+       ecb.outf = common_outf;
+
+       res->ptr = ptr;
+       res->size = 0;
+
+       ecb.priv = res;
+       return xdl_diff(f1, f2, &xpp, &xecfg, &ecb);
+}
+
+void *merge_file(struct blob *base, struct blob *our, struct blob *their, unsigned long *size)
+{
+       void *res = NULL;
+       mmfile_t f1, f2, common;
+
+       /*
+        * Removed in either branch?
+        *
+        * NOTE! This depends on the caller having done the
+        * proper warning about removing a file that got
+        * modified in the other branch!
+        */
+       if (!our || !their) {
+               enum object_type type;
+               if (base)
+                       return NULL;
+               if (!our)
+                       our = their;
+               return read_sha1_file(our->object.sha1, &type, size);
+       }
+
+       if (fill_mmfile_blob(&f1, our) < 0)
+               goto out_no_mmfile;
+       if (fill_mmfile_blob(&f2, their) < 0)
+               goto out_free_f1;
+
+       if (base) {
+               if (fill_mmfile_blob(&common, base) < 0)
+                       goto out_free_f2_f1;
+       } else {
+               if (generate_common_file(&common, &f1, &f2) < 0)
+                       goto out_free_f2_f1;
+       }
+       res = three_way_filemerge(&common, &f1, &f2, size);
+       free_mmfile(&common);
+out_free_f2_f1:
+       free_mmfile(&f2);
+out_free_f1:
+       free_mmfile(&f1);
+out_no_mmfile:
+       return res;
+}
diff --git a/merge-index.c b/merge-index.c
new file mode 100644 (file)
index 0000000..fa719cb
--- /dev/null
@@ -0,0 +1,127 @@
+#include "cache.h"
+#include "run-command.h"
+
+static const char *pgm;
+static const char *arguments[9];
+static int one_shot, quiet;
+static int err;
+
+static void run_program(void)
+{
+       struct child_process child;
+       memset(&child, 0, sizeof(child));
+       child.argv = arguments;
+       if (run_command(&child)) {
+               if (one_shot) {
+                       err++;
+               } else {
+                       if (!quiet)
+                               die("merge program failed");
+                       exit(1);
+               }
+       }
+}
+
+static int merge_entry(int pos, const char *path)
+{
+       int found;
+
+       if (pos >= active_nr)
+               die("git-merge-index: %s not in the cache", path);
+       arguments[0] = pgm;
+       arguments[1] = "";
+       arguments[2] = "";
+       arguments[3] = "";
+       arguments[4] = path;
+       arguments[5] = "";
+       arguments[6] = "";
+       arguments[7] = "";
+       arguments[8] = NULL;
+       found = 0;
+       do {
+               static char hexbuf[4][60];
+               static char ownbuf[4][60];
+               struct cache_entry *ce = active_cache[pos];
+               int stage = ce_stage(ce);
+
+               if (strcmp(ce->name, path))
+                       break;
+               found++;
+               strcpy(hexbuf[stage], sha1_to_hex(ce->sha1));
+               sprintf(ownbuf[stage], "%o", ntohl(ce->ce_mode));
+               arguments[stage] = hexbuf[stage];
+               arguments[stage + 4] = ownbuf[stage];
+       } while (++pos < active_nr);
+       if (!found)
+               die("git-merge-index: %s not in the cache", path);
+       run_program();
+       return found;
+}
+
+static void merge_file(const char *path)
+{
+       int pos = cache_name_pos(path, strlen(path));
+
+       /*
+        * If it already exists in the cache as stage0, it's
+        * already merged and there is nothing to do.
+        */
+       if (pos < 0)
+               merge_entry(-pos-1, path);
+}
+
+static void merge_all(void)
+{
+       int i;
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (!ce_stage(ce))
+                       continue;
+               i += merge_entry(i, ce->name)-1;
+       }
+}
+
+int main(int argc, char **argv)
+{
+       int i, force_file = 0;
+
+       /* Without this we cannot rely on waitpid() to tell
+        * what happened to our children.
+        */
+       signal(SIGCHLD, SIG_DFL);
+
+       if (argc < 3)
+               usage("git-merge-index [-o] [-q] <merge-program> (-a | <filename>*)");
+
+       setup_git_directory();
+       read_cache();
+
+       i = 1;
+       if (!strcmp(argv[i], "-o")) {
+               one_shot = 1;
+               i++;
+       }
+       if (!strcmp(argv[i], "-q")) {
+               quiet = 1;
+               i++;
+       }
+       pgm = argv[i++];
+       for (; i < argc; i++) {
+               char *arg = argv[i];
+               if (!force_file && *arg == '-') {
+                       if (!strcmp(arg, "--")) {
+                               force_file = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "-a")) {
+                               merge_all();
+                               continue;
+                       }
+                       die("git-merge-index: unknown option %s", arg);
+               }
+               merge_file(arg);
+       }
+       if (err && !quiet)
+               die("merge program failed");
+       return err;
+}
diff --git a/merge-recursive.c b/merge-recursive.c
new file mode 100644 (file)
index 0000000..9a1e2f2
--- /dev/null
@@ -0,0 +1,1751 @@
+/*
+ * Recursive Merge algorithm stolen from git-merge-recursive.py by
+ * Fredrik Kuivinen.
+ * The thieves were Alex Riesen and Johannes Schindelin, in June/July 2006
+ */
+#include "cache.h"
+#include "cache-tree.h"
+#include "commit.h"
+#include "blob.h"
+#include "tree-walk.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "run-command.h"
+#include "tag.h"
+#include "unpack-trees.h"
+#include "path-list.h"
+#include "xdiff-interface.h"
+#include "interpolate.h"
+#include "attr.h"
+
+static int subtree_merge;
+
+static struct tree *shift_tree_object(struct tree *one, struct tree *two)
+{
+       unsigned char shifted[20];
+
+       /*
+        * NEEDSWORK: this limits the recursion depth to hardcoded
+        * value '2' to avoid excessive overhead.
+        */
+       shift_tree(one->object.sha1, two->object.sha1, shifted, 2);
+       if (!hashcmp(two->object.sha1, shifted))
+               return two;
+       return lookup_tree(shifted);
+}
+
+/*
+ * A virtual commit has
+ * - (const char *)commit->util set to the name, and
+ * - *(int *)commit->object.sha1 set to the virtual id.
+ */
+
+static unsigned commit_list_count(const struct commit_list *l)
+{
+       unsigned c = 0;
+       for (; l; l = l->next )
+               c++;
+       return c;
+}
+
+static struct commit *make_virtual_commit(struct tree *tree, const char *comment)
+{
+       struct commit *commit = xcalloc(1, sizeof(struct commit));
+       static unsigned virtual_id = 1;
+       commit->tree = tree;
+       commit->util = (void*)comment;
+       *(int*)commit->object.sha1 = virtual_id++;
+       /* avoid warnings */
+       commit->object.parsed = 1;
+       return commit;
+}
+
+/*
+ * Since we use get_tree_entry(), which does not put the read object into
+ * the object pool, we cannot rely on a == b.
+ */
+static int sha_eq(const unsigned char *a, const unsigned char *b)
+{
+       if (!a && !b)
+               return 2;
+       return a && b && hashcmp(a, b) == 0;
+}
+
+/*
+ * Since we want to write the index eventually, we cannot reuse the index
+ * for these (temporary) data.
+ */
+struct stage_data
+{
+       struct
+       {
+               unsigned mode;
+               unsigned char sha[20];
+       } stages[4];
+       unsigned processed:1;
+};
+
+static struct path_list current_file_set = {NULL, 0, 0, 1};
+static struct path_list current_directory_set = {NULL, 0, 0, 1};
+
+static int call_depth = 0;
+static int verbosity = 2;
+static int rename_limit = -1;
+static int buffer_output = 1;
+static struct strbuf obuf = STRBUF_INIT;
+
+static int show(int v)
+{
+       return (!call_depth && verbosity >= v) || verbosity >= 5;
+}
+
+static void flush_output(void)
+{
+       if (obuf.len) {
+               fputs(obuf.buf, stdout);
+               strbuf_reset(&obuf);
+       }
+}
+
+static void output(int v, const char *fmt, ...)
+{
+       int len;
+       va_list ap;
+
+       if (!show(v))
+               return;
+
+       strbuf_grow(&obuf, call_depth * 2 + 2);
+       memset(obuf.buf + obuf.len, ' ', call_depth * 2);
+       strbuf_setlen(&obuf, obuf.len + call_depth * 2);
+
+       va_start(ap, fmt);
+       len = vsnprintf(obuf.buf + obuf.len, strbuf_avail(&obuf), fmt, ap);
+       va_end(ap);
+
+       if (len < 0)
+               len = 0;
+       if (len >= strbuf_avail(&obuf)) {
+               strbuf_grow(&obuf, len + 2);
+               va_start(ap, fmt);
+               len = vsnprintf(obuf.buf + obuf.len, strbuf_avail(&obuf), fmt, ap);
+               va_end(ap);
+               if (len >= strbuf_avail(&obuf)) {
+                       die("this should not happen, your snprintf is broken");
+               }
+       }
+       strbuf_setlen(&obuf, obuf.len + len);
+       strbuf_add(&obuf, "\n", 1);
+       if (!buffer_output)
+               flush_output();
+}
+
+static void output_commit_title(struct commit *commit)
+{
+       int i;
+       flush_output();
+       for (i = call_depth; i--;)
+               fputs("  ", stdout);
+       if (commit->util)
+               printf("virtual %s\n", (char *)commit->util);
+       else {
+               printf("%s ", find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV));
+               if (parse_commit(commit) != 0)
+                       printf("(bad commit)\n");
+               else {
+                       const char *s;
+                       int len;
+                       for (s = commit->buffer; *s; s++)
+                               if (*s == '\n' && s[1] == '\n') {
+                                       s += 2;
+                                       break;
+                               }
+                       for (len = 0; s[len] && '\n' != s[len]; len++)
+                               ; /* do nothing */
+                       printf("%.*s\n", len, s);
+               }
+       }
+}
+
+static int add_cacheinfo(unsigned int mode, const unsigned char *sha1,
+               const char *path, int stage, int refresh, int options)
+{
+       struct cache_entry *ce;
+       ce = make_cache_entry(mode, sha1 ? sha1 : null_sha1, path, stage, refresh);
+       if (!ce)
+               return error("addinfo_cache failed for path '%s'", path);
+       return add_cache_entry(ce, options);
+}
+
+/*
+ * This is a global variable which is used in a number of places but
+ * only written to in the 'merge' function.
+ *
+ * index_only == 1    => Don't leave any non-stage 0 entries in the cache and
+ *                       don't update the working directory.
+ *               0    => Leave unmerged entries in the cache and update
+ *                       the working directory.
+ */
+static int index_only = 0;
+
+static void init_tree_desc_from_tree(struct tree_desc *desc, struct tree *tree)
+{
+       parse_tree(tree);
+       init_tree_desc(desc, tree->buffer, tree->size);
+}
+
+static int git_merge_trees(int index_only,
+                          struct tree *common,
+                          struct tree *head,
+                          struct tree *merge)
+{
+       int rc;
+       struct tree_desc t[3];
+       struct unpack_trees_options opts;
+
+       memset(&opts, 0, sizeof(opts));
+       if (index_only)
+               opts.index_only = 1;
+       else
+               opts.update = 1;
+       opts.merge = 1;
+       opts.head_idx = 2;
+       opts.fn = threeway_merge;
+
+       init_tree_desc_from_tree(t+0, common);
+       init_tree_desc_from_tree(t+1, head);
+       init_tree_desc_from_tree(t+2, merge);
+
+       rc = unpack_trees(3, t, &opts);
+       cache_tree_free(&active_cache_tree);
+       return rc;
+}
+
+static int unmerged_index(void)
+{
+       int i;
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (ce_stage(ce))
+                       return 1;
+       }
+       return 0;
+}
+
+static struct tree *git_write_tree(void)
+{
+       struct tree *result = NULL;
+
+       if (unmerged_index()) {
+               int i;
+               output(0, "There are unmerged index entries:");
+               for (i = 0; i < active_nr; i++) {
+                       struct cache_entry *ce = active_cache[i];
+                       if (ce_stage(ce))
+                               output(0, "%d %.*s", ce_stage(ce), ce_namelen(ce), ce->name);
+               }
+               return NULL;
+       }
+
+       if (!active_cache_tree)
+               active_cache_tree = cache_tree();
+
+       if (!cache_tree_fully_valid(active_cache_tree) &&
+           cache_tree_update(active_cache_tree,
+                             active_cache, active_nr, 0, 0) < 0)
+               die("error building trees");
+
+       result = lookup_tree(active_cache_tree->sha1);
+
+       return result;
+}
+
+static int save_files_dirs(const unsigned char *sha1,
+               const char *base, int baselen, const char *path,
+               unsigned int mode, int stage)
+{
+       int len = strlen(path);
+       char *newpath = xmalloc(baselen + len + 1);
+       memcpy(newpath, base, baselen);
+       memcpy(newpath + baselen, path, len);
+       newpath[baselen + len] = '\0';
+
+       if (S_ISDIR(mode))
+               path_list_insert(newpath, &current_directory_set);
+       else
+               path_list_insert(newpath, &current_file_set);
+       free(newpath);
+
+       return READ_TREE_RECURSIVE;
+}
+
+static int get_files_dirs(struct tree *tree)
+{
+       int n;
+       if (read_tree_recursive(tree, "", 0, 0, NULL, save_files_dirs) != 0)
+               return 0;
+       n = current_file_set.nr + current_directory_set.nr;
+       return n;
+}
+
+/*
+ * Returns a index_entry instance which doesn't have to correspond to
+ * a real cache entry in Git's index.
+ */
+static struct stage_data *insert_stage_data(const char *path,
+               struct tree *o, struct tree *a, struct tree *b,
+               struct path_list *entries)
+{
+       struct path_list_item *item;
+       struct stage_data *e = xcalloc(1, sizeof(struct stage_data));
+       get_tree_entry(o->object.sha1, path,
+                       e->stages[1].sha, &e->stages[1].mode);
+       get_tree_entry(a->object.sha1, path,
+                       e->stages[2].sha, &e->stages[2].mode);
+       get_tree_entry(b->object.sha1, path,
+                       e->stages[3].sha, &e->stages[3].mode);
+       item = path_list_insert(path, entries);
+       item->util = e;
+       return e;
+}
+
+/*
+ * Create a dictionary mapping file names to stage_data objects. The
+ * dictionary contains one entry for every path with a non-zero stage entry.
+ */
+static struct path_list *get_unmerged(void)
+{
+       struct path_list *unmerged = xcalloc(1, sizeof(struct path_list));
+       int i;
+
+       unmerged->strdup_paths = 1;
+
+       for (i = 0; i < active_nr; i++) {
+               struct path_list_item *item;
+               struct stage_data *e;
+               struct cache_entry *ce = active_cache[i];
+               if (!ce_stage(ce))
+                       continue;
+
+               item = path_list_lookup(ce->name, unmerged);
+               if (!item) {
+                       item = path_list_insert(ce->name, unmerged);
+                       item->util = xcalloc(1, sizeof(struct stage_data));
+               }
+               e = item->util;
+               e->stages[ce_stage(ce)].mode = ntohl(ce->ce_mode);
+               hashcpy(e->stages[ce_stage(ce)].sha, ce->sha1);
+       }
+
+       return unmerged;
+}
+
+struct rename
+{
+       struct diff_filepair *pair;
+       struct stage_data *src_entry;
+       struct stage_data *dst_entry;
+       unsigned processed:1;
+};
+
+/*
+ * Get information of all renames which occurred between 'o_tree' and
+ * 'tree'. We need the three trees in the merge ('o_tree', 'a_tree' and
+ * 'b_tree') to be able to associate the correct cache entries with
+ * the rename information. 'tree' is always equal to either a_tree or b_tree.
+ */
+static struct path_list *get_renames(struct tree *tree,
+                                       struct tree *o_tree,
+                                       struct tree *a_tree,
+                                       struct tree *b_tree,
+                                       struct path_list *entries)
+{
+       int i;
+       struct path_list *renames;
+       struct diff_options opts;
+
+       renames = xcalloc(1, sizeof(struct path_list));
+       diff_setup(&opts);
+       DIFF_OPT_SET(&opts, RECURSIVE);
+       opts.detect_rename = DIFF_DETECT_RENAME;
+       opts.rename_limit = rename_limit;
+       opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+       if (diff_setup_done(&opts) < 0)
+               die("diff setup failed");
+       diff_tree_sha1(o_tree->object.sha1, tree->object.sha1, "", &opts);
+       diffcore_std(&opts);
+       for (i = 0; i < diff_queued_diff.nr; ++i) {
+               struct path_list_item *item;
+               struct rename *re;
+               struct diff_filepair *pair = diff_queued_diff.queue[i];
+               if (pair->status != 'R') {
+                       diff_free_filepair(pair);
+                       continue;
+               }
+               re = xmalloc(sizeof(*re));
+               re->processed = 0;
+               re->pair = pair;
+               item = path_list_lookup(re->pair->one->path, entries);
+               if (!item)
+                       re->src_entry = insert_stage_data(re->pair->one->path,
+                                       o_tree, a_tree, b_tree, entries);
+               else
+                       re->src_entry = item->util;
+
+               item = path_list_lookup(re->pair->two->path, entries);
+               if (!item)
+                       re->dst_entry = insert_stage_data(re->pair->two->path,
+                                       o_tree, a_tree, b_tree, entries);
+               else
+                       re->dst_entry = item->util;
+               item = path_list_insert(pair->one->path, renames);
+               item->util = re;
+       }
+       opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+       diff_queued_diff.nr = 0;
+       diff_flush(&opts);
+       return renames;
+}
+
+static int update_stages(const char *path, struct diff_filespec *o,
+                        struct diff_filespec *a, struct diff_filespec *b,
+                        int clear)
+{
+       int options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
+       if (clear)
+               if (remove_file_from_cache(path))
+                       return -1;
+       if (o)
+               if (add_cacheinfo(o->mode, o->sha1, path, 1, 0, options))
+                       return -1;
+       if (a)
+               if (add_cacheinfo(a->mode, a->sha1, path, 2, 0, options))
+                       return -1;
+       if (b)
+               if (add_cacheinfo(b->mode, b->sha1, path, 3, 0, options))
+                       return -1;
+       return 0;
+}
+
+static int remove_path(const char *name)
+{
+       int ret;
+       char *slash, *dirs;
+
+       ret = unlink(name);
+       if (ret)
+               return ret;
+       dirs = xstrdup(name);
+       while ((slash = strrchr(name, '/'))) {
+               *slash = '\0';
+               if (rmdir(name) != 0)
+                       break;
+       }
+       free(dirs);
+       return ret;
+}
+
+static int remove_file(int clean, const char *path, int no_wd)
+{
+       int update_cache = index_only || clean;
+       int update_working_directory = !index_only && !no_wd;
+
+       if (update_cache) {
+               if (remove_file_from_cache(path))
+                       return -1;
+       }
+       if (update_working_directory) {
+               unlink(path);
+               if (errno != ENOENT || errno != EISDIR)
+                       return -1;
+               remove_path(path);
+       }
+       return 0;
+}
+
+static char *unique_path(const char *path, const char *branch)
+{
+       char *newpath = xmalloc(strlen(path) + 1 + strlen(branch) + 8 + 1);
+       int suffix = 0;
+       struct stat st;
+       char *p = newpath + strlen(path);
+       strcpy(newpath, path);
+       *(p++) = '~';
+       strcpy(p, branch);
+       for (; *p; ++p)
+               if ('/' == *p)
+                       *p = '_';
+       while (path_list_has_path(&current_file_set, newpath) ||
+              path_list_has_path(&current_directory_set, newpath) ||
+              lstat(newpath, &st) == 0)
+               sprintf(p, "_%d", suffix++);
+
+       path_list_insert(newpath, &current_file_set);
+       return newpath;
+}
+
+static int mkdir_p(const char *path, unsigned long mode)
+{
+       /* path points to cache entries, so xstrdup before messing with it */
+       char *buf = xstrdup(path);
+       int result = safe_create_leading_directories(buf);
+       free(buf);
+       return result;
+}
+
+static void flush_buffer(int fd, const char *buf, unsigned long size)
+{
+       while (size > 0) {
+               long ret = write_in_full(fd, buf, size);
+               if (ret < 0) {
+                       /* Ignore epipe */
+                       if (errno == EPIPE)
+                               break;
+                       die("merge-recursive: %s", strerror(errno));
+               } else if (!ret) {
+                       die("merge-recursive: disk full?");
+               }
+               size -= ret;
+               buf += ret;
+       }
+}
+
+static int make_room_for_path(const char *path)
+{
+       int status;
+       const char *msg = "failed to create path '%s'%s";
+
+       status = mkdir_p(path, 0777);
+       if (status) {
+               if (status == -3) {
+                       /* something else exists */
+                       error(msg, path, ": perhaps a D/F conflict?");
+                       return -1;
+               }
+               die(msg, path, "");
+       }
+
+       /* Successful unlink is good.. */
+       if (!unlink(path))
+               return 0;
+       /* .. and so is no existing file */
+       if (errno == ENOENT)
+               return 0;
+       /* .. but not some other error (who really cares what?) */
+       return error(msg, path, ": perhaps a D/F conflict?");
+}
+
+static void update_file_flags(const unsigned char *sha,
+                             unsigned mode,
+                             const char *path,
+                             int update_cache,
+                             int update_wd)
+{
+       if (index_only)
+               update_wd = 0;
+
+       if (update_wd) {
+               enum object_type type;
+               void *buf;
+               unsigned long size;
+
+               buf = read_sha1_file(sha, &type, &size);
+               if (!buf)
+                       die("cannot read object %s '%s'", sha1_to_hex(sha), path);
+               if (type != OBJ_BLOB)
+                       die("blob expected for %s '%s'", sha1_to_hex(sha), path);
+
+               if (make_room_for_path(path) < 0) {
+                       update_wd = 0;
+                       goto update_index;
+               }
+               if (S_ISREG(mode) || (!has_symlinks && S_ISLNK(mode))) {
+                       int fd;
+                       if (mode & 0100)
+                               mode = 0777;
+                       else
+                               mode = 0666;
+                       fd = open(path, O_WRONLY | O_TRUNC | O_CREAT, mode);
+                       if (fd < 0)
+                               die("failed to open %s: %s", path, strerror(errno));
+                       flush_buffer(fd, buf, size);
+                       close(fd);
+               } else if (S_ISLNK(mode)) {
+                       char *lnk = xmemdupz(buf, size);
+                       mkdir_p(path, 0777);
+                       unlink(path);
+                       symlink(lnk, path);
+                       free(lnk);
+               } else
+                       die("do not know what to do with %06o %s '%s'",
+                           mode, sha1_to_hex(sha), path);
+       }
+ update_index:
+       if (update_cache)
+               add_cacheinfo(mode, sha, path, 0, update_wd, ADD_CACHE_OK_TO_ADD);
+}
+
+static void update_file(int clean,
+                       const unsigned char *sha,
+                       unsigned mode,
+                       const char *path)
+{
+       update_file_flags(sha, mode, path, index_only || clean, !index_only);
+}
+
+/* Low level file merging, update and removal */
+
+struct merge_file_info
+{
+       unsigned char sha[20];
+       unsigned mode;
+       unsigned clean:1,
+                merge:1;
+};
+
+static void fill_mm(const unsigned char *sha1, mmfile_t *mm)
+{
+       unsigned long size;
+       enum object_type type;
+
+       if (!hashcmp(sha1, null_sha1)) {
+               mm->ptr = xstrdup("");
+               mm->size = 0;
+               return;
+       }
+
+       mm->ptr = read_sha1_file(sha1, &type, &size);
+       if (!mm->ptr || type != OBJ_BLOB)
+               die("unable to read blob object %s", sha1_to_hex(sha1));
+       mm->size = size;
+}
+
+/*
+ * Customizable low-level merge drivers support.
+ */
+
+struct ll_merge_driver;
+typedef int (*ll_merge_fn)(const struct ll_merge_driver *,
+                          const char *path,
+                          mmfile_t *orig,
+                          mmfile_t *src1, const char *name1,
+                          mmfile_t *src2, const char *name2,
+                          mmbuffer_t *result);
+
+struct ll_merge_driver {
+       const char *name;
+       const char *description;
+       ll_merge_fn fn;
+       const char *recursive;
+       struct ll_merge_driver *next;
+       char *cmdline;
+};
+
+/*
+ * Built-in low-levels
+ */
+static int ll_binary_merge(const struct ll_merge_driver *drv_unused,
+                          const char *path_unused,
+                          mmfile_t *orig,
+                          mmfile_t *src1, const char *name1,
+                          mmfile_t *src2, const char *name2,
+                          mmbuffer_t *result)
+{
+       /*
+        * The tentative merge result is "ours" for the final round,
+        * or common ancestor for an internal merge.  Still return
+        * "conflicted merge" status.
+        */
+       mmfile_t *stolen = index_only ? orig : src1;
+
+       result->ptr = stolen->ptr;
+       result->size = stolen->size;
+       stolen->ptr = NULL;
+       return 1;
+}
+
+static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,
+                       const char *path_unused,
+                       mmfile_t *orig,
+                       mmfile_t *src1, const char *name1,
+                       mmfile_t *src2, const char *name2,
+                       mmbuffer_t *result)
+{
+       xpparam_t xpp;
+
+       if (buffer_is_binary(orig->ptr, orig->size) ||
+           buffer_is_binary(src1->ptr, src1->size) ||
+           buffer_is_binary(src2->ptr, src2->size)) {
+               warning("Cannot merge binary files: %s vs. %s\n",
+                       name1, name2);
+               return ll_binary_merge(drv_unused, path_unused,
+                                      orig, src1, name1,
+                                      src2, name2,
+                                      result);
+       }
+
+       memset(&xpp, 0, sizeof(xpp));
+       return xdl_merge(orig,
+                        src1, name1,
+                        src2, name2,
+                        &xpp, XDL_MERGE_ZEALOUS,
+                        result);
+}
+
+static int ll_union_merge(const struct ll_merge_driver *drv_unused,
+                         const char *path_unused,
+                         mmfile_t *orig,
+                         mmfile_t *src1, const char *name1,
+                         mmfile_t *src2, const char *name2,
+                         mmbuffer_t *result)
+{
+       char *src, *dst;
+       long size;
+       const int marker_size = 7;
+
+       int status = ll_xdl_merge(drv_unused, path_unused,
+                                 orig, src1, NULL, src2, NULL, result);
+       if (status <= 0)
+               return status;
+       size = result->size;
+       src = dst = result->ptr;
+       while (size) {
+               char ch;
+               if ((marker_size < size) &&
+                   (*src == '<' || *src == '=' || *src == '>')) {
+                       int i;
+                       ch = *src;
+                       for (i = 0; i < marker_size; i++)
+                               if (src[i] != ch)
+                                       goto not_a_marker;
+                       if (src[marker_size] != '\n')
+                               goto not_a_marker;
+                       src += marker_size + 1;
+                       size -= marker_size + 1;
+                       continue;
+               }
+       not_a_marker:
+               do {
+                       ch = *src++;
+                       *dst++ = ch;
+                       size--;
+               } while (ch != '\n' && size);
+       }
+       result->size = dst - result->ptr;
+       return 0;
+}
+
+#define LL_BINARY_MERGE 0
+#define LL_TEXT_MERGE 1
+#define LL_UNION_MERGE 2
+static struct ll_merge_driver ll_merge_drv[] = {
+       { "binary", "built-in binary merge", ll_binary_merge },
+       { "text", "built-in 3-way text merge", ll_xdl_merge },
+       { "union", "built-in union merge", ll_union_merge },
+};
+
+static void create_temp(mmfile_t *src, char *path)
+{
+       int fd;
+
+       strcpy(path, ".merge_file_XXXXXX");
+       fd = xmkstemp(path);
+       if (write_in_full(fd, src->ptr, src->size) != src->size)
+               die("unable to write temp-file");
+       close(fd);
+}
+
+/*
+ * User defined low-level merge driver support.
+ */
+static int ll_ext_merge(const struct ll_merge_driver *fn,
+                       const char *path,
+                       mmfile_t *orig,
+                       mmfile_t *src1, const char *name1,
+                       mmfile_t *src2, const char *name2,
+                       mmbuffer_t *result)
+{
+       char temp[3][50];
+       char cmdbuf[2048];
+       struct interp table[] = {
+               { "%O" },
+               { "%A" },
+               { "%B" },
+       };
+       struct child_process child;
+       const char *args[20];
+       int status, fd, i;
+       struct stat st;
+
+       if (fn->cmdline == NULL)
+               die("custom merge driver %s lacks command line.", fn->name);
+
+       result->ptr = NULL;
+       result->size = 0;
+       create_temp(orig, temp[0]);
+       create_temp(src1, temp[1]);
+       create_temp(src2, temp[2]);
+
+       interp_set_entry(table, 0, temp[0]);
+       interp_set_entry(table, 1, temp[1]);
+       interp_set_entry(table, 2, temp[2]);
+
+       output(1, "merging %s using %s", path,
+              fn->description ? fn->description : fn->name);
+
+       interpolate(cmdbuf, sizeof(cmdbuf), fn->cmdline, table, 3);
+
+       memset(&child, 0, sizeof(child));
+       child.argv = args;
+       args[0] = "sh";
+       args[1] = "-c";
+       args[2] = cmdbuf;
+       args[3] = NULL;
+
+       status = run_command(&child);
+       if (status < -ERR_RUN_COMMAND_FORK)
+               ; /* failure in run-command */
+       else
+               status = -status;
+       fd = open(temp[1], O_RDONLY);
+       if (fd < 0)
+               goto bad;
+       if (fstat(fd, &st))
+               goto close_bad;
+       result->size = st.st_size;
+       result->ptr = xmalloc(result->size + 1);
+       if (read_in_full(fd, result->ptr, result->size) != result->size) {
+               free(result->ptr);
+               result->ptr = NULL;
+               result->size = 0;
+       }
+ close_bad:
+       close(fd);
+ bad:
+       for (i = 0; i < 3; i++)
+               unlink(temp[i]);
+       return status;
+}
+
+/*
+ * merge.default and merge.driver configuration items
+ */
+static struct ll_merge_driver *ll_user_merge, **ll_user_merge_tail;
+static const char *default_ll_merge;
+
+static int read_merge_config(const char *var, const char *value)
+{
+       struct ll_merge_driver *fn;
+       const char *ep, *name;
+       int namelen;
+
+       if (!strcmp(var, "merge.default")) {
+               if (value)
+                       default_ll_merge = strdup(value);
+               return 0;
+       }
+
+       /*
+        * We are not interested in anything but "merge.<name>.variable";
+        * especially, we do not want to look at variables such as
+        * "merge.summary", "merge.tool", and "merge.verbosity".
+        */
+       if (prefixcmp(var, "merge.") || (ep = strrchr(var, '.')) == var + 5)
+               return 0;
+
+       /*
+        * Find existing one as we might be processing merge.<name>.var2
+        * after seeing merge.<name>.var1.
+        */
+       name = var + 6;
+       namelen = ep - name;
+       for (fn = ll_user_merge; fn; fn = fn->next)
+               if (!strncmp(fn->name, name, namelen) && !fn->name[namelen])
+                       break;
+       if (!fn) {
+               fn = xcalloc(1, sizeof(struct ll_merge_driver));
+               fn->name = xmemdupz(name, namelen);
+               fn->fn = ll_ext_merge;
+               *ll_user_merge_tail = fn;
+               ll_user_merge_tail = &(fn->next);
+       }
+
+       ep++;
+
+       if (!strcmp("name", ep)) {
+               if (!value)
+                       return error("%s: lacks value", var);
+               fn->description = strdup(value);
+               return 0;
+       }
+
+       if (!strcmp("driver", ep)) {
+               if (!value)
+                       return error("%s: lacks value", var);
+               /*
+                * merge.<name>.driver specifies the command line:
+                *
+                *      command-line
+                *
+                * The command-line will be interpolated with the following
+                * tokens and is given to the shell:
+                *
+                *    %O - temporary file name for the merge base.
+                *    %A - temporary file name for our version.
+                *    %B - temporary file name for the other branches' version.
+                *
+                * The external merge driver should write the results in the
+                * file named by %A, and signal that it has done with zero exit
+                * status.
+                */
+               fn->cmdline = strdup(value);
+               return 0;
+       }
+
+       if (!strcmp("recursive", ep)) {
+               if (!value)
+                       return error("%s: lacks value", var);
+               fn->recursive = strdup(value);
+               return 0;
+       }
+
+       return 0;
+}
+
+static void initialize_ll_merge(void)
+{
+       if (ll_user_merge_tail)
+               return;
+       ll_user_merge_tail = &ll_user_merge;
+       git_config(read_merge_config);
+}
+
+static const struct ll_merge_driver *find_ll_merge_driver(const char *merge_attr)
+{
+       struct ll_merge_driver *fn;
+       const char *name;
+       int i;
+
+       initialize_ll_merge();
+
+       if (ATTR_TRUE(merge_attr))
+               return &ll_merge_drv[LL_TEXT_MERGE];
+       else if (ATTR_FALSE(merge_attr))
+               return &ll_merge_drv[LL_BINARY_MERGE];
+       else if (ATTR_UNSET(merge_attr)) {
+               if (!default_ll_merge)
+                       return &ll_merge_drv[LL_TEXT_MERGE];
+               else
+                       name = default_ll_merge;
+       }
+       else
+               name = merge_attr;
+
+       for (fn = ll_user_merge; fn; fn = fn->next)
+               if (!strcmp(fn->name, name))
+                       return fn;
+
+       for (i = 0; i < ARRAY_SIZE(ll_merge_drv); i++)
+               if (!strcmp(ll_merge_drv[i].name, name))
+                       return &ll_merge_drv[i];
+
+       /* default to the 3-way */
+       return &ll_merge_drv[LL_TEXT_MERGE];
+}
+
+static const char *git_path_check_merge(const char *path)
+{
+       static struct git_attr_check attr_merge_check;
+
+       if (!attr_merge_check.attr)
+               attr_merge_check.attr = git_attr("merge", 5);
+
+       if (git_checkattr(path, 1, &attr_merge_check))
+               return NULL;
+       return attr_merge_check.value;
+}
+
+static int ll_merge(mmbuffer_t *result_buf,
+                   struct diff_filespec *o,
+                   struct diff_filespec *a,
+                   struct diff_filespec *b,
+                   const char *branch1,
+                   const char *branch2)
+{
+       mmfile_t orig, src1, src2;
+       char *name1, *name2;
+       int merge_status;
+       const char *ll_driver_name;
+       const struct ll_merge_driver *driver;
+
+       name1 = xstrdup(mkpath("%s:%s", branch1, a->path));
+       name2 = xstrdup(mkpath("%s:%s", branch2, b->path));
+
+       fill_mm(o->sha1, &orig);
+       fill_mm(a->sha1, &src1);
+       fill_mm(b->sha1, &src2);
+
+       ll_driver_name = git_path_check_merge(a->path);
+       driver = find_ll_merge_driver(ll_driver_name);
+
+       if (index_only && driver->recursive)
+               driver = find_ll_merge_driver(driver->recursive);
+       merge_status = driver->fn(driver, a->path,
+                                 &orig, &src1, name1, &src2, name2,
+                                 result_buf);
+
+       free(name1);
+       free(name2);
+       free(orig.ptr);
+       free(src1.ptr);
+       free(src2.ptr);
+       return merge_status;
+}
+
+static struct merge_file_info merge_file(struct diff_filespec *o,
+               struct diff_filespec *a, struct diff_filespec *b,
+               const char *branch1, const char *branch2)
+{
+       struct merge_file_info result;
+       result.merge = 0;
+       result.clean = 1;
+
+       if ((S_IFMT & a->mode) != (S_IFMT & b->mode)) {
+               result.clean = 0;
+               if (S_ISREG(a->mode)) {
+                       result.mode = a->mode;
+                       hashcpy(result.sha, a->sha1);
+               } else {
+                       result.mode = b->mode;
+                       hashcpy(result.sha, b->sha1);
+               }
+       } else {
+               if (!sha_eq(a->sha1, o->sha1) && !sha_eq(b->sha1, o->sha1))
+                       result.merge = 1;
+
+               result.mode = a->mode == o->mode ? b->mode: a->mode;
+
+               if (sha_eq(a->sha1, o->sha1))
+                       hashcpy(result.sha, b->sha1);
+               else if (sha_eq(b->sha1, o->sha1))
+                       hashcpy(result.sha, a->sha1);
+               else if (S_ISREG(a->mode)) {
+                       mmbuffer_t result_buf;
+                       int merge_status;
+
+                       merge_status = ll_merge(&result_buf, o, a, b,
+                                               branch1, branch2);
+
+                       if ((merge_status < 0) || !result_buf.ptr)
+                               die("Failed to execute internal merge");
+
+                       if (write_sha1_file(result_buf.ptr, result_buf.size,
+                                           blob_type, result.sha))
+                               die("Unable to add %s to database",
+                                   a->path);
+
+                       free(result_buf.ptr);
+                       result.clean = (merge_status == 0);
+               } else {
+                       if (!(S_ISLNK(a->mode) || S_ISLNK(b->mode)))
+                               die("cannot merge modes?");
+
+                       hashcpy(result.sha, a->sha1);
+
+                       if (!sha_eq(a->sha1, b->sha1))
+                               result.clean = 0;
+               }
+       }
+
+       return result;
+}
+
+static void conflict_rename_rename(struct rename *ren1,
+                                  const char *branch1,
+                                  struct rename *ren2,
+                                  const char *branch2)
+{
+       char *del[2];
+       int delp = 0;
+       const char *ren1_dst = ren1->pair->two->path;
+       const char *ren2_dst = ren2->pair->two->path;
+       const char *dst_name1 = ren1_dst;
+       const char *dst_name2 = ren2_dst;
+       if (path_list_has_path(&current_directory_set, ren1_dst)) {
+               dst_name1 = del[delp++] = unique_path(ren1_dst, branch1);
+               output(1, "%s is a directory in %s added as %s instead",
+                      ren1_dst, branch2, dst_name1);
+               remove_file(0, ren1_dst, 0);
+       }
+       if (path_list_has_path(&current_directory_set, ren2_dst)) {
+               dst_name2 = del[delp++] = unique_path(ren2_dst, branch2);
+               output(1, "%s is a directory in %s added as %s instead",
+                      ren2_dst, branch1, dst_name2);
+               remove_file(0, ren2_dst, 0);
+       }
+       if (index_only) {
+               remove_file_from_cache(dst_name1);
+               remove_file_from_cache(dst_name2);
+               /*
+                * Uncomment to leave the conflicting names in the resulting tree
+                *
+                * update_file(0, ren1->pair->two->sha1, ren1->pair->two->mode, dst_name1);
+                * update_file(0, ren2->pair->two->sha1, ren2->pair->two->mode, dst_name2);
+                */
+       } else {
+               update_stages(dst_name1, NULL, ren1->pair->two, NULL, 1);
+               update_stages(dst_name2, NULL, NULL, ren2->pair->two, 1);
+       }
+       while (delp--)
+               free(del[delp]);
+}
+
+static void conflict_rename_dir(struct rename *ren1,
+                               const char *branch1)
+{
+       char *new_path = unique_path(ren1->pair->two->path, branch1);
+       output(1, "Renamed %s to %s instead", ren1->pair->one->path, new_path);
+       remove_file(0, ren1->pair->two->path, 0);
+       update_file(0, ren1->pair->two->sha1, ren1->pair->two->mode, new_path);
+       free(new_path);
+}
+
+static void conflict_rename_rename_2(struct rename *ren1,
+                                    const char *branch1,
+                                    struct rename *ren2,
+                                    const char *branch2)
+{
+       char *new_path1 = unique_path(ren1->pair->two->path, branch1);
+       char *new_path2 = unique_path(ren2->pair->two->path, branch2);
+       output(1, "Renamed %s to %s and %s to %s instead",
+              ren1->pair->one->path, new_path1,
+              ren2->pair->one->path, new_path2);
+       remove_file(0, ren1->pair->two->path, 0);
+       update_file(0, ren1->pair->two->sha1, ren1->pair->two->mode, new_path1);
+       update_file(0, ren2->pair->two->sha1, ren2->pair->two->mode, new_path2);
+       free(new_path2);
+       free(new_path1);
+}
+
+static int process_renames(struct path_list *a_renames,
+                          struct path_list *b_renames,
+                          const char *a_branch,
+                          const char *b_branch)
+{
+       int clean_merge = 1, i, j;
+       struct path_list a_by_dst = {NULL, 0, 0, 0}, b_by_dst = {NULL, 0, 0, 0};
+       const struct rename *sre;
+
+       for (i = 0; i < a_renames->nr; i++) {
+               sre = a_renames->items[i].util;
+               path_list_insert(sre->pair->two->path, &a_by_dst)->util
+                       = sre->dst_entry;
+       }
+       for (i = 0; i < b_renames->nr; i++) {
+               sre = b_renames->items[i].util;
+               path_list_insert(sre->pair->two->path, &b_by_dst)->util
+                       = sre->dst_entry;
+       }
+
+       for (i = 0, j = 0; i < a_renames->nr || j < b_renames->nr;) {
+               int compare;
+               char *src;
+               struct path_list *renames1, *renames2, *renames2Dst;
+               struct rename *ren1 = NULL, *ren2 = NULL;
+               const char *branch1, *branch2;
+               const char *ren1_src, *ren1_dst;
+
+               if (i >= a_renames->nr) {
+                       compare = 1;
+                       ren2 = b_renames->items[j++].util;
+               } else if (j >= b_renames->nr) {
+                       compare = -1;
+                       ren1 = a_renames->items[i++].util;
+               } else {
+                       compare = strcmp(a_renames->items[i].path,
+                                       b_renames->items[j].path);
+                       if (compare <= 0)
+                               ren1 = a_renames->items[i++].util;
+                       if (compare >= 0)
+                               ren2 = b_renames->items[j++].util;
+               }
+
+               /* TODO: refactor, so that 1/2 are not needed */
+               if (ren1) {
+                       renames1 = a_renames;
+                       renames2 = b_renames;
+                       renames2Dst = &b_by_dst;
+                       branch1 = a_branch;
+                       branch2 = b_branch;
+               } else {
+                       struct rename *tmp;
+                       renames1 = b_renames;
+                       renames2 = a_renames;
+                       renames2Dst = &a_by_dst;
+                       branch1 = b_branch;
+                       branch2 = a_branch;
+                       tmp = ren2;
+                       ren2 = ren1;
+                       ren1 = tmp;
+               }
+               src = ren1->pair->one->path;
+
+               ren1->dst_entry->processed = 1;
+               ren1->src_entry->processed = 1;
+
+               if (ren1->processed)
+                       continue;
+               ren1->processed = 1;
+
+               ren1_src = ren1->pair->one->path;
+               ren1_dst = ren1->pair->two->path;
+
+               if (ren2) {
+                       const char *ren2_src = ren2->pair->one->path;
+                       const char *ren2_dst = ren2->pair->two->path;
+                       /* Renamed in 1 and renamed in 2 */
+                       if (strcmp(ren1_src, ren2_src) != 0)
+                               die("ren1.src != ren2.src");
+                       ren2->dst_entry->processed = 1;
+                       ren2->processed = 1;
+                       if (strcmp(ren1_dst, ren2_dst) != 0) {
+                               clean_merge = 0;
+                               output(1, "CONFLICT (rename/rename): "
+                                      "Rename \"%s\"->\"%s\" in branch \"%s\" "
+                                      "rename \"%s\"->\"%s\" in \"%s\"%s",
+                                      src, ren1_dst, branch1,
+                                      src, ren2_dst, branch2,
+                                      index_only ? " (left unresolved)": "");
+                               if (index_only) {
+                                       remove_file_from_cache(src);
+                                       update_file(0, ren1->pair->one->sha1,
+                                                   ren1->pair->one->mode, src);
+                               }
+                               conflict_rename_rename(ren1, branch1, ren2, branch2);
+                       } else {
+                               struct merge_file_info mfi;
+                               remove_file(1, ren1_src, 1);
+                               mfi = merge_file(ren1->pair->one,
+                                                ren1->pair->two,
+                                                ren2->pair->two,
+                                                branch1,
+                                                branch2);
+                               if (mfi.merge || !mfi.clean)
+                                       output(1, "Renamed %s->%s", src, ren1_dst);
+
+                               if (mfi.merge)
+                                       output(2, "Auto-merged %s", ren1_dst);
+
+                               if (!mfi.clean) {
+                                       output(1, "CONFLICT (content): merge conflict in %s",
+                                              ren1_dst);
+                                       clean_merge = 0;
+
+                                       if (!index_only)
+                                               update_stages(ren1_dst,
+                                                             ren1->pair->one,
+                                                             ren1->pair->two,
+                                                             ren2->pair->two,
+                                                             1 /* clear */);
+                               }
+                               update_file(mfi.clean, mfi.sha, mfi.mode, ren1_dst);
+                       }
+               } else {
+                       /* Renamed in 1, maybe changed in 2 */
+                       struct path_list_item *item;
+                       /* we only use sha1 and mode of these */
+                       struct diff_filespec src_other, dst_other;
+                       int try_merge, stage = a_renames == renames1 ? 3: 2;
+
+                       remove_file(1, ren1_src, index_only || stage == 3);
+
+                       hashcpy(src_other.sha1, ren1->src_entry->stages[stage].sha);
+                       src_other.mode = ren1->src_entry->stages[stage].mode;
+                       hashcpy(dst_other.sha1, ren1->dst_entry->stages[stage].sha);
+                       dst_other.mode = ren1->dst_entry->stages[stage].mode;
+
+                       try_merge = 0;
+
+                       if (path_list_has_path(&current_directory_set, ren1_dst)) {
+                               clean_merge = 0;
+                               output(1, "CONFLICT (rename/directory): Renamed %s->%s in %s "
+                                      " directory %s added in %s",
+                                      ren1_src, ren1_dst, branch1,
+                                      ren1_dst, branch2);
+                               conflict_rename_dir(ren1, branch1);
+                       } else if (sha_eq(src_other.sha1, null_sha1)) {
+                               clean_merge = 0;
+                               output(1, "CONFLICT (rename/delete): Renamed %s->%s in %s "
+                                      "and deleted in %s",
+                                      ren1_src, ren1_dst, branch1,
+                                      branch2);
+                               update_file(0, ren1->pair->two->sha1, ren1->pair->two->mode, ren1_dst);
+                       } else if (!sha_eq(dst_other.sha1, null_sha1)) {
+                               const char *new_path;
+                               clean_merge = 0;
+                               try_merge = 1;
+                               output(1, "CONFLICT (rename/add): Renamed %s->%s in %s. "
+                                      "%s added in %s",
+                                      ren1_src, ren1_dst, branch1,
+                                      ren1_dst, branch2);
+                               new_path = unique_path(ren1_dst, branch2);
+                               output(1, "Added as %s instead", new_path);
+                               update_file(0, dst_other.sha1, dst_other.mode, new_path);
+                       } else if ((item = path_list_lookup(ren1_dst, renames2Dst))) {
+                               ren2 = item->util;
+                               clean_merge = 0;
+                               ren2->processed = 1;
+                               output(1, "CONFLICT (rename/rename): Renamed %s->%s in %s. "
+                                      "Renamed %s->%s in %s",
+                                      ren1_src, ren1_dst, branch1,
+                                      ren2->pair->one->path, ren2->pair->two->path, branch2);
+                               conflict_rename_rename_2(ren1, branch1, ren2, branch2);
+                       } else
+                               try_merge = 1;
+
+                       if (try_merge) {
+                               struct diff_filespec *o, *a, *b;
+                               struct merge_file_info mfi;
+                               src_other.path = (char *)ren1_src;
+
+                               o = ren1->pair->one;
+                               if (a_renames == renames1) {
+                                       a = ren1->pair->two;
+                                       b = &src_other;
+                               } else {
+                                       b = ren1->pair->two;
+                                       a = &src_other;
+                               }
+                               mfi = merge_file(o, a, b,
+                                               a_branch, b_branch);
+
+                               if (mfi.clean &&
+                                   sha_eq(mfi.sha, ren1->pair->two->sha1) &&
+                                   mfi.mode == ren1->pair->two->mode)
+                                       /*
+                                        * This messaged is part of
+                                        * t6022 test. If you change
+                                        * it update the test too.
+                                        */
+                                       output(3, "Skipped %s (merged same as existing)", ren1_dst);
+                               else {
+                                       if (mfi.merge || !mfi.clean)
+                                               output(1, "Renamed %s => %s", ren1_src, ren1_dst);
+                                       if (mfi.merge)
+                                               output(2, "Auto-merged %s", ren1_dst);
+                                       if (!mfi.clean) {
+                                               output(1, "CONFLICT (rename/modify): Merge conflict in %s",
+                                                      ren1_dst);
+                                               clean_merge = 0;
+
+                                               if (!index_only)
+                                                       update_stages(ren1_dst,
+                                                                     o, a, b, 1);
+                                       }
+                                       update_file(mfi.clean, mfi.sha, mfi.mode, ren1_dst);
+                               }
+                       }
+               }
+       }
+       path_list_clear(&a_by_dst, 0);
+       path_list_clear(&b_by_dst, 0);
+
+       return clean_merge;
+}
+
+static unsigned char *stage_sha(const unsigned char *sha, unsigned mode)
+{
+       return (is_null_sha1(sha) || mode == 0) ? NULL: (unsigned char *)sha;
+}
+
+/* Per entry merge function */
+static int process_entry(const char *path, struct stage_data *entry,
+                        const char *branch1,
+                        const char *branch2)
+{
+       /*
+       printf("processing entry, clean cache: %s\n", index_only ? "yes": "no");
+       print_index_entry("\tpath: ", entry);
+       */
+       int clean_merge = 1;
+       unsigned o_mode = entry->stages[1].mode;
+       unsigned a_mode = entry->stages[2].mode;
+       unsigned b_mode = entry->stages[3].mode;
+       unsigned char *o_sha = stage_sha(entry->stages[1].sha, o_mode);
+       unsigned char *a_sha = stage_sha(entry->stages[2].sha, a_mode);
+       unsigned char *b_sha = stage_sha(entry->stages[3].sha, b_mode);
+
+       if (o_sha && (!a_sha || !b_sha)) {
+               /* Case A: Deleted in one */
+               if ((!a_sha && !b_sha) ||
+                   (sha_eq(a_sha, o_sha) && !b_sha) ||
+                   (!a_sha && sha_eq(b_sha, o_sha))) {
+                       /* Deleted in both or deleted in one and
+                        * unchanged in the other */
+                       if (a_sha)
+                               output(2, "Removed %s", path);
+                       /* do not touch working file if it did not exist */
+                       remove_file(1, path, !a_sha);
+               } else {
+                       /* Deleted in one and changed in the other */
+                       clean_merge = 0;
+                       if (!a_sha) {
+                               output(1, "CONFLICT (delete/modify): %s deleted in %s "
+                                      "and modified in %s. Version %s of %s left in tree.",
+                                      path, branch1,
+                                      branch2, branch2, path);
+                               update_file(0, b_sha, b_mode, path);
+                       } else {
+                               output(1, "CONFLICT (delete/modify): %s deleted in %s "
+                                      "and modified in %s. Version %s of %s left in tree.",
+                                      path, branch2,
+                                      branch1, branch1, path);
+                               update_file(0, a_sha, a_mode, path);
+                       }
+               }
+
+       } else if ((!o_sha && a_sha && !b_sha) ||
+                  (!o_sha && !a_sha && b_sha)) {
+               /* Case B: Added in one. */
+               const char *add_branch;
+               const char *other_branch;
+               unsigned mode;
+               const unsigned char *sha;
+               const char *conf;
+
+               if (a_sha) {
+                       add_branch = branch1;
+                       other_branch = branch2;
+                       mode = a_mode;
+                       sha = a_sha;
+                       conf = "file/directory";
+               } else {
+                       add_branch = branch2;
+                       other_branch = branch1;
+                       mode = b_mode;
+                       sha = b_sha;
+                       conf = "directory/file";
+               }
+               if (path_list_has_path(&current_directory_set, path)) {
+                       const char *new_path = unique_path(path, add_branch);
+                       clean_merge = 0;
+                       output(1, "CONFLICT (%s): There is a directory with name %s in %s. "
+                              "Added %s as %s",
+                              conf, path, other_branch, path, new_path);
+                       remove_file(0, path, 0);
+                       update_file(0, sha, mode, new_path);
+               } else {
+                       output(2, "Added %s", path);
+                       update_file(1, sha, mode, path);
+               }
+       } else if (a_sha && b_sha) {
+               /* Case C: Added in both (check for same permissions) and */
+               /* case D: Modified in both, but differently. */
+               const char *reason = "content";
+               struct merge_file_info mfi;
+               struct diff_filespec o, a, b;
+
+               if (!o_sha) {
+                       reason = "add/add";
+                       o_sha = (unsigned char *)null_sha1;
+               }
+               output(2, "Auto-merged %s", path);
+               o.path = a.path = b.path = (char *)path;
+               hashcpy(o.sha1, o_sha);
+               o.mode = o_mode;
+               hashcpy(a.sha1, a_sha);
+               a.mode = a_mode;
+               hashcpy(b.sha1, b_sha);
+               b.mode = b_mode;
+
+               mfi = merge_file(&o, &a, &b,
+                                branch1, branch2);
+
+               if (mfi.clean)
+                       update_file(1, mfi.sha, mfi.mode, path);
+               else {
+                       clean_merge = 0;
+                       output(1, "CONFLICT (%s): Merge conflict in %s",
+                                       reason, path);
+
+                       if (index_only)
+                               update_file(0, mfi.sha, mfi.mode, path);
+                       else
+                               update_file_flags(mfi.sha, mfi.mode, path,
+                                             0 /* update_cache */, 1 /* update_working_directory */);
+               }
+       } else if (!o_sha && !a_sha && !b_sha) {
+               /*
+                * this entry was deleted altogether. a_mode == 0 means
+                * we had that path and want to actively remove it.
+                */
+               remove_file(1, path, !a_mode);
+       } else
+               die("Fatal merge failure, shouldn't happen.");
+
+       return clean_merge;
+}
+
+static int merge_trees(struct tree *head,
+                      struct tree *merge,
+                      struct tree *common,
+                      const char *branch1,
+                      const char *branch2,
+                      struct tree **result)
+{
+       int code, clean;
+
+       if (subtree_merge) {
+               merge = shift_tree_object(head, merge);
+               common = shift_tree_object(head, common);
+       }
+
+       if (sha_eq(common->object.sha1, merge->object.sha1)) {
+               output(0, "Already uptodate!");
+               *result = head;
+               return 1;
+       }
+
+       code = git_merge_trees(index_only, common, head, merge);
+
+       if (code != 0)
+               die("merging of trees %s and %s failed",
+                   sha1_to_hex(head->object.sha1),
+                   sha1_to_hex(merge->object.sha1));
+
+       if (unmerged_index()) {
+               struct path_list *entries, *re_head, *re_merge;
+               int i;
+               path_list_clear(&current_file_set, 1);
+               path_list_clear(&current_directory_set, 1);
+               get_files_dirs(head);
+               get_files_dirs(merge);
+
+               entries = get_unmerged();
+               re_head  = get_renames(head, common, head, merge, entries);
+               re_merge = get_renames(merge, common, head, merge, entries);
+               clean = process_renames(re_head, re_merge,
+                               branch1, branch2);
+               for (i = 0; i < entries->nr; i++) {
+                       const char *path = entries->items[i].path;
+                       struct stage_data *e = entries->items[i].util;
+                       if (!e->processed
+                               && !process_entry(path, e, branch1, branch2))
+                               clean = 0;
+               }
+
+               path_list_clear(re_merge, 0);
+               path_list_clear(re_head, 0);
+               path_list_clear(entries, 1);
+
+       }
+       else
+               clean = 1;
+
+       if (index_only)
+               *result = git_write_tree();
+
+       return clean;
+}
+
+static struct commit_list *reverse_commit_list(struct commit_list *list)
+{
+       struct commit_list *next = NULL, *current, *backup;
+       for (current = list; current; current = backup) {
+               backup = current->next;
+               current->next = next;
+               next = current;
+       }
+       return next;
+}
+
+/*
+ * Merge the commits h1 and h2, return the resulting virtual
+ * commit object and a flag indicating the cleanness of the merge.
+ */
+static int merge(struct commit *h1,
+                struct commit *h2,
+                const char *branch1,
+                const char *branch2,
+                struct commit_list *ca,
+                struct commit **result)
+{
+       struct commit_list *iter;
+       struct commit *merged_common_ancestors;
+       struct tree *mrtree = mrtree;
+       int clean;
+
+       if (show(4)) {
+               output(4, "Merging:");
+               output_commit_title(h1);
+               output_commit_title(h2);
+       }
+
+       if (!ca) {
+               ca = get_merge_bases(h1, h2, 1);
+               ca = reverse_commit_list(ca);
+       }
+
+       if (show(5)) {
+               output(5, "found %u common ancestor(s):", commit_list_count(ca));
+               for (iter = ca; iter; iter = iter->next)
+                       output_commit_title(iter->item);
+       }
+
+       merged_common_ancestors = pop_commit(&ca);
+       if (merged_common_ancestors == NULL) {
+               /* if there is no common ancestor, make an empty tree */
+               struct tree *tree = xcalloc(1, sizeof(struct tree));
+
+               tree->object.parsed = 1;
+               tree->object.type = OBJ_TREE;
+               pretend_sha1_file(NULL, 0, OBJ_TREE, tree->object.sha1);
+               merged_common_ancestors = make_virtual_commit(tree, "ancestor");
+       }
+
+       for (iter = ca; iter; iter = iter->next) {
+               call_depth++;
+               /*
+                * When the merge fails, the result contains files
+                * with conflict markers. The cleanness flag is
+                * ignored, it was never actually used, as result of
+                * merge_trees has always overwritten it: the committed
+                * "conflicts" were already resolved.
+                */
+               discard_cache();
+               merge(merged_common_ancestors, iter->item,
+                     "Temporary merge branch 1",
+                     "Temporary merge branch 2",
+                     NULL,
+                     &merged_common_ancestors);
+               call_depth--;
+
+               if (!merged_common_ancestors)
+                       die("merge returned no commit");
+       }
+
+       discard_cache();
+       if (!call_depth) {
+               read_cache();
+               index_only = 0;
+       } else
+               index_only = 1;
+
+       clean = merge_trees(h1->tree, h2->tree, merged_common_ancestors->tree,
+                           branch1, branch2, &mrtree);
+
+       if (index_only) {
+               *result = make_virtual_commit(mrtree, "merged tree");
+               commit_list_insert(h1, &(*result)->parents);
+               commit_list_insert(h2, &(*result)->parents->next);
+       }
+       flush_output();
+       return clean;
+}
+
+static const char *better_branch_name(const char *branch)
+{
+       static char githead_env[8 + 40 + 1];
+       char *name;
+
+       if (strlen(branch) != 40)
+               return branch;
+       sprintf(githead_env, "GITHEAD_%s", branch);
+       name = getenv(githead_env);
+       return name ? name : branch;
+}
+
+static struct commit *get_ref(const char *ref)
+{
+       unsigned char sha1[20];
+       struct object *object;
+
+       if (get_sha1(ref, sha1))
+               die("Could not resolve ref '%s'", ref);
+       object = deref_tag(parse_object(sha1), ref, strlen(ref));
+       if (object->type == OBJ_TREE)
+               return make_virtual_commit((struct tree*)object,
+                       better_branch_name(ref));
+       if (object->type != OBJ_COMMIT)
+               return NULL;
+       if (parse_commit((struct commit *)object))
+               die("Could not parse commit '%s'", sha1_to_hex(object->sha1));
+       return (struct commit *)object;
+}
+
+static int merge_config(const char *var, const char *value)
+{
+       if (!strcasecmp(var, "merge.verbosity")) {
+               verbosity = git_config_int(var, value);
+               return 0;
+       }
+       if (!strcasecmp(var, "diff.renamelimit")) {
+               rename_limit = git_config_int(var, value);
+               return 0;
+       }
+       return git_default_config(var, value);
+}
+
+int main(int argc, char *argv[])
+{
+       static const char *bases[20];
+       static unsigned bases_count = 0;
+       int i, clean;
+       const char *branch1, *branch2;
+       struct commit *result, *h1, *h2;
+       struct commit_list *ca = NULL;
+       struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
+       int index_fd;
+
+       if (argv[0]) {
+               int namelen = strlen(argv[0]);
+               if (8 < namelen &&
+                   !strcmp(argv[0] + namelen - 8, "-subtree"))
+                       subtree_merge = 1;
+       }
+
+       git_config(merge_config);
+       if (getenv("GIT_MERGE_VERBOSITY"))
+               verbosity = strtol(getenv("GIT_MERGE_VERBOSITY"), NULL, 10);
+
+       if (argc < 4)
+               die("Usage: %s <base>... -- <head> <remote> ...\n", argv[0]);
+
+       for (i = 1; i < argc; ++i) {
+               if (!strcmp(argv[i], "--"))
+                       break;
+               if (bases_count < sizeof(bases)/sizeof(*bases))
+                       bases[bases_count++] = argv[i];
+       }
+       if (argc - i != 3) /* "--" "<head>" "<remote>" */
+               die("Not handling anything other than two heads merge.");
+       if (verbosity >= 5)
+               buffer_output = 0;
+
+       branch1 = argv[++i];
+       branch2 = argv[++i];
+
+       h1 = get_ref(branch1);
+       h2 = get_ref(branch2);
+
+       branch1 = better_branch_name(branch1);
+       branch2 = better_branch_name(branch2);
+
+       if (show(3))
+               printf("Merging %s with %s\n", branch1, branch2);
+
+       index_fd = hold_locked_index(lock, 1);
+
+       for (i = 0; i < bases_count; i++) {
+               struct commit *ancestor = get_ref(bases[i]);
+               ca = commit_list_insert(ancestor, &ca);
+       }
+       clean = merge(h1, h2, branch1, branch2, ca, &result);
+
+       if (active_cache_changed &&
+           (write_cache(index_fd, active_cache, active_nr) ||
+            close(index_fd) || commit_locked_index(lock)))
+                       die ("unable to write %s", get_index_file());
+
+       return clean ? 0: 1;
+}
diff --git a/merge-tree.c b/merge-tree.c
new file mode 100644 (file)
index 0000000..7d4f628
--- /dev/null
@@ -0,0 +1,356 @@
+#include "cache.h"
+#include "tree-walk.h"
+#include "xdiff-interface.h"
+#include "blob.h"
+
+static const char merge_tree_usage[] = "git-merge-tree <base-tree> <branch1> <branch2>";
+static int resolve_directories = 1;
+
+struct merge_list {
+       struct merge_list *next;
+       struct merge_list *link;        /* other stages for this object */
+
+       unsigned int stage : 2,
+                    flags : 30;
+       unsigned int mode;
+       const char *path;
+       struct blob *blob;
+};
+
+static struct merge_list *merge_result, **merge_result_end = &merge_result;
+
+static void add_merge_entry(struct merge_list *entry)
+{
+       *merge_result_end = entry;
+       merge_result_end = &entry->next;
+}
+
+static void merge_trees(struct tree_desc t[3], const char *base);
+
+static const char *explanation(struct merge_list *entry)
+{
+       switch (entry->stage) {
+       case 0:
+               return "merged";
+       case 3:
+               return "added in remote";
+       case 2:
+               if (entry->link)
+                       return "added in both";
+               return "added in local";
+       }
+
+       /* Existed in base */
+       entry = entry->link;
+       if (!entry)
+               return "removed in both";
+
+       if (entry->link)
+               return "changed in both";
+
+       if (entry->stage == 3)
+               return "removed in local";
+       return "removed in remote";
+}
+
+extern void *merge_file(struct blob *, struct blob *, struct blob *, unsigned long *);
+
+static void *result(struct merge_list *entry, unsigned long *size)
+{
+       enum object_type type;
+       struct blob *base, *our, *their;
+
+       if (!entry->stage)
+               return read_sha1_file(entry->blob->object.sha1, &type, size);
+       base = NULL;
+       if (entry->stage == 1) {
+               base = entry->blob;
+               entry = entry->link;
+       }
+       our = NULL;
+       if (entry && entry->stage == 2) {
+               our = entry->blob;
+               entry = entry->link;
+       }
+       their = NULL;
+       if (entry)
+               their = entry->blob;
+       return merge_file(base, our, their, size);
+}
+
+static void *origin(struct merge_list *entry, unsigned long *size)
+{
+       enum object_type type;
+       while (entry) {
+               if (entry->stage == 2)
+                       return read_sha1_file(entry->blob->object.sha1, &type, size);
+               entry = entry->link;
+       }
+       return NULL;
+}
+
+static int show_outf(void *priv_, mmbuffer_t *mb, int nbuf)
+{
+       int i;
+       for (i = 0; i < nbuf; i++)
+               printf("%.*s", (int) mb[i].size, mb[i].ptr);
+       return 0;
+}
+
+static void show_diff(struct merge_list *entry)
+{
+       unsigned long size;
+       mmfile_t src, dst;
+       xpparam_t xpp;
+       xdemitconf_t xecfg;
+       xdemitcb_t ecb;
+
+       xpp.flags = XDF_NEED_MINIMAL;
+       memset(&xecfg, 0, sizeof(xecfg));
+       xecfg.ctxlen = 3;
+       ecb.outf = show_outf;
+       ecb.priv = NULL;
+
+       src.ptr = origin(entry, &size);
+       if (!src.ptr)
+               size = 0;
+       src.size = size;
+       dst.ptr = result(entry, &size);
+       if (!dst.ptr)
+               size = 0;
+       dst.size = size;
+       xdl_diff(&src, &dst, &xpp, &xecfg, &ecb);
+       free(src.ptr);
+       free(dst.ptr);
+}
+
+static void show_result_list(struct merge_list *entry)
+{
+       printf("%s\n", explanation(entry));
+       do {
+               struct merge_list *link = entry->link;
+               static const char *desc[4] = { "result", "base", "our", "their" };
+               printf("  %-6s %o %s %s\n", desc[entry->stage], entry->mode, sha1_to_hex(entry->blob->object.sha1), entry->path);
+               entry = link;
+       } while (entry);
+}
+
+static void show_result(void)
+{
+       struct merge_list *walk;
+
+       walk = merge_result;
+       while (walk) {
+               show_result_list(walk);
+               show_diff(walk);
+               walk = walk->next;
+       }
+}
+
+/* An empty entry never compares same, not even to another empty entry */
+static int same_entry(struct name_entry *a, struct name_entry *b)
+{
+       return  a->sha1 &&
+               b->sha1 &&
+               !hashcmp(a->sha1, b->sha1) &&
+               a->mode == b->mode;
+}
+
+static struct merge_list *create_entry(unsigned stage, unsigned mode, const unsigned char *sha1, const char *path)
+{
+       struct merge_list *res = xmalloc(sizeof(*res));
+
+       memset(res, 0, sizeof(*res));
+       res->stage = stage;
+       res->path = path;
+       res->mode = mode;
+       res->blob = lookup_blob(sha1);
+       return res;
+}
+
+static void resolve(const char *base, struct name_entry *branch1, struct name_entry *result)
+{
+       struct merge_list *orig, *final;
+       const char *path;
+
+       /* If it's already branch1, don't bother showing it */
+       if (!branch1)
+               return;
+
+       path = xstrdup(mkpath("%s%s", base, result->path));
+       orig = create_entry(2, branch1->mode, branch1->sha1, path);
+       final = create_entry(0, result->mode, result->sha1, path);
+
+       final->link = orig;
+
+       add_merge_entry(final);
+}
+
+static int unresolved_directory(const char *base, struct name_entry n[3])
+{
+       int baselen, pathlen;
+       char *newbase;
+       struct name_entry *p;
+       struct tree_desc t[3];
+       void *buf0, *buf1, *buf2;
+
+       if (!resolve_directories)
+               return 0;
+       p = n;
+       if (!p->mode) {
+               p++;
+               if (!p->mode)
+                       p++;
+       }
+       if (!S_ISDIR(p->mode))
+               return 0;
+       baselen = strlen(base);
+       pathlen = tree_entry_len(p->path, p->sha1);
+       newbase = xmalloc(baselen + pathlen + 2);
+       memcpy(newbase, base, baselen);
+       memcpy(newbase + baselen, p->path, pathlen);
+       memcpy(newbase + baselen + pathlen, "/", 2);
+
+       buf0 = fill_tree_descriptor(t+0, n[0].sha1);
+       buf1 = fill_tree_descriptor(t+1, n[1].sha1);
+       buf2 = fill_tree_descriptor(t+2, n[2].sha1);
+       merge_trees(t, newbase);
+
+       free(buf0);
+       free(buf1);
+       free(buf2);
+       free(newbase);
+       return 1;
+}
+
+
+static struct merge_list *link_entry(unsigned stage, const char *base, struct name_entry *n, struct merge_list *entry)
+{
+       const char *path;
+       struct merge_list *link;
+
+       if (!n->mode)
+               return entry;
+       if (entry)
+               path = entry->path;
+       else
+               path = xstrdup(mkpath("%s%s", base, n->path));
+       link = create_entry(stage, n->mode, n->sha1, path);
+       link->link = entry;
+       return link;
+}
+
+static void unresolved(const char *base, struct name_entry n[3])
+{
+       struct merge_list *entry = NULL;
+
+       if (unresolved_directory(base, n))
+               return;
+
+       /*
+        * Do them in reverse order so that the resulting link
+        * list has the stages in order - link_entry adds new
+        * links at the front.
+        */
+       entry = link_entry(3, base, n + 2, entry);
+       entry = link_entry(2, base, n + 1, entry);
+       entry = link_entry(1, base, n + 0, entry);
+
+       add_merge_entry(entry);
+}
+
+/*
+ * Merge two trees together (t[1] and t[2]), using a common base (t[0])
+ * as the origin.
+ *
+ * This walks the (sorted) trees in lock-step, checking every possible
+ * name. Note that directories automatically sort differently from other
+ * files (see "base_name_compare"), so you'll never see file/directory
+ * conflicts, because they won't ever compare the same.
+ *
+ * IOW, if a directory changes to a filename, it will automatically be
+ * seen as the directory going away, and the filename being created.
+ *
+ * Think of this as a three-way diff.
+ *
+ * The output will be either:
+ *  - successful merge
+ *      "0 mode sha1 filename"
+ *    NOTE NOTE NOTE! FIXME! We really really need to walk the index
+ *    in parallel with this too!
+ *
+ *  - conflict:
+ *     "1 mode sha1 filename"
+ *     "2 mode sha1 filename"
+ *     "3 mode sha1 filename"
+ *    where not all of the 1/2/3 lines may exist, of course.
+ *
+ * The successful merge rules are the same as for the three-way merge
+ * in git-read-tree.
+ */
+static void threeway_callback(int n, unsigned long mask, struct name_entry *entry, const char *base)
+{
+       /* Same in both? */
+       if (same_entry(entry+1, entry+2)) {
+               if (entry[0].sha1) {
+                       resolve(base, NULL, entry+1);
+                       return;
+               }
+       }
+
+       if (same_entry(entry+0, entry+1)) {
+               if (entry[2].sha1 && !S_ISDIR(entry[2].mode)) {
+                       resolve(base, entry+1, entry+2);
+                       return;
+               }
+       }
+
+       if (same_entry(entry+0, entry+2)) {
+               if (entry[1].sha1 && !S_ISDIR(entry[1].mode)) {
+                       resolve(base, NULL, entry+1);
+                       return;
+               }
+       }
+
+       unresolved(base, entry);
+}
+
+static void merge_trees(struct tree_desc t[3], const char *base)
+{
+       traverse_trees(3, t, base, threeway_callback);
+}
+
+static void *get_tree_descriptor(struct tree_desc *desc, const char *rev)
+{
+       unsigned char sha1[20];
+       void *buf;
+
+       if (get_sha1(rev, sha1))
+               die("unknown rev %s", rev);
+       buf = fill_tree_descriptor(desc, sha1);
+       if (!buf)
+               die("%s is not a tree", rev);
+       return buf;
+}
+
+int main(int argc, char **argv)
+{
+       struct tree_desc t[3];
+       void *buf1, *buf2, *buf3;
+
+       if (argc != 4)
+               usage(merge_tree_usage);
+
+       setup_git_directory();
+
+       buf1 = get_tree_descriptor(t+0, argv[1]);
+       buf2 = get_tree_descriptor(t+1, argv[2]);
+       buf3 = get_tree_descriptor(t+2, argv[3]);
+       merge_trees(t, "");
+       free(buf1);
+       free(buf2);
+       free(buf3);
+
+       show_result();
+       return 0;
+}
diff --git a/mktag.c b/mktag.c
new file mode 100644 (file)
index 0000000..b05260c
--- /dev/null
+++ b/mktag.c
@@ -0,0 +1,138 @@
+#include "cache.h"
+#include "tag.h"
+
+/*
+ * A signature file has a very simple fixed format: four lines
+ * of "object <sha1>" + "type <typename>" + "tag <tagname>" +
+ * "tagger <committer>", followed by a blank line, a free-form tag
+ * message and a signature block that git itself doesn't care about,
+ * but that can be verified with gpg or similar.
+ *
+ * The first three lines are guaranteed to be at least 63 bytes:
+ * "object <sha1>\n" is 48 bytes, "type tag\n" at 9 bytes is the
+ * shortest possible type-line, and "tag .\n" at 6 bytes is the
+ * shortest single-character-tag line.
+ */
+
+/*
+ * We refuse to tag something we can't verify. Just because.
+ */
+static int verify_object(unsigned char *sha1, const char *expected_type)
+{
+       int ret = -1;
+       enum object_type type;
+       unsigned long size;
+       void *buffer = read_sha1_file(sha1, &type, &size);
+
+       if (buffer) {
+               if (type == type_from_string(expected_type))
+                       ret = check_sha1_signature(sha1, buffer, size, expected_type);
+               free(buffer);
+       }
+       return ret;
+}
+
+#ifdef NO_C99_FORMAT
+#define PD_FMT "%d"
+#else
+#define PD_FMT "%td"
+#endif
+
+static int verify_tag(char *buffer, unsigned long size)
+{
+       int typelen;
+       char type[20];
+       unsigned char sha1[20];
+       const char *object, *type_line, *tag_line, *tagger_line;
+
+       if (size < 64)
+               return error("wanna fool me ? you obviously got the size wrong !");
+
+       buffer[size] = 0;
+
+       /* Verify object line */
+       object = buffer;
+       if (memcmp(object, "object ", 7))
+               return error("char%d: does not start with \"object \"", 0);
+
+       if (get_sha1_hex(object + 7, sha1))
+               return error("char%d: could not get SHA1 hash", 7);
+
+       /* Verify type line */
+       type_line = object + 48;
+       if (memcmp(type_line - 1, "\ntype ", 6))
+               return error("char%d: could not find \"\\ntype \"", 47);
+
+       /* Verify tag-line */
+       tag_line = strchr(type_line, '\n');
+       if (!tag_line)
+               return error("char" PD_FMT ": could not find next \"\\n\"", type_line - buffer);
+       tag_line++;
+       if (memcmp(tag_line, "tag ", 4) || tag_line[4] == '\n')
+               return error("char" PD_FMT ": no \"tag \" found", tag_line - buffer);
+
+       /* Get the actual type */
+       typelen = tag_line - type_line - strlen("type \n");
+       if (typelen >= sizeof(type))
+               return error("char" PD_FMT ": type too long", type_line+5 - buffer);
+
+       memcpy(type, type_line+5, typelen);
+       type[typelen] = 0;
+
+       /* Verify that the object matches */
+       if (verify_object(sha1, type))
+               return error("char%d: could not verify object %s", 7, sha1_to_hex(sha1));
+
+       /* Verify the tag-name: we don't allow control characters or spaces in it */
+       tag_line += 4;
+       for (;;) {
+               unsigned char c = *tag_line++;
+               if (c == '\n')
+                       break;
+               if (c > ' ')
+                       continue;
+               return error("char" PD_FMT ": could not verify tag name", tag_line - buffer);
+       }
+
+       /* Verify the tagger line */
+       tagger_line = tag_line;
+
+       if (memcmp(tagger_line, "tagger", 6) || (tagger_line[6] == '\n'))
+               return error("char" PD_FMT ": could not find \"tagger\"", tagger_line - buffer);
+
+       /* TODO: check for committer info + blank line? */
+       /* Also, the minimum length is probably + "tagger .", or 63+8=71 */
+
+       /* The actual stuff afterwards we don't care about.. */
+       return 0;
+}
+
+#undef PD_FMT
+
+int main(int argc, char **argv)
+{
+       struct strbuf buf;
+       unsigned char result_sha1[20];
+
+       if (argc != 1)
+               usage("git-mktag < signaturefile");
+
+       setup_git_directory();
+
+       strbuf_init(&buf, 0);
+       if (strbuf_read(&buf, 0, 4096) < 0) {
+               die("could not read from stdin");
+       }
+
+       /* Verify it for some basic sanity: it needs to start with
+          "object <sha1>\ntype\ntagger " */
+       if (verify_tag(buf.buf, buf.len) < 0)
+               die("invalid tag signature file");
+
+       if (write_sha1_file(buf.buf, buf.len, tag_type, result_sha1) < 0)
+               die("unable to write tag file");
+
+       strbuf_release(&buf);
+       printf("%s\n", sha1_to_hex(result_sha1));
+       return 0;
+}
diff --git a/mktree.c b/mktree.c
new file mode 100644 (file)
index 0000000..e0da110
--- /dev/null
+++ b/mktree.c
@@ -0,0 +1,130 @@
+/*
+ * GIT - the stupid content tracker
+ *
+ * Copyright (c) Junio C Hamano, 2006
+ */
+#include "cache.h"
+#include "quote.h"
+#include "tree.h"
+
+static struct treeent {
+       unsigned mode;
+       unsigned char sha1[20];
+       int len;
+       char name[FLEX_ARRAY];
+} **entries;
+static int alloc, used;
+
+static void append_to_tree(unsigned mode, unsigned char *sha1, char *path)
+{
+       struct treeent *ent;
+       int len = strlen(path);
+       if (strchr(path, '/'))
+               die("path %s contains slash", path);
+
+       if (alloc <= used) {
+               alloc = alloc_nr(used);
+               entries = xrealloc(entries, sizeof(*entries) * alloc);
+       }
+       ent = entries[used++] = xmalloc(sizeof(**entries) + len + 1);
+       ent->mode = mode;
+       ent->len = len;
+       hashcpy(ent->sha1, sha1);
+       memcpy(ent->name, path, len+1);
+}
+
+static int ent_compare(const void *a_, const void *b_)
+{
+       struct treeent *a = *(struct treeent **)a_;
+       struct treeent *b = *(struct treeent **)b_;
+       return base_name_compare(a->name, a->len, a->mode,
+                                b->name, b->len, b->mode);
+}
+
+static void write_tree(unsigned char *sha1)
+{
+       struct strbuf buf;
+       size_t size;
+       int i;
+
+       qsort(entries, used, sizeof(*entries), ent_compare);
+       for (size = i = 0; i < used; i++)
+               size += 32 + entries[i]->len;
+
+       strbuf_init(&buf, size);
+       for (i = 0; i < used; i++) {
+               struct treeent *ent = entries[i];
+               strbuf_addf(&buf, "%o %s%c", ent->mode, ent->name, '\0');
+               strbuf_add(&buf, ent->sha1, 20);
+       }
+
+       write_sha1_file(buf.buf, buf.len, tree_type, sha1);
+}
+
+static const char mktree_usage[] = "git-mktree [-z]";
+
+int main(int ac, char **av)
+{
+       struct strbuf sb;
+       struct strbuf p_uq;
+       unsigned char sha1[20];
+       int line_termination = '\n';
+
+       setup_git_directory();
+
+       while ((1 < ac) && av[1][0] == '-') {
+               char *arg = av[1];
+               if (!strcmp("-z", arg))
+                       line_termination = 0;
+               else
+                       usage(mktree_usage);
+               ac--;
+               av++;
+       }
+
+       strbuf_init(&sb, 0);
+       strbuf_init(&p_uq, 0);
+       while (strbuf_getline(&sb, stdin, line_termination) != EOF) {
+               char *ptr, *ntr;
+               unsigned mode;
+               enum object_type type;
+               char *path;
+
+               ptr = sb.buf;
+               /* Input is non-recursive ls-tree output format
+                * mode SP type SP sha1 TAB name
+                */
+               mode = strtoul(ptr, &ntr, 8);
+               if (ptr == ntr || !ntr || *ntr != ' ')
+                       die("input format error: %s", sb.buf);
+               ptr = ntr + 1; /* type */
+               ntr = strchr(ptr, ' ');
+               if (!ntr || sb.buf + sb.len <= ntr + 40 ||
+                   ntr[41] != '\t' ||
+                   get_sha1_hex(ntr + 1, sha1))
+                       die("input format error: %s", sb.buf);
+               type = sha1_object_info(sha1, NULL);
+               if (type < 0)
+                       die("object %s unavailable", sha1_to_hex(sha1));
+               *ntr++ = 0; /* now at the beginning of SHA1 */
+               if (type != type_from_string(ptr))
+                       die("object type %s mismatch (%s)", ptr, typename(type));
+
+               path = ntr + 41;  /* at the beginning of name */
+               if (line_termination && path[0] == '"') {
+                       strbuf_reset(&p_uq);
+                       if (unquote_c_style(&p_uq, path, NULL)) {
+                               die("invalid quoting");
+                       }
+                       path = p_uq.buf;
+               }
+
+               append_to_tree(mode, sha1, path);
+       }
+       strbuf_release(&p_uq);
+       strbuf_release(&sb);
+
+       write_tree(sha1);
+       puts(sha1_to_hex(sha1));
+       exit(0);
+}
diff --git a/mozilla-sha1/sha1.c b/mozilla-sha1/sha1.c
new file mode 100644 (file)
index 0000000..3f06b83
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is SHA 180-1 Reference Implementation (Compact version)
+ *
+ * The Initial Developer of the Original Code is Paul Kocher of
+ * Cryptography Research.  Portions created by Paul Kocher are
+ * Copyright (C) 1995-9 by Cryptography Research, Inc.  All
+ * Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ *     Paul Kocher
+ *
+ * Alternatively, the contents of this file may be used under the
+ * terms of the GNU General Public License Version 2 or later (the
+ * "GPL"), in which case the provisions of the GPL are applicable
+ * instead of those above.  If you wish to allow use of your
+ * version of this file only under the terms of the GPL and not to
+ * allow others to use your version of this file under the MPL,
+ * indicate your decision by deleting the provisions above and
+ * replace them with the notice and other provisions required by
+ * the GPL.  If you do not delete the provisions above, a recipient
+ * may use your version of this file under either the MPL or the
+ * GPL.
+ */
+
+#include "sha1.h"
+
+static void shaHashBlock(SHA_CTX *ctx);
+
+void SHA1_Init(SHA_CTX *ctx) {
+  int i;
+
+  ctx->lenW = 0;
+  ctx->sizeHi = ctx->sizeLo = 0;
+
+  /* Initialize H with the magic constants (see FIPS180 for constants)
+   */
+  ctx->H[0] = 0x67452301;
+  ctx->H[1] = 0xefcdab89;
+  ctx->H[2] = 0x98badcfe;
+  ctx->H[3] = 0x10325476;
+  ctx->H[4] = 0xc3d2e1f0;
+
+  for (i = 0; i < 80; i++)
+    ctx->W[i] = 0;
+}
+
+
+void SHA1_Update(SHA_CTX *ctx, const void *_dataIn, int len) {
+  const unsigned char *dataIn = _dataIn;
+  int i;
+
+  /* Read the data into W and process blocks as they get full
+   */
+  for (i = 0; i < len; i++) {
+    ctx->W[ctx->lenW / 4] <<= 8;
+    ctx->W[ctx->lenW / 4] |= (unsigned int)dataIn[i];
+    if ((++ctx->lenW) % 64 == 0) {
+      shaHashBlock(ctx);
+      ctx->lenW = 0;
+    }
+    ctx->sizeLo += 8;
+    ctx->sizeHi += (ctx->sizeLo < 8);
+  }
+}
+
+
+void SHA1_Final(unsigned char hashout[20], SHA_CTX *ctx) {
+  unsigned char pad0x80 = 0x80;
+  unsigned char pad0x00 = 0x00;
+  unsigned char padlen[8];
+  int i;
+
+  /* Pad with a binary 1 (e.g. 0x80), then zeroes, then length
+   */
+  padlen[0] = (unsigned char)((ctx->sizeHi >> 24) & 255);
+  padlen[1] = (unsigned char)((ctx->sizeHi >> 16) & 255);
+  padlen[2] = (unsigned char)((ctx->sizeHi >> 8) & 255);
+  padlen[3] = (unsigned char)((ctx->sizeHi >> 0) & 255);
+  padlen[4] = (unsigned char)((ctx->sizeLo >> 24) & 255);
+  padlen[5] = (unsigned char)((ctx->sizeLo >> 16) & 255);
+  padlen[6] = (unsigned char)((ctx->sizeLo >> 8) & 255);
+  padlen[7] = (unsigned char)((ctx->sizeLo >> 0) & 255);
+  SHA1_Update(ctx, &pad0x80, 1);
+  while (ctx->lenW != 56)
+    SHA1_Update(ctx, &pad0x00, 1);
+  SHA1_Update(ctx, padlen, 8);
+
+  /* Output hash
+   */
+  for (i = 0; i < 20; i++) {
+    hashout[i] = (unsigned char)(ctx->H[i / 4] >> 24);
+    ctx->H[i / 4] <<= 8;
+  }
+
+  /*
+   *  Re-initialize the context (also zeroizes contents)
+   */
+  SHA1_Init(ctx);
+}
+
+
+#define SHA_ROT(X,n) (((X) << (n)) | ((X) >> (32-(n))))
+
+static void shaHashBlock(SHA_CTX *ctx) {
+  int t;
+  unsigned int A,B,C,D,E,TEMP;
+
+  for (t = 16; t <= 79; t++)
+    ctx->W[t] =
+      SHA_ROT(ctx->W[t-3] ^ ctx->W[t-8] ^ ctx->W[t-14] ^ ctx->W[t-16], 1);
+
+  A = ctx->H[0];
+  B = ctx->H[1];
+  C = ctx->H[2];
+  D = ctx->H[3];
+  E = ctx->H[4];
+
+  for (t = 0; t <= 19; t++) {
+    TEMP = SHA_ROT(A,5) + (((C^D)&B)^D)     + E + ctx->W[t] + 0x5a827999;
+    E = D; D = C; C = SHA_ROT(B, 30); B = A; A = TEMP;
+  }
+  for (t = 20; t <= 39; t++) {
+    TEMP = SHA_ROT(A,5) + (B^C^D)           + E + ctx->W[t] + 0x6ed9eba1;
+    E = D; D = C; C = SHA_ROT(B, 30); B = A; A = TEMP;
+  }
+  for (t = 40; t <= 59; t++) {
+    TEMP = SHA_ROT(A,5) + ((B&C)|(D&(B|C))) + E + ctx->W[t] + 0x8f1bbcdc;
+    E = D; D = C; C = SHA_ROT(B, 30); B = A; A = TEMP;
+  }
+  for (t = 60; t <= 79; t++) {
+    TEMP = SHA_ROT(A,5) + (B^C^D)           + E + ctx->W[t] + 0xca62c1d6;
+    E = D; D = C; C = SHA_ROT(B, 30); B = A; A = TEMP;
+  }
+
+  ctx->H[0] += A;
+  ctx->H[1] += B;
+  ctx->H[2] += C;
+  ctx->H[3] += D;
+  ctx->H[4] += E;
+}
diff --git a/mozilla-sha1/sha1.h b/mozilla-sha1/sha1.h
new file mode 100644 (file)
index 0000000..16f2d3d
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is SHA 180-1 Header File
+ *
+ * The Initial Developer of the Original Code is Paul Kocher of
+ * Cryptography Research.  Portions created by Paul Kocher are
+ * Copyright (C) 1995-9 by Cryptography Research, Inc.  All
+ * Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ *     Paul Kocher
+ *
+ * Alternatively, the contents of this file may be used under the
+ * terms of the GNU General Public License Version 2 or later (the
+ * "GPL"), in which case the provisions of the GPL are applicable
+ * instead of those above.  If you wish to allow use of your
+ * version of this file only under the terms of the GPL and not to
+ * allow others to use your version of this file under the MPL,
+ * indicate your decision by deleting the provisions above and
+ * replace them with the notice and other provisions required by
+ * the GPL.  If you do not delete the provisions above, a recipient
+ * may use your version of this file under either the MPL or the
+ * GPL.
+ */
+
+typedef struct {
+  unsigned int H[5];
+  unsigned int W[80];
+  int lenW;
+  unsigned int sizeHi,sizeLo;
+} SHA_CTX;
+
+void SHA1_Init(SHA_CTX *ctx);
+void SHA1_Update(SHA_CTX *ctx, const void *dataIn, int len);
+void SHA1_Final(unsigned char hashout[20], SHA_CTX *ctx);
diff --git a/object-refs.c b/object-refs.c
new file mode 100644 (file)
index 0000000..5345671
--- /dev/null
@@ -0,0 +1,87 @@
+#include "cache.h"
+#include "object.h"
+#include "decorate.h"
+
+int track_object_refs = 0;
+
+static struct decoration ref_decorate;
+
+struct object_refs *lookup_object_refs(struct object *base)
+{
+       return lookup_decoration(&ref_decorate, base);
+}
+
+static void add_object_refs(struct object *obj, struct object_refs *refs)
+{
+       if (add_decoration(&ref_decorate, obj, refs))
+               die("object %s tried to add refs twice!", sha1_to_hex(obj->sha1));
+}
+
+struct object_refs *alloc_object_refs(unsigned count)
+{
+       struct object_refs *refs;
+       size_t size = sizeof(*refs) + count*sizeof(struct object *);
+
+       refs = xcalloc(1, size);
+       refs->count = count;
+       return refs;
+}
+
+static int compare_object_pointers(const void *a, const void *b)
+{
+       const struct object * const *pa = a;
+       const struct object * const *pb = b;
+       if (*pa == *pb)
+               return 0;
+       else if (*pa < *pb)
+               return -1;
+       else
+               return 1;
+}
+
+void set_object_refs(struct object *obj, struct object_refs *refs)
+{
+       unsigned int i, j;
+
+       /* Do not install empty list of references */
+       if (refs->count < 1) {
+               free(refs);
+               return;
+       }
+
+       /* Sort the list and filter out duplicates */
+       qsort(refs->ref, refs->count, sizeof(refs->ref[0]),
+             compare_object_pointers);
+       for (i = j = 1; i < refs->count; i++) {
+               if (refs->ref[i] != refs->ref[i - 1])
+                       refs->ref[j++] = refs->ref[i];
+       }
+       if (j < refs->count) {
+               /* Duplicates were found - reallocate list */
+               size_t size = sizeof(*refs) + j*sizeof(struct object *);
+               refs->count = j;
+               refs = xrealloc(refs, size);
+       }
+
+       for (i = 0; i < refs->count; i++)
+               refs->ref[i]->used = 1;
+       add_object_refs(obj, refs);
+}
+
+void mark_reachable(struct object *obj, unsigned int mask)
+{
+       const struct object_refs *refs;
+
+       if (!track_object_refs)
+               die("cannot do reachability with object refs turned off");
+       /* If we've been here already, don't bother */
+       if (obj->flags & mask)
+               return;
+       obj->flags |= mask;
+       refs = lookup_object_refs(obj);
+       if (refs) {
+               unsigned i;
+               for (i = 0; i < refs->count; i++)
+                       mark_reachable(refs->ref[i], mask);
+       }
+}
diff --git a/object.c b/object.c
new file mode 100644 (file)
index 0000000..16793d9
--- /dev/null
+++ b/object.c
@@ -0,0 +1,257 @@
+#include "cache.h"
+#include "object.h"
+#include "blob.h"
+#include "tree.h"
+#include "commit.h"
+#include "tag.h"
+
+static struct object **obj_hash;
+static int nr_objs, obj_hash_size;
+
+unsigned int get_max_object_index(void)
+{
+       return obj_hash_size;
+}
+
+struct object *get_indexed_object(unsigned int idx)
+{
+       return obj_hash[idx];
+}
+
+static const char *object_type_strings[] = {
+       NULL,           /* OBJ_NONE = 0 */
+       "commit",       /* OBJ_COMMIT = 1 */
+       "tree",         /* OBJ_TREE = 2 */
+       "blob",         /* OBJ_BLOB = 3 */
+       "tag",          /* OBJ_TAG = 4 */
+};
+
+const char *typename(unsigned int type)
+{
+       if (type >= ARRAY_SIZE(object_type_strings))
+               return NULL;
+       return object_type_strings[type];
+}
+
+int type_from_string(const char *str)
+{
+       int i;
+
+       for (i = 1; i < ARRAY_SIZE(object_type_strings); i++)
+               if (!strcmp(str, object_type_strings[i]))
+                       return i;
+       die("invalid object type \"%s\"", str);
+}
+
+static unsigned int hash_obj(struct object *obj, unsigned int n)
+{
+       unsigned int hash = *(unsigned int *)obj->sha1;
+       return hash % n;
+}
+
+static void insert_obj_hash(struct object *obj, struct object **hash, unsigned int size)
+{
+       int j = hash_obj(obj, size);
+
+       while (hash[j]) {
+               j++;
+               if (j >= size)
+                       j = 0;
+       }
+       hash[j] = obj;
+}
+
+static int hashtable_index(const unsigned char *sha1)
+{
+       unsigned int i;
+       memcpy(&i, sha1, sizeof(unsigned int));
+       return (int)(i % obj_hash_size);
+}
+
+struct object *lookup_object(const unsigned char *sha1)
+{
+       int i;
+       struct object *obj;
+
+       if (!obj_hash)
+               return NULL;
+
+       i = hashtable_index(sha1);
+       while ((obj = obj_hash[i]) != NULL) {
+               if (!hashcmp(sha1, obj->sha1))
+                       break;
+               i++;
+               if (i == obj_hash_size)
+                       i = 0;
+       }
+       return obj;
+}
+
+static void grow_object_hash(void)
+{
+       int i;
+       int new_hash_size = obj_hash_size < 32 ? 32 : 2 * obj_hash_size;
+       struct object **new_hash;
+
+       new_hash = xcalloc(new_hash_size, sizeof(struct object *));
+       for (i = 0; i < obj_hash_size; i++) {
+               struct object *obj = obj_hash[i];
+               if (!obj)
+                       continue;
+               insert_obj_hash(obj, new_hash, new_hash_size);
+       }
+       free(obj_hash);
+       obj_hash = new_hash;
+       obj_hash_size = new_hash_size;
+}
+
+void *create_object(const unsigned char *sha1, int type, void *o)
+{
+       struct object *obj = o;
+
+       obj->parsed = 0;
+       obj->used = 0;
+       obj->type = type;
+       obj->flags = 0;
+       hashcpy(obj->sha1, sha1);
+
+       if (obj_hash_size - 1 <= nr_objs * 2)
+               grow_object_hash();
+
+       insert_obj_hash(obj, obj_hash, obj_hash_size);
+       nr_objs++;
+       return obj;
+}
+
+struct object *lookup_unknown_object(const unsigned char *sha1)
+{
+       struct object *obj = lookup_object(sha1);
+       if (!obj)
+               obj = create_object(sha1, OBJ_NONE, alloc_object_node());
+       return obj;
+}
+
+struct object *parse_object_buffer(const unsigned char *sha1, enum object_type type, unsigned long size, void *buffer, int *eaten_p)
+{
+       struct object *obj;
+       int eaten = 0;
+
+       if (type == OBJ_BLOB) {
+               struct blob *blob = lookup_blob(sha1);
+               parse_blob_buffer(blob, buffer, size);
+               obj = &blob->object;
+       } else if (type == OBJ_TREE) {
+               struct tree *tree = lookup_tree(sha1);
+               obj = &tree->object;
+               if (!tree->object.parsed) {
+                       parse_tree_buffer(tree, buffer, size);
+                       eaten = 1;
+               }
+       } else if (type == OBJ_COMMIT) {
+               struct commit *commit = lookup_commit(sha1);
+               parse_commit_buffer(commit, buffer, size);
+               if (!commit->buffer) {
+                       commit->buffer = buffer;
+                       eaten = 1;
+               }
+               obj = &commit->object;
+       } else if (type == OBJ_TAG) {
+               struct tag *tag = lookup_tag(sha1);
+               parse_tag_buffer(tag, buffer, size);
+               obj = &tag->object;
+       } else {
+               warning("object %s has unknown type id %d\n", sha1_to_hex(sha1), type);
+               obj = NULL;
+       }
+       if (obj && obj->type == OBJ_NONE)
+               obj->type = type;
+       *eaten_p = eaten;
+       return obj;
+}
+
+struct object *parse_object(const unsigned char *sha1)
+{
+       unsigned long size;
+       enum object_type type;
+       int eaten;
+       void *buffer = read_sha1_file(sha1, &type, &size);
+
+       if (buffer) {
+               struct object *obj;
+               if (check_sha1_signature(sha1, buffer, size, typename(type)) < 0) {
+                       free(buffer);
+                       error("sha1 mismatch %s\n", sha1_to_hex(sha1));
+                       return NULL;
+               }
+
+               obj = parse_object_buffer(sha1, type, size, buffer, &eaten);
+               if (!eaten)
+                       free(buffer);
+               return obj;
+       }
+       return NULL;
+}
+
+struct object_list *object_list_insert(struct object *item,
+                                      struct object_list **list_p)
+{
+       struct object_list *new_list = xmalloc(sizeof(struct object_list));
+        new_list->item = item;
+        new_list->next = *list_p;
+        *list_p = new_list;
+        return new_list;
+}
+
+void object_list_append(struct object *item,
+                       struct object_list **list_p)
+{
+       while (*list_p) {
+               list_p = &((*list_p)->next);
+       }
+       *list_p = xmalloc(sizeof(struct object_list));
+       (*list_p)->next = NULL;
+       (*list_p)->item = item;
+}
+
+unsigned object_list_length(struct object_list *list)
+{
+       unsigned ret = 0;
+       while (list) {
+               list = list->next;
+               ret++;
+       }
+       return ret;
+}
+
+int object_list_contains(struct object_list *list, struct object *obj)
+{
+       while (list) {
+               if (list->item == obj)
+                       return 1;
+               list = list->next;
+       }
+       return 0;
+}
+
+void add_object_array(struct object *obj, const char *name, struct object_array *array)
+{
+       add_object_array_with_mode(obj, name, array, S_IFINVALID);
+}
+
+void add_object_array_with_mode(struct object *obj, const char *name, struct object_array *array, unsigned mode)
+{
+       unsigned nr = array->nr;
+       unsigned alloc = array->alloc;
+       struct object_array_entry *objects = array->objects;
+
+       if (nr >= alloc) {
+               alloc = (alloc + 32) * 2;
+               objects = xrealloc(objects, alloc * sizeof(*objects));
+               array->alloc = alloc;
+               array->objects = objects;
+       }
+       objects[nr].item = obj;
+       objects[nr].name = name;
+       objects[nr].mode = mode;
+       array->nr = ++nr;
+}
diff --git a/object.h b/object.h
new file mode 100644 (file)
index 0000000..397bbfa
--- /dev/null
+++ b/object.h
@@ -0,0 +1,83 @@
+#ifndef OBJECT_H
+#define OBJECT_H
+
+struct object_list {
+       struct object *item;
+       struct object_list *next;
+};
+
+struct object_refs {
+       unsigned count;
+       struct object *ref[FLEX_ARRAY]; /* more */
+};
+
+struct object_array {
+       unsigned int nr;
+       unsigned int alloc;
+       struct object_array_entry {
+               struct object *item;
+               const char *name;
+               unsigned mode;
+       } *objects;
+};
+
+#define TYPE_BITS   3
+#define FLAG_BITS  27
+
+/*
+ * The object type is stored in 3 bits.
+ */
+struct object {
+       unsigned parsed : 1;
+       unsigned used : 1;
+       unsigned type : TYPE_BITS;
+       unsigned flags : FLAG_BITS;
+       unsigned char sha1[20];
+};
+
+extern int track_object_refs;
+
+extern const char *typename(unsigned int type);
+extern int type_from_string(const char *str);
+
+extern unsigned int get_max_object_index(void);
+extern struct object *get_indexed_object(unsigned int);
+extern struct object_refs *lookup_object_refs(struct object *);
+
+/** Internal only **/
+struct object *lookup_object(const unsigned char *sha1);
+
+extern void *create_object(const unsigned char *sha1, int type, void *obj);
+
+/** Returns the object, having parsed it to find out what it is. **/
+struct object *parse_object(const unsigned char *sha1);
+
+/* Given the result of read_sha1_file(), returns the object after
+ * parsing it.  eaten_p indicates if the object has a borrowed copy
+ * of buffer and the caller should not free() it.
+ */
+struct object *parse_object_buffer(const unsigned char *sha1, enum object_type type, unsigned long size, void *buffer, int *eaten_p);
+
+/** Returns the object, with potentially excess memory allocated. **/
+struct object *lookup_unknown_object(const unsigned  char *sha1);
+
+struct object_refs *alloc_object_refs(unsigned count);
+void set_object_refs(struct object *obj, struct object_refs *refs);
+
+void mark_reachable(struct object *obj, unsigned int mask);
+
+struct object_list *object_list_insert(struct object *item,
+                                      struct object_list **list_p);
+
+void object_list_append(struct object *item,
+                       struct object_list **list_p);
+
+unsigned object_list_length(struct object_list *list);
+
+int object_list_contains(struct object_list *list, struct object *obj);
+
+/* Object array handling .. */
+void add_object_array(struct object *obj, const char *name, struct object_array *array);
+void add_object_array_with_mode(struct object *obj, const char *name, struct object_array *array, unsigned mode);
+
+#endif /* OBJECT_H */
diff --git a/pack-check.c b/pack-check.c
new file mode 100644 (file)
index 0000000..d7dd62b
--- /dev/null
@@ -0,0 +1,191 @@
+#include "cache.h"
+#include "pack.h"
+
+struct idx_entry
+{
+       const unsigned char *sha1;
+       off_t                offset;
+};
+
+static int compare_entries(const void *e1, const void *e2)
+{
+       const struct idx_entry *entry1 = e1;
+       const struct idx_entry *entry2 = e2;
+       if (entry1->offset < entry2->offset)
+               return -1;
+       if (entry1->offset > entry2->offset)
+               return 1;
+       return 0;
+}
+
+static int verify_packfile(struct packed_git *p,
+               struct pack_window **w_curs)
+{
+       off_t index_size = p->index_size;
+       const unsigned char *index_base = p->index_data;
+       SHA_CTX ctx;
+       unsigned char sha1[20];
+       off_t offset = 0, pack_sig = p->pack_size - 20;
+       uint32_t nr_objects, i;
+       int err;
+       struct idx_entry *entries;
+
+       /* Note that the pack header checks are actually performed by
+        * use_pack when it first opens the pack file.  If anything
+        * goes wrong during those checks then the call will die out
+        * immediately.
+        */
+
+       SHA1_Init(&ctx);
+       while (offset < pack_sig) {
+               unsigned int remaining;
+               unsigned char *in = use_pack(p, w_curs, offset, &remaining);
+               offset += remaining;
+               if (offset > pack_sig)
+                       remaining -= (unsigned int)(offset - pack_sig);
+               SHA1_Update(&ctx, in, remaining);
+       }
+       SHA1_Final(sha1, &ctx);
+       if (hashcmp(sha1, use_pack(p, w_curs, pack_sig, NULL)))
+               return error("Packfile %s SHA1 mismatch with itself",
+                            p->pack_name);
+       if (hashcmp(sha1, index_base + index_size - 40))
+               return error("Packfile %s SHA1 mismatch with idx",
+                            p->pack_name);
+       unuse_pack(w_curs);
+
+       /* Make sure everything reachable from idx is valid.  Since we
+        * have verified that nr_objects matches between idx and pack,
+        * we do not do scan-streaming check on the pack file.
+        */
+       nr_objects = p->num_objects;
+       entries = xmalloc(nr_objects * sizeof(*entries));
+       /* first sort entries by pack offset, since unpacking them is more efficient that way */
+       for (i = 0; i < nr_objects; i++) {
+               entries[i].sha1 = nth_packed_object_sha1(p, i);
+               if (!entries[i].sha1)
+                       die("internal error pack-check nth-packed-object");
+               entries[i].offset = find_pack_entry_one(entries[i].sha1, p);
+               if (!entries[i].offset)
+                       die("internal error pack-check find-pack-entry-one");
+       }
+       qsort(entries, nr_objects, sizeof(*entries), compare_entries);
+
+       for (i = 0, err = 0; i < nr_objects; i++) {
+               void *data;
+               enum object_type type;
+               unsigned long size;
+
+               data = unpack_entry(p, entries[i].offset, &type, &size);
+               if (!data) {
+                       err = error("cannot unpack %s from %s",
+                                   sha1_to_hex(entries[i].sha1), p->pack_name);
+                       continue;
+               }
+               if (check_sha1_signature(entries[i].sha1, data, size, typename(type))) {
+                       err = error("packed %s from %s is corrupt",
+                                   sha1_to_hex(entries[i].sha1), p->pack_name);
+                       free(data);
+                       continue;
+               }
+               free(data);
+       }
+       free(entries);
+
+       return err;
+}
+
+
+#define MAX_CHAIN 50
+
+static void show_pack_info(struct packed_git *p)
+{
+       uint32_t nr_objects, i, chain_histogram[MAX_CHAIN+1];
+       nr_objects = p->num_objects;
+       memset(chain_histogram, 0, sizeof(chain_histogram));
+
+       for (i = 0; i < nr_objects; i++) {
+               const unsigned char *sha1;
+               unsigned char base_sha1[20];
+               const char *type;
+               unsigned long size;
+               unsigned long store_size;
+               off_t offset;
+               unsigned int delta_chain_length;
+
+               sha1 = nth_packed_object_sha1(p, i);
+               if (!sha1)
+                       die("internal error pack-check nth-packed-object");
+               offset = find_pack_entry_one(sha1, p);
+               if (!offset)
+                       die("internal error pack-check find-pack-entry-one");
+
+               type = packed_object_info_detail(p, offset, &size, &store_size,
+                                                &delta_chain_length,
+                                                base_sha1);
+               printf("%s ", sha1_to_hex(sha1));
+               if (!delta_chain_length)
+                       printf("%-6s %lu %"PRIuMAX"\n",
+                              type, size, (uintmax_t)offset);
+               else {
+                       printf("%-6s %lu %"PRIuMAX" %u %s\n",
+                              type, size, (uintmax_t)offset,
+                              delta_chain_length, sha1_to_hex(base_sha1));
+                       if (delta_chain_length <= MAX_CHAIN)
+                               chain_histogram[delta_chain_length]++;
+                       else
+                               chain_histogram[0]++;
+               }
+       }
+
+       for (i = 0; i <= MAX_CHAIN; i++) {
+               if (!chain_histogram[i])
+                       continue;
+               printf("chain length = %d: %d object%s\n", i,
+                      chain_histogram[i], chain_histogram[i] > 1 ? "s" : "");
+       }
+       if (chain_histogram[0])
+               printf("chain length > %d: %d object%s\n", MAX_CHAIN,
+                      chain_histogram[0], chain_histogram[0] > 1 ? "s" : "");
+}
+
+int verify_pack(struct packed_git *p, int verbose)
+{
+       off_t index_size;
+       const unsigned char *index_base;
+       SHA_CTX ctx;
+       unsigned char sha1[20];
+       int ret;
+
+       if (open_pack_index(p))
+               return error("packfile %s index not opened", p->pack_name);
+       index_size = p->index_size;
+       index_base = p->index_data;
+
+       ret = 0;
+       /* Verify SHA1 sum of the index file */
+       SHA1_Init(&ctx);
+       SHA1_Update(&ctx, index_base, (unsigned int)(index_size - 20));
+       SHA1_Final(sha1, &ctx);
+       if (hashcmp(sha1, index_base + index_size - 20))
+               ret = error("Packfile index for %s SHA1 mismatch",
+                           p->pack_name);
+
+       if (!ret) {
+               /* Verify pack file */
+               struct pack_window *w_curs = NULL;
+               ret = verify_packfile(p, &w_curs);
+               unuse_pack(&w_curs);
+       }
+
+       if (verbose) {
+               if (ret)
+                       printf("%s: bad\n", p->pack_name);
+               else {
+                       show_pack_info(p);
+                       printf("%s: ok\n", p->pack_name);
+               }
+       }
+
+       return ret;
+}
diff --git a/pack-redundant.c b/pack-redundant.c
new file mode 100644 (file)
index 0000000..f5cd0ac
--- /dev/null
@@ -0,0 +1,695 @@
+/*
+*
+* Copyright 2005, Lukas Sandstrom <lukass@etek.chalmers.se>
+*
+* This file is licensed under the GPL v2.
+*
+*/
+
+#include "cache.h"
+
+#define BLKSIZE 512
+
+static const char pack_redundant_usage[] =
+"git-pack-redundant [ --verbose ] [ --alt-odb ] < --all | <.pack filename> ...>";
+
+static int load_all_packs, verbose, alt_odb;
+
+struct llist_item {
+       struct llist_item *next;
+       const unsigned char *sha1;
+};
+static struct llist {
+       struct llist_item *front;
+       struct llist_item *back;
+       size_t size;
+} *all_objects; /* all objects which must be present in local packfiles */
+
+static struct pack_list {
+       struct pack_list *next;
+       struct packed_git *pack;
+       struct llist *unique_objects;
+       struct llist *all_objects;
+} *local_packs = NULL, *altodb_packs = NULL;
+
+struct pll {
+       struct pll *next;
+       struct pack_list *pl;
+};
+
+static struct llist_item *free_nodes;
+
+static inline void llist_item_put(struct llist_item *item)
+{
+       item->next = free_nodes;
+       free_nodes = item;
+}
+
+static inline struct llist_item *llist_item_get(void)
+{
+       struct llist_item *new;
+       if ( free_nodes ) {
+               new = free_nodes;
+               free_nodes = free_nodes->next;
+       } else {
+               int i = 1;
+               new = xmalloc(sizeof(struct llist_item) * BLKSIZE);
+               for(;i < BLKSIZE; i++) {
+                       llist_item_put(&new[i]);
+               }
+       }
+       return new;
+}
+
+static void llist_free(struct llist *list)
+{
+       while((list->back = list->front)) {
+               list->front = list->front->next;
+               llist_item_put(list->back);
+       }
+       free(list);
+}
+
+static inline void llist_init(struct llist **list)
+{
+       *list = xmalloc(sizeof(struct llist));
+       (*list)->front = (*list)->back = NULL;
+       (*list)->size = 0;
+}
+
+static struct llist * llist_copy(struct llist *list)
+{
+       struct llist *ret;
+       struct llist_item *new, *old, *prev;
+
+       llist_init(&ret);
+
+       if ((ret->size = list->size) == 0)
+               return ret;
+
+       new = ret->front = llist_item_get();
+       new->sha1 = list->front->sha1;
+
+       old = list->front->next;
+       while (old) {
+               prev = new;
+               new = llist_item_get();
+               prev->next = new;
+               new->sha1 = old->sha1;
+               old = old->next;
+       }
+       new->next = NULL;
+       ret->back = new;
+
+       return ret;
+}
+
+static inline struct llist_item *llist_insert(struct llist *list,
+                                             struct llist_item *after,
+                                              const unsigned char *sha1)
+{
+       struct llist_item *new = llist_item_get();
+       new->sha1 = sha1;
+       new->next = NULL;
+
+       if (after != NULL) {
+               new->next = after->next;
+               after->next = new;
+               if (after == list->back)
+                       list->back = new;
+       } else {/* insert in front */
+               if (list->size == 0)
+                       list->back = new;
+               else
+                       new->next = list->front;
+               list->front = new;
+       }
+       list->size++;
+       return new;
+}
+
+static inline struct llist_item *llist_insert_back(struct llist *list,
+                                                  const unsigned char *sha1)
+{
+       return llist_insert(list, list->back, sha1);
+}
+
+static inline struct llist_item *llist_insert_sorted_unique(struct llist *list,
+                       const unsigned char *sha1, struct llist_item *hint)
+{
+       struct llist_item *prev = NULL, *l;
+
+       l = (hint == NULL) ? list->front : hint;
+       while (l) {
+               int cmp = hashcmp(l->sha1, sha1);
+               if (cmp > 0) { /* we insert before this entry */
+                       return llist_insert(list, prev, sha1);
+               }
+               if(!cmp) { /* already exists */
+                       return l;
+               }
+               prev = l;
+               l = l->next;
+       }
+       /* insert at the end */
+       return llist_insert_back(list, sha1);
+}
+
+/* returns a pointer to an item in front of sha1 */
+static inline struct llist_item * llist_sorted_remove(struct llist *list, const unsigned char *sha1, struct llist_item *hint)
+{
+       struct llist_item *prev, *l;
+
+redo_from_start:
+       l = (hint == NULL) ? list->front : hint;
+       prev = NULL;
+       while (l) {
+               int cmp = hashcmp(l->sha1, sha1);
+               if (cmp > 0) /* not in list, since sorted */
+                       return prev;
+               if(!cmp) { /* found */
+                       if (prev == NULL) {
+                               if (hint != NULL && hint != list->front) {
+                                       /* we don't know the previous element */
+                                       hint = NULL;
+                                       goto redo_from_start;
+                               }
+                               list->front = l->next;
+                       } else
+                               prev->next = l->next;
+                       if (l == list->back)
+                               list->back = prev;
+                       llist_item_put(l);
+                       list->size--;
+                       return prev;
+               }
+               prev = l;
+               l = l->next;
+       }
+       return prev;
+}
+
+/* computes A\B */
+static void llist_sorted_difference_inplace(struct llist *A,
+                                    struct llist *B)
+{
+       struct llist_item *hint, *b;
+
+       hint = NULL;
+       b = B->front;
+
+       while (b) {
+               hint = llist_sorted_remove(A, b->sha1, hint);
+               b = b->next;
+       }
+}
+
+static inline struct pack_list * pack_list_insert(struct pack_list **pl,
+                                          struct pack_list *entry)
+{
+       struct pack_list *p = xmalloc(sizeof(struct pack_list));
+       memcpy(p, entry, sizeof(struct pack_list));
+       p->next = *pl;
+       *pl = p;
+       return p;
+}
+
+static inline size_t pack_list_size(struct pack_list *pl)
+{
+       size_t ret = 0;
+       while(pl) {
+               ret++;
+               pl = pl->next;
+       }
+       return ret;
+}
+
+static struct pack_list * pack_list_difference(const struct pack_list *A,
+                                              const struct pack_list *B)
+{
+       struct pack_list *ret;
+       const struct pack_list *pl;
+
+       if (A == NULL)
+               return NULL;
+
+       pl = B;
+       while (pl != NULL) {
+               if (A->pack == pl->pack)
+                       return pack_list_difference(A->next, B);
+               pl = pl->next;
+       }
+       ret = xmalloc(sizeof(struct pack_list));
+       memcpy(ret, A, sizeof(struct pack_list));
+       ret->next = pack_list_difference(A->next, B);
+       return ret;
+}
+
+static void cmp_two_packs(struct pack_list *p1, struct pack_list *p2)
+{
+       unsigned long p1_off = 0, p2_off = 0, p1_step, p2_step;
+       const unsigned char *p1_base, *p2_base;
+       struct llist_item *p1_hint = NULL, *p2_hint = NULL;
+
+       p1_base = p1->pack->index_data;
+       p2_base = p2->pack->index_data;
+       p1_base += 256 * 4 + ((p1->pack->index_version < 2) ? 4 : 8);
+       p2_base += 256 * 4 + ((p2->pack->index_version < 2) ? 4 : 8);
+       p1_step = (p1->pack->index_version < 2) ? 24 : 20;
+       p2_step = (p2->pack->index_version < 2) ? 24 : 20;
+
+       while (p1_off < p1->pack->num_objects * p1_step &&
+              p2_off < p2->pack->num_objects * p2_step)
+       {
+               int cmp = hashcmp(p1_base + p1_off, p2_base + p2_off);
+               /* cmp ~ p1 - p2 */
+               if (cmp == 0) {
+                       p1_hint = llist_sorted_remove(p1->unique_objects,
+                                       p1_base + p1_off, p1_hint);
+                       p2_hint = llist_sorted_remove(p2->unique_objects,
+                                       p1_base + p1_off, p2_hint);
+                       p1_off += p1_step;
+                       p2_off += p2_step;
+                       continue;
+               }
+               if (cmp < 0) { /* p1 has the object, p2 doesn't */
+                       p1_off += p1_step;
+               } else { /* p2 has the object, p1 doesn't */
+                       p2_off += p2_step;
+               }
+       }
+}
+
+static void pll_free(struct pll *l)
+{
+       struct pll *old;
+       struct pack_list *opl;
+
+       while (l) {
+               old = l;
+               while (l->pl) {
+                       opl = l->pl;
+                       l->pl = opl->next;
+                       free(opl);
+               }
+               l = l->next;
+               free(old);
+       }
+}
+
+/* all the permutations have to be free()d at the same time,
+ * since they refer to each other
+ */
+static struct pll * get_permutations(struct pack_list *list, int n)
+{
+       struct pll *subset, *ret = NULL, *new_pll = NULL, *pll;
+
+       if (list == NULL || pack_list_size(list) < n || n == 0)
+               return NULL;
+
+       if (n == 1) {
+               while (list) {
+                       new_pll = xmalloc(sizeof(pll));
+                       new_pll->pl = NULL;
+                       pack_list_insert(&new_pll->pl, list);
+                       new_pll->next = ret;
+                       ret = new_pll;
+                       list = list->next;
+               }
+               return ret;
+       }
+
+       while (list->next) {
+               subset = get_permutations(list->next, n - 1);
+               while (subset) {
+                       new_pll = xmalloc(sizeof(pll));
+                       new_pll->pl = subset->pl;
+                       pack_list_insert(&new_pll->pl, list);
+                       new_pll->next = ret;
+                       ret = new_pll;
+                       subset = subset->next;
+               }
+               list = list->next;
+       }
+       return ret;
+}
+
+static int is_superset(struct pack_list *pl, struct llist *list)
+{
+       struct llist *diff;
+
+       diff = llist_copy(list);
+
+       while (pl) {
+               llist_sorted_difference_inplace(diff, pl->all_objects);
+               if (diff->size == 0) { /* we're done */
+                       llist_free(diff);
+                       return 1;
+               }
+               pl = pl->next;
+       }
+       llist_free(diff);
+       return 0;
+}
+
+static size_t sizeof_union(struct packed_git *p1, struct packed_git *p2)
+{
+       size_t ret = 0;
+       unsigned long p1_off = 0, p2_off = 0, p1_step, p2_step;
+       const unsigned char *p1_base, *p2_base;
+
+       p1_base = p1->index_data;
+       p2_base = p2->index_data;
+       p1_base += 256 * 4 + ((p1->index_version < 2) ? 4 : 8);
+       p2_base += 256 * 4 + ((p2->index_version < 2) ? 4 : 8);
+       p1_step = (p1->index_version < 2) ? 24 : 20;
+       p2_step = (p2->index_version < 2) ? 24 : 20;
+
+       while (p1_off < p1->num_objects * p1_step &&
+              p2_off < p2->num_objects * p2_step)
+       {
+               int cmp = hashcmp(p1_base + p1_off, p2_base + p2_off);
+               /* cmp ~ p1 - p2 */
+               if (cmp == 0) {
+                       ret++;
+                       p1_off += p1_step;
+                       p2_off += p2_step;
+                       continue;
+               }
+               if (cmp < 0) { /* p1 has the object, p2 doesn't */
+                       p1_off += p1_step;
+               } else { /* p2 has the object, p1 doesn't */
+                       p2_off += p2_step;
+               }
+       }
+       return ret;
+}
+
+/* another O(n^2) function ... */
+static size_t get_pack_redundancy(struct pack_list *pl)
+{
+       struct pack_list *subset;
+       size_t ret = 0;
+
+       if (pl == NULL)
+               return 0;
+
+       while ((subset = pl->next)) {
+               while(subset) {
+                       ret += sizeof_union(pl->pack, subset->pack);
+                       subset = subset->next;
+               }
+               pl = pl->next;
+       }
+       return ret;
+}
+
+static inline off_t pack_set_bytecount(struct pack_list *pl)
+{
+       off_t ret = 0;
+       while (pl) {
+               ret += pl->pack->pack_size;
+               ret += pl->pack->index_size;
+               pl = pl->next;
+       }
+       return ret;
+}
+
+static void minimize(struct pack_list **min)
+{
+       struct pack_list *pl, *unique = NULL,
+               *non_unique = NULL, *min_perm = NULL;
+       struct pll *perm, *perm_all, *perm_ok = NULL, *new_perm;
+       struct llist *missing;
+       off_t min_perm_size = 0, perm_size;
+       int n;
+
+       pl = local_packs;
+       while (pl) {
+               if(pl->unique_objects->size)
+                       pack_list_insert(&unique, pl);
+               else
+                       pack_list_insert(&non_unique, pl);
+               pl = pl->next;
+       }
+       /* find out which objects are missing from the set of unique packs */
+       missing = llist_copy(all_objects);
+       pl = unique;
+       while (pl) {
+               llist_sorted_difference_inplace(missing, pl->all_objects);
+               pl = pl->next;
+       }
+
+       /* return if there are no objects missing from the unique set */
+       if (missing->size == 0) {
+               *min = unique;
+               return;
+       }
+
+       /* find the permutations which contain all missing objects */
+       for (n = 1; n <= pack_list_size(non_unique) && !perm_ok; n++) {
+               perm_all = perm = get_permutations(non_unique, n);
+               while (perm) {
+                       if (is_superset(perm->pl, missing)) {
+                               new_perm = xmalloc(sizeof(struct pll));
+                               memcpy(new_perm, perm, sizeof(struct pll));
+                               new_perm->next = perm_ok;
+                               perm_ok = new_perm;
+                       }
+                       perm = perm->next;
+               }
+               if (perm_ok)
+                       break;
+               pll_free(perm_all);
+       }
+       if (perm_ok == NULL)
+               die("Internal error: No complete sets found!\n");
+
+       /* find the permutation with the smallest size */
+       perm = perm_ok;
+       while (perm) {
+               perm_size = pack_set_bytecount(perm->pl);
+               if (!min_perm_size || min_perm_size > perm_size) {
+                       min_perm_size = perm_size;
+                       min_perm = perm->pl;
+               }
+               perm = perm->next;
+       }
+       *min = min_perm;
+       /* add the unique packs to the list */
+       pl = unique;
+       while(pl) {
+               pack_list_insert(min, pl);
+               pl = pl->next;
+       }
+}
+
+static void load_all_objects(void)
+{
+       struct pack_list *pl = local_packs;
+       struct llist_item *hint, *l;
+
+       llist_init(&all_objects);
+
+       while (pl) {
+               hint = NULL;
+               l = pl->all_objects->front;
+               while (l) {
+                       hint = llist_insert_sorted_unique(all_objects,
+                                                         l->sha1, hint);
+                       l = l->next;
+               }
+               pl = pl->next;
+       }
+       /* remove objects present in remote packs */
+       pl = altodb_packs;
+       while (pl) {
+               llist_sorted_difference_inplace(all_objects, pl->all_objects);
+               pl = pl->next;
+       }
+}
+
+/* this scales like O(n^2) */
+static void cmp_local_packs(void)
+{
+       struct pack_list *subset, *pl = local_packs;
+
+       while ((subset = pl)) {
+               while((subset = subset->next))
+                       cmp_two_packs(pl, subset);
+               pl = pl->next;
+       }
+}
+
+static void scan_alt_odb_packs(void)
+{
+       struct pack_list *local, *alt;
+
+       alt = altodb_packs;
+       while (alt) {
+               local = local_packs;
+               while (local) {
+                       llist_sorted_difference_inplace(local->unique_objects,
+                                                       alt->all_objects);
+                       local = local->next;
+               }
+               llist_sorted_difference_inplace(all_objects, alt->all_objects);
+               alt = alt->next;
+       }
+}
+
+static struct pack_list * add_pack(struct packed_git *p)
+{
+       struct pack_list l;
+       unsigned long off = 0, step;
+       const unsigned char *base;
+
+       if (!p->pack_local && !(alt_odb || verbose))
+               return NULL;
+
+       l.pack = p;
+       llist_init(&l.all_objects);
+
+       if (open_pack_index(p))
+               return NULL;
+
+       base = p->index_data;
+       base += 256 * 4 + ((p->index_version < 2) ? 4 : 8);
+       step = (p->index_version < 2) ? 24 : 20;
+       while (off < p->num_objects * step) {
+               llist_insert_back(l.all_objects, base + off);
+               off += step;
+       }
+       /* this list will be pruned in cmp_two_packs later */
+       l.unique_objects = llist_copy(l.all_objects);
+       if (p->pack_local)
+               return pack_list_insert(&local_packs, &l);
+       else
+               return pack_list_insert(&altodb_packs, &l);
+}
+
+static struct pack_list * add_pack_file(char *filename)
+{
+       struct packed_git *p = packed_git;
+
+       if (strlen(filename) < 40)
+               die("Bad pack filename: %s\n", filename);
+
+       while (p) {
+               if (strstr(p->pack_name, filename))
+                       return add_pack(p);
+               p = p->next;
+       }
+       die("Filename %s not found in packed_git\n", filename);
+}
+
+static void load_all(void)
+{
+       struct packed_git *p = packed_git;
+
+       while (p) {
+               add_pack(p);
+               p = p->next;
+       }
+}
+
+int main(int argc, char **argv)
+{
+       int i;
+       struct pack_list *min, *red, *pl;
+       struct llist *ignore;
+       unsigned char *sha1;
+       char buf[42]; /* 40 byte sha1 + \n + \0 */
+
+       setup_git_directory();
+
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if(!strcmp(arg, "--")) {
+                       i++;
+                       break;
+               }
+               if(!strcmp(arg, "--all")) {
+                       load_all_packs = 1;
+                       continue;
+               }
+               if(!strcmp(arg, "--verbose")) {
+                       verbose = 1;
+                       continue;
+               }
+               if(!strcmp(arg, "--alt-odb")) {
+                       alt_odb = 1;
+                       continue;
+               }
+               if(*arg == '-')
+                       usage(pack_redundant_usage);
+               else
+                       break;
+       }
+
+       prepare_packed_git();
+
+       if (load_all_packs)
+               load_all();
+       else
+               while (*(argv + i) != NULL)
+                       add_pack_file(*(argv + i++));
+
+       if (local_packs == NULL)
+               die("Zero packs found!\n");
+
+       load_all_objects();
+
+       cmp_local_packs();
+       if (alt_odb)
+               scan_alt_odb_packs();
+
+       /* ignore objects given on stdin */
+       llist_init(&ignore);
+       if (!isatty(0)) {
+               while (fgets(buf, sizeof(buf), stdin)) {
+                       sha1 = xmalloc(20);
+                       if (get_sha1_hex(buf, sha1))
+                               die("Bad sha1 on stdin: %s", buf);
+                       llist_insert_sorted_unique(ignore, sha1, NULL);
+               }
+       }
+       llist_sorted_difference_inplace(all_objects, ignore);
+       pl = local_packs;
+       while (pl) {
+               llist_sorted_difference_inplace(pl->unique_objects, ignore);
+               pl = pl->next;
+       }
+
+       minimize(&min);
+
+       if (verbose) {
+               fprintf(stderr, "There are %lu packs available in alt-odbs.\n",
+                       (unsigned long)pack_list_size(altodb_packs));
+               fprintf(stderr, "The smallest (bytewise) set of packs is:\n");
+               pl = min;
+               while (pl) {
+                       fprintf(stderr, "\t%s\n", pl->pack->pack_name);
+                       pl = pl->next;
+               }
+               fprintf(stderr, "containing %lu duplicate objects "
+                               "with a total size of %lukb.\n",
+                       (unsigned long)get_pack_redundancy(min),
+                       (unsigned long)pack_set_bytecount(min)/1024);
+               fprintf(stderr, "A total of %lu unique objects were considered.\n",
+                       (unsigned long)all_objects->size);
+               fprintf(stderr, "Redundant packs (with indexes):\n");
+       }
+       pl = red = pack_list_difference(local_packs, min);
+       while (pl) {
+               printf("%s\n%s\n",
+                      sha1_pack_index_name(pl->pack->sha1),
+                      pl->pack->pack_name);
+               pl = pl->next;
+       }
+       if (verbose)
+               fprintf(stderr, "%luMB of redundant packs in total.\n",
+                       (unsigned long)pack_set_bytecount(red)/(1024*1024));
+
+       return 0;
+}
diff --git a/pack-write.c b/pack-write.c
new file mode 100644 (file)
index 0000000..665e2b2
--- /dev/null
@@ -0,0 +1,208 @@
+#include "cache.h"
+#include "pack.h"
+#include "csum-file.h"
+
+uint32_t pack_idx_default_version = 1;
+uint32_t pack_idx_off32_limit = 0x7fffffff;
+
+static int sha1_compare(const void *_a, const void *_b)
+{
+       struct pack_idx_entry *a = *(struct pack_idx_entry **)_a;
+       struct pack_idx_entry *b = *(struct pack_idx_entry **)_b;
+       return hashcmp(a->sha1, b->sha1);
+}
+
+/*
+ * On entry *sha1 contains the pack content SHA1 hash, on exit it is
+ * the SHA1 hash of sorted object names. The objects array passed in
+ * will be sorted by SHA1 on exit.
+ */
+char *write_idx_file(char *index_name, struct pack_idx_entry **objects,
+                    int nr_objects, unsigned char *sha1)
+{
+       struct sha1file *f;
+       struct pack_idx_entry **sorted_by_sha, **list, **last;
+       off_t last_obj_offset = 0;
+       uint32_t array[256];
+       int i, fd;
+       SHA_CTX ctx;
+       uint32_t index_version;
+
+       if (nr_objects) {
+               sorted_by_sha = objects;
+               list = sorted_by_sha;
+               last = sorted_by_sha + nr_objects;
+               for (i = 0; i < nr_objects; ++i) {
+                       if (objects[i]->offset > last_obj_offset)
+                               last_obj_offset = objects[i]->offset;
+               }
+               qsort(sorted_by_sha, nr_objects, sizeof(sorted_by_sha[0]),
+                     sha1_compare);
+       }
+       else
+               sorted_by_sha = list = last = NULL;
+
+       if (!index_name) {
+               static char tmpfile[PATH_MAX];
+               snprintf(tmpfile, sizeof(tmpfile),
+                        "%s/tmp_idx_XXXXXX", get_object_directory());
+               fd = xmkstemp(tmpfile);
+               index_name = xstrdup(tmpfile);
+       } else {
+               unlink(index_name);
+               fd = open(index_name, O_CREAT|O_EXCL|O_WRONLY, 0600);
+       }
+       if (fd < 0)
+               die("unable to create %s: %s", index_name, strerror(errno));
+       f = sha1fd(fd, index_name);
+
+       /* if last object's offset is >= 2^31 we should use index V2 */
+       index_version = (last_obj_offset >> 31) ? 2 : pack_idx_default_version;
+
+       /* index versions 2 and above need a header */
+       if (index_version >= 2) {
+               struct pack_idx_header hdr;
+               hdr.idx_signature = htonl(PACK_IDX_SIGNATURE);
+               hdr.idx_version = htonl(index_version);
+               sha1write(f, &hdr, sizeof(hdr));
+       }
+
+       /*
+        * Write the first-level table (the list is sorted,
+        * but we use a 256-entry lookup to be able to avoid
+        * having to do eight extra binary search iterations).
+        */
+       for (i = 0; i < 256; i++) {
+               struct pack_idx_entry **next = list;
+               while (next < last) {
+                       struct pack_idx_entry *obj = *next;
+                       if (obj->sha1[0] != i)
+                               break;
+                       next++;
+               }
+               array[i] = htonl(next - sorted_by_sha);
+               list = next;
+       }
+       sha1write(f, array, 256 * 4);
+
+       /* compute the SHA1 hash of sorted object names. */
+       SHA1_Init(&ctx);
+
+       /*
+        * Write the actual SHA1 entries..
+        */
+       list = sorted_by_sha;
+       for (i = 0; i < nr_objects; i++) {
+               struct pack_idx_entry *obj = *list++;
+               if (index_version < 2) {
+                       uint32_t offset = htonl(obj->offset);
+                       sha1write(f, &offset, 4);
+               }
+               sha1write(f, obj->sha1, 20);
+               SHA1_Update(&ctx, obj->sha1, 20);
+       }
+
+       if (index_version >= 2) {
+               unsigned int nr_large_offset = 0;
+
+               /* write the crc32 table */
+               list = sorted_by_sha;
+               for (i = 0; i < nr_objects; i++) {
+                       struct pack_idx_entry *obj = *list++;
+                       uint32_t crc32_val = htonl(obj->crc32);
+                       sha1write(f, &crc32_val, 4);
+               }
+
+               /* write the 32-bit offset table */
+               list = sorted_by_sha;
+               for (i = 0; i < nr_objects; i++) {
+                       struct pack_idx_entry *obj = *list++;
+                       uint32_t offset = (obj->offset <= pack_idx_off32_limit) ?
+                               obj->offset : (0x80000000 | nr_large_offset++);
+                       offset = htonl(offset);
+                       sha1write(f, &offset, 4);
+               }
+
+               /* write the large offset table */
+               list = sorted_by_sha;
+               while (nr_large_offset) {
+                       struct pack_idx_entry *obj = *list++;
+                       uint64_t offset = obj->offset;
+                       if (offset > pack_idx_off32_limit) {
+                               uint32_t split[2];
+                               split[0] = htonl(offset >> 32);
+                               split[1] = htonl(offset & 0xffffffff);
+                               sha1write(f, split, 8);
+                               nr_large_offset--;
+                       }
+               }
+       }
+
+       sha1write(f, sha1, 20);
+       sha1close(f, NULL, 1);
+       SHA1_Final(sha1, &ctx);
+       return index_name;
+}
+
+void fixup_pack_header_footer(int pack_fd,
+                        unsigned char *pack_file_sha1,
+                        const char *pack_name,
+                        uint32_t object_count)
+{
+       static const int buf_sz = 128 * 1024;
+       SHA_CTX c;
+       struct pack_header hdr;
+       char *buf;
+
+       if (lseek(pack_fd, 0, SEEK_SET) != 0)
+               die("Failed seeking to start: %s", strerror(errno));
+       if (read_in_full(pack_fd, &hdr, sizeof(hdr)) != sizeof(hdr))
+               die("Unable to reread header of %s: %s", pack_name, strerror(errno));
+       if (lseek(pack_fd, 0, SEEK_SET) != 0)
+               die("Failed seeking to start: %s", strerror(errno));
+       hdr.hdr_entries = htonl(object_count);
+       write_or_die(pack_fd, &hdr, sizeof(hdr));
+
+       SHA1_Init(&c);
+       SHA1_Update(&c, &hdr, sizeof(hdr));
+
+       buf = xmalloc(buf_sz);
+       for (;;) {
+               ssize_t n = xread(pack_fd, buf, buf_sz);
+               if (!n)
+                       break;
+               if (n < 0)
+                       die("Failed to checksum %s: %s", pack_name, strerror(errno));
+               SHA1_Update(&c, buf, n);
+       }
+       free(buf);
+
+       SHA1_Final(pack_file_sha1, &c);
+       write_or_die(pack_fd, pack_file_sha1, 20);
+}
+
+char *index_pack_lockfile(int ip_out)
+{
+       int len, s;
+       char packname[46];
+
+       /*
+        * The first thing we expects from index-pack's output
+        * is "pack\t%40s\n" or "keep\t%40s\n" (46 bytes) where
+        * %40s is the newly created pack SHA1 name.  In the "keep"
+        * case, we need it to remove the corresponding .keep file
+        * later on.  If we don't get that then tough luck with it.
+        */
+       for (len = 0;
+                len < 46 && (s = xread(ip_out, packname+len, 46-len)) > 0;
+                len += s);
+       if (len == 46 && packname[45] == '\n' &&
+               memcmp(packname, "keep\t", 5) == 0) {
+               char path[PATH_MAX];
+               packname[45] = 0;
+               snprintf(path, sizeof(path), "%s/pack/pack-%s.keep",
+                        get_object_directory(), packname + 5);
+               return xstrdup(path);
+       }
+       return NULL;
+}
diff --git a/pack.h b/pack.h
new file mode 100644 (file)
index 0000000..b31b376
--- /dev/null
+++ b/pack.h
@@ -0,0 +1,68 @@
+#ifndef PACK_H
+#define PACK_H
+
+#include "object.h"
+
+/*
+ * Packed object header
+ */
+#define PACK_SIGNATURE 0x5041434b      /* "PACK" */
+#define PACK_VERSION 2
+#define pack_version_ok(v) ((v) == htonl(2) || (v) == htonl(3))
+struct pack_header {
+       uint32_t hdr_signature;
+       uint32_t hdr_version;
+       uint32_t hdr_entries;
+};
+
+/*
+ * The first four bytes of index formats later than version 1 should
+ * start with this signature, as all older git binaries would find this
+ * value illegal and abort reading the file.
+ *
+ * This is the case because the number of objects in a packfile
+ * cannot exceed 1,431,660,000 as every object would need at least
+ * 3 bytes of data and the overall packfile cannot exceed 4 GiB with
+ * version 1 of the index file due to the offsets limited to 32 bits.
+ * Clearly the signature exceeds this maximum.
+ *
+ * Very old git binaries will also compare the first 4 bytes to the
+ * next 4 bytes in the index and abort with a "non-monotonic index"
+ * error if the second 4 byte word is smaller than the first 4
+ * byte word.  This would be true in the proposed future index
+ * format as idx_signature would be greater than idx_version.
+ */
+#define PACK_IDX_SIGNATURE 0xff744f63  /* "\377tOc" */
+
+/* These may be overridden by command-line parameters */
+extern uint32_t pack_idx_default_version;
+extern uint32_t pack_idx_off32_limit;
+
+/*
+ * Packed object index header
+ */
+struct pack_idx_header {
+       uint32_t idx_signature;
+       uint32_t idx_version;
+};
+
+/*
+ * Common part of object structure used for write_idx_file
+ */
+struct pack_idx_entry {
+       unsigned char sha1[20];
+       uint32_t crc32;
+       off_t offset;
+};
+
+extern char *write_idx_file(char *index_name, struct pack_idx_entry **objects, int nr_objects, unsigned char *sha1);
+
+extern int verify_pack(struct packed_git *, int);
+extern void fixup_pack_header_footer(int, unsigned char *, const char *, uint32_t);
+extern char *index_pack_lockfile(int fd);
+
+#define PH_ERROR_EOF           (-1)
+#define PH_ERROR_PACK_SIGNATURE        (-2)
+#define PH_ERROR_PROTOCOL      (-3)
+extern int read_pack_header(int fd, struct pack_header *);
+#endif
diff --git a/pager.c b/pager.c
new file mode 100644 (file)
index 0000000..fb7a1a6
--- /dev/null
+++ b/pager.c
@@ -0,0 +1,72 @@
+#include "cache.h"
+
+/*
+ * This is split up from the rest of git so that we might do
+ * something different on Windows, for example.
+ */
+
+static void run_pager(const char *pager)
+{
+       /*
+        * Work around bug in "less" by not starting it until we
+        * have real input
+        */
+       fd_set in;
+
+       FD_ZERO(&in);
+       FD_SET(0, &in);
+       select(1, &in, NULL, &in, NULL);
+
+       execlp(pager, pager, NULL);
+       execl("/bin/sh", "sh", "-c", pager, NULL);
+}
+
+void setup_pager(void)
+{
+       pid_t pid;
+       int fd[2];
+       const char *pager = getenv("GIT_PAGER");
+
+       if (!isatty(1))
+               return;
+       if (!pager) {
+               if (!pager_program)
+                       git_config(git_default_config);
+               pager = pager_program;
+       }
+       if (!pager)
+               pager = getenv("PAGER");
+       if (!pager)
+               pager = "less";
+       else if (!*pager || !strcmp(pager, "cat"))
+               return;
+
+       pager_in_use = 1; /* means we are emitting to terminal */
+
+       if (pipe(fd) < 0)
+               return;
+       pid = fork();
+       if (pid < 0) {
+               close(fd[0]);
+               close(fd[1]);
+               return;
+       }
+
+       /* return in the child */
+       if (!pid) {
+               dup2(fd[1], 1);
+               close(fd[0]);
+               close(fd[1]);
+               return;
+       }
+
+       /* The original process turns into the PAGER */
+       dup2(fd[0], 0);
+       close(fd[0]);
+       close(fd[1]);
+
+       setenv("LESS", "FRSX", 0);
+       run_pager(pager);
+       die("unable to execute pager '%s'", pager);
+       exit(255);
+}
diff --git a/parse-options.c b/parse-options.c
new file mode 100644 (file)
index 0000000..e12b428
--- /dev/null
@@ -0,0 +1,371 @@
+#include "git-compat-util.h"
+#include "parse-options.h"
+
+#define OPT_SHORT 1
+#define OPT_UNSET 2
+
+struct optparse_t {
+       const char **argv;
+       int argc;
+       const char *opt;
+};
+
+static inline const char *get_arg(struct optparse_t *p)
+{
+       if (p->opt) {
+               const char *res = p->opt;
+               p->opt = NULL;
+               return res;
+       }
+       p->argc--;
+       return *++p->argv;
+}
+
+static inline const char *skip_prefix(const char *str, const char *prefix)
+{
+       size_t len = strlen(prefix);
+       return strncmp(str, prefix, len) ? NULL : str + len;
+}
+
+static int opterror(const struct option *opt, const char *reason, int flags)
+{
+       if (flags & OPT_SHORT)
+               return error("switch `%c' %s", opt->short_name, reason);
+       if (flags & OPT_UNSET)
+               return error("option `no-%s' %s", opt->long_name, reason);
+       return error("option `%s' %s", opt->long_name, reason);
+}
+
+static int get_value(struct optparse_t *p,
+                     const struct option *opt, int flags)
+{
+       const char *s, *arg;
+       const int unset = flags & OPT_UNSET;
+
+       if (unset && p->opt)
+               return opterror(opt, "takes no value", flags);
+       if (unset && (opt->flags & PARSE_OPT_NONEG))
+               return opterror(opt, "isn't available", flags);
+
+       if (!(flags & OPT_SHORT) && p->opt) {
+               switch (opt->type) {
+               case OPTION_CALLBACK:
+                       if (!(opt->flags & PARSE_OPT_NOARG))
+                               break;
+                       /* FALLTHROUGH */
+               case OPTION_BOOLEAN:
+               case OPTION_BIT:
+               case OPTION_SET_INT:
+               case OPTION_SET_PTR:
+                       return opterror(opt, "takes no value", flags);
+               default:
+                       break;
+               }
+       }
+
+       arg = p->opt ? p->opt : (p->argc > 1 ? p->argv[1] : NULL);
+       switch (opt->type) {
+       case OPTION_BIT:
+               if (unset)
+                       *(int *)opt->value &= ~opt->defval;
+               else
+                       *(int *)opt->value |= opt->defval;
+               return 0;
+
+       case OPTION_BOOLEAN:
+               *(int *)opt->value = unset ? 0 : *(int *)opt->value + 1;
+               return 0;
+
+       case OPTION_SET_INT:
+               *(int *)opt->value = unset ? 0 : opt->defval;
+               return 0;
+
+       case OPTION_SET_PTR:
+               *(void **)opt->value = unset ? NULL : (void *)opt->defval;
+               return 0;
+
+       case OPTION_STRING:
+               if (unset) {
+                       *(const char **)opt->value = NULL;
+                       return 0;
+               }
+               if (opt->flags & PARSE_OPT_OPTARG && (!arg || *arg == '-')) {
+                       *(const char **)opt->value = (const char *)opt->defval;
+                       return 0;
+               }
+               if (!arg)
+                       return opterror(opt, "requires a value", flags);
+               *(const char **)opt->value = get_arg(p);
+               return 0;
+
+       case OPTION_CALLBACK:
+               if (unset)
+                       return (*opt->callback)(opt, NULL, 1);
+               if (opt->flags & PARSE_OPT_NOARG)
+                       return (*opt->callback)(opt, NULL, 0);
+               if (opt->flags & PARSE_OPT_OPTARG && (!arg || *arg == '-'))
+                       return (*opt->callback)(opt, NULL, 0);
+               if (!arg)
+                       return opterror(opt, "requires a value", flags);
+               return (*opt->callback)(opt, get_arg(p), 0);
+
+       case OPTION_INTEGER:
+               if (unset) {
+                       *(int *)opt->value = 0;
+                       return 0;
+               }
+               if (opt->flags & PARSE_OPT_OPTARG && (!arg || !isdigit(*arg))) {
+                       *(int *)opt->value = opt->defval;
+                       return 0;
+               }
+               if (!arg)
+                       return opterror(opt, "requires a value", flags);
+               *(int *)opt->value = strtol(get_arg(p), (char **)&s, 10);
+               if (*s)
+                       return opterror(opt, "expects a numerical value", flags);
+               return 0;
+
+       default:
+               die("should not happen, someone must be hit on the forehead");
+       }
+}
+
+static int parse_short_opt(struct optparse_t *p, const struct option *options)
+{
+       for (; options->type != OPTION_END; options++) {
+               if (options->short_name == *p->opt) {
+                       p->opt = p->opt[1] ? p->opt + 1 : NULL;
+                       return get_value(p, options, OPT_SHORT);
+               }
+       }
+       return error("unknown switch `%c'", *p->opt);
+}
+
+static int parse_long_opt(struct optparse_t *p, const char *arg,
+                          const struct option *options)
+{
+       const char *arg_end = strchr(arg, '=');
+       const struct option *abbrev_option = NULL, *ambiguous_option = NULL;
+       int abbrev_flags = 0, ambiguous_flags = 0;
+
+       if (!arg_end)
+               arg_end = arg + strlen(arg);
+
+       for (; options->type != OPTION_END; options++) {
+               const char *rest;
+               int flags = 0;
+
+               if (!options->long_name)
+                       continue;
+
+               rest = skip_prefix(arg, options->long_name);
+               if (!rest) {
+                       /* abbreviated? */
+                       if (!strncmp(options->long_name, arg, arg_end - arg)) {
+is_abbreviated:
+                               if (abbrev_option) {
+                                       /*
+                                        * If this is abbreviated, it is
+                                        * ambiguous. So when there is no
+                                        * exact match later, we need to
+                                        * error out.
+                                        */
+                                       ambiguous_option = abbrev_option;
+                                       ambiguous_flags = abbrev_flags;
+                               }
+                               if (!(flags & OPT_UNSET) && *arg_end)
+                                       p->opt = arg_end + 1;
+                               abbrev_option = options;
+                               abbrev_flags = flags;
+                               continue;
+                       }
+                       /* negated and abbreviated very much? */
+                       if (!prefixcmp("no-", arg)) {
+                               flags |= OPT_UNSET;
+                               goto is_abbreviated;
+                       }
+                       /* negated? */
+                       if (strncmp(arg, "no-", 3))
+                               continue;
+                       flags |= OPT_UNSET;
+                       rest = skip_prefix(arg + 3, options->long_name);
+                       /* abbreviated and negated? */
+                       if (!rest && !prefixcmp(options->long_name, arg + 3))
+                               goto is_abbreviated;
+                       if (!rest)
+                               continue;
+               }
+               if (*rest) {
+                       if (*rest != '=')
+                               continue;
+                       p->opt = rest + 1;
+               }
+               return get_value(p, options, flags);
+       }
+
+       if (ambiguous_option)
+               return error("Ambiguous option: %s "
+                       "(could be --%s%s or --%s%s)",
+                       arg,
+                       (ambiguous_flags & OPT_UNSET) ?  "no-" : "",
+                       ambiguous_option->long_name,
+                       (abbrev_flags & OPT_UNSET) ?  "no-" : "",
+                       abbrev_option->long_name);
+       if (abbrev_option)
+               return get_value(p, abbrev_option, abbrev_flags);
+       return error("unknown option `%s'", arg);
+}
+
+static NORETURN void usage_with_options_internal(const char * const *,
+                                                 const struct option *, int);
+
+int parse_options(int argc, const char **argv, const struct option *options,
+                  const char * const usagestr[], int flags)
+{
+       struct optparse_t args = { argv + 1, argc - 1, NULL };
+       int j = 0;
+
+       for (; args.argc; args.argc--, args.argv++) {
+               const char *arg = args.argv[0];
+
+               if (*arg != '-' || !arg[1]) {
+                       argv[j++] = args.argv[0];
+                       continue;
+               }
+
+               if (arg[1] != '-') {
+                       args.opt = arg + 1;
+                       do {
+                               if (*args.opt == 'h')
+                                       usage_with_options(usagestr, options);
+                               if (parse_short_opt(&args, options) < 0)
+                                       usage_with_options(usagestr, options);
+                       } while (args.opt);
+                       continue;
+               }
+
+               if (!arg[2]) { /* "--" */
+                       if (!(flags & PARSE_OPT_KEEP_DASHDASH)) {
+                               args.argc--;
+                               args.argv++;
+                       }
+                       break;
+               }
+
+               if (!strcmp(arg + 2, "help-all"))
+                       usage_with_options_internal(usagestr, options, 1);
+               if (!strcmp(arg + 2, "help"))
+                       usage_with_options(usagestr, options);
+               if (parse_long_opt(&args, arg + 2, options))
+                       usage_with_options(usagestr, options);
+       }
+
+       memmove(argv + j, args.argv, args.argc * sizeof(*argv));
+       argv[j + args.argc] = NULL;
+       return j + args.argc;
+}
+
+#define USAGE_OPTS_WIDTH 24
+#define USAGE_GAP         2
+
+void usage_with_options_internal(const char * const *usagestr,
+                                 const struct option *opts, int full)
+{
+       fprintf(stderr, "usage: %s\n", *usagestr++);
+       while (*usagestr && **usagestr)
+               fprintf(stderr, "   or: %s\n", *usagestr++);
+       while (*usagestr)
+               fprintf(stderr, "    %s\n", *usagestr++);
+
+       if (opts->type != OPTION_GROUP)
+               fputc('\n', stderr);
+
+       for (; opts->type != OPTION_END; opts++) {
+               size_t pos;
+               int pad;
+
+               if (opts->type == OPTION_GROUP) {
+                       fputc('\n', stderr);
+                       if (*opts->help)
+                               fprintf(stderr, "%s\n", opts->help);
+                       continue;
+               }
+               if (!full && (opts->flags & PARSE_OPT_HIDDEN))
+                       continue;
+
+               pos = fprintf(stderr, "    ");
+               if (opts->short_name)
+                       pos += fprintf(stderr, "-%c", opts->short_name);
+               if (opts->long_name && opts->short_name)
+                       pos += fprintf(stderr, ", ");
+               if (opts->long_name)
+                       pos += fprintf(stderr, "--%s", opts->long_name);
+
+               switch (opts->type) {
+               case OPTION_INTEGER:
+                       if (opts->flags & PARSE_OPT_OPTARG)
+                               pos += fprintf(stderr, " [<n>]");
+                       else
+                               pos += fprintf(stderr, " <n>");
+                       break;
+               case OPTION_CALLBACK:
+                       if (opts->flags & PARSE_OPT_NOARG)
+                               break;
+                       /* FALLTHROUGH */
+               case OPTION_STRING:
+                       if (opts->argh) {
+                               if (opts->flags & PARSE_OPT_OPTARG)
+                                       pos += fprintf(stderr, " [<%s>]", opts->argh);
+                               else
+                                       pos += fprintf(stderr, " <%s>", opts->argh);
+                       } else {
+                               if (opts->flags & PARSE_OPT_OPTARG)
+                                       pos += fprintf(stderr, " [...]");
+                               else
+                                       pos += fprintf(stderr, " ...");
+                       }
+                       break;
+               default: /* OPTION_{BIT,BOOLEAN,SET_INT,SET_PTR} */
+                       break;
+               }
+
+               if (pos <= USAGE_OPTS_WIDTH)
+                       pad = USAGE_OPTS_WIDTH - pos;
+               else {
+                       fputc('\n', stderr);
+                       pad = USAGE_OPTS_WIDTH;
+               }
+               fprintf(stderr, "%*s%s\n", pad + USAGE_GAP, "", opts->help);
+       }
+       fputc('\n', stderr);
+
+       exit(129);
+}
+
+void usage_with_options(const char * const *usagestr,
+                        const struct option *opts)
+{
+       usage_with_options_internal(usagestr, opts, 0);
+}
+
+/*----- some often used options -----*/
+#include "cache.h"
+
+int parse_opt_abbrev_cb(const struct option *opt, const char *arg, int unset)
+{
+       int v;
+
+       if (!arg) {
+               v = unset ? 0 : DEFAULT_ABBREV;
+       } else {
+               v = strtol(arg, (char **)&arg, 10);
+               if (*arg)
+                       return opterror(opt, "expects a numerical value", 0);
+               if (v && v < MINIMUM_ABBREV)
+                       v = MINIMUM_ABBREV;
+               else if (v > 40)
+                       v = 40;
+       }
+       *(int *)(opt->value) = v;
+       return 0;
+}
diff --git a/parse-options.h b/parse-options.h
new file mode 100644 (file)
index 0000000..102ac31
--- /dev/null
@@ -0,0 +1,119 @@
+#ifndef PARSE_OPTIONS_H
+#define PARSE_OPTIONS_H
+
+enum parse_opt_type {
+       /* special types */
+       OPTION_END,
+       OPTION_GROUP,
+       /* options with no arguments */
+       OPTION_BIT,
+       OPTION_BOOLEAN, /* _INCR would have been a better name */
+       OPTION_SET_INT,
+       OPTION_SET_PTR,
+       /* options with arguments (usually) */
+       OPTION_STRING,
+       OPTION_INTEGER,
+       OPTION_CALLBACK,
+};
+
+enum parse_opt_flags {
+       PARSE_OPT_KEEP_DASHDASH = 1,
+};
+
+enum parse_opt_option_flags {
+       PARSE_OPT_OPTARG  = 1,
+       PARSE_OPT_NOARG   = 2,
+       PARSE_OPT_NONEG   = 4,
+       PARSE_OPT_HIDDEN  = 8,
+};
+
+struct option;
+typedef int parse_opt_cb(const struct option *, const char *arg, int unset);
+
+/*
+ * `type`::
+ *   holds the type of the option, you must have an OPTION_END last in your
+ *   array.
+ *
+ * `short_name`::
+ *   the character to use as a short option name, '\0' if none.
+ *
+ * `long_name`::
+ *   the long option name, without the leading dashes, NULL if none.
+ *
+ * `value`::
+ *   stores pointers to the values to be filled.
+ *
+ * `argh`::
+ *   token to explain the kind of argument this option wants. Keep it
+ *   homogenous across the repository.
+ *
+ * `help`::
+ *   the short help associated to what the option does.
+ *   Must never be NULL (except for OPTION_END).
+ *   OPTION_GROUP uses this pointer to store the group header.
+ *
+ * `flags`::
+ *   mask of parse_opt_option_flags.
+ *   PARSE_OPT_OPTARG: says that the argument is optionnal (not for BOOLEANs)
+ *   PARSE_OPT_NOARG: says that this option takes no argument, for CALLBACKs
+ *   PARSE_OPT_NONEG: says that this option cannot be negated
+ *   PARSE_OPT_HIDDEN this option is skipped in the default usage, showed in
+ *                    the long one.
+ *
+ * `callback`::
+ *   pointer to the callback to use for OPTION_CALLBACK.
+ *
+ * `defval`::
+ *   default value to fill (*->value) with for PARSE_OPT_OPTARG.
+ *   OPTION_{BIT,SET_INT,SET_PTR} store the {mask,integer,pointer} to put in
+ *   the value when met.
+ *   CALLBACKS can use it like they want.
+ */
+struct option {
+       enum parse_opt_type type;
+       int short_name;
+       const char *long_name;
+       void *value;
+       const char *argh;
+       const char *help;
+
+       int flags;
+       parse_opt_cb *callback;
+       intptr_t defval;
+};
+
+#define OPT_END()                   { OPTION_END }
+#define OPT_GROUP(h)                { OPTION_GROUP, 0, NULL, NULL, NULL, (h) }
+#define OPT_BIT(s, l, v, h, b)      { OPTION_BIT, (s), (l), (v), NULL, (h), 0, NULL, (b) }
+#define OPT_BOOLEAN(s, l, v, h)     { OPTION_BOOLEAN, (s), (l), (v), NULL, (h) }
+#define OPT_SET_INT(s, l, v, h, i)  { OPTION_SET_INT, (s), (l), (v), NULL, (h), 0, NULL, (i) }
+#define OPT_SET_PTR(s, l, v, h, p)  { OPTION_SET_PTR, (s), (l), (v), NULL, (h), 0, NULL, (p) }
+#define OPT_INTEGER(s, l, v, h)     { OPTION_INTEGER, (s), (l), (v), NULL, (h) }
+#define OPT_STRING(s, l, v, a, h)   { OPTION_STRING,  (s), (l), (v), (a), (h) }
+#define OPT_CALLBACK(s, l, v, a, h, f) \
+       { OPTION_CALLBACK, (s), (l), (v), (a), (h), 0, (f) }
+
+/* parse_options() will filter out the processed options and leave the
+ * non-option argments in argv[].
+ * Returns the number of arguments left in argv[].
+ */
+extern int parse_options(int argc, const char **argv,
+                         const struct option *options,
+                         const char * const usagestr[], int flags);
+
+extern NORETURN void usage_with_options(const char * const *usagestr,
+                                        const struct option *options);
+
+/*----- some often used options -----*/
+extern int parse_opt_abbrev_cb(const struct option *, const char *, int);
+
+#define OPT__VERBOSE(var)  OPT_BOOLEAN('v', "verbose", (var), "be verbose")
+#define OPT__QUIET(var)    OPT_BOOLEAN('q', "quiet",   (var), "be quiet")
+#define OPT__DRY_RUN(var)  OPT_BOOLEAN('n', "dry-run", (var), "dry run")
+#define OPT__ABBREV(var)  \
+       { OPTION_CALLBACK, 0, "abbrev", (var), "n", \
+         "use <n> digits to display SHA-1s", \
+         PARSE_OPT_OPTARG, &parse_opt_abbrev_cb, 0 }
+
+#endif
diff --git a/patch-delta.c b/patch-delta.c
new file mode 100644 (file)
index 0000000..ed9db81
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * patch-delta.c:
+ * recreate a buffer from a source and the delta produced by diff-delta.c
+ *
+ * (C) 2005 Nicolas Pitre <nico@cam.org>
+ *
+ * This code is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#include "git-compat-util.h"
+#include "delta.h"
+
+void *patch_delta(const void *src_buf, unsigned long src_size,
+                 const void *delta_buf, unsigned long delta_size,
+                 unsigned long *dst_size)
+{
+       const unsigned char *data, *top;
+       unsigned char *dst_buf, *out, cmd;
+       unsigned long size;
+
+       if (delta_size < DELTA_SIZE_MIN)
+               return NULL;
+
+       data = delta_buf;
+       top = (const unsigned char *) delta_buf + delta_size;
+
+       /* make sure the orig file size matches what we expect */
+       size = get_delta_hdr_size(&data, top);
+       if (size != src_size)
+               return NULL;
+
+       /* now the result size */
+       size = get_delta_hdr_size(&data, top);
+       dst_buf = xmalloc(size + 1);
+       dst_buf[size] = 0;
+
+       out = dst_buf;
+       while (data < top) {
+               cmd = *data++;
+               if (cmd & 0x80) {
+                       unsigned long cp_off = 0, cp_size = 0;
+                       if (cmd & 0x01) cp_off = *data++;
+                       if (cmd & 0x02) cp_off |= (*data++ << 8);
+                       if (cmd & 0x04) cp_off |= (*data++ << 16);
+                       if (cmd & 0x08) cp_off |= (*data++ << 24);
+                       if (cmd & 0x10) cp_size = *data++;
+                       if (cmd & 0x20) cp_size |= (*data++ << 8);
+                       if (cmd & 0x40) cp_size |= (*data++ << 16);
+                       if (cp_size == 0) cp_size = 0x10000;
+                       if (cp_off + cp_size < cp_size ||
+                           cp_off + cp_size > src_size ||
+                           cp_size > size)
+                               break;
+                       memcpy(out, (char *) src_buf + cp_off, cp_size);
+                       out += cp_size;
+                       size -= cp_size;
+               } else if (cmd) {
+                       if (cmd > size)
+                               break;
+                       memcpy(out, data, cmd);
+                       out += cmd;
+                       data += cmd;
+                       size -= cmd;
+               } else {
+                       /*
+                        * cmd == 0 is reserved for future encoding
+                        * extensions. In the mean time we must fail when
+                        * encountering them (might be data corruption).
+                        */
+                       error("unexpected delta opcode 0");
+                       goto bad;
+               }
+       }
+
+       /* sanity check */
+       if (data != top || size != 0) {
+               error("delta replay has gone wild");
+               bad:
+               free(dst_buf);
+               return NULL;
+       }
+
+       *dst_size = out - dst_buf;
+       return dst_buf;
+}
diff --git a/patch-id.c b/patch-id.c
new file mode 100644 (file)
index 0000000..9349bc5
--- /dev/null
@@ -0,0 +1,84 @@
+#include "cache.h"
+
+static void flush_current_id(int patchlen, unsigned char *id, SHA_CTX *c)
+{
+       unsigned char result[20];
+       char name[50];
+
+       if (!patchlen)
+               return;
+
+       SHA1_Final(result, c);
+       memcpy(name, sha1_to_hex(id), 41);
+       printf("%s %s\n", sha1_to_hex(result), name);
+       SHA1_Init(c);
+}
+
+static int remove_space(char *line)
+{
+       char *src = line;
+       char *dst = line;
+       unsigned char c;
+
+       while ((c = *src++) != '\0') {
+               if (!isspace(c))
+                       *dst++ = c;
+       }
+       return dst - line;
+}
+
+static void generate_id_list(void)
+{
+       static unsigned char sha1[20];
+       static char line[1000];
+       SHA_CTX ctx;
+       int patchlen = 0;
+
+       SHA1_Init(&ctx);
+       while (fgets(line, sizeof(line), stdin) != NULL) {
+               unsigned char n[20];
+               char *p = line;
+               int len;
+
+               if (!memcmp(line, "diff-tree ", 10))
+                       p += 10;
+               else if (!memcmp(line, "commit ", 7))
+                       p += 7;
+
+               if (!get_sha1_hex(p, n)) {
+                       flush_current_id(patchlen, sha1, &ctx);
+                       hashcpy(sha1, n);
+                       patchlen = 0;
+                       continue;
+               }
+
+               /* Ignore commit comments */
+               if (!patchlen && memcmp(line, "diff ", 5))
+                       continue;
+
+               /* Ignore git-diff index header */
+               if (!memcmp(line, "index ", 6))
+                       continue;
+
+               /* Ignore line numbers when computing the SHA1 of the patch */
+               if (!memcmp(line, "@@ -", 4))
+                       continue;
+
+               /* Compute the sha without whitespace */
+               len = remove_space(line);
+               patchlen += len;
+               SHA1_Update(&ctx, line, len);
+       }
+       flush_current_id(patchlen, sha1, &ctx);
+}
+
+static const char patch_id_usage[] = "git-patch-id < patch";
+
+int main(int argc, char **argv)
+{
+       if (argc != 1)
+               usage(patch_id_usage);
+
+       generate_id_list();
+       return 0;
+}
diff --git a/patch-ids.c b/patch-ids.c
new file mode 100644 (file)
index 0000000..3be5d31
--- /dev/null
@@ -0,0 +1,192 @@
+#include "cache.h"
+#include "diff.h"
+#include "commit.h"
+#include "patch-ids.h"
+
+static int commit_patch_id(struct commit *commit, struct diff_options *options,
+                   unsigned char *sha1)
+{
+       if (commit->parents)
+               diff_tree_sha1(commit->parents->item->object.sha1,
+                              commit->object.sha1, "", options);
+       else
+               diff_root_tree_sha1(commit->object.sha1, "", options);
+       diffcore_std(options);
+       return diff_flush_patch_id(options, sha1);
+}
+
+static uint32_t take2(const unsigned char *id)
+{
+       return ((id[0] << 8) | id[1]);
+}
+
+/*
+ * Conventional binary search loop looks like this:
+ *
+ *      do {
+ *              int mi = (lo + hi) / 2;
+ *              int cmp = "entry pointed at by mi" minus "target";
+ *              if (!cmp)
+ *                      return (mi is the wanted one)
+ *              if (cmp > 0)
+ *                      hi = mi; "mi is larger than target"
+ *              else
+ *                      lo = mi+1; "mi is smaller than target"
+ *      } while (lo < hi);
+ *
+ * The invariants are:
+ *
+ * - When entering the loop, lo points at a slot that is never
+ *   above the target (it could be at the target), hi points at a
+ *   slot that is guaranteed to be above the target (it can never
+ *   be at the target).
+ *
+ * - We find a point 'mi' between lo and hi (mi could be the same
+ *   as lo, but never can be the same as hi), and check if it hits
+ *   the target.  There are three cases:
+ *
+ *    - if it is a hit, we are happy.
+ *
+ *    - if it is strictly higher than the target, we update hi with
+ *      it.
+ *
+ *    - if it is strictly lower than the target, we update lo to be
+ *      one slot after it, because we allow lo to be at the target.
+ *
+ * When choosing 'mi', we do not have to take the "middle" but
+ * anywhere in between lo and hi, as long as lo <= mi < hi is
+ * satisfied.  When we somehow know that the distance between the
+ * target and lo is much shorter than the target and hi, we could
+ * pick mi that is much closer to lo than the midway.
+ */
+static int patch_pos(struct patch_id **table, int nr, const unsigned char *id)
+{
+       int hi = nr;
+       int lo = 0;
+       int mi = 0;
+
+       if (!nr)
+               return -1;
+
+       if (nr != 1) {
+               unsigned lov, hiv, miv, ofs;
+
+               for (ofs = 0; ofs < 18; ofs += 2) {
+                       lov = take2(table[0]->patch_id + ofs);
+                       hiv = take2(table[nr-1]->patch_id + ofs);
+                       miv = take2(id + ofs);
+                       if (miv < lov)
+                               return -1;
+                       if (hiv < miv)
+                               return -1 - nr;
+                       if (lov != hiv) {
+                               /*
+                                * At this point miv could be equal
+                                * to hiv (but id could still be higher);
+                                * the invariant of (mi < hi) should be
+                                * kept.
+                                */
+                               mi = (nr-1) * (miv - lov) / (hiv - lov);
+                               if (lo <= mi && mi < hi)
+                                       break;
+                               die("oops");
+                       }
+               }
+               if (18 <= ofs)
+                       die("cannot happen -- lo and hi are identical");
+       }
+
+       do {
+               int cmp;
+               cmp = hashcmp(table[mi]->patch_id, id);
+               if (!cmp)
+                       return mi;
+               if (cmp > 0)
+                       hi = mi;
+               else
+                       lo = mi + 1;
+               mi = (hi + lo) / 2;
+       } while (lo < hi);
+       return -lo-1;
+}
+
+#define BUCKET_SIZE 190 /* 190 * 21 = 3990, with slop close enough to 4K */
+struct patch_id_bucket {
+       struct patch_id_bucket *next;
+       int nr;
+       struct patch_id bucket[BUCKET_SIZE];
+};
+
+int init_patch_ids(struct patch_ids *ids)
+{
+       memset(ids, 0, sizeof(*ids));
+       diff_setup(&ids->diffopts);
+       DIFF_OPT_SET(&ids->diffopts, RECURSIVE);
+       if (diff_setup_done(&ids->diffopts) < 0)
+               return error("diff_setup_done failed");
+       return 0;
+}
+
+int free_patch_ids(struct patch_ids *ids)
+{
+       struct patch_id_bucket *next, *patches;
+
+       free(ids->table);
+       for (patches = ids->patches; patches; patches = next) {
+               next = patches->next;
+               free(patches);
+       }
+       return 0;
+}
+
+static struct patch_id *add_commit(struct commit *commit,
+                                  struct patch_ids *ids,
+                                  int no_add)
+{
+       struct patch_id_bucket *bucket;
+       struct patch_id *ent;
+       unsigned char sha1[20];
+       int pos;
+
+       if (commit_patch_id(commit, &ids->diffopts, sha1))
+               return NULL;
+       pos = patch_pos(ids->table, ids->nr, sha1);
+       if (0 <= pos)
+               return ids->table[pos];
+       if (no_add)
+               return NULL;
+
+       pos = -1 - pos;
+
+       bucket = ids->patches;
+       if (!bucket || (BUCKET_SIZE <= bucket->nr)) {
+               bucket = xcalloc(1, sizeof(*bucket));
+               bucket->next = ids->patches;
+               ids->patches = bucket;
+       }
+       ent = &bucket->bucket[bucket->nr++];
+       hashcpy(ent->patch_id, sha1);
+
+       if (ids->alloc <= ids->nr) {
+               ids->alloc = alloc_nr(ids->nr);
+               ids->table = xrealloc(ids->table, sizeof(ent) * ids->alloc);
+       }
+       if (pos < ids->nr)
+               memmove(ids->table + pos + 1, ids->table + pos,
+                       sizeof(ent) * (ids->nr - pos));
+       ids->nr++;
+       ids->table[pos] = ent;
+       return ids->table[pos];
+}
+
+struct patch_id *has_commit_patch_id(struct commit *commit,
+                                    struct patch_ids *ids)
+{
+       return add_commit(commit, ids, 1);
+}
+
+struct patch_id *add_commit_patch_id(struct commit *commit,
+                                    struct patch_ids *ids)
+{
+       return add_commit(commit, ids, 0);
+}
diff --git a/patch-ids.h b/patch-ids.h
new file mode 100644 (file)
index 0000000..c8c7ca1
--- /dev/null
@@ -0,0 +1,21 @@
+#ifndef PATCH_IDS_H
+#define PATCH_IDS_H
+
+struct patch_id {
+       unsigned char patch_id[20];
+       char seen;
+};
+
+struct patch_ids {
+       struct diff_options diffopts;
+       int nr, alloc;
+       struct patch_id **table;
+       struct patch_id_bucket *patches;
+};
+
+int init_patch_ids(struct patch_ids *);
+int free_patch_ids(struct patch_ids *);
+struct patch_id *add_commit_patch_id(struct commit *, struct patch_ids *);
+struct patch_id *has_commit_patch_id(struct commit *, struct patch_ids *);
+
+#endif /* PATCH_IDS_H */
diff --git a/path-list.c b/path-list.c
new file mode 100644 (file)
index 0000000..3d83b7b
--- /dev/null
@@ -0,0 +1,104 @@
+#include "cache.h"
+#include "path-list.h"
+
+/* if there is no exact match, point to the index where the entry could be
+ * inserted */
+static int get_entry_index(const struct path_list *list, const char *path,
+               int *exact_match)
+{
+       int left = -1, right = list->nr;
+
+       while (left + 1 < right) {
+               int middle = (left + right) / 2;
+               int compare = strcmp(path, list->items[middle].path);
+               if (compare < 0)
+                       right = middle;
+               else if (compare > 0)
+                       left = middle;
+               else {
+                       *exact_match = 1;
+                       return middle;
+               }
+       }
+
+       *exact_match = 0;
+       return right;
+}
+
+/* returns -1-index if already exists */
+static int add_entry(struct path_list *list, const char *path)
+{
+       int exact_match;
+       int index = get_entry_index(list, path, &exact_match);
+
+       if (exact_match)
+               return -1 - index;
+
+       if (list->nr + 1 >= list->alloc) {
+               list->alloc += 32;
+               list->items = xrealloc(list->items, list->alloc
+                               * sizeof(struct path_list_item));
+       }
+       if (index < list->nr)
+               memmove(list->items + index + 1, list->items + index,
+                               (list->nr - index)
+                               * sizeof(struct path_list_item));
+       list->items[index].path = list->strdup_paths ?
+               xstrdup(path) : (char *)path;
+       list->items[index].util = NULL;
+       list->nr++;
+
+       return index;
+}
+
+struct path_list_item *path_list_insert(const char *path, struct path_list *list)
+{
+       int index = add_entry(list, path);
+
+       if (index < 0)
+               index = -1 - index;
+
+       return list->items + index;
+}
+
+int path_list_has_path(const struct path_list *list, const char *path)
+{
+       int exact_match;
+       get_entry_index(list, path, &exact_match);
+       return exact_match;
+}
+
+struct path_list_item *path_list_lookup(const char *path, struct path_list *list)
+{
+       int exact_match, i = get_entry_index(list, path, &exact_match);
+       if (!exact_match)
+               return NULL;
+       return list->items + i;
+}
+
+void path_list_clear(struct path_list *list, int free_util)
+{
+       if (list->items) {
+               int i;
+               if (list->strdup_paths) {
+                       for (i = 0; i < list->nr; i++)
+                               free(list->items[i].path);
+               }
+               if (free_util) {
+                       for (i = 0; i < list->nr; i++)
+                               free(list->items[i].util);
+               }
+               free(list->items);
+       }
+       list->items = NULL;
+       list->nr = list->alloc = 0;
+}
+
+void print_path_list(const char *text, const struct path_list *p)
+{
+       int i;
+       if ( text )
+               printf("%s\n", text);
+       for (i = 0; i < p->nr; i++)
+               printf("%s:%p\n", p->items[i].path, p->items[i].util);
+}
diff --git a/path-list.h b/path-list.h
new file mode 100644 (file)
index 0000000..5931e2c
--- /dev/null
@@ -0,0 +1,22 @@
+#ifndef PATH_LIST_H
+#define PATH_LIST_H
+
+struct path_list_item {
+       char *path;
+       void *util;
+};
+struct path_list
+{
+       struct path_list_item *items;
+       unsigned int nr, alloc;
+       unsigned int strdup_paths:1;
+};
+
+void print_path_list(const char *text, const struct path_list *p);
+
+int path_list_has_path(const struct path_list *list, const char *path);
+void path_list_clear(struct path_list *list, int free_util);
+struct path_list_item *path_list_insert(const char *path, struct path_list *list);
+struct path_list_item *path_list_lookup(const char *path, struct path_list *list);
+
+#endif /* PATH_LIST_H */
diff --git a/path.c b/path.c
new file mode 100644 (file)
index 0000000..4260952
--- /dev/null
+++ b/path.c
@@ -0,0 +1,355 @@
+/*
+ * I'm tired of doing "vsnprintf()" etc just to open a
+ * file, so here's a "return static buffer with printf"
+ * interface for paths.
+ *
+ * It's obviously not thread-safe. Sue me. But it's quite
+ * useful for doing things like
+ *
+ *   f = open(mkpath("%s/%s.git", base, name), O_RDONLY);
+ *
+ * which is what it's designed for.
+ */
+#include "cache.h"
+
+static char bad_path[] = "/bad-path/";
+
+static char *get_pathname(void)
+{
+       static char pathname_array[4][PATH_MAX];
+       static int index;
+       return pathname_array[3 & ++index];
+}
+
+static char *cleanup_path(char *path)
+{
+       /* Clean it up */
+       if (!memcmp(path, "./", 2)) {
+               path += 2;
+               while (*path == '/')
+                       path++;
+       }
+       return path;
+}
+
+char *mkpath(const char *fmt, ...)
+{
+       va_list args;
+       unsigned len;
+       char *pathname = get_pathname();
+
+       va_start(args, fmt);
+       len = vsnprintf(pathname, PATH_MAX, fmt, args);
+       va_end(args);
+       if (len >= PATH_MAX)
+               return bad_path;
+       return cleanup_path(pathname);
+}
+
+char *git_path(const char *fmt, ...)
+{
+       const char *git_dir = get_git_dir();
+       char *pathname = get_pathname();
+       va_list args;
+       unsigned len;
+
+       len = strlen(git_dir);
+       if (len > PATH_MAX-100)
+               return bad_path;
+       memcpy(pathname, git_dir, len);
+       if (len && git_dir[len-1] != '/')
+               pathname[len++] = '/';
+       va_start(args, fmt);
+       len += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);
+       va_end(args);
+       if (len >= PATH_MAX)
+               return bad_path;
+       return cleanup_path(pathname);
+}
+
+
+/* git_mkstemp() - create tmp file honoring TMPDIR variable */
+int git_mkstemp(char *path, size_t len, const char *template)
+{
+       const char *tmp;
+       size_t n;
+
+       tmp = getenv("TMPDIR");
+       if (!tmp)
+               tmp = "/tmp";
+       n = snprintf(path, len, "%s/%s", tmp, template);
+       if (len <= n) {
+               errno = ENAMETOOLONG;
+               return -1;
+       }
+       return mkstemp(path);
+}
+
+
+int validate_headref(const char *path)
+{
+       struct stat st;
+       char *buf, buffer[256];
+       unsigned char sha1[20];
+       int len, fd;
+
+       if (lstat(path, &st) < 0)
+               return -1;
+
+       /* Make sure it is a "refs/.." symlink */
+       if (S_ISLNK(st.st_mode)) {
+               len = readlink(path, buffer, sizeof(buffer)-1);
+               if (len >= 5 && !memcmp("refs/", buffer, 5))
+                       return 0;
+               return -1;
+       }
+
+       /*
+        * Anything else, just open it and try to see if it is a symbolic ref.
+        */
+       fd = open(path, O_RDONLY);
+       if (fd < 0)
+               return -1;
+       len = read_in_full(fd, buffer, sizeof(buffer)-1);
+       close(fd);
+
+       /*
+        * Is it a symbolic ref?
+        */
+       if (len < 4)
+               return -1;
+       if (!memcmp("ref:", buffer, 4)) {
+               buf = buffer + 4;
+               len -= 4;
+               while (len && isspace(*buf))
+                       buf++, len--;
+               if (len >= 5 && !memcmp("refs/", buf, 5))
+                       return 0;
+       }
+
+       /*
+        * Is this a detached HEAD?
+        */
+       if (!get_sha1_hex(buffer, sha1))
+               return 0;
+
+       return -1;
+}
+
+static char *user_path(char *buf, char *path, int sz)
+{
+       struct passwd *pw;
+       char *slash;
+       int len, baselen;
+
+       if (!path || path[0] != '~')
+               return NULL;
+       path++;
+       slash = strchr(path, '/');
+       if (path[0] == '/' || !path[0]) {
+               pw = getpwuid(getuid());
+       }
+       else {
+               if (slash) {
+                       *slash = 0;
+                       pw = getpwnam(path);
+                       *slash = '/';
+               }
+               else
+                       pw = getpwnam(path);
+       }
+       if (!pw || !pw->pw_dir || sz <= strlen(pw->pw_dir))
+               return NULL;
+       baselen = strlen(pw->pw_dir);
+       memcpy(buf, pw->pw_dir, baselen);
+       while ((1 < baselen) && (buf[baselen-1] == '/')) {
+               buf[baselen-1] = 0;
+               baselen--;
+       }
+       if (slash && slash[1]) {
+               len = strlen(slash);
+               if (sz <= baselen + len)
+                       return NULL;
+               memcpy(buf + baselen, slash, len + 1);
+       }
+       return buf;
+}
+
+/*
+ * First, one directory to try is determined by the following algorithm.
+ *
+ * (0) If "strict" is given, the path is used as given and no DWIM is
+ *     done. Otherwise:
+ * (1) "~/path" to mean path under the running user's home directory;
+ * (2) "~user/path" to mean path under named user's home directory;
+ * (3) "relative/path" to mean cwd relative directory; or
+ * (4) "/absolute/path" to mean absolute directory.
+ *
+ * Unless "strict" is given, we try access() for existence of "%s.git/.git",
+ * "%s/.git", "%s.git", "%s" in this order.  The first one that exists is
+ * what we try.
+ *
+ * Second, we try chdir() to that.  Upon failure, we return NULL.
+ *
+ * Then, we try if the current directory is a valid git repository.
+ * Upon failure, we return NULL.
+ *
+ * If all goes well, we return the directory we used to chdir() (but
+ * before ~user is expanded), avoiding getcwd() resolving symbolic
+ * links.  User relative paths are also returned as they are given,
+ * except DWIM suffixing.
+ */
+char *enter_repo(char *path, int strict)
+{
+       static char used_path[PATH_MAX];
+       static char validated_path[PATH_MAX];
+
+       if (!path)
+               return NULL;
+
+       if (!strict) {
+               static const char *suffix[] = {
+                       ".git/.git", "/.git", ".git", "", NULL,
+               };
+               int len = strlen(path);
+               int i;
+               while ((1 < len) && (path[len-1] == '/')) {
+                       path[len-1] = 0;
+                       len--;
+               }
+               if (PATH_MAX <= len)
+                       return NULL;
+               if (path[0] == '~') {
+                       if (!user_path(used_path, path, PATH_MAX))
+                               return NULL;
+                       strcpy(validated_path, path);
+                       path = used_path;
+               }
+               else if (PATH_MAX - 10 < len)
+                       return NULL;
+               else {
+                       path = strcpy(used_path, path);
+                       strcpy(validated_path, path);
+               }
+               len = strlen(path);
+               for (i = 0; suffix[i]; i++) {
+                       strcpy(path + len, suffix[i]);
+                       if (!access(path, F_OK)) {
+                               strcat(validated_path, suffix[i]);
+                               break;
+                       }
+               }
+               if (!suffix[i] || chdir(path))
+                       return NULL;
+               path = validated_path;
+       }
+       else if (chdir(path))
+               return NULL;
+
+       if (access("objects", X_OK) == 0 && access("refs", X_OK) == 0 &&
+           validate_headref("HEAD") == 0) {
+               setenv(GIT_DIR_ENVIRONMENT, ".", 1);
+               check_repository_format();
+               return path;
+       }
+
+       return NULL;
+}
+
+int adjust_shared_perm(const char *path)
+{
+       struct stat st;
+       int mode;
+
+       if (!shared_repository)
+               return 0;
+       if (lstat(path, &st) < 0)
+               return -1;
+       mode = st.st_mode;
+       if (mode & S_IRUSR)
+               mode |= (shared_repository == PERM_GROUP
+                        ? S_IRGRP
+                        : (shared_repository == PERM_EVERYBODY
+                           ? (S_IRGRP|S_IROTH)
+                           : 0));
+
+       if (mode & S_IWUSR)
+               mode |= S_IWGRP;
+
+       if (mode & S_IXUSR)
+               mode |= (shared_repository == PERM_GROUP
+                        ? S_IXGRP
+                        : (shared_repository == PERM_EVERYBODY
+                           ? (S_IXGRP|S_IXOTH)
+                           : 0));
+       if (S_ISDIR(mode))
+               mode |= S_ISGID;
+       if ((mode & st.st_mode) != mode && chmod(path, mode) < 0)
+               return -2;
+       return 0;
+}
+
+/* We allow "recursive" symbolic links. Only within reason, though. */
+#define MAXDEPTH 5
+
+const char *make_absolute_path(const char *path)
+{
+       static char bufs[2][PATH_MAX + 1], *buf = bufs[0], *next_buf = bufs[1];
+       char cwd[1024] = "";
+       int buf_index = 1, len;
+
+       int depth = MAXDEPTH;
+       char *last_elem = NULL;
+       struct stat st;
+
+       if (strlcpy(buf, path, PATH_MAX) >= PATH_MAX)
+               die ("Too long path: %.*s", 60, path);
+
+       while (depth--) {
+               if (stat(buf, &st) || !S_ISDIR(st.st_mode)) {
+                       char *last_slash = strrchr(buf, '/');
+                       if (last_slash) {
+                               *last_slash = '\0';
+                               last_elem = xstrdup(last_slash + 1);
+                       } else
+                               last_elem = xstrdup(buf);
+               }
+
+               if (*buf) {
+                       if (!*cwd && !getcwd(cwd, sizeof(cwd)))
+                               die ("Could not get current working directory");
+
+                       if (chdir(buf))
+                               die ("Could not switch to '%s'", buf);
+               }
+               if (!getcwd(buf, PATH_MAX))
+                       die ("Could not get current working directory");
+
+               if (last_elem) {
+                       int len = strlen(buf);
+                       if (len + strlen(last_elem) + 2 > PATH_MAX)
+                               die ("Too long path name: '%s/%s'",
+                                               buf, last_elem);
+                       buf[len] = '/';
+                       strcpy(buf + len + 1, last_elem);
+                       free(last_elem);
+                       last_elem = NULL;
+               }
+
+               if (!lstat(buf, &st) && S_ISLNK(st.st_mode)) {
+                       len = readlink(buf, next_buf, PATH_MAX);
+                       if (len < 0)
+                               die ("Invalid symlink: %s", buf);
+                       next_buf[len] = '\0';
+                       buf = next_buf;
+                       buf_index = 1 - buf_index;
+                       next_buf = bufs[buf_index];
+               } else
+                       break;
+       }
+
+       if (*cwd && chdir(cwd))
+               die ("Could not change back to '%s'", cwd);
+
+       return buf;
+}
diff --git a/perl/.gitignore b/perl/.gitignore
new file mode 100644 (file)
index 0000000..98b2477
--- /dev/null
@@ -0,0 +1,5 @@
+perl.mak
+perl.mak.old
+blib
+blibdirs
+pm_to_blib
diff --git a/perl/Git.pm b/perl/Git.pm
new file mode 100644 (file)
index 0000000..a2812ea
--- /dev/null
@@ -0,0 +1,950 @@
+=head1 NAME
+
+Git - Perl interface to the Git version control system
+
+=cut
+
+
+package Git;
+
+use strict;
+
+
+BEGIN {
+
+our ($VERSION, @ISA, @EXPORT, @EXPORT_OK);
+
+# Totally unstable API.
+$VERSION = '0.01';
+
+
+=head1 SYNOPSIS
+
+  use Git;
+
+  my $version = Git::command_oneline('version');
+
+  git_cmd_try { Git::command_noisy('update-server-info') }
+              '%s failed w/ code %d';
+
+  my $repo = Git->repository (Directory => '/srv/git/cogito.git');
+
+
+  my @revs = $repo->command('rev-list', '--since=last monday', '--all');
+
+  my ($fh, $c) = $repo->command_output_pipe('rev-list', '--since=last monday', '--all');
+  my $lastrev = <$fh>; chomp $lastrev;
+  $repo->command_close_pipe($fh, $c);
+
+  my $lastrev = $repo->command_oneline( [ 'rev-list', '--all' ],
+                                        STDERR => 0 );
+
+=cut
+
+
+require Exporter;
+
+@ISA = qw(Exporter);
+
+@EXPORT = qw(git_cmd_try);
+
+# Methods which can be called as standalone functions as well:
+@EXPORT_OK = qw(command command_oneline command_noisy
+                command_output_pipe command_input_pipe command_close_pipe
+                version exec_path hash_object git_cmd_try);
+
+
+=head1 DESCRIPTION
+
+This module provides Perl scripts easy way to interface the Git version control
+system. The modules have an easy and well-tested way to call arbitrary Git
+commands; in the future, the interface will also provide specialized methods
+for doing easily operations which are not totally trivial to do over
+the generic command interface.
+
+While some commands can be executed outside of any context (e.g. 'version'
+or 'init'), most operations require a repository context, which in practice
+means getting an instance of the Git object using the repository() constructor.
+(In the future, we will also get a new_repository() constructor.) All commands
+called as methods of the object are then executed in the context of the
+repository.
+
+Part of the "repository state" is also information about path to the attached
+working copy (unless you work with a bare repository). You can also navigate
+inside of the working copy using the C<wc_chdir()> method. (Note that
+the repository object is self-contained and will not change working directory
+of your process.)
+
+TODO: In the future, we might also do
+
+       my $remoterepo = $repo->remote_repository (Name => 'cogito', Branch => 'master');
+       $remoterepo ||= Git->remote_repository ('http://git.or.cz/cogito.git/');
+       my @refs = $remoterepo->refs();
+
+Currently, the module merely wraps calls to external Git tools. In the future,
+it will provide a much faster way to interact with Git by linking directly
+to libgit. This should be completely opaque to the user, though (performance
+increate nonwithstanding).
+
+=cut
+
+
+use Carp qw(carp croak); # but croak is bad - throw instead
+use Error qw(:try);
+use Cwd qw(abs_path);
+
+}
+
+
+=head1 CONSTRUCTORS
+
+=over 4
+
+=item repository ( OPTIONS )
+
+=item repository ( DIRECTORY )
+
+=item repository ()
+
+Construct a new repository object.
+C<OPTIONS> are passed in a hash like fashion, using key and value pairs.
+Possible options are:
+
+B<Repository> - Path to the Git repository.
+
+B<WorkingCopy> - Path to the associated working copy; not strictly required
+as many commands will happily crunch on a bare repository.
+
+B<WorkingSubdir> - Subdirectory in the working copy to work inside.
+Just left undefined if you do not want to limit the scope of operations.
+
+B<Directory> - Path to the Git working directory in its usual setup.
+The C<.git> directory is searched in the directory and all the parent
+directories; if found, C<WorkingCopy> is set to the directory containing
+it and C<Repository> to the C<.git> directory itself. If no C<.git>
+directory was found, the C<Directory> is assumed to be a bare repository,
+C<Repository> is set to point at it and C<WorkingCopy> is left undefined.
+If the C<$GIT_DIR> environment variable is set, things behave as expected
+as well.
+
+You should not use both C<Directory> and either of C<Repository> and
+C<WorkingCopy> - the results of that are undefined.
+
+Alternatively, a directory path may be passed as a single scalar argument
+to the constructor; it is equivalent to setting only the C<Directory> option
+field.
+
+Calling the constructor with no options whatsoever is equivalent to
+calling it with C<< Directory => '.' >>. In general, if you are building
+a standard porcelain command, simply doing C<< Git->repository() >> should
+do the right thing and setup the object to reflect exactly where the user
+is right now.
+
+=cut
+
+sub repository {
+       my $class = shift;
+       my @args = @_;
+       my %opts = ();
+       my $self;
+
+       if (defined $args[0]) {
+               if ($#args % 2 != 1) {
+                       # Not a hash.
+                       $#args == 0 or throw Error::Simple("bad usage");
+                       %opts = ( Directory => $args[0] );
+               } else {
+                       %opts = @args;
+               }
+       }
+
+       if (not defined $opts{Repository} and not defined $opts{WorkingCopy}) {
+               $opts{Directory} ||= '.';
+       }
+
+       if ($opts{Directory}) {
+               -d $opts{Directory} or throw Error::Simple("Directory not found: $!");
+
+               my $search = Git->repository(WorkingCopy => $opts{Directory});
+               my $dir;
+               try {
+                       $dir = $search->command_oneline(['rev-parse', '--git-dir'],
+                                                       STDERR => 0);
+               } catch Git::Error::Command with {
+                       $dir = undef;
+               };
+
+               if ($dir) {
+                       $dir =~ m#^/# or $dir = $opts{Directory} . '/' . $dir;
+                       $opts{Repository} = $dir;
+
+                       # If --git-dir went ok, this shouldn't die either.
+                       my $prefix = $search->command_oneline('rev-parse', '--show-prefix');
+                       $dir = abs_path($opts{Directory}) . '/';
+                       if ($prefix) {
+                               if (substr($dir, -length($prefix)) ne $prefix) {
+                                       throw Error::Simple("rev-parse confused me - $dir does not have trailing $prefix");
+                               }
+                               substr($dir, -length($prefix)) = '';
+                       }
+                       $opts{WorkingCopy} = $dir;
+                       $opts{WorkingSubdir} = $prefix;
+
+               } else {
+                       # A bare repository? Let's see...
+                       $dir = $opts{Directory};
+
+                       unless (-d "$dir/refs" and -d "$dir/objects" and -e "$dir/HEAD") {
+                               # Mimick git-rev-parse --git-dir error message:
+                               throw Error::Simple('fatal: Not a git repository');
+                       }
+                       my $search = Git->repository(Repository => $dir);
+                       try {
+                               $search->command('symbolic-ref', 'HEAD');
+                       } catch Git::Error::Command with {
+                               # Mimick git-rev-parse --git-dir error message:
+                               throw Error::Simple('fatal: Not a git repository');
+                       }
+
+                       $opts{Repository} = abs_path($dir);
+               }
+
+               delete $opts{Directory};
+       }
+
+       $self = { opts => \%opts };
+       bless $self, $class;
+}
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item command ( COMMAND [, ARGUMENTS... ] )
+
+=item command ( [ COMMAND, ARGUMENTS... ], { Opt => Val ... } )
+
+Execute the given Git C<COMMAND> (specify it without the 'git-'
+prefix), optionally with the specified extra C<ARGUMENTS>.
+
+The second more elaborate form can be used if you want to further adjust
+the command execution. Currently, only one option is supported:
+
+B<STDERR> - How to deal with the command's error output. By default (C<undef>)
+it is delivered to the caller's C<STDERR>. A false value (0 or '') will cause
+it to be thrown away. If you want to process it, you can get it in a filehandle
+you specify, but you must be extremely careful; if the error output is not
+very short and you want to read it in the same process as where you called
+C<command()>, you are set up for a nice deadlock!
+
+The method can be called without any instance or on a specified Git repository
+(in that case the command will be run in the repository context).
+
+In scalar context, it returns all the command output in a single string
+(verbatim).
+
+In array context, it returns an array containing lines printed to the
+command's stdout (without trailing newlines).
+
+In both cases, the command's stdin and stderr are the same as the caller's.
+
+=cut
+
+sub command {
+       my ($fh, $ctx) = command_output_pipe(@_);
+
+       if (not defined wantarray) {
+               # Nothing to pepper the possible exception with.
+               _cmd_close($fh, $ctx);
+
+       } elsif (not wantarray) {
+               local $/;
+               my $text = <$fh>;
+               try {
+                       _cmd_close($fh, $ctx);
+               } catch Git::Error::Command with {
+                       # Pepper with the output:
+                       my $E = shift;
+                       $E->{'-outputref'} = \$text;
+                       throw $E;
+               };
+               return $text;
+
+       } else {
+               my @lines = <$fh>;
+               defined and chomp for @lines;
+               try {
+                       _cmd_close($fh, $ctx);
+               } catch Git::Error::Command with {
+                       my $E = shift;
+                       $E->{'-outputref'} = \@lines;
+                       throw $E;
+               };
+               return @lines;
+       }
+}
+
+
+=item command_oneline ( COMMAND [, ARGUMENTS... ] )
+
+=item command_oneline ( [ COMMAND, ARGUMENTS... ], { Opt => Val ... } )
+
+Execute the given C<COMMAND> in the same way as command()
+does but always return a scalar string containing the first line
+of the command's standard output.
+
+=cut
+
+sub command_oneline {
+       my ($fh, $ctx) = command_output_pipe(@_);
+
+       my $line = <$fh>;
+       defined $line and chomp $line;
+       try {
+               _cmd_close($fh, $ctx);
+       } catch Git::Error::Command with {
+               # Pepper with the output:
+               my $E = shift;
+               $E->{'-outputref'} = \$line;
+               throw $E;
+       };
+       return $line;
+}
+
+
+=item command_output_pipe ( COMMAND [, ARGUMENTS... ] )
+
+=item command_output_pipe ( [ COMMAND, ARGUMENTS... ], { Opt => Val ... } )
+
+Execute the given C<COMMAND> in the same way as command()
+does but return a pipe filehandle from which the command output can be
+read.
+
+The function can return C<($pipe, $ctx)> in array context.
+See C<command_close_pipe()> for details.
+
+=cut
+
+sub command_output_pipe {
+       _command_common_pipe('-|', @_);
+}
+
+
+=item command_input_pipe ( COMMAND [, ARGUMENTS... ] )
+
+=item command_input_pipe ( [ COMMAND, ARGUMENTS... ], { Opt => Val ... } )
+
+Execute the given C<COMMAND> in the same way as command_output_pipe()
+does but return an input pipe filehandle instead; the command output
+is not captured.
+
+The function can return C<($pipe, $ctx)> in array context.
+See C<command_close_pipe()> for details.
+
+=cut
+
+sub command_input_pipe {
+       _command_common_pipe('|-', @_);
+}
+
+
+=item command_close_pipe ( PIPE [, CTX ] )
+
+Close the C<PIPE> as returned from C<command_*_pipe()>, checking
+whether the command finished successfully. The optional C<CTX> argument
+is required if you want to see the command name in the error message,
+and it is the second value returned by C<command_*_pipe()> when
+called in array context. The call idiom is:
+
+       my ($fh, $ctx) = $r->command_output_pipe('status');
+       while (<$fh>) { ... }
+       $r->command_close_pipe($fh, $ctx);
+
+Note that you should not rely on whatever actually is in C<CTX>;
+currently it is simply the command name but in future the context might
+have more complicated structure.
+
+=cut
+
+sub command_close_pipe {
+       my ($self, $fh, $ctx) = _maybe_self(@_);
+       $ctx ||= '<unknown>';
+       _cmd_close($fh, $ctx);
+}
+
+
+=item command_noisy ( COMMAND [, ARGUMENTS... ] )
+
+Execute the given C<COMMAND> in the same way as command() does but do not
+capture the command output - the standard output is not redirected and goes
+to the standard output of the caller application.
+
+While the method is called command_noisy(), you might want to as well use
+it for the most silent Git commands which you know will never pollute your
+stdout but you want to avoid the overhead of the pipe setup when calling them.
+
+The function returns only after the command has finished running.
+
+=cut
+
+sub command_noisy {
+       my ($self, $cmd, @args) = _maybe_self(@_);
+       _check_valid_cmd($cmd);
+
+       my $pid = fork;
+       if (not defined $pid) {
+               throw Error::Simple("fork failed: $!");
+       } elsif ($pid == 0) {
+               _cmd_exec($self, $cmd, @args);
+       }
+       if (waitpid($pid, 0) > 0 and $?>>8 != 0) {
+               throw Git::Error::Command(join(' ', $cmd, @args), $? >> 8);
+       }
+}
+
+
+=item version ()
+
+Return the Git version in use.
+
+=cut
+
+sub version {
+       my $verstr = command_oneline('--version');
+       $verstr =~ s/^git version //;
+       $verstr;
+}
+
+
+=item exec_path ()
+
+Return path to the Git sub-command executables (the same as
+C<git --exec-path>). Useful mostly only internally.
+
+=cut
+
+sub exec_path { command_oneline('--exec-path') }
+
+
+=item repo_path ()
+
+Return path to the git repository. Must be called on a repository instance.
+
+=cut
+
+sub repo_path { $_[0]->{opts}->{Repository} }
+
+
+=item wc_path ()
+
+Return path to the working copy. Must be called on a repository instance.
+
+=cut
+
+sub wc_path { $_[0]->{opts}->{WorkingCopy} }
+
+
+=item wc_subdir ()
+
+Return path to the subdirectory inside of a working copy. Must be called
+on a repository instance.
+
+=cut
+
+sub wc_subdir { $_[0]->{opts}->{WorkingSubdir} ||= '' }
+
+
+=item wc_chdir ( SUBDIR )
+
+Change the working copy subdirectory to work within. The C<SUBDIR> is
+relative to the working copy root directory (not the current subdirectory).
+Must be called on a repository instance attached to a working copy
+and the directory must exist.
+
+=cut
+
+sub wc_chdir {
+       my ($self, $subdir) = @_;
+       $self->wc_path()
+               or throw Error::Simple("bare repository");
+
+       -d $self->wc_path().'/'.$subdir
+               or throw Error::Simple("subdir not found: $!");
+       # Of course we will not "hold" the subdirectory so anyone
+       # can delete it now and we will never know. But at least we tried.
+
+       $self->{opts}->{WorkingSubdir} = $subdir;
+}
+
+
+=item config ( VARIABLE )
+
+Retrieve the configuration C<VARIABLE> in the same manner as C<config>
+does. In scalar context requires the variable to be set only one time
+(exception is thrown otherwise), in array context returns allows the
+variable to be set multiple times and returns all the values.
+
+Must be called on a repository instance.
+
+This currently wraps command('config') so it is not so fast.
+
+=cut
+
+sub config {
+       my ($self, $var) = @_;
+       $self->repo_path()
+               or throw Error::Simple("not a repository");
+
+       try {
+               if (wantarray) {
+                       return $self->command('config', '--get-all', $var);
+               } else {
+                       return $self->command_oneline('config', '--get', $var);
+               }
+       } catch Git::Error::Command with {
+               my $E = shift;
+               if ($E->value() == 1) {
+                       # Key not found.
+                       return undef;
+               } else {
+                       throw $E;
+               }
+       };
+}
+
+
+=item config_bool ( VARIABLE )
+
+Retrieve the bool configuration C<VARIABLE>. The return value
+is usable as a boolean in perl (and C<undef> if it's not defined,
+of course).
+
+Must be called on a repository instance.
+
+This currently wraps command('config') so it is not so fast.
+
+=cut
+
+sub config_bool {
+       my ($self, $var) = @_;
+       $self->repo_path()
+               or throw Error::Simple("not a repository");
+
+       try {
+               my $val = $self->command_oneline('config', '--bool', '--get',
+                                             $var);
+               return undef unless defined $val;
+               return $val eq 'true';
+       } catch Git::Error::Command with {
+               my $E = shift;
+               if ($E->value() == 1) {
+                       # Key not found.
+                       return undef;
+               } else {
+                       throw $E;
+               }
+       };
+}
+
+=item config_int ( VARIABLE )
+
+Retrieve the integer configuration C<VARIABLE>. The return value
+is simple decimal number.  An optional value suffix of 'k', 'm',
+or 'g' in the config file will cause the value to be multiplied
+by 1024, 1048576 (1024^2), or 1073741824 (1024^3) prior to output.
+It would return C<undef> if configuration variable is not defined,
+
+Must be called on a repository instance.
+
+This currently wraps command('config') so it is not so fast.
+
+=cut
+
+sub config_int {
+       my ($self, $var) = @_;
+       $self->repo_path()
+               or throw Error::Simple("not a repository");
+
+       try {
+               return $self->command_oneline('config', '--int', '--get', $var);
+       } catch Git::Error::Command with {
+               my $E = shift;
+               if ($E->value() == 1) {
+                       # Key not found.
+                       return undef;
+               } else {
+                       throw $E;
+               }
+       };
+}
+
+=item get_colorbool ( NAME )
+
+Finds if color should be used for NAMEd operation from the configuration,
+and returns boolean (true for "use color", false for "do not use color").
+
+=cut
+
+sub get_colorbool {
+       my ($self, $var) = @_;
+       my $stdout_to_tty = (-t STDOUT) ? "true" : "false";
+       my $use_color = $self->command_oneline('config', '--get-colorbool',
+                                              $var, $stdout_to_tty);
+       return ($use_color eq 'true');
+}
+
+=item get_color ( SLOT, COLOR )
+
+Finds color for SLOT from the configuration, while defaulting to COLOR,
+and returns the ANSI color escape sequence:
+
+       print $repo->get_color("color.interactive.prompt", "underline blue white");
+       print "some text";
+       print $repo->get_color("", "normal");
+
+=cut
+
+sub get_color {
+       my ($self, $slot, $default) = @_;
+       my $color = $self->command_oneline('config', '--get-color', $slot, $default);
+       if (!defined $color) {
+               $color = "";
+       }
+       return $color;
+}
+
+=item ident ( TYPE | IDENTSTR )
+
+=item ident_person ( TYPE | IDENTSTR | IDENTARRAY )
+
+This suite of functions retrieves and parses ident information, as stored
+in the commit and tag objects or produced by C<var GIT_type_IDENT> (thus
+C<TYPE> can be either I<author> or I<committer>; case is insignificant).
+
+The C<ident> method retrieves the ident information from C<git-var>
+and either returns it as a scalar string or as an array with the fields parsed.
+Alternatively, it can take a prepared ident string (e.g. from the commit
+object) and just parse it.
+
+C<ident_person> returns the person part of the ident - name and email;
+it can take the same arguments as C<ident> or the array returned by C<ident>.
+
+The synopsis is like:
+
+       my ($name, $email, $time_tz) = ident('author');
+       "$name <$email>" eq ident_person('author');
+       "$name <$email>" eq ident_person($name);
+       $time_tz =~ /^\d+ [+-]\d{4}$/;
+
+Both methods must be called on a repository instance.
+
+=cut
+
+sub ident {
+       my ($self, $type) = @_;
+       my $identstr;
+       if (lc $type eq lc 'committer' or lc $type eq lc 'author') {
+               $identstr = $self->command_oneline('var', 'GIT_'.uc($type).'_IDENT');
+       } else {
+               $identstr = $type;
+       }
+       if (wantarray) {
+               return $identstr =~ /^(.*) <(.*)> (\d+ [+-]\d{4})$/;
+       } else {
+               return $identstr;
+       }
+}
+
+sub ident_person {
+       my ($self, @ident) = @_;
+       $#ident == 0 and @ident = $self->ident($ident[0]);
+       return "$ident[0] <$ident[1]>";
+}
+
+
+=item hash_object ( TYPE, FILENAME )
+
+Compute the SHA1 object id of the given C<FILENAME> (or data waiting in
+C<FILEHANDLE>) considering it is of the C<TYPE> object type (C<blob>,
+C<commit>, C<tree>).
+
+The method can be called without any instance or on a specified Git repository,
+it makes zero difference.
+
+The function returns the SHA1 hash.
+
+=cut
+
+# TODO: Support for passing FILEHANDLE instead of FILENAME
+sub hash_object {
+       my ($self, $type, $file) = _maybe_self(@_);
+       command_oneline('hash-object', '-t', $type, $file);
+}
+
+
+
+=back
+
+=head1 ERROR HANDLING
+
+All functions are supposed to throw Perl exceptions in case of errors.
+See the L<Error> module on how to catch those. Most exceptions are mere
+L<Error::Simple> instances.
+
+However, the C<command()>, C<command_oneline()> and C<command_noisy()>
+functions suite can throw C<Git::Error::Command> exceptions as well: those are
+thrown when the external command returns an error code and contain the error
+code as well as access to the captured command's output. The exception class
+provides the usual C<stringify> and C<value> (command's exit code) methods and
+in addition also a C<cmd_output> method that returns either an array or a
+string with the captured command output (depending on the original function
+call context; C<command_noisy()> returns C<undef>) and $<cmdline> which
+returns the command and its arguments (but without proper quoting).
+
+Note that the C<command_*_pipe()> functions cannot throw this exception since
+it has no idea whether the command failed or not. You will only find out
+at the time you C<close> the pipe; if you want to have that automated,
+use C<command_close_pipe()>, which can throw the exception.
+
+=cut
+
+{
+       package Git::Error::Command;
+
+       @Git::Error::Command::ISA = qw(Error);
+
+       sub new {
+               my $self = shift;
+               my $cmdline = '' . shift;
+               my $value = 0 + shift;
+               my $outputref = shift;
+               my(@args) = ();
+
+               local $Error::Depth = $Error::Depth + 1;
+
+               push(@args, '-cmdline', $cmdline);
+               push(@args, '-value', $value);
+               push(@args, '-outputref', $outputref);
+
+               $self->SUPER::new(-text => 'command returned error', @args);
+       }
+
+       sub stringify {
+               my $self = shift;
+               my $text = $self->SUPER::stringify;
+               $self->cmdline() . ': ' . $text . ': ' . $self->value() . "\n";
+       }
+
+       sub cmdline {
+               my $self = shift;
+               $self->{'-cmdline'};
+       }
+
+       sub cmd_output {
+               my $self = shift;
+               my $ref = $self->{'-outputref'};
+               defined $ref or undef;
+               if (ref $ref eq 'ARRAY') {
+                       return @$ref;
+               } else { # SCALAR
+                       return $$ref;
+               }
+       }
+}
+
+=over 4
+
+=item git_cmd_try { CODE } ERRMSG
+
+This magical statement will automatically catch any C<Git::Error::Command>
+exceptions thrown by C<CODE> and make your program die with C<ERRMSG>
+on its lips; the message will have %s substituted for the command line
+and %d for the exit status. This statement is useful mostly for producing
+more user-friendly error messages.
+
+In case of no exception caught the statement returns C<CODE>'s return value.
+
+Note that this is the only auto-exported function.
+
+=cut
+
+sub git_cmd_try(&$) {
+       my ($code, $errmsg) = @_;
+       my @result;
+       my $err;
+       my $array = wantarray;
+       try {
+               if ($array) {
+                       @result = &$code;
+               } else {
+                       $result[0] = &$code;
+               }
+       } catch Git::Error::Command with {
+               my $E = shift;
+               $err = $errmsg;
+               $err =~ s/\%s/$E->cmdline()/ge;
+               $err =~ s/\%d/$E->value()/ge;
+               # We can't croak here since Error.pm would mangle
+               # that to Error::Simple.
+       };
+       $err and croak $err;
+       return $array ? @result : $result[0];
+}
+
+
+=back
+
+=head1 COPYRIGHT
+
+Copyright 2006 by Petr Baudis E<lt>pasky@suse.czE<gt>.
+
+This module is free software; it may be used, copied, modified
+and distributed under the terms of the GNU General Public Licence,
+either version 2, or (at your option) any later version.
+
+=cut
+
+
+# Take raw method argument list and return ($obj, @args) in case
+# the method was called upon an instance and (undef, @args) if
+# it was called directly.
+sub _maybe_self {
+       # This breaks inheritance. Oh well.
+       ref $_[0] eq 'Git' ? @_ : (undef, @_);
+}
+
+# Check if the command id is something reasonable.
+sub _check_valid_cmd {
+       my ($cmd) = @_;
+       $cmd =~ /^[a-z0-9A-Z_-]+$/ or throw Error::Simple("bad command: $cmd");
+}
+
+# Common backend for the pipe creators.
+sub _command_common_pipe {
+       my $direction = shift;
+       my ($self, @p) = _maybe_self(@_);
+       my (%opts, $cmd, @args);
+       if (ref $p[0]) {
+               ($cmd, @args) = @{shift @p};
+               %opts = ref $p[0] ? %{$p[0]} : @p;
+       } else {
+               ($cmd, @args) = @p;
+       }
+       _check_valid_cmd($cmd);
+
+       my $fh;
+       if ($^O eq 'MSWin32') {
+               # ActiveState Perl
+               #defined $opts{STDERR} and
+               #       warn 'ignoring STDERR option - running w/ ActiveState';
+               $direction eq '-|' or
+                       die 'input pipe for ActiveState not implemented';
+               # the strange construction with *ACPIPE is just to
+               # explain the tie below that we want to bind to
+               # a handle class, not scalar. It is not known if
+               # it is something specific to ActiveState Perl or
+               # just a Perl quirk.
+               tie (*ACPIPE, 'Git::activestate_pipe', $cmd, @args);
+               $fh = *ACPIPE;
+
+       } else {
+               my $pid = open($fh, $direction);
+               if (not defined $pid) {
+                       throw Error::Simple("open failed: $!");
+               } elsif ($pid == 0) {
+                       if (defined $opts{STDERR}) {
+                               close STDERR;
+                       }
+                       if ($opts{STDERR}) {
+                               open (STDERR, '>&', $opts{STDERR})
+                                       or die "dup failed: $!";
+                       }
+                       _cmd_exec($self, $cmd, @args);
+               }
+       }
+       return wantarray ? ($fh, join(' ', $cmd, @args)) : $fh;
+}
+
+# When already in the subprocess, set up the appropriate state
+# for the given repository and execute the git command.
+sub _cmd_exec {
+       my ($self, @args) = @_;
+       if ($self) {
+               $self->repo_path() and $ENV{'GIT_DIR'} = $self->repo_path();
+               $self->wc_path() and chdir($self->wc_path());
+               $self->wc_subdir() and chdir($self->wc_subdir());
+       }
+       _execv_git_cmd(@args);
+       die qq[exec "@args" failed: $!];
+}
+
+# Execute the given Git command ($_[0]) with arguments ($_[1..])
+# by searching for it at proper places.
+sub _execv_git_cmd { exec('git', @_); }
+
+# Close pipe to a subprocess.
+sub _cmd_close {
+       my ($fh, $ctx) = @_;
+       if (not close $fh) {
+               if ($!) {
+                       # It's just close, no point in fatalities
+                       carp "error closing pipe: $!";
+               } elsif ($? >> 8) {
+                       # The caller should pepper this.
+                       throw Git::Error::Command($ctx, $? >> 8);
+               }
+               # else we might e.g. closed a live stream; the command
+               # dying of SIGPIPE would drive us here.
+       }
+}
+
+
+sub DESTROY { }
+
+
+# Pipe implementation for ActiveState Perl.
+
+package Git::activestate_pipe;
+use strict;
+
+sub TIEHANDLE {
+       my ($class, @params) = @_;
+       # FIXME: This is probably horrible idea and the thing will explode
+       # at the moment you give it arguments that require some quoting,
+       # but I have no ActiveState clue... --pasky
+       # Let's just hope ActiveState Perl does at least the quoting
+       # correctly.
+       my @data = qx{git @params};
+       bless { i => 0, data => \@data }, $class;
+}
+
+sub READLINE {
+       my $self = shift;
+       if ($self->{i} >= scalar @{$self->{data}}) {
+               return undef;
+       }
+       my $i = $self->{i};
+       if (wantarray) {
+               $self->{i} = $#{$self->{'data'}} + 1;
+               return splice(@{$self->{'data'}}, $i);
+       }
+       $self->{i} = $i + 1;
+       return $self->{'data'}->[ $i ];
+}
+
+sub CLOSE {
+       my $self = shift;
+       delete $self->{data};
+       delete $self->{i};
+}
+
+sub EOF {
+       my $self = shift;
+       return ($self->{i} >= scalar @{$self->{data}});
+}
+
+
+1; # Famous last words
diff --git a/perl/Makefile b/perl/Makefile
new file mode 100644 (file)
index 0000000..5e079ad
--- /dev/null
@@ -0,0 +1,42 @@
+#
+# Makefile for perl support modules and routine
+#
+makfile:=perl.mak
+
+PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
+prefix_SQ = $(subst ','\'',$(prefix))
+
+ifndef V
+       QUIET = @
+endif
+
+all install instlibdir: $(makfile)
+       $(QUIET)$(MAKE) -f $(makfile) $@
+
+clean:
+       $(QUIET)test -f $(makfile) && $(MAKE) -f $(makfile) $@ || exit 0
+       $(RM) ppport.h
+       $(RM) $(makfile)
+       $(RM) $(makfile).old
+
+ifdef NO_PERL_MAKEMAKER
+instdir_SQ = $(subst ','\'',$(prefix)/lib)
+$(makfile): ../GIT-CFLAGS Makefile
+       echo all: > $@
+       echo '  :' >> $@
+       echo install: >> $@
+       echo '  mkdir -p $(instdir_SQ)' >> $@
+       echo '  $(RM) $(instdir_SQ)/Git.pm; cp Git.pm $(instdir_SQ)' >> $@
+       echo '  $(RM) $(instdir_SQ)/Error.pm; \
+       cp private-Error.pm $(instdir_SQ)/Error.pm' >> $@
+       echo instlibdir: >> $@
+       echo '  echo $(instdir_SQ)' >> $@
+else
+$(makfile): Makefile.PL ../GIT-CFLAGS
+       $(PERL_PATH) $< PREFIX='$(prefix_SQ)'
+endif
+
+# this is just added comfort for calling make directly in perl dir
+# (even though GIT-CFLAGS aren't used yet. If ever)
+../GIT-CFLAGS:
+       $(MAKE) -C .. GIT-CFLAGS
diff --git a/perl/Makefile.PL b/perl/Makefile.PL
new file mode 100644 (file)
index 0000000..6aecd89
--- /dev/null
@@ -0,0 +1,34 @@
+use ExtUtils::MakeMaker;
+
+sub MY::postamble {
+       return <<'MAKE_FRAG';
+instlibdir:
+       @echo '$(INSTALLSITELIB)'
+
+MAKE_FRAG
+}
+
+my %pm = ('Git.pm' => '$(INST_LIBDIR)/Git.pm');
+
+# We come with our own bundled Error.pm. It's not in the set of default
+# Perl modules so install it if it's not available on the system yet.
+eval { require Error };
+if ($@ || $Error::VERSION < 0.15009) {
+       $pm{'private-Error.pm'} = '$(INST_LIBDIR)/Error.pm';
+}
+
+my %extra;
+$extra{DESTDIR} = $ENV{DESTDIR} if $ENV{DESTDIR};
+
+# redirect stdout, otherwise the message "Writing perl.mak for Git"
+# disrupts the output for the target 'instlibdir'
+open STDOUT, ">&STDERR";
+
+WriteMakefile(
+       NAME            => 'Git',
+       VERSION_FROM    => 'Git.pm',
+       PM              => \%pm,
+       MAKEFILE        => 'perl.mak',
+       INSTALLSITEMAN3DIR => '$(SITEPREFIX)/share/man/man3',
+       %extra
+);
diff --git a/perl/private-Error.pm b/perl/private-Error.pm
new file mode 100644 (file)
index 0000000..11e9cd9
--- /dev/null
@@ -0,0 +1,827 @@
+# Error.pm
+#
+# Copyright (c) 1997-8 Graham Barr <gbarr@ti.com>. All rights reserved.
+# This program is free software; you can redistribute it and/or
+# modify it under the same terms as Perl itself.
+#
+# Based on my original Error.pm, and Exceptions.pm by Peter Seibel
+# <peter@weblogic.com> and adapted by Jesse Glick <jglick@sig.bsh.com>.
+#
+# but modified ***significantly***
+
+package Error;
+
+use strict;
+use vars qw($VERSION);
+use 5.004;
+
+$VERSION = "0.15009";
+
+use overload (
+       '""'       =>   'stringify',
+       '0+'       =>   'value',
+       'bool'     =>   sub { return 1; },
+       'fallback' =>   1
+);
+
+$Error::Depth = 0;     # Depth to pass to caller()
+$Error::Debug = 0;     # Generate verbose stack traces
+@Error::STACK = ();    # Clause stack for try
+$Error::THROWN = undef;        # last error thrown, a workaround until die $ref works
+
+my $LAST;              # Last error created
+my %ERROR;             # Last error associated with package
+
+sub throw_Error_Simple
+{
+    my $args = shift;
+    return Error::Simple->new($args->{'text'});
+}
+
+$Error::ObjectifyCallback = \&throw_Error_Simple;
+
+
+# Exported subs are defined in Error::subs
+
+sub import {
+    shift;
+    local $Exporter::ExportLevel = $Exporter::ExportLevel + 1;
+    Error::subs->import(@_);
+}
+
+# I really want to use last for the name of this method, but it is a keyword
+# which prevent the syntax  last Error
+
+sub prior {
+    shift; # ignore
+
+    return $LAST unless @_;
+
+    my $pkg = shift;
+    return exists $ERROR{$pkg} ? $ERROR{$pkg} : undef
+       unless ref($pkg);
+
+    my $obj = $pkg;
+    my $err = undef;
+    if($obj->isa('HASH')) {
+       $err = $obj->{'__Error__'}
+           if exists $obj->{'__Error__'};
+    }
+    elsif($obj->isa('GLOB')) {
+       $err = ${*$obj}{'__Error__'}
+           if exists ${*$obj}{'__Error__'};
+    }
+
+    $err;
+}
+
+sub flush {
+    shift; #ignore
+
+    unless (@_) {
+       $LAST = undef;
+       return;
+    }
+
+    my $pkg = shift;
+    return unless ref($pkg);
+
+    undef $ERROR{$pkg} if defined $ERROR{$pkg};
+}
+
+# Return as much information as possible about where the error
+# happened. The -stacktrace element only exists if $Error::DEBUG
+# was set when the error was created
+
+sub stacktrace {
+    my $self = shift;
+
+    return $self->{'-stacktrace'}
+       if exists $self->{'-stacktrace'};
+
+    my $text = exists $self->{'-text'} ? $self->{'-text'} : "Died";
+
+    $text .= sprintf(" at %s line %d.\n", $self->file, $self->line)
+       unless($text =~ /\n$/s);
+
+    $text;
+}
+
+# Allow error propagation, ie
+#
+# $ber->encode(...) or
+#    return Error->prior($ber)->associate($ldap);
+
+sub associate {
+    my $err = shift;
+    my $obj = shift;
+
+    return unless ref($obj);
+
+    if($obj->isa('HASH')) {
+       $obj->{'__Error__'} = $err;
+    }
+    elsif($obj->isa('GLOB')) {
+       ${*$obj}{'__Error__'} = $err;
+    }
+    $obj = ref($obj);
+    $ERROR{ ref($obj) } = $err;
+
+    return;
+}
+
+sub new {
+    my $self = shift;
+    my($pkg,$file,$line) = caller($Error::Depth);
+
+    my $err = bless {
+       '-package' => $pkg,
+       '-file'    => $file,
+       '-line'    => $line,
+       @_
+    }, $self;
+
+    $err->associate($err->{'-object'})
+       if(exists $err->{'-object'});
+
+    # To always create a stacktrace would be very inefficient, so
+    # we only do it if $Error::Debug is set
+
+    if($Error::Debug) {
+       require Carp;
+       local $Carp::CarpLevel = $Error::Depth;
+       my $text = defined($err->{'-text'}) ? $err->{'-text'} : "Error";
+       my $trace = Carp::longmess($text);
+       # Remove try calls from the trace
+       $trace =~ s/(\n\s+\S+__ANON__[^\n]+)?\n\s+eval[^\n]+\n\s+Error::subs::try[^\n]+(?=\n)//sog;
+       $trace =~ s/(\n\s+\S+__ANON__[^\n]+)?\n\s+eval[^\n]+\n\s+Error::subs::run_clauses[^\n]+\n\s+Error::subs::try[^\n]+(?=\n)//sog;
+       $err->{'-stacktrace'} = $trace
+    }
+
+    $@ = $LAST = $ERROR{$pkg} = $err;
+}
+
+# Throw an error. this contains some very gory code.
+
+sub throw {
+    my $self = shift;
+    local $Error::Depth = $Error::Depth + 1;
+
+    # if we are not rethrow-ing then create the object to throw
+    $self = $self->new(@_) unless ref($self);
+
+    die $Error::THROWN = $self;
+}
+
+# syntactic sugar for
+#
+#    die with Error( ... );
+
+sub with {
+    my $self = shift;
+    local $Error::Depth = $Error::Depth + 1;
+
+    $self->new(@_);
+}
+
+# syntactic sugar for
+#
+#    record Error( ... ) and return;
+
+sub record {
+    my $self = shift;
+    local $Error::Depth = $Error::Depth + 1;
+
+    $self->new(@_);
+}
+
+# catch clause for
+#
+# try { ... } catch CLASS with { ... }
+
+sub catch {
+    my $pkg = shift;
+    my $code = shift;
+    my $clauses = shift || {};
+    my $catch = $clauses->{'catch'} ||= [];
+
+    unshift @$catch,  $pkg, $code;
+
+    $clauses;
+}
+
+# Object query methods
+
+sub object {
+    my $self = shift;
+    exists $self->{'-object'} ? $self->{'-object'} : undef;
+}
+
+sub file {
+    my $self = shift;
+    exists $self->{'-file'} ? $self->{'-file'} : undef;
+}
+
+sub line {
+    my $self = shift;
+    exists $self->{'-line'} ? $self->{'-line'} : undef;
+}
+
+sub text {
+    my $self = shift;
+    exists $self->{'-text'} ? $self->{'-text'} : undef;
+}
+
+# overload methods
+
+sub stringify {
+    my $self = shift;
+    defined $self->{'-text'} ? $self->{'-text'} : "Died";
+}
+
+sub value {
+    my $self = shift;
+    exists $self->{'-value'} ? $self->{'-value'} : undef;
+}
+
+package Error::Simple;
+
+@Error::Simple::ISA = qw(Error);
+
+sub new {
+    my $self  = shift;
+    my $text  = "" . shift;
+    my $value = shift;
+    my(@args) = ();
+
+    local $Error::Depth = $Error::Depth + 1;
+
+    @args = ( -file => $1, -line => $2)
+       if($text =~ s/\s+at\s+(\S+)\s+line\s+(\d+)(?:,\s*<[^>]*>\s+line\s+\d+)?\.?\n?$//s);
+    push(@args, '-value', 0 + $value)
+       if defined($value);
+
+    $self->SUPER::new(-text => $text, @args);
+}
+
+sub stringify {
+    my $self = shift;
+    my $text = $self->SUPER::stringify;
+    $text .= sprintf(" at %s line %d.\n", $self->file, $self->line)
+       unless($text =~ /\n$/s);
+    $text;
+}
+
+##########################################################################
+##########################################################################
+
+# Inspired by code from Jesse Glick <jglick@sig.bsh.com> and
+# Peter Seibel <peter@weblogic.com>
+
+package Error::subs;
+
+use Exporter ();
+use vars qw(@EXPORT_OK @ISA %EXPORT_TAGS);
+
+@EXPORT_OK   = qw(try with finally except otherwise);
+%EXPORT_TAGS = (try => \@EXPORT_OK);
+
+@ISA = qw(Exporter);
+
+
+sub blessed {
+       my $item = shift;
+       local $@; # don't kill an outer $@
+       ref $item and eval { $item->can('can') };
+}
+
+
+sub run_clauses ($$$\@) {
+    my($clauses,$err,$wantarray,$result) = @_;
+    my $code = undef;
+
+    $err = $Error::ObjectifyCallback->({'text' =>$err}) unless ref($err);
+
+    CATCH: {
+
+       # catch
+       my $catch;
+       if(defined($catch = $clauses->{'catch'})) {
+           my $i = 0;
+
+           CATCHLOOP:
+           for( ; $i < @$catch ; $i += 2) {
+               my $pkg = $catch->[$i];
+               unless(defined $pkg) {
+                   #except
+                   splice(@$catch,$i,2,$catch->[$i+1]->());
+                   $i -= 2;
+                   next CATCHLOOP;
+               }
+               elsif(blessed($err) && $err->isa($pkg)) {
+                   $code = $catch->[$i+1];
+                   while(1) {
+                       my $more = 0;
+                       local($Error::THROWN);
+                       my $ok = eval {
+                           if($wantarray) {
+                               @{$result} = $code->($err,\$more);
+                           }
+                           elsif(defined($wantarray)) {
+                               @{$result} = ();
+                               $result->[0] = $code->($err,\$more);
+                           }
+                           else {
+                               $code->($err,\$more);
+                           }
+                           1;
+                       };
+                       if( $ok ) {
+                           next CATCHLOOP if $more;
+                           undef $err;
+                       }
+                       else {
+                           $err = defined($Error::THROWN)
+                                   ? $Error::THROWN : $@;
+                $err = $Error::ObjectifyCallback->({'text' =>$err})
+                    unless ref($err);
+                       }
+                       last CATCH;
+                   };
+               }
+           }
+       }
+
+       # otherwise
+       my $owise;
+       if(defined($owise = $clauses->{'otherwise'})) {
+           my $code = $clauses->{'otherwise'};
+           my $more = 0;
+           my $ok = eval {
+               if($wantarray) {
+                   @{$result} = $code->($err,\$more);
+               }
+               elsif(defined($wantarray)) {
+                   @{$result} = ();
+                   $result->[0] = $code->($err,\$more);
+               }
+               else {
+                   $code->($err,\$more);
+               }
+               1;
+           };
+           if( $ok ) {
+               undef $err;
+           }
+           else {
+               $err = defined($Error::THROWN)
+                       ? $Error::THROWN : $@;
+
+        $err = $Error::ObjectifyCallback->({'text' =>$err})
+            unless ref($err);
+           }
+       }
+    }
+    $err;
+}
+
+sub try (&;$) {
+    my $try = shift;
+    my $clauses = @_ ? shift : {};
+    my $ok = 0;
+    my $err = undef;
+    my @result = ();
+
+    unshift @Error::STACK, $clauses;
+
+    my $wantarray = wantarray();
+
+    do {
+       local $Error::THROWN = undef;
+    local $@ = undef;
+
+       $ok = eval {
+           if($wantarray) {
+               @result = $try->();
+           }
+           elsif(defined $wantarray) {
+               $result[0] = $try->();
+           }
+           else {
+               $try->();
+           }
+           1;
+       };
+
+       $err = defined($Error::THROWN) ? $Error::THROWN : $@
+           unless $ok;
+    };
+
+    shift @Error::STACK;
+
+    $err = run_clauses($clauses,$err,wantarray,@result)
+       unless($ok);
+
+    $clauses->{'finally'}->()
+       if(defined($clauses->{'finally'}));
+
+    if (defined($err))
+    {
+        if (blessed($err) && $err->can('throw'))
+        {
+            throw $err;
+        }
+        else
+        {
+            die $err;
+        }
+    }
+
+    wantarray ? @result : $result[0];
+}
+
+# Each clause adds a sub to the list of clauses. The finally clause is
+# always the last, and the otherwise clause is always added just before
+# the finally clause.
+#
+# All clauses, except the finally clause, add a sub which takes one argument
+# this argument will be the error being thrown. The sub will return a code ref
+# if that clause can handle that error, otherwise undef is returned.
+#
+# The otherwise clause adds a sub which unconditionally returns the users
+# code reference, this is why it is forced to be last.
+#
+# The catch clause is defined in Error.pm, as the syntax causes it to
+# be called as a method
+
+sub with (&;$) {
+    @_
+}
+
+sub finally (&) {
+    my $code = shift;
+    my $clauses = { 'finally' => $code };
+    $clauses;
+}
+
+# The except clause is a block which returns a hashref or a list of
+# key-value pairs, where the keys are the classes and the values are subs.
+
+sub except (&;$) {
+    my $code = shift;
+    my $clauses = shift || {};
+    my $catch = $clauses->{'catch'} ||= [];
+
+    my $sub = sub {
+       my $ref;
+       my(@array) = $code->($_[0]);
+       if(@array == 1 && ref($array[0])) {
+           $ref = $array[0];
+           $ref = [ %$ref ]
+               if(UNIVERSAL::isa($ref,'HASH'));
+       }
+       else {
+           $ref = \@array;
+       }
+       @$ref
+    };
+
+    unshift @{$catch}, undef, $sub;
+
+    $clauses;
+}
+
+sub otherwise (&;$) {
+    my $code = shift;
+    my $clauses = shift || {};
+
+    if(exists $clauses->{'otherwise'}) {
+       require Carp;
+       Carp::croak("Multiple otherwise clauses");
+    }
+
+    $clauses->{'otherwise'} = $code;
+
+    $clauses;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Error - Error/exception handling in an OO-ish way
+
+=head1 SYNOPSIS
+
+    use Error qw(:try);
+
+    throw Error::Simple( "A simple error");
+
+    sub xyz {
+        ...
+       record Error::Simple("A simple error")
+           and return;
+    }
+
+    unlink($file) or throw Error::Simple("$file: $!",$!);
+
+    try {
+       do_some_stuff();
+       die "error!" if $condition;
+       throw Error::Simple -text => "Oops!" if $other_condition;
+    }
+    catch Error::IO with {
+       my $E = shift;
+       print STDERR "File ", $E->{'-file'}, " had a problem\n";
+    }
+    except {
+       my $E = shift;
+       my $general_handler=sub {send_message $E->{-description}};
+       return {
+           UserException1 => $general_handler,
+           UserException2 => $general_handler
+       };
+    }
+    otherwise {
+       print STDERR "Well I don't know what to say\n";
+    }
+    finally {
+       close_the_garage_door_already(); # Should be reliable
+    }; # Don't forget the trailing ; or you might be surprised
+
+=head1 DESCRIPTION
+
+The C<Error> package provides two interfaces. Firstly C<Error> provides
+a procedural interface to exception handling. Secondly C<Error> is a
+base class for errors/exceptions that can either be thrown, for
+subsequent catch, or can simply be recorded.
+
+Errors in the class C<Error> should not be thrown directly, but the
+user should throw errors from a sub-class of C<Error>.
+
+=head1 PROCEDURAL INTERFACE
+
+C<Error> exports subroutines to perform exception handling. These will
+be exported if the C<:try> tag is used in the C<use> line.
+
+=over 4
+
+=item try BLOCK CLAUSES
+
+C<try> is the main subroutine called by the user. All other subroutines
+exported are clauses to the try subroutine.
+
+The BLOCK will be evaluated and, if no error is throw, try will return
+the result of the block.
+
+C<CLAUSES> are the subroutines below, which describe what to do in the
+event of an error being thrown within BLOCK.
+
+=item catch CLASS with BLOCK
+
+This clauses will cause all errors that satisfy C<$err-E<gt>isa(CLASS)>
+to be caught and handled by evaluating C<BLOCK>.
+
+C<BLOCK> will be passed two arguments. The first will be the error
+being thrown. The second is a reference to a scalar variable. If this
+variable is set by the catch block then, on return from the catch
+block, try will continue processing as if the catch block was never
+found.
+
+To propagate the error the catch block may call C<$err-E<gt>throw>
+
+If the scalar reference by the second argument is not set, and the
+error is not thrown. Then the current try block will return with the
+result from the catch block.
+
+=item except BLOCK
+
+When C<try> is looking for a handler, if an except clause is found
+C<BLOCK> is evaluated. The return value from this block should be a
+HASHREF or a list of key-value pairs, where the keys are class names
+and the values are CODE references for the handler of errors of that
+type.
+
+=item otherwise BLOCK
+
+Catch any error by executing the code in C<BLOCK>
+
+When evaluated C<BLOCK> will be passed one argument, which will be the
+error being processed.
+
+Only one otherwise block may be specified per try block
+
+=item finally BLOCK
+
+Execute the code in C<BLOCK> either after the code in the try block has
+successfully completed, or if the try block throws an error then
+C<BLOCK> will be executed after the handler has completed.
+
+If the handler throws an error then the error will be caught, the
+finally block will be executed and the error will be re-thrown.
+
+Only one finally block may be specified per try block
+
+=back
+
+=head1 CLASS INTERFACE
+
+=head2 CONSTRUCTORS
+
+The C<Error> object is implemented as a HASH. This HASH is initialized
+with the arguments that are passed to it's constructor. The elements
+that are used by, or are retrievable by the C<Error> class are listed
+below, other classes may add to these.
+
+       -file
+       -line
+       -text
+       -value
+       -object
+
+If C<-file> or C<-line> are not specified in the constructor arguments
+then these will be initialized with the file name and line number where
+the constructor was called from.
+
+If the error is associated with an object then the object should be
+passed as the C<-object> argument. This will allow the C<Error> package
+to associate the error with the object.
+
+The C<Error> package remembers the last error created, and also the
+last error associated with a package. This could either be the last
+error created by a sub in that package, or the last error which passed
+an object blessed into that package as the C<-object> argument.
+
+=over 4
+
+=item throw ( [ ARGS ] )
+
+Create a new C<Error> object and throw an error, which will be caught
+by a surrounding C<try> block, if there is one. Otherwise it will cause
+the program to exit.
+
+C<throw> may also be called on an existing error to re-throw it.
+
+=item with ( [ ARGS ] )
+
+Create a new C<Error> object and returns it. This is defined for
+syntactic sugar, eg
+
+    die with Some::Error ( ... );
+
+=item record ( [ ARGS ] )
+
+Create a new C<Error> object and returns it. This is defined for
+syntactic sugar, eg
+
+    record Some::Error ( ... )
+       and return;
+
+=back
+
+=head2 STATIC METHODS
+
+=over 4
+
+=item prior ( [ PACKAGE ] )
+
+Return the last error created, or the last error associated with
+C<PACKAGE>
+
+=item flush ( [ PACKAGE ] )
+
+Flush the last error created, or the last error associated with
+C<PACKAGE>.It is necessary to clear the error stack before exiting the
+package or uncaught errors generated using C<record> will be reported.
+
+     $Error->flush;
+
+=cut
+
+=back
+
+=head2 OBJECT METHODS
+
+=over 4
+
+=item stacktrace
+
+If the variable C<$Error::Debug> was non-zero when the error was
+created, then C<stacktrace> returns a string created by calling
+C<Carp::longmess>. If the variable was zero the C<stacktrace> returns
+the text of the error appended with the filename and line number of
+where the error was created, providing the text does not end with a
+newline.
+
+=item object
+
+The object this error was associated with
+
+=item file
+
+The file where the constructor of this error was called from
+
+=item line
+
+The line where the constructor of this error was called from
+
+=item text
+
+The text of the error
+
+=back
+
+=head2 OVERLOAD METHODS
+
+=over 4
+
+=item stringify
+
+A method that converts the object into a string. This method may simply
+return the same as the C<text> method, or it may append more
+information. For example the file name and line number.
+
+By default this method returns the C<-text> argument that was passed to
+the constructor, or the string C<"Died"> if none was given.
+
+=item value
+
+A method that will return a value that can be associated with the
+error. For example if an error was created due to the failure of a
+system call, then this may return the numeric value of C<$!> at the
+time.
+
+By default this method returns the C<-value> argument that was passed
+to the constructor.
+
+=back
+
+=head1 PRE-DEFINED ERROR CLASSES
+
+=over 4
+
+=item Error::Simple
+
+This class can be used to hold simple error strings and values. It's
+constructor takes two arguments. The first is a text value, the second
+is a numeric value. These values are what will be returned by the
+overload methods.
+
+If the text value ends with C<at file line 1> as $@ strings do, then
+this infomation will be used to set the C<-file> and C<-line> arguments
+of the error object.
+
+This class is used internally if an eval'd block die's with an error
+that is a plain string. (Unless C<$Error::ObjectifyCallback> is modified)
+
+=back
+
+=head1 $Error::ObjectifyCallback
+
+This variable holds a reference to a subroutine that converts errors that
+are plain strings to objects. It is used by Error.pm to convert textual
+errors to objects, and can be overridden by the user.
+
+It accepts a single argument which is a hash reference to named parameters.
+Currently the only named parameter passed is C<'text'> which is the text
+of the error, but others may be available in the future.
+
+For example the following code will cause Error.pm to throw objects of the
+class MyError::Bar by default:
+
+    sub throw_MyError_Bar
+    {
+        my $args = shift;
+        my $err = MyError::Bar->new();
+        $err->{'MyBarText'} = $args->{'text'};
+        return $err;
+    }
+
+    {
+        local $Error::ObjectifyCallback = \&throw_MyError_Bar;
+
+        # Error handling here.
+    }
+
+=head1 KNOWN BUGS
+
+None, but that does not mean there are not any.
+
+=head1 AUTHORS
+
+Graham Barr <gbarr@pobox.com>
+
+The code that inspired me to write this was originally written by
+Peter Seibel <peter@weblogic.com> and adapted by Jesse Glick
+<jglick@sig.bsh.com>.
+
+=head1 MAINTAINER
+
+Shlomi Fish <shlomif@iglu.org.il>
+
+=head1 PAST MAINTAINERS
+
+Arun Kumar U <u_arunkumar@yahoo.com>
+
+=cut
diff --git a/pkt-line.c b/pkt-line.c
new file mode 100644 (file)
index 0000000..355546a
--- /dev/null
@@ -0,0 +1,114 @@
+#include "cache.h"
+#include "pkt-line.h"
+
+/*
+ * Write a packetized stream, where each line is preceded by
+ * its length (including the header) as a 4-byte hex number.
+ * A length of 'zero' means end of stream (and a length of 1-3
+ * would be an error).
+ *
+ * This is all pretty stupid, but we use this packetized line
+ * format to make a streaming format possible without ever
+ * over-running the read buffers. That way we'll never read
+ * into what might be the pack data (which should go to another
+ * process entirely).
+ *
+ * The writing side could use stdio, but since the reading
+ * side can't, we stay with pure read/write interfaces.
+ */
+ssize_t safe_write(int fd, const void *buf, ssize_t n)
+{
+       ssize_t nn = n;
+       while (n) {
+               int ret = xwrite(fd, buf, n);
+               if (ret > 0) {
+                       buf = (char *) buf + ret;
+                       n -= ret;
+                       continue;
+               }
+               if (!ret)
+                       die("write error (disk full?)");
+               die("write error (%s)", strerror(errno));
+       }
+       return nn;
+}
+
+/*
+ * If we buffered things up above (we don't, but we should),
+ * we'd flush it here
+ */
+void packet_flush(int fd)
+{
+       safe_write(fd, "0000", 4);
+}
+
+#define hex(a) (hexchar[(a) & 15])
+void packet_write(int fd, const char *fmt, ...)
+{
+       static char buffer[1000];
+       static char hexchar[] = "0123456789abcdef";
+       va_list args;
+       unsigned n;
+
+       va_start(args, fmt);
+       n = vsnprintf(buffer + 4, sizeof(buffer) - 4, fmt, args);
+       va_end(args);
+       if (n >= sizeof(buffer)-4)
+               die("protocol error: impossibly long line");
+       n += 4;
+       buffer[0] = hex(n >> 12);
+       buffer[1] = hex(n >> 8);
+       buffer[2] = hex(n >> 4);
+       buffer[3] = hex(n);
+       safe_write(fd, buffer, n);
+}
+
+static void safe_read(int fd, void *buffer, unsigned size)
+{
+       size_t n = 0;
+
+       while (n < size) {
+               ssize_t ret = xread(fd, (char *) buffer + n, size - n);
+               if (ret < 0)
+                       die("read error (%s)", strerror(errno));
+               if (!ret)
+                       die("The remote end hung up unexpectedly");
+               n += ret;
+       }
+}
+
+int packet_read_line(int fd, char *buffer, unsigned size)
+{
+       int n;
+       unsigned len;
+       char linelen[4];
+
+       safe_read(fd, linelen, 4);
+
+       len = 0;
+       for (n = 0; n < 4; n++) {
+               unsigned char c = linelen[n];
+               len <<= 4;
+               if (c >= '0' && c <= '9') {
+                       len += c - '0';
+                       continue;
+               }
+               if (c >= 'a' && c <= 'f') {
+                       len += c - 'a' + 10;
+                       continue;
+               }
+               if (c >= 'A' && c <= 'F') {
+                       len += c - 'A' + 10;
+                       continue;
+               }
+               die("protocol error: bad line length character");
+       }
+       if (!len)
+               return 0;
+       len -= 4;
+       if (len >= size)
+               die("protocol error: bad line length %d", len);
+       safe_read(fd, buffer, len);
+       buffer[len] = 0;
+       return len;
+}
diff --git a/pkt-line.h b/pkt-line.h
new file mode 100644 (file)
index 0000000..9df653f
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef PKTLINE_H
+#define PKTLINE_H
+
+#include "git-compat-util.h"
+
+/*
+ * Silly packetized line writing interface
+ */
+void packet_flush(int fd);
+void packet_write(int fd, const char *fmt, ...) __attribute__((format (printf, 2, 3)));
+
+int packet_read_line(int fd, char *buffer, unsigned size);
+ssize_t safe_write(int, const void *, ssize_t);
+
+#endif
diff --git a/po/.gitignore b/po/.gitignore
deleted file mode 100644 (file)
index a89cf44..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-*.msg
-*~
diff --git a/po/README b/po/README
deleted file mode 100644 (file)
index 9d8b736..0000000
--- a/po/README
+++ /dev/null
@@ -1,209 +0,0 @@
-Localizing git-gui for your language
-====================================
-
-This short note is to help you, who reads and writes English and your
-own language, help us getting git-gui localized for more languages.  It
-does not try to be a comprehensive manual of GNU gettext, which is the
-i18n framework we use, but tries to help you get started by covering the
-basics and how it is used in this project.
-
-1. Getting started.
-
-You would first need to have a working "git".  Your distribution may
-have it as "git-core" package (do not get "GNU Interactive Tools" --
-that is a different "git").  You would also need GNU gettext toolchain
-to test the resulting translation out.  Although you can work on message
-translation files with a regular text editor, it is a good idea to have
-specialized so-called "po file editors" (e.g. emacs po-mode, KBabel,
-poedit, GTranslator --- any of them would work well).  Please install
-them.
-
-You would then need to clone the git-gui internationalization project
-repository, so that you can work on it:
-
-       $ git clone mob@repo.or.cz:/srv/git/git-gui/git-gui-i18n.git/
-       $ cd git-gui-i18n
-       $ git checkout --track -b mob origin/mob
-       $ git config remote.origin.push mob
-
-The "git checkout" command creates a 'mob' branch from upstream's
-corresponding branch and makes it your current branch.  You will be
-working on this branch.
-
-The "git config" command records in your repository configuration file
-that you would push "mob" branch to the upstream when you say "git
-push".
-
-
-2. Starting a new language.
-
-In the git-gui-i18n directory is a po/ subdirectory.  It has a
-handful files whose names end with ".po".  Is there a file that has
-messages in your language?
-
-If you do not know what your language should be named, you need to find
-it.  This currently follows ISO 639-1 two letter codes:
-
-       http://www.loc.gov/standards/iso639-2/php/code_list.php
-
-For example, if you are preparing a translation for Afrikaans, the
-language code is "af".  If there already is a translation for your
-language, you do not have to perform any step in this section, but keep
-reading, because we are covering the basics.
-
-If you did not find your language, you would need to start one yourself.
-Copy po/git-gui.pot file to po/af.po (replace "af" with the code for
-your language).  Edit the first several lines to match existing *.po
-files to make it clear this is a translation table for git-gui project,
-and you are the primary translator.  The result of your editing would
-look something like this:
-
-    # Translation of git-gui to Afrikaans
-    # Copyright (C) 2007 Shawn Pearce
-    # This file is distributed under the same license as the git-gui package.
-    # YOUR NAME <YOUR@E-MAIL.ADDRESS>, 2007.
-    #
-    #, fuzzy
-    msgid ""
-    msgstr ""
-    "Project-Id-Version: git-gui\n"
-    "Report-Msgid-Bugs-To: \n"
-    "POT-Creation-Date: 2007-07-24 22:19+0300\n"
-    "PO-Revision-Date: 2007-07-25 18:00+0900\n"
-    "Last-Translator: YOUR NAME <YOUR@E-MAIL.ADDRESS>\n"
-    "Language-Team: Afrikaans\n"
-    "MIME-Version: 1.0\n"
-    "Content-Type: text/plain; charset=UTF-8\n"
-    "Content-Transfer-Encoding: 8bit\n"
-
-You will find many pairs of a "msgid" line followed by a "msgstr" line.
-These pairs define how messages in git-gui application are translated to
-your language.  Your primarily job is to fill in the empty double quote
-pairs on msgstr lines with the translation of the strings on their
-matching msgid lines.  A few tips:
-
- - Control characters, such as newlines, are written in backslash
-   sequence similar to string literals in the C programming language.
-   When the string given on a msgid line has such a backslash sequence,
-   you would typically want to have corresponding ones in the string on
-   your msgstr line.
-
- - Some messages contain an optional context indicator at the end,
-   for example "@@noun" or "@@verb".  This indicator allows the
-   software to select the correct translation depending upon the use.
-   The indicator is not actually part of the message and will not
-   be shown to the end-user.
-
-   If your language does not require a different translation you
-   will still need to translate both messages.
-
- - Often the messages being translated are format strings given to
-   "printf()"-like functions.  Make sure "%s", "%d", and "%%" in your
-   translated messages match the original.
-
-   When you have to change the order of words, you can add "<number>\$"
-   between '%' and the conversion ('s', 'd', etc.) to say "<number>-th
-   parameter to the format string is used at this point".  For example,
-   if the original message is like this:
-
-       "Length is %d, Weight is %d"
-
-   and if for whatever reason your translation needs to say weight first
-   and then length, you can say something like:
-
-       "WEIGHT IS %2\$d, LENGTH IS %1\$d"
-
-   The reason you need a backslash before dollar sign is because
-   this is a double quoted string in Tcl language, and without
-   it the letter introduces a variable interpolation, which you
-   do not want here.
-
- - A long message can be split across multiple lines by ending the
-   string with a double quote, and starting another string on the next
-   line with another double quote.  They will be concatenated in the
-   result.  For example:
-
-   #: lib/remote_branch_delete.tcl:189
-   #, tcl-format
-   msgid ""
-   "One or more of the merge tests failed because you have not fetched the "
-   "necessary commits.  Try fetching from %s first."
-   msgstr ""
-   "HERE YOU WILL WRITE YOUR TRANSLATION OF THE ABOVE LONG "
-   "MESSAGE IN YOUR LANGUAGE."
-
-You can test your translation by running "make install", which would
-create po/af.msg file and installs the result, and then running the
-resulting git-gui under your locale:
-
-       $ make install
-       $ LANG=af git-gui
-
-There is a trick to test your translation without first installing:
-
-       $ make
-       $ LANG=af ./git-gui.sh
-
-When you are satisfied with your translation, commit your changes, and
-push it back to the 'mob' branch:
-
-       $ edit po/af.po
-       ... be sure to update Last-Translator: and
-       ... PO-Revision-Date: lines.
-       $ git add po/af.po
-       $ git commit -m 'Started Afrikaans translation.'
-       $ git push
-
-
-3. Updating your translation.
-
-There may already be a translation for your language, and you may want
-to contribute an update.  This may be because you would want to improve
-the translation of existing messages, or because the git-gui software
-itself was updated and there are new messages that need translation.
-
-In any case, make sure you are up-to-date before starting your work:
-
-       $ git pull
-
-In the former case, you will edit po/af.po (again, replace "af" with
-your language code), and after testing and updating the Last-Translator:
-and PO-Revision-Date: lines, "add/commit/push" as in the previous
-section.
-
-By comparing "POT-Creation-Date:" line in po/git-gui.pot file and
-po/af.po file, you can tell if there are new messages that need to be
-translated.  You would need the GNU gettext package to perform this
-step.
-
-       $ msgmerge -U po/af.po po/git-gui.pot
-
-[NEEDSWORK: who is responsible for updating po/git-gui.pot file by
-running xgettext?  IIRC, Christian recommended against running it
-nilly-willy because it can become a source of unnecessary merge
-conflicts.  Perhaps we should mention something like "
-
-The po/git-gui.pot file is updated by the internationalization
-coordinator from time to time.  You _could_ update it yourself, but
-translators are discouraged from doing so because we would want all
-language teams to be working off of the same version of git-gui.pot.
-
-" here?]
-
-This updates po/af.po (again, replace "af" with your language
-code) so that it contains msgid lines (i.e. the original) that
-your translation did not have before.  There are a few things to
-watch out for:
-
- - The original text in English of an older message you already
-   translated might have been changed.  You will notice a comment line
-   that begins with "#, fuzzy" in front of such a message.  msgmerge
-   tool made its best effort to match your old translation with the
-   message from the updated software, but you may find cases that it
-   matched your old translated message to a new msgid and the pairing
-   does not make any sense -- you would need to fix them, and then
-   remove the "#, fuzzy" line from the message (your fixed translation
-   of the message will not be used before you remove the marker).
-
- - New messages added to the software will have msgstr lines with empty
-   strings.  You would need to translate them.
diff --git a/po/de.po b/po/de.po
deleted file mode 100644 (file)
index ac8ae0a..0000000
--- a/po/de.po
+++ /dev/null
@@ -1,1904 +0,0 @@
-# Translation of git-gui to German.
-# Copyright (C) 2007 Shawn Pearce, et al.
-# This file is distributed under the same license as the git package.
-# Christian Stimming <stimming@tuhh.de>, 2007
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: git-gui\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2007-11-24 10:36+0100\n"
-"PO-Revision-Date: 2007-11-24 10:55+0100\n"
-"Last-Translator: Christian Stimming <stimming@tuhh.de>\n"
-"Language-Team: German\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
-#: git-gui.sh:733
-msgid "git-gui: fatal error"
-msgstr "git-gui: Programmfehler"
-
-#: git-gui.sh:565
-#, tcl-format
-msgid "Invalid font specified in %s:"
-msgstr "Ungültige Zeichensatz-Angabe in %s:"
-
-#: git-gui.sh:590
-msgid "Main Font"
-msgstr "Programmschriftart"
-
-#: git-gui.sh:591
-msgid "Diff/Console Font"
-msgstr "Vergleich-Schriftart"
-
-#: git-gui.sh:605
-msgid "Cannot find git in PATH."
-msgstr "Git kann im PATH nicht gefunden werden."
-
-#: git-gui.sh:632
-msgid "Cannot parse Git version string:"
-msgstr "Git Versionsangabe kann nicht erkannt werden:"
-
-#: git-gui.sh:650
-#, tcl-format
-msgid ""
-"Git version cannot be determined.\n"
-"\n"
-"%s claims it is version '%s'.\n"
-"\n"
-"%s requires at least Git 1.5.0 or later.\n"
-"\n"
-"Assume '%s' is version 1.5.0?\n"
-msgstr ""
-"Die Version von Git kann nicht bestimmt werden.\n"
-"\n"
-"»%s« behauptet, es sei Version »%s«.\n"
-"\n"
-"%s benötigt mindestens Git 1.5.0 oder höher.\n"
-"\n"
-"Soll angenommen werden, »%s« sei Version 1.5.0?\n"
-
-#: git-gui.sh:888
-msgid "Git directory not found:"
-msgstr "Git-Verzeichnis nicht gefunden:"
-
-#: git-gui.sh:895
-msgid "Cannot move to top of working directory:"
-msgstr ""
-"Es konnte nicht in das oberste Verzeichnis der Arbeitskopie gewechselt "
-"werden:"
-
-#: git-gui.sh:902
-msgid "Cannot use funny .git directory:"
-msgstr "Unerwartete Struktur des .git Verzeichnis:"
-
-#: git-gui.sh:907
-msgid "No working directory"
-msgstr "Kein Arbeitsverzeichnis"
-
-#: git-gui.sh:1054
-msgid "Refreshing file status..."
-msgstr "Dateistatus aktualisieren..."
-
-#: git-gui.sh:1119
-msgid "Scanning for modified files ..."
-msgstr "Nach geänderten Dateien suchen..."
-
-#: git-gui.sh:1294 lib/browser.tcl:245
-msgid "Ready."
-msgstr "Bereit."
-
-#: git-gui.sh:1560
-msgid "Unmodified"
-msgstr "Unverändert"
-
-#: git-gui.sh:1562
-msgid "Modified, not staged"
-msgstr "Verändert, nicht bereitgestellt"
-
-#: git-gui.sh:1563 git-gui.sh:1568
-msgid "Staged for commit"
-msgstr "Bereitgestellt zum Eintragen"
-
-#: git-gui.sh:1564 git-gui.sh:1569
-msgid "Portions staged for commit"
-msgstr "Teilweise bereitgestellt zum Eintragen"
-
-#: git-gui.sh:1565 git-gui.sh:1570
-msgid "Staged for commit, missing"
-msgstr "Bereitgestellt zum Eintragen, fehlend"
-
-#: git-gui.sh:1567
-msgid "Untracked, not staged"
-msgstr "Nicht unter Versionskontrolle, nicht bereitgestellt"
-
-#: git-gui.sh:1572
-msgid "Missing"
-msgstr "Fehlend"
-
-#: git-gui.sh:1573
-msgid "Staged for removal"
-msgstr "Bereitgestellt zum Löschen"
-
-#: git-gui.sh:1574
-msgid "Staged for removal, still present"
-msgstr "Bereitgestellt zum Löschen, trotzdem vorhanden"
-
-#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
-msgid "Requires merge resolution"
-msgstr "Konfliktauflösung nötig"
-
-#: git-gui.sh:1614
-msgid "Starting gitk... please wait..."
-msgstr "Gitk wird gestartet... bitte warten."
-
-#: git-gui.sh:1623
-#, tcl-format
-msgid ""
-"Unable to start gitk:\n"
-"\n"
-"%s does not exist"
-msgstr ""
-"Gitk kann nicht gestartet werden:\n"
-"\n"
-"%s existiert nicht"
-
-#: git-gui.sh:1823 lib/choose_repository.tcl:35
-msgid "Repository"
-msgstr "Projektarchiv"
-
-#: git-gui.sh:1824
-msgid "Edit"
-msgstr "Bearbeiten"
-
-#: git-gui.sh:1826 lib/choose_rev.tcl:560
-msgid "Branch"
-msgstr "Zweig"
-
-#: git-gui.sh:1829 lib/choose_rev.tcl:547
-msgid "Commit@@noun"
-msgstr "Version"
-
-#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
-msgid "Merge"
-msgstr "Zusammenführen"
-
-#: git-gui.sh:1833 lib/choose_rev.tcl:556
-msgid "Remote"
-msgstr "Andere Archive"
-
-#: git-gui.sh:1842
-msgid "Browse Current Branch's Files"
-msgstr "Aktuellen Zweig durchblättern"
-
-#: git-gui.sh:1846
-msgid "Browse Branch Files..."
-msgstr "Einen Zweig durchblättern..."
-
-#: git-gui.sh:1851
-msgid "Visualize Current Branch's History"
-msgstr "Aktuellen Zweig darstellen"
-
-#: git-gui.sh:1855
-msgid "Visualize All Branch History"
-msgstr "Alle Zweige darstellen"
-
-#: git-gui.sh:1862
-#, tcl-format
-msgid "Browse %s's Files"
-msgstr "Zweig »%s« durchblättern"
-
-#: git-gui.sh:1864
-#, tcl-format
-msgid "Visualize %s's History"
-msgstr "Historie von »%s« darstellen"
-
-#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
-msgid "Database Statistics"
-msgstr "Datenbankstatistik"
-
-#: git-gui.sh:1872 lib/database.tcl:34
-msgid "Compress Database"
-msgstr "Datenbank komprimieren"
-
-#: git-gui.sh:1875
-msgid "Verify Database"
-msgstr "Datenbank überprüfen"
-
-#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
-#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
-msgid "Create Desktop Icon"
-msgstr "Desktop-Icon erstellen"
-
-#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
-msgid "Quit"
-msgstr "Beenden"
-
-#: git-gui.sh:1902
-msgid "Undo"
-msgstr "Rückgängig"
-
-#: git-gui.sh:1905
-msgid "Redo"
-msgstr "Wiederholen"
-
-#: git-gui.sh:1909 git-gui.sh:2403
-msgid "Cut"
-msgstr "Ausschneiden"
-
-#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
-#: lib/console.tcl:67
-msgid "Copy"
-msgstr "Kopieren"
-
-#: git-gui.sh:1915 git-gui.sh:2409
-msgid "Paste"
-msgstr "Einfügen"
-
-#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
-#: lib/remote_branch_delete.tcl:38
-msgid "Delete"
-msgstr "Löschen"
-
-#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
-msgid "Select All"
-msgstr "Alle auswählen"
-
-#: git-gui.sh:1931
-msgid "Create..."
-msgstr "Erstellen..."
-
-#: git-gui.sh:1937
-msgid "Checkout..."
-msgstr "Umstellen..."
-
-#: git-gui.sh:1943
-msgid "Rename..."
-msgstr "Umbenennen..."
-
-#: git-gui.sh:1948 git-gui.sh:2048
-msgid "Delete..."
-msgstr "Löschen..."
-
-#: git-gui.sh:1953
-msgid "Reset..."
-msgstr "Zurücksetzen..."
-
-#: git-gui.sh:1965 git-gui.sh:2350
-msgid "New Commit"
-msgstr "Neue Version"
-
-#: git-gui.sh:1973 git-gui.sh:2357
-msgid "Amend Last Commit"
-msgstr "Letzte Version nachbessern"
-
-#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
-msgid "Rescan"
-msgstr "Neu laden"
-
-#: git-gui.sh:1988
-msgid "Stage To Commit"
-msgstr "Zum Eintragen bereitstellen"
-
-#: git-gui.sh:1994
-msgid "Stage Changed Files To Commit"
-msgstr "Geänderte Dateien zum Eintragen bereitstellen"
-
-#: git-gui.sh:2000
-msgid "Unstage From Commit"
-msgstr "Aus der Bereitstellung herausnehmen"
-
-#: git-gui.sh:2005 lib/index.tcl:393
-msgid "Revert Changes"
-msgstr "Änderungen revidieren"
-
-#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
-msgid "Sign Off"
-msgstr "Abzeichnen"
-
-#: git-gui.sh:2016 git-gui.sh:2333
-msgid "Commit@@verb"
-msgstr "Eintragen"
-
-#: git-gui.sh:2027
-msgid "Local Merge..."
-msgstr "Lokales Zusammenführen..."
-
-#: git-gui.sh:2032
-msgid "Abort Merge..."
-msgstr "Zusammenführen abbrechen..."
-
-#: git-gui.sh:2044
-msgid "Push..."
-msgstr "Versenden..."
-
-#: git-gui.sh:2055 lib/choose_repository.tcl:40
-msgid "Apple"
-msgstr "Apple"
-
-#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
-#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
-#, tcl-format
-msgid "About %s"
-msgstr "Über %s"
-
-#: git-gui.sh:2062
-msgid "Preferences..."
-msgstr "Einstellungen..."
-
-#: git-gui.sh:2070 git-gui.sh:2595
-msgid "Options..."
-msgstr "Optionen..."
-
-#: git-gui.sh:2076 lib/choose_repository.tcl:46
-msgid "Help"
-msgstr "Hilfe"
-
-#: git-gui.sh:2117
-msgid "Online Documentation"
-msgstr "Online-Dokumentation"
-
-#: git-gui.sh:2201
-#, tcl-format
-msgid "fatal: cannot stat path %s: No such file or directory"
-msgstr "Fehler: Verzeichnis »%s« kann nicht gelesen werden: Datei oder Verzeichnis nicht gefunden"
-
-#: git-gui.sh:2234
-msgid "Current Branch:"
-msgstr "Aktueller Zweig:"
-
-#: git-gui.sh:2255
-msgid "Staged Changes (Will Commit)"
-msgstr "Bereitgestellte Änderungen (zum Eintragen)"
-
-#: git-gui.sh:2274
-msgid "Unstaged Changes"
-msgstr "Nicht bereitgestellte Änderungen"
-
-#: git-gui.sh:2323
-msgid "Stage Changed"
-msgstr "Alles bereitstellen"
-
-#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
-msgid "Push"
-msgstr "Versenden"
-
-#: git-gui.sh:2369
-msgid "Initial Commit Message:"
-msgstr "Erste Versionsbeschreibung:"
-
-#: git-gui.sh:2370
-msgid "Amended Commit Message:"
-msgstr "Nachgebesserte Versionsbeschreibung:"
-
-#: git-gui.sh:2371
-msgid "Amended Initial Commit Message:"
-msgstr "Nachgebesserte erste Versionsbeschreibung:"
-
-#: git-gui.sh:2372
-msgid "Amended Merge Commit Message:"
-msgstr "Nachgebesserte Zusammenführungs-Versionsbeschreibung:"
-
-#: git-gui.sh:2373
-msgid "Merge Commit Message:"
-msgstr "Zusammenführungs-Versionsbeschreibung:"
-
-#: git-gui.sh:2374
-msgid "Commit Message:"
-msgstr "Versionsbeschreibung:"
-
-#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
-msgid "Copy All"
-msgstr "Alle kopieren"
-
-#: git-gui.sh:2443 lib/blame.tcl:104
-msgid "File:"
-msgstr "Datei:"
-
-#: git-gui.sh:2545
-msgid "Refresh"
-msgstr "Aktualisieren"
-
-#: git-gui.sh:2566
-msgid "Apply/Reverse Hunk"
-msgstr "Änderung anwenden/umkehren"
-
-#: git-gui.sh:2572
-msgid "Decrease Font Size"
-msgstr "Schriftgröße verkleinern"
-
-#: git-gui.sh:2576
-msgid "Increase Font Size"
-msgstr "Schriftgröße vergrößern"
-
-#: git-gui.sh:2581
-msgid "Show Less Context"
-msgstr "Weniger Kontext anzeigen"
-
-#: git-gui.sh:2588
-msgid "Show More Context"
-msgstr "Mehr Kontext anzeigen"
-
-#: git-gui.sh:2602
-msgid "Unstage Hunk From Commit"
-msgstr "Aus der Bereitstellung herausnehmen"
-
-#: git-gui.sh:2604
-msgid "Stage Hunk For Commit"
-msgstr "In die Bereitstellung hinzufügen"
-
-#: git-gui.sh:2623
-msgid "Initializing..."
-msgstr "Initialisieren..."
-
-#: git-gui.sh:2718
-#, tcl-format
-msgid ""
-"Possible environment issues exist.\n"
-"\n"
-"The following environment variables are probably\n"
-"going to be ignored by any Git subprocess run\n"
-"by %s:\n"
-"\n"
-msgstr ""
-"Möglicherweise gibt es Probleme mit manchen Umgebungsvariablen.\n"
-"\n"
-"Die folgenden Umgebungsvariablen können vermutlich nicht \n"
-"von %s an Git weitergegeben werden:\n"
-"\n"
-
-#: git-gui.sh:2748
-msgid ""
-"\n"
-"This is due to a known issue with the\n"
-"Tcl binary distributed by Cygwin."
-msgstr ""
-"\n"
-"Dies ist ein bekanntes Problem der Tcl-Version, die\n"
-"in Cygwin mitgeliefert wird."
-
-#: git-gui.sh:2753
-#, tcl-format
-msgid ""
-"\n"
-"\n"
-"A good replacement for %s\n"
-"is placing values for the user.name and\n"
-"user.email settings into your personal\n"
-"~/.gitconfig file.\n"
-msgstr ""
-"\n"
-"\n"
-"Um den Namen »%s« zu ändern, sollten Sie die \n"
-"gewünschten Werte für die Einstellung user.name und \n"
-"user.email in Ihre Datei ~/.gitconfig einfügen.\n"
-
-#: lib/about.tcl:25
-msgid "git-gui - a graphical user interface for Git."
-msgstr "git-gui - eine grafische Oberfläche für Git."
-
-#: lib/blame.tcl:77
-msgid "File Viewer"
-msgstr "Datei-Browser"
-
-#: lib/blame.tcl:81
-msgid "Commit:"
-msgstr "Version:"
-
-#: lib/blame.tcl:249
-msgid "Copy Commit"
-msgstr "Version kopieren"
-
-#: lib/blame.tcl:369
-#, tcl-format
-msgid "Reading %s..."
-msgstr "%s lesen..."
-
-#: lib/blame.tcl:473
-msgid "Loading copy/move tracking annotations..."
-msgstr "Annotierungen für Kopieren/Verschieben werden geladen..."
-
-#: lib/blame.tcl:493
-msgid "lines annotated"
-msgstr "Zeilen annotiert"
-
-#: lib/blame.tcl:674
-msgid "Loading original location annotations..."
-msgstr "Annotierungen für ursprünglichen Ort werden geladen..."
-
-#: lib/blame.tcl:677
-msgid "Annotation complete."
-msgstr "Annotierung vollständig."
-
-#: lib/blame.tcl:731
-msgid "Loading annotation..."
-msgstr "Annotierung laden..."
-
-#: lib/blame.tcl:787
-msgid "Author:"
-msgstr "Autor:"
-
-#: lib/blame.tcl:791
-msgid "Committer:"
-msgstr "Eintragender:"
-
-#: lib/blame.tcl:796
-msgid "Original File:"
-msgstr "Ursprüngliche Datei:"
-
-#: lib/blame.tcl:910
-msgid "Originally By:"
-msgstr "Ursprünglich von:"
-
-#: lib/blame.tcl:916
-msgid "In File:"
-msgstr "In Datei:"
-
-#: lib/blame.tcl:921
-msgid "Copied Or Moved Here By:"
-msgstr "Kopiert oder verschoben durch:"
-
-#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
-msgid "Checkout Branch"
-msgstr "Zweig umstellen"
-
-#: lib/branch_checkout.tcl:23
-msgid "Checkout"
-msgstr "Umstellen"
-
-#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
-#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
-#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
-#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
-msgid "Cancel"
-msgstr "Abbrechen"
-
-#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
-msgid "Revision"
-msgstr "Version"
-
-#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
-msgid "Options"
-msgstr "Optionen"
-
-#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
-msgid "Fetch Tracking Branch"
-msgstr "Übernahmezweig anfordern"
-
-#: lib/branch_checkout.tcl:44
-msgid "Detach From Local Branch"
-msgstr "Verbindung zu lokalem Zweig lösen"
-
-#: lib/branch_create.tcl:22
-msgid "Create Branch"
-msgstr "Zweig erstellen"
-
-#: lib/branch_create.tcl:27
-msgid "Create New Branch"
-msgstr "Neuen Zweig erstellen"
-
-#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
-msgid "Create"
-msgstr "Erstellen"
-
-#: lib/branch_create.tcl:40
-msgid "Branch Name"
-msgstr "Zweigname"
-
-#: lib/branch_create.tcl:43
-msgid "Name:"
-msgstr "Name:"
-
-#: lib/branch_create.tcl:58
-msgid "Match Tracking Branch Name"
-msgstr "Passend zu Übernahmezweig-Name"
-
-#: lib/branch_create.tcl:66
-msgid "Starting Revision"
-msgstr "Anfangsversion"
-
-#: lib/branch_create.tcl:72
-msgid "Update Existing Branch:"
-msgstr "Existierenden Zweig aktualisieren:"
-
-#: lib/branch_create.tcl:75
-msgid "No"
-msgstr "Nein"
-
-#: lib/branch_create.tcl:80
-msgid "Fast Forward Only"
-msgstr "Nur Schnellzusammenführung"
-
-#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
-msgid "Reset"
-msgstr "Zurücksetzen"
-
-#: lib/branch_create.tcl:97
-msgid "Checkout After Creation"
-msgstr "Arbeitskopie umstellen nach Erstellen"
-
-#: lib/branch_create.tcl:131
-msgid "Please select a tracking branch."
-msgstr "Bitte wählen Sie einen Übernahmezweig."
-
-#: lib/branch_create.tcl:140
-#, tcl-format
-msgid "Tracking branch %s is not a branch in the remote repository."
-msgstr "Übernahmezweig »%s« ist kein Zweig im anderen Projektarchiv."
-
-#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
-msgid "Please supply a branch name."
-msgstr "Bitte geben Sie einen Zweignamen an."
-
-#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
-#, tcl-format
-msgid "'%s' is not an acceptable branch name."
-msgstr "»%s« ist kein zulässiger Zweigname."
-
-#: lib/branch_delete.tcl:15
-msgid "Delete Branch"
-msgstr "Zweig löschen"
-
-#: lib/branch_delete.tcl:20
-msgid "Delete Local Branch"
-msgstr "Lokalen Zweig löschen"
-
-#: lib/branch_delete.tcl:37
-msgid "Local Branches"
-msgstr "Lokale Zweige"
-
-#: lib/branch_delete.tcl:52
-msgid "Delete Only If Merged Into"
-msgstr "Nur löschen, wenn darin zusammengeführt"
-
-#: lib/branch_delete.tcl:54
-msgid "Always (Do not perform merge test.)"
-msgstr "Immer (ohne Zusammenführungstest)"
-
-#: lib/branch_delete.tcl:103
-#, tcl-format
-msgid "The following branches are not completely merged into %s:"
-msgstr "Folgende Zweige sind noch nicht mit »%s« zusammengeführt:"
-
-#: lib/branch_delete.tcl:115
-msgid ""
-"Recovering deleted branches is difficult. \n"
-"\n"
-" Delete the selected branches?"
-msgstr ""
-"Gelöschte Zweige können nur mit größerem Aufwand wiederhergestellt werden.\n"
-"\n"
-"Gewählte Zweige jetzt löschen?"
-
-#: lib/branch_delete.tcl:141
-#, tcl-format
-msgid ""
-"Failed to delete branches:\n"
-"%s"
-msgstr ""
-"Fehler beim Löschen der Zweige:\n"
-"%s"
-
-#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
-msgid "Rename Branch"
-msgstr "Zweig umbenennen"
-
-#: lib/branch_rename.tcl:26
-msgid "Rename"
-msgstr "Umbenennen"
-
-#: lib/branch_rename.tcl:36
-msgid "Branch:"
-msgstr "Zweig:"
-
-#: lib/branch_rename.tcl:39
-msgid "New Name:"
-msgstr "Neuer Name:"
-
-#: lib/branch_rename.tcl:75
-msgid "Please select a branch to rename."
-msgstr "Bitte wählen Sie einen Zweig zum umbenennen."
-
-#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
-#, tcl-format
-msgid "Branch '%s' already exists."
-msgstr "Zweig »%s« existiert bereits."
-
-#: lib/branch_rename.tcl:117
-#, tcl-format
-msgid "Failed to rename '%s'."
-msgstr "Fehler beim Umbenennen von »%s«."
-
-#: lib/browser.tcl:17
-msgid "Starting..."
-msgstr "Starten..."
-
-#: lib/browser.tcl:26
-msgid "File Browser"
-msgstr "Datei-Browser"
-
-#: lib/browser.tcl:125 lib/browser.tcl:142
-#, tcl-format
-msgid "Loading %s..."
-msgstr "%s laden..."
-
-#: lib/browser.tcl:186
-msgid "[Up To Parent]"
-msgstr "[Nach oben]"
-
-#: lib/browser.tcl:266 lib/browser.tcl:272
-msgid "Browse Branch Files"
-msgstr "Dateien des Zweigs durchblättern"
-
-#: lib/browser.tcl:277 lib/choose_repository.tcl:391
-#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
-#: lib/choose_repository.tcl:989
-msgid "Browse"
-msgstr "Blättern"
-
-#: lib/checkout_op.tcl:79
-#, tcl-format
-msgid "Fetching %s from %s"
-msgstr "Änderungen »%s« von »%s« anfordern"
-
-#: lib/checkout_op.tcl:127
-#, tcl-format
-msgid "fatal: Cannot resolve %s"
-msgstr "Fehler: »%s« kann nicht als Zweig oder Version erkannt werden"
-
-#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
-msgid "Close"
-msgstr "Schließen"
-
-#: lib/checkout_op.tcl:169
-#, tcl-format
-msgid "Branch '%s' does not exist."
-msgstr "Zweig »%s« existiert nicht."
-
-#: lib/checkout_op.tcl:206
-#, tcl-format
-msgid ""
-"Branch '%s' already exists.\n"
-"\n"
-"It cannot fast-forward to %s.\n"
-"A merge is required."
-msgstr ""
-"Zweig »%s« existiert bereits.\n"
-"\n"
-"Zweig kann nicht mit »%s« schnellzusammengeführt werden. Reguläres "
-"Zusammenführen ist notwendig."
-
-#: lib/checkout_op.tcl:220
-#, tcl-format
-msgid "Merge strategy '%s' not supported."
-msgstr "Zusammenführungsmethode »%s« nicht unterstützt."
-
-#: lib/checkout_op.tcl:239
-#, tcl-format
-msgid "Failed to update '%s'."
-msgstr "Aktualisieren von »%s« fehlgeschlagen."
-
-#: lib/checkout_op.tcl:251
-msgid "Staging area (index) is already locked."
-msgstr "Bereitstellung (»index«) ist zur Bearbeitung gesperrt (»locked«)."
-
-#: lib/checkout_op.tcl:266
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before the current branch can be changed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"Der letzte geladene Status stimmt nicht mehr mit dem Projektarchiv überein.\n"
-"\n"
-"Ein anderes Git-Programm hat das Projektarchiv seit dem letzten Laden "
-"geändert.  Vor dem Wechseln des lokalen Zweigs muss neu geladen werden.\n"
-"\n"
-"Es wird gleich neu geladen.\n"
-
-#: lib/checkout_op.tcl:322
-#, tcl-format
-msgid "Updating working directory to '%s'..."
-msgstr "Arbeitskopie umstellen auf »%s«..."
-
-#: lib/checkout_op.tcl:353
-#, tcl-format
-msgid "Aborted checkout of '%s' (file level merging is required)."
-msgstr ""
-"Zweig umstellen von »%s« abgebrochen (Zusammenführen der Dateien ist "
-"notwendig)."
-
-#: lib/checkout_op.tcl:354
-msgid "File level merge required."
-msgstr "Zusammenführen der Dateien ist notwendig."
-
-#: lib/checkout_op.tcl:358
-#, tcl-format
-msgid "Staying on branch '%s'."
-msgstr "Es wird auf Zweig »%s« verblieben."
-
-#: lib/checkout_op.tcl:429
-msgid ""
-"You are no longer on a local branch.\n"
-"\n"
-"If you wanted to be on a branch, create one now starting from 'This Detached "
-"Checkout'."
-msgstr ""
-"Die Arbeitskopie ist nicht auf einem lokalen Zweig.\n"
-"\n"
-"Wenn Sie auf einem Zweig arbeiten möchten, erstellen Sie bitte jetzt einen "
-"Zweig mit der Auswahl »Abgetrennte Arbeitskopie-Version«."
-
-#: lib/checkout_op.tcl:446
-#, tcl-format
-msgid "Checked out '%s'."
-msgstr "Umgestellt auf »%s«."
-
-#: lib/checkout_op.tcl:478
-#, tcl-format
-msgid "Resetting '%s' to '%s' will lose the following commits:"
-msgstr "Zurücksetzen von »%s« nach »%s« wird folgende Versionen verwerfen:"
-
-#: lib/checkout_op.tcl:500
-msgid "Recovering lost commits may not be easy."
-msgstr ""
-"Verworfene Versionen können nur mit größerem Aufwand wiederhergestellt "
-"werden."
-
-#: lib/checkout_op.tcl:505
-#, tcl-format
-msgid "Reset '%s'?"
-msgstr "»%s« zurücksetzen?"
-
-#: lib/checkout_op.tcl:510 lib/merge.tcl:164
-msgid "Visualize"
-msgstr "Darstellen"
-
-#: lib/checkout_op.tcl:578
-#, tcl-format
-msgid ""
-"Failed to set current branch.\n"
-"\n"
-"This working directory is only partially switched.  We successfully updated "
-"your files, but failed to update an internal Git file.\n"
-"\n"
-"This should not have occurred.  %s will now close and give up."
-msgstr ""
-"Lokaler Zweig kann nicht gesetzt werden.\n"
-"\n"
-"Diese Arbeitskopie ist nur teilweise umgestellt. Die Dateien sind korrekt "
-"aktualisiert, aber einige interne Git-Dateien konnten nicht geändert "
-"werden.\n"
-"\n"
-"Dies ist ein interner Programmfehler von %s. Programm wird jetzt abgebrochen."
-
-#: lib/choose_font.tcl:39
-msgid "Select"
-msgstr "Auswählen"
-
-#: lib/choose_font.tcl:53
-msgid "Font Family"
-msgstr "Schriftfamilie"
-
-#: lib/choose_font.tcl:73
-msgid "Font Size"
-msgstr "Schriftgröße"
-
-#: lib/choose_font.tcl:90
-msgid "Font Example"
-msgstr "Schriftbeispiel"
-
-#: lib/choose_font.tcl:101
-msgid ""
-"This is example text.\n"
-"If you like this text, it can be your font."
-msgstr ""
-"Dies ist ein Beispieltext.\n"
-"Wenn Ihnen dieser Text gefällt, sollten Sie diese Schriftart wählen."
-
-#: lib/choose_repository.tcl:27
-msgid "Git Gui"
-msgstr "Git Gui"
-
-#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
-msgid "Create New Repository"
-msgstr "Neues Projektarchiv"
-
-#: lib/choose_repository.tcl:86
-msgid "New..."
-msgstr "Neu..."
-
-#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
-msgid "Clone Existing Repository"
-msgstr "Projektarchiv kopieren"
-
-#: lib/choose_repository.tcl:99
-msgid "Clone..."
-msgstr "Kopieren..."
-
-#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
-msgid "Open Existing Repository"
-msgstr "Projektarchiv öffnen"
-
-#: lib/choose_repository.tcl:112
-msgid "Open..."
-msgstr "Öffnen..."
-
-#: lib/choose_repository.tcl:125
-msgid "Recent Repositories"
-msgstr "Zuletzt benutzte Projektarchive"
-
-#: lib/choose_repository.tcl:131
-msgid "Open Recent Repository:"
-msgstr "Zuletzt benutztes Projektarchiv öffnen:"
-
-#: lib/choose_repository.tcl:294
-#, tcl-format
-msgid "Location %s already exists."
-msgstr "Projektarchiv »%s« existiert bereits."
-
-#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
-#: lib/choose_repository.tcl:314
-#, tcl-format
-msgid "Failed to create repository %s:"
-msgstr "Projektarchiv »%s« konnte nicht erstellt werden:"
-
-#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
-msgid "Directory:"
-msgstr "Verzeichnis:"
-
-#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
-#: lib/choose_repository.tcl:1013
-msgid "Git Repository"
-msgstr "Git Projektarchiv"
-
-#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
-#, tcl-format
-msgid "Directory %s already exists."
-msgstr "Verzeichnis »%s« existiert bereits."
-
-#: lib/choose_repository.tcl:442
-#, tcl-format
-msgid "File %s already exists."
-msgstr "Datei »%s« existiert bereits."
-
-#: lib/choose_repository.tcl:463
-msgid "Clone"
-msgstr "Kopieren"
-
-#: lib/choose_repository.tcl:476
-msgid "URL:"
-msgstr "URL:"
-
-#: lib/choose_repository.tcl:496
-msgid "Clone Type:"
-msgstr "Art der Kopie:"
-
-#: lib/choose_repository.tcl:502
-msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
-msgstr "Standard (schnell, teilweise redundant, Hardlinks)"
-
-#: lib/choose_repository.tcl:508
-msgid "Full Copy (Slower, Redundant Backup)"
-msgstr "Alles kopieren (langsamer, volle Redundanz)"
-
-#: lib/choose_repository.tcl:514
-msgid "Shared (Fastest, Not Recommended, No Backup)"
-msgstr "Verknüpft (schnell, nicht empfohlen, kein Backup)"
-
-#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
-#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
-#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
-#, tcl-format
-msgid "Not a Git repository: %s"
-msgstr "Kein Git-Projektarchiv in »%s« gefunden."
-
-#: lib/choose_repository.tcl:586
-msgid "Standard only available for local repository."
-msgstr "Standard ist nur für lokale Projektarchive verfügbar."
-
-#: lib/choose_repository.tcl:590
-msgid "Shared only available for local repository."
-msgstr "Verknüpft ist nur für lokale Projektarchive verfügbar."
-
-#: lib/choose_repository.tcl:617
-msgid "Failed to configure origin"
-msgstr "Der Ursprungsort konnte nicht eingerichtet werden"
-
-#: lib/choose_repository.tcl:629
-msgid "Counting objects"
-msgstr "Objekte werden gezählt"
-
-#: lib/choose_repository.tcl:630
-msgid "buckets"
-msgstr "Buckets"
-
-#: lib/choose_repository.tcl:654
-#, tcl-format
-msgid "Unable to copy objects/info/alternates: %s"
-msgstr "Kopien von Objekten/Info/Alternates konnten nicht erstellt werden: %s"
-
-#: lib/choose_repository.tcl:690
-#, tcl-format
-msgid "Nothing to clone from %s."
-msgstr "Von »%s« konnte nichts kopiert werden."
-
-#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
-#: lib/choose_repository.tcl:918
-msgid "The 'master' branch has not been initialized."
-msgstr "Der »master«-Zweig wurde noch nicht initialisiert."
-
-#: lib/choose_repository.tcl:705
-msgid "Hardlinks are unavailable.  Falling back to copying."
-msgstr "Hardlinks nicht verfügbar. Stattdessen wird kopiert."
-
-#: lib/choose_repository.tcl:717
-#, tcl-format
-msgid "Cloning from %s"
-msgstr "Kopieren von »%s«"
-
-#: lib/choose_repository.tcl:748
-msgid "Copying objects"
-msgstr "Objektdatenbank kopieren"
-
-#: lib/choose_repository.tcl:749
-msgid "KiB"
-msgstr "KB"
-
-#: lib/choose_repository.tcl:773
-#, tcl-format
-msgid "Unable to copy object: %s"
-msgstr "Objekt kann nicht kopiert werden: %s"
-
-#: lib/choose_repository.tcl:783
-msgid "Linking objects"
-msgstr "Objekte verlinken"
-
-#: lib/choose_repository.tcl:784
-msgid "objects"
-msgstr "Objekte"
-
-#: lib/choose_repository.tcl:792
-#, tcl-format
-msgid "Unable to hardlink object: %s"
-msgstr "Für Objekt konnte kein Hardlink erstellt werden: %s"
-
-#: lib/choose_repository.tcl:847
-msgid "Cannot fetch branches and objects.  See console output for details."
-msgstr "Zweige und Objekte konnten nicht angefordert werden.  Kontrollieren Sie die Ausgaben auf der Konsole für weitere Angaben."
-
-#: lib/choose_repository.tcl:858
-msgid "Cannot fetch tags.  See console output for details."
-msgstr "Markierungen konnten nicht angefordert werden.  Kontrollieren Sie die Ausgaben auf der Konsole für weitere Angaben."
-
-#: lib/choose_repository.tcl:882
-msgid "Cannot determine HEAD.  See console output for details."
-msgstr "Die Zweigspitze (HEAD) konnte nicht gefunden werden.  Kontrollieren Sie die Ausgaben auf der Konsole für weitere Angaben."
-
-#: lib/choose_repository.tcl:891
-#, tcl-format
-msgid "Unable to cleanup %s"
-msgstr "Verzeichnis »%s« kann nicht aufgeräumt werden."
-
-#: lib/choose_repository.tcl:897
-msgid "Clone failed."
-msgstr "Kopieren fehlgeschlagen."
-
-#: lib/choose_repository.tcl:904
-msgid "No default branch obtained."
-msgstr "Kein voreingestellter Zweig gefunden."
-
-#: lib/choose_repository.tcl:915
-#, tcl-format
-msgid "Cannot resolve %s as a commit."
-msgstr "»%s« wurde nicht als Version gefunden."
-
-#: lib/choose_repository.tcl:927
-msgid "Creating working directory"
-msgstr "Arbeitskopie erstellen"
-
-#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
-#: lib/index.tcl:193
-msgid "files"
-msgstr "Dateien"
-
-#: lib/choose_repository.tcl:957
-msgid "Initial file checkout failed."
-msgstr "Erstellen der Arbeitskopie fehlgeschlagen."
-
-#: lib/choose_repository.tcl:973
-msgid "Open"
-msgstr "Öffnen"
-
-#: lib/choose_repository.tcl:983
-msgid "Repository:"
-msgstr "Projektarchiv:"
-
-#: lib/choose_repository.tcl:1033
-#, tcl-format
-msgid "Failed to open repository %s:"
-msgstr "Projektarchiv »%s« konnte nicht geöffnet werden."
-
-#: lib/choose_rev.tcl:53
-msgid "This Detached Checkout"
-msgstr "Abgetrennte Arbeitskopie-Version"
-
-#: lib/choose_rev.tcl:60
-msgid "Revision Expression:"
-msgstr "Version Regexp-Ausdruck:"
-
-#: lib/choose_rev.tcl:74
-msgid "Local Branch"
-msgstr "Lokaler Zweig"
-
-#: lib/choose_rev.tcl:79
-msgid "Tracking Branch"
-msgstr "Übernahmezweig"
-
-#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
-msgid "Tag"
-msgstr "Markierung"
-
-#: lib/choose_rev.tcl:317
-#, tcl-format
-msgid "Invalid revision: %s"
-msgstr "Ungültige Version: %s"
-
-#: lib/choose_rev.tcl:338
-msgid "No revision selected."
-msgstr "Keine Version ausgewählt."
-
-#: lib/choose_rev.tcl:346
-msgid "Revision expression is empty."
-msgstr "Versions-Ausdruck ist leer."
-
-#: lib/choose_rev.tcl:530
-msgid "Updated"
-msgstr "Aktualisiert"
-
-#: lib/choose_rev.tcl:558
-msgid "URL"
-msgstr "URL"
-
-#: lib/commit.tcl:9
-msgid ""
-"There is nothing to amend.\n"
-"\n"
-"You are about to create the initial commit.  There is no commit before this "
-"to amend.\n"
-msgstr ""
-"Keine Version zur Nachbesserung vorhanden.\n"
-"\n"
-"Sie sind dabei, die erste Version zu übertragen. Es gibt keine existierende "
-"Version, die Sie nachbessern könnten.\n"
-
-#: lib/commit.tcl:18
-msgid ""
-"Cannot amend while merging.\n"
-"\n"
-"You are currently in the middle of a merge that has not been fully "
-"completed.  You cannot amend the prior commit unless you first abort the "
-"current merge activity.\n"
-msgstr ""
-"Nachbesserung währen Zusammenführung nicht möglich.\n"
-"\n"
-"Sie haben das Zusammenführen von Versionen angefangen, aber noch nicht "
-"beendet. Sie können keine vorige Übertragung nachbessern, solange eine "
-"unfertige Zusammenführung existiert. Dazu müssen Sie die Zusammenführung "
-"beenden oder abbrechen.\n"
-
-#: lib/commit.tcl:49
-msgid "Error loading commit data for amend:"
-msgstr "Fehler beim Laden der Versionsdaten für Nachbessern:"
-
-#: lib/commit.tcl:76
-msgid "Unable to obtain your identity:"
-msgstr "Benutzername konnte nicht bestimmt werden:"
-
-#: lib/commit.tcl:81
-msgid "Invalid GIT_COMMITTER_IDENT:"
-msgstr "Ungültiger Wert von GIT_COMMITTER_INDENT:"
-
-#: lib/commit.tcl:133
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before another commit can be created.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"Der letzte geladene Status stimmt nicht mehr mit dem Projektarchiv überein.\n"
-"\n"
-"Ein anderes Git-Programm hat das Projektarchiv seit dem letzten Laden "
-"geändert.  Vor dem Eintragen einer neuen Version muss neu geladen werden.\n"
-"\n"
-"Es wird gleich neu geladen.\n"
-
-#: lib/commit.tcl:154
-#, tcl-format
-msgid ""
-"Unmerged files cannot be committed.\n"
-"\n"
-"File %s has merge conflicts.  You must resolve them and stage the file "
-"before committing.\n"
-msgstr ""
-"Nicht zusammengeführte Dateien können nicht eingetragen werden.\n"
-"\n"
-"Die Datei »%s« hat noch nicht aufgelöste Zusammenführungs-Konflikte. Sie "
-"müssen diese Konflikte auflösen, bevor Sie eintragen können.\n"
-
-#: lib/commit.tcl:162
-#, tcl-format
-msgid ""
-"Unknown file state %s detected.\n"
-"\n"
-"File %s cannot be committed by this program.\n"
-msgstr ""
-"Unbekannter Dateizustand »%s«.\n"
-"\n"
-"Datei »%s« kann nicht eingetragen werden.\n"
-
-#: lib/commit.tcl:170
-msgid ""
-"No changes to commit.\n"
-"\n"
-"You must stage at least 1 file before you can commit.\n"
-msgstr ""
-"Keine Änderungen vorhanden, die eingetragen werden könnten.\n"
-"\n"
-"Sie müssen mindestens eine Datei bereitstellen, bevor Sie eintragen können.\n"
-
-#: lib/commit.tcl:183
-msgid ""
-"Please supply a commit message.\n"
-"\n"
-"A good commit message has the following format:\n"
-"\n"
-"- First line: Describe in one sentence what you did.\n"
-"- Second line: Blank\n"
-"- Remaining lines: Describe why this change is good.\n"
-msgstr ""
-"Bitte geben Sie eine Versionsbeschreibung ein.\n"
-"\n"
-"Eine gute Versionsbeschreibung enthält folgende Abschnitte:\n"
-"\n"
-"- Erste Zeile: Eine Zusammenfassung, was man gemacht hat.\n"
-"\n"
-"- Zweite Zeile: Leerzeile\n"
-"\n"
-"- Rest: Eine ausführliche Beschreibung, warum diese Änderung hilfreich ist.\n"
-
-#: lib/commit.tcl:257
-msgid "write-tree failed:"
-msgstr "write-tree fehlgeschlagen:"
-
-#: lib/commit.tcl:275
-#, tcl-format
-msgid "Commit %s appears to be corrupt"
-msgstr "Version »%s« scheint beschädigt zu sein"
-
-#: lib/commit.tcl:279
-msgid ""
-"No changes to commit.\n"
-"\n"
-"No files were modified by this commit and it was not a merge commit.\n"
-"\n"
-"A rescan will be automatically started now.\n"
-msgstr ""
-"Keine Änderungen einzutragen.\n"
-"\n"
-"Es gibt keine geänderte Datei bei dieser Version und es wurde auch nichts "
-"zusammengeführt.\n"
-"\n"
-"Das Arbeitsverzeichnis wird daher jetzt neu geladen.\n"
-
-#: lib/commit.tcl:286
-msgid "No changes to commit."
-msgstr "Keine Änderungen, die eingetragen werden können."
-
-#: lib/commit.tcl:303
-#, tcl-format
-msgid "warning: Tcl does not support encoding '%s'."
-msgstr "Warning: Tcl/Tk unterstützt die Zeichencodierung »%s« nicht."
-
-#: lib/commit.tcl:317
-msgid "commit-tree failed:"
-msgstr "commit-tree fehlgeschlagen:"
-
-#: lib/commit.tcl:339
-msgid "update-ref failed:"
-msgstr "update-ref fehlgeschlagen:"
-
-#: lib/commit.tcl:430
-#, tcl-format
-msgid "Created commit %s: %s"
-msgstr "Version %s übertragen: %s"
-
-#: lib/console.tcl:57
-msgid "Working... please wait..."
-msgstr "Verarbeitung. Bitte warten..."
-
-#: lib/console.tcl:183
-msgid "Success"
-msgstr "Erfolgreich"
-
-#: lib/console.tcl:196
-msgid "Error: Command Failed"
-msgstr "Fehler: Kommando fehlgeschlagen"
-
-#: lib/database.tcl:43
-msgid "Number of loose objects"
-msgstr "Anzahl unverknüpfter Objekte"
-
-#: lib/database.tcl:44
-msgid "Disk space used by loose objects"
-msgstr "Festplattenplatz von unverknüpften Objekten"
-
-#: lib/database.tcl:45
-msgid "Number of packed objects"
-msgstr "Anzahl komprimierter Objekte"
-
-#: lib/database.tcl:46
-msgid "Number of packs"
-msgstr "Anzahl Komprimierungseinheiten"
-
-#: lib/database.tcl:47
-msgid "Disk space used by packed objects"
-msgstr "Festplattenplatz von komprimierten Objekten"
-
-#: lib/database.tcl:48
-msgid "Packed objects waiting for pruning"
-msgstr "Komprimierte Objekte, die zum Entfernen vorgesehen sind"
-
-#: lib/database.tcl:49
-msgid "Garbage files"
-msgstr "Dateien im Mülleimer"
-
-#: lib/database.tcl:72
-msgid "Compressing the object database"
-msgstr "Objektdatenbank komprimieren"
-
-#: lib/database.tcl:83
-msgid "Verifying the object database with fsck-objects"
-msgstr "Die Objektdatenbank durch »fsck-objects« überprüfen lassen"
-
-#: lib/database.tcl:108
-#, tcl-format
-msgid ""
-"This repository currently has approximately %i loose objects.\n"
-"\n"
-"To maintain optimal performance it is strongly recommended that you compress "
-"the database when more than %i loose objects exist.\n"
-"\n"
-"Compress the database now?"
-msgstr ""
-"Dieses Projektarchiv enthält ungefähr %i nicht verknüpfte Objekte.\n"
-"\n"
-"Für eine optimale Performance wird empfohlen, die Datenbank des "
-"Projektarchivs zu komprimieren, sobald mehr als %i nicht verknüpfte Objekte "
-"vorliegen.\n"
-"\n"
-"Soll die Datenbank jetzt komprimiert werden?"
-
-#: lib/date.tcl:25
-#, tcl-format
-msgid "Invalid date from Git: %s"
-msgstr "Ungültiges Datum von Git: %s"
-
-#: lib/diff.tcl:42
-#, tcl-format
-msgid ""
-"No differences detected.\n"
-"\n"
-"%s has no changes.\n"
-"\n"
-"The modification date of this file was updated by another application, but "
-"the content within the file was not changed.\n"
-"\n"
-"A rescan will be automatically started to find other files which may have "
-"the same state."
-msgstr ""
-"Keine Änderungen feststellbar.\n"
-"\n"
-"»%s« enthält keine Änderungen. Zwar wurde das Änderungsdatum dieser Datei "
-"von einem anderen Programm modifiziert, aber der Inhalt der Datei ist "
-"unverändert.\n"
-"\n"
-"Das Arbeitsverzeichnis wird jetzt neu geladen, um diese Änderung bei allen "
-"Dateien zu prüfen."
-
-#: lib/diff.tcl:81
-#, tcl-format
-msgid "Loading diff of %s..."
-msgstr "Vergleich von »%s« laden..."
-
-#: lib/diff.tcl:114 lib/diff.tcl:184
-#, tcl-format
-msgid "Unable to display %s"
-msgstr "Datei »%s« kann nicht angezeigt werden"
-
-#: lib/diff.tcl:115
-msgid "Error loading file:"
-msgstr "Fehler beim Laden der Datei:"
-
-#: lib/diff.tcl:122
-msgid "Git Repository (subproject)"
-msgstr "Git-Projektarchiv (Unterprojekt)"
-
-#: lib/diff.tcl:134
-msgid "* Binary file (not showing content)."
-msgstr "* Binärdatei (Inhalt wird nicht angezeigt)"
-
-#: lib/diff.tcl:185
-msgid "Error loading diff:"
-msgstr "Fehler beim Laden des Vergleichs:"
-
-#: lib/diff.tcl:302
-msgid "Failed to unstage selected hunk."
-msgstr "Fehler beim Herausnehmen der gewählten Dateien aus der Bereitstellung."
-
-#: lib/diff.tcl:309
-msgid "Failed to stage selected hunk."
-msgstr "Fehler beim Bereitstellen der gewählten Dateien."
-
-#: lib/error.tcl:12 lib/error.tcl:102
-msgid "error"
-msgstr "Fehler"
-
-#: lib/error.tcl:28
-msgid "warning"
-msgstr "Warnung"
-
-#: lib/error.tcl:81
-msgid "You must correct the above errors before committing."
-msgstr ""
-"Sie müssen die obigen Fehler zuerst beheben, bevor Sie eintragen können."
-
-#: lib/index.tcl:6
-msgid "Unable to unlock the index."
-msgstr "Bereitstellung kann nicht wieder freigegeben werden."
-
-#: lib/index.tcl:15
-msgid "Index Error"
-msgstr "Fehler in Bereitstellung"
-
-#: lib/index.tcl:21
-msgid ""
-"Updating the Git index failed.  A rescan will be automatically started to "
-"resynchronize git-gui."
-msgstr "Das Aktualisieren der Git-Bereitstellung ist fehlgeschlagen. Eine allgemeine Git-Aktualisierung wird jetzt gestartet, um git-gui wieder mit git zu synchronisieren."
-
-#: lib/index.tcl:27
-msgid "Continue"
-msgstr "Fortsetzen"
-
-#: lib/index.tcl:31
-msgid "Unlock Index"
-msgstr "Bereitstellung freigeben"
-
-#: lib/index.tcl:282
-#, tcl-format
-msgid "Unstaging %s from commit"
-msgstr "Datei »%s« aus der Bereitstellung herausnehmen"
-
-#: lib/index.tcl:326
-#, tcl-format
-msgid "Adding %s"
-msgstr "»%s« hinzufügen..."
-
-#: lib/index.tcl:381
-#, tcl-format
-msgid "Revert changes in file %s?"
-msgstr "Änderungen in Datei »%s« revidieren?"
-
-#: lib/index.tcl:383
-#, tcl-format
-msgid "Revert changes in these %i files?"
-msgstr "Änderungen in den gewählten %i Dateien revidieren?"
-
-#: lib/index.tcl:389
-msgid "Any unstaged changes will be permanently lost by the revert."
-msgstr ""
-"Alle nicht bereitgestellten Änderungen werden beim Revidieren verloren gehen."
-
-#: lib/index.tcl:392
-msgid "Do Nothing"
-msgstr "Nichts tun"
-
-#: lib/merge.tcl:13
-msgid ""
-"Cannot merge while amending.\n"
-"\n"
-"You must finish amending this commit before starting any type of merge.\n"
-msgstr ""
-"Zusammenführen kann nicht gleichzeitig mit Nachbessern durchgeführt werden.\n"
-"\n"
-"Sie müssen zuerst die Nachbesserungs-Version abschließen, bevor Sie "
-"zusammenführen können.\n"
-
-#: lib/merge.tcl:27
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before a merge can be performed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"Der letzte geladene Status stimmt nicht mehr mit dem Projektarchiv überein.\n"
-"\n"
-"Ein anderes Git-Programm hat das Projektarchiv seit dem letzten Laden "
-"geändert.  Vor einem Zusammenführen muss neu geladen werden.\n"
-"\n"
-"Es wird gleich neu geladen.\n"
-
-#: lib/merge.tcl:44
-#, tcl-format
-msgid ""
-"You are in the middle of a conflicted merge.\n"
-"\n"
-"File %s has merge conflicts.\n"
-"\n"
-"You must resolve them, stage the file, and commit to complete the current "
-"merge.  Only then can you begin another merge.\n"
-msgstr ""
-"Zusammenführung mit Konflikten.\n"
-"\n"
-"Die Datei »%s« enthält Konflikte beim Zusammenführen. Sie müssen diese "
-"Konflikte per Hand auflösen. Anschließend müssen Sie die Datei wieder "
-"bereitstellen und eintragen, um die Zusammenführung abzuschließen. Erst "
-"danach kann eine neue Zusammenführung begonnen werden.\n"
-
-#: lib/merge.tcl:54
-#, tcl-format
-msgid ""
-"You are in the middle of a change.\n"
-"\n"
-"File %s is modified.\n"
-"\n"
-"You should complete the current commit before starting a merge.  Doing so "
-"will help you abort a failed merge, should the need arise.\n"
-msgstr ""
-"Es liegen Änderungen vor.\n"
-"\n"
-"Die Datei »%s« wurde geändert.  Sie sollten zuerst die bereitgestellte "
-"Version abschließen, bevor Sie eine Zusammenführung beginnen.  Mit dieser "
-"Reihenfolge können Sie mögliche Konflikte beim Zusammenführen wesentlich "
-"einfacher beheben oder abbrechen.\n"
-
-#: lib/merge.tcl:106
-#, tcl-format
-msgid "%s of %s"
-msgstr "%s von %s"
-
-#: lib/merge.tcl:119
-#, tcl-format
-msgid "Merging %s and %s"
-msgstr "Zusammenführen von %s und %s"
-
-#: lib/merge.tcl:131
-msgid "Merge completed successfully."
-msgstr "Zusammenführen erfolgreich abgeschlossen."
-
-#: lib/merge.tcl:133
-msgid "Merge failed.  Conflict resolution is required."
-msgstr "Zusammenführen fehlgeschlagen. Konfliktauflösung ist notwendig."
-
-#: lib/merge.tcl:158
-#, tcl-format
-msgid "Merge Into %s"
-msgstr "Zusammenführen in %s"
-
-#: lib/merge.tcl:177
-msgid "Revision To Merge"
-msgstr "Zusammenzuführende Version"
-
-#: lib/merge.tcl:212
-msgid ""
-"Cannot abort while amending.\n"
-"\n"
-"You must finish amending this commit.\n"
-msgstr ""
-"Abbruch der Nachbesserung ist nicht möglich.\n"
-"\n"
-"Sie müssen die Nachbesserung der Version abschließen.\n"
-
-#: lib/merge.tcl:222
-msgid ""
-"Abort merge?\n"
-"\n"
-"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with aborting the current merge?"
-msgstr ""
-"Zusammenführen abbrechen?\n"
-"\n"
-"Wenn Sie abbrechen, gehen alle noch nicht eingetragenen Änderungen "
-"verloren.\n"
-"\n"
-"Zusammenführen jetzt abbrechen?"
-
-#: lib/merge.tcl:228
-msgid ""
-"Reset changes?\n"
-"\n"
-"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with resetting the current changes?"
-msgstr ""
-"Änderungen zurücksetzen?\n"
-"\n"
-"Wenn Sie zurücksetzen, gehen alle noch nicht eingetragenen Änderungen "
-"verloren.\n"
-"\n"
-"Änderungen jetzt zurücksetzen?"
-
-#: lib/merge.tcl:239
-msgid "Aborting"
-msgstr "Abbruch"
-
-#: lib/merge.tcl:266
-msgid "Abort failed."
-msgstr "Abbruch fehlgeschlagen."
-
-#: lib/merge.tcl:268
-msgid "Abort completed.  Ready."
-msgstr "Abbruch durchgeführt. Bereit."
-
-#: lib/option.tcl:82
-msgid "Restore Defaults"
-msgstr "Voreinstellungen wiederherstellen"
-
-#: lib/option.tcl:86
-msgid "Save"
-msgstr "Speichern"
-
-#: lib/option.tcl:96
-#, tcl-format
-msgid "%s Repository"
-msgstr "Projektarchiv %s"
-
-#: lib/option.tcl:97
-msgid "Global (All Repositories)"
-msgstr "Global (Alle Projektarchive)"
-
-#: lib/option.tcl:103
-msgid "User Name"
-msgstr "Benutzername"
-
-#: lib/option.tcl:104
-msgid "Email Address"
-msgstr "E-Mail-Adresse"
-
-#: lib/option.tcl:106
-msgid "Summarize Merge Commits"
-msgstr "Zusammenführungs-Versionen zusammenfassen"
-
-#: lib/option.tcl:107
-msgid "Merge Verbosity"
-msgstr "Ausführlichkeit der Zusammenführen-Meldungen"
-
-#: lib/option.tcl:108
-msgid "Show Diffstat After Merge"
-msgstr "Vergleichsstatistik nach Zusammenführen anzeigen"
-
-#: lib/option.tcl:110
-msgid "Trust File Modification Timestamps"
-msgstr "Auf Dateiänderungsdatum verlassen"
-
-#: lib/option.tcl:111
-msgid "Prune Tracking Branches During Fetch"
-msgstr "Übernahmezweige entfernen während Anforderung"
-
-#: lib/option.tcl:112
-msgid "Match Tracking Branches"
-msgstr "Passend zu Übernahmezweig"
-
-#: lib/option.tcl:113
-msgid "Number of Diff Context Lines"
-msgstr "Anzahl der Kontextzeilen beim Vergleich"
-
-#: lib/option.tcl:114
-msgid "New Branch Name Template"
-msgstr "Namensvorschlag für neue Zweige"
-
-#: lib/option.tcl:176
-msgid "Change Font"
-msgstr "Schriftart ändern"
-
-#: lib/option.tcl:180
-#, tcl-format
-msgid "Choose %s"
-msgstr "%s wählen"
-
-#: lib/option.tcl:186
-msgid "pt."
-msgstr "pt."
-
-#: lib/option.tcl:200
-msgid "Preferences"
-msgstr "Einstellungen"
-
-#: lib/option.tcl:235
-msgid "Failed to completely save options:"
-msgstr "Optionen konnten nicht gespeichert werden:"
-
-#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
-msgid "Delete Remote Branch"
-msgstr "Zweig im anderen Projektarchiv löschen"
-
-#: lib/remote_branch_delete.tcl:47
-msgid "From Repository"
-msgstr "Von Projektarchiv"
-
-#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
-msgid "Remote:"
-msgstr "Anderes Archiv:"
-
-#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
-msgid "Arbitrary URL:"
-msgstr "Kommunikation mit URL:"
-
-#: lib/remote_branch_delete.tcl:84
-msgid "Branches"
-msgstr "Zweige"
-
-#: lib/remote_branch_delete.tcl:109
-msgid "Delete Only If"
-msgstr "Löschen, falls"
-
-#: lib/remote_branch_delete.tcl:111
-msgid "Merged Into:"
-msgstr "Zusammenführen mit:"
-
-#: lib/remote_branch_delete.tcl:119
-msgid "Always (Do not perform merge checks)"
-msgstr "Immer (Keine Zusammenführungsprüfung)"
-
-#: lib/remote_branch_delete.tcl:152
-msgid "A branch is required for 'Merged Into'."
-msgstr "Für »Zusammenführen mit« muss ein Zweig angegeben werden."
-
-#: lib/remote_branch_delete.tcl:184
-#, tcl-format
-msgid ""
-"The following branches are not completely merged into %s:\n"
-"\n"
-" - %s"
-msgstr ""
-"Folgende Zweige sind noch nicht mit »%s« zusammengeführt:\n"
-"\n"
-" - %s"
-
-#: lib/remote_branch_delete.tcl:189
-#, tcl-format
-msgid ""
-"One or more of the merge tests failed because you have not fetched the "
-"necessary commits.  Try fetching from %s first."
-msgstr ""
-"Ein oder mehrere Zusammenführungen sind fehlgeschlagen, da Sie nicht die "
-"notwendigen Versionen vorher angefordert haben.  Sie sollten versuchen, "
-"zuerst von »%s« anzufordern."
-
-#: lib/remote_branch_delete.tcl:207
-msgid "Please select one or more branches to delete."
-msgstr "Bitte wählen Sie mindestens einen Zweig, der gelöscht werden soll."
-
-#: lib/remote_branch_delete.tcl:216
-msgid ""
-"Recovering deleted branches is difficult.\n"
-"\n"
-"Delete the selected branches?"
-msgstr ""
-"Das Wiederherstellen von gelöschten Zweigen ist nur mit größerem Aufwand "
-"möglich.\n"
-"\n"
-"Sollen die ausgewählten Zweige gelöscht werden?"
-
-#: lib/remote_branch_delete.tcl:226
-#, tcl-format
-msgid "Deleting branches from %s"
-msgstr "Zweige auf »%s« werden gelöscht"
-
-#: lib/remote_branch_delete.tcl:286
-msgid "No repository selected."
-msgstr "Kein Projektarchiv ausgewählt."
-
-#: lib/remote_branch_delete.tcl:291
-#, tcl-format
-msgid "Scanning %s..."
-msgstr "»%s« laden..."
-
-#: lib/remote.tcl:165
-msgid "Prune from"
-msgstr "Entfernen von"
-
-#: lib/remote.tcl:170
-msgid "Fetch from"
-msgstr "Anfordern von"
-
-#: lib/remote.tcl:213
-msgid "Push to"
-msgstr "Versenden nach"
-
-#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
-msgid "Cannot write shortcut:"
-msgstr "Fehler beim Schreiben der Verknüpfung:"
-
-#: lib/shortcut.tcl:136
-msgid "Cannot write icon:"
-msgstr "Fehler beim Erstellen des Icons:"
-
-#: lib/status_bar.tcl:83
-#, tcl-format
-msgid "%s ... %*i of %*i %s (%3i%%)"
-msgstr "%s ... %*i von %*i %s (%3i%%)"
-
-#: lib/transport.tcl:6
-#, tcl-format
-msgid "fetch %s"
-msgstr "»%s« anfordern"
-
-#: lib/transport.tcl:7
-#, tcl-format
-msgid "Fetching new changes from %s"
-msgstr "Neue Änderungen von »%s« holen"
-
-#: lib/transport.tcl:18
-#, tcl-format
-msgid "remote prune %s"
-msgstr "Entfernen von »%s« im anderen Archiv"
-
-#: lib/transport.tcl:19
-#, tcl-format
-msgid "Pruning tracking branches deleted from %s"
-msgstr "Übernahmezweige entfernen, die in »%s« gelöscht wurden"
-
-#: lib/transport.tcl:25 lib/transport.tcl:71
-#, tcl-format
-msgid "push %s"
-msgstr "»%s« versenden..."
-
-#: lib/transport.tcl:26
-#, tcl-format
-msgid "Pushing changes to %s"
-msgstr "Änderungen nach »%s« versenden"
-
-#: lib/transport.tcl:72
-#, tcl-format
-msgid "Pushing %s %s to %s"
-msgstr "%s %s nach %s versenden"
-
-#: lib/transport.tcl:89
-msgid "Push Branches"
-msgstr "Zweige versenden"
-
-#: lib/transport.tcl:103
-msgid "Source Branches"
-msgstr "Herkunftszweige"
-
-#: lib/transport.tcl:120
-msgid "Destination Repository"
-msgstr "Ziel-Projektarchiv"
-
-#: lib/transport.tcl:158
-msgid "Transfer Options"
-msgstr "Netzwerk-Einstellungen"
-
-#: lib/transport.tcl:160
-msgid "Force overwrite existing branch (may discard changes)"
-msgstr ""
-"Überschreiben von existierenden Zweigen erzwingen (könnte Änderungen löschen)"
-
-#: lib/transport.tcl:164
-msgid "Use thin pack (for slow network connections)"
-msgstr "Kompaktes Datenformat benutzen (für langsame Netzverbindungen)"
-
-#: lib/transport.tcl:168
-msgid "Include tags"
-msgstr "Mit Markierungen übertragen"
diff --git a/po/git-gui.pot b/po/git-gui.pot
deleted file mode 100644 (file)
index dfa48ae..0000000
+++ /dev/null
@@ -1,1742 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2007-11-24 10:36+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=CHARSET\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
-#: git-gui.sh:733
-msgid "git-gui: fatal error"
-msgstr ""
-
-#: git-gui.sh:565
-#, tcl-format
-msgid "Invalid font specified in %s:"
-msgstr ""
-
-#: git-gui.sh:590
-msgid "Main Font"
-msgstr ""
-
-#: git-gui.sh:591
-msgid "Diff/Console Font"
-msgstr ""
-
-#: git-gui.sh:605
-msgid "Cannot find git in PATH."
-msgstr ""
-
-#: git-gui.sh:632
-msgid "Cannot parse Git version string:"
-msgstr ""
-
-#: git-gui.sh:650
-#, tcl-format
-msgid ""
-"Git version cannot be determined.\n"
-"\n"
-"%s claims it is version '%s'.\n"
-"\n"
-"%s requires at least Git 1.5.0 or later.\n"
-"\n"
-"Assume '%s' is version 1.5.0?\n"
-msgstr ""
-
-#: git-gui.sh:888
-msgid "Git directory not found:"
-msgstr ""
-
-#: git-gui.sh:895
-msgid "Cannot move to top of working directory:"
-msgstr ""
-
-#: git-gui.sh:902
-msgid "Cannot use funny .git directory:"
-msgstr ""
-
-#: git-gui.sh:907
-msgid "No working directory"
-msgstr ""
-
-#: git-gui.sh:1054
-msgid "Refreshing file status..."
-msgstr ""
-
-#: git-gui.sh:1119
-msgid "Scanning for modified files ..."
-msgstr ""
-
-#: git-gui.sh:1294 lib/browser.tcl:245
-msgid "Ready."
-msgstr ""
-
-#: git-gui.sh:1560
-msgid "Unmodified"
-msgstr ""
-
-#: git-gui.sh:1562
-msgid "Modified, not staged"
-msgstr ""
-
-#: git-gui.sh:1563 git-gui.sh:1568
-msgid "Staged for commit"
-msgstr ""
-
-#: git-gui.sh:1564 git-gui.sh:1569
-msgid "Portions staged for commit"
-msgstr ""
-
-#: git-gui.sh:1565 git-gui.sh:1570
-msgid "Staged for commit, missing"
-msgstr ""
-
-#: git-gui.sh:1567
-msgid "Untracked, not staged"
-msgstr ""
-
-#: git-gui.sh:1572
-msgid "Missing"
-msgstr ""
-
-#: git-gui.sh:1573
-msgid "Staged for removal"
-msgstr ""
-
-#: git-gui.sh:1574
-msgid "Staged for removal, still present"
-msgstr ""
-
-#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
-msgid "Requires merge resolution"
-msgstr ""
-
-#: git-gui.sh:1614
-msgid "Starting gitk... please wait..."
-msgstr ""
-
-#: git-gui.sh:1623
-#, tcl-format
-msgid ""
-"Unable to start gitk:\n"
-"\n"
-"%s does not exist"
-msgstr ""
-
-#: git-gui.sh:1823 lib/choose_repository.tcl:35
-msgid "Repository"
-msgstr ""
-
-#: git-gui.sh:1824
-msgid "Edit"
-msgstr ""
-
-#: git-gui.sh:1826 lib/choose_rev.tcl:560
-msgid "Branch"
-msgstr ""
-
-#: git-gui.sh:1829 lib/choose_rev.tcl:547
-msgid "Commit@@noun"
-msgstr ""
-
-#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
-msgid "Merge"
-msgstr ""
-
-#: git-gui.sh:1833 lib/choose_rev.tcl:556
-msgid "Remote"
-msgstr ""
-
-#: git-gui.sh:1842
-msgid "Browse Current Branch's Files"
-msgstr ""
-
-#: git-gui.sh:1846
-msgid "Browse Branch Files..."
-msgstr ""
-
-#: git-gui.sh:1851
-msgid "Visualize Current Branch's History"
-msgstr ""
-
-#: git-gui.sh:1855
-msgid "Visualize All Branch History"
-msgstr ""
-
-#: git-gui.sh:1862
-#, tcl-format
-msgid "Browse %s's Files"
-msgstr ""
-
-#: git-gui.sh:1864
-#, tcl-format
-msgid "Visualize %s's History"
-msgstr ""
-
-#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
-msgid "Database Statistics"
-msgstr ""
-
-#: git-gui.sh:1872 lib/database.tcl:34
-msgid "Compress Database"
-msgstr ""
-
-#: git-gui.sh:1875
-msgid "Verify Database"
-msgstr ""
-
-#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
-#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
-msgid "Create Desktop Icon"
-msgstr ""
-
-#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
-msgid "Quit"
-msgstr ""
-
-#: git-gui.sh:1902
-msgid "Undo"
-msgstr ""
-
-#: git-gui.sh:1905
-msgid "Redo"
-msgstr ""
-
-#: git-gui.sh:1909 git-gui.sh:2403
-msgid "Cut"
-msgstr ""
-
-#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
-#: lib/console.tcl:67
-msgid "Copy"
-msgstr ""
-
-#: git-gui.sh:1915 git-gui.sh:2409
-msgid "Paste"
-msgstr ""
-
-#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
-#: lib/remote_branch_delete.tcl:38
-msgid "Delete"
-msgstr ""
-
-#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
-msgid "Select All"
-msgstr ""
-
-#: git-gui.sh:1931
-msgid "Create..."
-msgstr ""
-
-#: git-gui.sh:1937
-msgid "Checkout..."
-msgstr ""
-
-#: git-gui.sh:1943
-msgid "Rename..."
-msgstr ""
-
-#: git-gui.sh:1948 git-gui.sh:2048
-msgid "Delete..."
-msgstr ""
-
-#: git-gui.sh:1953
-msgid "Reset..."
-msgstr ""
-
-#: git-gui.sh:1965 git-gui.sh:2350
-msgid "New Commit"
-msgstr ""
-
-#: git-gui.sh:1973 git-gui.sh:2357
-msgid "Amend Last Commit"
-msgstr ""
-
-#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
-msgid "Rescan"
-msgstr ""
-
-#: git-gui.sh:1988
-msgid "Stage To Commit"
-msgstr ""
-
-#: git-gui.sh:1994
-msgid "Stage Changed Files To Commit"
-msgstr ""
-
-#: git-gui.sh:2000
-msgid "Unstage From Commit"
-msgstr ""
-
-#: git-gui.sh:2005 lib/index.tcl:393
-msgid "Revert Changes"
-msgstr ""
-
-#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
-msgid "Sign Off"
-msgstr ""
-
-#: git-gui.sh:2016 git-gui.sh:2333
-msgid "Commit@@verb"
-msgstr ""
-
-#: git-gui.sh:2027
-msgid "Local Merge..."
-msgstr ""
-
-#: git-gui.sh:2032
-msgid "Abort Merge..."
-msgstr ""
-
-#: git-gui.sh:2044
-msgid "Push..."
-msgstr ""
-
-#: git-gui.sh:2055 lib/choose_repository.tcl:40
-msgid "Apple"
-msgstr ""
-
-#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
-#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
-#, tcl-format
-msgid "About %s"
-msgstr ""
-
-#: git-gui.sh:2062
-msgid "Preferences..."
-msgstr ""
-
-#: git-gui.sh:2070 git-gui.sh:2595
-msgid "Options..."
-msgstr ""
-
-#: git-gui.sh:2076 lib/choose_repository.tcl:46
-msgid "Help"
-msgstr ""
-
-#: git-gui.sh:2117
-msgid "Online Documentation"
-msgstr ""
-
-#: git-gui.sh:2201
-#, tcl-format
-msgid "fatal: cannot stat path %s: No such file or directory"
-msgstr ""
-
-#: git-gui.sh:2234
-msgid "Current Branch:"
-msgstr ""
-
-#: git-gui.sh:2255
-msgid "Staged Changes (Will Commit)"
-msgstr ""
-
-#: git-gui.sh:2274
-msgid "Unstaged Changes"
-msgstr ""
-
-#: git-gui.sh:2323
-msgid "Stage Changed"
-msgstr ""
-
-#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
-msgid "Push"
-msgstr ""
-
-#: git-gui.sh:2369
-msgid "Initial Commit Message:"
-msgstr ""
-
-#: git-gui.sh:2370
-msgid "Amended Commit Message:"
-msgstr ""
-
-#: git-gui.sh:2371
-msgid "Amended Initial Commit Message:"
-msgstr ""
-
-#: git-gui.sh:2372
-msgid "Amended Merge Commit Message:"
-msgstr ""
-
-#: git-gui.sh:2373
-msgid "Merge Commit Message:"
-msgstr ""
-
-#: git-gui.sh:2374
-msgid "Commit Message:"
-msgstr ""
-
-#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
-msgid "Copy All"
-msgstr ""
-
-#: git-gui.sh:2443 lib/blame.tcl:104
-msgid "File:"
-msgstr ""
-
-#: git-gui.sh:2545
-msgid "Refresh"
-msgstr ""
-
-#: git-gui.sh:2566
-msgid "Apply/Reverse Hunk"
-msgstr ""
-
-#: git-gui.sh:2572
-msgid "Decrease Font Size"
-msgstr ""
-
-#: git-gui.sh:2576
-msgid "Increase Font Size"
-msgstr ""
-
-#: git-gui.sh:2581
-msgid "Show Less Context"
-msgstr ""
-
-#: git-gui.sh:2588
-msgid "Show More Context"
-msgstr ""
-
-#: git-gui.sh:2602
-msgid "Unstage Hunk From Commit"
-msgstr ""
-
-#: git-gui.sh:2604
-msgid "Stage Hunk For Commit"
-msgstr ""
-
-#: git-gui.sh:2623
-msgid "Initializing..."
-msgstr ""
-
-#: git-gui.sh:2718
-#, tcl-format
-msgid ""
-"Possible environment issues exist.\n"
-"\n"
-"The following environment variables are probably\n"
-"going to be ignored by any Git subprocess run\n"
-"by %s:\n"
-"\n"
-msgstr ""
-
-#: git-gui.sh:2748
-msgid ""
-"\n"
-"This is due to a known issue with the\n"
-"Tcl binary distributed by Cygwin."
-msgstr ""
-
-#: git-gui.sh:2753
-#, tcl-format
-msgid ""
-"\n"
-"\n"
-"A good replacement for %s\n"
-"is placing values for the user.name and\n"
-"user.email settings into your personal\n"
-"~/.gitconfig file.\n"
-msgstr ""
-
-#: lib/about.tcl:25
-msgid "git-gui - a graphical user interface for Git."
-msgstr ""
-
-#: lib/blame.tcl:77
-msgid "File Viewer"
-msgstr ""
-
-#: lib/blame.tcl:81
-msgid "Commit:"
-msgstr ""
-
-#: lib/blame.tcl:249
-msgid "Copy Commit"
-msgstr ""
-
-#: lib/blame.tcl:369
-#, tcl-format
-msgid "Reading %s..."
-msgstr ""
-
-#: lib/blame.tcl:473
-msgid "Loading copy/move tracking annotations..."
-msgstr ""
-
-#: lib/blame.tcl:493
-msgid "lines annotated"
-msgstr ""
-
-#: lib/blame.tcl:674
-msgid "Loading original location annotations..."
-msgstr ""
-
-#: lib/blame.tcl:677
-msgid "Annotation complete."
-msgstr ""
-
-#: lib/blame.tcl:731
-msgid "Loading annotation..."
-msgstr ""
-
-#: lib/blame.tcl:787
-msgid "Author:"
-msgstr ""
-
-#: lib/blame.tcl:791
-msgid "Committer:"
-msgstr ""
-
-#: lib/blame.tcl:796
-msgid "Original File:"
-msgstr ""
-
-#: lib/blame.tcl:910
-msgid "Originally By:"
-msgstr ""
-
-#: lib/blame.tcl:916
-msgid "In File:"
-msgstr ""
-
-#: lib/blame.tcl:921
-msgid "Copied Or Moved Here By:"
-msgstr ""
-
-#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
-msgid "Checkout Branch"
-msgstr ""
-
-#: lib/branch_checkout.tcl:23
-msgid "Checkout"
-msgstr ""
-
-#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
-#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
-#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
-#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
-msgid "Cancel"
-msgstr ""
-
-#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
-msgid "Revision"
-msgstr ""
-
-#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
-msgid "Options"
-msgstr ""
-
-#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
-msgid "Fetch Tracking Branch"
-msgstr ""
-
-#: lib/branch_checkout.tcl:44
-msgid "Detach From Local Branch"
-msgstr ""
-
-#: lib/branch_create.tcl:22
-msgid "Create Branch"
-msgstr ""
-
-#: lib/branch_create.tcl:27
-msgid "Create New Branch"
-msgstr ""
-
-#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
-msgid "Create"
-msgstr ""
-
-#: lib/branch_create.tcl:40
-msgid "Branch Name"
-msgstr ""
-
-#: lib/branch_create.tcl:43
-msgid "Name:"
-msgstr ""
-
-#: lib/branch_create.tcl:58
-msgid "Match Tracking Branch Name"
-msgstr ""
-
-#: lib/branch_create.tcl:66
-msgid "Starting Revision"
-msgstr ""
-
-#: lib/branch_create.tcl:72
-msgid "Update Existing Branch:"
-msgstr ""
-
-#: lib/branch_create.tcl:75
-msgid "No"
-msgstr ""
-
-#: lib/branch_create.tcl:80
-msgid "Fast Forward Only"
-msgstr ""
-
-#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
-msgid "Reset"
-msgstr ""
-
-#: lib/branch_create.tcl:97
-msgid "Checkout After Creation"
-msgstr ""
-
-#: lib/branch_create.tcl:131
-msgid "Please select a tracking branch."
-msgstr ""
-
-#: lib/branch_create.tcl:140
-#, tcl-format
-msgid "Tracking branch %s is not a branch in the remote repository."
-msgstr ""
-
-#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
-msgid "Please supply a branch name."
-msgstr ""
-
-#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
-#, tcl-format
-msgid "'%s' is not an acceptable branch name."
-msgstr ""
-
-#: lib/branch_delete.tcl:15
-msgid "Delete Branch"
-msgstr ""
-
-#: lib/branch_delete.tcl:20
-msgid "Delete Local Branch"
-msgstr ""
-
-#: lib/branch_delete.tcl:37
-msgid "Local Branches"
-msgstr ""
-
-#: lib/branch_delete.tcl:52
-msgid "Delete Only If Merged Into"
-msgstr ""
-
-#: lib/branch_delete.tcl:54
-msgid "Always (Do not perform merge test.)"
-msgstr ""
-
-#: lib/branch_delete.tcl:103
-#, tcl-format
-msgid "The following branches are not completely merged into %s:"
-msgstr ""
-
-#: lib/branch_delete.tcl:115
-msgid ""
-"Recovering deleted branches is difficult. \n"
-"\n"
-" Delete the selected branches?"
-msgstr ""
-
-#: lib/branch_delete.tcl:141
-#, tcl-format
-msgid ""
-"Failed to delete branches:\n"
-"%s"
-msgstr ""
-
-#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
-msgid "Rename Branch"
-msgstr ""
-
-#: lib/branch_rename.tcl:26
-msgid "Rename"
-msgstr ""
-
-#: lib/branch_rename.tcl:36
-msgid "Branch:"
-msgstr ""
-
-#: lib/branch_rename.tcl:39
-msgid "New Name:"
-msgstr ""
-
-#: lib/branch_rename.tcl:75
-msgid "Please select a branch to rename."
-msgstr ""
-
-#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
-#, tcl-format
-msgid "Branch '%s' already exists."
-msgstr ""
-
-#: lib/branch_rename.tcl:117
-#, tcl-format
-msgid "Failed to rename '%s'."
-msgstr ""
-
-#: lib/browser.tcl:17
-msgid "Starting..."
-msgstr ""
-
-#: lib/browser.tcl:26
-msgid "File Browser"
-msgstr ""
-
-#: lib/browser.tcl:125 lib/browser.tcl:142
-#, tcl-format
-msgid "Loading %s..."
-msgstr ""
-
-#: lib/browser.tcl:186
-msgid "[Up To Parent]"
-msgstr ""
-
-#: lib/browser.tcl:266 lib/browser.tcl:272
-msgid "Browse Branch Files"
-msgstr ""
-
-#: lib/browser.tcl:277 lib/choose_repository.tcl:391
-#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
-#: lib/choose_repository.tcl:989
-msgid "Browse"
-msgstr ""
-
-#: lib/checkout_op.tcl:79
-#, tcl-format
-msgid "Fetching %s from %s"
-msgstr ""
-
-#: lib/checkout_op.tcl:127
-#, tcl-format
-msgid "fatal: Cannot resolve %s"
-msgstr ""
-
-#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
-msgid "Close"
-msgstr ""
-
-#: lib/checkout_op.tcl:169
-#, tcl-format
-msgid "Branch '%s' does not exist."
-msgstr ""
-
-#: lib/checkout_op.tcl:206
-#, tcl-format
-msgid ""
-"Branch '%s' already exists.\n"
-"\n"
-"It cannot fast-forward to %s.\n"
-"A merge is required."
-msgstr ""
-
-#: lib/checkout_op.tcl:220
-#, tcl-format
-msgid "Merge strategy '%s' not supported."
-msgstr ""
-
-#: lib/checkout_op.tcl:239
-#, tcl-format
-msgid "Failed to update '%s'."
-msgstr ""
-
-#: lib/checkout_op.tcl:251
-msgid "Staging area (index) is already locked."
-msgstr ""
-
-#: lib/checkout_op.tcl:266
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before the current branch can be changed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-
-#: lib/checkout_op.tcl:322
-#, tcl-format
-msgid "Updating working directory to '%s'..."
-msgstr ""
-
-#: lib/checkout_op.tcl:353
-#, tcl-format
-msgid "Aborted checkout of '%s' (file level merging is required)."
-msgstr ""
-
-#: lib/checkout_op.tcl:354
-msgid "File level merge required."
-msgstr ""
-
-#: lib/checkout_op.tcl:358
-#, tcl-format
-msgid "Staying on branch '%s'."
-msgstr ""
-
-#: lib/checkout_op.tcl:429
-msgid ""
-"You are no longer on a local branch.\n"
-"\n"
-"If you wanted to be on a branch, create one now starting from 'This Detached "
-"Checkout'."
-msgstr ""
-
-#: lib/checkout_op.tcl:446
-#, tcl-format
-msgid "Checked out '%s'."
-msgstr ""
-
-#: lib/checkout_op.tcl:478
-#, tcl-format
-msgid "Resetting '%s' to '%s' will lose the following commits:"
-msgstr ""
-
-#: lib/checkout_op.tcl:500
-msgid "Recovering lost commits may not be easy."
-msgstr ""
-
-#: lib/checkout_op.tcl:505
-#, tcl-format
-msgid "Reset '%s'?"
-msgstr ""
-
-#: lib/checkout_op.tcl:510 lib/merge.tcl:164
-msgid "Visualize"
-msgstr ""
-
-#: lib/checkout_op.tcl:578
-#, tcl-format
-msgid ""
-"Failed to set current branch.\n"
-"\n"
-"This working directory is only partially switched.  We successfully updated "
-"your files, but failed to update an internal Git file.\n"
-"\n"
-"This should not have occurred.  %s will now close and give up."
-msgstr ""
-
-#: lib/choose_font.tcl:39
-msgid "Select"
-msgstr ""
-
-#: lib/choose_font.tcl:53
-msgid "Font Family"
-msgstr ""
-
-#: lib/choose_font.tcl:73
-msgid "Font Size"
-msgstr ""
-
-#: lib/choose_font.tcl:90
-msgid "Font Example"
-msgstr ""
-
-#: lib/choose_font.tcl:101
-msgid ""
-"This is example text.\n"
-"If you like this text, it can be your font."
-msgstr ""
-
-#: lib/choose_repository.tcl:27
-msgid "Git Gui"
-msgstr ""
-
-#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
-msgid "Create New Repository"
-msgstr ""
-
-#: lib/choose_repository.tcl:86
-msgid "New..."
-msgstr ""
-
-#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
-msgid "Clone Existing Repository"
-msgstr ""
-
-#: lib/choose_repository.tcl:99
-msgid "Clone..."
-msgstr ""
-
-#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
-msgid "Open Existing Repository"
-msgstr ""
-
-#: lib/choose_repository.tcl:112
-msgid "Open..."
-msgstr ""
-
-#: lib/choose_repository.tcl:125
-msgid "Recent Repositories"
-msgstr ""
-
-#: lib/choose_repository.tcl:131
-msgid "Open Recent Repository:"
-msgstr ""
-
-#: lib/choose_repository.tcl:294
-#, tcl-format
-msgid "Location %s already exists."
-msgstr ""
-
-#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
-#: lib/choose_repository.tcl:314
-#, tcl-format
-msgid "Failed to create repository %s:"
-msgstr ""
-
-#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
-msgid "Directory:"
-msgstr ""
-
-#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
-#: lib/choose_repository.tcl:1013
-msgid "Git Repository"
-msgstr ""
-
-#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
-#, tcl-format
-msgid "Directory %s already exists."
-msgstr ""
-
-#: lib/choose_repository.tcl:442
-#, tcl-format
-msgid "File %s already exists."
-msgstr ""
-
-#: lib/choose_repository.tcl:463
-msgid "Clone"
-msgstr ""
-
-#: lib/choose_repository.tcl:476
-msgid "URL:"
-msgstr ""
-
-#: lib/choose_repository.tcl:496
-msgid "Clone Type:"
-msgstr ""
-
-#: lib/choose_repository.tcl:502
-msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
-msgstr ""
-
-#: lib/choose_repository.tcl:508
-msgid "Full Copy (Slower, Redundant Backup)"
-msgstr ""
-
-#: lib/choose_repository.tcl:514
-msgid "Shared (Fastest, Not Recommended, No Backup)"
-msgstr ""
-
-#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
-#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
-#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
-#, tcl-format
-msgid "Not a Git repository: %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:586
-msgid "Standard only available for local repository."
-msgstr ""
-
-#: lib/choose_repository.tcl:590
-msgid "Shared only available for local repository."
-msgstr ""
-
-#: lib/choose_repository.tcl:617
-msgid "Failed to configure origin"
-msgstr ""
-
-#: lib/choose_repository.tcl:629
-msgid "Counting objects"
-msgstr ""
-
-#: lib/choose_repository.tcl:630
-msgid "buckets"
-msgstr ""
-
-#: lib/choose_repository.tcl:654
-#, tcl-format
-msgid "Unable to copy objects/info/alternates: %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:690
-#, tcl-format
-msgid "Nothing to clone from %s."
-msgstr ""
-
-#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
-#: lib/choose_repository.tcl:918
-msgid "The 'master' branch has not been initialized."
-msgstr ""
-
-#: lib/choose_repository.tcl:705
-msgid "Hardlinks are unavailable.  Falling back to copying."
-msgstr ""
-
-#: lib/choose_repository.tcl:717
-#, tcl-format
-msgid "Cloning from %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:748
-msgid "Copying objects"
-msgstr ""
-
-#: lib/choose_repository.tcl:749
-msgid "KiB"
-msgstr ""
-
-#: lib/choose_repository.tcl:773
-#, tcl-format
-msgid "Unable to copy object: %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:783
-msgid "Linking objects"
-msgstr ""
-
-#: lib/choose_repository.tcl:784
-msgid "objects"
-msgstr ""
-
-#: lib/choose_repository.tcl:792
-#, tcl-format
-msgid "Unable to hardlink object: %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:847
-msgid "Cannot fetch branches and objects.  See console output for details."
-msgstr ""
-
-#: lib/choose_repository.tcl:858
-msgid "Cannot fetch tags.  See console output for details."
-msgstr ""
-
-#: lib/choose_repository.tcl:882
-msgid "Cannot determine HEAD.  See console output for details."
-msgstr ""
-
-#: lib/choose_repository.tcl:891
-#, tcl-format
-msgid "Unable to cleanup %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:897
-msgid "Clone failed."
-msgstr ""
-
-#: lib/choose_repository.tcl:904
-msgid "No default branch obtained."
-msgstr ""
-
-#: lib/choose_repository.tcl:915
-#, tcl-format
-msgid "Cannot resolve %s as a commit."
-msgstr ""
-
-#: lib/choose_repository.tcl:927
-msgid "Creating working directory"
-msgstr ""
-
-#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
-#: lib/index.tcl:193
-msgid "files"
-msgstr ""
-
-#: lib/choose_repository.tcl:957
-msgid "Initial file checkout failed."
-msgstr ""
-
-#: lib/choose_repository.tcl:973
-msgid "Open"
-msgstr ""
-
-#: lib/choose_repository.tcl:983
-msgid "Repository:"
-msgstr ""
-
-#: lib/choose_repository.tcl:1033
-#, tcl-format
-msgid "Failed to open repository %s:"
-msgstr ""
-
-#: lib/choose_rev.tcl:53
-msgid "This Detached Checkout"
-msgstr ""
-
-#: lib/choose_rev.tcl:60
-msgid "Revision Expression:"
-msgstr ""
-
-#: lib/choose_rev.tcl:74
-msgid "Local Branch"
-msgstr ""
-
-#: lib/choose_rev.tcl:79
-msgid "Tracking Branch"
-msgstr ""
-
-#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
-msgid "Tag"
-msgstr ""
-
-#: lib/choose_rev.tcl:317
-#, tcl-format
-msgid "Invalid revision: %s"
-msgstr ""
-
-#: lib/choose_rev.tcl:338
-msgid "No revision selected."
-msgstr ""
-
-#: lib/choose_rev.tcl:346
-msgid "Revision expression is empty."
-msgstr ""
-
-#: lib/choose_rev.tcl:530
-msgid "Updated"
-msgstr ""
-
-#: lib/choose_rev.tcl:558
-msgid "URL"
-msgstr ""
-
-#: lib/commit.tcl:9
-msgid ""
-"There is nothing to amend.\n"
-"\n"
-"You are about to create the initial commit.  There is no commit before this "
-"to amend.\n"
-msgstr ""
-
-#: lib/commit.tcl:18
-msgid ""
-"Cannot amend while merging.\n"
-"\n"
-"You are currently in the middle of a merge that has not been fully "
-"completed.  You cannot amend the prior commit unless you first abort the "
-"current merge activity.\n"
-msgstr ""
-
-#: lib/commit.tcl:49
-msgid "Error loading commit data for amend:"
-msgstr ""
-
-#: lib/commit.tcl:76
-msgid "Unable to obtain your identity:"
-msgstr ""
-
-#: lib/commit.tcl:81
-msgid "Invalid GIT_COMMITTER_IDENT:"
-msgstr ""
-
-#: lib/commit.tcl:133
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before another commit can be created.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-
-#: lib/commit.tcl:154
-#, tcl-format
-msgid ""
-"Unmerged files cannot be committed.\n"
-"\n"
-"File %s has merge conflicts.  You must resolve them and stage the file "
-"before committing.\n"
-msgstr ""
-
-#: lib/commit.tcl:162
-#, tcl-format
-msgid ""
-"Unknown file state %s detected.\n"
-"\n"
-"File %s cannot be committed by this program.\n"
-msgstr ""
-
-#: lib/commit.tcl:170
-msgid ""
-"No changes to commit.\n"
-"\n"
-"You must stage at least 1 file before you can commit.\n"
-msgstr ""
-
-#: lib/commit.tcl:183
-msgid ""
-"Please supply a commit message.\n"
-"\n"
-"A good commit message has the following format:\n"
-"\n"
-"- First line: Describe in one sentence what you did.\n"
-"- Second line: Blank\n"
-"- Remaining lines: Describe why this change is good.\n"
-msgstr ""
-
-#: lib/commit.tcl:257
-msgid "write-tree failed:"
-msgstr ""
-
-#: lib/commit.tcl:275
-#, tcl-format
-msgid "Commit %s appears to be corrupt"
-msgstr ""
-
-#: lib/commit.tcl:279
-msgid ""
-"No changes to commit.\n"
-"\n"
-"No files were modified by this commit and it was not a merge commit.\n"
-"\n"
-"A rescan will be automatically started now.\n"
-msgstr ""
-
-#: lib/commit.tcl:286
-msgid "No changes to commit."
-msgstr ""
-
-#: lib/commit.tcl:303
-#, tcl-format
-msgid "warning: Tcl does not support encoding '%s'."
-msgstr ""
-
-#: lib/commit.tcl:317
-msgid "commit-tree failed:"
-msgstr ""
-
-#: lib/commit.tcl:339
-msgid "update-ref failed:"
-msgstr ""
-
-#: lib/commit.tcl:430
-#, tcl-format
-msgid "Created commit %s: %s"
-msgstr ""
-
-#: lib/console.tcl:57
-msgid "Working... please wait..."
-msgstr ""
-
-#: lib/console.tcl:183
-msgid "Success"
-msgstr ""
-
-#: lib/console.tcl:196
-msgid "Error: Command Failed"
-msgstr ""
-
-#: lib/database.tcl:43
-msgid "Number of loose objects"
-msgstr ""
-
-#: lib/database.tcl:44
-msgid "Disk space used by loose objects"
-msgstr ""
-
-#: lib/database.tcl:45
-msgid "Number of packed objects"
-msgstr ""
-
-#: lib/database.tcl:46
-msgid "Number of packs"
-msgstr ""
-
-#: lib/database.tcl:47
-msgid "Disk space used by packed objects"
-msgstr ""
-
-#: lib/database.tcl:48
-msgid "Packed objects waiting for pruning"
-msgstr ""
-
-#: lib/database.tcl:49
-msgid "Garbage files"
-msgstr ""
-
-#: lib/database.tcl:72
-msgid "Compressing the object database"
-msgstr ""
-
-#: lib/database.tcl:83
-msgid "Verifying the object database with fsck-objects"
-msgstr ""
-
-#: lib/database.tcl:108
-#, tcl-format
-msgid ""
-"This repository currently has approximately %i loose objects.\n"
-"\n"
-"To maintain optimal performance it is strongly recommended that you compress "
-"the database when more than %i loose objects exist.\n"
-"\n"
-"Compress the database now?"
-msgstr ""
-
-#: lib/date.tcl:25
-#, tcl-format
-msgid "Invalid date from Git: %s"
-msgstr ""
-
-#: lib/diff.tcl:42
-#, tcl-format
-msgid ""
-"No differences detected.\n"
-"\n"
-"%s has no changes.\n"
-"\n"
-"The modification date of this file was updated by another application, but "
-"the content within the file was not changed.\n"
-"\n"
-"A rescan will be automatically started to find other files which may have "
-"the same state."
-msgstr ""
-
-#: lib/diff.tcl:81
-#, tcl-format
-msgid "Loading diff of %s..."
-msgstr ""
-
-#: lib/diff.tcl:114 lib/diff.tcl:184
-#, tcl-format
-msgid "Unable to display %s"
-msgstr ""
-
-#: lib/diff.tcl:115
-msgid "Error loading file:"
-msgstr ""
-
-#: lib/diff.tcl:122
-msgid "Git Repository (subproject)"
-msgstr ""
-
-#: lib/diff.tcl:134
-msgid "* Binary file (not showing content)."
-msgstr ""
-
-#: lib/diff.tcl:185
-msgid "Error loading diff:"
-msgstr ""
-
-#: lib/diff.tcl:302
-msgid "Failed to unstage selected hunk."
-msgstr ""
-
-#: lib/diff.tcl:309
-msgid "Failed to stage selected hunk."
-msgstr ""
-
-#: lib/error.tcl:12 lib/error.tcl:102
-msgid "error"
-msgstr ""
-
-#: lib/error.tcl:28
-msgid "warning"
-msgstr ""
-
-#: lib/error.tcl:81
-msgid "You must correct the above errors before committing."
-msgstr ""
-
-#: lib/index.tcl:6
-msgid "Unable to unlock the index."
-msgstr ""
-
-#: lib/index.tcl:15
-msgid "Index Error"
-msgstr ""
-
-#: lib/index.tcl:21
-msgid ""
-"Updating the Git index failed.  A rescan will be automatically started to "
-"resynchronize git-gui."
-msgstr ""
-
-#: lib/index.tcl:27
-msgid "Continue"
-msgstr ""
-
-#: lib/index.tcl:31
-msgid "Unlock Index"
-msgstr ""
-
-#: lib/index.tcl:282
-#, tcl-format
-msgid "Unstaging %s from commit"
-msgstr ""
-
-#: lib/index.tcl:326
-#, tcl-format
-msgid "Adding %s"
-msgstr ""
-
-#: lib/index.tcl:381
-#, tcl-format
-msgid "Revert changes in file %s?"
-msgstr ""
-
-#: lib/index.tcl:383
-#, tcl-format
-msgid "Revert changes in these %i files?"
-msgstr ""
-
-#: lib/index.tcl:389
-msgid "Any unstaged changes will be permanently lost by the revert."
-msgstr ""
-
-#: lib/index.tcl:392
-msgid "Do Nothing"
-msgstr ""
-
-#: lib/merge.tcl:13
-msgid ""
-"Cannot merge while amending.\n"
-"\n"
-"You must finish amending this commit before starting any type of merge.\n"
-msgstr ""
-
-#: lib/merge.tcl:27
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before a merge can be performed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-
-#: lib/merge.tcl:44
-#, tcl-format
-msgid ""
-"You are in the middle of a conflicted merge.\n"
-"\n"
-"File %s has merge conflicts.\n"
-"\n"
-"You must resolve them, stage the file, and commit to complete the current "
-"merge.  Only then can you begin another merge.\n"
-msgstr ""
-
-#: lib/merge.tcl:54
-#, tcl-format
-msgid ""
-"You are in the middle of a change.\n"
-"\n"
-"File %s is modified.\n"
-"\n"
-"You should complete the current commit before starting a merge.  Doing so "
-"will help you abort a failed merge, should the need arise.\n"
-msgstr ""
-
-#: lib/merge.tcl:106
-#, tcl-format
-msgid "%s of %s"
-msgstr ""
-
-#: lib/merge.tcl:119
-#, tcl-format
-msgid "Merging %s and %s"
-msgstr ""
-
-#: lib/merge.tcl:131
-msgid "Merge completed successfully."
-msgstr ""
-
-#: lib/merge.tcl:133
-msgid "Merge failed.  Conflict resolution is required."
-msgstr ""
-
-#: lib/merge.tcl:158
-#, tcl-format
-msgid "Merge Into %s"
-msgstr ""
-
-#: lib/merge.tcl:177
-msgid "Revision To Merge"
-msgstr ""
-
-#: lib/merge.tcl:212
-msgid ""
-"Cannot abort while amending.\n"
-"\n"
-"You must finish amending this commit.\n"
-msgstr ""
-
-#: lib/merge.tcl:222
-msgid ""
-"Abort merge?\n"
-"\n"
-"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with aborting the current merge?"
-msgstr ""
-
-#: lib/merge.tcl:228
-msgid ""
-"Reset changes?\n"
-"\n"
-"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with resetting the current changes?"
-msgstr ""
-
-#: lib/merge.tcl:239
-msgid "Aborting"
-msgstr ""
-
-#: lib/merge.tcl:266
-msgid "Abort failed."
-msgstr ""
-
-#: lib/merge.tcl:268
-msgid "Abort completed.  Ready."
-msgstr ""
-
-#: lib/option.tcl:82
-msgid "Restore Defaults"
-msgstr ""
-
-#: lib/option.tcl:86
-msgid "Save"
-msgstr ""
-
-#: lib/option.tcl:96
-#, tcl-format
-msgid "%s Repository"
-msgstr ""
-
-#: lib/option.tcl:97
-msgid "Global (All Repositories)"
-msgstr ""
-
-#: lib/option.tcl:103
-msgid "User Name"
-msgstr ""
-
-#: lib/option.tcl:104
-msgid "Email Address"
-msgstr ""
-
-#: lib/option.tcl:106
-msgid "Summarize Merge Commits"
-msgstr ""
-
-#: lib/option.tcl:107
-msgid "Merge Verbosity"
-msgstr ""
-
-#: lib/option.tcl:108
-msgid "Show Diffstat After Merge"
-msgstr ""
-
-#: lib/option.tcl:110
-msgid "Trust File Modification Timestamps"
-msgstr ""
-
-#: lib/option.tcl:111
-msgid "Prune Tracking Branches During Fetch"
-msgstr ""
-
-#: lib/option.tcl:112
-msgid "Match Tracking Branches"
-msgstr ""
-
-#: lib/option.tcl:113
-msgid "Number of Diff Context Lines"
-msgstr ""
-
-#: lib/option.tcl:114
-msgid "New Branch Name Template"
-msgstr ""
-
-#: lib/option.tcl:176
-msgid "Change Font"
-msgstr ""
-
-#: lib/option.tcl:180
-#, tcl-format
-msgid "Choose %s"
-msgstr ""
-
-#: lib/option.tcl:186
-msgid "pt."
-msgstr ""
-
-#: lib/option.tcl:200
-msgid "Preferences"
-msgstr ""
-
-#: lib/option.tcl:235
-msgid "Failed to completely save options:"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
-msgid "Delete Remote Branch"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:47
-msgid "From Repository"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
-msgid "Remote:"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
-msgid "Arbitrary URL:"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:84
-msgid "Branches"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:109
-msgid "Delete Only If"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:111
-msgid "Merged Into:"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:119
-msgid "Always (Do not perform merge checks)"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:152
-msgid "A branch is required for 'Merged Into'."
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:184
-#, tcl-format
-msgid ""
-"The following branches are not completely merged into %s:\n"
-"\n"
-" - %s"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:189
-#, tcl-format
-msgid ""
-"One or more of the merge tests failed because you have not fetched the "
-"necessary commits.  Try fetching from %s first."
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:207
-msgid "Please select one or more branches to delete."
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:216
-msgid ""
-"Recovering deleted branches is difficult.\n"
-"\n"
-"Delete the selected branches?"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:226
-#, tcl-format
-msgid "Deleting branches from %s"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:286
-msgid "No repository selected."
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:291
-#, tcl-format
-msgid "Scanning %s..."
-msgstr ""
-
-#: lib/remote.tcl:165
-msgid "Prune from"
-msgstr ""
-
-#: lib/remote.tcl:170
-msgid "Fetch from"
-msgstr ""
-
-#: lib/remote.tcl:213
-msgid "Push to"
-msgstr ""
-
-#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
-msgid "Cannot write shortcut:"
-msgstr ""
-
-#: lib/shortcut.tcl:136
-msgid "Cannot write icon:"
-msgstr ""
-
-#: lib/status_bar.tcl:83
-#, tcl-format
-msgid "%s ... %*i of %*i %s (%3i%%)"
-msgstr ""
-
-#: lib/transport.tcl:6
-#, tcl-format
-msgid "fetch %s"
-msgstr ""
-
-#: lib/transport.tcl:7
-#, tcl-format
-msgid "Fetching new changes from %s"
-msgstr ""
-
-#: lib/transport.tcl:18
-#, tcl-format
-msgid "remote prune %s"
-msgstr ""
-
-#: lib/transport.tcl:19
-#, tcl-format
-msgid "Pruning tracking branches deleted from %s"
-msgstr ""
-
-#: lib/transport.tcl:25 lib/transport.tcl:71
-#, tcl-format
-msgid "push %s"
-msgstr ""
-
-#: lib/transport.tcl:26
-#, tcl-format
-msgid "Pushing changes to %s"
-msgstr ""
-
-#: lib/transport.tcl:72
-#, tcl-format
-msgid "Pushing %s %s to %s"
-msgstr ""
-
-#: lib/transport.tcl:89
-msgid "Push Branches"
-msgstr ""
-
-#: lib/transport.tcl:103
-msgid "Source Branches"
-msgstr ""
-
-#: lib/transport.tcl:120
-msgid "Destination Repository"
-msgstr ""
-
-#: lib/transport.tcl:158
-msgid "Transfer Options"
-msgstr ""
-
-#: lib/transport.tcl:160
-msgid "Force overwrite existing branch (may discard changes)"
-msgstr ""
-
-#: lib/transport.tcl:164
-msgid "Use thin pack (for slow network connections)"
-msgstr ""
-
-#: lib/transport.tcl:168
-msgid "Include tags"
-msgstr ""
diff --git a/po/glossary/Makefile b/po/glossary/Makefile
deleted file mode 100644 (file)
index 749aa2e..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-PO_TEMPLATE = git-gui-glossary.pot
-
-ALL_POFILES = $(wildcard *.po)
-
-$(PO_TEMPLATE): $(subst .pot,.txt,$(PO_TEMPLATE))
-       ./txt-to-pot.sh $< > $@
-
-update-po:: git-gui-glossary.pot
-       $(foreach p, $(ALL_POFILES), echo Updating $p ; msgmerge -U $p $(PO_TEMPLATE) ; )
diff --git a/po/glossary/de.po b/po/glossary/de.po
deleted file mode 100644 (file)
index c94786c..0000000
+++ /dev/null
@@ -1,185 +0,0 @@
-# Translation of git-gui glossary to German
-# Copyright (C) 2007 Shawn Pearce, et al.
-# This file is distributed under the same license as the git package.
-# Christian Stimming <stimming@tuhh.de>, 2007
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: git-gui glossary\n"
-"POT-Creation-Date: 2007-10-19 21:43+0200\n"
-"PO-Revision-Date: 2007-10-20 15:24+0200\n"
-"Last-Translator: Christian Stimming <stimming@tuhh.de>\n"
-"Language-Team: German \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#. "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
-msgid ""
-"English Term (Dear translator: This file will never be visible to the user!)"
-msgstr ""
-"Deutsche Übersetzung.\n"
-"Andere deutsche SCM:\n"
-"  http://tortoisesvn.net/docs/release/TortoiseSVN_de/index.html und http://"
-"tortoisesvn.tigris.org/svn/tortoisesvn/trunk/Languages/Tortoise_de.po "
-"(username=guest, password empty, gut),\n"
-"  http://msdn.microsoft.com/de-de/library/ms181038(vs.80).aspx (MS Visual "
-"Source Safe, kommerziell),\n"
-"  http://cvsbook.red-bean.com/translations/german/Kap_06.html "
-"(mittelmäßig),\n"
-"  http://tortoisecvs.cvs.sourceforge.net/tortoisecvs/po/TortoiseCVS/de_DE.po?"
-"view=markup (mittelmäßig),\n"
-"  http://rapidsvn.tigris.org/svn/rapidsvn/trunk/src/locale/de/rapidsvn.po "
-"(username=guest, password empty, schlecht)"
-
-#. ""
-msgid "amend"
-msgstr "nachbessern (ergänzen)"
-
-#. ""
-msgid "annotate"
-msgstr "annotieren"
-
-#. "A 'branch' is an active line of development."
-msgid "branch [noun]"
-msgstr "Zweig"
-
-#. ""
-msgid "branch [verb]"
-msgstr "verzweigen"
-
-#. ""
-msgid "checkout [noun]"
-msgstr ""
-"Arbeitskopie (Erstellung einer Arbeitskopie; Auscheck? Ausspielung? Abruf? "
-"Source Safe: Auscheckvorgang)"
-
-#. "The action of updating the working tree to a revision which was stored in the object database."
-msgid "checkout [verb]"
-msgstr ""
-"Arbeitskopie erstellen; Zweig umstellen [checkout a branch] (auschecken? "
-"ausspielen? abrufen? Source Safe: auschecken)"
-
-#. ""
-msgid "clone [verb]"
-msgstr "kopieren"
-
-#. "A single point in the git history."
-msgid "commit [noun]"
-msgstr ""
-"Version; Eintragung; Änderung (Buchung?, Eintragung?, Übertragung?, "
-"Sendung?, Übergabe?, Einspielung?, Ablagevorgang?)"
-
-#. "The action of storing a new snapshot of the project's state in the git history."
-msgid "commit [verb]"
-msgstr ""
-"eintragen (TortoiseSVN: übertragen; Source Safe: einchecken; senden?, "
-"übergeben?, einspielen?, einpflegen?, ablegen?)"
-
-#. ""
-msgid "diff [noun]"
-msgstr "Vergleich (Source Safe: Unterschiede)"
-
-#. ""
-msgid "diff [verb]"
-msgstr "vergleichen"
-
-#. "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
-msgid "fast forward merge"
-msgstr "Schnellzusammenführung"
-
-#. "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
-msgid "fetch"
-msgstr "anfordern (holen?)"
-
-#. "A collection of files. The index is a stored version of your working tree."
-msgid "index (in git-gui: staging area)"
-msgstr "Bereitstellung"
-
-#. "A successful merge results in the creation of a new commit representing the result of the merge."
-msgid "merge [noun]"
-msgstr "Zusammenführung"
-
-#. "To bring the contents of another branch into the current branch."
-msgid "merge [verb]"
-msgstr "zusammenführen"
-
-#. ""
-msgid "message"
-msgstr "Beschreibung (Meldung?, Nachricht?; Source Safe: Kommentar)"
-
-#. "Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in 'remotes/<name>'."
-msgid "prune"
-msgstr "entfernen"
-
-#. "Pulling a branch means to fetch it and merge it."
-msgid "pull"
-msgstr "übernehmen (ziehen?)"
-
-#. "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
-msgid "push"
-msgstr "versenden (ausliefern? hochladen? verschicken? schieben?)"
-
-#. ""
-msgid "redo"
-msgstr "wiederholen"
-
-#. "An other repository ('remote'). One might have a set of remotes whose branches one tracks."
-msgid "remote"
-msgstr "Andere Archive (Gegenseite?, Entfernte?, Server?)"
-
-#. "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
-msgid "repository"
-msgstr "Projektarchiv"
-
-#. ""
-msgid "reset"
-msgstr "zurücksetzen (zurückkehren?)"
-
-#. ""
-msgid "revert"
-msgstr "revidieren"
-
-#. "A particular state of files and directories which was stored in the object database."
-msgid "revision"
-msgstr "Version (TortoiseSVN: Revision; Source Safe: Version)"
-
-#. ""
-msgid "sign off"
-msgstr "abzeichnen (gegenzeichnen?, freizeichnen?, absegnen?)"
-
-#. ""
-msgid "staging area"
-msgstr "Bereitstellung"
-
-#. ""
-msgid "status"
-msgstr "Status"
-
-#. "A ref pointing to a tag or commit object"
-msgid "tag [noun]"
-msgstr "Markierung"
-
-#. ""
-msgid "tag [verb]"
-msgstr "markieren"
-
-#. "A regular git branch that is used to follow changes from another repository."
-msgid "tracking branch"
-msgstr "Übernahmezweig"
-
-#. ""
-msgid "undo"
-msgstr "rückgängig"
-
-#. ""
-msgid "update"
-msgstr "aktualisieren"
-
-#. ""
-msgid "verify"
-msgstr "überprüfen"
-
-#. "The tree of actual checked out files."
-msgid "working copy, working tree"
-msgstr "Arbeitskopie"
diff --git a/po/glossary/git-gui-glossary.pot b/po/glossary/git-gui-glossary.pot
deleted file mode 100644 (file)
index 48af803..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR Free Software Foundation, Inc.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"POT-Creation-Date: 2007-10-19 21:43+0200\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=CHARSET\n"
-"Content-Transfer-Encoding: ENCODING\n"
-
-#. "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
-msgid "English Term (Dear translator: This file will never be visible to the user!)"
-msgstr ""
-
-#. ""
-msgid "amend"
-msgstr ""
-
-#. ""
-msgid "annotate"
-msgstr ""
-
-#. "A 'branch' is an active line of development."
-msgid "branch [noun]"
-msgstr ""
-
-#. ""
-msgid "branch [verb]"
-msgstr ""
-
-#. ""
-msgid "checkout [noun]"
-msgstr ""
-
-#. "The action of updating the working tree to a revision which was stored in the object database."
-msgid "checkout [verb]"
-msgstr ""
-
-#. ""
-msgid "clone [verb]"
-msgstr ""
-
-#. "A single point in the git history."
-msgid "commit [noun]"
-msgstr ""
-
-#. "The action of storing a new snapshot of the project's state in the git history."
-msgid "commit [verb]"
-msgstr ""
-
-#. ""
-msgid "diff [noun]"
-msgstr ""
-
-#. ""
-msgid "diff [verb]"
-msgstr ""
-
-#. "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
-msgid "fast forward merge"
-msgstr ""
-
-#. "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
-msgid "fetch"
-msgstr ""
-
-#. "A collection of files. The index is a stored version of your working tree."
-msgid "index (in git-gui: staging area)"
-msgstr ""
-
-#. "A successful merge results in the creation of a new commit representing the result of the merge."
-msgid "merge [noun]"
-msgstr ""
-
-#. "To bring the contents of another branch into the current branch."
-msgid "merge [verb]"
-msgstr ""
-
-#. ""
-msgid "message"
-msgstr ""
-
-#. "Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in 'remotes/<name>'."
-msgid "prune"
-msgstr ""
-
-#. "Pulling a branch means to fetch it and merge it."
-msgid "pull"
-msgstr ""
-
-#. "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
-msgid "push"
-msgstr ""
-
-#. ""
-msgid "redo"
-msgstr ""
-
-#. "An other repository ('remote'). One might have a set of remotes whose branches one tracks."
-msgid "remote"
-msgstr ""
-
-#. "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
-msgid "repository"
-msgstr ""
-
-#. ""
-msgid "reset"
-msgstr ""
-
-#. ""
-msgid "revert"
-msgstr ""
-
-#. "A particular state of files and directories which was stored in the object database."
-msgid "revision"
-msgstr ""
-
-#. ""
-msgid "sign off"
-msgstr ""
-
-#. ""
-msgid "staging area"
-msgstr ""
-
-#. ""
-msgid "status"
-msgstr ""
-
-#. "A ref pointing to a tag or commit object"
-msgid "tag [noun]"
-msgstr ""
-
-#. ""
-msgid "tag [verb]"
-msgstr ""
-
-#. "A regular git branch that is used to follow changes from another repository."
-msgid "tracking branch"
-msgstr ""
-
-#. ""
-msgid "undo"
-msgstr ""
-
-#. ""
-msgid "update"
-msgstr ""
-
-#. ""
-msgid "verify"
-msgstr ""
-
-#. "The tree of actual checked out files."
-msgid "working copy, working tree"
-msgstr ""
-
diff --git a/po/glossary/git-gui-glossary.txt b/po/glossary/git-gui-glossary.txt
deleted file mode 100644 (file)
index 500d0a0..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-"English Term (Dear translator: This file will never be visible to the user!)" "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
-"amend"        ""
-"annotate"     ""
-"branch [noun]"        "A 'branch' is an active line of development."
-"branch [verb]"        ""
-"checkout [noun]"      ""
-"checkout [verb]"      "The action of updating the working tree to a revision which was stored in the object database."
-"clone [verb]" ""
-"commit [noun]"        "A single point in the git history."
-"commit [verb]"        "The action of storing a new snapshot of the project's state in the git history."
-"diff [noun]"  ""
-"diff [verb]"  ""
-"fast forward merge"   "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
-"fetch"        "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
-"index (in git-gui: staging area)"     "A collection of files. The index is a stored version of your working tree."
-"merge [noun]" "A successful merge results in the creation of a new commit representing the result of the merge."
-"merge [verb]" "To bring the contents of another branch into the current branch."
-"message"      ""
-"prune"        "Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in 'remotes/<name>'."
-"pull" "Pulling a branch means to fetch it and merge it."
-"push" "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
-"redo" ""
-"remote"       "An other repository ('remote'). One might have a set of remotes whose branches one tracks."
-"repository"   "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
-"reset"        ""
-"revert"       ""
-"revision"     "A particular state of files and directories which was stored in the object database."
-"sign off"     ""
-"staging area" ""
-"status"       ""
-"tag [noun]"   "A ref pointing to a tag or commit object"
-"tag [verb]"   ""
-"tracking branch"      "A regular git branch that is used to follow changes from another repository."
-"undo" ""
-"update"       ""
-"verify"       ""
-"working copy, working tree"   "The tree of actual checked out files."
diff --git a/po/glossary/it.po b/po/glossary/it.po
deleted file mode 100644 (file)
index bb46b48..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-# Translation of git-gui glossary to Italian
-# Copyright (C) 2007 Shawn Pearce, et al.
-# This file is distributed under the same license as the git package.
-# Christian Stimming <stimming@tuhh.de>, 2007
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: git-gui glossary\n"
-"POT-Creation-Date: 2007-10-19 21:43+0200\n"
-"PO-Revision-Date: 2007-10-10 15:24+0200\n"
-"Last-Translator: Michele Ballabio <barra_cuda@katamail.com>\n"
-"Language-Team: Italian \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#. "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
-msgid ""
-"English Term (Dear translator: This file will never be visible to the user!)"
-msgstr ""
-"Traduzione italiana.\n"
-"Altri SCM in italiano:\n"
-"  http://tortoisesvn.tigris.org/svn/tortoisesvn/trunk/Languages/Tortoise_it."
-"po (username=guest, password empty),\n"
-"  http://tortoisecvs.cvs.sourceforge.net/tortoisecvs/po/TortoiseCVS/it_IT.po?"
-"view=markup ,\n"
-"  http://rapidsvn.tigris.org/svn/rapidsvn/trunk/src/locale/it_IT/rapidsvn.po "
-"(username=guest, password empty)"
-
-#. ""
-msgid "amend"
-msgstr "correggere, correzione"
-
-#. ""
-msgid "annotate"
-msgstr "annotare, annotazione"
-
-#. "A 'branch' is an active line of development."
-msgid "branch [noun]"
-msgstr "ramo, diramazione, ramificazione"
-
-#. ""
-msgid "branch [verb]"
-msgstr "creare ramo, ramificare, diramare"
-
-#. ""
-msgid "checkout [noun]"
-msgstr "attivazione, checkout, revisione attiva, prelievo (TortoiseCVS)?"
-
-#. "The action of updating the working tree to a revision which was stored in the object database."
-msgid "checkout [verb]"
-msgstr ""
-"attivare, effettuare un checkout, attivare revisione, prelevare "
-"(TortoiseCVS), ritirare (TSVN)?"
-
-#. ""
-msgid "clone [verb]"
-msgstr "clonare"
-
-#. "A single point in the git history."
-msgid "commit [noun]"
-msgstr "revisione, commit, deposito (TortoiseCVS), invio (TSVN)?"
-
-#. "The action of storing a new snapshot of the project's state in the git history."
-msgid "commit [verb]"
-msgstr ""
-"creare una nuova revisione, archiviare, effettuare un commit, depositare "
-"(nel server), fare un deposito (TortoiseCVS), inviare (TSVN)?"
-
-#. ""
-msgid "diff [noun]"
-msgstr "differenza, confronto, comparazione, raffronto"
-
-#. ""
-msgid "diff [verb]"
-msgstr "confronta, mostra le differenze"
-
-#. "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
-msgid "fast forward merge"
-msgstr "fusione in 'fast-forward', fusione in avanti veloce"
-
-#. "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
-msgid "fetch"
-msgstr "recuperare, prelevare, prendere da, recuperare (TSVN)"
-
-#. "A collection of files. The index is a stored version of your working tree."
-msgid "index (in git-gui: staging area)"
-msgstr "indice"
-
-#. "A successful merge results in the creation of a new commit representing the result of the merge."
-msgid "merge [noun]"
-msgstr "fusione, unione"
-
-#. "To bring the contents of another branch into the current branch."
-msgid "merge [verb]"
-msgstr "effettuare la fusione, unire, fondere, eseguire la fusione"
-
-#. ""
-msgid "message"
-msgstr "messaggio, commento"
-
-#. "Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in 'remotes/<name>'."
-msgid "prune"
-msgstr "potatura"
-
-#. "Pulling a branch means to fetch it and merge it."
-msgid "pull"
-msgstr ""
-"prendi (recupera) e fondi (unisci)? (in pratica una traduzione di fetch + "
-"merge)"
-
-#. "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
-msgid "push"
-msgstr "propaga"
-
-#. ""
-msgid "redo"
-msgstr "ripeti, rifai"
-
-#. "An other repository ('remote'). One might have a set of remotes whose branches one tracks."
-msgid "remote"
-msgstr "remoto"
-
-#. "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
-msgid "repository"
-msgstr "archivio, repository, database? deposito (rapidsvn)?"
-
-#. ""
-msgid "reset"
-msgstr "ripristinare, annullare, azzerare, ripristinare"
-
-#. ""
-msgid "revert"
-msgstr ""
-"annullare, inverti (rapidsvn), ritorna allo stato precedente, annulla le "
-"modifiche della revisione"
-
-#. "A particular state of files and directories which was stored in the object database."
-msgid "revision"
-msgstr "revisione (TortoiseSVN)"
-
-#. ""
-msgid "sign off"
-msgstr "sign off, firma"
-
-#. ""
-msgid "staging area"
-msgstr ""
-"area di preparazione, zona di preparazione, modifiche in preparazione? "
-"modifiche in allestimento?"
-
-#. ""
-msgid "status"
-msgstr "stato"
-
-#. "A ref pointing to a tag or commit object"
-msgid "tag [noun]"
-msgstr "etichetta, etichettatura (TortoiseCVS)"
-
-#. ""
-msgid "tag [verb]"
-msgstr "etichettare"
-
-#. "A regular git branch that is used to follow changes from another repository."
-msgid "tracking branch"
-msgstr ""
-"duplicato locale di ramo remoto, ramo in 'tracking', ramo inseguitore? ramo "
-"di {inseguimento,allineamento,rilevamento,puntamento}?"
-
-#. ""
-msgid "undo"
-msgstr "annulla"
-
-#. ""
-msgid "update"
-msgstr "aggiornamento, aggiornare"
-
-#. ""
-msgid "verify"
-msgstr "verifica, verificare"
-
-#. "The tree of actual checked out files."
-msgid "working copy, working tree"
-msgstr "directory di lavoro, copia di lavoro"
diff --git a/po/glossary/txt-to-pot.sh b/po/glossary/txt-to-pot.sh
deleted file mode 100755 (executable)
index 49bf7c5..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/sh
-# This is a very, _very_, simple script to convert a tab-separated
-# .txt file into a .pot/.po.
-# Its not clever but it took me 2 minutes to write :)
-# Michael Twomey <michael.twomey@ireland.sun.com>
-# 23 March 2001
-# with slight GnuCash modifications by Christian Stimming <stimming@tuhh.de>
-# 19 Aug 2001, 23 Jul 2007
-
-#check args
-if [ $# -eq 0 ]
-then
-       cat <<!
-Usage: `basename $0` git-gui-glossary.txt > git-gui-glossary.pot
-!
-       exit 1;
-fi
-
-GLOSSARY_CSV="$1";
-
-if [ ! -f "$GLOSSARY_CSV" ]
-then
-       echo "Can't find $GLOSSARY_CSV.";
-       exit 1;
-fi
-
-cat <<!
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR Free Software Foundation, Inc.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"POT-Creation-Date: `date +'%Y-%m-%d %H:%M%z'`\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=CHARSET\n"
-"Content-Transfer-Encoding: ENCODING\n"
-
-!
-
-#Yes this is the most simple awk script you've ever seen :)
-awk -F'\t' '{if ($2 != "") print "#. "$2; print "msgid "$1; print "msgstr \"\"\n"}' \
-$GLOSSARY_CSV
diff --git a/po/glossary/zh_cn.po b/po/glossary/zh_cn.po
deleted file mode 100644 (file)
index 158835b..0000000
+++ /dev/null
@@ -1,170 +0,0 @@
-# Translation of git-gui glossary to Simplified Chinese
-# Copyright (C) 2007 Shawn Pearce, et al.
-# This file is distributed under the same license as the git package.
-# Xudong Guan <xudong.guan@gmail.com> and the zh-kernel.org mailing list, 2007
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: git-gui glossary\n"
-"PO-Revision-Date: 2007-07-23 22:07+0200\n"
-"Last-Translator: Xudong Guan <xudong.guan@gmail.com>\n"
-"Language-Team: Simplified Chinese \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#. "English Definition (Dear translator: This file will never be visible to the user! It should only serve as a tool for you, the translator. Nothing more.)"
-msgid ""
-"English Term (Dear translator: This file will never be visible to the user!)"
-msgstr "注:这个文件是为了帮助翻译人员统一名词术语。最终用户不会关心这个文件。"
-
-#. ""
-#. amend指用户修改最近一次commit的操作,修订?修改?修正?
-#. [WANG Cong]: 根据我的了解,这个词似乎翻译成“修订”多一些。“修正”也可以,“修改”再次之。
-#. [ZHANG Le]: 修订,感觉一般指对一些大型出版物的大规模升级,比如修订新华字典
-#              修正,其实每次amend的结果也不一定就是最后结果,说不定还需要修改。所以不
-#              如就叫修改
-msgid "amend"
-msgstr "修订"
-
-#. ""
-#. git annotate 文件名:用来标注文件的每一行在什么时候被谁最后修改。
-#. [WANG Cong]: "标记"一般是mark。;)
-#. [ZHANG Le]: 标注,或者干脆用原意:注解,或注释
-msgid "annotate"
-msgstr "标注"
-
-#. "A 'branch' is an active line of development."
-msgid "branch [noun]"
-msgstr "分支"
-
-#. ""
-msgid "branch [verb]"
-msgstr "建立分支"
-
-#. ""
-#. [WANG Cong]: 网上有人翻译成“检出”,我感觉更好一些,毕竟把check的意思翻译出来了。
-#. [ZHNAG Le]: 提取吧,提取分支/版本
-#. [rae l]: 签出。subversion软件中的大多词汇已有翻译,既然git与subversion同是SCM管理,可以参考同类软件的翻译也不错。
-msgid "checkout [noun]"
-msgstr "签出"
-
-#. "The action of updating the working tree to a revision which was stored in the object database."
-msgid "checkout [verb]"
-msgstr "签出"
-
-#. "A single point in the git history."
-msgid "commit [noun]"
-msgstr "提交"
-
-#. "The action of storing a new snapshot of the project's state in the git history."
-msgid "commit [verb]"
-msgstr "提交"
-
-#. ""
-#. 差异?差别?
-#. [ZHANG Le]: 个人感觉差别更加中性一些
-msgid "diff [noun]"
-msgstr "差别"
-
-#. ""
-msgid "diff [verb]"
-msgstr "比较"
-
-#. "A fast-forward is a special type of merge where you have a revision and you are merging another branch's changes that happen to be a descendant of what you have."
-msgid "fast forward merge"
-msgstr "快进式合并"
-
-#. "Fetching a branch means to get the branch's head from a remote repository, to find out which objects are missing from the local object database, and to get them, too."
-#. 获取?取得?下载?更新?注意和update的区分
-msgid "fetch"
-msgstr "获取"
-
-#. "A collection of files. The index is a stored version of your working tree."
-#. index是working tree和repository之间的缓存
-msgid "index (in git-gui: staging area)"
-msgstr "工作缓存?"
-
-#. "A successful merge results in the creation of a new commit representing the result of the merge."
-msgid "merge [noun]"
-msgstr "合并"
-
-#. "To bring the contents of another branch into the current branch."
-msgid "merge [verb]"
-msgstr "合并"
-
-#. ""
-#. message是指commit中的文字信息
-msgid "message"
-msgstr "描述"
-
-#. "Pulling a branch means to fetch it and merge it."
-msgid "pull"
-msgstr "获取+合并"
-
-#. "Pushing a branch means to get the branch's head ref from a remote repository, and ... (well, can someone please explain it for mere mortals?)"
-msgid "push"
-msgstr "推入"
-
-#. ""
-msgid "redo"
-msgstr "重做"
-
-#. "A collection of refs (?) together with an object database containing all objects which are reachable from the refs... (oops, you've lost me here. Again, please an explanation for mere mortals?)"
-msgid "repository"
-msgstr "仓库"
-
-#. ""
-msgid "reset"
-msgstr "重置"
-
-#. ""
-msgid "revert"
-msgstr "恢复"
-
-#. "A particular state of files and directories which was stored in the object database."
-msgid "revision"
-msgstr "版本"
-
-#. ""
-msgid "sign off"
-msgstr "签名"
-
-#. ""
-#. 似乎是git-gui里面显示的本次提交的文件清单区域
-msgid "staging area"
-msgstr "提交暂存区"
-
-#. ""
-msgid "status"
-msgstr "状态"
-
-#. "A ref pointing to a tag or commit object"
-msgid "tag [noun]"
-msgstr "标签"
-
-#. ""
-msgid "tag [verb]"
-msgstr "添加标签"
-
-#. "A regular git branch that is used to follow changes from another repository."
-msgid "tracking branch"
-msgstr "跟踪分支"
-
-#. ""
-msgid "undo"
-msgstr "撤销"
-
-#. ""
-msgid "update"
-msgstr "更新。注意和fetch的区分"
-
-#. ""
-msgid "verify"
-msgstr "验证"
-
-#. "The tree of actual checked out files."
-#. "工作副本?工作区域?工作目录"
-#. [LI Yang]: 当前副本, 当前源码树?
-msgid "working copy, working tree"
-msgstr "工作副本,工作源码树"
diff --git a/po/hu.po b/po/hu.po
deleted file mode 100644 (file)
index 627c05e..0000000
--- a/po/hu.po
+++ /dev/null
@@ -1,1937 +0,0 @@
-# Hungarian translations for git-gui-i package.
-# Copyright (C) 2007 THE git-gui-i'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the git-gui-i package.
-# Miklos Vajna <vmiklos@frugalware.org>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: git-gui-i 18n\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2007-11-24 10:36+0100\n"
-"PO-Revision-Date: 2007-12-04 01:15+0100\n"
-"Last-Translator: Miklos Vajna <vmiklos@frugalware.org>\n"
-"Language-Team: Hungarian\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
-#: git-gui.sh:733
-msgid "git-gui: fatal error"
-msgstr "git-gui: végzetes hiba"
-
-#: git-gui.sh:565
-#, tcl-format
-msgid "Invalid font specified in %s:"
-msgstr "Érvénytelen font lett megadva itt: %s:"
-
-#: git-gui.sh:590
-msgid "Main Font"
-msgstr "Fő betűtípus"
-
-#: git-gui.sh:591
-msgid "Diff/Console Font"
-msgstr "Diff/konzol betűtípus"
-
-#: git-gui.sh:605
-msgid "Cannot find git in PATH."
-msgstr "A git nem található a PATH-ban."
-
-#: git-gui.sh:632
-msgid "Cannot parse Git version string:"
-msgstr "Nem értelmezhető a Git verzió sztring:"
-
-#: git-gui.sh:650
-#, tcl-format
-msgid ""
-"Git version cannot be determined.\n"
-"\n"
-"%s claims it is version '%s'.\n"
-"\n"
-"%s requires at least Git 1.5.0 or later.\n"
-"\n"
-"Assume '%s' is version 1.5.0?\n"
-msgstr ""
-"Nem állípítható meg a Git verziója.\n"
-"\n"
-"A(z) %s szerint a verzió '%s'.\n"
-"\n"
-"A(z) %s a Git 1.5.0 vagy későbbi verzióját igényli.\n"
-"\n"
-"Feltételezhetjük, hogy a(z) '%s' verziója legalább 1.5.0?\n"
-
-#: git-gui.sh:888
-msgid "Git directory not found:"
-msgstr "A Git könyvtár nem található:"
-
-#: git-gui.sh:895
-msgid "Cannot move to top of working directory:"
-msgstr "Nem lehet a munkakönyvtár tetejére lépni:"
-
-#: git-gui.sh:902
-msgid "Cannot use funny .git directory:"
-msgstr "Nem használható vicces .git könyvtár:"
-
-#: git-gui.sh:907
-msgid "No working directory"
-msgstr "Nincs munkakönyvtár"
-
-#: git-gui.sh:1054
-msgid "Refreshing file status..."
-msgstr "A fájlok státuszának frissítése..."
-
-#: git-gui.sh:1119
-msgid "Scanning for modified files ..."
-msgstr "Módosított fájlok keresése ..."
-
-#: git-gui.sh:1294 lib/browser.tcl:245
-msgid "Ready."
-msgstr "Kész."
-
-#: git-gui.sh:1560
-msgid "Unmodified"
-msgstr "Nem módosított"
-
-#: git-gui.sh:1562
-msgid "Modified, not staged"
-msgstr "Módosított, de nem kiválasztott"
-
-#: git-gui.sh:1563 git-gui.sh:1568
-msgid "Staged for commit"
-msgstr "Kiválasztva commitolásra"
-
-#: git-gui.sh:1564 git-gui.sh:1569
-msgid "Portions staged for commit"
-msgstr "Részek kiválasztva commitolásra"
-
-#: git-gui.sh:1565 git-gui.sh:1570
-msgid "Staged for commit, missing"
-msgstr "Kiválasztva commitolásra, hiányzó"
-
-#: git-gui.sh:1567
-msgid "Untracked, not staged"
-msgstr "Nem követett, nem kiválasztott"
-
-#: git-gui.sh:1572
-msgid "Missing"
-msgstr "Hiányzó"
-
-#: git-gui.sh:1573
-msgid "Staged for removal"
-msgstr "Kiválasztva eltávolításra"
-
-#: git-gui.sh:1574
-msgid "Staged for removal, still present"
-msgstr "Kiválasztva eltávolításra, jelenleg is elérhető"
-
-#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
-msgid "Requires merge resolution"
-msgstr "Merge feloldás szükséges"
-
-#: git-gui.sh:1614
-msgid "Starting gitk... please wait..."
-msgstr "A gitk indítása... várjunk..."
-
-#: git-gui.sh:1623
-#, tcl-format
-msgid ""
-"Unable to start gitk:\n"
-"\n"
-"%s does not exist"
-msgstr ""
-"A gitk indítása sikertelen:\n"
-"\n"
-"A(z) %s nem létezik"
-
-#: git-gui.sh:1823 lib/choose_repository.tcl:35
-msgid "Repository"
-msgstr "Repó"
-
-#: git-gui.sh:1824
-msgid "Edit"
-msgstr "Szerkesztés"
-
-#: git-gui.sh:1826 lib/choose_rev.tcl:560
-msgid "Branch"
-msgstr "Branch"
-
-#: git-gui.sh:1829 lib/choose_rev.tcl:547
-msgid "Commit@@noun"
-msgstr "Commit@@főnév"
-
-#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
-msgid "Merge"
-msgstr "Merge"
-
-#: git-gui.sh:1833 lib/choose_rev.tcl:556
-msgid "Remote"
-msgstr "Távoli"
-
-#: git-gui.sh:1842
-msgid "Browse Current Branch's Files"
-msgstr "A jelenlegi branch fájljainak böngészése"
-
-#: git-gui.sh:1846
-msgid "Browse Branch Files..."
-msgstr "A branch fájljainak böngészése..."
-
-#: git-gui.sh:1851
-msgid "Visualize Current Branch's History"
-msgstr "A jelenlegi branch történetének vizualizálása"
-
-#: git-gui.sh:1855
-msgid "Visualize All Branch History"
-msgstr "Az összes branch történetének vizualizálása"
-
-#: git-gui.sh:1862
-#, tcl-format
-msgid "Browse %s's Files"
-msgstr "A(z) %s branch fájljainak böngészése"
-
-#: git-gui.sh:1864
-#, tcl-format
-msgid "Visualize %s's History"
-msgstr "A(z) %s branch történetének vizualizálása"
-
-#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
-msgid "Database Statistics"
-msgstr "Adatbázis statisztikák"
-
-#: git-gui.sh:1872 lib/database.tcl:34
-msgid "Compress Database"
-msgstr "Adatbázis tömörítése"
-
-#: git-gui.sh:1875
-msgid "Verify Database"
-msgstr "Adatbázis ellenőrzése"
-
-#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
-#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
-msgid "Create Desktop Icon"
-msgstr "Asztal ikon létrehozása"
-
-#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
-msgid "Quit"
-msgstr "Kilépés"
-
-#: git-gui.sh:1902
-msgid "Undo"
-msgstr "Visszavonás"
-
-#: git-gui.sh:1905
-msgid "Redo"
-msgstr "Mégis"
-
-#: git-gui.sh:1909 git-gui.sh:2403
-msgid "Cut"
-msgstr "Kivágás"
-
-#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
-#: lib/console.tcl:67
-msgid "Copy"
-msgstr "Másolás"
-
-#: git-gui.sh:1915 git-gui.sh:2409
-msgid "Paste"
-msgstr "Beillesztés"
-
-#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
-#: lib/remote_branch_delete.tcl:38
-msgid "Delete"
-msgstr "Törlés"
-
-#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
-msgid "Select All"
-msgstr "Mindent kiválaszt"
-
-#: git-gui.sh:1931
-msgid "Create..."
-msgstr "Létrehozás..."
-
-#: git-gui.sh:1937
-msgid "Checkout..."
-msgstr "Checkout..."
-
-#: git-gui.sh:1943
-msgid "Rename..."
-msgstr "Átnevezés..."
-
-#: git-gui.sh:1948 git-gui.sh:2048
-msgid "Delete..."
-msgstr "Törlés..."
-
-#: git-gui.sh:1953
-msgid "Reset..."
-msgstr "Visszaállítás..."
-
-#: git-gui.sh:1965 git-gui.sh:2350
-msgid "New Commit"
-msgstr "Új commit"
-
-#: git-gui.sh:1973 git-gui.sh:2357
-msgid "Amend Last Commit"
-msgstr "Utolsó commit javítása"
-
-#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
-msgid "Rescan"
-msgstr "Keresés újra"
-
-#: git-gui.sh:1988
-msgid "Stage To Commit"
-msgstr "Kiválasztás commitolásra"
-
-#: git-gui.sh:1994
-msgid "Stage Changed Files To Commit"
-msgstr "Módosított fájlok kiválasztása commitolásra"
-
-#: git-gui.sh:2000
-msgid "Unstage From Commit"
-msgstr "Commitba való kiválasztás visszavonása"
-
-#: git-gui.sh:2005 lib/index.tcl:393
-msgid "Revert Changes"
-msgstr "Változtatások visszaállítása"
-
-#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
-msgid "Sign Off"
-msgstr "Aláír"
-
-#: git-gui.sh:2016 git-gui.sh:2333
-msgid "Commit@@verb"
-msgstr "Commit@@ige"
-
-#: git-gui.sh:2027
-msgid "Local Merge..."
-msgstr "Helyi merge..."
-
-#: git-gui.sh:2032
-msgid "Abort Merge..."
-msgstr "Merge megszakítása..."
-
-#: git-gui.sh:2044
-msgid "Push..."
-msgstr "Push..."
-
-#: git-gui.sh:2055 lib/choose_repository.tcl:40
-msgid "Apple"
-msgstr "Apple"
-
-#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
-#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
-#, tcl-format
-msgid "About %s"
-msgstr "Névjegy: %s"
-
-#: git-gui.sh:2062
-msgid "Preferences..."
-msgstr "Beállítások..."
-
-#: git-gui.sh:2070 git-gui.sh:2595
-msgid "Options..."
-msgstr "Opciók..."
-
-#: git-gui.sh:2076 lib/choose_repository.tcl:46
-msgid "Help"
-msgstr "Segítség"
-
-#: git-gui.sh:2117
-msgid "Online Documentation"
-msgstr "Online dokumentáció"
-
-#: git-gui.sh:2201
-#, tcl-format
-msgid "fatal: cannot stat path %s: No such file or directory"
-msgstr "végzetes hiba: nem érhető el a(z) %s útvonal: Nincs ilyen fájl vagy könyvtár"
-
-#: git-gui.sh:2234
-msgid "Current Branch:"
-msgstr "Jelenlegi branch:"
-
-#: git-gui.sh:2255
-msgid "Staged Changes (Will Commit)"
-msgstr "Kiválasztott változtatások (commitolva lesz)"
-
-#: git-gui.sh:2274
-msgid "Unstaged Changes"
-msgstr "Kiválasztatlan változtatások"
-
-#: git-gui.sh:2323
-msgid "Stage Changed"
-msgstr "Változtatások kiválasztása"
-
-#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
-msgid "Push"
-msgstr "Push"
-
-#: git-gui.sh:2369
-msgid "Initial Commit Message:"
-msgstr "Kezdeti commit üzenet:"
-
-#: git-gui.sh:2370
-msgid "Amended Commit Message:"
-msgstr "Javító commit üzenet:"
-
-#: git-gui.sh:2371
-msgid "Amended Initial Commit Message:"
-msgstr "Kezdeti javító commit üzenet:"
-
-#: git-gui.sh:2372
-msgid "Amended Merge Commit Message:"
-msgstr "Javító merge commit üzenet:"
-
-#: git-gui.sh:2373
-msgid "Merge Commit Message:"
-msgstr "Merge commit üzenet:"
-
-#: git-gui.sh:2374
-msgid "Commit Message:"
-msgstr "Commit üzenet:"
-
-#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
-msgid "Copy All"
-msgstr "Összes másolása"
-
-#: git-gui.sh:2443 lib/blame.tcl:104
-msgid "File:"
-msgstr "Fájl:"
-
-#: git-gui.sh:2545
-msgid "Refresh"
-msgstr "Frissítés"
-
-#: git-gui.sh:2566
-msgid "Apply/Reverse Hunk"
-msgstr "Hunk alkalmazása/visszaállítása"
-
-#: git-gui.sh:2572
-msgid "Decrease Font Size"
-msgstr "Font méret csökkentése"
-
-#: git-gui.sh:2576
-msgid "Increase Font Size"
-msgstr "Fönt méret növelése"
-
-#: git-gui.sh:2581
-msgid "Show Less Context"
-msgstr "Kevesebb környezet mutatása"
-
-#: git-gui.sh:2588
-msgid "Show More Context"
-msgstr "Több környezet mutatása"
-
-#: git-gui.sh:2602
-msgid "Unstage Hunk From Commit"
-msgstr "Hunk törlése commitból"
-
-#: git-gui.sh:2604
-msgid "Stage Hunk For Commit"
-msgstr "Hunk kiválasztása commitba"
-
-#: git-gui.sh:2623
-msgid "Initializing..."
-msgstr "Inicializálás..."
-
-#: git-gui.sh:2718
-#, tcl-format
-msgid ""
-"Possible environment issues exist.\n"
-"\n"
-"The following environment variables are probably\n"
-"going to be ignored by any Git subprocess run\n"
-"by %s:\n"
-"\n"
-msgstr ""
-"Lehetséges, hogy környezeti problémák vannak.\n"
-"\n"
-"A következő környezeti változók valószínűleg\n"
-"figyelmen kívül lesznek hagyva a(z) %s által\n"
-"indított folyamatok által:\n"
-"\n"
-
-#: git-gui.sh:2748
-msgid ""
-"\n"
-"This is due to a known issue with the\n"
-"Tcl binary distributed by Cygwin."
-msgstr ""
-"\n"
-"Ez a Cygwin által terjesztett Tcl binárisban\n"
-"lévő ismert hiba miatt van."
-
-#: git-gui.sh:2753
-#, tcl-format
-msgid ""
-"\n"
-"\n"
-"A good replacement for %s\n"
-"is placing values for the user.name and\n"
-"user.email settings into your personal\n"
-"~/.gitconfig file.\n"
-msgstr ""
-"\n"
-"\n"
-"Egy jó helyettesítés a(z) %s számára\n"
-"a user.name és user.email beállítások\n"
-"elhelyezése a személyes\n"
-"~/.gitconfig fájlba.\n"
-
-#: lib/about.tcl:25
-msgid "git-gui - a graphical user interface for Git."
-msgstr "git-gui - egy grafikus felület a Githez."
-
-#: lib/blame.tcl:77
-msgid "File Viewer"
-msgstr "Fájl néző"
-
-#: lib/blame.tcl:81
-msgid "Commit:"
-msgstr "Commit:"
-
-#: lib/blame.tcl:249
-msgid "Copy Commit"
-msgstr "Commit másolása"
-
-#: lib/blame.tcl:369
-#, tcl-format
-msgid "Reading %s..."
-msgstr "A(z) %s olvasása..."
-
-#: lib/blame.tcl:473
-msgid "Loading copy/move tracking annotations..."
-msgstr "A másolást/átnevezést követő annotációk betöltése..."
-
-#: lib/blame.tcl:493
-msgid "lines annotated"
-msgstr "sor annotálva"
-
-#: lib/blame.tcl:674
-msgid "Loading original location annotations..."
-msgstr "Az eredeti hely annotációk betöltése..."
-
-#: lib/blame.tcl:677
-msgid "Annotation complete."
-msgstr "Az annotáció kész."
-
-#: lib/blame.tcl:731
-msgid "Loading annotation..."
-msgstr "Az annotáció betöltése..."
-
-#: lib/blame.tcl:787
-msgid "Author:"
-msgstr "Szerző:"
-
-#: lib/blame.tcl:791
-msgid "Committer:"
-msgstr "Commiter:"
-
-#: lib/blame.tcl:796
-msgid "Original File:"
-msgstr "Eredeti fájl:"
-
-#: lib/blame.tcl:910
-msgid "Originally By:"
-msgstr "Eredeti szerző:"
-
-#: lib/blame.tcl:916
-msgid "In File:"
-msgstr "Ebben a fájlban:"
-
-#: lib/blame.tcl:921
-msgid "Copied Or Moved Here By:"
-msgstr "Ide másolta vagy helyezte:"
-
-#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
-msgid "Checkout Branch"
-msgstr "Branch checkoutolása"
-
-#: lib/branch_checkout.tcl:23
-msgid "Checkout"
-msgstr "Checkout"
-
-#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
-#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
-#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
-#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
-msgid "Cancel"
-msgstr "Mégsem"
-
-#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
-msgid "Revision"
-msgstr "Revízió"
-
-#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
-msgid "Options"
-msgstr "Opciók"
-
-#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
-msgid "Fetch Tracking Branch"
-msgstr "Követő branch letöltése"
-
-#: lib/branch_checkout.tcl:44
-msgid "Detach From Local Branch"
-msgstr "Helyi branch leválasztása"
-
-#: lib/branch_create.tcl:22
-msgid "Create Branch"
-msgstr "Branch létrehozása"
-
-#: lib/branch_create.tcl:27
-msgid "Create New Branch"
-msgstr "Új branch létrehozása"
-
-#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
-msgid "Create"
-msgstr "Létrehozás"
-
-#: lib/branch_create.tcl:40
-msgid "Branch Name"
-msgstr "Branch neve"
-
-#: lib/branch_create.tcl:43
-msgid "Name:"
-msgstr "Név:"
-
-#: lib/branch_create.tcl:58
-msgid "Match Tracking Branch Name"
-msgstr "Egyeztetendő követési branch név"
-
-#: lib/branch_create.tcl:66
-msgid "Starting Revision"
-msgstr "A következő revíziótól"
-
-#: lib/branch_create.tcl:72
-msgid "Update Existing Branch:"
-msgstr "Létező branch frissítése"
-
-#: lib/branch_create.tcl:75
-msgid "No"
-msgstr "Nem"
-
-#: lib/branch_create.tcl:80
-msgid "Fast Forward Only"
-msgstr "Csak fast forward"
-
-#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
-msgid "Reset"
-msgstr "Visszaállítás"
-
-#: lib/branch_create.tcl:97
-msgid "Checkout After Creation"
-msgstr "Checkout létrehozás után"
-
-#: lib/branch_create.tcl:131
-msgid "Please select a tracking branch."
-msgstr "Válasszunk ki egy követő branchet."
-
-#: lib/branch_create.tcl:140
-#, tcl-format
-msgid "Tracking branch %s is not a branch in the remote repository."
-msgstr "A(z) %s követő branch nem branch a távoli repóban."
-
-#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
-msgid "Please supply a branch name."
-msgstr "Adjunk megy egy branch nevet."
-
-#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
-#, tcl-format
-msgid "'%s' is not an acceptable branch name."
-msgstr "A(z) '%s' nem egy elfogadható branch név."
-
-#: lib/branch_delete.tcl:15
-msgid "Delete Branch"
-msgstr "Branch törlése"
-
-#: lib/branch_delete.tcl:20
-msgid "Delete Local Branch"
-msgstr "Helyi branch törlése"
-
-#: lib/branch_delete.tcl:37
-msgid "Local Branches"
-msgstr "Helyi branchek"
-
-#: lib/branch_delete.tcl:52
-msgid "Delete Only If Merged Into"
-msgstr "Csak már merge-ölt törlése"
-
-#: lib/branch_delete.tcl:54
-msgid "Always (Do not perform merge test.)"
-msgstr "Mindig (Ne legyen merge teszt.)"
-
-#: lib/branch_delete.tcl:103
-#, tcl-format
-msgid "The following branches are not completely merged into %s:"
-msgstr "A következő branchek nem teljesen lettek merge-ölve ebbe: %s:"
-
-#: lib/branch_delete.tcl:115
-msgid ""
-"Recovering deleted branches is difficult. \n"
-"\n"
-" Delete the selected branches?"
-msgstr ""
-"A törölt branchek visszaállítása bonyolult. \n"
-"\n"
-" Biztosan törli a kiválasztott brancheket?"
-
-#: lib/branch_delete.tcl:141
-#, tcl-format
-msgid ""
-"Failed to delete branches:\n"
-"%s"
-msgstr ""
-"Nem sikerült törölni a következő brancheket:\n"
-"%s"
-
-#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
-msgid "Rename Branch"
-msgstr "Branch átnevezése"
-
-#: lib/branch_rename.tcl:26
-msgid "Rename"
-msgstr "Átnevezés"
-
-#: lib/branch_rename.tcl:36
-msgid "Branch:"
-msgstr "Branch:"
-
-#: lib/branch_rename.tcl:39
-msgid "New Name:"
-msgstr "Új név:"
-
-#: lib/branch_rename.tcl:75
-msgid "Please select a branch to rename."
-msgstr "Válasszunk ki egy átnevezendő branchet."
-
-#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
-#, tcl-format
-msgid "Branch '%s' already exists."
-msgstr "A(z) '%s' branch már létezik."
-
-#: lib/branch_rename.tcl:117
-#, tcl-format
-msgid "Failed to rename '%s'."
-msgstr "Nem sikerült átnevezni: '%s'."
-
-#: lib/browser.tcl:17
-msgid "Starting..."
-msgstr "Indítás..."
-
-#: lib/browser.tcl:26
-msgid "File Browser"
-msgstr "Fájl böngésző"
-
-#: lib/browser.tcl:125 lib/browser.tcl:142
-#, tcl-format
-msgid "Loading %s..."
-msgstr "A(z) %s betöltése..."
-
-#: lib/browser.tcl:186
-msgid "[Up To Parent]"
-msgstr "[Fel a szülőhöz]"
-
-#: lib/browser.tcl:266 lib/browser.tcl:272
-msgid "Browse Branch Files"
-msgstr "A branch fájljainak böngészése"
-
-#: lib/browser.tcl:277 lib/choose_repository.tcl:391
-#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
-#: lib/choose_repository.tcl:989
-msgid "Browse"
-msgstr "Böngészés"
-
-#: lib/checkout_op.tcl:79
-#, tcl-format
-msgid "Fetching %s from %s"
-msgstr "A(z) %s letöltése innen: %s"
-
-#: lib/checkout_op.tcl:127
-#, tcl-format
-msgid "fatal: Cannot resolve %s"
-msgstr "végzetes: Nem lehet feloldani a következőt: %s"
-
-#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
-msgid "Close"
-msgstr "Bezárás"
-
-#: lib/checkout_op.tcl:169
-#, tcl-format
-msgid "Branch '%s' does not exist."
-msgstr "A(z) '%s' branch nem létezik."
-
-#: lib/checkout_op.tcl:206
-#, tcl-format
-msgid ""
-"Branch '%s' already exists.\n"
-"\n"
-"It cannot fast-forward to %s.\n"
-"A merge is required."
-msgstr ""
-"A(z) '%s' branch már létezik.\n"
-"\n"
-"Nem lehet fast-forwardolni a következőhöz: %s.\n"
-"Egy merge szükséges."
-
-#: lib/checkout_op.tcl:220
-#, tcl-format
-msgid "Merge strategy '%s' not supported."
-msgstr "A(z) '%s' merge strategy nem támogatott."
-
-#: lib/checkout_op.tcl:239
-#, tcl-format
-msgid "Failed to update '%s'."
-msgstr "Nem sikerült frissíteni a következőt: '%s'."
-
-#: lib/checkout_op.tcl:251
-msgid "Staging area (index) is already locked."
-msgstr "A kiválasztási terület (index) már zárolva van."
-
-#: lib/checkout_op.tcl:266
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before the current branch can be changed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"Az utolsó keresési állapot nem egyezik meg a repó állpotával.\n"
-"\n"
-"Egy másik Git program módosította ezt a repót az utolsó keresés óta. Egy "
-"újrakeresés mindenképpen szükséges mielőtt a jelenlegi branchet módosítani "
-"lehetne.\n"
-"\n"
-"Az újrakeresés most automatikusan el fog indulni.\n"
-
-#: lib/checkout_op.tcl:322
-#, tcl-format
-msgid "Updating working directory to '%s'..."
-msgstr "A munkkönyvtár frissiítése a következőre: '%s'..."
-
-#: lib/checkout_op.tcl:353
-#, tcl-format
-msgid "Aborted checkout of '%s' (file level merging is required)."
-msgstr "A(z) '%s' checkoutja megszakítva (fájlszintű merge-ölés szükséges)."
-
-#: lib/checkout_op.tcl:354
-msgid "File level merge required."
-msgstr "Fájlszintű merge-ölés szükséges."
-
-#: lib/checkout_op.tcl:358
-#, tcl-format
-msgid "Staying on branch '%s'."
-msgstr "Jelenleg a(z) '%s' branchen."
-
-#: lib/checkout_op.tcl:429
-msgid ""
-"You are no longer on a local branch.\n"
-"\n"
-"If you wanted to be on a branch, create one now starting from 'This Detached "
-"Checkout'."
-msgstr ""
-"Már nem egy helyi branchen vagyunk.\n"
-"\n"
-"Ha egy branchen szeretnénk lenni, hozzunk létre egyet az 'Ez a leválasztott "
-"checkout'-ból."
-
-#: lib/checkout_op.tcl:446
-#, tcl-format
-msgid "Checked out '%s'."
-msgstr "'%s' kifejtve."
-
-#: lib/checkout_op.tcl:478
-#, tcl-format
-msgid "Resetting '%s' to '%s' will lose the following commits:"
-msgstr ""
-"A(z) '%s' -> '%s' visszaállítás a következő commitok elvesztését jelenti:"
-
-#: lib/checkout_op.tcl:500
-msgid "Recovering lost commits may not be easy."
-msgstr "Az elveszett commitok helyreállítása nem biztos, hogy egyszerű."
-
-#: lib/checkout_op.tcl:505
-#, tcl-format
-msgid "Reset '%s'?"
-msgstr "Visszaállítjuk a következőt: '%s'?"
-
-#: lib/checkout_op.tcl:510 lib/merge.tcl:164
-msgid "Visualize"
-msgstr "Vizualizálás"
-
-#: lib/checkout_op.tcl:578
-#, tcl-format
-msgid ""
-"Failed to set current branch.\n"
-"\n"
-"This working directory is only partially switched.  We successfully updated "
-"your files, but failed to update an internal Git file.\n"
-"\n"
-"This should not have occurred.  %s will now close and give up."
-msgstr ""
-"Nem sikerült beállítani a jelenlegi branchet.\n"
-"\n"
-"A munkakönyvtár csak részben váltott át.  A fájlok sikeresen frissítve "
-"lettek, de nem sikerült frissíteni egy belső Git fájlt.\n"
-"\n"
-"Ennek nem szabad megtörténnie.  A(z) %s most kilép és feladja."
-
-#: lib/choose_font.tcl:39
-msgid "Select"
-msgstr "Kiválaszt"
-
-#: lib/choose_font.tcl:53
-msgid "Font Family"
-msgstr "Font család"
-
-#: lib/choose_font.tcl:73
-msgid "Font Size"
-msgstr "Font méret"
-
-#: lib/choose_font.tcl:90
-msgid "Font Example"
-msgstr "Font példa"
-
-#: lib/choose_font.tcl:101
-msgid ""
-"This is example text.\n"
-"If you like this text, it can be your font."
-msgstr ""
-"Ez egy példa szöveg.\n"
-"Ha ez megfelel, ez lehet a betűtípus."
-
-#: lib/choose_repository.tcl:27
-msgid "Git Gui"
-msgstr "Git Gui"
-
-#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
-msgid "Create New Repository"
-msgstr "Új repó létrehozása"
-
-#: lib/choose_repository.tcl:86
-msgid "New..."
-msgstr "Új..."
-
-#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
-msgid "Clone Existing Repository"
-msgstr "Létező repó másolása"
-
-#: lib/choose_repository.tcl:99
-msgid "Clone..."
-msgstr "Másolás..."
-
-#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
-msgid "Open Existing Repository"
-msgstr "Létező könyvtár megnyitása"
-
-#: lib/choose_repository.tcl:112
-msgid "Open..."
-msgstr "Meggyitás..."
-
-#: lib/choose_repository.tcl:125
-msgid "Recent Repositories"
-msgstr "Legutóbbi repók"
-
-#: lib/choose_repository.tcl:131
-msgid "Open Recent Repository:"
-msgstr "Legutóbbi repók megnyitása:"
-
-#: lib/choose_repository.tcl:294
-#, tcl-format
-msgid "Location %s already exists."
-msgstr "A(z) '%s' hely már létezik."
-
-#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
-#: lib/choose_repository.tcl:314
-#, tcl-format
-msgid "Failed to create repository %s:"
-msgstr "Nem sikerült letrehozni a(z) %s repót:"
-
-#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
-msgid "Directory:"
-msgstr "Könyvtár:"
-
-#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
-#: lib/choose_repository.tcl:1013
-msgid "Git Repository"
-msgstr "Git repó"
-
-#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
-#, tcl-format
-msgid "Directory %s already exists."
-msgstr "A(z) '%s' könyvtár már létezik."
-
-#: lib/choose_repository.tcl:442
-#, tcl-format
-msgid "File %s already exists."
-msgstr "A(z) '%s' fájl már létezik."
-
-#: lib/choose_repository.tcl:463
-msgid "Clone"
-msgstr "Bezárás"
-
-#: lib/choose_repository.tcl:476
-msgid "URL:"
-msgstr "URL:"
-
-#: lib/choose_repository.tcl:496
-msgid "Clone Type:"
-msgstr "Másolás típusa:"
-
-#: lib/choose_repository.tcl:502
-msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
-msgstr "Általános (Gyors, félig-redundáns, hardlinkek)"
-
-#: lib/choose_repository.tcl:508
-msgid "Full Copy (Slower, Redundant Backup)"
-msgstr "Teljes másolás (Lassabb, redundáns biztonsági mentés)"
-
-#: lib/choose_repository.tcl:514
-msgid "Shared (Fastest, Not Recommended, No Backup)"
-msgstr "Megosztott (Leggyorsabb, nem ajánlott, nincs mentés)"
-
-#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
-#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
-#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
-#, tcl-format
-msgid "Not a Git repository: %s"
-msgstr "Nem Git repó: %s"
-
-#: lib/choose_repository.tcl:586
-msgid "Standard only available for local repository."
-msgstr "A standard csak helyi repókra érhető el."
-
-#: lib/choose_repository.tcl:590
-msgid "Shared only available for local repository."
-msgstr "A megosztott csak helyi repókra érhető el."
-
-#: lib/choose_repository.tcl:617
-msgid "Failed to configure origin"
-msgstr "Nem sikerült beállítani az origint"
-
-#: lib/choose_repository.tcl:629
-msgid "Counting objects"
-msgstr "Objektumok számolása"
-
-#: lib/choose_repository.tcl:630
-msgid "buckets"
-msgstr "vödrök"
-
-#: lib/choose_repository.tcl:654
-#, tcl-format
-msgid "Unable to copy objects/info/alternates: %s"
-msgstr "Nem sikerült másolni az objects/info/alternates-t: %s"
-
-#: lib/choose_repository.tcl:690
-#, tcl-format
-msgid "Nothing to clone from %s."
-msgstr "Semmi másolni való nincs innen: %s"
-
-#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
-#: lib/choose_repository.tcl:918
-msgid "The 'master' branch has not been initialized."
-msgstr "A 'master' branch nincs inicializálva."
-
-#: lib/choose_repository.tcl:705
-msgid "Hardlinks are unavailable.  Falling back to copying."
-msgstr "Nem érhetőek el hardlinkek.  Másolás használata."
-
-#: lib/choose_repository.tcl:717
-#, tcl-format
-msgid "Cloning from %s"
-msgstr "Másolás innen: %s"
-
-#: lib/choose_repository.tcl:748
-msgid "Copying objects"
-msgstr "Objektumok másolása"
-
-#: lib/choose_repository.tcl:749
-msgid "KiB"
-msgstr "KiB"
-
-#: lib/choose_repository.tcl:773
-#, tcl-format
-msgid "Unable to copy object: %s"
-msgstr "Nem sikerült másolni az objektumot: %s"
-
-#: lib/choose_repository.tcl:783
-msgid "Linking objects"
-msgstr "Objektumok összefűzése"
-
-#: lib/choose_repository.tcl:784
-msgid "objects"
-msgstr "objektum"
-
-#: lib/choose_repository.tcl:792
-#, tcl-format
-msgid "Unable to hardlink object: %s"
-msgstr "Nem sikerült hardlinkelni az objektumot: %s"
-
-#: lib/choose_repository.tcl:847
-msgid "Cannot fetch branches and objects.  See console output for details."
-msgstr "Nem sikerült letölteni a branch-eket és az objektumokat.  Bővebben a konzolos kimenetben."
-
-#: lib/choose_repository.tcl:858
-msgid "Cannot fetch tags.  See console output for details."
-msgstr "Nem sikerült letölteni a tageket.  Bővebben a konzolos kimenetben."
-
-#: lib/choose_repository.tcl:882
-msgid "Cannot determine HEAD.  See console output for details."
-msgstr "Nem sikerült megállapítani a HEAD-et.  Bővebben a konzolos kimenetben."
-
-#: lib/choose_repository.tcl:891
-#, tcl-format
-msgid "Unable to cleanup %s"
-msgstr "Nem sikerült tiszítani: %s."
-
-#: lib/choose_repository.tcl:897
-msgid "Clone failed."
-msgstr "A másolás nem sikerült."
-
-#: lib/choose_repository.tcl:904
-msgid "No default branch obtained."
-msgstr "Nincs alapértelmezett branch."
-
-#: lib/choose_repository.tcl:915
-#, tcl-format
-msgid "Cannot resolve %s as a commit."
-msgstr "Nem sikerült felöldani a(z) %s objektumot commitként."
-
-#: lib/choose_repository.tcl:927
-msgid "Creating working directory"
-msgstr "Munkakönyvtár létrehozása"
-
-#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
-#: lib/index.tcl:193
-msgid "files"
-msgstr "fájl"
-
-#: lib/choose_repository.tcl:957
-msgid "Initial file checkout failed."
-msgstr "A kezdeti fájl-kibontás sikertelen."
-
-#: lib/choose_repository.tcl:973
-msgid "Open"
-msgstr "Megnyitás"
-
-#: lib/choose_repository.tcl:983
-msgid "Repository:"
-msgstr "Repó:"
-
-#: lib/choose_repository.tcl:1033
-#, tcl-format
-msgid "Failed to open repository %s:"
-msgstr "Nem sikerült megnyitni a(z) %s repót:"
-
-#: lib/choose_rev.tcl:53
-msgid "This Detached Checkout"
-msgstr "Ez a leválasztott checkout"
-
-#: lib/choose_rev.tcl:60
-msgid "Revision Expression:"
-msgstr "Revízió kifejezés:"
-
-#: lib/choose_rev.tcl:74
-msgid "Local Branch"
-msgstr "Helyi branch"
-
-#: lib/choose_rev.tcl:79
-msgid "Tracking Branch"
-msgstr "Követő branch"
-
-#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
-msgid "Tag"
-msgstr "Tag"
-
-#: lib/choose_rev.tcl:317
-#, tcl-format
-msgid "Invalid revision: %s"
-msgstr "Érvénytelen revízió: %s"
-
-#: lib/choose_rev.tcl:338
-msgid "No revision selected."
-msgstr "Nincs kiválasztva revízió."
-
-#: lib/choose_rev.tcl:346
-msgid "Revision expression is empty."
-msgstr "A revízió kifejezés üres."
-
-#: lib/choose_rev.tcl:530
-msgid "Updated"
-msgstr "Frissítve"
-
-#: lib/choose_rev.tcl:558
-msgid "URL"
-msgstr "URL"
-
-#: lib/commit.tcl:9
-msgid ""
-"There is nothing to amend.\n"
-"\n"
-"You are about to create the initial commit.  There is no commit before this "
-"to amend.\n"
-msgstr ""
-"Nincs semmi javítanivaló.\n"
-"\n"
-"Az első commit létrehozása előtt nincs semmilyen commit amit javitani "
-"lehetne.\n"
-
-#: lib/commit.tcl:18
-msgid ""
-"Cannot amend while merging.\n"
-"\n"
-"You are currently in the middle of a merge that has not been fully "
-"completed.  You cannot amend the prior commit unless you first abort the "
-"current merge activity.\n"
-msgstr ""
-"Nem lehet javítani merge alatt.\n"
-"\n"
-"A jelenlegi merge még nem teljesen fejeződött be. Csak akkor javíthat egy "
-"előbbi commitot, hogyha megszakítja a jelenlegi merge folyamatot.\n"
-
-#: lib/commit.tcl:49
-msgid "Error loading commit data for amend:"
-msgstr "Hiba a javítandó commit adat betöltése közben:"
-
-#: lib/commit.tcl:76
-msgid "Unable to obtain your identity:"
-msgstr "Nem sikerült megállapítani az azonosítót:"
-
-#: lib/commit.tcl:81
-msgid "Invalid GIT_COMMITTER_IDENT:"
-msgstr "Érvénytelen GIT_COMMITTER_IDENT:"
-
-#: lib/commit.tcl:133
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before another commit can be created.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"Az utolsó keresési állapot nem egyezik meg a repó állapotával.\n"
-"\n"
-"Egy másik Git program módosította ezt a repót az utolsó keresés óta. Egy "
-"újrakeresés mindenképpen szükséges mielőtt a jelenlegi branchet módosítani "
-"lehetne.\n"
-"\n"
-"Az újrakeresés most automatikusan el fog indulni.\n"
-
-#: lib/commit.tcl:154
-#, tcl-format
-msgid ""
-"Unmerged files cannot be committed.\n"
-"\n"
-"File %s has merge conflicts.  You must resolve them and stage the file "
-"before committing.\n"
-msgstr ""
-"Nem commitolhatunk fájlokat merge előtt.\n"
-"\n"
-"A(z) %s fájlban ütközések vannak. Egyszer azokat ki kell javítani, majd "
-"hozzá ki kell választani a fájlt mielőtt commitolni lehetne.\n"
-
-#: lib/commit.tcl:162
-#, tcl-format
-msgid ""
-"Unknown file state %s detected.\n"
-"\n"
-"File %s cannot be committed by this program.\n"
-msgstr ""
-"Ismeretlen fájl típus %s érzékelve.\n"
-"\n"
-"A(z) %s fájlt nem tudja ez a program commitolni.\n"
-
-#: lib/commit.tcl:170
-msgid ""
-"No changes to commit.\n"
-"\n"
-"You must stage at least 1 file before you can commit.\n"
-msgstr ""
-"Nincs commitolandó változtatás.\n"
-"\n"
-"Legalább egy fájl ki kell választani, hogy commitolni lehessen.\n"
-
-#: lib/commit.tcl:183
-msgid ""
-"Please supply a commit message.\n"
-"\n"
-"A good commit message has the following format:\n"
-"\n"
-"- First line: Describe in one sentence what you did.\n"
-"- Second line: Blank\n"
-"- Remaining lines: Describe why this change is good.\n"
-msgstr ""
-"Adjunk megy egy commit üzenetet.\n"
-"\n"
-"Egy jó commit üzenetnek a következő a formátuma:\n"
-"\n"
-"- Első sor: Egy mondatban leírja, hogy mit csináltunk.\n"
-"- Második sor: Üres\n"
-"- A többi sor: Leírja, hogy miért jó ez a változtatás.\n"
-
-#: lib/commit.tcl:257
-msgid "write-tree failed:"
-msgstr "a write-tree sikertelen:"
-
-#: lib/commit.tcl:275
-#, tcl-format
-msgid "Commit %s appears to be corrupt"
-msgstr "A(z) %s commit sérültnek tűnik"
-
-#: lib/commit.tcl:279
-msgid ""
-"No changes to commit.\n"
-"\n"
-"No files were modified by this commit and it was not a merge commit.\n"
-"\n"
-"A rescan will be automatically started now.\n"
-msgstr ""
-"Nincs commitolandó változtatás.\n"
-"\n"
-"Egyetlen fájlt se módosított ez a commit és merge commit se volt.\n"
-"\n"
-"Az újrakeresés most automatikusan el fog indulni.\n"
-
-#: lib/commit.tcl:286
-msgid "No changes to commit."
-msgstr "Nincs commitolandó változtatás."
-
-#: lib/commit.tcl:303
-#, tcl-format
-msgid "warning: Tcl does not support encoding '%s'."
-msgstr "figyelmeztetés: a Tcl nem támogatja a(z) '%s' kódolást."
-
-#: lib/commit.tcl:317
-msgid "commit-tree failed:"
-msgstr "a commit-tree sikertelen:"
-
-#: lib/commit.tcl:339
-msgid "update-ref failed:"
-msgstr "az update-ref sikertelen:"
-
-#: lib/commit.tcl:430
-#, tcl-format
-msgid "Created commit %s: %s"
-msgstr "Létrejött a %s commit: %s"
-
-#: lib/console.tcl:57
-msgid "Working... please wait..."
-msgstr "Munka folyamatban.. Várjunk..."
-
-#: lib/console.tcl:183
-msgid "Success"
-msgstr "Siker"
-
-#: lib/console.tcl:196
-msgid "Error: Command Failed"
-msgstr "Hiba: a parancs sikertelen"
-
-#: lib/database.tcl:43
-msgid "Number of loose objects"
-msgstr "Elvesztett objektumok száma"
-
-#: lib/database.tcl:44
-msgid "Disk space used by loose objects"
-msgstr "Elveszett objektumok által elfoglalt lemezterület"
-
-#: lib/database.tcl:45
-msgid "Number of packed objects"
-msgstr "Csomagolt objektumok számra"
-
-#: lib/database.tcl:46
-msgid "Number of packs"
-msgstr "Csomagok száma"
-
-#: lib/database.tcl:47
-msgid "Disk space used by packed objects"
-msgstr "A csomagolt objektumok által használt lemezterület"
-
-#: lib/database.tcl:48
-msgid "Packed objects waiting for pruning"
-msgstr "Eltávolításra váró csomagolt objektumok számra"
-
-#: lib/database.tcl:49
-msgid "Garbage files"
-msgstr "Hulladék fájlok"
-
-#: lib/database.tcl:72
-msgid "Compressing the object database"
-msgstr "Az objektum adatbázis tömörítése"
-
-#: lib/database.tcl:83
-msgid "Verifying the object database with fsck-objects"
-msgstr "Az objektum adatbázis ellenőrzése az fsck-objects használatával"
-
-#: lib/database.tcl:108
-#, tcl-format
-msgid ""
-"This repository currently has approximately %i loose objects.\n"
-"\n"
-"To maintain optimal performance it is strongly recommended that you compress "
-"the database when more than %i loose objects exist.\n"
-"\n"
-"Compress the database now?"
-msgstr ""
-"Ennek a repónak jelenleg %i különálló objektuma van.\n"
-"\n"
-"Az optimális teljesítményhez erősen ajánlott az adatbázis tömörítése, ha "
-"több mint %i objektum létezik.\n"
-"\n"
-"Lehet most tömöríteni az adatbázist?"
-
-#: lib/date.tcl:25
-#, tcl-format
-msgid "Invalid date from Git: %s"
-msgstr "Érvénytelen dátum a Git-től: %s"
-
-#: lib/diff.tcl:42
-#, tcl-format
-msgid ""
-"No differences detected.\n"
-"\n"
-"%s has no changes.\n"
-"\n"
-"The modification date of this file was updated by another application, but "
-"the content within the file was not changed.\n"
-"\n"
-"A rescan will be automatically started to find other files which may have "
-"the same state."
-msgstr ""
-"Nincsenek változások.\n"
-"\n"
-"A(z) %s módosítatlan.\n"
-"\n"
-"A fájl módosítási dátumát frissítette egy másik alkalmazás, de a fájl "
-"tartalma változatlan.\n"
-"\n"
-"Egy újrakeresés fog indulni a hasonló állapotú fájlok megtalálása érdekében."
-
-#: lib/diff.tcl:81
-#, tcl-format
-msgid "Loading diff of %s..."
-msgstr "A(z) %s diff-jének betöltése..."
-
-#: lib/diff.tcl:114 lib/diff.tcl:184
-#, tcl-format
-msgid "Unable to display %s"
-msgstr "Nem lehet megjeleníteni a következőt: %s"
-
-#: lib/diff.tcl:115
-msgid "Error loading file:"
-msgstr "Hiba a fájl betöltése közben:"
-
-#: lib/diff.tcl:122
-msgid "Git Repository (subproject)"
-msgstr "Git repó (alprojekt)"
-
-#: lib/diff.tcl:134
-msgid "* Binary file (not showing content)."
-msgstr "* Bináris fájl (tartalom elrejtése)."
-
-#: lib/diff.tcl:185
-msgid "Error loading diff:"
-msgstr "Hiba a diff betöltése közben:"
-
-#: lib/diff.tcl:302
-msgid "Failed to unstage selected hunk."
-msgstr "Nem visszavonni a hunk kiválasztását."
-
-#: lib/diff.tcl:309
-msgid "Failed to stage selected hunk."
-msgstr "Nem sikerült kiválasztani a hunkot."
-
-#: lib/error.tcl:12 lib/error.tcl:102
-msgid "error"
-msgstr "hiba"
-
-#: lib/error.tcl:28
-msgid "warning"
-msgstr "figyelmeztetés"
-
-#: lib/error.tcl:81
-msgid "You must correct the above errors before committing."
-msgstr "Ki kell javítanunk a fenti hibákat commit előtt."
-
-#: lib/index.tcl:6
-msgid "Unable to unlock the index."
-msgstr "Nem sikerült az index zárolásának feloldása."
-
-#: lib/index.tcl:15
-msgid "Index Error"
-msgstr "Index hiba"
-
-#: lib/index.tcl:21
-msgid ""
-"Updating the Git index failed.  A rescan will be automatically started to "
-"resynchronize git-gui."
-msgstr ""
-"A Git index frissítése sikertelen volt.  Egy újraolvasás automatikusan elindult, hogy "
-"a git-gui újra szinkonban legyen."
-
-#: lib/index.tcl:27
-msgid "Continue"
-msgstr "Folytatás"
-
-#: lib/index.tcl:31
-msgid "Unlock Index"
-msgstr "Index zárolásának feloldása"
-
-#: lib/index.tcl:282
-#, tcl-format
-msgid "Unstaging %s from commit"
-msgstr "A(z) %s commitba való kiválasztásának visszavonása"
-
-#: lib/index.tcl:326
-#, tcl-format
-msgid "Adding %s"
-msgstr "A(z) %s hozzáadása..."
-
-#: lib/index.tcl:381
-#, tcl-format
-msgid "Revert changes in file %s?"
-msgstr "Visszaállítja a változtatásokat a(z) %s fájlban?"
-
-#: lib/index.tcl:383
-#, tcl-format
-msgid "Revert changes in these %i files?"
-msgstr "Visszaállítja a változtatásokat ebben e %i fájlban?"
-
-#: lib/index.tcl:389
-msgid "Any unstaged changes will be permanently lost by the revert."
-msgstr ""
-"Minden nem kiválasztott változtatás el fog veszni ezáltal a visszaállítás "
-"által."
-
-#: lib/index.tcl:392
-msgid "Do Nothing"
-msgstr "Ne csináljunk semmit"
-
-#: lib/merge.tcl:13
-msgid ""
-"Cannot merge while amending.\n"
-"\n"
-"You must finish amending this commit before starting any type of merge.\n"
-msgstr ""
-"Javítás közben nem lehetséges a merge.\n"
-"\n"
-"Egyszer be kell fejezni ennek a commitnak a javítását, majd kezdődhet egy "
-"merge.\n"
-
-#: lib/merge.tcl:27
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before a merge can be performed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"Az utolsó keresési állapot nem egyezik meg a repó állapotával.\n"
-"\n"
-"Egy másik Git program módosította ezt a repót az utolsó keresés óta. Egy "
-"újrakeresés mindenképpen szükséges mielőtt a jelenlegi branchet módosítani "
-"lehetne.\n"
-"\n"
-"Az újrakeresés most automatikusan el fog indulni.\n"
-
-#: lib/merge.tcl:44
-#, tcl-format
-msgid ""
-"You are in the middle of a conflicted merge.\n"
-"\n"
-"File %s has merge conflicts.\n"
-"\n"
-"You must resolve them, stage the file, and commit to complete the current "
-"merge.  Only then can you begin another merge.\n"
-msgstr ""
-"Jelenleg egy ütközés feloldása közben vagyunk.\n"
-"\n"
-"A(z) %s fájlban ütközések vannak.\n"
-"\n"
-"Fel kell oldanunk őket, kiválasztani a fájlt, és commitolni hogy befejezzük "
-"a jelenlegi merge-t. Csak ezután kezdhetünk el egy újabbat.\n"
-
-#: lib/merge.tcl:54
-#, tcl-format
-msgid ""
-"You are in the middle of a change.\n"
-"\n"
-"File %s is modified.\n"
-"\n"
-"You should complete the current commit before starting a merge.  Doing so "
-"will help you abort a failed merge, should the need arise.\n"
-msgstr ""
-"Jelenleg egy változtatás közben vagyunk.\n"
-"\n"
-"A(z) %s fájl megváltozott.\n"
-"\n"
-"Először be kell fejeznünk a jelenlegi commitot, hogy elkezdhessünk egy merge-"
-"t. Ez segíteni fog, hogy félbeszakíthassunk egy merge-t.\n"
-
-#: lib/merge.tcl:106
-#, tcl-format
-msgid "%s of %s"
-msgstr "%s / %s"
-
-#: lib/merge.tcl:119
-#, tcl-format
-msgid "Merging %s and %s"
-msgstr "A(z) %s és a(z) %s merge-ölése"
-
-#: lib/merge.tcl:131
-msgid "Merge completed successfully."
-msgstr "A merge sikeresen befejeződött."
-
-#: lib/merge.tcl:133
-msgid "Merge failed.  Conflict resolution is required."
-msgstr "A merge sikertelen. Fel kell oldanunk az ütközéseket."
-
-#: lib/merge.tcl:158
-#, tcl-format
-msgid "Merge Into %s"
-msgstr "Merge-ölés a következőbe: %s"
-
-#: lib/merge.tcl:177
-msgid "Revision To Merge"
-msgstr "Merge-ölni szándékozott revízió"
-
-#: lib/merge.tcl:212
-msgid ""
-"Cannot abort while amending.\n"
-"\n"
-"You must finish amending this commit.\n"
-msgstr ""
-"A commit javítás közben megszakítva.\n"
-"\n"
-"Be kell fejeznünk ennek a commitnak a javítását.\n"
-
-#: lib/merge.tcl:222
-msgid ""
-"Abort merge?\n"
-"\n"
-"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with aborting the current merge?"
-msgstr ""
-"Megszakítjuk a merge-t?\n"
-"\n"
-"A jelenlegi merge megszakítása *MINDEN* nem commitolt változtatás "
-"elvesztését jelenti.\n"
-"\n"
-"Folytatjuk a jelenlegi merge megszakítását?"
-
-#: lib/merge.tcl:228
-msgid ""
-"Reset changes?\n"
-"\n"
-"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with resetting the current changes?"
-msgstr ""
-"Visszavonjuk a módosításokat?\n"
-"\n"
-"A módosítások visszavonása *MINDEN* nem commitolt változtatás elvesztését "
-"jelenti.\n"
-"\n"
-"Folytatjuk a jelenlegi módosítások visszavonását?"
-
-#: lib/merge.tcl:239
-msgid "Aborting"
-msgstr "Félbeszakítás"
-
-#: lib/merge.tcl:266
-msgid "Abort failed."
-msgstr "A félbeszakítás nem sikerült."
-
-#: lib/merge.tcl:268
-msgid "Abort completed.  Ready."
-msgstr "A megkeszakítás befejeződött. Kész."
-
-#: lib/option.tcl:82
-msgid "Restore Defaults"
-msgstr "Alapértelmezés visszaállítása"
-
-#: lib/option.tcl:86
-msgid "Save"
-msgstr "Mentés"
-
-#: lib/option.tcl:96
-#, tcl-format
-msgid "%s Repository"
-msgstr "%s Repó"
-
-#: lib/option.tcl:97
-msgid "Global (All Repositories)"
-msgstr "Globális (minden repó)"
-
-#: lib/option.tcl:103
-msgid "User Name"
-msgstr "Felhasználónév"
-
-#: lib/option.tcl:104
-msgid "Email Address"
-msgstr "Email cím"
-
-#: lib/option.tcl:106
-msgid "Summarize Merge Commits"
-msgstr "A merge commitok összegzése"
-
-#: lib/option.tcl:107
-msgid "Merge Verbosity"
-msgstr "Merge beszédesség"
-
-#: lib/option.tcl:108
-msgid "Show Diffstat After Merge"
-msgstr "Diffstat mutatása merge után"
-
-#: lib/option.tcl:110
-msgid "Trust File Modification Timestamps"
-msgstr "A fájl módosítási dátumok megbízhatóak"
-
-#: lib/option.tcl:111
-msgid "Prune Tracking Branches During Fetch"
-msgstr "A követő branchek eltávolítása letöltés alatt"
-
-#: lib/option.tcl:112
-msgid "Match Tracking Branches"
-msgstr "A követő branchek egyeztetése"
-
-#: lib/option.tcl:113
-msgid "Number of Diff Context Lines"
-msgstr "A diff környezeti sorok száma"
-
-#: lib/option.tcl:114
-msgid "New Branch Name Template"
-msgstr "Új branch név sablon"
-
-#: lib/option.tcl:176
-msgid "Change Font"
-msgstr "Betűtípus megváltoztatása"
-
-#: lib/option.tcl:180
-#, tcl-format
-msgid "Choose %s"
-msgstr "%s választása"
-
-#: lib/option.tcl:186
-msgid "pt."
-msgstr "pt."
-
-#: lib/option.tcl:200
-msgid "Preferences"
-msgstr "Beállítások"
-
-#: lib/option.tcl:235
-msgid "Failed to completely save options:"
-msgstr "Nem sikerült teljesen elmenteni a beállításokat:"
-
-#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
-msgid "Delete Remote Branch"
-msgstr "Távoli branch törlése"
-
-#: lib/remote_branch_delete.tcl:47
-msgid "From Repository"
-msgstr "Forrás repó"
-
-#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
-msgid "Remote:"
-msgstr "Távoli:"
-
-#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
-msgid "Arbitrary URL:"
-msgstr "Tetszőleges URL:"
-
-#: lib/remote_branch_delete.tcl:84
-msgid "Branches"
-msgstr "Branchek"
-
-#: lib/remote_branch_delete.tcl:109
-msgid "Delete Only If"
-msgstr "Törlés csak akkor ha"
-
-#: lib/remote_branch_delete.tcl:111
-msgid "Merged Into:"
-msgstr "Merge-ölt a következőbe:"
-
-#: lib/remote_branch_delete.tcl:119
-msgid "Always (Do not perform merge checks)"
-msgstr "Mindig (Ne végezzen merge vizsgálatokat)"
-
-#: lib/remote_branch_delete.tcl:152
-msgid "A branch is required for 'Merged Into'."
-msgstr "Egy branch szükséges a 'Merge-ölt a következőbe'-hez."
-
-#: lib/remote_branch_delete.tcl:184
-#, tcl-format
-msgid ""
-"The following branches are not completely merged into %s:\n"
-"\n"
-" - %s"
-msgstr ""
-"A következő branchek nem teljesen lettek merge-ölve ebbe: %s:"
-"\n"
-" - %s"
-
-#: lib/remote_branch_delete.tcl:189
-#, tcl-format
-msgid ""
-"One or more of the merge tests failed because you have not fetched the "
-"necessary commits.  Try fetching from %s first."
-msgstr ""
-"Egy vagy több merge teszt hibát jelzett, mivel nem töltöttük le a megfelelő "
-"commitokat. Próbáljunk meg letölteni a következőből: %s először."
-
-#: lib/remote_branch_delete.tcl:207
-msgid "Please select one or more branches to delete."
-msgstr "Válasszunk ki egy vagy több branchet törlésre."
-
-#: lib/remote_branch_delete.tcl:216
-msgid ""
-"Recovering deleted branches is difficult.\n"
-"\n"
-"Delete the selected branches?"
-msgstr ""
-"A törölt branchek visszaállítása nehéz.\n"
-"\n"
-"Töröljük a kiválasztott brancheket?"
-
-#: lib/remote_branch_delete.tcl:226
-#, tcl-format
-msgid "Deleting branches from %s"
-msgstr "Brancek törlése innen: %s"
-
-#: lib/remote_branch_delete.tcl:286
-msgid "No repository selected."
-msgstr "Nincs kiválasztott repó."
-
-#: lib/remote_branch_delete.tcl:291
-#, tcl-format
-msgid "Scanning %s..."
-msgstr "Keresés itt: %s..."
-
-#: lib/remote.tcl:165
-msgid "Prune from"
-msgstr "Törlés innen"
-
-# tcl-format
-#: lib/remote.tcl:170
-msgid "Fetch from"
-msgstr "Letöltés innen"
-
-#: lib/remote.tcl:213
-msgid "Push to"
-msgstr "Push ide"
-
-#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
-msgid "Cannot write shortcut:"
-msgstr "Nem sikerült írni a gyorsbillentyűt:"
-
-#: lib/shortcut.tcl:136
-msgid "Cannot write icon:"
-msgstr "Nem sikerült írni az ikont:"
-
-#: lib/status_bar.tcl:83
-#, tcl-format
-msgid "%s ... %*i of %*i %s (%3i%%)"
-msgstr "%s ... %*i / %*i %s (%3i%%)"
-
-#: lib/transport.tcl:6
-#, tcl-format
-msgid "fetch %s"
-msgstr "a(z) %s letöltése"
-
-#: lib/transport.tcl:7
-#, tcl-format
-msgid "Fetching new changes from %s"
-msgstr "Új változások letöltése innen: %s"
-
-#: lib/transport.tcl:18
-#, tcl-format
-msgid "remote prune %s"
-msgstr "a(z) %s távoli törlése"
-
-#: lib/transport.tcl:19
-#, tcl-format
-msgid "Pruning tracking branches deleted from %s"
-msgstr "A %s repóból törölt követő branchek törlése"
-
-#: lib/transport.tcl:25 lib/transport.tcl:71
-#, tcl-format
-msgid "push %s"
-msgstr "%s push-olása"
-
-#: lib/transport.tcl:26
-#, tcl-format
-msgid "Pushing changes to %s"
-msgstr "Változások pusholása ide: %s"
-
-#: lib/transport.tcl:72
-#, tcl-format
-msgid "Pushing %s %s to %s"
-msgstr "Pusholás: %s %s, ide: %s"
-
-#: lib/transport.tcl:89
-msgid "Push Branches"
-msgstr "Branchek pusholása"
-
-#: lib/transport.tcl:103
-msgid "Source Branches"
-msgstr "Forrás branchek"
-
-#: lib/transport.tcl:120
-msgid "Destination Repository"
-msgstr "Cél repó"
-
-#: lib/transport.tcl:158
-msgid "Transfer Options"
-msgstr "Átviteli opciók"
-
-#: lib/transport.tcl:160
-msgid "Force overwrite existing branch (may discard changes)"
-msgstr "Létező branch felülírásának erőltetése (lehet, hogy el fog dobni változtatásokat)"
-
-#: lib/transport.tcl:164
-msgid "Use thin pack (for slow network connections)"
-msgstr "Vékony csomagok használata (lassú hálózati kapcsolatok számára)"
-
-#: lib/transport.tcl:168
-msgid "Include tags"
-msgstr "Tageket is"
-
-#~ msgid "Cannot find the git directory:"
-#~ msgstr "Nem található a git könyvtár:"
-
-#~ msgid "Unstaged Changes (Will Not Be Committed)"
-#~ msgstr "Nem kiválasztott változtatások (nem lesz commitolva)"
-
-#~ msgid "Push to %s..."
-#~ msgstr "Pusholás ide: %s..."
-
-#~ msgid "Add To Commit"
-#~ msgstr "Hozzáadás a commithoz"
-
-#~ msgid "Add Existing To Commit"
-#~ msgstr "Hozzáadás létező commithoz"
-
-#~ msgid "Running miga..."
-#~ msgstr "A miga futtatása..."
-
-#~ msgid "Add Existing"
-#~ msgstr "Létező hozzáadása"
-
-#~ msgid ""
-#~ "Abort commit?\n"
-#~ "\n"
-#~ "Aborting the current commit will cause *ALL* uncommitted changes to be "
-#~ "lost.\n"
-#~ "\n"
-#~ "Continue with aborting the current commit?"
-#~ msgstr ""
-#~ "Megszakítjuk a commitot?\n"
-#~ "\n"
-#~ "A jelenlegi commit megszakítása *MINDEN* nem commitolt változtatás "
-#~ "elvesztését jelenti.\n"
-#~ "\n"
-#~ "Folytatjuk a jelenlegi commit megszakítását?"
-
-#~ msgid "Aborting... please wait..."
-#~ msgstr "Megszakítás... várjunk..."
diff --git a/po/it.po b/po/it.po
deleted file mode 100644 (file)
index 33a8399..0000000
--- a/po/it.po
+++ /dev/null
@@ -1,1913 +0,0 @@
-# Translation of git-gui to Italian
-# Copyright (C) 2007 Shawn Pearce
-# This file is distributed under the same license as the git-gui package.
-# Paolo Ciarrocchi <paolo.ciarrocchi@gmail.com>, 2007
-# Michele Ballabio <barra_cuda@katamail.com>, 2007.
-# 
-# 
-msgid ""
-msgstr ""
-"Project-Id-Version: git-gui\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2007-11-09 11:18+0100\n"
-"PO-Revision-Date: 2007-11-01 21:05+0100\n"
-"Last-Translator: Michele Ballabio <barra_cuda@katamail.com>\n"
-"Language-Team: Italian <tp@lists.linux.it>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
-#: git-gui.sh:733
-msgid "git-gui: fatal error"
-msgstr "git-gui: errore grave"
-
-#: git-gui.sh:565
-#, tcl-format
-msgid "Invalid font specified in %s:"
-msgstr "Caratteri non validi specificati in %s:"
-
-#: git-gui.sh:590
-msgid "Main Font"
-msgstr "Caratteri principali"
-
-#: git-gui.sh:591
-msgid "Diff/Console Font"
-msgstr "Caratteri per confronti e terminale"
-
-#: git-gui.sh:605
-msgid "Cannot find git in PATH."
-msgstr "Impossibile trovare git nel PATH"
-
-#: git-gui.sh:632
-msgid "Cannot parse Git version string:"
-msgstr "Impossibile determinare la versione di Git:"
-
-#: git-gui.sh:650
-#, tcl-format
-msgid ""
-"Git version cannot be determined.\n"
-"\n"
-"%s claims it is version '%s'.\n"
-"\n"
-"%s requires at least Git 1.5.0 or later.\n"
-"\n"
-"Assume '%s' is version 1.5.0?\n"
-msgstr ""
-"La versione di Git non può essere determinata.\n"
-"\n"
-"%s riporta che la versione è '%s'.\n"
-"\n"
-"%s richiede almeno Git 1.5.0 o superiore.\n"
-"\n"
-"Assumere che '%s' sia alla versione 1.5.0?\n"
-
-#: git-gui.sh:888
-msgid "Git directory not found:"
-msgstr "Non trovo la directory di git: "
-
-#: git-gui.sh:895
-msgid "Cannot move to top of working directory:"
-msgstr "Impossibile spostarsi sulla directory principale del progetto:"
-
-#: git-gui.sh:902
-msgid "Cannot use funny .git directory:"
-msgstr "Impossibile usare una .git directory strana:"
-
-#: git-gui.sh:907
-msgid "No working directory"
-msgstr "Nessuna directory di lavoro"
-
-#: git-gui.sh:1054
-msgid "Refreshing file status..."
-msgstr "Controllo dello stato dei file in corso..."
-
-#: git-gui.sh:1119
-msgid "Scanning for modified files ..."
-msgstr "Ricerca di file modificati in corso..."
-
-#: git-gui.sh:1294 lib/browser.tcl:245
-msgid "Ready."
-msgstr "Pronto."
-
-#: git-gui.sh:1560
-msgid "Unmodified"
-msgstr "Non modificato"
-
-#: git-gui.sh:1562
-msgid "Modified, not staged"
-msgstr "Modificato, non preparato per una nuova revisione"
-
-#: git-gui.sh:1563 git-gui.sh:1568
-msgid "Staged for commit"
-msgstr "Preparato per una nuova revisione"
-
-#: git-gui.sh:1564 git-gui.sh:1569
-msgid "Portions staged for commit"
-msgstr "Parti preparate per una nuova revisione"
-
-#: git-gui.sh:1565 git-gui.sh:1570
-msgid "Staged for commit, missing"
-msgstr "Preparato per una nuova revisione, mancante"
-
-#: git-gui.sh:1567
-msgid "Untracked, not staged"
-msgstr "Non tracciato, non preparato per una nuova revisione"
-
-#: git-gui.sh:1572
-msgid "Missing"
-msgstr "Mancante"
-
-#: git-gui.sh:1573
-msgid "Staged for removal"
-msgstr "Preparato per la rimozione"
-
-#: git-gui.sh:1574
-msgid "Staged for removal, still present"
-msgstr "Preparato alla rimozione, ancora presente"
-
-#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
-msgid "Requires merge resolution"
-msgstr "Richiede risoluzione dei conflitti"
-
-#: git-gui.sh:1614
-msgid "Starting gitk... please wait..."
-msgstr "Avvio di gitk... attendere..."
-
-#: git-gui.sh:1623
-#, tcl-format
-msgid ""
-"Unable to start gitk:\n"
-"\n"
-"%s does not exist"
-msgstr ""
-"Impossibile avviare gitk:\n"
-"\n"
-"%s non esiste"
-
-#: git-gui.sh:1823 lib/choose_repository.tcl:35
-msgid "Repository"
-msgstr "Archivio"
-
-#: git-gui.sh:1824
-msgid "Edit"
-msgstr "Modifica"
-
-#: git-gui.sh:1826 lib/choose_rev.tcl:560
-msgid "Branch"
-msgstr "Ramo"
-
-#: git-gui.sh:1829 lib/choose_rev.tcl:547
-msgid "Commit@@noun"
-msgstr "Revisione"
-
-#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
-msgid "Merge"
-msgstr "Fusione (Merge)"
-
-#: git-gui.sh:1833 lib/choose_rev.tcl:556
-msgid "Remote"
-msgstr "Remoto"
-
-#: git-gui.sh:1842
-msgid "Browse Current Branch's Files"
-msgstr "Esplora i file del ramo attuale"
-
-#: git-gui.sh:1846
-msgid "Browse Branch Files..."
-msgstr "Esplora i file del ramo..."
-
-#: git-gui.sh:1851
-msgid "Visualize Current Branch's History"
-msgstr "Visualizza la cronologia del ramo attuale"
-
-#: git-gui.sh:1855
-msgid "Visualize All Branch History"
-msgstr "Visualizza la cronologia di tutti i rami"
-
-#: git-gui.sh:1862
-#, tcl-format
-msgid "Browse %s's Files"
-msgstr "Esplora i file di %s"
-
-#: git-gui.sh:1864
-#, tcl-format
-msgid "Visualize %s's History"
-msgstr "Visualizza la cronologia di %s"
-
-#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
-msgid "Database Statistics"
-msgstr "Statistiche dell'archivio"
-
-#: git-gui.sh:1872 lib/database.tcl:34
-msgid "Compress Database"
-msgstr "Comprimi l'archivio"
-
-#: git-gui.sh:1875
-msgid "Verify Database"
-msgstr "Verifica l'archivio"
-
-#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
-#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
-msgid "Create Desktop Icon"
-msgstr "Crea icona desktop"
-
-#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
-msgid "Quit"
-msgstr "Esci"
-
-#: git-gui.sh:1902
-msgid "Undo"
-msgstr "Annulla"
-
-#: git-gui.sh:1905
-msgid "Redo"
-msgstr "Ripeti"
-
-#: git-gui.sh:1909 git-gui.sh:2403
-msgid "Cut"
-msgstr "Taglia"
-
-#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
-#: lib/console.tcl:67
-msgid "Copy"
-msgstr "Copia"
-
-#: git-gui.sh:1915 git-gui.sh:2409
-msgid "Paste"
-msgstr "Incolla"
-
-#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
-#: lib/remote_branch_delete.tcl:38
-msgid "Delete"
-msgstr "Elimina"
-
-#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
-msgid "Select All"
-msgstr "Seleziona tutto"
-
-#: git-gui.sh:1931
-msgid "Create..."
-msgstr "Crea..."
-
-#: git-gui.sh:1937
-msgid "Checkout..."
-msgstr "Attiva..."
-
-#: git-gui.sh:1943
-msgid "Rename..."
-msgstr "Rinomina"
-
-#: git-gui.sh:1948 git-gui.sh:2048
-msgid "Delete..."
-msgstr "Elimina..."
-
-#: git-gui.sh:1953
-msgid "Reset..."
-msgstr "Ripristina..."
-
-#: git-gui.sh:1965 git-gui.sh:2350
-msgid "New Commit"
-msgstr "Nuova revisione"
-
-#: git-gui.sh:1973 git-gui.sh:2357
-msgid "Amend Last Commit"
-msgstr "Correggi l'ultima revisione"
-
-#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
-msgid "Rescan"
-msgstr "Analizza nuovamente"
-
-#: git-gui.sh:1988
-msgid "Stage To Commit"
-msgstr "Prepara per una nuova revisione"
-
-#: git-gui.sh:1994
-msgid "Stage Changed Files To Commit"
-msgstr "Prepara i file modificati per una nuova revisione"
-
-#: git-gui.sh:2000
-msgid "Unstage From Commit"
-msgstr "Annulla preparazione"
-
-#: git-gui.sh:2005 lib/index.tcl:393
-msgid "Revert Changes"
-msgstr "Annulla modifiche"
-
-#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
-msgid "Sign Off"
-msgstr "Sign Off"
-
-#: git-gui.sh:2016 git-gui.sh:2333
-msgid "Commit@@verb"
-msgstr "Nuova revisione"
-
-#: git-gui.sh:2027
-msgid "Local Merge..."
-msgstr "Fusione locale..."
-
-#: git-gui.sh:2032
-msgid "Abort Merge..."
-msgstr "Interrompi fusione..."
-
-#: git-gui.sh:2044
-msgid "Push..."
-msgstr "Propaga..."
-
-#: git-gui.sh:2055 lib/choose_repository.tcl:40
-msgid "Apple"
-msgstr "Apple"
-
-#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
-#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
-#, tcl-format
-msgid "About %s"
-msgstr "Informazioni su %s"
-
-#: git-gui.sh:2062
-msgid "Preferences..."
-msgstr "Preferenze..."
-
-#: git-gui.sh:2070 git-gui.sh:2595
-msgid "Options..."
-msgstr "Opzioni..."
-
-#: git-gui.sh:2076 lib/choose_repository.tcl:46
-msgid "Help"
-msgstr "Aiuto"
-
-#: git-gui.sh:2117
-msgid "Online Documentation"
-msgstr "Documentazione sul web"
-
-#: git-gui.sh:2201
-#, tcl-format
-msgid "fatal: cannot stat path %s: No such file or directory"
-msgstr ""
-"errore grave: impossibile effettuare lo stat del path %s: file o directory "
-"non trovata"
-
-#: git-gui.sh:2234
-msgid "Current Branch:"
-msgstr "Ramo attuale:"
-
-#: git-gui.sh:2255
-msgid "Staged Changes (Will Commit)"
-msgstr "Modifiche preparate (saranno nella nuova revisione)"
-
-#: git-gui.sh:2274
-msgid "Unstaged Changes"
-msgstr "Modifiche non preparate"
-
-#: git-gui.sh:2323
-msgid "Stage Changed"
-msgstr "Prepara modificati"
-
-#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
-msgid "Push"
-msgstr "Propaga (Push)"
-
-#: git-gui.sh:2369
-msgid "Initial Commit Message:"
-msgstr "Messaggio di revisione iniziale:"
-
-#: git-gui.sh:2370
-msgid "Amended Commit Message:"
-msgstr "Messaggio di revisione corretto:"
-
-#: git-gui.sh:2371
-msgid "Amended Initial Commit Message:"
-msgstr "Messaggio iniziale di revisione corretto:"
-
-#: git-gui.sh:2372
-msgid "Amended Merge Commit Message:"
-msgstr "Messaggio di fusione corretto:"
-
-#: git-gui.sh:2373
-msgid "Merge Commit Message:"
-msgstr "Messaggio di fusione:"
-
-#: git-gui.sh:2374
-msgid "Commit Message:"
-msgstr "Messaggio di revisione:"
-
-#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
-msgid "Copy All"
-msgstr "Copia tutto"
-
-#: git-gui.sh:2443 lib/blame.tcl:104
-msgid "File:"
-msgstr "File:"
-
-#: git-gui.sh:2545
-msgid "Refresh"
-msgstr "Rinfresca"
-
-#: git-gui.sh:2566
-msgid "Apply/Reverse Hunk"
-msgstr "Applica/Inverti sezione"
-
-#: git-gui.sh:2572
-msgid "Decrease Font Size"
-msgstr "Diminuisci dimensione caratteri"
-
-#: git-gui.sh:2576
-msgid "Increase Font Size"
-msgstr "Aumenta dimensione caratteri"
-
-#: git-gui.sh:2581
-msgid "Show Less Context"
-msgstr "Mostra meno contesto"
-
-#: git-gui.sh:2588
-msgid "Show More Context"
-msgstr "Mostra più contesto"
-
-#: git-gui.sh:2602
-msgid "Unstage Hunk From Commit"
-msgstr "Sezione non preparata per una nuova revisione"
-
-#: git-gui.sh:2604
-msgid "Stage Hunk For Commit"
-msgstr "Prepara sezione per una nuova revisione"
-
-#: git-gui.sh:2623
-msgid "Initializing..."
-msgstr "Inizializzazione..."
-
-#: git-gui.sh:2718
-#, tcl-format
-msgid ""
-"Possible environment issues exist.\n"
-"\n"
-"The following environment variables are probably\n"
-"going to be ignored by any Git subprocess run\n"
-"by %s:\n"
-"\n"
-msgstr ""
-"Possibili problemi con le variabili d'ambiente.\n"
-"\n"
-"Le seguenti variabili d'ambiente saranno probabilmente\n"
-"ignorate da tutti i sottoprocessi di Git avviati\n"
-"da %s:\n"
-"\n"
-
-#: git-gui.sh:2748
-msgid ""
-"\n"
-"This is due to a known issue with the\n"
-"Tcl binary distributed by Cygwin."
-msgstr ""
-"\n"
-"Ciò è dovuto a un problema conosciuto\n"
-"causato dall'eseguibile Tcl distribuito da Cygwin."
-
-#: git-gui.sh:2753
-#, tcl-format
-msgid ""
-"\n"
-"\n"
-"A good replacement for %s\n"
-"is placing values for the user.name and\n"
-"user.email settings into your personal\n"
-"~/.gitconfig file.\n"
-msgstr ""
-"\n"
-"\n"
-"Una buona alternativa a %s\n"
-"consiste nell'assegnare valori alle variabili di configurazione\n"
-"user.name e user.email nel tuo file ~/.gitconfig personale.\n"
-
-#: lib/about.tcl:25
-msgid "git-gui - a graphical user interface for Git."
-msgstr "git-gui - un'interfaccia grafica per Git."
-
-#: lib/blame.tcl:77
-msgid "File Viewer"
-msgstr "Mostra file"
-
-#: lib/blame.tcl:81
-msgid "Commit:"
-msgstr "Revisione:"
-
-#: lib/blame.tcl:249
-msgid "Copy Commit"
-msgstr "Copia revisione"
-
-#: lib/blame.tcl:369
-#, tcl-format
-msgid "Reading %s..."
-msgstr "Lettura di %s..."
-
-#: lib/blame.tcl:473
-msgid "Loading copy/move tracking annotations..."
-msgstr "Caricamento annotazioni per copie/spostamenti..."
-
-#: lib/blame.tcl:493
-msgid "lines annotated"
-msgstr "linee annotate"
-
-#: lib/blame.tcl:674
-msgid "Loading original location annotations..."
-msgstr "Caricamento annotazioni per posizione originaria..."
-
-#: lib/blame.tcl:677
-msgid "Annotation complete."
-msgstr "Annotazione completata."
-
-#: lib/blame.tcl:731
-msgid "Loading annotation..."
-msgstr "Caricamento annotazioni..."
-
-#: lib/blame.tcl:787
-msgid "Author:"
-msgstr "Autore:"
-
-#: lib/blame.tcl:791
-msgid "Committer:"
-msgstr "Revisione creata da:"
-
-#: lib/blame.tcl:796
-msgid "Original File:"
-msgstr "File originario:"
-
-#: lib/blame.tcl:910
-msgid "Originally By:"
-msgstr "In origine da:"
-
-#: lib/blame.tcl:916
-msgid "In File:"
-msgstr "Nel file:"
-
-#: lib/blame.tcl:921
-msgid "Copied Or Moved Here By:"
-msgstr "Copiato o spostato qui da:"
-
-#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
-msgid "Checkout Branch"
-msgstr "Attiva ramo"
-
-#: lib/branch_checkout.tcl:23
-msgid "Checkout"
-msgstr "Attiva"
-
-#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
-#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
-#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
-#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
-msgid "Cancel"
-msgstr "Annulla"
-
-#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
-msgid "Revision"
-msgstr "Revisione"
-
-#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
-msgid "Options"
-msgstr "Opzioni"
-
-#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
-msgid "Fetch Tracking Branch"
-msgstr "Recupera duplicato locale di ramo remoto"
-
-#: lib/branch_checkout.tcl:44
-msgid "Detach From Local Branch"
-msgstr "Stacca da ramo locale"
-
-#: lib/branch_create.tcl:22
-msgid "Create Branch"
-msgstr "Crea ramo"
-
-#: lib/branch_create.tcl:27
-msgid "Create New Branch"
-msgstr "Crea nuovo ramo"
-
-#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
-msgid "Create"
-msgstr "Crea"
-
-#: lib/branch_create.tcl:40
-msgid "Branch Name"
-msgstr "Nome del ramo"
-
-#: lib/branch_create.tcl:43
-msgid "Name:"
-msgstr "Nome:"
-
-#: lib/branch_create.tcl:58
-msgid "Match Tracking Branch Name"
-msgstr "Appaia nome del duplicato locale di ramo remoto"
-
-#: lib/branch_create.tcl:66
-msgid "Starting Revision"
-msgstr "Revisione iniziale"
-
-#: lib/branch_create.tcl:72
-msgid "Update Existing Branch:"
-msgstr "Aggiorna ramo esistente:"
-
-#: lib/branch_create.tcl:75
-msgid "No"
-msgstr "No"
-
-#: lib/branch_create.tcl:80
-msgid "Fast Forward Only"
-msgstr "Solo fast forward"
-
-#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
-msgid "Reset"
-msgstr "Ripristina"
-
-#: lib/branch_create.tcl:97
-msgid "Checkout After Creation"
-msgstr "Attiva dopo la creazione"
-
-#: lib/branch_create.tcl:131
-msgid "Please select a tracking branch."
-msgstr "Scegliere un duplicato locale di ramo remoto"
-
-#: lib/branch_create.tcl:140
-#, tcl-format
-msgid "Tracking branch %s is not a branch in the remote repository."
-msgstr ""
-"Il duplicato locale del ramo remoto %s non è un ramo nell'archivio remoto."
-
-#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
-msgid "Please supply a branch name."
-msgstr "Inserire un nome per il ramo."
-
-#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
-#, tcl-format
-msgid "'%s' is not an acceptable branch name."
-msgstr "'%s' non è utilizzabile come nome di ramo."
-
-#: lib/branch_delete.tcl:15
-msgid "Delete Branch"
-msgstr "Elimina ramo"
-
-#: lib/branch_delete.tcl:20
-msgid "Delete Local Branch"
-msgstr "Elimina ramo locale"
-
-#: lib/branch_delete.tcl:37
-msgid "Local Branches"
-msgstr "Rami locali"
-
-#: lib/branch_delete.tcl:52
-msgid "Delete Only If Merged Into"
-msgstr "Cancella solo se fuso con un altro ramo"
-
-#: lib/branch_delete.tcl:54
-msgid "Always (Do not perform merge test.)"
-msgstr "Sempre (Non effettuare verifiche di fusione)."
-
-#: lib/branch_delete.tcl:103
-#, tcl-format
-msgid "The following branches are not completely merged into %s:"
-msgstr "I rami seguenti non sono stati fusi completamente in %s:"
-
-#: lib/branch_delete.tcl:115
-msgid ""
-"Recovering deleted branches is difficult. \n"
-"\n"
-" Delete the selected branches?"
-msgstr ""
-"Ricomporre rami cancellati può essere complicato. \n"
-"\n"
-" Eliminare i rami selezionati?"
-
-#: lib/branch_delete.tcl:141
-#, tcl-format
-msgid ""
-"Failed to delete branches:\n"
-"%s"
-msgstr ""
-"Impossibile cancellare i rami:\n"
-"%s"
-
-#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
-msgid "Rename Branch"
-msgstr "Rinomina ramo"
-
-#: lib/branch_rename.tcl:26
-msgid "Rename"
-msgstr "Rinomina"
-
-#: lib/branch_rename.tcl:36
-msgid "Branch:"
-msgstr "Ramo:"
-
-#: lib/branch_rename.tcl:39
-msgid "New Name:"
-msgstr "Nuovo Nome:"
-
-#: lib/branch_rename.tcl:75
-msgid "Please select a branch to rename."
-msgstr "Scegliere un ramo da rinominare."
-
-#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
-#, tcl-format
-msgid "Branch '%s' already exists."
-msgstr "Il ramo '%s' esiste già."
-
-#: lib/branch_rename.tcl:117
-#, tcl-format
-msgid "Failed to rename '%s'."
-msgstr "Impossibile rinominare '%s'."
-
-#: lib/browser.tcl:17
-msgid "Starting..."
-msgstr "Avvio in corso..."
-
-#: lib/browser.tcl:26
-msgid "File Browser"
-msgstr "File browser"
-
-#: lib/browser.tcl:125 lib/browser.tcl:142
-#, tcl-format
-msgid "Loading %s..."
-msgstr "Caricamento %s..."
-
-#: lib/browser.tcl:186
-msgid "[Up To Parent]"
-msgstr "[Directory superiore]"
-
-#: lib/browser.tcl:266 lib/browser.tcl:272
-msgid "Browse Branch Files"
-msgstr "Esplora i file del ramo"
-
-#: lib/browser.tcl:277 lib/choose_repository.tcl:391
-#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
-#: lib/choose_repository.tcl:989
-msgid "Browse"
-msgstr "Sfoglia"
-
-#: lib/checkout_op.tcl:79
-#, tcl-format
-msgid "Fetching %s from %s"
-msgstr "Recupero %s da %s"
-
-#: lib/checkout_op.tcl:127
-#, tcl-format
-msgid "fatal: Cannot resolve %s"
-msgstr "errore grave: impossibile risolvere %s"
-
-#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
-msgid "Close"
-msgstr "Chiudi"
-
-#: lib/checkout_op.tcl:169
-#, tcl-format
-msgid "Branch '%s' does not exist."
-msgstr "Il ramo '%s' non esiste."
-
-#: lib/checkout_op.tcl:206
-#, tcl-format
-msgid ""
-"Branch '%s' already exists.\n"
-"\n"
-"It cannot fast-forward to %s.\n"
-"A merge is required."
-msgstr ""
-"Il ramo '%s' esiste già.\n"
-"\n"
-"Non può effettuare un 'fast-forward' a %s.\n"
-"E' necessaria una fusione."
-
-#: lib/checkout_op.tcl:220
-#, tcl-format
-msgid "Merge strategy '%s' not supported."
-msgstr "La strategia di fusione '%s' non è supportata."
-
-#: lib/checkout_op.tcl:239
-#, tcl-format
-msgid "Failed to update '%s'."
-msgstr "Impossibile aggiornare '%s'."
-
-#: lib/checkout_op.tcl:251
-msgid "Staging area (index) is already locked."
-msgstr ""
-"L'area di preparazione per una nuova revisione (indice) è già bloccata."
-
-#: lib/checkout_op.tcl:266
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before the current branch can be changed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"L'ultimo stato analizzato non corrisponde allo stato dell'archivio.\n"
-"\n"
-"Un altro programma Git ha modificato questo archivio dall'ultima analisi. "
-"Bisogna effettuare una nuova analisi prima di poter cambiare il ramo "
-"attuale.\n"
-"\n"
-"La nuova analisi comincerà ora.\n"
-
-#: lib/checkout_op.tcl:322
-#, tcl-format
-msgid "Updating working directory to '%s'..."
-msgstr "Aggiornamento della directory di lavoro a '%s' in corso..."
-
-#: lib/checkout_op.tcl:353
-#, tcl-format
-msgid "Aborted checkout of '%s' (file level merging is required)."
-msgstr "Attivazione di '%s' fallita (richiesta una fusione a livello file)."
-
-#: lib/checkout_op.tcl:354
-msgid "File level merge required."
-msgstr "E' richiesta una fusione a livello file."
-
-#: lib/checkout_op.tcl:358
-#, tcl-format
-msgid "Staying on branch '%s'."
-msgstr "Si rimarrà sul ramo '%s'."
-
-#: lib/checkout_op.tcl:429
-msgid ""
-"You are no longer on a local branch.\n"
-"\n"
-"If you wanted to be on a branch, create one now starting from 'This Detached "
-"Checkout'."
-msgstr ""
-"Non si è più su un ramo locale\n"
-"\n"
-"Se si vuole rimanere su un ramo, crearne uno ora a partire da 'Questa "
-"revisione attiva staccata'."
-
-#: lib/checkout_op.tcl:446
-#, tcl-format
-msgid "Checked out '%s'."
-msgstr "Attivazione di '%s' completata."
-
-#: lib/checkout_op.tcl:478
-#, tcl-format
-msgid "Resetting '%s' to '%s' will lose the following commits:"
-msgstr ""
-"Ripristinare '%s' a '%s' comporterà la perdita delle seguenti revisioni:"
-
-#: lib/checkout_op.tcl:500
-msgid "Recovering lost commits may not be easy."
-msgstr "Ricomporre le revisioni perdute potrebbe non essere semplice."
-
-#: lib/checkout_op.tcl:505
-#, tcl-format
-msgid "Reset '%s'?"
-msgstr "Ripristinare '%s'?"
-
-#: lib/checkout_op.tcl:510 lib/merge.tcl:164
-msgid "Visualize"
-msgstr "Visualizza"
-
-#: lib/checkout_op.tcl:578
-#, tcl-format
-msgid ""
-"Failed to set current branch.\n"
-"\n"
-"This working directory is only partially switched.  We successfully updated "
-"your files, but failed to update an internal Git file.\n"
-"\n"
-"This should not have occurred.  %s will now close and give up."
-msgstr ""
-"Impossibile preparare il ramo attuale.\n"
-"\n"
-"Questa directory di lavoro è stata convertita solo parzialmente. I file sono "
-"stati aggiornati correttamente, ma l'aggiornamento di un file di Git ha "
-"prodotto degli errori.\n"
-"\n"
-"Questo non sarebbe dovuto succedere.  %s ora terminerà senza altre azioni."
-
-#: lib/choose_font.tcl:39
-msgid "Select"
-msgstr "Seleziona"
-
-#: lib/choose_font.tcl:53
-msgid "Font Family"
-msgstr "Famiglia di caratteri"
-
-#: lib/choose_font.tcl:73
-msgid "Font Size"
-msgstr "Dimensione caratteri"
-
-#: lib/choose_font.tcl:90
-msgid "Font Example"
-msgstr "Esempio caratteri"
-
-#: lib/choose_font.tcl:101
-msgid ""
-"This is example text.\n"
-"If you like this text, it can be your font."
-msgstr ""
-"Questo è un testo d'esempio.\n"
-"Se ti piace questo testo, può essere il carattere giusto."
-
-#: lib/choose_repository.tcl:27
-msgid "Git Gui"
-msgstr "Git Gui"
-
-#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
-msgid "Create New Repository"
-msgstr "Crea nuovo archivio"
-
-#: lib/choose_repository.tcl:86
-msgid "New..."
-msgstr "Nuovo..."
-
-#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
-msgid "Clone Existing Repository"
-msgstr "Clona archivio esistente"
-
-#: lib/choose_repository.tcl:99
-msgid "Clone..."
-msgstr "Clona..."
-
-#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
-msgid "Open Existing Repository"
-msgstr "Apri archivio esistente"
-
-#: lib/choose_repository.tcl:112
-msgid "Open..."
-msgstr "Apri..."
-
-#: lib/choose_repository.tcl:125
-msgid "Recent Repositories"
-msgstr "Archivi recenti"
-
-#: lib/choose_repository.tcl:131
-msgid "Open Recent Repository:"
-msgstr "Apri archivio recente:"
-
-#: lib/choose_repository.tcl:294
-#, tcl-format
-msgid "Location %s already exists."
-msgstr "La posizione %s esiste già."
-
-#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
-#: lib/choose_repository.tcl:314
-#, tcl-format
-msgid "Failed to create repository %s:"
-msgstr "Impossibile creare l'archivio %s:"
-
-#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
-msgid "Directory:"
-msgstr "Directory:"
-
-#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
-#: lib/choose_repository.tcl:1013
-msgid "Git Repository"
-msgstr "Archivio Git"
-
-#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
-#, tcl-format
-msgid "Directory %s already exists."
-msgstr "La directory %s esiste già."
-
-#: lib/choose_repository.tcl:442
-#, tcl-format
-msgid "File %s already exists."
-msgstr "Il file %s esiste già."
-
-#: lib/choose_repository.tcl:463
-msgid "Clone"
-msgstr "Clona"
-
-#: lib/choose_repository.tcl:476
-msgid "URL:"
-msgstr "URL:"
-
-#: lib/choose_repository.tcl:496
-msgid "Clone Type:"
-msgstr "Tipo di clone:"
-
-#: lib/choose_repository.tcl:502
-msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
-msgstr "Standard (veloce, semi-ridondante, con hardlink)"
-
-#: lib/choose_repository.tcl:508
-msgid "Full Copy (Slower, Redundant Backup)"
-msgstr "Copia completa (più lento, backup ridondante)"
-
-#: lib/choose_repository.tcl:514
-msgid "Shared (Fastest, Not Recommended, No Backup)"
-msgstr "Shared (il più veloce, non raccomandato, nessun backup)"
-
-#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
-#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
-#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
-#, tcl-format
-msgid "Not a Git repository: %s"
-msgstr "%s non è un archivio Git."
-
-#: lib/choose_repository.tcl:586
-msgid "Standard only available for local repository."
-msgstr "Standard è disponibile solo per archivi locali."
-
-#: lib/choose_repository.tcl:590
-msgid "Shared only available for local repository."
-msgstr "Shared è disponibile solo per archivi locali."
-
-#: lib/choose_repository.tcl:617
-msgid "Failed to configure origin"
-msgstr "Impossibile configurare origin"
-
-#: lib/choose_repository.tcl:629
-msgid "Counting objects"
-msgstr "Calcolo oggetti"
-
-#: lib/choose_repository.tcl:630
-msgid "buckets"
-msgstr ""
-
-#: lib/choose_repository.tcl:654
-#, tcl-format
-msgid "Unable to copy objects/info/alternates: %s"
-msgstr "Impossibile copiare oggetti/info/alternate: %s"
-
-#: lib/choose_repository.tcl:690
-#, tcl-format
-msgid "Nothing to clone from %s."
-msgstr "Niente da clonare da %s."
-
-#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
-#: lib/choose_repository.tcl:918
-msgid "The 'master' branch has not been initialized."
-msgstr "Il ramo 'master' non è stato inizializzato."
-
-#: lib/choose_repository.tcl:705
-msgid "Hardlinks are unavailable.  Falling back to copying."
-msgstr "Impossibile utilizzare gli hardlink. Si ricorrerà alla copia."
-
-#: lib/choose_repository.tcl:717
-#, tcl-format
-msgid "Cloning from %s"
-msgstr "Clonazione da %s"
-
-#: lib/choose_repository.tcl:748
-msgid "Copying objects"
-msgstr "Copia degli oggetti"
-
-#: lib/choose_repository.tcl:749
-msgid "KiB"
-msgstr "KiB"
-
-#: lib/choose_repository.tcl:773
-#, tcl-format
-msgid "Unable to copy object: %s"
-msgstr "Impossibile copiare oggetto: %s"
-
-#: lib/choose_repository.tcl:783
-msgid "Linking objects"
-msgstr "Collegamento oggetti"
-
-#: lib/choose_repository.tcl:784
-msgid "objects"
-msgstr "oggetti"
-
-#: lib/choose_repository.tcl:792
-#, tcl-format
-msgid "Unable to hardlink object: %s"
-msgstr "Hardlink impossibile sull'oggetto: %s"
-
-#: lib/choose_repository.tcl:847
-msgid "Cannot fetch branches and objects.  See console output for details."
-msgstr ""
-"Impossibile recuperare rami e oggetti. Controllare i dettagli forniti dalla "
-"console."
-
-#: lib/choose_repository.tcl:858
-msgid "Cannot fetch tags.  See console output for details."
-msgstr ""
-"Impossibile recuperare le etichette. Controllare i dettagli forniti dalla "
-"console."
-
-#: lib/choose_repository.tcl:882
-msgid "Cannot determine HEAD.  See console output for details."
-msgstr ""
-"Impossibile determinare HEAD. Controllare i dettagli forniti dalla console."
-
-#: lib/choose_repository.tcl:891
-#, tcl-format
-msgid "Unable to cleanup %s"
-msgstr "Impossibile ripulire %s"
-
-#: lib/choose_repository.tcl:897
-msgid "Clone failed."
-msgstr "Clonazione non riuscita."
-
-#: lib/choose_repository.tcl:904
-msgid "No default branch obtained."
-msgstr "Non è stato trovato un ramo predefinito."
-
-#: lib/choose_repository.tcl:915
-#, tcl-format
-msgid "Cannot resolve %s as a commit."
-msgstr "Impossibile risolvere %s come una revisione."
-
-#: lib/choose_repository.tcl:927
-msgid "Creating working directory"
-msgstr "Creazione directory di lavoro"
-
-#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
-#: lib/index.tcl:193
-msgid "files"
-msgstr "file"
-
-#: lib/choose_repository.tcl:957
-msgid "Initial file checkout failed."
-msgstr "Attivazione iniziale non riuscita."
-
-#: lib/choose_repository.tcl:973
-msgid "Open"
-msgstr "Apri"
-
-#: lib/choose_repository.tcl:983
-msgid "Repository:"
-msgstr "Archivio:"
-
-#: lib/choose_repository.tcl:1033
-#, tcl-format
-msgid "Failed to open repository %s:"
-msgstr "Impossibile accedere all'archivio %s:"
-
-#: lib/choose_rev.tcl:53
-msgid "This Detached Checkout"
-msgstr "Questa revisione attiva staccata"
-
-#: lib/choose_rev.tcl:60
-msgid "Revision Expression:"
-msgstr "Espressione di revisione:"
-
-#: lib/choose_rev.tcl:74
-msgid "Local Branch"
-msgstr "Ramo locale"
-
-#: lib/choose_rev.tcl:79
-msgid "Tracking Branch"
-msgstr "Duplicato locale di ramo remoto"
-
-#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
-msgid "Tag"
-msgstr "Etichetta"
-
-#: lib/choose_rev.tcl:317
-#, tcl-format
-msgid "Invalid revision: %s"
-msgstr "Revisione non valida: %s"
-
-#: lib/choose_rev.tcl:338
-msgid "No revision selected."
-msgstr "Nessuna revisione selezionata."
-
-#: lib/choose_rev.tcl:346
-msgid "Revision expression is empty."
-msgstr "L'espressione di revisione è vuota."
-
-#: lib/choose_rev.tcl:530
-msgid "Updated"
-msgstr "Aggiornato"
-
-#: lib/choose_rev.tcl:558
-msgid "URL"
-msgstr "URL"
-
-#: lib/commit.tcl:9
-msgid ""
-"There is nothing to amend.\n"
-"\n"
-"You are about to create the initial commit.  There is no commit before this "
-"to amend.\n"
-msgstr ""
-"Non c'è niente da correggere.\n"
-"\n"
-"Stai per creare la revisione iniziale. Non esiste una revisione precedente "
-"da correggere.\n"
-
-#: lib/commit.tcl:18
-msgid ""
-"Cannot amend while merging.\n"
-"\n"
-"You are currently in the middle of a merge that has not been fully "
-"completed.  You cannot amend the prior commit unless you first abort the "
-"current merge activity.\n"
-msgstr ""
-"Non è possibile effettuare una correzione durante una fusione.\n"
-"\n"
-"In questo momento si sta effettuando una fusione che non è stata del tutto "
-"completata. Non puoi correggere la revisione precedente a meno che prima tu "
-"non interrompa l'operazione di fusione in corso.\n"
-
-#: lib/commit.tcl:49
-msgid "Error loading commit data for amend:"
-msgstr "Errore durante il caricamento dei dati della revisione da correggere:"
-
-#: lib/commit.tcl:76
-msgid "Unable to obtain your identity:"
-msgstr "Impossibile ottenere la tua identità:"
-
-#: lib/commit.tcl:81
-msgid "Invalid GIT_COMMITTER_IDENT:"
-msgstr "GIT_COMMITTER_IDENT non valida:"
-
-#: lib/commit.tcl:133
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before another commit can be created.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"L'ultimo stato analizzato non corrisponde allo stato dell'archivio.\n"
-"\n"
-"Un altro programma Git ha modificato questo archivio dall'ultima analisi. "
-"Bisogna effettuare una nuova analisi prima di poter creare una nuova "
-"revisione.\n"
-"\n"
-"La nuova analisi comincerà ora.\n"
-
-#: lib/commit.tcl:154
-#, tcl-format
-msgid ""
-"Unmerged files cannot be committed.\n"
-"\n"
-"File %s has merge conflicts.  You must resolve them and stage the file "
-"before committing.\n"
-msgstr ""
-"Non è possibile creare una revisione con file non sottoposti a fusione.\n"
-"\n"
-"Il file %s presenta dei conflitti. Devi risolverli e preparare il file per "
-"creare una nuova revisione prima di effettuare questa azione.\n"
-
-#: lib/commit.tcl:162
-#, tcl-format
-msgid ""
-"Unknown file state %s detected.\n"
-"\n"
-"File %s cannot be committed by this program.\n"
-msgstr ""
-"Stato di file %s sconosciuto.\n"
-"\n"
-"Questo programma non può creare una revisione contenente il file %s.\n"
-
-#: lib/commit.tcl:170
-msgid ""
-"No changes to commit.\n"
-"\n"
-"You must stage at least 1 file before you can commit.\n"
-msgstr ""
-"Nessuna modifica per la nuova revisione.\n"
-"\n"
-"Devi preparare per una nuova revisione almeno 1 file prima di effettuare "
-"questa operazione.\n"
-
-#: lib/commit.tcl:183
-msgid ""
-"Please supply a commit message.\n"
-"\n"
-"A good commit message has the following format:\n"
-"\n"
-"- First line: Describe in one sentance what you did.\n"
-"- Second line: Blank\n"
-"- Remaining lines: Describe why this change is good.\n"
-msgstr ""
-"Bisogna fornire un messaggio di revisione.\n"
-"\n"
-"Un buon messaggio di revisione ha il seguente formato:\n"
-"\n"
-"- Prima linea: descrivi in una frase ciò che hai fatto.\n"
-"- Seconda linea: vuota.\n"
-"- Terza linea: spiega a cosa serve la tua modifica.\n"
-
-#: lib/commit.tcl:257
-msgid "write-tree failed:"
-msgstr "write-tree non riuscito:"
-
-#: lib/commit.tcl:275
-#, tcl-format
-msgid "Commit %s appears to be corrupt"
-msgstr "La revisione %s sembra essere danneggiata"
-
-#: lib/commit.tcl:279
-msgid ""
-"No changes to commit.\n"
-"\n"
-"No files were modified by this commit and it was not a merge commit.\n"
-"\n"
-"A rescan will be automatically started now.\n"
-msgstr ""
-"Nessuna modifica per la nuova revisione.\n"
-"\n"
-"Questa revisione non modifica alcun file e non effettua alcuna fusione.\n"
-"\n"
-"Si procederà subito ad una nuova analisi.\n"
-
-#: lib/commit.tcl:286
-msgid "No changes to commit."
-msgstr "Nessuna modifica per la nuova revisione."
-
-#: lib/commit.tcl:303
-#, tcl-format
-msgid "warning: Tcl does not support encoding '%s'."
-msgstr "attenzione: Tcl non supporta la codifica '%s'."
-
-#: lib/commit.tcl:317
-msgid "commit-tree failed:"
-msgstr "commit-tree non riuscito:"
-
-#: lib/commit.tcl:339
-msgid "update-ref failed:"
-msgstr "update-ref non riuscito:"
-
-#: lib/commit.tcl:430
-#, tcl-format
-msgid "Created commit %s: %s"
-msgstr "Creata revisione %s: %s"
-
-#: lib/console.tcl:57
-msgid "Working... please wait..."
-msgstr "Elaborazione in corso... attendere..."
-
-#: lib/console.tcl:183
-msgid "Success"
-msgstr "Successo"
-
-#: lib/console.tcl:196
-msgid "Error: Command Failed"
-msgstr "Errore: comando non riuscito"
-
-#: lib/database.tcl:43
-msgid "Number of loose objects"
-msgstr "Numero di oggetti slegati"
-
-#: lib/database.tcl:44
-msgid "Disk space used by loose objects"
-msgstr "Spazio su disco utilizzato da oggetti slegati"
-
-#: lib/database.tcl:45
-msgid "Number of packed objects"
-msgstr "Numero di oggetti impacchettati"
-
-#: lib/database.tcl:46
-msgid "Number of packs"
-msgstr "Numero di pacchetti"
-
-#: lib/database.tcl:47
-msgid "Disk space used by packed objects"
-msgstr "Spazio su disco utilizzato da oggetti impacchettati"
-
-#: lib/database.tcl:48
-msgid "Packed objects waiting for pruning"
-msgstr "Oggetti impacchettati che attendono la potatura"
-
-#: lib/database.tcl:49
-msgid "Garbage files"
-msgstr "File inutili"
-
-#: lib/database.tcl:72
-msgid "Compressing the object database"
-msgstr "Compressione dell'archivio in corso"
-
-#: lib/database.tcl:83
-msgid "Verifying the object database with fsck-objects"
-msgstr "Verifica dell'archivio con fsck-objects in corso"
-
-#: lib/database.tcl:108
-#, tcl-format
-msgid ""
-"This repository currently has approximately %i loose objects.\n"
-"\n"
-"To maintain optimal performance it is strongly recommended that you compress "
-"the database when more than %i loose objects exist.\n"
-"\n"
-"Compress the database now?"
-msgstr ""
-"Questo archivio attualmente ha circa %i oggetti slegati.\n"
-"\n"
-"Per mantenere buone prestazioni si raccomanda di comprimere l'archivio "
-"quando sono presenti più di %i oggetti slegati.\n"
-"\n"
-"Comprimere l'archivio ora?"
-
-#: lib/date.tcl:25
-#, tcl-format
-msgid "Invalid date from Git: %s"
-msgstr "Git ha restituito una data non valida: %s"
-
-#: lib/diff.tcl:42
-#, tcl-format
-msgid ""
-"No differences detected.\n"
-"\n"
-"%s has no changes.\n"
-"\n"
-"The modification date of this file was updated by another application, but "
-"the content within the file was not changed.\n"
-"\n"
-"A rescan will be automatically started to find other files which may have "
-"the same state."
-msgstr ""
-"Non sono state trovate differenze.\n"
-"\n"
-"%s non ha modifiche.\n"
-"\n"
-"La data di modifica di questo file è stata cambiata da un'altra "
-"applicazione, ma il contenuto del file è rimasto invariato.\n"
-"\n"
-"Si procederà automaticamente ad una nuova analisi per trovare altri file che "
-"potrebbero avere lo stesso stato."
-
-#: lib/diff.tcl:81
-#, tcl-format
-msgid "Loading diff of %s..."
-msgstr "Caricamento delle differenze di %s..."
-
-#: lib/diff.tcl:114 lib/diff.tcl:184
-#, tcl-format
-msgid "Unable to display %s"
-msgstr "Impossibile visualizzare %s"
-
-#: lib/diff.tcl:115
-msgid "Error loading file:"
-msgstr "Errore nel caricamento del file:"
-
-#: lib/diff.tcl:122
-msgid "Git Repository (subproject)"
-msgstr "Archivio Git (sottoprogetto)"
-
-#: lib/diff.tcl:134
-msgid "* Binary file (not showing content)."
-msgstr "* File binario (il contenuto non sarà mostrato)."
-
-#: lib/diff.tcl:185
-msgid "Error loading diff:"
-msgstr "Errore nel caricamento delle differenze:"
-
-#: lib/diff.tcl:302
-msgid "Failed to unstage selected hunk."
-msgstr "Impossibile rimuovere la sezione scelta dalla nuova revisione."
-
-#: lib/diff.tcl:309
-msgid "Failed to stage selected hunk."
-msgstr "Impossibile preparare la sezione scelta per una nuova revisione."
-
-#: lib/error.tcl:12 lib/error.tcl:102
-msgid "error"
-msgstr "errore"
-
-#: lib/error.tcl:28
-msgid "warning"
-msgstr "attenzione"
-
-#: lib/error.tcl:81
-msgid "You must correct the above errors before committing."
-msgstr ""
-"Bisogna correggere gli errori suddetti prima di creare una nuova revisione."
-
-#: lib/index.tcl:6
-msgid "Unable to unlock the index."
-msgstr "Impossibile sbloccare l'accesso all'indice"
-
-#: lib/index.tcl:15
-msgid "Index Error"
-msgstr "Errore nell'indice"
-
-#: lib/index.tcl:21
-msgid ""
-"Updating the Git index failed.  A rescan will be automatically started to "
-"resynchronize git-gui."
-msgstr ""
-"Impossibile aggiornare l'indice. Ora sarà avviata una nuova analisi che "
-"aggiornerà git-gui."
-
-#: lib/index.tcl:27
-msgid "Continue"
-msgstr "Continua"
-
-#: lib/index.tcl:31
-msgid "Unlock Index"
-msgstr "Sblocca l'accesso all'indice"
-
-#: lib/index.tcl:282
-#, tcl-format
-msgid "Unstaging %s from commit"
-msgstr "%s non farà parte della prossima revisione"
-
-#: lib/index.tcl:326
-#, tcl-format
-msgid "Adding %s"
-msgstr "Aggiunta di %s in corso"
-
-#: lib/index.tcl:381
-#, tcl-format
-msgid "Revert changes in file %s?"
-msgstr "Annullare le modifiche nel file %s?"
-
-#: lib/index.tcl:383
-#, tcl-format
-msgid "Revert changes in these %i files?"
-msgstr "Annullare le modifiche in questi %i file?"
-
-#: lib/index.tcl:389
-msgid "Any unstaged changes will be permanently lost by the revert."
-msgstr ""
-"Tutte le modifiche non preparate per una nuova revisione saranno perse per "
-"sempre."
-
-#: lib/index.tcl:392
-msgid "Do Nothing"
-msgstr "Non fare niente"
-
-#: lib/merge.tcl:13
-msgid ""
-"Cannot merge while amending.\n"
-"\n"
-"You must finish amending this commit before starting any type of merge.\n"
-msgstr ""
-"Non posso effettuare fusioni durante una correzione.\n"
-"\n"
-"Bisogna finire di correggere questa revisione prima di iniziare una "
-"qualunque fusione.\n"
-
-#: lib/merge.tcl:27
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before a merge can be performed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"L'ultimo stato analizzato non corrisponde allo stato dell'archivio.\n"
-"\n"
-"Un altro programma Git ha modificato questo archivio dall'ultima analisi."
-"Bisogna effettuare una nuova analisi prima di poter effettuare una fusione.\n"
-"\n"
-"La nuova analisi comincerà ora.\n"
-
-#: lib/merge.tcl:44
-#, tcl-format
-msgid ""
-"You are in the middle of a conflicted merge.\n"
-"\n"
-"File %s has merge conflicts.\n"
-"\n"
-"You must resolve them, stage the file, and commit to complete the current "
-"merge.  Only then can you begin another merge.\n"
-msgstr ""
-"Sei nel mezzo di una fusione con conflitti.\n"
-"\n"
-"Il file %s ha dei conflitti.\n"
-"\n"
-"Bisogna risolvere i conflitti, preparare il file per una nuova revisione ed "
-"infine crearla per completare la fusione attuale. Solo a questo punto potrai "
-"iniziare un'altra fusione.\n"
-
-#: lib/merge.tcl:54
-#, tcl-format
-msgid ""
-"You are in the middle of a change.\n"
-"\n"
-"File %s is modified.\n"
-"\n"
-"You should complete the current commit before starting a merge.  Doing so "
-"will help you abort a failed merge, should the need arise.\n"
-msgstr ""
-"Sei nel mezzo di una modifica.\n"
-"\n"
-"Il file %s è stato modificato.\n"
-"\n"
-"Bisogna completare la creazione della revisione attuale prima di iniziare "
-"una fusione. In questo modo sarà più facile interrompere una fusione non "
-"riuscita, nel caso ce ne fosse bisogno.\n"
-
-#: lib/merge.tcl:106
-#, tcl-format
-msgid "%s of %s"
-msgstr "%s di %s"
-
-#: lib/merge.tcl:119
-#, tcl-format
-msgid "Merging %s and %s"
-msgstr "Fusione di %s e %s in corso"
-
-#: lib/merge.tcl:131
-msgid "Merge completed successfully."
-msgstr "Fusione completata con successo."
-
-#: lib/merge.tcl:133
-msgid "Merge failed.  Conflict resolution is required."
-msgstr "Fusione non riuscita. Bisogna risolvere i conflitti."
-
-#: lib/merge.tcl:158
-#, tcl-format
-msgid "Merge Into %s"
-msgstr "Fusione in %s"
-
-#: lib/merge.tcl:177
-msgid "Revision To Merge"
-msgstr "Revisione da fondere"
-
-#: lib/merge.tcl:212
-msgid ""
-"Cannot abort while amending.\n"
-"\n"
-"You must finish amending this commit.\n"
-msgstr ""
-"Interruzione impossibile durante una correzione.\n"
-"\n"
-"Bisogna finire di correggere questa revisione.\n"
-
-#: lib/merge.tcl:222
-msgid ""
-"Abort merge?\n"
-"\n"
-"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with aborting the current merge?"
-msgstr ""
-"Interrompere fusione?\n"
-"\n"
-"L'interruzione della fusione attuale causerà la perdita di *TUTTE* le "
-"modifiche non ancora presenti nell'archivio.\n"
-"\n"
-"Continuare con l'interruzione della fusione attuale?"
-
-#: lib/merge.tcl:228
-msgid ""
-"Reset changes?\n"
-"\n"
-"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with resetting the current changes?"
-msgstr ""
-"Ripristinare la revisione corrente e annullare le modifiche?\n"
-"\n"
-"L'annullamento delle modifiche causerà la perdita di *TUTTE* le modifiche "
-"non ancora presenti nell'archivio.\n"
-"\n"
-"Continuare con l'annullamento delle modifiche attuali?"
-
-#: lib/merge.tcl:239
-msgid "Aborting"
-msgstr "Interruzione in corso"
-
-#: lib/merge.tcl:266
-msgid "Abort failed."
-msgstr "Interruzione non riuscita."
-
-#: lib/merge.tcl:268
-msgid "Abort completed.  Ready."
-msgstr "Interruzione completata. Pronto."
-
-#: lib/option.tcl:82
-msgid "Restore Defaults"
-msgstr "Ripristina valori predefiniti"
-
-#: lib/option.tcl:86
-msgid "Save"
-msgstr "Salva"
-
-#: lib/option.tcl:96
-#, tcl-format
-msgid "%s Repository"
-msgstr "Archivio di %s"
-
-#: lib/option.tcl:97
-msgid "Global (All Repositories)"
-msgstr "Tutti gli archivi"
-
-#: lib/option.tcl:103
-msgid "User Name"
-msgstr "Nome utente"
-
-#: lib/option.tcl:104
-msgid "Email Address"
-msgstr "Indirizzo Email"
-
-#: lib/option.tcl:106
-msgid "Summarize Merge Commits"
-msgstr "Riepilogo nelle revisioni di fusione"
-
-#: lib/option.tcl:107
-msgid "Merge Verbosity"
-msgstr "Prolissità della fusione"
-
-#: lib/option.tcl:108
-msgid "Show Diffstat After Merge"
-msgstr "Mostra statistiche delle differenze dopo la fusione"
-
-#: lib/option.tcl:110
-msgid "Trust File Modification Timestamps"
-msgstr "Fidati delle date di modifica dei file"
-
-#: lib/option.tcl:111
-msgid "Prune Tracking Branches During Fetch"
-msgstr ""
-"Effettua potatura dei duplicati locali di rami remoti durante il recupero"
-
-#: lib/option.tcl:112
-msgid "Match Tracking Branches"
-msgstr "Appaia duplicati locali di rami remoti"
-
-#: lib/option.tcl:113
-msgid "Number of Diff Context Lines"
-msgstr "Numero di linee di contesto nelle differenze"
-
-#: lib/option.tcl:114
-msgid "New Branch Name Template"
-msgstr "Modello per il nome di un nuovo ramo"
-
-#: lib/option.tcl:176
-msgid "Change Font"
-msgstr "Cambia caratteri"
-
-#: lib/option.tcl:180
-#, tcl-format
-msgid "Choose %s"
-msgstr "Scegli %s"
-
-#: lib/option.tcl:186
-msgid "pt."
-msgstr "pt."
-
-#: lib/option.tcl:200
-msgid "Preferences"
-msgstr "Preferenze"
-
-#: lib/option.tcl:235
-msgid "Failed to completely save options:"
-msgstr "Impossibile salvare completamente le opzioni:"
-
-#: lib/remote.tcl:165
-msgid "Prune from"
-msgstr "Effettua potatura da"
-
-#: lib/remote.tcl:170
-msgid "Fetch from"
-msgstr "Recupera da"
-
-#: lib/remote.tcl:213
-msgid "Push to"
-msgstr "Propaga verso"
-
-#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
-msgid "Delete Remote Branch"
-msgstr "Cancella ramo remoto"
-
-#: lib/remote_branch_delete.tcl:47
-msgid "From Repository"
-msgstr "Da archivio"
-
-#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
-msgid "Remote:"
-msgstr "Remoto:"
-
-#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
-msgid "Arbitrary URL:"
-msgstr "URL specifico:"
-
-#: lib/remote_branch_delete.tcl:84
-msgid "Branches"
-msgstr "Rami"
-
-#: lib/remote_branch_delete.tcl:109
-msgid "Delete Only If"
-msgstr "Elimina solo se"
-
-#: lib/remote_branch_delete.tcl:111
-msgid "Merged Into:"
-msgstr "Fuso in:"
-
-#: lib/remote_branch_delete.tcl:119
-msgid "Always (Do not perform merge checks)"
-msgstr "Sempre (non verificare le fusioni)"
-
-#: lib/remote_branch_delete.tcl:152
-msgid "A branch is required for 'Merged Into'."
-msgstr "Si richiede un ramo per 'Fuso in'."
-
-#: lib/remote_branch_delete.tcl:184
-#, tcl-format
-msgid ""
-"The following branches are not completely merged into %s:\n"
-"\n"
-" - %s"
-msgstr ""
-"I rami seguenti non sono stati fusi completamente in %s:\n"
-"\n"
-" - %s"
-
-#: lib/remote_branch_delete.tcl:189
-#, tcl-format
-msgid ""
-"One or more of the merge tests failed because you have not fetched the "
-"necessary commits.  Try fetching from %s first."
-msgstr ""
-"Impossibile verificare una o più fusioni: mancano le revisioni necessarie. "
-"Prova prima a recuperarle da %s."
-
-#: lib/remote_branch_delete.tcl:207
-msgid "Please select one or more branches to delete."
-msgstr "Scegliere uno o più rami da cancellare."
-
-#: lib/remote_branch_delete.tcl:216
-msgid ""
-"Recovering deleted branches is difficult.\n"
-"\n"
-"Delete the selected branches?"
-msgstr ""
-"Ricomporre rami cancellati è difficile.\n"
-"\n"
-"Cancellare i rami selezionati?"
-
-#: lib/remote_branch_delete.tcl:226
-#, tcl-format
-msgid "Deleting branches from %s"
-msgstr "Cancellazione rami da %s"
-
-#: lib/remote_branch_delete.tcl:286
-msgid "No repository selected."
-msgstr "Nessun archivio selezionato."
-
-#: lib/remote_branch_delete.tcl:291
-#, tcl-format
-msgid "Scanning %s..."
-msgstr "Analisi in corso %s..."
-
-#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
-msgid "Cannot write shortcut:"
-msgstr "Impossibile scrivere shortcut:"
-
-#: lib/shortcut.tcl:136
-msgid "Cannot write icon:"
-msgstr "Impossibile scrivere icona:"
-
-#: lib/status_bar.tcl:83
-#, tcl-format
-msgid "%s ... %*i of %*i %s (%3i%%)"
-msgstr "%1$s ... %6$s: %2$*i di %4$*i (%7$3i%%)"
-
-#: lib/transport.tcl:6
-#, tcl-format
-msgid "fetch %s"
-msgstr "recupera da %s"
-
-#: lib/transport.tcl:7
-#, tcl-format
-msgid "Fetching new changes from %s"
-msgstr "Recupero nuove modifiche da %s"
-
-#: lib/transport.tcl:18
-#, tcl-format
-msgid "remote prune %s"
-msgstr "potatura remota di %s"
-
-#: lib/transport.tcl:19
-#, tcl-format
-msgid "Pruning tracking branches deleted from %s"
-msgstr "Effettua potatura dei duplicati locali di rami remoti cancellati da %s"
-
-#: lib/transport.tcl:25 lib/transport.tcl:71
-#, tcl-format
-msgid "push %s"
-msgstr "propaga verso %s"
-
-#: lib/transport.tcl:26
-#, tcl-format
-msgid "Pushing changes to %s"
-msgstr "Propagazione modifiche a %s"
-
-#: lib/transport.tcl:72
-#, tcl-format
-msgid "Pushing %s %s to %s"
-msgstr "Propagazione %s %s a %s"
-
-#: lib/transport.tcl:89
-msgid "Push Branches"
-msgstr "Propaga rami"
-
-#: lib/transport.tcl:103
-msgid "Source Branches"
-msgstr "Rami di origine"
-
-#: lib/transport.tcl:120
-msgid "Destination Repository"
-msgstr "Archivio di destinazione"
-
-#: lib/transport.tcl:158
-msgid "Transfer Options"
-msgstr "Opzioni di trasferimento"
-
-#: lib/transport.tcl:160
-msgid "Force overwrite existing branch (may discard changes)"
-msgstr "Sovrascrivi ramo esistente (alcune modifiche potrebbero essere perse)"
-
-#: lib/transport.tcl:164
-msgid "Use thin pack (for slow network connections)"
-msgstr "Utilizza 'thin pack' (per connessioni lente)"
-
-#: lib/transport.tcl:168
-msgid "Include tags"
-msgstr "Includi etichette"
diff --git a/po/ja.po b/po/ja.po
deleted file mode 100644 (file)
index e2cf5bd..0000000
--- a/po/ja.po
+++ /dev/null
@@ -1,1883 +0,0 @@
-# Translation of git-gui to Japanese
-# Copyright (C) 2007 Shawn Pearce
-# This file is distributed under the same license as the git-gui package.
-# しらいし ななこ <nanako3@bluebottle.com>, 2007.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: git-gui\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2007-11-24 10:36+0100\n"
-"PO-Revision-Date: 2007-12-05 06:12+0900\n"
-"Last-Translator: しらいし ななこ <nanako3@bluebottle.com>\n"
-"Language-Team: Japanese\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: git-gui.sh:41 git-gui.sh:604 git-gui.sh:618 git-gui.sh:631 git-gui.sh:714
-#: git-gui.sh:733
-msgid "git-gui: fatal error"
-msgstr "git-gui: 致命的なエラー"
-
-#: git-gui.sh:565
-#, tcl-format
-msgid "Invalid font specified in %s:"
-msgstr "%s に無効なフォントが指定されています:"
-
-#: git-gui.sh:590
-msgid "Main Font"
-msgstr "主フォント"
-
-#: git-gui.sh:591
-msgid "Diff/Console Font"
-msgstr "diff/コンソール・フォント"
-
-#: git-gui.sh:605
-msgid "Cannot find git in PATH."
-msgstr "PATH 中に git が見つかりません"
-
-#: git-gui.sh:632
-msgid "Cannot parse Git version string:"
-msgstr "Git バージョン名が理解できません:"
-
-#: git-gui.sh:650
-#, tcl-format
-msgid ""
-"Git version cannot be determined.\n"
-"\n"
-"%s claims it is version '%s'.\n"
-"\n"
-"%s requires at least Git 1.5.0 or later.\n"
-"\n"
-"Assume '%s' is version 1.5.0?\n"
-msgstr ""
-"Git のバージョンが確認できません。\n"
-"\n"
-"%s はバージョン '%s' とのことです。\n"
-"\n"
-"%s は最低でも 1.5.0 かそれ以降の Git が必要です\n"
-"\n"
-"'%s' はバージョン 1.5.0 と思って良いですか?\n"
-
-#: git-gui.sh:888
-msgid "Git directory not found:"
-msgstr "Git ディレクトリが見つかりません:"
-
-#: git-gui.sh:895
-msgid "Cannot move to top of working directory:"
-msgstr "作業ディレクトリの最上位に移動できません"
-
-#: git-gui.sh:902
-msgid "Cannot use funny .git directory:"
-msgstr "変な .git ディレクトリは使えません"
-
-#: git-gui.sh:907
-msgid "No working directory"
-msgstr "作業ディレクトリがありません"
-
-#: git-gui.sh:1054
-msgid "Refreshing file status..."
-msgstr "ファイル状態を更新しています…"
-
-#: git-gui.sh:1119
-msgid "Scanning for modified files ..."
-msgstr "変更されたファイルをスキャンしています…"
-
-#: git-gui.sh:1294 lib/browser.tcl:245
-msgid "Ready."
-msgstr "準備完了"
-
-#: git-gui.sh:1560
-msgid "Unmodified"
-msgstr "変更無し"
-
-#: git-gui.sh:1562
-msgid "Modified, not staged"
-msgstr "変更あり、コミット未予定"
-
-#: git-gui.sh:1563 git-gui.sh:1568
-msgid "Staged for commit"
-msgstr "コミット予定済"
-
-#: git-gui.sh:1564 git-gui.sh:1569
-msgid "Portions staged for commit"
-msgstr "部分的にコミット予定済"
-
-#: git-gui.sh:1565 git-gui.sh:1570
-msgid "Staged for commit, missing"
-msgstr "コミット予定済、ファイル無し"
-
-#: git-gui.sh:1567
-msgid "Untracked, not staged"
-msgstr "管理外、コミット未予定"
-
-#: git-gui.sh:1572
-msgid "Missing"
-msgstr "ファイル無し"
-
-#: git-gui.sh:1573
-msgid "Staged for removal"
-msgstr "削除予定済"
-
-#: git-gui.sh:1574
-msgid "Staged for removal, still present"
-msgstr "削除予定済、ファイル未削除"
-
-#: git-gui.sh:1576 git-gui.sh:1577 git-gui.sh:1578 git-gui.sh:1579
-msgid "Requires merge resolution"
-msgstr "要マージ解決"
-
-#: git-gui.sh:1614
-msgid "Starting gitk... please wait..."
-msgstr "gitk を起動中…お待ち下さい…"
-
-#: git-gui.sh:1623
-#, tcl-format
-msgid ""
-"Unable to start gitk:\n"
-"\n"
-"%s does not exist"
-msgstr ""
-"gitk を起動できません:\n"
-"\n"
-"%s がありません"
-
-#: git-gui.sh:1823 lib/choose_repository.tcl:35
-msgid "Repository"
-msgstr "リポジトリ"
-
-#: git-gui.sh:1824
-msgid "Edit"
-msgstr "編集"
-
-#: git-gui.sh:1826 lib/choose_rev.tcl:560
-msgid "Branch"
-msgstr "ブランチ"
-
-#: git-gui.sh:1829 lib/choose_rev.tcl:547
-msgid "Commit@@noun"
-msgstr "コミット"
-
-#: git-gui.sh:1832 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
-msgid "Merge"
-msgstr "マージ"
-
-#: git-gui.sh:1833 lib/choose_rev.tcl:556
-msgid "Remote"
-msgstr "リモート"
-
-#: git-gui.sh:1842
-msgid "Browse Current Branch's Files"
-msgstr "現在のブランチのファイルを見る"
-
-#: git-gui.sh:1846
-msgid "Browse Branch Files..."
-msgstr "ブランチのファイルを見る…"
-
-#: git-gui.sh:1851
-msgid "Visualize Current Branch's History"
-msgstr "現在のブランチの履歴を見る"
-
-#: git-gui.sh:1855
-msgid "Visualize All Branch History"
-msgstr "全てのブランチの履歴を見る"
-
-#: git-gui.sh:1862
-#, tcl-format
-msgid "Browse %s's Files"
-msgstr "ブランチ %s のファイルを見る"
-
-#: git-gui.sh:1864
-#, tcl-format
-msgid "Visualize %s's History"
-msgstr "ブランチ %s の履歴を見る"
-
-#: git-gui.sh:1869 lib/database.tcl:27 lib/database.tcl:67
-msgid "Database Statistics"
-msgstr "データベース統計"
-
-#: git-gui.sh:1872 lib/database.tcl:34
-msgid "Compress Database"
-msgstr "データベース圧縮"
-
-#: git-gui.sh:1875
-msgid "Verify Database"
-msgstr "データベース検証"
-
-#: git-gui.sh:1882 git-gui.sh:1886 git-gui.sh:1890 lib/shortcut.tcl:7
-#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
-msgid "Create Desktop Icon"
-msgstr "デスクトップ・アイコンを作る"
-
-#: git-gui.sh:1895 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
-msgid "Quit"
-msgstr "終了"
-
-#: git-gui.sh:1902
-msgid "Undo"
-msgstr "元に戻す"
-
-#: git-gui.sh:1905
-msgid "Redo"
-msgstr "やり直し"
-
-#: git-gui.sh:1909 git-gui.sh:2403
-msgid "Cut"
-msgstr "切り取り"
-
-#: git-gui.sh:1912 git-gui.sh:2406 git-gui.sh:2477 git-gui.sh:2549
-#: lib/console.tcl:67
-msgid "Copy"
-msgstr "コピー"
-
-#: git-gui.sh:1915 git-gui.sh:2409
-msgid "Paste"
-msgstr "貼り付け"
-
-#: git-gui.sh:1918 git-gui.sh:2412 lib/branch_delete.tcl:26
-#: lib/remote_branch_delete.tcl:38
-msgid "Delete"
-msgstr "削除"
-
-#: git-gui.sh:1922 git-gui.sh:2416 git-gui.sh:2553 lib/console.tcl:69
-msgid "Select All"
-msgstr "全て選択"
-
-#: git-gui.sh:1931
-msgid "Create..."
-msgstr "作成…"
-
-#: git-gui.sh:1937
-msgid "Checkout..."
-msgstr "チェックアウト"
-
-#: git-gui.sh:1943
-msgid "Rename..."
-msgstr "名前変更…"
-
-#: git-gui.sh:1948 git-gui.sh:2048
-msgid "Delete..."
-msgstr "削除…"
-
-#: git-gui.sh:1953
-msgid "Reset..."
-msgstr "リセット…"
-
-#: git-gui.sh:1965 git-gui.sh:2350
-msgid "New Commit"
-msgstr "新規コミット"
-
-#: git-gui.sh:1973 git-gui.sh:2357
-msgid "Amend Last Commit"
-msgstr "最新コミットを訂正"
-
-#: git-gui.sh:1982 git-gui.sh:2317 lib/remote_branch_delete.tcl:99
-msgid "Rescan"
-msgstr "再スキャン"
-
-#: git-gui.sh:1988
-msgid "Stage To Commit"
-msgstr "コミット予定する"
-
-#: git-gui.sh:1994
-msgid "Stage Changed Files To Commit"
-msgstr "変更されたファイルをコミット予定"
-
-#: git-gui.sh:2000
-msgid "Unstage From Commit"
-msgstr "コミットから降ろす"
-
-#: git-gui.sh:2005 lib/index.tcl:393
-msgid "Revert Changes"
-msgstr "変更を元に戻す"
-
-#: git-gui.sh:2012 git-gui.sh:2329 git-gui.sh:2427
-msgid "Sign Off"
-msgstr "署名"
-
-#: git-gui.sh:2016 git-gui.sh:2333
-msgid "Commit@@verb"
-msgstr "コミット"
-
-#: git-gui.sh:2027
-msgid "Local Merge..."
-msgstr "ローカル・マージ…"
-
-#: git-gui.sh:2032
-msgid "Abort Merge..."
-msgstr "マージ中止…"
-
-#: git-gui.sh:2044
-msgid "Push..."
-msgstr "プッシュ…"
-
-#: git-gui.sh:2055 lib/choose_repository.tcl:40
-msgid "Apple"
-msgstr "りんご"
-
-#: git-gui.sh:2058 git-gui.sh:2080 lib/about.tcl:13
-#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
-#, tcl-format
-msgid "About %s"
-msgstr "%s について"
-
-#: git-gui.sh:2062
-msgid "Preferences..."
-msgstr "設定…"
-
-#: git-gui.sh:2070 git-gui.sh:2595
-msgid "Options..."
-msgstr "オプション…"
-
-#: git-gui.sh:2076 lib/choose_repository.tcl:46
-msgid "Help"
-msgstr "ヘルプ"
-
-#: git-gui.sh:2117
-msgid "Online Documentation"
-msgstr "オンライン・ドキュメント"
-
-#: git-gui.sh:2201
-#, tcl-format
-msgid "fatal: cannot stat path %s: No such file or directory"
-msgstr ""
-"致命的: パス %s が stat できません。そのようなファイルやディレクトリはありま"
-"せん"
-
-#: git-gui.sh:2234
-msgid "Current Branch:"
-msgstr "現在のブランチ"
-
-#: git-gui.sh:2255
-msgid "Staged Changes (Will Commit)"
-msgstr "ステージングされた(コミット予定済の)変更"
-
-#: git-gui.sh:2274
-msgid "Unstaged Changes"
-msgstr "コミット予定に入っていない変更"
-
-#: git-gui.sh:2323
-msgid "Stage Changed"
-msgstr "変更をコミット予定に入れる"
-
-#: git-gui.sh:2339 lib/transport.tcl:93 lib/transport.tcl:182
-msgid "Push"
-msgstr "プッシュ"
-
-#: git-gui.sh:2369
-msgid "Initial Commit Message:"
-msgstr "最初のコミットメッセージ:"
-
-#: git-gui.sh:2370
-msgid "Amended Commit Message:"
-msgstr "訂正したコミットメッセージ:"
-
-#: git-gui.sh:2371
-msgid "Amended Initial Commit Message:"
-msgstr "訂正した最初のコミットメッセージ:"
-
-#: git-gui.sh:2372
-msgid "Amended Merge Commit Message:"
-msgstr "訂正したマージコミットメッセージ:"
-
-#: git-gui.sh:2373
-msgid "Merge Commit Message:"
-msgstr "マージコミットメッセージ:"
-
-#: git-gui.sh:2374
-msgid "Commit Message:"
-msgstr "コミットメッセージ:"
-
-#: git-gui.sh:2419 git-gui.sh:2557 lib/console.tcl:71
-msgid "Copy All"
-msgstr "全てコピー"
-
-#: git-gui.sh:2443 lib/blame.tcl:104
-msgid "File:"
-msgstr "ファイル:"
-
-#: git-gui.sh:2545
-msgid "Refresh"
-msgstr "再読み込み"
-
-#: git-gui.sh:2566
-msgid "Apply/Reverse Hunk"
-msgstr "パッチを適用/取り消す"
-
-#: git-gui.sh:2572
-msgid "Decrease Font Size"
-msgstr "フォントを小さく"
-
-#: git-gui.sh:2576
-msgid "Increase Font Size"
-msgstr "フォントを大きく"
-
-#: git-gui.sh:2581
-msgid "Show Less Context"
-msgstr "文脈を少なく"
-
-#: git-gui.sh:2588
-msgid "Show More Context"
-msgstr "文脈を多く"
-
-#: git-gui.sh:2602
-msgid "Unstage Hunk From Commit"
-msgstr "パッチをコミット予定から外す"
-
-#: git-gui.sh:2604
-msgid "Stage Hunk For Commit"
-msgstr "パッチをコミット予定に加える"
-
-#: git-gui.sh:2623
-msgid "Initializing..."
-msgstr "初期化しています…"
-
-#: git-gui.sh:2718
-#, tcl-format
-msgid ""
-"Possible environment issues exist.\n"
-"\n"
-"The following environment variables are probably\n"
-"going to be ignored by any Git subprocess run\n"
-"by %s:\n"
-"\n"
-msgstr ""
-"環境に問題がある可能性があります\n"
-"\n"
-"以下の環境変数は %s が起動する Git サブプロセスによって無視されるでしょう:\n"
-"\n"
-
-#: git-gui.sh:2748
-msgid ""
-"\n"
-"This is due to a known issue with the\n"
-"Tcl binary distributed by Cygwin."
-msgstr ""
-"\n"
-"これは Cygwin で配布されている Tcl バイナリに\n"
-"関しての既知の問題によります"
-
-#: git-gui.sh:2753
-#, tcl-format
-msgid ""
-"\n"
-"\n"
-"A good replacement for %s\n"
-"is placing values for the user.name and\n"
-"user.email settings into your personal\n"
-"~/.gitconfig file.\n"
-msgstr ""
-"\n"
-"\n"
-"個人的な ~/.gitconfig ファイル内で user.name と user.email の値を設定\n"
-"するのが、%s の良い代用となります\n"
-
-#: lib/about.tcl:25
-msgid "git-gui - a graphical user interface for Git."
-msgstr "Git のグラフィカルUI git-gui"
-
-#: lib/blame.tcl:77
-msgid "File Viewer"
-msgstr "ファイルピューワ"
-
-#: lib/blame.tcl:81
-msgid "Commit:"
-msgstr "コミット:"
-
-#: lib/blame.tcl:249
-msgid "Copy Commit"
-msgstr "コミットをコピー"
-
-#: lib/blame.tcl:369
-#, tcl-format
-msgid "Reading %s..."
-msgstr "%s を読んでいます…"
-
-#: lib/blame.tcl:473
-msgid "Loading copy/move tracking annotations..."
-msgstr "コピー・移動追跡データを読んでいます…"
-
-#: lib/blame.tcl:493
-msgid "lines annotated"
-msgstr "行を注釈しました"
-
-#: lib/blame.tcl:674
-msgid "Loading original location annotations..."
-msgstr "元位置行の注釈データを読んでいます…"
-
-#: lib/blame.tcl:677
-msgid "Annotation complete."
-msgstr "注釈完了しました"
-
-#: lib/blame.tcl:731
-msgid "Loading annotation..."
-msgstr "注釈を読み込んでいます…"
-
-#: lib/blame.tcl:787
-msgid "Author:"
-msgstr "作者:"
-
-#: lib/blame.tcl:791
-msgid "Committer:"
-msgstr "コミット者:"
-
-#: lib/blame.tcl:796
-msgid "Original File:"
-msgstr "元ファイル"
-
-#: lib/blame.tcl:910
-msgid "Originally By:"
-msgstr "原作者:"
-
-#: lib/blame.tcl:916
-msgid "In File:"
-msgstr "ファイル:"
-
-#: lib/blame.tcl:921
-msgid "Copied Or Moved Here By:"
-msgstr "複写・移動者:"
-
-#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
-msgid "Checkout Branch"
-msgstr "ブランチをチェックアウト"
-
-#: lib/branch_checkout.tcl:23
-msgid "Checkout"
-msgstr "チェックアウト"
-
-#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
-#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
-#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
-#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
-msgid "Cancel"
-msgstr "中止"
-
-#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
-msgid "Revision"
-msgstr "リビジョン"
-
-#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
-msgid "Options"
-msgstr "オプション"
-
-#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
-msgid "Fetch Tracking Branch"
-msgstr "トラッキング・ブランチをフェッチ"
-
-#: lib/branch_checkout.tcl:44
-msgid "Detach From Local Branch"
-msgstr "ローカル・ブランチから削除"
-
-#: lib/branch_create.tcl:22
-msgid "Create Branch"
-msgstr "ブランチを作成"
-
-#: lib/branch_create.tcl:27
-msgid "Create New Branch"
-msgstr "ブランチを新規作成"
-
-#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
-msgid "Create"
-msgstr "作成"
-
-#: lib/branch_create.tcl:40
-msgid "Branch Name"
-msgstr "ブランチ名"
-
-#: lib/branch_create.tcl:43
-msgid "Name:"
-msgstr "名前:"
-
-#: lib/branch_create.tcl:58
-msgid "Match Tracking Branch Name"
-msgstr "トラッキング・ブランチ名を合わせる"
-
-#: lib/branch_create.tcl:66
-msgid "Starting Revision"
-msgstr "初期リビジョン"
-
-#: lib/branch_create.tcl:72
-msgid "Update Existing Branch:"
-msgstr "既存のブランチを更新:"
-
-#: lib/branch_create.tcl:75
-msgid "No"
-msgstr "いいえ"
-
-#: lib/branch_create.tcl:80
-msgid "Fast Forward Only"
-msgstr "早送りのみ"
-
-#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
-msgid "Reset"
-msgstr "リセット"
-
-#: lib/branch_create.tcl:97
-msgid "Checkout After Creation"
-msgstr "作成してすぐチェックアウト"
-
-#: lib/branch_create.tcl:131
-msgid "Please select a tracking branch."
-msgstr "トラッキング・ブランチを選択して下さい。"
-
-#: lib/branch_create.tcl:140
-#, tcl-format
-msgid "Tracking branch %s is not a branch in the remote repository."
-msgstr "トラッキング・ブランチ %s は遠隔リポジトリのブランチではありません。"
-
-#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
-msgid "Please supply a branch name."
-msgstr "ブランチ名を指定して下さい。"
-
-#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
-#, tcl-format
-msgid "'%s' is not an acceptable branch name."
-msgstr "'%s' はブランチ名に使えません。"
-
-#: lib/branch_delete.tcl:15
-msgid "Delete Branch"
-msgstr "ブランチ削除"
-
-#: lib/branch_delete.tcl:20
-msgid "Delete Local Branch"
-msgstr "ローカル・ブランチを削除"
-
-#: lib/branch_delete.tcl:37
-msgid "Local Branches"
-msgstr "ローカル・ブランチ"
-
-#: lib/branch_delete.tcl:52
-msgid "Delete Only If Merged Into"
-msgstr "マージ済みの時のみ削除"
-
-#: lib/branch_delete.tcl:54
-msgid "Always (Do not perform merge test.)"
-msgstr "無条件(マージテストしない)"
-
-#: lib/branch_delete.tcl:103
-#, tcl-format
-msgid "The following branches are not completely merged into %s:"
-msgstr "以下のブランチは %s に完全にマージされていません:"
-
-#: lib/branch_delete.tcl:115
-msgid ""
-"Recovering deleted branches is difficult. \n"
-"\n"
-" Delete the selected branches?"
-msgstr ""
-"ブランチを削除すると元に戻すのは困難です。 \n"
-"\n"
-" 選択したブランチを削除しますか?"
-
-#: lib/branch_delete.tcl:141
-#, tcl-format
-msgid ""
-"Failed to delete branches:\n"
-"%s"
-msgstr ""
-"以下のブランチを削除できません:\n"
-"%s"
-
-#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
-msgid "Rename Branch"
-msgstr "ブランチの名前変更"
-
-#: lib/branch_rename.tcl:26
-msgid "Rename"
-msgstr "名前変更"
-
-#: lib/branch_rename.tcl:36
-msgid "Branch:"
-msgstr "ブランチ:"
-
-#: lib/branch_rename.tcl:39
-msgid "New Name:"
-msgstr "新しい名前:"
-
-#: lib/branch_rename.tcl:75
-msgid "Please select a branch to rename."
-msgstr "名前を変更するブランチを選んで下さい。"
-
-#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
-#, tcl-format
-msgid "Branch '%s' already exists."
-msgstr "'%s'というブランチは既に存在します。"
-
-#: lib/branch_rename.tcl:117
-#, tcl-format
-msgid "Failed to rename '%s'."
-msgstr "'%s'の名前変更に失敗しました。"
-
-#: lib/browser.tcl:17
-msgid "Starting..."
-msgstr "起動中…"
-
-#: lib/browser.tcl:26
-msgid "File Browser"
-msgstr "ファイル・ブラウザ"
-
-#: lib/browser.tcl:125 lib/browser.tcl:142
-#, tcl-format
-msgid "Loading %s..."
-msgstr "%s をロード中…"
-
-#: lib/browser.tcl:186
-msgid "[Up To Parent]"
-msgstr "[上位フォルダへ]"
-
-#: lib/browser.tcl:266 lib/browser.tcl:272
-msgid "Browse Branch Files"
-msgstr "現在のブランチのファイルを見る"
-
-#: lib/browser.tcl:277 lib/choose_repository.tcl:391
-#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
-#: lib/choose_repository.tcl:989
-msgid "Browse"
-msgstr "ブラウズ"
-
-#: lib/checkout_op.tcl:79
-#, tcl-format
-msgid "Fetching %s from %s"
-msgstr "%s から %s をフェッチしています"
-
-#: lib/checkout_op.tcl:127
-#, tcl-format
-msgid "fatal: Cannot resolve %s"
-msgstr "致命的エラー: %s を解決できません"
-
-#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
-msgid "Close"
-msgstr "閉じる"
-
-#: lib/checkout_op.tcl:169
-#, tcl-format
-msgid "Branch '%s' does not exist."
-msgstr "ブランチ'%s'は存在しません。"
-
-#: lib/checkout_op.tcl:206
-#, tcl-format
-msgid ""
-"Branch '%s' already exists.\n"
-"\n"
-"It cannot fast-forward to %s.\n"
-"A merge is required."
-msgstr ""
-"ブランチ '%s' は既に存在します。\n"
-"\n"
-"%s に早送りできません。\n"
-"マージが必要です。"
-
-#: lib/checkout_op.tcl:220
-#, tcl-format
-msgid "Merge strategy '%s' not supported."
-msgstr "'%s' マージ戦略はサポートされていません。"
-
-#: lib/checkout_op.tcl:239
-#, tcl-format
-msgid "Failed to update '%s'."
-msgstr "'%s' の更新に失敗しました。"
-
-#: lib/checkout_op.tcl:251
-msgid "Staging area (index) is already locked."
-msgstr "インデックスは既にロックされています。"
-
-#: lib/checkout_op.tcl:266
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before the current branch can be changed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"最後にスキャンした状態はリポジトリの状態と合致しません。\n"
-"\n"
-"最後にスキャンして以後、別の Git プログラムがリポジトリを変更しています。現在"
-"のブランチを変更する前に、再スキャンが必要です。\n"
-"\n"
-"自動的に再スキャンを開始します。\n"
-
-#: lib/checkout_op.tcl:322
-#, tcl-format
-msgid "Updating working directory to '%s'..."
-msgstr "作業ディレクトリを '%s' に更新しています…"
-
-#: lib/checkout_op.tcl:353
-#, tcl-format
-msgid "Aborted checkout of '%s' (file level merging is required)."
-msgstr "'%s' のチェックアウトを中止しました(ファイル毎のマージが必要です)。"
-
-#: lib/checkout_op.tcl:354
-msgid "File level merge required."
-msgstr "ファイル毎のマージが必要です。"
-
-#: lib/checkout_op.tcl:358
-#, tcl-format
-msgid "Staying on branch '%s'."
-msgstr "ブランチ '%s' に滞まります。"
-
-#: lib/checkout_op.tcl:429
-msgid ""
-"You are no longer on a local branch.\n"
-"\n"
-"If you wanted to be on a branch, create one now starting from 'This Detached "
-"Checkout'."
-msgstr ""
-"ローカル・ブランチから離れます。\n"
-"\n"
-"ブランチ上に滞まりたいときは、この「分離されたチェックアウト」から新規ブラン"
-"チを開始してください。"
-
-#: lib/checkout_op.tcl:446
-#, tcl-format
-msgid "Checked out '%s'."
-msgstr "'%s' をチェックアウトしました"
-
-#: lib/checkout_op.tcl:478
-#, tcl-format
-msgid "Resetting '%s' to '%s' will lose the following commits:"
-msgstr "'%s' を '%s' にリセットすると、以下のコミットが失なわれます:"
-
-#: lib/checkout_op.tcl:500
-msgid "Recovering lost commits may not be easy."
-msgstr "失なわれたコミットを回復するのは簡単ではありません。"
-
-#: lib/checkout_op.tcl:505
-#, tcl-format
-msgid "Reset '%s'?"
-msgstr "'%s' をリセットしますか?"
-
-#: lib/checkout_op.tcl:510 lib/merge.tcl:164
-msgid "Visualize"
-msgstr "可視化"
-
-#: lib/checkout_op.tcl:578
-#, tcl-format
-msgid ""
-"Failed to set current branch.\n"
-"\n"
-"This working directory is only partially switched.  We successfully updated "
-"your files, but failed to update an internal Git file.\n"
-"\n"
-"This should not have occurred.  %s will now close and give up."
-msgstr ""
-"現在のブランチを設定できません。\n"
-"\n"
-"作業ディレクトリは部分的にしか切り替わっていません。ファイルの更新には成功し"
-"ましたが、 Git の内部データを更新できませんでした。\n"
-"起こるはずのないエラーです。あきらめて %s を終了します。"
-
-#: lib/choose_font.tcl:39
-msgid "Select"
-msgstr "選択"
-
-#: lib/choose_font.tcl:53
-msgid "Font Family"
-msgstr "フォント・ファミリー"
-
-#: lib/choose_font.tcl:73
-msgid "Font Size"
-msgstr "フォントの大きさ"
-
-#: lib/choose_font.tcl:90
-msgid "Font Example"
-msgstr "フォント・サンプル"
-
-#: lib/choose_font.tcl:101
-msgid ""
-"This is example text.\n"
-"If you like this text, it can be your font."
-msgstr ""
-"これはサンプル文です。\n"
-"このフォントが気に入ればお使いになれます。"
-
-#: lib/choose_repository.tcl:27
-msgid "Git Gui"
-msgstr "Git GUI"
-
-#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
-msgid "Create New Repository"
-msgstr "新しいリポジトリを作る"
-
-#: lib/choose_repository.tcl:86
-msgid "New..."
-msgstr "新規…"
-
-#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
-msgid "Clone Existing Repository"
-msgstr "既存リポジトリを複製する"
-
-#: lib/choose_repository.tcl:99
-msgid "Clone..."
-msgstr "複製…"
-
-#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
-msgid "Open Existing Repository"
-msgstr "既存リポジトリを開く"
-
-#: lib/choose_repository.tcl:112
-msgid "Open..."
-msgstr "開く…"
-
-#: lib/choose_repository.tcl:125
-msgid "Recent Repositories"
-msgstr "最近使ったリポジトリ"
-
-#: lib/choose_repository.tcl:131
-msgid "Open Recent Repository:"
-msgstr "最近使ったリポジトリを開く"
-
-#: lib/choose_repository.tcl:294
-#, tcl-format
-msgid "Location %s already exists."
-msgstr "'%s' は既に存在します。"
-
-#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
-#: lib/choose_repository.tcl:314
-#, tcl-format
-msgid "Failed to create repository %s:"
-msgstr "リポジトリ %s を作製できません:"
-
-#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
-msgid "Directory:"
-msgstr "ディレクトリ:"
-
-#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
-#: lib/choose_repository.tcl:1013
-msgid "Git Repository"
-msgstr "GIT リポジトリ"
-
-#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
-#, tcl-format
-msgid "Directory %s already exists."
-msgstr "ディレクトリ '%s' は既に存在します。"
-
-#: lib/choose_repository.tcl:442
-#, tcl-format
-msgid "File %s already exists."
-msgstr "ファイル '%s' は既に存在します。"
-
-#: lib/choose_repository.tcl:463
-msgid "Clone"
-msgstr "複製"
-
-#: lib/choose_repository.tcl:476
-msgid "URL:"
-msgstr "URL:"
-
-#: lib/choose_repository.tcl:496
-msgid "Clone Type:"
-msgstr "複製方式:"
-
-#: lib/choose_repository.tcl:502
-msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
-msgstr "標準(高速・中冗長度・ハードリンク)"
-
-#: lib/choose_repository.tcl:508
-msgid "Full Copy (Slower, Redundant Backup)"
-msgstr "全複写(低速・冗長バックアップ)"
-
-#: lib/choose_repository.tcl:514
-msgid "Shared (Fastest, Not Recommended, No Backup)"
-msgstr "共有(最高速・非推奨・バックアップ無し)"
-
-#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
-#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
-#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
-#, tcl-format
-msgid "Not a Git repository: %s"
-msgstr "Git リポジトリではありません: %s"
-
-#: lib/choose_repository.tcl:586
-msgid "Standard only available for local repository."
-msgstr "標準方式は同一計算機上のリポジトリにのみ使えます。"
-
-#: lib/choose_repository.tcl:590
-msgid "Shared only available for local repository."
-msgstr "共有方式は同一計算機上のリポジトリにのみ使えます。"
-
-#: lib/choose_repository.tcl:617
-msgid "Failed to configure origin"
-msgstr "origin を設定できませんでした"
-
-#: lib/choose_repository.tcl:629
-msgid "Counting objects"
-msgstr "オブジェクトを数えています"
-
-#: lib/choose_repository.tcl:630
-msgid "buckets"
-msgstr "バケツ"
-
-#: lib/choose_repository.tcl:654
-#, tcl-format
-msgid "Unable to copy objects/info/alternates: %s"
-msgstr "objects/info/alternates を複写できません: %s"
-
-#: lib/choose_repository.tcl:690
-#, tcl-format
-msgid "Nothing to clone from %s."
-msgstr "%s から複製する内容はありません"
-
-#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
-#: lib/choose_repository.tcl:918
-msgid "The 'master' branch has not been initialized."
-msgstr "'master' ブランチが初期化されていません"
-
-#: lib/choose_repository.tcl:705
-msgid "Hardlinks are unavailable.  Falling back to copying."
-msgstr "ハードリンクが作れないので、コピーします"
-
-#: lib/choose_repository.tcl:717
-#, tcl-format
-msgid "Cloning from %s"
-msgstr "%s から複製しています"
-
-#: lib/choose_repository.tcl:748
-msgid "Copying objects"
-msgstr "オブジェクトを複写しています"
-
-#: lib/choose_repository.tcl:749
-msgid "KiB"
-msgstr "KiB"
-
-#: lib/choose_repository.tcl:773
-#, tcl-format
-msgid "Unable to copy object: %s"
-msgstr "オブジェクトを複写できません: %s"
-
-#: lib/choose_repository.tcl:783
-msgid "Linking objects"
-msgstr "オブジェクトを連結しています"
-
-#: lib/choose_repository.tcl:784
-msgid "objects"
-msgstr "オブジェクト"
-
-#: lib/choose_repository.tcl:792
-#, tcl-format
-msgid "Unable to hardlink object: %s"
-msgstr "オブジェクトをハードリンクできません: %s"
-
-#: lib/choose_repository.tcl:847
-msgid "Cannot fetch branches and objects.  See console output for details."
-msgstr "ブランチやオブジェクトを取得できません。コンソール出力を見て下さい"
-
-#: lib/choose_repository.tcl:858
-msgid "Cannot fetch tags.  See console output for details."
-msgstr "タグを取得できません。コンソール出力を見て下さい"
-
-#: lib/choose_repository.tcl:882
-msgid "Cannot determine HEAD.  See console output for details."
-msgstr "HEAD を確定できません。コンソール出力を見て下さい"
-
-#: lib/choose_repository.tcl:891
-#, tcl-format
-msgid "Unable to cleanup %s"
-msgstr "%s を掃除できません"
-
-#: lib/choose_repository.tcl:897
-msgid "Clone failed."
-msgstr "複写に失敗しました。"
-
-#: lib/choose_repository.tcl:904
-msgid "No default branch obtained."
-msgstr "デフォールト・ブランチが取得されませんでした"
-
-#: lib/choose_repository.tcl:915
-#, tcl-format
-msgid "Cannot resolve %s as a commit."
-msgstr "%s をコミットとして解釈できません"
-
-#: lib/choose_repository.tcl:927
-msgid "Creating working directory"
-msgstr "作業ディレクトリを作成しています"
-
-#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
-#: lib/index.tcl:193
-msgid "files"
-msgstr "ファイル"
-
-#: lib/choose_repository.tcl:957
-msgid "Initial file checkout failed."
-msgstr "初期チェックアウトに失敗しました"
-
-#: lib/choose_repository.tcl:973
-msgid "Open"
-msgstr "開く"
-
-#: lib/choose_repository.tcl:983
-msgid "Repository:"
-msgstr "リポジトリ:"
-
-#: lib/choose_repository.tcl:1033
-#, tcl-format
-msgid "Failed to open repository %s:"
-msgstr "リポジトリ %s を開けません:"
-
-#: lib/choose_rev.tcl:53
-msgid "This Detached Checkout"
-msgstr "分離されたチェックアウト"
-
-#: lib/choose_rev.tcl:60
-msgid "Revision Expression:"
-msgstr "リビジョン式:"
-
-#: lib/choose_rev.tcl:74
-msgid "Local Branch"
-msgstr "ローカル・ブランチ"
-
-#: lib/choose_rev.tcl:79
-msgid "Tracking Branch"
-msgstr "トラッキング・ブランチ"
-
-#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
-msgid "Tag"
-msgstr "タグ"
-
-#: lib/choose_rev.tcl:317
-#, tcl-format
-msgid "Invalid revision: %s"
-msgstr "無効なリビジョン: %s"
-
-#: lib/choose_rev.tcl:338
-msgid "No revision selected."
-msgstr "リビジョンが未選択です。"
-
-#: lib/choose_rev.tcl:346
-msgid "Revision expression is empty."
-msgstr "リビジョン式が空です。"
-
-#: lib/choose_rev.tcl:530
-msgid "Updated"
-msgstr "更新しました"
-
-#: lib/choose_rev.tcl:558
-msgid "URL"
-msgstr "URL"
-
-#: lib/commit.tcl:9
-msgid ""
-"There is nothing to amend.\n"
-"\n"
-"You are about to create the initial commit.  There is no commit before this "
-"to amend.\n"
-msgstr ""
-"訂正するコミットがそもそもありません。\n"
-"\n"
-"これから作るのは最初のコミットです。その前にはまだ訂正するようなコミットはあ"
-"りません。\n"
-
-#: lib/commit.tcl:18
-msgid ""
-"Cannot amend while merging.\n"
-"\n"
-"You are currently in the middle of a merge that has not been fully "
-"completed.  You cannot amend the prior commit unless you first abort the "
-"current merge activity.\n"
-msgstr ""
-"マージ中にコミットの訂正はできません。\n"
-"\n"
-"現在はまだマージの途中です。先にこのマージを中止しないと、前のコミットの訂正"
-"はできません\n"
-
-#: lib/commit.tcl:49
-msgid "Error loading commit data for amend:"
-msgstr "訂正するコミットのデータを読めません:"
-
-#: lib/commit.tcl:76
-msgid "Unable to obtain your identity:"
-msgstr "ユーザの正体を確認できません:"
-
-#: lib/commit.tcl:81
-msgid "Invalid GIT_COMMITTER_IDENT:"
-msgstr "GIT_COMMITTER_IDENT が無効です:"
-
-#: lib/commit.tcl:133
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before another commit can be created.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"最後にスキャンした状態はリポジトリの状態と合致しません。\n"
-"\n"
-"最後にスキャンして以後、別の Git プログラムがリポジトリを変更しています。新し"
-"くコミットする前に、再スキャンが必要です。\n"
-"\n"
-"自動的に再スキャンを開始します。\n"
-
-#: lib/commit.tcl:154
-#, tcl-format
-msgid ""
-"Unmerged files cannot be committed.\n"
-"\n"
-"File %s has merge conflicts.  You must resolve them and stage the file "
-"before committing.\n"
-msgstr ""
-"マージしていないファイルはコミットできません。\n"
-"\n"
-"ファイル %s にはマージ衝突が残っています。まず解決してコミット予定に加える必"
-"要があります。\n"
-
-#: lib/commit.tcl:162
-#, tcl-format
-msgid ""
-"Unknown file state %s detected.\n"
-"\n"
-"File %s cannot be committed by this program.\n"
-msgstr ""
-"不明なファイル状態 %s です。\n"
-"\n"
-"ファイル %s は本プログラムではコミットできません。\n"
-
-#: lib/commit.tcl:170
-msgid ""
-"No changes to commit.\n"
-"\n"
-"You must stage at least 1 file before you can commit.\n"
-msgstr ""
-"コミットする変更がありません。\n"
-"\n"
-"最低一つの変更をコミット予定に加えてからコミットして下さい。\n"
-
-#: lib/commit.tcl:183
-msgid ""
-"Please supply a commit message.\n"
-"\n"
-"A good commit message has the following format:\n"
-"\n"
-"- First line: Describe in one sentence what you did.\n"
-"- Second line: Blank\n"
-"- Remaining lines: Describe why this change is good.\n"
-msgstr ""
-"コミット・メッセージを入力して下さい。\n"
-"\n"
-"正しいコミット・メッセージは:\n"
-"\n"
-"- 第1行: 何をしたか、を1行で要約。\n"
-"- 第2行: 空白\n"
-"- 残りの行: なぜ、この変更が良い変更か、の説明。\n"
-
-#: lib/commit.tcl:257
-msgid "write-tree failed:"
-msgstr "write-tree が失敗しました:"
-
-#: lib/commit.tcl:275
-#, tcl-format
-msgid "Commit %s appears to be corrupt"
-msgstr "コミット %s は壊れています"
-
-#: lib/commit.tcl:279
-msgid ""
-"No changes to commit.\n"
-"\n"
-"No files were modified by this commit and it was not a merge commit.\n"
-"\n"
-"A rescan will be automatically started now.\n"
-msgstr ""
-"コミットする変更がありません。\n"
-"\n"
-"マージでなく、また、一つも変更点がありません。\n"
-"\n"
-"自動的に再スキャンを開始します。\n"
-
-#: lib/commit.tcl:286
-msgid "No changes to commit."
-msgstr "コミットする変更がありません。"
-
-#: lib/commit.tcl:303
-#, tcl-format
-msgid "warning: Tcl does not support encoding '%s'."
-msgstr "警告: Tcl はエンコーディング '%s' をサポートしていません"
-
-#: lib/commit.tcl:317
-msgid "commit-tree failed:"
-msgstr "commit-tree が失敗しました:"
-
-#: lib/commit.tcl:339
-msgid "update-ref failed:"
-msgstr "update-ref が失敗しました:"
-
-#: lib/commit.tcl:430
-#, tcl-format
-msgid "Created commit %s: %s"
-msgstr "コミット %s を作成しました: %s"
-
-#: lib/console.tcl:57
-msgid "Working... please wait..."
-msgstr "実行中…お待ち下さい…"
-
-#: lib/console.tcl:183
-msgid "Success"
-msgstr "成功"
-
-#: lib/console.tcl:196
-msgid "Error: Command Failed"
-msgstr "エラー: コマンドが失敗しました"
-
-#: lib/database.tcl:43
-msgid "Number of loose objects"
-msgstr "ばらばらなオブジェクトの数"
-
-#: lib/database.tcl:44
-msgid "Disk space used by loose objects"
-msgstr "ばらばらなオブジェクトの使用するディスク量"
-
-#: lib/database.tcl:45
-msgid "Number of packed objects"
-msgstr "パックされたオブジェクトの数"
-
-#: lib/database.tcl:46
-msgid "Number of packs"
-msgstr "パックの数"
-
-#: lib/database.tcl:47
-msgid "Disk space used by packed objects"
-msgstr "パックされたオブジェクトの使用するディスク量"
-
-#: lib/database.tcl:48
-msgid "Packed objects waiting for pruning"
-msgstr "パックに存在するので捨てて良いオブジェクトの数"
-
-#: lib/database.tcl:49
-msgid "Garbage files"
-msgstr "ゴミファイル"
-
-#: lib/database.tcl:72
-msgid "Compressing the object database"
-msgstr "データベース圧縮"
-
-#: lib/database.tcl:83
-msgid "Verifying the object database with fsck-objects"
-msgstr "fsck-objects でオブジェクト・データベースを検証しています"
-
-#: lib/database.tcl:108
-#, tcl-format
-msgid ""
-"This repository currently has approximately %i loose objects.\n"
-"\n"
-"To maintain optimal performance it is strongly recommended that you compress "
-"the database when more than %i loose objects exist.\n"
-"\n"
-"Compress the database now?"
-msgstr ""
-"このリポジトリにはおおよそ %i 個の個別オブジェクトがあります\n"
-"\n"
-"最適な性能を保つために、%i 個以上の個別オブジェクトを作る毎にデータベースを圧"
-"縮することを推奨します\n"
-"\n"
-"データベースを圧縮しますか?"
-
-#: lib/date.tcl:25
-#, tcl-format
-msgid "Invalid date from Git: %s"
-msgstr "Git から出た無効な日付: %s"
-
-#: lib/diff.tcl:42
-#, tcl-format
-msgid ""
-"No differences detected.\n"
-"\n"
-"%s has no changes.\n"
-"\n"
-"The modification date of this file was updated by another application, but "
-"the content within the file was not changed.\n"
-"\n"
-"A rescan will be automatically started to find other files which may have "
-"the same state."
-msgstr ""
-"変更がありません。\n"
-"\n"
-"%s には変更がありません。\n"
-"\n"
-"このファイルの変更時刻は他のアプリケーションによって更新されていますがファイ"
-"ル内容には変更がありません。\n"
-"\n"
-"同様な状態のファイルを探すために、自動的に再スキャンを開始します。"
-
-#: lib/diff.tcl:81
-#, tcl-format
-msgid "Loading diff of %s..."
-msgstr "%s の変更点をロード中…"
-
-#: lib/diff.tcl:114 lib/diff.tcl:184
-#, tcl-format
-msgid "Unable to display %s"
-msgstr "%s を表示できません"
-
-#: lib/diff.tcl:115
-msgid "Error loading file:"
-msgstr "ファイルを読む際のエラーです:"
-
-#: lib/diff.tcl:122
-msgid "Git Repository (subproject)"
-msgstr "Git リポジトリ(サブプロジェクト)"
-
-#: lib/diff.tcl:134
-msgid "* Binary file (not showing content)."
-msgstr "* バイナリファイル(内容は表示しません)"
-
-#: lib/diff.tcl:185
-msgid "Error loading diff:"
-msgstr "diff を読む際のエラーです:"
-
-#: lib/diff.tcl:302
-msgid "Failed to unstage selected hunk."
-msgstr "選択されたパッチをコミット予定から外せません。"
-
-#: lib/diff.tcl:309
-msgid "Failed to stage selected hunk."
-msgstr "選択されたパッチをコミット予定に加えられません。"
-
-#: lib/error.tcl:12 lib/error.tcl:102
-msgid "error"
-msgstr "エラー"
-
-#: lib/error.tcl:28
-msgid "warning"
-msgstr "警告"
-
-#: lib/error.tcl:81
-msgid "You must correct the above errors before committing."
-msgstr "コミットする前に、以上のエラーを修正して下さい"
-
-#: lib/index.tcl:6
-msgid "Unable to unlock the index."
-msgstr "インデックスをロックできません"
-
-#: lib/index.tcl:15
-msgid "Index Error"
-msgstr "索引エラー"
-
-#: lib/index.tcl:21
-msgid ""
-"Updating the Git index failed.  A rescan will be automatically started to "
-"resynchronize git-gui."
-msgstr "GIT インデックスの更新が失敗しました。git-gui と同期をとるために再スキャンします。"
-
-#: lib/index.tcl:27
-msgid "Continue"
-msgstr "続行"
-
-#: lib/index.tcl:31
-msgid "Unlock Index"
-msgstr "インデックスのロック解除"
-
-#: lib/index.tcl:282
-#, tcl-format
-msgid "Unstaging %s from commit"
-msgstr "コミットから '%s' を降ろす"
-
-#: lib/index.tcl:326
-#, tcl-format
-msgid "Adding %s"
-msgstr "コミットに %s を加えています"
-
-#: lib/index.tcl:381
-#, tcl-format
-msgid "Revert changes in file %s?"
-msgstr "ファイル %s にした変更を元に戻しますか?"
-
-#: lib/index.tcl:383
-#, tcl-format
-msgid "Revert changes in these %i files?"
-msgstr "これら %i 個のファイルにした変更を元に戻しますか?"
-
-#: lib/index.tcl:389
-msgid "Any unstaged changes will be permanently lost by the revert."
-msgstr "変更を元に戻すとコミット予定していない変更は全て失われます。"
-
-#: lib/index.tcl:392
-msgid "Do Nothing"
-msgstr "何もしない"
-
-#: lib/merge.tcl:13
-msgid ""
-"Cannot merge while amending.\n"
-"\n"
-"You must finish amending this commit before starting any type of merge.\n"
-msgstr ""
-"訂正中にはマージできません。\n"
-"\n"
-"訂正処理を完了するまでは新たにマージを開始できません。\n"
-
-#: lib/merge.tcl:27
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before a merge can be performed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"最後にスキャンした状態はリポジトリの状態と合致しません。\n"
-"\n"
-"最後にスキャンして以後、別の Git プログラムがリポジトリを変更しています。マー"
-"ジを開始する前に、再スキャンが必要です。\n"
-"\n"
-"自動的に再スキャンを開始します。\n"
-
-#: lib/merge.tcl:44
-#, tcl-format
-msgid ""
-"You are in the middle of a conflicted merge.\n"
-"\n"
-"File %s has merge conflicts.\n"
-"\n"
-"You must resolve them, stage the file, and commit to complete the current "
-"merge.  Only then can you begin another merge.\n"
-msgstr ""
-"衝突のあったマージの途中です。\n"
-"\n"
-"ファイル %s にはマージ中の衝突が残っています。\n"
-"\n"
-"このファイルの衝突を解決し、コミット予定に加えて、コミットすることでマージを"
-"完了します。そうやって始めて、新たなマージを開始できるようになります。\n"
-
-#: lib/merge.tcl:54
-#, tcl-format
-msgid ""
-"You are in the middle of a change.\n"
-"\n"
-"File %s is modified.\n"
-"\n"
-"You should complete the current commit before starting a merge.  Doing so "
-"will help you abort a failed merge, should the need arise.\n"
-msgstr ""
-"変更の途中です。\n"
-"\n"
-"ファイル %s は変更中です。\n"
-"\n"
-"現在のコミットを完了してからマージを開始して下さい。そうする方がマージに失敗"
-"したときの回復が楽です。\n"
-
-#: lib/merge.tcl:106
-#, tcl-format
-msgid "%s of %s"
-msgstr "%s の %s ブランチ"
-
-#: lib/merge.tcl:119
-#, tcl-format
-msgid "Merging %s and %s"
-msgstr "%s と %s をマージします"
-
-#: lib/merge.tcl:131
-msgid "Merge completed successfully."
-msgstr "マージが完了しました"
-
-#: lib/merge.tcl:133
-msgid "Merge failed.  Conflict resolution is required."
-msgstr "マージが失敗しました。衝突の解決が必要です。"
-
-#: lib/merge.tcl:158
-#, tcl-format
-msgid "Merge Into %s"
-msgstr "%s にマージ"
-
-#: lib/merge.tcl:177
-msgid "Revision To Merge"
-msgstr "マージするリビジョン"
-
-#: lib/merge.tcl:212
-msgid ""
-"Cannot abort while amending.\n"
-"\n"
-"You must finish amending this commit.\n"
-msgstr ""
-"訂正中には中止できません。\n"
-"\n"
-"まず今のコミット訂正を完了させて下さい。\n"
-
-#: lib/merge.tcl:222
-msgid ""
-"Abort merge?\n"
-"\n"
-"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with aborting the current merge?"
-msgstr ""
-"マージを中断しますか?\n"
-"\n"
-"現在のマージを中断すると、コミットしていない全ての変更が失われます。\n"
-"\n"
-"マージを中断してよろしいですか?"
-
-#: lib/merge.tcl:228
-msgid ""
-"Reset changes?\n"
-"\n"
-"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with resetting the current changes?"
-msgstr ""
-"変更点をリセットしますか?\n"
-"\n"
-"変更点をリセットすると、コミットしていない全ての変更が失われます。\n"
-"\n"
-"リセットしてよろしいですか?"
-
-#: lib/merge.tcl:239
-msgid "Aborting"
-msgstr "中断しています"
-
-#: lib/merge.tcl:266
-msgid "Abort failed."
-msgstr "中断に失敗しました。"
-
-#: lib/merge.tcl:268
-msgid "Abort completed.  Ready."
-msgstr "中断完了。"
-
-#: lib/option.tcl:82
-msgid "Restore Defaults"
-msgstr "既定値に戻す"
-
-#: lib/option.tcl:86
-msgid "Save"
-msgstr "保存"
-
-#: lib/option.tcl:96
-#, tcl-format
-msgid "%s Repository"
-msgstr "%s リポジトリ"
-
-#: lib/option.tcl:97
-msgid "Global (All Repositories)"
-msgstr "大域(全てのリポジトリ)"
-
-#: lib/option.tcl:103
-msgid "User Name"
-msgstr "ユーザ名"
-
-#: lib/option.tcl:104
-msgid "Email Address"
-msgstr "電子メールアドレス"
-
-#: lib/option.tcl:106
-msgid "Summarize Merge Commits"
-msgstr "マージコミットの要約"
-
-#: lib/option.tcl:107
-msgid "Merge Verbosity"
-msgstr "マージの冗長度"
-
-#: lib/option.tcl:108
-msgid "Show Diffstat After Merge"
-msgstr "マージ後に diffstat を表示"
-
-#: lib/option.tcl:110
-msgid "Trust File Modification Timestamps"
-msgstr "ファイル変更時刻を信頼する"
-
-#: lib/option.tcl:111
-msgid "Prune Tracking Branches During Fetch"
-msgstr "フェッチ中にトラッキングブランチを刈る"
-
-#: lib/option.tcl:112
-msgid "Match Tracking Branches"
-msgstr "トラッキングブランチを合わせる"
-
-#: lib/option.tcl:113
-msgid "Number of Diff Context Lines"
-msgstr "diff の文脈行数"
-
-#: lib/option.tcl:114
-msgid "New Branch Name Template"
-msgstr "新しいブランチ名のテンプレート"
-
-#: lib/option.tcl:176
-msgid "Change Font"
-msgstr "フォントを変更"
-
-#: lib/option.tcl:180
-#, tcl-format
-msgid "Choose %s"
-msgstr "%s を選択"
-
-#: lib/option.tcl:186
-msgid "pt."
-msgstr "ポイント"
-
-#: lib/option.tcl:200
-msgid "Preferences"
-msgstr "設定"
-
-#: lib/option.tcl:235
-msgid "Failed to completely save options:"
-msgstr "完全にオプションを保存できません:"
-
-#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
-msgid "Delete Remote Branch"
-msgstr "リモート・ブランチを削除"
-
-#: lib/remote_branch_delete.tcl:47
-msgid "From Repository"
-msgstr "元のリポジトリ"
-
-#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
-msgid "Remote:"
-msgstr "リモート:"
-
-#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
-msgid "Arbitrary URL:"
-msgstr "任意の URL:"
-
-#: lib/remote_branch_delete.tcl:84
-msgid "Branches"
-msgstr "ブランチ"
-
-#: lib/remote_branch_delete.tcl:109
-msgid "Delete Only If"
-msgstr "条件付で削除"
-
-#: lib/remote_branch_delete.tcl:111
-msgid "Merged Into:"
-msgstr "マージ先:"
-
-#: lib/remote_branch_delete.tcl:119
-msgid "Always (Do not perform merge checks)"
-msgstr "無条件(マージ検査をしない)"
-
-#: lib/remote_branch_delete.tcl:152
-msgid "A branch is required for 'Merged Into'."
-msgstr "'マージ先' にはブランチが必要です。"
-
-#: lib/remote_branch_delete.tcl:184
-#, tcl-format
-msgid ""
-"The following branches are not completely merged into %s:\n"
-"\n"
-" - %s"
-msgstr ""
-"以下のブランチは %s に完全にマージされていません:\n"
-"\n"
-" - %s"
-
-#: lib/remote_branch_delete.tcl:189
-#, tcl-format
-msgid ""
-"One or more of the merge tests failed because you have not fetched the "
-"necessary commits.  Try fetching from %s first."
-msgstr ""
-"必要なコミットが不足しているために、マージ検査が失敗しました。まず %s から"
-"フェッチして下さい。"
-
-#: lib/remote_branch_delete.tcl:207
-msgid "Please select one or more branches to delete."
-msgstr "削除するブランチを選択して下さい。"
-
-#: lib/remote_branch_delete.tcl:216
-msgid ""
-"Recovering deleted branches is difficult.\n"
-"\n"
-"Delete the selected branches?"
-msgstr ""
-"削除したブランチを回復するのは困難です。\n"
-"\n"
-"選択したブランチを削除して良いですか?"
-
-#: lib/remote_branch_delete.tcl:226
-#, tcl-format
-msgid "Deleting branches from %s"
-msgstr "%s からブランチを削除しています。"
-
-#: lib/remote_branch_delete.tcl:286
-msgid "No repository selected."
-msgstr "リポジトリが選択されていません。"
-
-#: lib/remote_branch_delete.tcl:291
-#, tcl-format
-msgid "Scanning %s..."
-msgstr "%s をスキャンしています…"
-
-#: lib/remote.tcl:165
-msgid "Prune from"
-msgstr "から刈込む…"
-
-#: lib/remote.tcl:170
-msgid "Fetch from"
-msgstr "取得元"
-
-#: lib/remote.tcl:213
-msgid "Push to"
-msgstr "プッシュ先"
-
-#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
-msgid "Cannot write shortcut:"
-msgstr "ショートカットが書けません:"
-
-#: lib/shortcut.tcl:136
-msgid "Cannot write icon:"
-msgstr "アイコンが書けません:"
-
-#: lib/status_bar.tcl:83
-#, tcl-format
-msgid "%s ... %*i of %*i %s (%3i%%)"
-msgstr "%1$s ... %4$*i %6$s 中の %2$*i (%7$3i%%)"
-
-#: lib/transport.tcl:6
-#, tcl-format
-msgid "fetch %s"
-msgstr "%s を取得"
-
-#: lib/transport.tcl:7
-#, tcl-format
-msgid "Fetching new changes from %s"
-msgstr "%s から新しい変更をフェッチしています"
-
-#: lib/transport.tcl:18
-#, tcl-format
-msgid "remote prune %s"
-msgstr "遠隔刈込 %s"
-
-#: lib/transport.tcl:19
-#, tcl-format
-msgid "Pruning tracking branches deleted from %s"
-msgstr "%s から削除されたトラッキング・ブランチを刈っています"
-
-#: lib/transport.tcl:25 lib/transport.tcl:71
-#, tcl-format
-msgid "push %s"
-msgstr "%s をプッシュ"
-
-#: lib/transport.tcl:26
-#, tcl-format
-msgid "Pushing changes to %s"
-msgstr "%s へ変更をプッシュしています"
-
-#: lib/transport.tcl:72
-#, tcl-format
-msgid "Pushing %s %s to %s"
-msgstr "%3$s へ %1$s %2$s をプッシュしています"
-
-#: lib/transport.tcl:89
-msgid "Push Branches"
-msgstr "ブランチをプッシュ"
-
-#: lib/transport.tcl:103
-msgid "Source Branches"
-msgstr "元のブランチ"
-
-#: lib/transport.tcl:120
-msgid "Destination Repository"
-msgstr "送り先リポジトリ"
-
-#: lib/transport.tcl:158
-msgid "Transfer Options"
-msgstr "通信オプション"
-
-#: lib/transport.tcl:160
-msgid "Force overwrite existing branch (may discard changes)"
-msgstr "既存ブランチを上書き(変更を破棄する可能性があります)"
-
-#: lib/transport.tcl:164
-msgid "Use thin pack (for slow network connections)"
-msgstr "Thin Pack を使う(遅いネットワーク接続)"
-
-#: lib/transport.tcl:168
-msgid "Include tags"
-msgstr "タグを含める"
diff --git a/po/po2msg.sh b/po/po2msg.sh
deleted file mode 100644 (file)
index c63248e..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-#!/bin/sh
-# Tcl ignores the next line -*- tcl -*- \
-exec tclsh "$0" -- "$@"
-
-# This is a really stupid program, which serves as an alternative to
-# msgfmt.  It _only_ translates to Tcl mode, does _not_ validate the
-# input, and does _not_ output any statistics.
-
-proc u2a {s} {
-       set res ""
-       foreach i [split $s ""] {
-               scan $i %c c
-               if {$c<128} {
-                       # escape '[', '\' and ']'
-                       if {$c == 0x5b || $c == 0x5d} {
-                               append res "\\"
-                       }
-                       append res $i
-               } else {
-                       append res \\u[format %04.4x $c]
-               }
-       }
-       return $res
-}
-
-set output_directory "."
-set lang "dummy"
-set files [list]
-set show_statistics 0
-
-# parse options
-for {set i 0} {$i < $argc} {incr i} {
-       set arg [lindex $argv $i]
-       if {$arg == "--statistics"} {
-               incr show_statistics
-               continue
-       }
-       if {$arg == "--tcl"} {
-               # we know
-               continue
-       }
-       if {$arg == "-l"} {
-               incr i
-               set lang [lindex $argv $i]
-               continue
-       }
-       if {$arg == "-d"} {
-               incr i
-               set tmp [lindex $argv $i]
-               regsub "\[^/\]$" $tmp "&/" output_directory
-               continue
-       }
-       lappend files $arg
-}
-
-proc flush_msg {} {
-       global msgid msgstr mode lang out fuzzy
-       global translated_count fuzzy_count not_translated_count
-
-       if {![info exists msgid] || $mode == ""} {
-               return
-       }
-       set mode ""
-       if {$fuzzy == 1} {
-               incr fuzzy_count
-               set fuzzy 0
-               return
-       }
-
-       if {$msgid == ""} {
-               set prefix "set ::msgcat::header"
-       } else {
-               if {$msgstr == ""} {
-                       incr not_translated_count
-                       return
-               }
-               set prefix "::msgcat::mcset $lang \"[u2a $msgid]\""
-               incr translated_count
-       }
-
-       puts $out "$prefix \"[u2a $msgstr]\""
-}
-
-set fuzzy 0
-set translated_count 0
-set fuzzy_count 0
-set not_translated_count 0
-foreach file $files {
-       regsub "^.*/\(\[^/\]*\)\.po$" $file "$output_directory\\1.msg" outfile
-       set in [open $file "r"]
-       fconfigure $in -encoding utf-8
-       set out [open $outfile "w"]
-
-       set mode ""
-       while {[gets $in line] >= 0} {
-               if {[regexp "^#" $line]} {
-                       if {[regexp ", fuzzy" $line]} {
-                               set fuzzy 1
-                       } else {
-                               flush_msg
-                       }
-                       continue
-               } elseif {[regexp "^msgid \"(.*)\"$" $line dummy match]} {
-                       flush_msg
-                       set msgid $match
-                       set mode "msgid"
-               } elseif {[regexp "^msgstr \"(.*)\"$" $line dummy match]} {
-                       set msgstr $match
-                       set mode "msgstr"
-               } elseif {$line == ""} {
-                       flush_msg
-               } elseif {[regexp "^\"(.*)\"$" $line dummy match]} {
-                       if {$mode == "msgid"} {
-                               append msgid $match
-                       } elseif {$mode == "msgstr"} {
-                               append msgstr $match
-                       } else {
-                               puts stderr "I do not know what to do: $match"
-                       }
-               } else {
-                       puts stderr "Cannot handle $line"
-               }
-       }
-       flush_msg
-       close $in
-       close $out
-}
-
-if {$show_statistics} {
-       puts [concat "$translated_count translated messages, " \
-               "$fuzzy_count fuzzy ones, " \
-               "$not_translated_count untranslated ones."]
-}
diff --git a/po/ru.po b/po/ru.po
deleted file mode 100644 (file)
index 6727a83..0000000
--- a/po/ru.po
+++ /dev/null
@@ -1,1893 +0,0 @@
-# Translation of git-gui to russian
-# Copyright (C) 2007 Shawn Pearce
-# This file is distributed under the same license as the git-gui package.
-# Irina Riesen <irina.riesen@gmail.com>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: git-gui\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2007-10-31 21:23+0100\n"
-"PO-Revision-Date: 2007-10-22 22:30-0200\n"
-"Last-Translator: Alex Riesen <raa.lkml@gmail.com>\n"
-"Language-Team: Russian Translation <git@vger.kernel.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: git-gui.sh:41 git-gui.sh:597 git-gui.sh:611 git-gui.sh:624 git-gui.sh:707
-#: git-gui.sh:726
-msgid "git-gui: fatal error"
-msgstr "git-gui: критическая ошибка"
-
-#: git-gui.sh:558
-#, tcl-format
-msgid "Invalid font specified in %s:"
-msgstr "В %s установлен неверный шрифт:"
-
-#: git-gui.sh:583
-msgid "Main Font"
-msgstr "Шрифт интерфейса"
-
-#: git-gui.sh:584
-msgid "Diff/Console Font"
-msgstr "Шрифт консоли и изменений (diff)"
-
-#: git-gui.sh:598
-msgid "Cannot find git in PATH."
-msgstr "git не найден в PATH."
-
-#: git-gui.sh:625
-msgid "Cannot parse Git version string:"
-msgstr "Невозможно распознать строку версии Git: "
-
-#: git-gui.sh:643
-#, tcl-format
-msgid ""
-"Git version cannot be determined.\n"
-"\n"
-"%s claims it is version '%s'.\n"
-"\n"
-"%s requires at least Git 1.5.0 or later.\n"
-"\n"
-"Assume '%s' is version 1.5.0?\n"
-msgstr ""
-"Невозможно определить версию Git\n"
-"%s указывает на версию '%s'.\n"
-"\n"
-"для %s требуется версия Git, начиная с 1.5.0\n"
-"\n"
-"Принять '%s' как версию 1.5.0?\n"
-
-#: git-gui.sh:881
-msgid "Git directory not found:"
-msgstr "Каталог Git не найден:"
-
-#: git-gui.sh:888
-msgid "Cannot move to top of working directory:"
-msgstr "Невозможно перейти к корню рабочего каталога репозитория: "
-
-#: git-gui.sh:895
-msgid "Cannot use funny .git directory:"
-msgstr "Каталог.git испорчен: "
-
-#: git-gui.sh:900
-msgid "No working directory"
-msgstr "Отсутствует рабочий каталог"
-
-#: git-gui.sh:1047
-msgid "Refreshing file status..."
-msgstr "Обновление информации о состоянии файлов..."
-
-#: git-gui.sh:1112
-msgid "Scanning for modified files ..."
-msgstr "Поиск измененных файлов..."
-
-#: git-gui.sh:1287 lib/browser.tcl:245
-msgid "Ready."
-msgstr "Готово."
-
-#: git-gui.sh:1553
-msgid "Unmodified"
-msgstr "Не изменено"
-
-#: git-gui.sh:1555
-msgid "Modified, not staged"
-msgstr "Изменено, не подготовлено"
-
-#: git-gui.sh:1556 git-gui.sh:1561
-msgid "Staged for commit"
-msgstr "Подготовлено для сохранения"
-
-#: git-gui.sh:1557 git-gui.sh:1562
-msgid "Portions staged for commit"
-msgstr "Части, подготовленные для сохранения"
-
-#: git-gui.sh:1558 git-gui.sh:1563
-msgid "Staged for commit, missing"
-msgstr "Подготовлено для сохранения, отсутствует"
-
-#: git-gui.sh:1560
-msgid "Untracked, not staged"
-msgstr "Не отслеживается, не подготовлено"
-
-#: git-gui.sh:1565
-msgid "Missing"
-msgstr "Отсутствует"
-
-#: git-gui.sh:1566
-msgid "Staged for removal"
-msgstr "Подготовлено для удаления"
-
-#: git-gui.sh:1567
-msgid "Staged for removal, still present"
-msgstr "Подготовлено для удаления, еще не удалено"
-
-#: git-gui.sh:1569 git-gui.sh:1570 git-gui.sh:1571 git-gui.sh:1572
-msgid "Requires merge resolution"
-msgstr "Требуется разрешение конфликта при объединении"
-
-#: git-gui.sh:1607
-msgid "Starting gitk... please wait..."
-msgstr "Запускается gitk... пожалуйста, ждите..."
-
-#: git-gui.sh:1616
-#, tcl-format
-msgid ""
-"Unable to start gitk:\n"
-"\n"
-"%s does not exist"
-msgstr ""
-"Не удалось запустить gitk:\n"
-"\n"
-"%s не существует"
-
-#: git-gui.sh:1816 lib/choose_repository.tcl:35
-msgid "Repository"
-msgstr "Репозиторий"
-
-#: git-gui.sh:1817
-msgid "Edit"
-msgstr "Редактировать"
-
-#: git-gui.sh:1819 lib/choose_rev.tcl:560
-msgid "Branch"
-msgstr "Ветвь"
-
-#: git-gui.sh:1822 lib/choose_rev.tcl:547
-msgid "Commit@@noun"
-msgstr "Состояние"
-
-#: git-gui.sh:1825 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
-msgid "Merge"
-msgstr "Объединить"
-
-#: git-gui.sh:1826 lib/choose_rev.tcl:556
-msgid "Remote"
-msgstr "Внешние репозитории"
-
-#: git-gui.sh:1835
-msgid "Browse Current Branch's Files"
-msgstr "Просмотреть файлы текущей ветви"
-
-#: git-gui.sh:1839
-msgid "Browse Branch Files..."
-msgstr "Показать файлы ветви..."
-
-#: git-gui.sh:1844
-msgid "Visualize Current Branch's History"
-msgstr "История текущей ветви наглядно"
-
-#: git-gui.sh:1848
-msgid "Visualize All Branch History"
-msgstr "История всех ветвей наглядно"
-
-#: git-gui.sh:1855
-#, tcl-format
-msgid "Browse %s's Files"
-msgstr "Показать файлы ветви %s"
-
-#: git-gui.sh:1857
-#, tcl-format
-msgid "Visualize %s's History"
-msgstr "История ветви %s наглядно"
-
-#: git-gui.sh:1862 lib/database.tcl:27 lib/database.tcl:67
-msgid "Database Statistics"
-msgstr "Статистика базы данных"
-
-#: git-gui.sh:1865 lib/database.tcl:34
-msgid "Compress Database"
-msgstr "Сжать базу данных"
-
-#: git-gui.sh:1868
-msgid "Verify Database"
-msgstr "Проверить базу данных"
-
-#: git-gui.sh:1875 git-gui.sh:1879 git-gui.sh:1883 lib/shortcut.tcl:7
-#: lib/shortcut.tcl:39 lib/shortcut.tcl:71
-msgid "Create Desktop Icon"
-msgstr "Создать ярлык на рабочем столе"
-
-#: git-gui.sh:1888 lib/choose_repository.tcl:176 lib/choose_repository.tcl:184
-msgid "Quit"
-msgstr "Выход"
-
-#: git-gui.sh:1895
-msgid "Undo"
-msgstr "Отменить"
-
-#: git-gui.sh:1898
-msgid "Redo"
-msgstr "Повторить"
-
-#: git-gui.sh:1902 git-gui.sh:2395
-msgid "Cut"
-msgstr "Вырезать"
-
-#: git-gui.sh:1905 git-gui.sh:2398 git-gui.sh:2469 git-gui.sh:2541
-#: lib/console.tcl:67
-msgid "Copy"
-msgstr "Копировать"
-
-#: git-gui.sh:1908 git-gui.sh:2401
-msgid "Paste"
-msgstr "Вставить"
-
-#: git-gui.sh:1911 git-gui.sh:2404 lib/branch_delete.tcl:26
-#: lib/remote_branch_delete.tcl:38
-msgid "Delete"
-msgstr "Удалить"
-
-#: git-gui.sh:1915 git-gui.sh:2408 git-gui.sh:2545 lib/console.tcl:69
-msgid "Select All"
-msgstr "Выделить все"
-
-#: git-gui.sh:1924
-msgid "Create..."
-msgstr "Создать..."
-
-#: git-gui.sh:1930
-msgid "Checkout..."
-msgstr "Перейти..."
-
-#: git-gui.sh:1936
-msgid "Rename..."
-msgstr "Переименовать..."
-
-#: git-gui.sh:1941 git-gui.sh:2040
-msgid "Delete..."
-msgstr "Удалить..."
-
-#: git-gui.sh:1946
-msgid "Reset..."
-msgstr "Сбросить..."
-
-#: git-gui.sh:1958 git-gui.sh:2342
-msgid "New Commit"
-msgstr "Новое состояние"
-
-#: git-gui.sh:1966 git-gui.sh:2349
-msgid "Amend Last Commit"
-msgstr "Исправить последнее состояние"
-
-#: git-gui.sh:1975 git-gui.sh:2309 lib/remote_branch_delete.tcl:99
-msgid "Rescan"
-msgstr "Перечитать"
-
-#: git-gui.sh:1981
-msgid "Stage To Commit"
-msgstr "Подготовить для сохранения"
-
-#: git-gui.sh:1986
-msgid "Stage Changed Files To Commit"
-msgstr "Подготовить измененные файлы для сохранения"
-
-#: git-gui.sh:1992
-msgid "Unstage From Commit"
-msgstr "Убрать из подготовленного"
-
-#: git-gui.sh:1997 lib/index.tcl:393
-msgid "Revert Changes"
-msgstr "Отменить изменения"
-
-#: git-gui.sh:2004 git-gui.sh:2321 git-gui.sh:2419
-msgid "Sign Off"
-msgstr "Подписать"
-
-#: git-gui.sh:2008 git-gui.sh:2325
-msgid "Commit@@verb"
-msgstr "Сохранить"
-
-#: git-gui.sh:2019
-msgid "Local Merge..."
-msgstr "Локальное объединение..."
-
-#: git-gui.sh:2024
-msgid "Abort Merge..."
-msgstr "Прервать объединение..."
-
-#: git-gui.sh:2036
-msgid "Push..."
-msgstr "Отправить..."
-
-#: git-gui.sh:2047 lib/choose_repository.tcl:40
-msgid "Apple"
-msgstr ""
-
-#: git-gui.sh:2050 git-gui.sh:2072 lib/about.tcl:13
-#: lib/choose_repository.tcl:43 lib/choose_repository.tcl:49
-#, tcl-format
-msgid "About %s"
-msgstr "О %s"
-
-#: git-gui.sh:2054
-msgid "Preferences..."
-msgstr "Настройки..."
-
-#: git-gui.sh:2062 git-gui.sh:2587
-msgid "Options..."
-msgstr "Настройки..."
-
-#: git-gui.sh:2068 lib/choose_repository.tcl:46
-msgid "Help"
-msgstr "Помощь"
-
-#: git-gui.sh:2109
-msgid "Online Documentation"
-msgstr "Документация в интернете"
-
-#: git-gui.sh:2193
-#, tcl-format
-msgid "fatal: cannot stat path %s: No such file or directory"
-msgstr "критическая ошибка: %s: нет такого файла или каталога"
-
-#: git-gui.sh:2226
-msgid "Current Branch:"
-msgstr "Текущая ветвь:"
-
-#: git-gui.sh:2247
-msgid "Staged Changes (Will Commit)"
-msgstr "Подготовлено (будет сохранено)"
-
-#: git-gui.sh:2266
-msgid "Unstaged Changes"
-msgstr "Изменено (не будет сохранено)"
-
-#: git-gui.sh:2315
-msgid "Stage Changed"
-msgstr "Подготовить все"
-
-#: git-gui.sh:2331 lib/transport.tcl:93 lib/transport.tcl:182
-msgid "Push"
-msgstr "Отправить"
-
-#: git-gui.sh:2361
-msgid "Initial Commit Message:"
-msgstr "Комментарий к первому состоянию:"
-
-#: git-gui.sh:2362
-msgid "Amended Commit Message:"
-msgstr "Комментарий к исправленному состоянию:"
-
-#: git-gui.sh:2363
-msgid "Amended Initial Commit Message:"
-msgstr "Комментарий к исправленному первоначальному состоянию:"
-
-#: git-gui.sh:2364
-msgid "Amended Merge Commit Message:"
-msgstr "Комментарий к исправленному объединению:"
-
-#: git-gui.sh:2365
-msgid "Merge Commit Message:"
-msgstr "Комментарий к объединению:"
-
-#: git-gui.sh:2366
-msgid "Commit Message:"
-msgstr "Комментарий к состоянию:"
-
-#: git-gui.sh:2411 git-gui.sh:2549 lib/console.tcl:71
-msgid "Copy All"
-msgstr "Копировать все"
-
-#: git-gui.sh:2435 lib/blame.tcl:104
-msgid "File:"
-msgstr "Файл:"
-
-#: git-gui.sh:2537
-msgid "Refresh"
-msgstr "Обновить"
-
-#: git-gui.sh:2558
-msgid "Apply/Reverse Hunk"
-msgstr "Применить/Убрать изменение"
-
-#: git-gui.sh:2564
-msgid "Decrease Font Size"
-msgstr "Уменьшить размер шрифта"
-
-#: git-gui.sh:2568
-msgid "Increase Font Size"
-msgstr "Увеличить размер шрифта"
-
-#: git-gui.sh:2573
-msgid "Show Less Context"
-msgstr "Меньше контекста"
-
-#: git-gui.sh:2580
-msgid "Show More Context"
-msgstr "Больше контекста"
-
-#: git-gui.sh:2594
-msgid "Unstage Hunk From Commit"
-msgstr "Не сохранять часть"
-
-#: git-gui.sh:2596
-msgid "Stage Hunk For Commit"
-msgstr "Подготовить часть для сохранения"
-
-#: git-gui.sh:2615
-msgid "Initializing..."
-msgstr "Инициализация..."
-
-#: git-gui.sh:2706
-#, tcl-format
-msgid ""
-"Possible environment issues exist.\n"
-"\n"
-"The following environment variables are probably\n"
-"going to be ignored by any Git subprocess run\n"
-"by %s:\n"
-"\n"
-msgstr ""
-"Возможны ошибки в переменных окружения.\n"
-"\n"
-"Переменные окружения, которые возможно\n"
-"будут проигнорированы командами Git,\n"
-"запущенными из %s\n"
-"\n"
-
-#: git-gui.sh:2736
-msgid ""
-"\n"
-"This is due to a known issue with the\n"
-"Tcl binary distributed by Cygwin."
-msgstr ""
-"\n"
-"Это известная проблема с Tcl,\n"
-"распространяемым Cygwin."
-
-#: git-gui.sh:2741
-#, tcl-format
-msgid ""
-"\n"
-"\n"
-"A good replacement for %s\n"
-"is placing values for the user.name and\n"
-"user.email settings into your personal\n"
-"~/.gitconfig file.\n"
-msgstr ""
-"\n"
-"\n"
-"Вместо использования %s можно\n"
-"сохранить значения user.name и\n"
-"user.email в Вашем персональном\n"
-"файле ~/.gitconfig.\n"
-
-#: lib/about.tcl:25
-msgid "git-gui - a graphical user interface for Git."
-msgstr "git-gui - графический пользовательский интерфейс к Git."
-
-#: lib/blame.tcl:77
-msgid "File Viewer"
-msgstr "Просмотр файла"
-
-#: lib/blame.tcl:81
-msgid "Commit:"
-msgstr "Сохраненное состояние:"
-
-#: lib/blame.tcl:249
-msgid "Copy Commit"
-msgstr "Скопировать SHA-1"
-
-#: lib/blame.tcl:369
-#, tcl-format
-msgid "Reading %s..."
-msgstr "Чтение %s..."
-
-#: lib/blame.tcl:473
-msgid "Loading copy/move tracking annotations..."
-msgstr "Загрузка аннотации копирований/переименований..."
-
-#: lib/blame.tcl:493
-msgid "lines annotated"
-msgstr "строк прокомментировано"
-
-#: lib/blame.tcl:674
-msgid "Loading original location annotations..."
-msgstr "Загрузка аннотаций первоначального положения объекта..."
-
-#: lib/blame.tcl:677
-msgid "Annotation complete."
-msgstr "Аннотация завершена."
-
-#: lib/blame.tcl:731
-msgid "Loading annotation..."
-msgstr "Загрузка аннотации..."
-
-#: lib/blame.tcl:787
-msgid "Author:"
-msgstr "Автор:"
-
-#: lib/blame.tcl:791
-msgid "Committer:"
-msgstr "Сохранил:"
-
-#: lib/blame.tcl:796
-msgid "Original File:"
-msgstr "Исходный файл:"
-
-#: lib/blame.tcl:910
-msgid "Originally By:"
-msgstr "Источник:"
-
-#: lib/blame.tcl:916
-msgid "In File:"
-msgstr "Файл:"
-
-#: lib/blame.tcl:921
-msgid "Copied Or Moved Here By:"
-msgstr "Скопировано/перемещено в:"
-
-#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
-msgid "Checkout Branch"
-msgstr "Перейти на ветвь"
-
-#: lib/branch_checkout.tcl:23
-msgid "Checkout"
-msgstr "Перейти"
-
-#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
-#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
-#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
-#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
-msgid "Cancel"
-msgstr "Отменить"
-
-#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
-msgid "Revision"
-msgstr "Версия"
-
-#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
-msgid "Options"
-msgstr "Настройки"
-
-#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
-msgid "Fetch Tracking Branch"
-msgstr "Получить изменения из внешней ветви"
-
-#: lib/branch_checkout.tcl:44
-msgid "Detach From Local Branch"
-msgstr "Отсоединить от локальной ветви"
-
-#: lib/branch_create.tcl:22
-msgid "Create Branch"
-msgstr "Создание ветви"
-
-#: lib/branch_create.tcl:27
-msgid "Create New Branch"
-msgstr "Создать новую ветвь"
-
-#: lib/branch_create.tcl:31 lib/choose_repository.tcl:375
-msgid "Create"
-msgstr "Создать"
-
-#: lib/branch_create.tcl:40
-msgid "Branch Name"
-msgstr "Название ветви"
-
-#: lib/branch_create.tcl:43
-msgid "Name:"
-msgstr "Название:"
-
-#: lib/branch_create.tcl:58
-msgid "Match Tracking Branch Name"
-msgstr "Взять из имен ветвей слежения"
-
-#: lib/branch_create.tcl:66
-msgid "Starting Revision"
-msgstr "Начальная версия"
-
-#: lib/branch_create.tcl:72
-msgid "Update Existing Branch:"
-msgstr "Обновить имеющуюся ветвь:"
-
-#: lib/branch_create.tcl:75
-msgid "No"
-msgstr "Нет"
-
-#: lib/branch_create.tcl:80
-msgid "Fast Forward Only"
-msgstr "Только Fast Forward"
-
-#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
-msgid "Reset"
-msgstr "Сброс"
-
-#: lib/branch_create.tcl:97
-msgid "Checkout After Creation"
-msgstr "После создания сделать текущей"
-
-#: lib/branch_create.tcl:131
-msgid "Please select a tracking branch."
-msgstr "Укажите ветвь слежения."
-
-#: lib/branch_create.tcl:140
-#, tcl-format
-msgid "Tracking branch %s is not a branch in the remote repository."
-msgstr "Ветвь слежения %s не является ветвью во внешнем репозитории."
-
-#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
-msgid "Please supply a branch name."
-msgstr "Укажите название ветви."
-
-#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
-#, tcl-format
-msgid "'%s' is not an acceptable branch name."
-msgstr "Недопустимое название ветви '%s'."
-
-#: lib/branch_delete.tcl:15
-msgid "Delete Branch"
-msgstr "Удаление ветви"
-
-#: lib/branch_delete.tcl:20
-msgid "Delete Local Branch"
-msgstr "Удалить локальную ветвь"
-
-#: lib/branch_delete.tcl:37
-msgid "Local Branches"
-msgstr "Локальные ветви"
-
-#: lib/branch_delete.tcl:52
-msgid "Delete Only If Merged Into"
-msgstr "Удалить только в случае, если было объединение с"
-
-#: lib/branch_delete.tcl:54
-msgid "Always (Do not perform merge test.)"
-msgstr "Всегда (не выполнять проверку на объединение)"
-
-#: lib/branch_delete.tcl:103
-#, tcl-format
-msgid "The following branches are not completely merged into %s:"
-msgstr "Следующие ветви объединены с %s не полностью:"
-
-#: lib/branch_delete.tcl:115
-msgid ""
-"Recovering deleted branches is difficult. \n"
-"\n"
-" Delete the selected branches?"
-msgstr ""
-"Восстанавливать удаленные ветви сложно. \n"
-"\n"
-" Удалить выбранные ветви?"
-
-#: lib/branch_delete.tcl:141
-#, tcl-format
-msgid ""
-"Failed to delete branches:\n"
-"%s"
-msgstr ""
-"Не удалось удалить ветви:\n"
-"%s"
-
-#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
-msgid "Rename Branch"
-msgstr "Переименование ветви"
-
-#: lib/branch_rename.tcl:26
-msgid "Rename"
-msgstr "Переименовать"
-
-#: lib/branch_rename.tcl:36
-msgid "Branch:"
-msgstr "Ветвь:"
-
-#: lib/branch_rename.tcl:39
-msgid "New Name:"
-msgstr "Новое название:"
-
-#: lib/branch_rename.tcl:75
-msgid "Please select a branch to rename."
-msgstr "Укажите ветвь для переименования."
-
-#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
-#, tcl-format
-msgid "Branch '%s' already exists."
-msgstr "Ветвь '%s' уже существует."
-
-#: lib/branch_rename.tcl:117
-#, tcl-format
-msgid "Failed to rename '%s'."
-msgstr "Не удалось переименовать '%s'. "
-
-#: lib/browser.tcl:17
-msgid "Starting..."
-msgstr "Запуск..."
-
-#: lib/browser.tcl:26
-msgid "File Browser"
-msgstr "Просмотр списка файлов"
-
-#: lib/browser.tcl:125 lib/browser.tcl:142
-#, tcl-format
-msgid "Loading %s..."
-msgstr "Загрузка %s..."
-
-#: lib/browser.tcl:186
-msgid "[Up To Parent]"
-msgstr "[На уровень выше]"
-
-#: lib/browser.tcl:266 lib/browser.tcl:272
-msgid "Browse Branch Files"
-msgstr "Показать файлы ветви"
-
-#: lib/browser.tcl:277 lib/choose_repository.tcl:391
-#: lib/choose_repository.tcl:482 lib/choose_repository.tcl:492
-#: lib/choose_repository.tcl:989
-msgid "Browse"
-msgstr "Показать"
-
-#: lib/checkout_op.tcl:79
-#, tcl-format
-msgid "Fetching %s from %s"
-msgstr "Получение %s из %s "
-
-#: lib/checkout_op.tcl:127
-#, tcl-format
-msgid "fatal: Cannot resolve %s"
-msgstr "критическая ошибка: невозможно разрешить %s"
-
-#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
-msgid "Close"
-msgstr "Закрыть"
-
-#: lib/checkout_op.tcl:169
-#, tcl-format
-msgid "Branch '%s' does not exist."
-msgstr "Ветвь '%s' не существует "
-
-#: lib/checkout_op.tcl:206
-#, tcl-format
-msgid ""
-"Branch '%s' already exists.\n"
-"\n"
-"It cannot fast-forward to %s.\n"
-"A merge is required."
-msgstr ""
-"Ветвь '%s' уже существует.\n"
-"\n"
-"Она не может быть прокручена(fast-forward) к %s.\n"
-"Требуется объединение."
-
-#: lib/checkout_op.tcl:220
-#, tcl-format
-msgid "Merge strategy '%s' not supported."
-msgstr "Стратегия объединения '%s' не поддерживается."
-
-#: lib/checkout_op.tcl:239
-#, tcl-format
-msgid "Failed to update '%s'."
-msgstr "Не удалось обновить '%s'."
-
-#: lib/checkout_op.tcl:251
-msgid "Staging area (index) is already locked."
-msgstr "Рабочая область заблокирована другим процессом."
-
-#: lib/checkout_op.tcl:266
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before the current branch can be changed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"Последнее прочитанное состояние репозитория не соответствует текущему.\n"
-"\n"
-"С момента последней проверки репозиторий был изменен другой программой Git. "
-"Необходимо перечитать репозиторий, прежде чем изменять текущую ветвь.\n"
-"\n"
-"Это будет сделано сейчас автоматически.\n"
-
-#: lib/checkout_op.tcl:322
-#, tcl-format
-msgid "Updating working directory to '%s'..."
-msgstr "Обновление рабочего каталога из '%s'..."
-
-#: lib/checkout_op.tcl:353
-#, tcl-format
-msgid "Aborted checkout of '%s' (file level merging is required)."
-msgstr "Прерван переход на '%s' (требуется объединение на уровне файлов)"
-
-#: lib/checkout_op.tcl:354
-msgid "File level merge required."
-msgstr "Требуется объединение на уровне файлов."
-
-#: lib/checkout_op.tcl:358
-#, tcl-format
-msgid "Staying on branch '%s'."
-msgstr "Ветвь '%s' остается текущей."
-
-#: lib/checkout_op.tcl:429
-msgid ""
-"You are no longer on a local branch.\n"
-"\n"
-"If you wanted to be on a branch, create one now starting from 'This Detached "
-"Checkout'."
-msgstr ""
-"Вы находитесь не в локальной ветви.\n"
-"\n"
-"Если вы хотите снова вернуться к какой-нибудь ветви, создайте ее сейчас, "
-"начиная с 'Текущего отсоединенного состояния'."
-
-#: lib/checkout_op.tcl:446
-#, tcl-format
-msgid "Checked out '%s'."
-msgstr "Ветвь '%s' сделана текущей."
-
-#: lib/checkout_op.tcl:478
-#, tcl-format
-msgid "Resetting '%s' to '%s' will lose the following commits:"
-msgstr "Сброс '%s' в '%s' приведет к потере следующих сохраненных состояний: "
-
-#: lib/checkout_op.tcl:500
-msgid "Recovering lost commits may not be easy."
-msgstr "Восстановить потерянные сохраненные состояния будет сложно."
-
-#: lib/checkout_op.tcl:505
-#, tcl-format
-msgid "Reset '%s'?"
-msgstr "Сбросить '%s'?"
-
-#: lib/checkout_op.tcl:510 lib/merge.tcl:164
-msgid "Visualize"
-msgstr "Наглядно"
-
-#: lib/checkout_op.tcl:578
-#, tcl-format
-msgid ""
-"Failed to set current branch.\n"
-"\n"
-"This working directory is only partially switched.  We successfully updated "
-"your files, but failed to update an internal Git file.\n"
-"\n"
-"This should not have occurred.  %s will now close and give up."
-msgstr ""
-"Не удалось установить текущую ветвь.\n"
-"\n"
-"Ваш рабочий каталог обновлен только частично. Были обновлены все файлы кроме "
-"служебных файлов Git. \n"
-"\n"
-"Этого не должно было произойти. %s завершается."
-
-#: lib/choose_font.tcl:39
-msgid "Select"
-msgstr "Выбрать"
-
-#: lib/choose_font.tcl:53
-msgid "Font Family"
-msgstr "Шрифт"
-
-#: lib/choose_font.tcl:73
-msgid "Font Size"
-msgstr "Размер шрифта"
-
-#: lib/choose_font.tcl:90
-msgid "Font Example"
-msgstr "Пример текста"
-
-#: lib/choose_font.tcl:101
-msgid ""
-"This is example text.\n"
-"If you like this text, it can be your font."
-msgstr ""
-"Это пример текста.\n"
-"Если Вам нравится этот текст, это может быть Ваш шрифт."
-
-#: lib/choose_repository.tcl:27
-msgid "Git Gui"
-msgstr ""
-
-#: lib/choose_repository.tcl:80 lib/choose_repository.tcl:380
-msgid "Create New Repository"
-msgstr "Создать новый репозиторий"
-
-#: lib/choose_repository.tcl:86
-msgid "New..."
-msgstr "Новый..."
-
-#: lib/choose_repository.tcl:93 lib/choose_repository.tcl:468
-msgid "Clone Existing Repository"
-msgstr "Склонировать существующий репозиторий"
-
-#: lib/choose_repository.tcl:99
-msgid "Clone..."
-msgstr "Склонировать..."
-
-#: lib/choose_repository.tcl:106 lib/choose_repository.tcl:978
-msgid "Open Existing Repository"
-msgstr "Выбрать существующий репозиторий"
-
-#: lib/choose_repository.tcl:112
-msgid "Open..."
-msgstr "Открыть..."
-
-#: lib/choose_repository.tcl:125
-msgid "Recent Repositories"
-msgstr "Недавние репозитории"
-
-#: lib/choose_repository.tcl:131
-msgid "Open Recent Repository:"
-msgstr "Открыть последний репозиторий"
-
-#: lib/choose_repository.tcl:294
-#, tcl-format
-msgid "Location %s already exists."
-msgstr "Путь '%s' уже существует."
-
-#: lib/choose_repository.tcl:300 lib/choose_repository.tcl:307
-#: lib/choose_repository.tcl:314
-#, tcl-format
-msgid "Failed to create repository %s:"
-msgstr "Не удалось создать репозиторий %s:"
-
-#: lib/choose_repository.tcl:385 lib/choose_repository.tcl:486
-msgid "Directory:"
-msgstr "Каталог:"
-
-#: lib/choose_repository.tcl:415 lib/choose_repository.tcl:544
-#: lib/choose_repository.tcl:1013
-msgid "Git Repository"
-msgstr "Репозиторий"
-
-#: lib/choose_repository.tcl:430 lib/choose_repository.tcl:437
-#, tcl-format
-msgid "Directory %s already exists."
-msgstr "Каталог '%s' уже существует."
-
-#: lib/choose_repository.tcl:442
-#, tcl-format
-msgid "File %s already exists."
-msgstr "Файл '%s' уже существует."
-
-#: lib/choose_repository.tcl:463
-msgid "Clone"
-msgstr "Склонировать"
-
-#: lib/choose_repository.tcl:476
-msgid "URL:"
-msgstr "Ссылка:"
-
-#: lib/choose_repository.tcl:496
-msgid "Clone Type:"
-msgstr "Тип клона:"
-
-#: lib/choose_repository.tcl:502
-msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
-msgstr "Стандартный (Быстрый, полуизбыточный, \"жесткие\" ссылки)"
-
-#: lib/choose_repository.tcl:508
-msgid "Full Copy (Slower, Redundant Backup)"
-msgstr "Полная копия (Медленный, создает резервную копию)"
-
-#: lib/choose_repository.tcl:514
-msgid "Shared (Fastest, Not Recommended, No Backup)"
-msgstr "Общий (Самый быстрый, не рекомендуется, без резервной копии)"
-
-#: lib/choose_repository.tcl:550 lib/choose_repository.tcl:597
-#: lib/choose_repository.tcl:738 lib/choose_repository.tcl:808
-#: lib/choose_repository.tcl:1019 lib/choose_repository.tcl:1027
-#, tcl-format
-msgid "Not a Git repository: %s"
-msgstr "Каталог не является репозиторием: %s"
-
-#: lib/choose_repository.tcl:586
-msgid "Standard only available for local repository."
-msgstr "Стандартный клон возможен только для локального репозитория."
-
-#: lib/choose_repository.tcl:590
-msgid "Shared only available for local repository."
-msgstr "Общий клон возможен только для локального репозитория."
-
-#: lib/choose_repository.tcl:617
-msgid "Failed to configure origin"
-msgstr "Не могу сконфигурировать исходный репозиторий."
-
-#: lib/choose_repository.tcl:629
-msgid "Counting objects"
-msgstr "Считаю объекты"
-
-#: lib/choose_repository.tcl:630
-msgid "buckets"
-msgstr ""
-
-#: lib/choose_repository.tcl:654
-#, tcl-format
-msgid "Unable to copy objects/info/alternates: %s"
-msgstr "Не могу скопировать objects/info/alternates: %s"
-
-#: lib/choose_repository.tcl:690
-#, tcl-format
-msgid "Nothing to clone from %s."
-msgstr "Нечего клонировать с %s."
-
-#: lib/choose_repository.tcl:692 lib/choose_repository.tcl:906
-#: lib/choose_repository.tcl:918
-msgid "The 'master' branch has not been initialized."
-msgstr "Не инициализирована ветвь 'master'."
-
-#: lib/choose_repository.tcl:705
-msgid "Hardlinks are unavailable.  Falling back to copying."
-msgstr "\"Жесткие ссылки\" не доступны. Буду использовать копирование."
-
-#: lib/choose_repository.tcl:717
-#, tcl-format
-msgid "Cloning from %s"
-msgstr "Клонирование %s"
-
-#: lib/choose_repository.tcl:748
-msgid "Copying objects"
-msgstr "Копирование objects"
-
-#: lib/choose_repository.tcl:749
-msgid "KiB"
-msgstr "КБ"
-
-#: lib/choose_repository.tcl:773
-#, tcl-format
-msgid "Unable to copy object: %s"
-msgstr "Не могу скопировать объект: %s"
-
-#: lib/choose_repository.tcl:783
-msgid "Linking objects"
-msgstr "Создание ссылок на objects"
-
-#: lib/choose_repository.tcl:784
-msgid "objects"
-msgstr "объекты"
-
-#: lib/choose_repository.tcl:792
-#, tcl-format
-msgid "Unable to hardlink object: %s"
-msgstr "Не могу \"жестко связать\" объект: %s"
-
-#: lib/choose_repository.tcl:847
-msgid "Cannot fetch branches and objects.  See console output for details."
-msgstr ""
-"Не могу получить ветви и объекты. Дополнительная информация на консоли."
-
-#: lib/choose_repository.tcl:858
-msgid "Cannot fetch tags.  See console output for details."
-msgstr "Не могу получить метки. Дополнительная информация на консоли."
-
-#: lib/choose_repository.tcl:882
-msgid "Cannot determine HEAD.  See console output for details."
-msgstr "Не могу определить HEAD. Дополнительная информация на консоли."
-
-#: lib/choose_repository.tcl:891
-#, tcl-format
-msgid "Unable to cleanup %s"
-msgstr "Не могу очистить %s"
-
-#: lib/choose_repository.tcl:897
-msgid "Clone failed."
-msgstr "Клонирование не удалось."
-
-#: lib/choose_repository.tcl:904
-msgid "No default branch obtained."
-msgstr "Не было получено ветви по умолчанию."
-
-#: lib/choose_repository.tcl:915
-#, tcl-format
-msgid "Cannot resolve %s as a commit."
-msgstr "Не могу распознать %s как состояние."
-
-#: lib/choose_repository.tcl:927
-msgid "Creating working directory"
-msgstr "Создаю рабочий каталог"
-
-#: lib/choose_repository.tcl:928 lib/index.tcl:65 lib/index.tcl:127
-#: lib/index.tcl:193
-msgid "files"
-msgstr "файлов"
-
-#: lib/choose_repository.tcl:957
-msgid "Initial file checkout failed."
-msgstr "Не удалось получить начальное состояние файлов репозитория."
-
-#: lib/choose_repository.tcl:973
-msgid "Open"
-msgstr "Открыть"
-
-#: lib/choose_repository.tcl:983
-msgid "Repository:"
-msgstr "Репозиторий:"
-
-#: lib/choose_repository.tcl:1033
-#, tcl-format
-msgid "Failed to open repository %s:"
-msgstr "Не удалось открыть репозиторий %s:"
-
-#: lib/choose_rev.tcl:53
-msgid "This Detached Checkout"
-msgstr "Текущее отсоединенное состояние"
-
-#: lib/choose_rev.tcl:60
-msgid "Revision Expression:"
-msgstr "Выражение для определения версии:"
-
-#: lib/choose_rev.tcl:74
-msgid "Local Branch"
-msgstr "Локальная ветвь:"
-
-#: lib/choose_rev.tcl:79
-msgid "Tracking Branch"
-msgstr "Ветвь слежения"
-
-#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
-msgid "Tag"
-msgstr "Таг"
-
-#: lib/choose_rev.tcl:317
-#, tcl-format
-msgid "Invalid revision: %s"
-msgstr "Неверная версия: %s"
-
-#: lib/choose_rev.tcl:338
-msgid "No revision selected."
-msgstr "Версия не указана."
-
-#: lib/choose_rev.tcl:346
-msgid "Revision expression is empty."
-msgstr "Пустое выражение для определения версии."
-
-#: lib/choose_rev.tcl:530
-msgid "Updated"
-msgstr "Обновлено"
-
-#: lib/choose_rev.tcl:558
-msgid "URL"
-msgstr "Ссылка"
-
-#: lib/commit.tcl:9
-msgid ""
-"There is nothing to amend.\n"
-"\n"
-"You are about to create the initial commit.  There is no commit before this "
-"to amend.\n"
-msgstr ""
-"Отсутствует состояние для исправления.\n"
-"\n"
-"Вы создаете первое состояние в репозитории, здесь еще нечего исправлять.\n"
-
-#: lib/commit.tcl:18
-msgid ""
-"Cannot amend while merging.\n"
-"\n"
-"You are currently in the middle of a merge that has not been fully "
-"completed.  You cannot amend the prior commit unless you first abort the "
-"current merge activity.\n"
-msgstr ""
-"Невозможно исправить состояние во время объединения.\n"
-"\n"
-"Текущее объединение не завершено. Невозможно исправить предыдущее "
-"сохраненное состояние не прерывая текущее объединение.\n"
-
-#: lib/commit.tcl:49
-msgid "Error loading commit data for amend:"
-msgstr "Ошибка при загрузке данных для исправления сохраненного состояния:"
-
-#: lib/commit.tcl:76
-msgid "Unable to obtain your identity:"
-msgstr "Невозможно получить информацию об авторстве:"
-
-#: lib/commit.tcl:81
-msgid "Invalid GIT_COMMITTER_IDENT:"
-msgstr "Неверный GIT_COMMITTER_IDENT:"
-
-#: lib/commit.tcl:133
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before another commit can be created.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"Последнее прочитанное состояние репозитория не соответствует текущему.\n"
-"\n"
-"С момента последней проверки репозиторий был изменен другой программой Git. "
-"Необходимо перечитать репозиторий, прежде чем изменять текущую ветвь. \n"
-"\n"
-"Это будет сделано сейчас автоматически.\n"
-
-#: lib/commit.tcl:154
-#, tcl-format
-msgid ""
-"Unmerged files cannot be committed.\n"
-"\n"
-"File %s has merge conflicts.  You must resolve them and stage the file "
-"before committing.\n"
-msgstr ""
-"Нельзя сохранить необъединенные файлы.\n"
-"\n"
-"Для файла %s возник конфликт объединения. Разрешите конфликт и добавьте к "
-"подготовленным файлам перед сохранением.\n"
-
-#: lib/commit.tcl:162
-#, tcl-format
-msgid ""
-"Unknown file state %s detected.\n"
-"\n"
-"File %s cannot be committed by this program.\n"
-msgstr ""
-"Обнаружено неизвестное состояние файла %s.\n"
-"\n"
-"Файл %s не может быть сохранен данной программой.\n"
-
-#: lib/commit.tcl:170
-msgid ""
-"No changes to commit.\n"
-"\n"
-"You must stage at least 1 file before you can commit.\n"
-msgstr ""
-"Отсутствуют изменения для сохранения.\n"
-"\n"
-"Подготовьте хотя бы один файл до создания сохраненного состояния.\n"
-
-#: lib/commit.tcl:183
-msgid ""
-"Please supply a commit message.\n"
-"\n"
-"A good commit message has the following format:\n"
-"\n"
-"- First line: Describe in one sentance what you did.\n"
-"- Second line: Blank\n"
-"- Remaining lines: Describe why this change is good.\n"
-msgstr ""
-"Напишите комментарий к сохраненному состоянию.\n"
-"\n"
-"Рекомендуется следующий формат комментария:\n"
-"\n"
-"- первая строка: краткое описание сделанных изменений.\n"
-"- вторая строка пустая\n"
-"- оставшиеся строки: опишите, что дают ваши изменения.\n"
-
-#: lib/commit.tcl:257
-msgid "write-tree failed:"
-msgstr "Программа write-tree завершилась с ошибкой:"
-
-#: lib/commit.tcl:275
-#, tcl-format
-msgid "Commit %s appears to be corrupt"
-msgstr "Состояние %s выглядит поврежденным"
-
-#: lib/commit.tcl:279
-msgid ""
-"No changes to commit.\n"
-"\n"
-"No files were modified by this commit and it was not a merge commit.\n"
-"\n"
-"A rescan will be automatically started now.\n"
-msgstr ""
-"Отсутствуют изменения для сохранения.\n"
-"\n"
-"Ни один файл не был изменен и не было объединения.\n"
-"\n"
-"Сейчас автоматически запустится перечитывание репозитория.\n"
-
-#: lib/commit.tcl:286
-msgid "No changes to commit."
-msgstr "Отуствуют измения для сохранения."
-
-#: lib/commit.tcl:303
-#, tcl-format
-msgid "warning: Tcl does not support encoding '%s'."
-msgstr "предупреждение: Tcl не поддерживает кодировку '%s'."
-
-#: lib/commit.tcl:317
-msgid "commit-tree failed:"
-msgstr "Программа commit-tree завершилась с ошибкой:"
-
-#: lib/commit.tcl:339
-msgid "update-ref failed:"
-msgstr "Программа update-ref завершилась с ошибкой:"
-
-#: lib/commit.tcl:430
-#, tcl-format
-msgid "Created commit %s: %s"
-msgstr "Создано состояние %s: %s "
-
-#: lib/console.tcl:57
-msgid "Working... please wait..."
-msgstr "В процессе... пожалуйста, ждите..."
-
-#: lib/console.tcl:183
-msgid "Success"
-msgstr "Процесс успешно завершен"
-
-#: lib/console.tcl:196
-msgid "Error: Command Failed"
-msgstr "Ошибка: не удалось выполнить команду"
-
-#: lib/database.tcl:43
-msgid "Number of loose objects"
-msgstr "Количество несвязанных объектов"
-
-#: lib/database.tcl:44
-msgid "Disk space used by loose objects"
-msgstr "Объем дискового пространства, занятый несвязанными объектами"
-
-#: lib/database.tcl:45
-msgid "Number of packed objects"
-msgstr "Количество упакованных объектов"
-
-#: lib/database.tcl:46
-msgid "Number of packs"
-msgstr "Количество pack-файлов"
-
-#: lib/database.tcl:47
-msgid "Disk space used by packed objects"
-msgstr "Объем дискового пространства, занятый упакованными объектами"
-
-#: lib/database.tcl:48
-msgid "Packed objects waiting for pruning"
-msgstr "Несвязанные объекты, которые можно удалить"
-
-#: lib/database.tcl:49
-msgid "Garbage files"
-msgstr "Мусор"
-
-#: lib/database.tcl:72
-msgid "Compressing the object database"
-msgstr "Сжатие базы объектов"
-
-#: lib/database.tcl:83
-msgid "Verifying the object database with fsck-objects"
-msgstr "Проверка базы объектов при помощи fsck"
-
-#: lib/database.tcl:108
-#, tcl-format
-msgid ""
-"This repository currently has approximately %i loose objects.\n"
-"\n"
-"To maintain optimal performance it is strongly recommended that you compress "
-"the database when more than %i loose objects exist.\n"
-"\n"
-"Compress the database now?"
-msgstr ""
-"Этот репозиторий сейчас содержит примерно %i свободных объектов\n"
-"\n"
-"Для лучшей производительности рекомендуется сжать базу данных, когда есть "
-"более %i несвязанных объектов.\n"
-"\n"
-"Сжать базу данных сейчас?"
-
-#: lib/date.tcl:25
-#, tcl-format
-msgid "Invalid date from Git: %s"
-msgstr "Неправильная дата в репозитории: %s"
-
-#: lib/diff.tcl:42
-#, tcl-format
-msgid ""
-"No differences detected.\n"
-"\n"
-"%s has no changes.\n"
-"\n"
-"The modification date of this file was updated by another application, but "
-"the content within the file was not changed.\n"
-"\n"
-"A rescan will be automatically started to find other files which may have "
-"the same state."
-msgstr ""
-"Изменений не обнаружено.\n"
-"\n"
-"в %s отутствуют изменения.\n"
-"\n"
-"Дата изменения файла была обновлена другой программой, но содержимое файла "
-"осталось прежним.\n"
-"\n"
-"Сейчас будет запущено перечитывание репозитория, чтобы найти подобные файлы."
-
-#: lib/diff.tcl:81
-#, tcl-format
-msgid "Loading diff of %s..."
-msgstr "Загрузка изменений в %s..."
-
-#: lib/diff.tcl:114 lib/diff.tcl:184
-#, tcl-format
-msgid "Unable to display %s"
-msgstr "Не могу показать %s"
-
-#: lib/diff.tcl:115
-msgid "Error loading file:"
-msgstr "Ошибка загрузки файла:"
-
-#: lib/diff.tcl:122
-msgid "Git Repository (subproject)"
-msgstr "Репозиторий Git (подпроект)"
-
-#: lib/diff.tcl:134
-msgid "* Binary file (not showing content)."
-msgstr "* Двоичный файл (содержимое не показано)"
-
-#: lib/diff.tcl:185
-msgid "Error loading diff:"
-msgstr "Ошибка загрузки diff:"
-
-#: lib/diff.tcl:302
-msgid "Failed to unstage selected hunk."
-msgstr "Не удалось исключить выбранную часть."
-
-#: lib/diff.tcl:309
-msgid "Failed to stage selected hunk."
-msgstr "Не удалось подготовить к сохранению выбранную часть."
-
-#: lib/error.tcl:12 lib/error.tcl:102
-msgid "error"
-msgstr "ошибка"
-
-#: lib/error.tcl:28
-msgid "warning"
-msgstr "предупреждение"
-
-#: lib/error.tcl:81
-msgid "You must correct the above errors before committing."
-msgstr "Прежде чем сохранить, исправьте вышеуказанные ошибки."
-
-#: lib/index.tcl:6
-msgid "Unable to unlock the index."
-msgstr "Не удалось разблокировать индекс"
-
-#: lib/index.tcl:15
-msgid "Index Error"
-msgstr "Ошибка индекса"
-
-#: lib/index.tcl:21
-msgid ""
-"Updating the Git index failed.  A rescan will be automatically started to "
-"resynchronize git-gui."
-msgstr ""
-"Не удалось обновить индекс Git. Состояние репозитория будет"
-"перечитано автоматически."
-
-#: lib/index.tcl:27
-msgid "Continue"
-msgstr "Продолжить"
-
-#: lib/index.tcl:31
-msgid "Unlock Index"
-msgstr "Разблокировать индекс"
-
-#: lib/index.tcl:282
-#, tcl-format
-msgid "Unstaging %s from commit"
-msgstr "Удаление %s из подготовленного"
-
-#: lib/index.tcl:326
-#, tcl-format
-msgid "Adding %s"
-msgstr "Добавление %s..."
-
-#: lib/index.tcl:381
-#, tcl-format
-msgid "Revert changes in file %s?"
-msgstr "Отменить изменения в файле %s?"
-
-#: lib/index.tcl:383
-#, tcl-format
-msgid "Revert changes in these %i files?"
-msgstr "Отменить изменения в %i файле(-ах)?"
-
-#: lib/index.tcl:389
-msgid "Any unstaged changes will be permanently lost by the revert."
-msgstr ""
-"Любые изменения, не подготовленные к сохранению, будут потеряны при данной "
-"операции."
-
-#: lib/index.tcl:392
-msgid "Do Nothing"
-msgstr "Ничего не делать"
-
-#: lib/merge.tcl:13
-msgid ""
-"Cannot merge while amending.\n"
-"\n"
-"You must finish amending this commit before starting any type of merge.\n"
-msgstr ""
-"Невозможно выполнить объединение во время исправления.\n"
-"\n"
-"Завершите исправление данного состояния перед выполнением операции "
-"объединения.\n"
-
-#: lib/merge.tcl:27
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before a merge can be performed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-"Последнее прочитанное состояние репозитория не соответствует текущему.\n"
-"\n"
-"С момента последней проверки репозиторий был изменен другой программой Git. "
-"Необходимо перечитать репозиторий, прежде чем изменять текущую ветвь.\n"
-"\n"
-"Это будет сделано сейчас автоматически.\n"
-
-#: lib/merge.tcl:44
-#, tcl-format
-msgid ""
-"You are in the middle of a conflicted merge.\n"
-"\n"
-"File %s has merge conflicts.\n"
-"\n"
-"You must resolve them, stage the file, and commit to complete the current "
-"merge.  Only then can you begin another merge.\n"
-msgstr ""
-"Предыдущее объединение не завершено из-за конфликта.\n"
-"\n"
-"Для файла %s возник конфликт объединения.\n"
-"\n"
-"Разрешите конфликт, подготовьте файл и сохраните. Только после этого можно "
-"начать следующее объединение.\n"
-
-#: lib/merge.tcl:54
-#, tcl-format
-msgid ""
-"You are in the middle of a change.\n"
-"\n"
-"File %s is modified.\n"
-"\n"
-"You should complete the current commit before starting a merge.  Doing so "
-"will help you abort a failed merge, should the need arise.\n"
-msgstr ""
-"Изменения не сохранены.\n"
-"\n"
-"Файл %s изменен.\n"
-"\n"
-"Подготовьте и сохраните измения перед началом объединения. В случае "
-"необходимости это позволит прервать операцию объединения.\n"
-
-#: lib/merge.tcl:106
-#, tcl-format
-msgid "%s of %s"
-msgstr "%s из %s"
-
-#: lib/merge.tcl:119
-#, tcl-format
-msgid "Merging %s and %s"
-msgstr "Объединение %s и %s"
-
-#: lib/merge.tcl:131
-msgid "Merge completed successfully."
-msgstr "Объединение успешно завершено."
-
-#: lib/merge.tcl:133
-msgid "Merge failed.  Conflict resolution is required."
-msgstr "Не удалось завершить объединение. Требуется разрешение конфликта."
-
-#: lib/merge.tcl:158
-#, tcl-format
-msgid "Merge Into %s"
-msgstr "Объединить с %s"
-
-#: lib/merge.tcl:177
-msgid "Revision To Merge"
-msgstr "Версия для объединения"
-
-#: lib/merge.tcl:212
-msgid ""
-"Cannot abort while amending.\n"
-"\n"
-"You must finish amending this commit.\n"
-msgstr ""
-"Невозможно прервать исправление.\n"
-"\n"
-"Завершите текущее исправление сохраненного состояния.\n"
-
-#: lib/merge.tcl:222
-msgid ""
-"Abort merge?\n"
-"\n"
-"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with aborting the current merge?"
-msgstr ""
-"Прервать объединение?\n"
-"\n"
-"Прерывание объединения приведет к потере *ВСЕХ* несохраненных изменений.\n"
-"\n"
-"Продолжить?"
-
-#: lib/merge.tcl:228
-msgid ""
-"Reset changes?\n"
-"\n"
-"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with resetting the current changes?"
-msgstr ""
-"Прервать объединение?\n"
-"\n"
-"Прерывание объединения приведет к потере *ВСЕХ* несохраненных изменений.\n"
-"\n"
-"Продолжить?"
-
-#: lib/merge.tcl:239
-msgid "Aborting"
-msgstr "Прерываю"
-
-#: lib/merge.tcl:266
-msgid "Abort failed."
-msgstr "Прервать не удалось."
-
-#: lib/merge.tcl:268
-msgid "Abort completed.  Ready."
-msgstr "Прервано."
-
-#: lib/option.tcl:82
-msgid "Restore Defaults"
-msgstr "Восстановить настройки по умолчанию"
-
-#: lib/option.tcl:86
-msgid "Save"
-msgstr "Сохранить"
-
-#: lib/option.tcl:96
-#, tcl-format
-msgid "%s Repository"
-msgstr "для репозитория %s"
-
-#: lib/option.tcl:97
-msgid "Global (All Repositories)"
-msgstr "Общие (для всех репозиториев)"
-
-#: lib/option.tcl:103
-msgid "User Name"
-msgstr "Имя пользователя"
-
-#: lib/option.tcl:104
-msgid "Email Address"
-msgstr "Адес электронной почты"
-
-#: lib/option.tcl:106
-msgid "Summarize Merge Commits"
-msgstr "Суммарный комментарий при объединении"
-
-#: lib/option.tcl:107
-msgid "Merge Verbosity"
-msgstr "Уровень детальности сообщений при объединении"
-
-#: lib/option.tcl:108
-msgid "Show Diffstat After Merge"
-msgstr "Показать отчет об изменениях после объединения"
-
-#: lib/option.tcl:110
-msgid "Trust File Modification Timestamps"
-msgstr "Доверять времени модификации файла"
-
-#: lib/option.tcl:111
-msgid "Prune Tracking Branches During Fetch"
-msgstr "Чистка ветвей слежения при получении изменений"
-
-#: lib/option.tcl:112
-msgid "Match Tracking Branches"
-msgstr "Имя новой ветви взять из имен ветвей слежения"
-
-#: lib/option.tcl:113
-msgid "Number of Diff Context Lines"
-msgstr "Число строк в контексте diff"
-
-#: lib/option.tcl:114
-msgid "New Branch Name Template"
-msgstr "Шаблон для имени новой ветви"
-
-#: lib/option.tcl:176
-msgid "Change Font"
-msgstr "Изменить шрифт"
-
-#: lib/option.tcl:180
-#, tcl-format
-msgid "Choose %s"
-msgstr "Выберите %s"
-
-# carbon copy
-#: lib/option.tcl:186
-msgid "pt."
-msgstr ""
-
-#: lib/option.tcl:200
-msgid "Preferences"
-msgstr "Настройки"
-
-#: lib/option.tcl:235
-msgid "Failed to completely save options:"
-msgstr "Не удалось полностью сохранить настройки:"
-
-#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
-msgid "Delete Remote Branch"
-msgstr "Удалить внешнюю ветвь"
-
-#: lib/remote_branch_delete.tcl:47
-msgid "From Repository"
-msgstr "Из репозитория"
-
-#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
-msgid "Remote:"
-msgstr "внешний:"
-
-#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
-msgid "Arbitrary URL:"
-msgstr "по указанному URL:"
-
-#: lib/remote_branch_delete.tcl:84
-msgid "Branches"
-msgstr "Ветви"
-
-#: lib/remote_branch_delete.tcl:109
-msgid "Delete Only If"
-msgstr "Удалить только в случае, если"
-
-#: lib/remote_branch_delete.tcl:111
-msgid "Merged Into:"
-msgstr "Объединено с:"
-
-#: lib/remote_branch_delete.tcl:119
-msgid "Always (Do not perform merge checks)"
-msgstr "Всегда (не выполнять проверку объединений)"
-
-#: lib/remote_branch_delete.tcl:152
-msgid "A branch is required for 'Merged Into'."
-msgstr "Для опции 'Объединено с' требуется указать ветвь."
-
-#: lib/remote_branch_delete.tcl:184
-#, tcl-format
-msgid ""
-"The following branches are not completely merged into %s:\n"
-"\n"
-" - %s"
-msgstr ""
-"Следующие ветви объединены с %s не полностью:\n"
-" - %s"
-
-#: lib/remote_branch_delete.tcl:189
-#, tcl-format
-msgid ""
-"One or more of the merge tests failed because you have not fetched the "
-"necessary commits.  Try fetching from %s first."
-msgstr ""
-"Один или несколько тестов на объединение не прошли, потому что Вы не "
-"получили необходимые состояния. Попытайтесь получить их из %s."
-
-#: lib/remote_branch_delete.tcl:207
-msgid "Please select one or more branches to delete."
-msgstr "Укажите одну или несколько ветвей для удаления."
-
-#: lib/remote_branch_delete.tcl:216
-msgid ""
-"Recovering deleted branches is difficult.\n"
-"\n"
-"Delete the selected branches?"
-msgstr ""
-"Восстановить удаленные ветви сложно.\n"
-"\n"
-"Продолжить?"
-
-#: lib/remote_branch_delete.tcl:226
-#, tcl-format
-msgid "Deleting branches from %s"
-msgstr "Удаление ветвей из %s"
-
-#: lib/remote_branch_delete.tcl:286
-msgid "No repository selected."
-msgstr "Не указан репозиторий."
-
-#: lib/remote_branch_delete.tcl:291
-#, tcl-format
-msgid "Scanning %s..."
-msgstr "Перечитывание %s... "
-
-#: lib/remote.tcl:165
-msgid "Prune from"
-msgstr "Чистка"
-
-#: lib/remote.tcl:170
-msgid "Fetch from"
-msgstr "Получение из"
-
-#: lib/remote.tcl:213
-msgid "Push to"
-msgstr "Отправить"
-
-#: lib/shortcut.tcl:20 lib/shortcut.tcl:61
-msgid "Cannot write shortcut:"
-msgstr "Невозможно записать ссылку:"
-
-#: lib/shortcut.tcl:136
-msgid "Cannot write icon:"
-msgstr "Невозможно записать значок:"
-
-#: lib/status_bar.tcl:83
-#, tcl-format
-msgid "%s ... %*i of %*i %s (%3i%%)"
-msgstr "%s ... %*i из %*i %s (%3i%%)"
-
-#: lib/transport.tcl:6
-#, tcl-format
-msgid "fetch %s"
-msgstr "получение %s"
-
-#: lib/transport.tcl:7
-#, tcl-format
-msgid "Fetching new changes from %s"
-msgstr "Получение изменений из %s "
-
-# carbon copy
-#: lib/transport.tcl:18
-#, tcl-format
-msgid "remote prune %s"
-msgstr "чистка внешнего %s"
-
-#: lib/transport.tcl:19
-#, tcl-format
-msgid "Pruning tracking branches deleted from %s"
-msgstr "Чистка ветвей слежения, удаленных из %s"
-
-#: lib/transport.tcl:25 lib/transport.tcl:71
-#, tcl-format
-msgid "push %s"
-msgstr "отправить %s"
-
-#: lib/transport.tcl:26
-#, tcl-format
-msgid "Pushing changes to %s"
-msgstr "Отправка изменений в %s "
-
-#: lib/transport.tcl:72
-#, tcl-format
-msgid "Pushing %s %s to %s"
-msgstr "Отправка %s %s в %s"
-
-#: lib/transport.tcl:89
-msgid "Push Branches"
-msgstr "Отправить изменения в ветвях"
-
-#: lib/transport.tcl:103
-msgid "Source Branches"
-msgstr "Исходные ветви"
-
-#: lib/transport.tcl:120
-msgid "Destination Repository"
-msgstr "Репозиторий назначения"
-
-#: lib/transport.tcl:158
-msgid "Transfer Options"
-msgstr "Настройки отправки"
-
-#: lib/transport.tcl:160
-msgid "Force overwrite existing branch (may discard changes)"
-msgstr "Намеренно переписать существующую ветвь (возможна потеря изменений)"
-
-#: lib/transport.tcl:164
-msgid "Use thin pack (for slow network connections)"
-msgstr "Использовать thin pack (для медленных сетевых подключений)"
-
-#: lib/transport.tcl:168
-msgid "Include tags"
-msgstr "Передать таги"
-
-#~ msgid "Next >"
-#~ msgstr "Дальше >"
diff --git a/po/zh_cn.po b/po/zh_cn.po
deleted file mode 100644 (file)
index 621c947..0000000
+++ /dev/null
@@ -1,1769 +0,0 @@
-# Translation of git-gui to Chinese
-# Copyright (C) 2007 Shawn Pearce
-# This file is distributed under the same license as the git-gui package.
-# Xudong Guan <xudong.guan@gmail.com>, 2007.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: git-gui\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2007-10-10 04:04-0400\n"
-"PO-Revision-Date: 2007-07-21 01:23-0700\n"
-"Last-Translator: Xudong Guan <xudong.guan@gmail.com>\n"
-"Language-Team: Chinese\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: git-gui.sh:41 git-gui.sh:634 git-gui.sh:648 git-gui.sh:661 git-gui.sh:744
-#: git-gui.sh:763
-msgid "git-gui: fatal error"
-msgstr ""
-
-#: git-gui.sh:595
-#, tcl-format
-msgid "Invalid font specified in %s:"
-msgstr ""
-
-#: git-gui.sh:620
-msgid "Main Font"
-msgstr ""
-
-#: git-gui.sh:621
-msgid "Diff/Console Font"
-msgstr ""
-
-#: git-gui.sh:635
-msgid "Cannot find git in PATH."
-msgstr ""
-
-#: git-gui.sh:662
-msgid "Cannot parse Git version string:"
-msgstr ""
-
-#: git-gui.sh:680
-#, tcl-format
-msgid ""
-"Git version cannot be determined.\n"
-"\n"
-"%s claims it is version '%s'.\n"
-"\n"
-"%s requires at least Git 1.5.0 or later.\n"
-"\n"
-"Assume '%s' is version 1.5.0?\n"
-msgstr ""
-
-#: git-gui.sh:853
-msgid "Git directory not found:"
-msgstr ""
-
-#: git-gui.sh:860
-msgid "Cannot move to top of working directory:"
-msgstr ""
-
-#: git-gui.sh:867
-msgid "Cannot use funny .git directory:"
-msgstr ""
-
-#: git-gui.sh:872
-msgid "No working directory"
-msgstr ""
-
-#: git-gui.sh:1019
-msgid "Refreshing file status..."
-msgstr ""
-
-#: git-gui.sh:1084
-msgid "Scanning for modified files ..."
-msgstr ""
-
-#: git-gui.sh:1259 lib/browser.tcl:245
-#, fuzzy
-msgid "Ready."
-msgstr "重做"
-
-#: git-gui.sh:1525
-msgid "Unmodified"
-msgstr ""
-
-#: git-gui.sh:1527
-msgid "Modified, not staged"
-msgstr ""
-
-#: git-gui.sh:1528 git-gui.sh:1533
-#, fuzzy
-msgid "Staged for commit"
-msgstr "从本次提交移除"
-
-#: git-gui.sh:1529 git-gui.sh:1534
-#, fuzzy
-msgid "Portions staged for commit"
-msgstr "从本次提交移除"
-
-#: git-gui.sh:1530 git-gui.sh:1535
-msgid "Staged for commit, missing"
-msgstr ""
-
-#: git-gui.sh:1532
-msgid "Untracked, not staged"
-msgstr ""
-
-#: git-gui.sh:1537
-msgid "Missing"
-msgstr ""
-
-#: git-gui.sh:1538
-msgid "Staged for removal"
-msgstr ""
-
-#: git-gui.sh:1539
-msgid "Staged for removal, still present"
-msgstr ""
-
-#: git-gui.sh:1541 git-gui.sh:1542 git-gui.sh:1543 git-gui.sh:1544
-msgid "Requires merge resolution"
-msgstr ""
-
-#: git-gui.sh:1579
-msgid "Starting gitk... please wait..."
-msgstr ""
-
-#: git-gui.sh:1588
-#, tcl-format
-msgid ""
-"Unable to start gitk:\n"
-"\n"
-"%s does not exist"
-msgstr ""
-
-#: git-gui.sh:1788 lib/choose_repository.tcl:32
-msgid "Repository"
-msgstr "版本树"
-
-#: git-gui.sh:1789
-msgid "Edit"
-msgstr "编辑"
-
-#: git-gui.sh:1791 lib/choose_rev.tcl:560
-msgid "Branch"
-msgstr "分支"
-
-#: git-gui.sh:1794 lib/choose_rev.tcl:547
-#, fuzzy
-msgid "Commit@@noun"
-msgstr "提交"
-
-#: git-gui.sh:1797 lib/merge.tcl:121 lib/merge.tcl:150 lib/merge.tcl:168
-msgid "Merge"
-msgstr "合并"
-
-#: git-gui.sh:1798 lib/choose_rev.tcl:556
-#, fuzzy
-msgid "Remote"
-msgstr "改名..."
-
-#: git-gui.sh:1807
-msgid "Browse Current Branch's Files"
-msgstr "浏览当前分支文件"
-
-#: git-gui.sh:1811
-#, fuzzy
-msgid "Browse Branch Files..."
-msgstr "浏览当前分支文件"
-
-#: git-gui.sh:1816
-msgid "Visualize Current Branch's History"
-msgstr "调用gitk显示当前分支"
-
-#: git-gui.sh:1820
-msgid "Visualize All Branch History"
-msgstr "调用gitk显示所有分支"
-
-#: git-gui.sh:1827
-#, fuzzy, tcl-format
-msgid "Browse %s's Files"
-msgstr "浏览当前分支文件"
-
-#: git-gui.sh:1829
-#, fuzzy, tcl-format
-msgid "Visualize %s's History"
-msgstr "调用gitk显示所有分支"
-
-#: git-gui.sh:1834 lib/database.tcl:27 lib/database.tcl:67
-msgid "Database Statistics"
-msgstr "数据库统计数据"
-
-#: git-gui.sh:1837 lib/database.tcl:34
-msgid "Compress Database"
-msgstr "压缩数据库"
-
-#: git-gui.sh:1840
-msgid "Verify Database"
-msgstr "验证数据库"
-
-#: git-gui.sh:1847 git-gui.sh:1851 git-gui.sh:1855 lib/shortcut.tcl:9
-#: lib/shortcut.tcl:45 lib/shortcut.tcl:84
-msgid "Create Desktop Icon"
-msgstr "创建桌面图标"
-
-#: git-gui.sh:1860 lib/choose_repository.tcl:36 lib/choose_repository.tcl:95
-msgid "Quit"
-msgstr "退出"
-
-#: git-gui.sh:1867
-msgid "Undo"
-msgstr "撤销"
-
-#: git-gui.sh:1870
-msgid "Redo"
-msgstr "重做"
-
-#: git-gui.sh:1874 git-gui.sh:2366
-msgid "Cut"
-msgstr "剪切"
-
-#: git-gui.sh:1877 git-gui.sh:2369 git-gui.sh:2440 git-gui.sh:2512
-#: lib/console.tcl:67
-msgid "Copy"
-msgstr "复制"
-
-#: git-gui.sh:1880 git-gui.sh:2372
-msgid "Paste"
-msgstr "粘贴"
-
-#: git-gui.sh:1883 git-gui.sh:2375 lib/branch_delete.tcl:26
-#: lib/remote_branch_delete.tcl:38
-msgid "Delete"
-msgstr "删除"
-
-#: git-gui.sh:1887 git-gui.sh:2379 git-gui.sh:2516 lib/console.tcl:69
-msgid "Select All"
-msgstr "全选"
-
-#: git-gui.sh:1896
-msgid "Create..."
-msgstr "新建..."
-
-#: git-gui.sh:1902
-msgid "Checkout..."
-msgstr "切换..."
-
-#: git-gui.sh:1908
-msgid "Rename..."
-msgstr "改名..."
-
-#: git-gui.sh:1913 git-gui.sh:2012
-msgid "Delete..."
-msgstr "删除..."
-
-#: git-gui.sh:1918
-msgid "Reset..."
-msgstr "重置所有修动..."
-
-#: git-gui.sh:1930 git-gui.sh:2313
-msgid "New Commit"
-msgstr "新提交"
-
-#: git-gui.sh:1938 git-gui.sh:2320
-msgid "Amend Last Commit"
-msgstr "修订上次提交"
-
-#: git-gui.sh:1947 git-gui.sh:2280 lib/remote_branch_delete.tcl:99
-msgid "Rescan"
-msgstr "重新扫描"
-
-#: git-gui.sh:1953
-#, fuzzy
-msgid "Stage To Commit"
-msgstr "从本次提交移除"
-
-#: git-gui.sh:1958
-#, fuzzy
-msgid "Stage Changed Files To Commit"
-msgstr "将被提交的修改"
-
-#: git-gui.sh:1964
-msgid "Unstage From Commit"
-msgstr "从本次提交移除"
-
-#: git-gui.sh:1969 lib/index.tcl:352
-msgid "Revert Changes"
-msgstr "恢复修改"
-
-#: git-gui.sh:1976 git-gui.sh:2292 git-gui.sh:2390
-msgid "Sign Off"
-msgstr "签名"
-
-#: git-gui.sh:1980 git-gui.sh:2296
-#, fuzzy
-msgid "Commit@@verb"
-msgstr "提交"
-
-#: git-gui.sh:1991
-msgid "Local Merge..."
-msgstr "本地合并..."
-
-#: git-gui.sh:1996
-msgid "Abort Merge..."
-msgstr "取消合并..."
-
-#: git-gui.sh:2008
-msgid "Push..."
-msgstr "上传..."
-
-#: git-gui.sh:2019 lib/choose_repository.tcl:41
-msgid "Apple"
-msgstr "苹果"
-
-#: git-gui.sh:2022 git-gui.sh:2044 lib/about.tcl:13
-#: lib/choose_repository.tcl:44 lib/choose_repository.tcl:50
-#, tcl-format
-msgid "About %s"
-msgstr "关于%s"
-
-#: git-gui.sh:2026
-msgid "Preferences..."
-msgstr ""
-
-#: git-gui.sh:2034 git-gui.sh:2558
-msgid "Options..."
-msgstr "选项..."
-
-#: git-gui.sh:2040 lib/choose_repository.tcl:47
-msgid "Help"
-msgstr "帮助"
-
-#: git-gui.sh:2081
-msgid "Online Documentation"
-msgstr "在线文档"
-
-#: git-gui.sh:2165
-#, tcl-format
-msgid "fatal: cannot stat path %s: No such file or directory"
-msgstr ""
-
-#: git-gui.sh:2198
-msgid "Current Branch:"
-msgstr "当前分支:"
-
-#: git-gui.sh:2219
-#, fuzzy
-msgid "Staged Changes (Will Commit)"
-msgstr "将被提交的修改"
-
-#: git-gui.sh:2239
-msgid "Unstaged Changes"
-msgstr ""
-
-#: git-gui.sh:2286
-msgid "Stage Changed"
-msgstr ""
-
-#: git-gui.sh:2302 lib/transport.tcl:93 lib/transport.tcl:182
-msgid "Push"
-msgstr "上传"
-
-#: git-gui.sh:2332
-msgid "Initial Commit Message:"
-msgstr "初始提交描述:"
-
-#: git-gui.sh:2333
-msgid "Amended Commit Message:"
-msgstr "修订提交描述:"
-
-#: git-gui.sh:2334
-msgid "Amended Initial Commit Message:"
-msgstr "修订初始提交描述:"
-
-#: git-gui.sh:2335
-msgid "Amended Merge Commit Message:"
-msgstr "修订合并提交描述:"
-
-#: git-gui.sh:2336
-msgid "Merge Commit Message:"
-msgstr "合并提交描述:"
-
-#: git-gui.sh:2337
-msgid "Commit Message:"
-msgstr "提交描述:"
-
-#: git-gui.sh:2382 git-gui.sh:2520 lib/console.tcl:71
-msgid "Copy All"
-msgstr "全部复制"
-
-#: git-gui.sh:2406 lib/blame.tcl:104
-msgid "File:"
-msgstr ""
-
-#: git-gui.sh:2508
-msgid "Refresh"
-msgstr "刷新"
-
-#: git-gui.sh:2529
-msgid "Apply/Reverse Hunk"
-msgstr "应用/撤消此修改块"
-
-#: git-gui.sh:2535
-msgid "Decrease Font Size"
-msgstr "缩小字体"
-
-#: git-gui.sh:2539
-msgid "Increase Font Size"
-msgstr "放大字体"
-
-#: git-gui.sh:2544
-msgid "Show Less Context"
-msgstr "显示更多diff上下文"
-
-#: git-gui.sh:2551
-msgid "Show More Context"
-msgstr "显示更少diff上下文"
-
-#: git-gui.sh:2565
-#, fuzzy
-msgid "Unstage Hunk From Commit"
-msgstr "从本次提交移除"
-
-#: git-gui.sh:2567
-#, fuzzy
-msgid "Stage Hunk For Commit"
-msgstr "从本次提交移除"
-
-#: git-gui.sh:2586
-msgid "Initializing..."
-msgstr ""
-
-#: git-gui.sh:2677
-#, tcl-format
-msgid ""
-"Possible environment issues exist.\n"
-"\n"
-"The following environment variables are probably\n"
-"going to be ignored by any Git subprocess run\n"
-"by %s:\n"
-"\n"
-msgstr ""
-
-#: git-gui.sh:2707
-msgid ""
-"\n"
-"This is due to a known issue with the\n"
-"Tcl binary distributed by Cygwin."
-msgstr ""
-
-#: git-gui.sh:2712
-#, tcl-format
-msgid ""
-"\n"
-"\n"
-"A good replacement for %s\n"
-"is placing values for the user.name and\n"
-"user.email settings into your personal\n"
-"~/.gitconfig file.\n"
-msgstr ""
-
-#: lib/about.tcl:25
-msgid "git-gui - a graphical user interface for Git."
-msgstr ""
-
-#: lib/blame.tcl:77
-msgid "File Viewer"
-msgstr ""
-
-#: lib/blame.tcl:81
-#, fuzzy
-msgid "Commit:"
-msgstr "提交"
-
-#: lib/blame.tcl:249
-#, fuzzy
-msgid "Copy Commit"
-msgstr "提交"
-
-#: lib/blame.tcl:369
-#, tcl-format
-msgid "Reading %s..."
-msgstr ""
-
-#: lib/blame.tcl:473
-msgid "Loading copy/move tracking annotations..."
-msgstr ""
-
-#: lib/blame.tcl:493
-msgid "lines annotated"
-msgstr ""
-
-#: lib/blame.tcl:674
-msgid "Loading original location annotations..."
-msgstr ""
-
-#: lib/blame.tcl:677
-msgid "Annotation complete."
-msgstr ""
-
-#: lib/blame.tcl:731
-msgid "Loading annotation..."
-msgstr ""
-
-#: lib/blame.tcl:787
-msgid "Author:"
-msgstr ""
-
-#: lib/blame.tcl:791
-#, fuzzy
-msgid "Committer:"
-msgstr "提交"
-
-#: lib/blame.tcl:796
-msgid "Original File:"
-msgstr ""
-
-#: lib/blame.tcl:910
-msgid "Originally By:"
-msgstr ""
-
-#: lib/blame.tcl:916
-msgid "In File:"
-msgstr ""
-
-#: lib/blame.tcl:921
-msgid "Copied Or Moved Here By:"
-msgstr ""
-
-#: lib/branch_checkout.tcl:14 lib/branch_checkout.tcl:19
-#, fuzzy
-msgid "Checkout Branch"
-msgstr "当前分支:"
-
-#: lib/branch_checkout.tcl:23
-#, fuzzy
-msgid "Checkout"
-msgstr "切换..."
-
-#: lib/branch_checkout.tcl:27 lib/branch_create.tcl:35
-#: lib/branch_delete.tcl:32 lib/branch_rename.tcl:30 lib/browser.tcl:281
-#: lib/checkout_op.tcl:522 lib/choose_font.tcl:43 lib/merge.tcl:172
-#: lib/option.tcl:90 lib/remote_branch_delete.tcl:42 lib/transport.tcl:97
-msgid "Cancel"
-msgstr ""
-
-#: lib/branch_checkout.tcl:32 lib/browser.tcl:286
-msgid "Revision"
-msgstr ""
-
-#: lib/branch_checkout.tcl:36 lib/branch_create.tcl:69 lib/option.tcl:202
-#, fuzzy
-msgid "Options"
-msgstr "选项..."
-
-#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:92
-msgid "Fetch Tracking Branch"
-msgstr ""
-
-#: lib/branch_checkout.tcl:44
-msgid "Detach From Local Branch"
-msgstr ""
-
-#: lib/branch_create.tcl:22
-#, fuzzy
-msgid "Create Branch"
-msgstr "当前分支:"
-
-#: lib/branch_create.tcl:27
-#, fuzzy
-msgid "Create New Branch"
-msgstr "当前分支:"
-
-#: lib/branch_create.tcl:31 lib/choose_repository.tcl:199
-#, fuzzy
-msgid "Create"
-msgstr "新建..."
-
-#: lib/branch_create.tcl:40
-#, fuzzy
-msgid "Branch Name"
-msgstr "分支"
-
-#: lib/branch_create.tcl:43
-msgid "Name:"
-msgstr ""
-
-#: lib/branch_create.tcl:58
-msgid "Match Tracking Branch Name"
-msgstr ""
-
-#: lib/branch_create.tcl:66
-msgid "Starting Revision"
-msgstr ""
-
-#: lib/branch_create.tcl:72
-msgid "Update Existing Branch:"
-msgstr ""
-
-#: lib/branch_create.tcl:75
-msgid "No"
-msgstr ""
-
-#: lib/branch_create.tcl:80
-msgid "Fast Forward Only"
-msgstr ""
-
-#: lib/branch_create.tcl:85 lib/checkout_op.tcl:514
-#, fuzzy
-msgid "Reset"
-msgstr "重置所有修动..."
-
-#: lib/branch_create.tcl:97
-msgid "Checkout After Creation"
-msgstr ""
-
-#: lib/branch_create.tcl:131
-msgid "Please select a tracking branch."
-msgstr ""
-
-#: lib/branch_create.tcl:140
-#, tcl-format
-msgid "Tracking branch %s is not a branch in the remote repository."
-msgstr ""
-
-#: lib/branch_create.tcl:153 lib/branch_rename.tcl:86
-msgid "Please supply a branch name."
-msgstr ""
-
-#: lib/branch_create.tcl:164 lib/branch_rename.tcl:106
-#, tcl-format
-msgid "'%s' is not an acceptable branch name."
-msgstr ""
-
-#: lib/branch_delete.tcl:15
-#, fuzzy
-msgid "Delete Branch"
-msgstr "当前分支:"
-
-#: lib/branch_delete.tcl:20
-msgid "Delete Local Branch"
-msgstr ""
-
-#: lib/branch_delete.tcl:37
-#, fuzzy
-msgid "Local Branches"
-msgstr "分支"
-
-#: lib/branch_delete.tcl:52
-msgid "Delete Only If Merged Into"
-msgstr ""
-
-#: lib/branch_delete.tcl:54
-msgid "Always (Do not perform merge test.)"
-msgstr ""
-
-#: lib/branch_delete.tcl:103
-#, tcl-format
-msgid "The following branches are not completely merged into %s:"
-msgstr ""
-
-#: lib/branch_delete.tcl:115
-msgid ""
-"Recovering deleted branches is difficult. \n"
-"\n"
-" Delete the selected branches?"
-msgstr ""
-
-#: lib/branch_delete.tcl:141
-#, tcl-format
-msgid ""
-"Failed to delete branches:\n"
-"%s"
-msgstr ""
-
-#: lib/branch_rename.tcl:14 lib/branch_rename.tcl:22
-#, fuzzy
-msgid "Rename Branch"
-msgstr "当前分支:"
-
-#: lib/branch_rename.tcl:26
-#, fuzzy
-msgid "Rename"
-msgstr "改名..."
-
-#: lib/branch_rename.tcl:36
-#, fuzzy
-msgid "Branch:"
-msgstr "分支"
-
-#: lib/branch_rename.tcl:39
-msgid "New Name:"
-msgstr ""
-
-#: lib/branch_rename.tcl:75
-msgid "Please select a branch to rename."
-msgstr ""
-
-#: lib/branch_rename.tcl:96 lib/checkout_op.tcl:179
-#, tcl-format
-msgid "Branch '%s' already exists."
-msgstr ""
-
-#: lib/branch_rename.tcl:117
-#, tcl-format
-msgid "Failed to rename '%s'."
-msgstr ""
-
-#: lib/browser.tcl:17
-msgid "Starting..."
-msgstr ""
-
-#: lib/browser.tcl:26
-msgid "File Browser"
-msgstr ""
-
-#: lib/browser.tcl:125 lib/browser.tcl:142
-#, tcl-format
-msgid "Loading %s..."
-msgstr ""
-
-#: lib/browser.tcl:186
-msgid "[Up To Parent]"
-msgstr ""
-
-#: lib/browser.tcl:266 lib/browser.tcl:272
-#, fuzzy
-msgid "Browse Branch Files"
-msgstr "浏览当前分支文件"
-
-#: lib/browser.tcl:277 lib/choose_repository.tcl:215
-#: lib/choose_repository.tcl:305 lib/choose_repository.tcl:315
-#: lib/choose_repository.tcl:811
-msgid "Browse"
-msgstr ""
-
-#: lib/checkout_op.tcl:79
-#, tcl-format
-msgid "Fetching %s from %s"
-msgstr ""
-
-#: lib/checkout_op.tcl:127
-#, tcl-format
-msgid "fatal: Cannot resolve %s"
-msgstr ""
-
-#: lib/checkout_op.tcl:140 lib/console.tcl:79 lib/database.tcl:31
-msgid "Close"
-msgstr ""
-
-#: lib/checkout_op.tcl:169
-#, tcl-format
-msgid "Branch '%s' does not exist."
-msgstr ""
-
-#: lib/checkout_op.tcl:206
-#, tcl-format
-msgid ""
-"Branch '%s' already exists.\n"
-"\n"
-"It cannot fast-forward to %s.\n"
-"A merge is required."
-msgstr ""
-
-#: lib/checkout_op.tcl:220
-#, tcl-format
-msgid "Merge strategy '%s' not supported."
-msgstr ""
-
-#: lib/checkout_op.tcl:239
-#, tcl-format
-msgid "Failed to update '%s'."
-msgstr ""
-
-#: lib/checkout_op.tcl:251
-msgid "Staging area (index) is already locked."
-msgstr ""
-
-#: lib/checkout_op.tcl:266
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before the current branch can be changed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-
-#: lib/checkout_op.tcl:322
-#, tcl-format
-msgid "Updating working directory to '%s'..."
-msgstr ""
-
-#: lib/checkout_op.tcl:353
-#, tcl-format
-msgid "Aborted checkout of '%s' (file level merging is required)."
-msgstr ""
-
-#: lib/checkout_op.tcl:354
-msgid "File level merge required."
-msgstr ""
-
-#: lib/checkout_op.tcl:358
-#, tcl-format
-msgid "Staying on branch '%s'."
-msgstr ""
-
-#: lib/checkout_op.tcl:429
-msgid ""
-"You are no longer on a local branch.\n"
-"\n"
-"If you wanted to be on a branch, create one now starting from 'This Detached "
-"Checkout'."
-msgstr ""
-
-#: lib/checkout_op.tcl:446
-#, fuzzy, tcl-format
-msgid "Checked out '%s'."
-msgstr "切换..."
-
-#: lib/checkout_op.tcl:478
-#, tcl-format
-msgid "Resetting '%s' to '%s' will lose the following commits:"
-msgstr ""
-
-#: lib/checkout_op.tcl:500
-msgid "Recovering lost commits may not be easy."
-msgstr ""
-
-#: lib/checkout_op.tcl:505
-#, tcl-format
-msgid "Reset '%s'?"
-msgstr ""
-
-#: lib/checkout_op.tcl:510 lib/merge.tcl:164
-msgid "Visualize"
-msgstr ""
-
-#: lib/checkout_op.tcl:578
-#, tcl-format
-msgid ""
-"Failed to set current branch.\n"
-"\n"
-"This working directory is only partially switched.  We successfully updated "
-"your files, but failed to update an internal Git file.\n"
-"\n"
-"This should not have occurred.  %s will now close and give up."
-msgstr ""
-
-#: lib/choose_font.tcl:39
-#, fuzzy
-msgid "Select"
-msgstr "全选"
-
-#: lib/choose_font.tcl:53
-msgid "Font Family"
-msgstr ""
-
-#: lib/choose_font.tcl:73
-#, fuzzy
-msgid "Font Size"
-msgstr "缩小字体"
-
-#: lib/choose_font.tcl:90
-msgid "Font Example"
-msgstr ""
-
-#: lib/choose_font.tcl:101
-msgid ""
-"This is example text.\n"
-"If you like this text, it can be your font."
-msgstr ""
-
-#: lib/choose_repository.tcl:25
-msgid "Git Gui"
-msgstr ""
-
-#: lib/choose_repository.tcl:69 lib/choose_repository.tcl:204
-#, fuzzy
-msgid "Create New Repository"
-msgstr "版本树"
-
-#: lib/choose_repository.tcl:74 lib/choose_repository.tcl:291
-#, fuzzy
-msgid "Clone Existing Repository"
-msgstr "版本树"
-
-#: lib/choose_repository.tcl:79 lib/choose_repository.tcl:800
-#, fuzzy
-msgid "Open Existing Repository"
-msgstr "版本树"
-
-#: lib/choose_repository.tcl:91
-msgid "Next >"
-msgstr ""
-
-#: lib/choose_repository.tcl:152
-#, tcl-format
-msgid "Location %s already exists."
-msgstr ""
-
-#: lib/choose_repository.tcl:158 lib/choose_repository.tcl:165
-#: lib/choose_repository.tcl:172
-#, tcl-format
-msgid "Failed to create repository %s:"
-msgstr ""
-
-#: lib/choose_repository.tcl:209 lib/choose_repository.tcl:309
-msgid "Directory:"
-msgstr ""
-
-#: lib/choose_repository.tcl:238 lib/choose_repository.tcl:363
-#: lib/choose_repository.tcl:834
-#, fuzzy
-msgid "Git Repository"
-msgstr "版本树"
-
-#: lib/choose_repository.tcl:253 lib/choose_repository.tcl:260
-#, tcl-format
-msgid "Directory %s already exists."
-msgstr ""
-
-#: lib/choose_repository.tcl:265
-#, tcl-format
-msgid "File %s already exists."
-msgstr ""
-
-#: lib/choose_repository.tcl:286
-msgid "Clone"
-msgstr ""
-
-#: lib/choose_repository.tcl:299
-msgid "URL:"
-msgstr ""
-
-#: lib/choose_repository.tcl:319
-msgid "Clone Type:"
-msgstr ""
-
-#: lib/choose_repository.tcl:325
-msgid "Standard (Fast, Semi-Redundant, Hardlinks)"
-msgstr ""
-
-#: lib/choose_repository.tcl:331
-msgid "Full Copy (Slower, Redundant Backup)"
-msgstr ""
-
-#: lib/choose_repository.tcl:337
-msgid "Shared (Fastest, Not Recommended, No Backup)"
-msgstr ""
-
-#: lib/choose_repository.tcl:369 lib/choose_repository.tcl:418
-#: lib/choose_repository.tcl:560 lib/choose_repository.tcl:630
-#: lib/choose_repository.tcl:840 lib/choose_repository.tcl:848
-#, tcl-format
-msgid "Not a Git repository: %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:405
-msgid "Standard only available for local repository."
-msgstr ""
-
-#: lib/choose_repository.tcl:409
-msgid "Shared only available for local repository."
-msgstr ""
-
-#: lib/choose_repository.tcl:439
-msgid "Failed to configure origin"
-msgstr ""
-
-#: lib/choose_repository.tcl:451
-msgid "Counting objects"
-msgstr ""
-
-#: lib/choose_repository.tcl:452
-msgid "buckets"
-msgstr ""
-
-#: lib/choose_repository.tcl:476
-#, tcl-format
-msgid "Unable to copy objects/info/alternates: %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:512
-#, tcl-format
-msgid "Nothing to clone from %s."
-msgstr ""
-
-#: lib/choose_repository.tcl:514 lib/choose_repository.tcl:728
-#: lib/choose_repository.tcl:740
-msgid "The 'master' branch has not been initialized."
-msgstr ""
-
-#: lib/choose_repository.tcl:527
-msgid "Hardlinks are unavailable.  Falling back to copying."
-msgstr ""
-
-#: lib/choose_repository.tcl:539
-#, tcl-format
-msgid "Cloning from %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:570
-#, fuzzy
-msgid "Copying objects"
-msgstr "压缩数据库"
-
-#: lib/choose_repository.tcl:571
-msgid "KiB"
-msgstr ""
-
-#: lib/choose_repository.tcl:595
-#, tcl-format
-msgid "Unable to copy object: %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:605
-msgid "Linking objects"
-msgstr ""
-
-#: lib/choose_repository.tcl:606
-msgid "objects"
-msgstr ""
-
-#: lib/choose_repository.tcl:614
-#, tcl-format
-msgid "Unable to hardlink object: %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:669
-msgid "Cannot fetch branches and objects.  See console output for details."
-msgstr ""
-
-#: lib/choose_repository.tcl:680
-msgid "Cannot fetch tags.  See console output for details."
-msgstr ""
-
-#: lib/choose_repository.tcl:704
-msgid "Cannot determine HEAD.  See console output for details."
-msgstr ""
-
-#: lib/choose_repository.tcl:713
-#, tcl-format
-msgid "Unable to cleanup %s"
-msgstr ""
-
-#: lib/choose_repository.tcl:719
-msgid "Clone failed."
-msgstr ""
-
-#: lib/choose_repository.tcl:726
-msgid "No default branch obtained."
-msgstr ""
-
-#: lib/choose_repository.tcl:737
-#, tcl-format
-msgid "Cannot resolve %s as a commit."
-msgstr ""
-
-#: lib/choose_repository.tcl:749
-msgid "Creating working directory"
-msgstr ""
-
-#: lib/choose_repository.tcl:750 lib/index.tcl:15 lib/index.tcl:80
-#: lib/index.tcl:149
-msgid "files"
-msgstr ""
-
-#: lib/choose_repository.tcl:779
-msgid "Initial file checkout failed."
-msgstr ""
-
-#: lib/choose_repository.tcl:795
-msgid "Open"
-msgstr ""
-
-#: lib/choose_repository.tcl:805
-#, fuzzy
-msgid "Repository:"
-msgstr "版本树"
-
-#: lib/choose_repository.tcl:854
-#, tcl-format
-msgid "Failed to open repository %s:"
-msgstr ""
-
-#: lib/choose_rev.tcl:53
-msgid "This Detached Checkout"
-msgstr ""
-
-#: lib/choose_rev.tcl:60
-msgid "Revision Expression:"
-msgstr ""
-
-#: lib/choose_rev.tcl:74
-#, fuzzy
-msgid "Local Branch"
-msgstr "分支"
-
-#: lib/choose_rev.tcl:79
-#, fuzzy
-msgid "Tracking Branch"
-msgstr "当前分支:"
-
-#: lib/choose_rev.tcl:84 lib/choose_rev.tcl:537
-msgid "Tag"
-msgstr ""
-
-#: lib/choose_rev.tcl:317
-#, tcl-format
-msgid "Invalid revision: %s"
-msgstr ""
-
-#: lib/choose_rev.tcl:338
-msgid "No revision selected."
-msgstr ""
-
-#: lib/choose_rev.tcl:346
-msgid "Revision expression is empty."
-msgstr ""
-
-#: lib/choose_rev.tcl:530
-msgid "Updated"
-msgstr ""
-
-#: lib/choose_rev.tcl:558
-msgid "URL"
-msgstr ""
-
-#: lib/commit.tcl:9
-msgid ""
-"There is nothing to amend.\n"
-"\n"
-"You are about to create the initial commit.  There is no commit before this "
-"to amend.\n"
-msgstr ""
-
-#: lib/commit.tcl:18
-msgid ""
-"Cannot amend while merging.\n"
-"\n"
-"You are currently in the middle of a merge that has not been fully "
-"completed.  You cannot amend the prior commit unless you first abort the "
-"current merge activity.\n"
-msgstr ""
-
-#: lib/commit.tcl:49
-msgid "Error loading commit data for amend:"
-msgstr ""
-
-#: lib/commit.tcl:76
-msgid "Unable to obtain your identity:"
-msgstr ""
-
-#: lib/commit.tcl:81
-msgid "Invalid GIT_COMMITTER_IDENT:"
-msgstr ""
-
-#: lib/commit.tcl:133
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before another commit can be created.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-
-#: lib/commit.tcl:154
-#, tcl-format
-msgid ""
-"Unmerged files cannot be committed.\n"
-"\n"
-"File %s has merge conflicts.  You must resolve them and stage the file "
-"before committing.\n"
-msgstr ""
-
-#: lib/commit.tcl:162
-#, tcl-format
-msgid ""
-"Unknown file state %s detected.\n"
-"\n"
-"File %s cannot be committed by this program.\n"
-msgstr ""
-
-#: lib/commit.tcl:170
-msgid ""
-"No changes to commit.\n"
-"\n"
-"You must stage at least 1 file before you can commit.\n"
-msgstr ""
-
-#: lib/commit.tcl:183
-msgid ""
-"Please supply a commit message.\n"
-"\n"
-"A good commit message has the following format:\n"
-"\n"
-"- First line: Describe in one sentance what you did.\n"
-"- Second line: Blank\n"
-"- Remaining lines: Describe why this change is good.\n"
-msgstr ""
-
-#: lib/commit.tcl:257
-msgid "write-tree failed:"
-msgstr ""
-
-#: lib/commit.tcl:275
-#, tcl-format
-msgid "Commit %s appears to be corrupt"
-msgstr ""
-
-#: lib/commit.tcl:279
-msgid ""
-"No changes to commit.\n"
-"\n"
-"No files were modified by this commit and it was not a merge commit.\n"
-"\n"
-"A rescan will be automatically started now.\n"
-msgstr ""
-
-#: lib/commit.tcl:286
-msgid "No changes to commit."
-msgstr ""
-
-#: lib/commit.tcl:303
-#, tcl-format
-msgid "warning: Tcl does not support encoding '%s'."
-msgstr ""
-
-#: lib/commit.tcl:317
-msgid "commit-tree failed:"
-msgstr ""
-
-#: lib/commit.tcl:339
-msgid "update-ref failed:"
-msgstr ""
-
-#: lib/commit.tcl:430
-#, tcl-format
-msgid "Created commit %s: %s"
-msgstr ""
-
-#: lib/console.tcl:57
-msgid "Working... please wait..."
-msgstr ""
-
-#: lib/console.tcl:183
-msgid "Success"
-msgstr ""
-
-#: lib/console.tcl:196
-msgid "Error: Command Failed"
-msgstr ""
-
-#: lib/database.tcl:43
-msgid "Number of loose objects"
-msgstr ""
-
-#: lib/database.tcl:44
-msgid "Disk space used by loose objects"
-msgstr ""
-
-#: lib/database.tcl:45
-msgid "Number of packed objects"
-msgstr ""
-
-#: lib/database.tcl:46
-msgid "Number of packs"
-msgstr ""
-
-#: lib/database.tcl:47
-msgid "Disk space used by packed objects"
-msgstr ""
-
-#: lib/database.tcl:48
-msgid "Packed objects waiting for pruning"
-msgstr ""
-
-#: lib/database.tcl:49
-msgid "Garbage files"
-msgstr ""
-
-#: lib/database.tcl:72
-#, fuzzy
-msgid "Compressing the object database"
-msgstr "压缩数据库"
-
-#: lib/database.tcl:83
-msgid "Verifying the object database with fsck-objects"
-msgstr ""
-
-#: lib/database.tcl:108
-#, tcl-format
-msgid ""
-"This repository currently has approximately %i loose objects.\n"
-"\n"
-"To maintain optimal performance it is strongly recommended that you compress "
-"the database when more than %i loose objects exist.\n"
-"\n"
-"Compress the database now?"
-msgstr ""
-
-#: lib/date.tcl:25
-#, tcl-format
-msgid "Invalid date from Git: %s"
-msgstr ""
-
-#: lib/diff.tcl:42
-#, tcl-format
-msgid ""
-"No differences detected.\n"
-"\n"
-"%s has no changes.\n"
-"\n"
-"The modification date of this file was updated by another application, but "
-"the content within the file was not changed.\n"
-"\n"
-"A rescan will be automatically started to find other files which may have "
-"the same state."
-msgstr ""
-
-#: lib/diff.tcl:81
-#, tcl-format
-msgid "Loading diff of %s..."
-msgstr ""
-
-#: lib/diff.tcl:114 lib/diff.tcl:184
-#, tcl-format
-msgid "Unable to display %s"
-msgstr ""
-
-#: lib/diff.tcl:115
-msgid "Error loading file:"
-msgstr ""
-
-#: lib/diff.tcl:122
-msgid "Git Repository (subproject)"
-msgstr ""
-
-#: lib/diff.tcl:134
-msgid "* Binary file (not showing content)."
-msgstr ""
-
-#: lib/diff.tcl:185
-msgid "Error loading diff:"
-msgstr ""
-
-#: lib/diff.tcl:302
-msgid "Failed to unstage selected hunk."
-msgstr ""
-
-#: lib/diff.tcl:309
-msgid "Failed to stage selected hunk."
-msgstr ""
-
-#: lib/error.tcl:12 lib/error.tcl:102
-msgid "error"
-msgstr ""
-
-#: lib/error.tcl:28
-msgid "warning"
-msgstr ""
-
-#: lib/error.tcl:81
-msgid "You must correct the above errors before committing."
-msgstr ""
-
-#: lib/index.tcl:241
-#, fuzzy, tcl-format
-msgid "Unstaging %s from commit"
-msgstr "从本次提交移除"
-
-#: lib/index.tcl:285
-#, tcl-format
-msgid "Adding %s"
-msgstr ""
-
-#: lib/index.tcl:340
-#, fuzzy, tcl-format
-msgid "Revert changes in file %s?"
-msgstr "恢复修改"
-
-#: lib/index.tcl:342
-#, tcl-format
-msgid "Revert changes in these %i files?"
-msgstr ""
-
-#: lib/index.tcl:348
-msgid "Any unstaged changes will be permanently lost by the revert."
-msgstr ""
-
-#: lib/index.tcl:351
-msgid "Do Nothing"
-msgstr ""
-
-#: lib/merge.tcl:13
-msgid ""
-"Cannot merge while amending.\n"
-"\n"
-"You must finish amending this commit before starting any type of merge.\n"
-msgstr ""
-
-#: lib/merge.tcl:27
-msgid ""
-"Last scanned state does not match repository state.\n"
-"\n"
-"Another Git program has modified this repository since the last scan.  A "
-"rescan must be performed before a merge can be performed.\n"
-"\n"
-"The rescan will be automatically started now.\n"
-msgstr ""
-
-#: lib/merge.tcl:44
-#, tcl-format
-msgid ""
-"You are in the middle of a conflicted merge.\n"
-"\n"
-"File %s has merge conflicts.\n"
-"\n"
-"You must resolve them, stage the file, and commit to complete the current "
-"merge.  Only then can you begin another merge.\n"
-msgstr ""
-
-#: lib/merge.tcl:54
-#, tcl-format
-msgid ""
-"You are in the middle of a change.\n"
-"\n"
-"File %s is modified.\n"
-"\n"
-"You should complete the current commit before starting a merge.  Doing so "
-"will help you abort a failed merge, should the need arise.\n"
-msgstr ""
-
-#: lib/merge.tcl:106
-#, tcl-format
-msgid "%s of %s"
-msgstr ""
-
-#: lib/merge.tcl:119
-#, tcl-format
-msgid "Merging %s and %s"
-msgstr ""
-
-#: lib/merge.tcl:131
-msgid "Merge completed successfully."
-msgstr ""
-
-#: lib/merge.tcl:133
-msgid "Merge failed.  Conflict resolution is required."
-msgstr ""
-
-#: lib/merge.tcl:158
-#, tcl-format
-msgid "Merge Into %s"
-msgstr ""
-
-#: lib/merge.tcl:177
-msgid "Revision To Merge"
-msgstr ""
-
-#: lib/merge.tcl:212
-msgid ""
-"Cannot abort while amending.\n"
-"\n"
-"You must finish amending this commit.\n"
-msgstr ""
-
-#: lib/merge.tcl:222
-msgid ""
-"Abort merge?\n"
-"\n"
-"Aborting the current merge will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with aborting the current merge?"
-msgstr ""
-
-#: lib/merge.tcl:228
-msgid ""
-"Reset changes?\n"
-"\n"
-"Resetting the changes will cause *ALL* uncommitted changes to be lost.\n"
-"\n"
-"Continue with resetting the current changes?"
-msgstr ""
-
-#: lib/merge.tcl:239
-msgid "Aborting"
-msgstr ""
-
-#: lib/merge.tcl:266
-msgid "Abort failed."
-msgstr ""
-
-#: lib/merge.tcl:268
-msgid "Abort completed.  Ready."
-msgstr ""
-
-#: lib/option.tcl:82
-msgid "Restore Defaults"
-msgstr ""
-
-#: lib/option.tcl:86
-msgid "Save"
-msgstr ""
-
-#: lib/option.tcl:96
-#, fuzzy, tcl-format
-msgid "%s Repository"
-msgstr "版本树"
-
-#: lib/option.tcl:97
-msgid "Global (All Repositories)"
-msgstr ""
-
-#: lib/option.tcl:103
-msgid "User Name"
-msgstr ""
-
-#: lib/option.tcl:104
-msgid "Email Address"
-msgstr ""
-
-#: lib/option.tcl:106
-#, fuzzy
-msgid "Summarize Merge Commits"
-msgstr "修订合并提交描述:"
-
-#: lib/option.tcl:107
-msgid "Merge Verbosity"
-msgstr ""
-
-#: lib/option.tcl:108
-msgid "Show Diffstat After Merge"
-msgstr ""
-
-#: lib/option.tcl:110
-msgid "Trust File Modification Timestamps"
-msgstr ""
-
-#: lib/option.tcl:111
-msgid "Prune Tracking Branches During Fetch"
-msgstr ""
-
-#: lib/option.tcl:112
-msgid "Match Tracking Branches"
-msgstr ""
-
-#: lib/option.tcl:113
-msgid "Number of Diff Context Lines"
-msgstr ""
-
-#: lib/option.tcl:114
-msgid "New Branch Name Template"
-msgstr ""
-
-#: lib/option.tcl:176
-msgid "Change Font"
-msgstr ""
-
-#: lib/option.tcl:180
-#, tcl-format
-msgid "Choose %s"
-msgstr ""
-
-#: lib/option.tcl:186
-msgid "pt."
-msgstr ""
-
-#: lib/option.tcl:200
-msgid "Preferences"
-msgstr ""
-
-#: lib/option.tcl:235
-msgid "Failed to completely save options:"
-msgstr ""
-
-#: lib/remote.tcl:165
-msgid "Prune from"
-msgstr ""
-
-#: lib/remote.tcl:170
-#, fuzzy
-msgid "Fetch from"
-msgstr "导入"
-
-#: lib/remote.tcl:213
-#, fuzzy
-msgid "Push to"
-msgstr "上传"
-
-#: lib/remote_branch_delete.tcl:29 lib/remote_branch_delete.tcl:34
-msgid "Delete Remote Branch"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:47
-#, fuzzy
-msgid "From Repository"
-msgstr "版本树"
-
-#: lib/remote_branch_delete.tcl:50 lib/transport.tcl:123
-msgid "Remote:"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:66 lib/transport.tcl:138
-msgid "Arbitrary URL:"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:84
-#, fuzzy
-msgid "Branches"
-msgstr "分支"
-
-#: lib/remote_branch_delete.tcl:109
-#, fuzzy
-msgid "Delete Only If"
-msgstr "删除"
-
-#: lib/remote_branch_delete.tcl:111
-msgid "Merged Into:"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:119
-msgid "Always (Do not perform merge checks)"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:152
-msgid "A branch is required for 'Merged Into'."
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:184
-#, tcl-format
-msgid ""
-"The following branches are not completely merged into %s:\n"
-"\n"
-" - %s"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:189
-#, tcl-format
-msgid ""
-"One or more of the merge tests failed because you have not fetched the "
-"necessary commits.  Try fetching from %s first."
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:207
-msgid "Please select one or more branches to delete."
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:216
-msgid ""
-"Recovering deleted branches is difficult.\n"
-"\n"
-"Delete the selected branches?"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:226
-#, tcl-format
-msgid "Deleting branches from %s"
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:286
-msgid "No repository selected."
-msgstr ""
-
-#: lib/remote_branch_delete.tcl:291
-#, tcl-format
-msgid "Scanning %s..."
-msgstr ""
-
-#: lib/shortcut.tcl:26 lib/shortcut.tcl:74
-msgid "Cannot write script:"
-msgstr ""
-
-#: lib/shortcut.tcl:149
-msgid "Cannot write icon:"
-msgstr ""
-
-#: lib/status_bar.tcl:83
-#, tcl-format
-msgid "%s ... %*i of %*i %s (%3i%%)"
-msgstr ""
-
-#: lib/transport.tcl:6
-#, fuzzy, tcl-format
-msgid "fetch %s"
-msgstr "导入"
-
-#: lib/transport.tcl:7
-#, tcl-format
-msgid "Fetching new changes from %s"
-msgstr ""
-
-#: lib/transport.tcl:18
-#, tcl-format
-msgid "remote prune %s"
-msgstr ""
-
-#: lib/transport.tcl:19
-#, tcl-format
-msgid "Pruning tracking branches deleted from %s"
-msgstr ""
-
-#: lib/transport.tcl:25 lib/transport.tcl:71
-#, tcl-format
-msgid "push %s"
-msgstr ""
-
-#: lib/transport.tcl:26
-#, tcl-format
-msgid "Pushing changes to %s"
-msgstr ""
-
-#: lib/transport.tcl:72
-#, tcl-format
-msgid "Pushing %s %s to %s"
-msgstr ""
-
-#: lib/transport.tcl:89
-#, fuzzy
-msgid "Push Branches"
-msgstr "分支"
-
-#: lib/transport.tcl:103
-#, fuzzy
-msgid "Source Branches"
-msgstr "当前分支:"
-
-#: lib/transport.tcl:120
-#, fuzzy
-msgid "Destination Repository"
-msgstr "版本树"
-
-#: lib/transport.tcl:158
-msgid "Transfer Options"
-msgstr ""
-
-#: lib/transport.tcl:160
-msgid "Force overwrite existing branch (may discard changes)"
-msgstr ""
-
-#: lib/transport.tcl:164
-msgid "Use thin pack (for slow network connections)"
-msgstr ""
-
-#: lib/transport.tcl:168
-msgid "Include tags"
-msgstr ""
-
-#~ msgid "Add To Commit"
-#~ msgstr "添加到本次提交"
-
-#~ msgid "Add Existing To Commit"
-#~ msgstr "添加默认修改文件"
-
-#~ msgid "Unstaged Changes (Will Not Be Committed)"
-#~ msgstr "不被提交的修改"
-
-#~ msgid "Add Existing"
-#~ msgstr "添加默认修改文件"
-
-#, fuzzy
-#~ msgid "Push to %s..."
-#~ msgstr "上传..."
diff --git a/ppc/sha1.c b/ppc/sha1.c
new file mode 100644 (file)
index 0000000..738e36c
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SHA-1 implementation.
+ *
+ * Copyright (C) 2005 Paul Mackerras <paulus@samba.org>
+ *
+ * This version assumes we are running on a big-endian machine.
+ * It calls an external sha1_core() to process blocks of 64 bytes.
+ */
+#include <stdio.h>
+#include <string.h>
+#include "sha1.h"
+
+extern void sha1_core(uint32_t *hash, const unsigned char *p,
+                     unsigned int nblocks);
+
+int SHA1_Init(SHA_CTX *c)
+{
+       c->hash[0] = 0x67452301;
+       c->hash[1] = 0xEFCDAB89;
+       c->hash[2] = 0x98BADCFE;
+       c->hash[3] = 0x10325476;
+       c->hash[4] = 0xC3D2E1F0;
+       c->len = 0;
+       c->cnt = 0;
+       return 0;
+}
+
+int SHA1_Update(SHA_CTX *c, const void *ptr, unsigned long n)
+{
+       unsigned long nb;
+       const unsigned char *p = ptr;
+
+       c->len += (uint64_t) n << 3;
+       while (n != 0) {
+               if (c->cnt || n < 64) {
+                       nb = 64 - c->cnt;
+                       if (nb > n)
+                               nb = n;
+                       memcpy(&c->buf.b[c->cnt], p, nb);
+                       if ((c->cnt += nb) == 64) {
+                               sha1_core(c->hash, c->buf.b, 1);
+                               c->cnt = 0;
+                       }
+               } else {
+                       nb = n >> 6;
+                       sha1_core(c->hash, p, nb);
+                       nb <<= 6;
+               }
+               n -= nb;
+               p += nb;
+       }
+       return 0;
+}
+
+int SHA1_Final(unsigned char *hash, SHA_CTX *c)
+{
+       unsigned int cnt = c->cnt;
+
+       c->buf.b[cnt++] = 0x80;
+       if (cnt > 56) {
+               if (cnt < 64)
+                       memset(&c->buf.b[cnt], 0, 64 - cnt);
+               sha1_core(c->hash, c->buf.b, 1);
+               cnt = 0;
+       }
+       if (cnt < 56)
+               memset(&c->buf.b[cnt], 0, 56 - cnt);
+       c->buf.l[7] = c->len;
+       sha1_core(c->hash, c->buf.b, 1);
+       memcpy(hash, c->hash, 20);
+       return 0;
+}
diff --git a/ppc/sha1.h b/ppc/sha1.h
new file mode 100644 (file)
index 0000000..c3c51aa
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * SHA-1 implementation.
+ *
+ * Copyright (C) 2005 Paul Mackerras <paulus@samba.org>
+ */
+#include <stdint.h>
+
+typedef struct sha_context {
+       uint32_t hash[5];
+       uint32_t cnt;
+       uint64_t len;
+       union {
+               unsigned char b[64];
+               uint64_t l[8];
+       } buf;
+} SHA_CTX;
+
+int SHA1_Init(SHA_CTX *c);
+int SHA1_Update(SHA_CTX *c, const void *p, unsigned long n);
+int SHA1_Final(unsigned char *hash, SHA_CTX *c);
diff --git a/ppc/sha1ppc.S b/ppc/sha1ppc.S
new file mode 100644 (file)
index 0000000..f132696
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * SHA-1 implementation for PowerPC.
+ *
+ * Copyright (C) 2005 Paul Mackerras <paulus@samba.org>
+ */
+
+/*
+ * PowerPC calling convention:
+ * %r0 - volatile temp
+ * %r1 - stack pointer.
+ * %r2 - reserved
+ * %r3-%r12 - Incoming arguments & return values; volatile.
+ * %r13-%r31 - Callee-save registers
+ * %lr - Return address, volatile
+ * %ctr - volatile
+ *
+ * Register usage in this routine:
+ * %r0 - temp
+ * %r3 - argument (pointer to 5 words of SHA state)
+ * %r4 - argument (pointer to data to hash)
+ * %r5 - Constant K in SHA round (initially number of blocks to hash)
+ * %r6-%r10 - Working copies of SHA variables A..E (actually E..A order)
+ * %r11-%r26 - Data being hashed W[].
+ * %r27-%r31 - Previous copies of A..E, for final add back.
+ * %ctr - loop count
+ */
+
+
+/*
+ * We roll the registers for A, B, C, D, E around on each
+ * iteration; E on iteration t is D on iteration t+1, and so on.
+ * We use registers 6 - 10 for this.  (Registers 27 - 31 hold
+ * the previous values.)
+ */
+#define RA(t)  (((t)+4)%5+6)
+#define RB(t)  (((t)+3)%5+6)
+#define RC(t)  (((t)+2)%5+6)
+#define RD(t)  (((t)+1)%5+6)
+#define RE(t)  (((t)+0)%5+6)
+
+/* We use registers 11 - 26 for the W values */
+#define W(t)   ((t)%16+11)
+
+/* Register 5 is used for the constant k */
+
+/*
+ * The basic SHA-1 round function is:
+ * E += ROTL(A,5) + F(B,C,D) + W[i] + K;  B = ROTL(B,30)
+ * Then the variables are renamed: (A,B,C,D,E) = (E,A,B,C,D).
+ *
+ * Every 20 rounds, the function F() and the constant K changes:
+ * - 20 rounds of f0(b,c,d) = "bit wise b ? c : d" =  (^b & d) + (b & c)
+ * - 20 rounds of f1(b,c,d) = b^c^d = (b^d)^c
+ * - 20 rounds of f2(b,c,d) = majority(b,c,d) = (b&d) + ((b^d)&c)
+ * - 20 more rounds of f1(b,c,d)
+ *
+ * These are all scheduled for near-optimal performance on a G4.
+ * The G4 is a 3-issue out-of-order machine with 3 ALUs, but it can only
+ * *consider* starting the oldest 3 instructions per cycle.  So to get
+ * maximum performance out of it, you have to treat it as an in-order
+ * machine.  Which means interleaving the computation round t with the
+ * computation of W[t+4].
+ *
+ * The first 16 rounds use W values loaded directly from memory, while the
+ * remaining 64 use values computed from those first 16.  We preload
+ * 4 values before starting, so there are three kinds of rounds:
+ * - The first 12 (all f0) also load the W values from memory.
+ * - The next 64 compute W(i+4) in parallel. 8*f0, 20*f1, 20*f2, 16*f1.
+ * - The last 4 (all f1) do not do anything with W.
+ *
+ * Therefore, we have 6 different round functions:
+ * STEPD0_LOAD(t,s) - Perform round t and load W(s).  s < 16
+ * STEPD0_UPDATE(t,s) - Perform round t and compute W(s).  s >= 16.
+ * STEPD1_UPDATE(t,s)
+ * STEPD2_UPDATE(t,s)
+ * STEPD1(t) - Perform round t with no load or update.
+ *
+ * The G5 is more fully out-of-order, and can find the parallelism
+ * by itself.  The big limit is that it has a 2-cycle ALU latency, so
+ * even though it's 2-way, the code has to be scheduled as if it's
+ * 4-way, which can be a limit.  To help it, we try to schedule the
+ * read of RA(t) as late as possible so it doesn't stall waiting for
+ * the previous round's RE(t-1), and we try to rotate RB(t) as early
+ * as possible while reading RC(t) (= RB(t-1)) as late as possible.
+ */
+
+/* the initial loads. */
+#define LOADW(s) \
+       lwz     W(s),(s)*4(%r4)
+
+/*
+ * Perform a step with F0, and load W(s).  Uses W(s) as a temporary
+ * before loading it.
+ * This is actually 10 instructions, which is an awkward fit.
+ * It can execute grouped as listed, or delayed one instruction.
+ * (If delayed two instructions, there is a stall before the start of the
+ * second line.)  Thus, two iterations take 7 cycles, 3.5 cycles per round.
+ */
+#define STEPD0_LOAD(t,s) \
+add RE(t),RE(t),W(t); andc   %r0,RD(t),RB(t);  and    W(s),RC(t),RB(t); \
+add RE(t),RE(t),%r0;  rotlwi %r0,RA(t),5;      rotlwi RB(t),RB(t),30;   \
+add RE(t),RE(t),W(s); add    %r0,%r0,%r5;      lwz    W(s),(s)*4(%r4);  \
+add RE(t),RE(t),%r0
+
+/*
+ * This is likewise awkward, 13 instructions.  However, it can also
+ * execute starting with 2 out of 3 possible moduli, so it does 2 rounds
+ * in 9 cycles, 4.5 cycles/round.
+ */
+#define STEPD0_UPDATE(t,s,loadk...) \
+add RE(t),RE(t),W(t); andc   %r0,RD(t),RB(t); xor    W(s),W((s)-16),W((s)-3); \
+add RE(t),RE(t),%r0;  and    %r0,RC(t),RB(t); xor    W(s),W(s),W((s)-8);      \
+add RE(t),RE(t),%r0;  rotlwi %r0,RA(t),5;     xor    W(s),W(s),W((s)-14);     \
+add RE(t),RE(t),%r5;  loadk; rotlwi RB(t),RB(t),30;  rotlwi W(s),W(s),1;     \
+add RE(t),RE(t),%r0
+
+/* Nicely optimal.  Conveniently, also the most common. */
+#define STEPD1_UPDATE(t,s,loadk...) \
+add RE(t),RE(t),W(t); xor    %r0,RD(t),RB(t); xor    W(s),W((s)-16),W((s)-3); \
+add RE(t),RE(t),%r5;  loadk; xor %r0,%r0,RC(t);  xor W(s),W(s),W((s)-8);      \
+add RE(t),RE(t),%r0;  rotlwi %r0,RA(t),5;     xor    W(s),W(s),W((s)-14);     \
+add RE(t),RE(t),%r0;  rotlwi RB(t),RB(t),30;  rotlwi W(s),W(s),1
+
+/*
+ * The naked version, no UPDATE, for the last 4 rounds.  3 cycles per.
+ * We could use W(s) as a temp register, but we don't need it.
+ */
+#define STEPD1(t) \
+                        add   RE(t),RE(t),W(t); xor    %r0,RD(t),RB(t); \
+rotlwi RB(t),RB(t),30;  add   RE(t),RE(t),%r5;  xor    %r0,%r0,RC(t);   \
+add    RE(t),RE(t),%r0; rotlwi %r0,RA(t),5;     /* spare slot */        \
+add    RE(t),RE(t),%r0
+
+/*
+ * 14 instructions, 5 cycles per.  The majority function is a bit
+ * awkward to compute.  This can execute with a 1-instruction delay,
+ * but it causes a 2-instruction delay, which triggers a stall.
+ */
+#define STEPD2_UPDATE(t,s,loadk...) \
+add RE(t),RE(t),W(t); and    %r0,RD(t),RB(t); xor    W(s),W((s)-16),W((s)-3); \
+add RE(t),RE(t),%r0;  xor    %r0,RD(t),RB(t); xor    W(s),W(s),W((s)-8);      \
+add RE(t),RE(t),%r5;  loadk; and %r0,%r0,RC(t);  xor W(s),W(s),W((s)-14);     \
+add RE(t),RE(t),%r0;  rotlwi %r0,RA(t),5;     rotlwi W(s),W(s),1;             \
+add RE(t),RE(t),%r0;  rotlwi RB(t),RB(t),30
+
+#define STEP0_LOAD4(t,s)               \
+       STEPD0_LOAD(t,s);               \
+       STEPD0_LOAD((t+1),(s)+1);       \
+       STEPD0_LOAD((t)+2,(s)+2);       \
+       STEPD0_LOAD((t)+3,(s)+3)
+
+#define STEPUP4(fn, t, s, loadk...)            \
+       STEP##fn##_UPDATE(t,s,);                \
+       STEP##fn##_UPDATE((t)+1,(s)+1,);        \
+       STEP##fn##_UPDATE((t)+2,(s)+2,);        \
+       STEP##fn##_UPDATE((t)+3,(s)+3,loadk)
+
+#define STEPUP20(fn, t, s, loadk...)   \
+       STEPUP4(fn, t, s,);             \
+       STEPUP4(fn, (t)+4, (s)+4,);     \
+       STEPUP4(fn, (t)+8, (s)+8,);     \
+       STEPUP4(fn, (t)+12, (s)+12,);   \
+       STEPUP4(fn, (t)+16, (s)+16, loadk)
+
+       .globl  sha1_core
+sha1_core:
+       stwu    %r1,-80(%r1)
+       stmw    %r13,4(%r1)
+
+       /* Load up A - E */
+       lmw     %r27,0(%r3)
+
+       mtctr   %r5
+
+1:
+       LOADW(0)
+       lis     %r5,0x5a82
+       mr      RE(0),%r31
+       LOADW(1)
+       mr      RD(0),%r30
+       mr      RC(0),%r29
+       LOADW(2)
+       ori     %r5,%r5,0x7999  /* K0-19 */
+       mr      RB(0),%r28
+       LOADW(3)
+       mr      RA(0),%r27
+
+       STEP0_LOAD4(0, 4)
+       STEP0_LOAD4(4, 8)
+       STEP0_LOAD4(8, 12)
+       STEPUP4(D0, 12, 16,)
+       STEPUP4(D0, 16, 20, lis %r5,0x6ed9)
+
+       ori     %r5,%r5,0xeba1  /* K20-39 */
+       STEPUP20(D1, 20, 24, lis %r5,0x8f1b)
+
+       ori     %r5,%r5,0xbcdc  /* K40-59 */
+       STEPUP20(D2, 40, 44, lis %r5,0xca62)
+
+       ori     %r5,%r5,0xc1d6  /* K60-79 */
+       STEPUP4(D1, 60, 64,)
+       STEPUP4(D1, 64, 68,)
+       STEPUP4(D1, 68, 72,)
+       STEPUP4(D1, 72, 76,)
+       addi    %r4,%r4,64
+       STEPD1(76)
+       STEPD1(77)
+       STEPD1(78)
+       STEPD1(79)
+
+       /* Add results to original values */
+       add     %r31,%r31,RE(0)
+       add     %r30,%r30,RD(0)
+       add     %r29,%r29,RC(0)
+       add     %r28,%r28,RB(0)
+       add     %r27,%r27,RA(0)
+
+       bdnz    1b
+
+       /* Save final hash, restore registers, and return */
+       stmw    %r27,0(%r3)
+       lmw     %r13,4(%r1)
+       addi    %r1,%r1,80
+       blr
diff --git a/pretty.c b/pretty.c
new file mode 100644 (file)
index 0000000..9db75b4
--- /dev/null
+++ b/pretty.c
@@ -0,0 +1,825 @@
+#include "cache.h"
+#include "commit.h"
+#include "utf8.h"
+#include "diff.h"
+#include "revision.h"
+
+static struct cmt_fmt_map {
+       const char *n;
+       size_t cmp_len;
+       enum cmit_fmt v;
+} cmt_fmts[] = {
+       { "raw",        1,      CMIT_FMT_RAW },
+       { "medium",     1,      CMIT_FMT_MEDIUM },
+       { "short",      1,      CMIT_FMT_SHORT },
+       { "email",      1,      CMIT_FMT_EMAIL },
+       { "full",       5,      CMIT_FMT_FULL },
+       { "fuller",     5,      CMIT_FMT_FULLER },
+       { "oneline",    1,      CMIT_FMT_ONELINE },
+       { "format:",    7,      CMIT_FMT_USERFORMAT},
+};
+
+static char *user_format;
+
+enum cmit_fmt get_commit_format(const char *arg)
+{
+       int i;
+
+       if (!arg || !*arg)
+               return CMIT_FMT_DEFAULT;
+       if (*arg == '=')
+               arg++;
+       if (!prefixcmp(arg, "format:")) {
+               if (user_format)
+                       free(user_format);
+               user_format = xstrdup(arg + 7);
+               return CMIT_FMT_USERFORMAT;
+       }
+       for (i = 0; i < ARRAY_SIZE(cmt_fmts); i++) {
+               if (!strncmp(arg, cmt_fmts[i].n, cmt_fmts[i].cmp_len) &&
+                   !strncmp(arg, cmt_fmts[i].n, strlen(arg)))
+                       return cmt_fmts[i].v;
+       }
+
+       die("invalid --pretty format: %s", arg);
+}
+
+/*
+ * Generic support for pretty-printing the header
+ */
+static int get_one_line(const char *msg)
+{
+       int ret = 0;
+
+       for (;;) {
+               char c = *msg++;
+               if (!c)
+                       break;
+               ret++;
+               if (c == '\n')
+                       break;
+       }
+       return ret;
+}
+
+/* High bit set, or ISO-2022-INT */
+int non_ascii(int ch)
+{
+       ch = (ch & 0xff);
+       return ((ch & 0x80) || (ch == 0x1b));
+}
+
+static int is_rfc2047_special(char ch)
+{
+       return (non_ascii(ch) || (ch == '=') || (ch == '?') || (ch == '_'));
+}
+
+static void add_rfc2047(struct strbuf *sb, const char *line, int len,
+                      const char *encoding)
+{
+       int i, last;
+
+       for (i = 0; i < len; i++) {
+               int ch = line[i];
+               if (non_ascii(ch))
+                       goto needquote;
+               if ((i + 1 < len) && (ch == '=' && line[i+1] == '?'))
+                       goto needquote;
+       }
+       strbuf_add(sb, line, len);
+       return;
+
+needquote:
+       strbuf_grow(sb, len * 3 + strlen(encoding) + 100);
+       strbuf_addf(sb, "=?%s?q?", encoding);
+       for (i = last = 0; i < len; i++) {
+               unsigned ch = line[i] & 0xFF;
+               /*
+                * We encode ' ' using '=20' even though rfc2047
+                * allows using '_' for readability.  Unfortunately,
+                * many programs do not understand this and just
+                * leave the underscore in place.
+                */
+               if (is_rfc2047_special(ch) || ch == ' ') {
+                       strbuf_add(sb, line + last, i - last);
+                       strbuf_addf(sb, "=%02X", ch);
+                       last = i + 1;
+               }
+       }
+       strbuf_add(sb, line + last, len - last);
+       strbuf_addstr(sb, "?=");
+}
+
+static void add_user_info(const char *what, enum cmit_fmt fmt, struct strbuf *sb,
+                        const char *line, enum date_mode dmode,
+                        const char *encoding)
+{
+       char *date;
+       int namelen;
+       unsigned long time;
+       int tz;
+       const char *filler = "    ";
+
+       if (fmt == CMIT_FMT_ONELINE)
+               return;
+       date = strchr(line, '>');
+       if (!date)
+               return;
+       namelen = ++date - line;
+       time = strtoul(date, &date, 10);
+       tz = strtol(date, NULL, 10);
+
+       if (fmt == CMIT_FMT_EMAIL) {
+               char *name_tail = strchr(line, '<');
+               int display_name_length;
+               if (!name_tail)
+                       return;
+               while (line < name_tail && isspace(name_tail[-1]))
+                       name_tail--;
+               display_name_length = name_tail - line;
+               filler = "";
+               strbuf_addstr(sb, "From: ");
+               add_rfc2047(sb, line, display_name_length, encoding);
+               strbuf_add(sb, name_tail, namelen - display_name_length);
+               strbuf_addch(sb, '\n');
+       } else {
+               strbuf_addf(sb, "%s: %.*s%.*s\n", what,
+                             (fmt == CMIT_FMT_FULLER) ? 4 : 0,
+                             filler, namelen, line);
+       }
+       switch (fmt) {
+       case CMIT_FMT_MEDIUM:
+               strbuf_addf(sb, "Date:   %s\n", show_date(time, tz, dmode));
+               break;
+       case CMIT_FMT_EMAIL:
+               strbuf_addf(sb, "Date: %s\n", show_date(time, tz, DATE_RFC2822));
+               break;
+       case CMIT_FMT_FULLER:
+               strbuf_addf(sb, "%sDate: %s\n", what, show_date(time, tz, dmode));
+               break;
+       default:
+               /* notin' */
+               break;
+       }
+}
+
+static int is_empty_line(const char *line, int *len_p)
+{
+       int len = *len_p;
+       while (len && isspace(line[len-1]))
+               len--;
+       *len_p = len;
+       return !len;
+}
+
+static void add_merge_info(enum cmit_fmt fmt, struct strbuf *sb,
+                       const struct commit *commit, int abbrev)
+{
+       struct commit_list *parent = commit->parents;
+
+       if ((fmt == CMIT_FMT_ONELINE) || (fmt == CMIT_FMT_EMAIL) ||
+           !parent || !parent->next)
+               return;
+
+       strbuf_addstr(sb, "Merge:");
+
+       while (parent) {
+               struct commit *p = parent->item;
+               const char *hex = NULL;
+               const char *dots;
+               if (abbrev)
+                       hex = find_unique_abbrev(p->object.sha1, abbrev);
+               if (!hex)
+                       hex = sha1_to_hex(p->object.sha1);
+               dots = (abbrev && strlen(hex) != 40) ?  "..." : "";
+               parent = parent->next;
+
+               strbuf_addf(sb, " %s%s", hex, dots);
+       }
+       strbuf_addch(sb, '\n');
+}
+
+static char *get_header(const struct commit *commit, const char *key)
+{
+       int key_len = strlen(key);
+       const char *line = commit->buffer;
+
+       for (;;) {
+               const char *eol = strchr(line, '\n'), *next;
+
+               if (line == eol)
+                       return NULL;
+               if (!eol) {
+                       eol = line + strlen(line);
+                       next = NULL;
+               } else
+                       next = eol + 1;
+               if (eol - line > key_len &&
+                   !strncmp(line, key, key_len) &&
+                   line[key_len] == ' ') {
+                       return xmemdupz(line + key_len + 1, eol - line - key_len - 1);
+               }
+               line = next;
+       }
+}
+
+static char *replace_encoding_header(char *buf, const char *encoding)
+{
+       struct strbuf tmp;
+       size_t start, len;
+       char *cp = buf;
+
+       /* guess if there is an encoding header before a \n\n */
+       while (strncmp(cp, "encoding ", strlen("encoding "))) {
+               cp = strchr(cp, '\n');
+               if (!cp || *++cp == '\n')
+                       return buf;
+       }
+       start = cp - buf;
+       cp = strchr(cp, '\n');
+       if (!cp)
+               return buf; /* should not happen but be defensive */
+       len = cp + 1 - (buf + start);
+
+       strbuf_init(&tmp, 0);
+       strbuf_attach(&tmp, buf, strlen(buf), strlen(buf) + 1);
+       if (is_encoding_utf8(encoding)) {
+               /* we have re-coded to UTF-8; drop the header */
+               strbuf_remove(&tmp, start, len);
+       } else {
+               /* just replaces XXXX in 'encoding XXXX\n' */
+               strbuf_splice(&tmp, start + strlen("encoding "),
+                                         len - strlen("encoding \n"),
+                                         encoding, strlen(encoding));
+       }
+       return strbuf_detach(&tmp, NULL);
+}
+
+static char *logmsg_reencode(const struct commit *commit,
+                            const char *output_encoding)
+{
+       static const char *utf8 = "utf-8";
+       const char *use_encoding;
+       char *encoding;
+       char *out;
+
+       if (!*output_encoding)
+               return NULL;
+       encoding = get_header(commit, "encoding");
+       use_encoding = encoding ? encoding : utf8;
+       if (!strcmp(use_encoding, output_encoding))
+               if (encoding) /* we'll strip encoding header later */
+                       out = xstrdup(commit->buffer);
+               else
+                       return NULL; /* nothing to do */
+       else
+               out = reencode_string(commit->buffer,
+                                     output_encoding, use_encoding);
+       if (out)
+               out = replace_encoding_header(out, output_encoding);
+
+       free(encoding);
+       return out;
+}
+
+static void format_person_part(struct strbuf *sb, char part,
+                               const char *msg, int len)
+{
+       int start, end, tz = 0;
+       unsigned long date;
+       char *ep;
+
+       /* parse name */
+       for (end = 0; end < len && msg[end] != '<'; end++)
+               ; /* do nothing */
+       start = end + 1;
+       while (end > 0 && isspace(msg[end - 1]))
+               end--;
+       if (part == 'n') {      /* name */
+               strbuf_add(sb, msg, end);
+               return;
+       }
+
+       if (start >= len)
+               return;
+
+       /* parse email */
+       for (end = start + 1; end < len && msg[end] != '>'; end++)
+               ; /* do nothing */
+
+       if (end >= len)
+               return;
+
+       if (part == 'e') {      /* email */
+               strbuf_add(sb, msg + start, end - start);
+               return;
+       }
+
+       /* parse date */
+       for (start = end + 1; start < len && isspace(msg[start]); start++)
+               ; /* do nothing */
+       if (start >= len)
+               return;
+       date = strtoul(msg + start, &ep, 10);
+       if (msg + start == ep)
+               return;
+
+       if (part == 't') {      /* date, UNIX timestamp */
+               strbuf_add(sb, msg + start, ep - (msg + start));
+               return;
+       }
+
+       /* parse tz */
+       for (start = ep - msg + 1; start < len && isspace(msg[start]); start++)
+               ; /* do nothing */
+       if (start + 1 < len) {
+               tz = strtoul(msg + start + 1, NULL, 10);
+               if (msg[start] == '-')
+                       tz = -tz;
+       }
+
+       switch (part) {
+       case 'd':       /* date */
+               strbuf_addstr(sb, show_date(date, tz, DATE_NORMAL));
+               return;
+       case 'D':       /* date, RFC2822 style */
+               strbuf_addstr(sb, show_date(date, tz, DATE_RFC2822));
+               return;
+       case 'r':       /* date, relative */
+               strbuf_addstr(sb, show_date(date, tz, DATE_RELATIVE));
+               return;
+       case 'i':       /* date, ISO 8601 */
+               strbuf_addstr(sb, show_date(date, tz, DATE_ISO8601));
+               return;
+       }
+}
+
+struct chunk {
+       size_t off;
+       size_t len;
+};
+
+struct format_commit_context {
+       const struct commit *commit;
+
+       /* These offsets are relative to the start of the commit message. */
+       int commit_header_parsed;
+       struct chunk subject;
+       struct chunk author;
+       struct chunk committer;
+       struct chunk encoding;
+       size_t body_off;
+
+       /* The following ones are relative to the result struct strbuf. */
+       struct chunk abbrev_commit_hash;
+       struct chunk abbrev_tree_hash;
+       struct chunk abbrev_parent_hashes;
+};
+
+static int add_again(struct strbuf *sb, struct chunk *chunk)
+{
+       if (chunk->len) {
+               strbuf_adddup(sb, chunk->off, chunk->len);
+               return 1;
+       }
+
+       /*
+        * We haven't seen this chunk before.  Our caller is surely
+        * going to add it the hard way now.  Remember the most likely
+        * start of the to-be-added chunk: the current end of the
+        * struct strbuf.
+        */
+       chunk->off = sb->len;
+       return 0;
+}
+
+static void parse_commit_header(struct format_commit_context *context)
+{
+       const char *msg = context->commit->buffer;
+       int i;
+       enum { HEADER, SUBJECT, BODY } state;
+
+       for (i = 0, state = HEADER; msg[i] && state < BODY; i++) {
+               int eol;
+               for (eol = i; msg[eol] && msg[eol] != '\n'; eol++)
+                       ; /* do nothing */
+
+               if (state == SUBJECT) {
+                       context->subject.off = i;
+                       context->subject.len = eol - i;
+                       i = eol;
+               }
+               if (i == eol) {
+                       state++;
+                       /* strip empty lines */
+                       while (msg[eol + 1] == '\n')
+                               eol++;
+               } else if (!prefixcmp(msg + i, "author ")) {
+                       context->author.off = i + 7;
+                       context->author.len = eol - i - 7;
+               } else if (!prefixcmp(msg + i, "committer ")) {
+                       context->committer.off = i + 10;
+                       context->committer.len = eol - i - 10;
+               } else if (!prefixcmp(msg + i, "encoding ")) {
+                       context->encoding.off = i + 9;
+                       context->encoding.len = eol - i - 9;
+               }
+               i = eol;
+       }
+       context->body_off = i;
+       context->commit_header_parsed = 1;
+}
+
+static void format_commit_item(struct strbuf *sb, const char *placeholder,
+                               void *context)
+{
+       struct format_commit_context *c = context;
+       const struct commit *commit = c->commit;
+       const char *msg = commit->buffer;
+       struct commit_list *p;
+
+       /* these are independent of the commit */
+       switch (placeholder[0]) {
+       case 'C':
+               switch (placeholder[3]) {
+               case 'd':       /* red */
+                       strbuf_addstr(sb, "\033[31m");
+                       return;
+               case 'e':       /* green */
+                       strbuf_addstr(sb, "\033[32m");
+                       return;
+               case 'u':       /* blue */
+                       strbuf_addstr(sb, "\033[34m");
+                       return;
+               case 's':       /* reset color */
+                       strbuf_addstr(sb, "\033[m");
+                       return;
+               }
+       case 'n':               /* newline */
+               strbuf_addch(sb, '\n');
+               return;
+       }
+
+       /* these depend on the commit */
+       if (!commit->object.parsed)
+               parse_object(commit->object.sha1);
+
+       switch (placeholder[0]) {
+       case 'H':               /* commit hash */
+               strbuf_addstr(sb, sha1_to_hex(commit->object.sha1));
+               return;
+       case 'h':               /* abbreviated commit hash */
+               if (add_again(sb, &c->abbrev_commit_hash))
+                       return;
+               strbuf_addstr(sb, find_unique_abbrev(commit->object.sha1,
+                                                    DEFAULT_ABBREV));
+               c->abbrev_commit_hash.len = sb->len - c->abbrev_commit_hash.off;
+               return;
+       case 'T':               /* tree hash */
+               strbuf_addstr(sb, sha1_to_hex(commit->tree->object.sha1));
+               return;
+       case 't':               /* abbreviated tree hash */
+               if (add_again(sb, &c->abbrev_tree_hash))
+                       return;
+               strbuf_addstr(sb, find_unique_abbrev(commit->tree->object.sha1,
+                                                    DEFAULT_ABBREV));
+               c->abbrev_tree_hash.len = sb->len - c->abbrev_tree_hash.off;
+               return;
+       case 'P':               /* parent hashes */
+               for (p = commit->parents; p; p = p->next) {
+                       if (p != commit->parents)
+                               strbuf_addch(sb, ' ');
+                       strbuf_addstr(sb, sha1_to_hex(p->item->object.sha1));
+               }
+               return;
+       case 'p':               /* abbreviated parent hashes */
+               if (add_again(sb, &c->abbrev_parent_hashes))
+                       return;
+               for (p = commit->parents; p; p = p->next) {
+                       if (p != commit->parents)
+                               strbuf_addch(sb, ' ');
+                       strbuf_addstr(sb, find_unique_abbrev(
+                                       p->item->object.sha1, DEFAULT_ABBREV));
+               }
+               c->abbrev_parent_hashes.len = sb->len -
+                                             c->abbrev_parent_hashes.off;
+               return;
+       case 'm':               /* left/right/bottom */
+               strbuf_addch(sb, (commit->object.flags & BOUNDARY)
+                                ? '-'
+                                : (commit->object.flags & SYMMETRIC_LEFT)
+                                ? '<'
+                                : '>');
+               return;
+       }
+
+       /* For the rest we have to parse the commit header. */
+       if (!c->commit_header_parsed)
+               parse_commit_header(c);
+
+       switch (placeholder[0]) {
+       case 's':
+               strbuf_add(sb, msg + c->subject.off, c->subject.len);
+               return;
+       case 'a':
+               format_person_part(sb, placeholder[1],
+                                  msg + c->author.off, c->author.len);
+               return;
+       case 'c':
+               format_person_part(sb, placeholder[1],
+                                  msg + c->committer.off, c->committer.len);
+               return;
+       case 'e':
+               strbuf_add(sb, msg + c->encoding.off, c->encoding.len);
+               return;
+       case 'b':
+               strbuf_addstr(sb, msg + c->body_off);
+               return;
+       }
+}
+
+void format_commit_message(const struct commit *commit,
+                           const void *format, struct strbuf *sb)
+{
+       const char *placeholders[] = {
+               "H",            /* commit hash */
+               "h",            /* abbreviated commit hash */
+               "T",            /* tree hash */
+               "t",            /* abbreviated tree hash */
+               "P",            /* parent hashes */
+               "p",            /* abbreviated parent hashes */
+               "an",           /* author name */
+               "ae",           /* author email */
+               "ad",           /* author date */
+               "aD",           /* author date, RFC2822 style */
+               "ar",           /* author date, relative */
+               "at",           /* author date, UNIX timestamp */
+               "ai",           /* author date, ISO 8601 */
+               "cn",           /* committer name */
+               "ce",           /* committer email */
+               "cd",           /* committer date */
+               "cD",           /* committer date, RFC2822 style */
+               "cr",           /* committer date, relative */
+               "ct",           /* committer date, UNIX timestamp */
+               "ci",           /* committer date, ISO 8601 */
+               "e",            /* encoding */
+               "s",            /* subject */
+               "b",            /* body */
+               "Cred",         /* red */
+               "Cgreen",       /* green */
+               "Cblue",        /* blue */
+               "Creset",       /* reset color */
+               "n",            /* newline */
+               "m",            /* left/right/bottom */
+               NULL
+       };
+       struct format_commit_context context;
+
+       memset(&context, 0, sizeof(context));
+       context.commit = commit;
+       strbuf_expand(sb, format, placeholders, format_commit_item, &context);
+}
+
+static void pp_header(enum cmit_fmt fmt,
+                     int abbrev,
+                     enum date_mode dmode,
+                     const char *encoding,
+                     const struct commit *commit,
+                     const char **msg_p,
+                     struct strbuf *sb)
+{
+       int parents_shown = 0;
+
+       for (;;) {
+               const char *line = *msg_p;
+               int linelen = get_one_line(*msg_p);
+
+               if (!linelen)
+                       return;
+               *msg_p += linelen;
+
+               if (linelen == 1)
+                       /* End of header */
+                       return;
+
+               if (fmt == CMIT_FMT_RAW) {
+                       strbuf_add(sb, line, linelen);
+                       continue;
+               }
+
+               if (!memcmp(line, "parent ", 7)) {
+                       if (linelen != 48)
+                               die("bad parent line in commit");
+                       continue;
+               }
+
+               if (!parents_shown) {
+                       struct commit_list *parent;
+                       int num;
+                       for (parent = commit->parents, num = 0;
+                            parent;
+                            parent = parent->next, num++)
+                               ;
+                       /* with enough slop */
+                       strbuf_grow(sb, num * 50 + 20);
+                       add_merge_info(fmt, sb, commit, abbrev);
+                       parents_shown = 1;
+               }
+
+               /*
+                * MEDIUM == DEFAULT shows only author with dates.
+                * FULL shows both authors but not dates.
+                * FULLER shows both authors and dates.
+                */
+               if (!memcmp(line, "author ", 7)) {
+                       strbuf_grow(sb, linelen + 80);
+                       add_user_info("Author", fmt, sb, line + 7, dmode, encoding);
+               }
+               if (!memcmp(line, "committer ", 10) &&
+                   (fmt == CMIT_FMT_FULL || fmt == CMIT_FMT_FULLER)) {
+                       strbuf_grow(sb, linelen + 80);
+                       add_user_info("Commit", fmt, sb, line + 10, dmode, encoding);
+               }
+       }
+}
+
+static void pp_title_line(enum cmit_fmt fmt,
+                         const char **msg_p,
+                         struct strbuf *sb,
+                         const char *subject,
+                         const char *after_subject,
+                         const char *encoding,
+                         int plain_non_ascii)
+{
+       struct strbuf title;
+
+       strbuf_init(&title, 80);
+
+       for (;;) {
+               const char *line = *msg_p;
+               int linelen = get_one_line(line);
+
+               *msg_p += linelen;
+               if (!linelen || is_empty_line(line, &linelen))
+                       break;
+
+               strbuf_grow(&title, linelen + 2);
+               if (title.len) {
+                       if (fmt == CMIT_FMT_EMAIL) {
+                               strbuf_addch(&title, '\n');
+                       }
+                       strbuf_addch(&title, ' ');
+               }
+               strbuf_add(&title, line, linelen);
+       }
+
+       strbuf_grow(sb, title.len + 1024);
+       if (subject) {
+               strbuf_addstr(sb, subject);
+               add_rfc2047(sb, title.buf, title.len, encoding);
+       } else {
+               strbuf_addbuf(sb, &title);
+       }
+       strbuf_addch(sb, '\n');
+
+       if (plain_non_ascii) {
+               const char *header_fmt =
+                       "MIME-Version: 1.0\n"
+                       "Content-Type: text/plain; charset=%s\n"
+                       "Content-Transfer-Encoding: 8bit\n";
+               strbuf_addf(sb, header_fmt, encoding);
+       }
+       if (after_subject) {
+               strbuf_addstr(sb, after_subject);
+       }
+       if (fmt == CMIT_FMT_EMAIL) {
+               strbuf_addch(sb, '\n');
+       }
+       strbuf_release(&title);
+}
+
+static void pp_remainder(enum cmit_fmt fmt,
+                        const char **msg_p,
+                        struct strbuf *sb,
+                        int indent)
+{
+       int first = 1;
+       for (;;) {
+               const char *line = *msg_p;
+               int linelen = get_one_line(line);
+               *msg_p += linelen;
+
+               if (!linelen)
+                       break;
+
+               if (is_empty_line(line, &linelen)) {
+                       if (first)
+                               continue;
+                       if (fmt == CMIT_FMT_SHORT)
+                               break;
+               }
+               first = 0;
+
+               strbuf_grow(sb, linelen + indent + 20);
+               if (indent) {
+                       memset(sb->buf + sb->len, ' ', indent);
+                       strbuf_setlen(sb, sb->len + indent);
+               }
+               strbuf_add(sb, line, linelen);
+               strbuf_addch(sb, '\n');
+       }
+}
+
+void pretty_print_commit(enum cmit_fmt fmt, const struct commit *commit,
+                                 struct strbuf *sb, int abbrev,
+                                 const char *subject, const char *after_subject,
+                                 enum date_mode dmode, int plain_non_ascii)
+{
+       unsigned long beginning_of_body;
+       int indent = 4;
+       const char *msg = commit->buffer;
+       char *reencoded;
+       const char *encoding;
+
+       if (fmt == CMIT_FMT_USERFORMAT) {
+               format_commit_message(commit, user_format, sb);
+               return;
+       }
+
+       encoding = (git_log_output_encoding
+                   ? git_log_output_encoding
+                   : git_commit_encoding);
+       if (!encoding)
+               encoding = "utf-8";
+       reencoded = logmsg_reencode(commit, encoding);
+       if (reencoded) {
+               msg = reencoded;
+       }
+
+       if (fmt == CMIT_FMT_ONELINE || fmt == CMIT_FMT_EMAIL)
+               indent = 0;
+
+       /* After-subject is used to pass in Content-Type: multipart
+        * MIME header; in that case we do not have to do the
+        * plaintext content type even if the commit message has
+        * non 7-bit ASCII character.  Otherwise, check if we need
+        * to say this is not a 7-bit ASCII.
+        */
+       if (fmt == CMIT_FMT_EMAIL && !after_subject) {
+               int i, ch, in_body;
+
+               for (in_body = i = 0; (ch = msg[i]); i++) {
+                       if (!in_body) {
+                               /* author could be non 7-bit ASCII but
+                                * the log may be so; skip over the
+                                * header part first.
+                                */
+                               if (ch == '\n' && msg[i+1] == '\n')
+                                       in_body = 1;
+                       }
+                       else if (non_ascii(ch)) {
+                               plain_non_ascii = 1;
+                               break;
+                       }
+               }
+       }
+
+       pp_header(fmt, abbrev, dmode, encoding, commit, &msg, sb);
+       if (fmt != CMIT_FMT_ONELINE && !subject) {
+               strbuf_addch(sb, '\n');
+       }
+
+       /* Skip excess blank lines at the beginning of body, if any... */
+       for (;;) {
+               int linelen = get_one_line(msg);
+               int ll = linelen;
+               if (!linelen)
+                       break;
+               if (!is_empty_line(msg, &ll))
+                       break;
+               msg += linelen;
+       }
+
+       /* These formats treat the title line specially. */
+       if (fmt == CMIT_FMT_ONELINE || fmt == CMIT_FMT_EMAIL)
+               pp_title_line(fmt, &msg, sb, subject,
+                             after_subject, encoding, plain_non_ascii);
+
+       beginning_of_body = sb->len;
+       if (fmt != CMIT_FMT_ONELINE)
+               pp_remainder(fmt, &msg, sb, indent);
+       strbuf_rtrim(sb);
+
+       /* Make sure there is an EOLN for the non-oneline case */
+       if (fmt != CMIT_FMT_ONELINE)
+               strbuf_addch(sb, '\n');
+
+       /*
+        * The caller may append additional body text in e-mail
+        * format.  Make sure we did not strip the blank line
+        * between the header and the body.
+        */
+       if (fmt == CMIT_FMT_EMAIL && sb->len <= beginning_of_body)
+               strbuf_addch(sb, '\n');
+       free(reencoded);
+}
diff --git a/progress.c b/progress.c
new file mode 100644 (file)
index 0000000..d19f80c
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+ * Simple text-based progress display module for GIT
+ *
+ * Copyright (c) 2007 by Nicolas Pitre <nico@cam.org>
+ *
+ * This code is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#include "git-compat-util.h"
+#include "progress.h"
+
+#define TP_IDX_MAX      8
+
+struct throughput {
+       off_t curr_total;
+       off_t prev_total;
+       struct timeval prev_tv;
+       unsigned int avg_bytes;
+       unsigned int avg_misecs;
+       unsigned int last_bytes[TP_IDX_MAX];
+       unsigned int last_misecs[TP_IDX_MAX];
+       unsigned int idx;
+       char display[32];
+};
+
+struct progress {
+       const char *title;
+       int last_value;
+       unsigned total;
+       unsigned last_percent;
+       unsigned delay;
+       unsigned delayed_percent_treshold;
+       struct throughput *throughput;
+};
+
+static volatile sig_atomic_t progress_update;
+
+static void progress_interval(int signum)
+{
+       progress_update = 1;
+}
+
+static void set_progress_signal(void)
+{
+       struct sigaction sa;
+       struct itimerval v;
+
+       progress_update = 0;
+
+       memset(&sa, 0, sizeof(sa));
+       sa.sa_handler = progress_interval;
+       sigemptyset(&sa.sa_mask);
+       sa.sa_flags = SA_RESTART;
+       sigaction(SIGALRM, &sa, NULL);
+
+       v.it_interval.tv_sec = 1;
+       v.it_interval.tv_usec = 0;
+       v.it_value = v.it_interval;
+       setitimer(ITIMER_REAL, &v, NULL);
+}
+
+static void clear_progress_signal(void)
+{
+       struct itimerval v = {{0,},};
+       setitimer(ITIMER_REAL, &v, NULL);
+       signal(SIGALRM, SIG_IGN);
+       progress_update = 0;
+}
+
+static int display(struct progress *progress, unsigned n, const char *done)
+{
+       const char *eol, *tp;
+
+       if (progress->delay) {
+               if (!progress_update || --progress->delay)
+                       return 0;
+               if (progress->total) {
+                       unsigned percent = n * 100 / progress->total;
+                       if (percent > progress->delayed_percent_treshold) {
+                               /* inhibit this progress report entirely */
+                               clear_progress_signal();
+                               progress->delay = -1;
+                               progress->total = 0;
+                               return 0;
+                       }
+               }
+       }
+
+       progress->last_value = n;
+       tp = (progress->throughput) ? progress->throughput->display : "";
+       eol = done ? done : "   \r";
+       if (progress->total) {
+               unsigned percent = n * 100 / progress->total;
+               if (percent != progress->last_percent || progress_update) {
+                       progress->last_percent = percent;
+                       fprintf(stderr, "%s: %3u%% (%u/%u)%s%s",
+                               progress->title, percent, n,
+                               progress->total, tp, eol);
+                       fflush(stderr);
+                       progress_update = 0;
+                       return 1;
+               }
+       } else if (progress_update) {
+               fprintf(stderr, "%s: %u%s%s", progress->title, n, tp, eol);
+               fflush(stderr);
+               progress_update = 0;
+               return 1;
+       }
+
+       return 0;
+}
+
+static void throughput_string(struct throughput *tp, off_t total,
+                             unsigned int rate)
+{
+       int l = sizeof(tp->display);
+       if (total > 1 << 30) {
+               l -= snprintf(tp->display, l, ", %u.%2.2u GiB",
+                             (int)(total >> 30),
+                             (int)(total & ((1 << 30) - 1)) / 10737419);
+       } else if (total > 1 << 20) {
+               l -= snprintf(tp->display, l, ", %u.%2.2u MiB",
+                             (int)(total >> 20),
+                             ((int)(total & ((1 << 20) - 1)) * 100) >> 20);
+       } else if (total > 1 << 10) {
+               l -= snprintf(tp->display, l, ", %u.%2.2u KiB",
+                             (int)(total >> 10),
+                             ((int)(total & ((1 << 10) - 1)) * 100) >> 10);
+       } else {
+               l -= snprintf(tp->display, l, ", %u bytes", (int)total);
+       }
+       if (rate)
+               snprintf(tp->display + sizeof(tp->display) - l, l,
+                        " | %u KiB/s", rate);
+}
+
+void display_throughput(struct progress *progress, off_t total)
+{
+       struct throughput *tp;
+       struct timeval tv;
+       unsigned int misecs;
+
+       if (!progress)
+               return;
+       tp = progress->throughput;
+
+       gettimeofday(&tv, NULL);
+
+       if (!tp) {
+               progress->throughput = tp = calloc(1, sizeof(*tp));
+               if (tp) {
+                       tp->prev_total = tp->curr_total = total;
+                       tp->prev_tv = tv;
+               }
+               return;
+       }
+       tp->curr_total = total;
+
+       /*
+        * We have x = bytes and y = microsecs.  We want z = KiB/s:
+        *
+        *      z = (x / 1024) / (y / 1000000)
+        *      z = x / y * 1000000 / 1024
+        *      z = x / (y * 1024 / 1000000)
+        *      z = x / y'
+        *
+        * To simplify things we'll keep track of misecs, or 1024th of a sec
+        * obtained with:
+        *
+        *      y' = y * 1024 / 1000000
+        *      y' = y / (1000000 / 1024)
+        *      y' = y / 977
+        */
+       misecs = (tv.tv_sec - tp->prev_tv.tv_sec) * 1024;
+       misecs += (int)(tv.tv_usec - tp->prev_tv.tv_usec) / 977;
+
+       if (misecs > 512) {
+               unsigned int count, rate;
+
+               count = total - tp->prev_total;
+               tp->prev_total = total;
+               tp->prev_tv = tv;
+               tp->avg_bytes += count;
+               tp->avg_misecs += misecs;
+               rate = tp->avg_bytes / tp->avg_misecs;
+               tp->avg_bytes -= tp->last_bytes[tp->idx];
+               tp->avg_misecs -= tp->last_misecs[tp->idx];
+               tp->last_bytes[tp->idx] = count;
+               tp->last_misecs[tp->idx] = misecs;
+               tp->idx = (tp->idx + 1) % TP_IDX_MAX;
+
+               throughput_string(tp, total, rate);
+               if (progress->last_value != -1 && progress_update)
+                       display(progress, progress->last_value, NULL);
+       }
+}
+
+int display_progress(struct progress *progress, unsigned n)
+{
+       return progress ? display(progress, n, NULL) : 0;
+}
+
+struct progress *start_progress_delay(const char *title, unsigned total,
+                                      unsigned percent_treshold, unsigned delay)
+{
+       struct progress *progress = malloc(sizeof(*progress));
+       if (!progress) {
+               /* unlikely, but here's a good fallback */
+               fprintf(stderr, "%s...\n", title);
+               fflush(stderr);
+               return NULL;
+       }
+       progress->title = title;
+       progress->total = total;
+       progress->last_value = -1;
+       progress->last_percent = -1;
+       progress->delayed_percent_treshold = percent_treshold;
+       progress->delay = delay;
+       progress->throughput = NULL;
+       set_progress_signal();
+       return progress;
+}
+
+struct progress *start_progress(const char *title, unsigned total)
+{
+       return start_progress_delay(title, total, 0, 0);
+}
+
+void stop_progress(struct progress **p_progress)
+{
+       stop_progress_msg(p_progress, "done");
+}
+
+void stop_progress_msg(struct progress **p_progress, const char *msg)
+{
+       struct progress *progress = *p_progress;
+       if (!progress)
+               return;
+       *p_progress = NULL;
+       if (progress->last_value != -1) {
+               /* Force the last update */
+               char buf[strlen(msg) + 5];
+               struct throughput *tp = progress->throughput;
+               if (tp) {
+                       unsigned int rate = !tp->avg_misecs ? 0 :
+                                       tp->avg_bytes / tp->avg_misecs;
+                       throughput_string(tp, tp->curr_total, rate);
+               }
+               progress_update = 1;
+               sprintf(buf, ", %s.\n", msg);
+               display(progress, progress->last_value, buf);
+       }
+       clear_progress_signal();
+       free(progress->throughput);
+       free(progress);
+}
diff --git a/progress.h b/progress.h
new file mode 100644 (file)
index 0000000..611e4c4
--- /dev/null
@@ -0,0 +1,14 @@
+#ifndef PROGRESS_H
+#define PROGRESS_H
+
+struct progress;
+
+void display_throughput(struct progress *progress, off_t total);
+int display_progress(struct progress *progress, unsigned n);
+struct progress *start_progress(const char *title, unsigned total);
+struct progress *start_progress_delay(const char *title, unsigned total,
+                                      unsigned percent_treshold, unsigned delay);
+void stop_progress(struct progress **progress);
+void stop_progress_msg(struct progress **progress, const char *msg);
+
+#endif
diff --git a/quote.c b/quote.c
new file mode 100644 (file)
index 0000000..6986b44
--- /dev/null
+++ b/quote.c
@@ -0,0 +1,387 @@
+#include "cache.h"
+#include "quote.h"
+
+/* Help to copy the thing properly quoted for the shell safety.
+ * any single quote is replaced with '\'', any exclamation point
+ * is replaced with '\!', and the whole thing is enclosed in a
+ *
+ * E.g.
+ *  original     sq_quote     result
+ *  name     ==> name      ==> 'name'
+ *  a b      ==> a b       ==> 'a b'
+ *  a'b      ==> a'\''b    ==> 'a'\''b'
+ *  a!b      ==> a'\!'b    ==> 'a'\!'b'
+ */
+static inline int need_bs_quote(char c)
+{
+       return (c == '\'' || c == '!');
+}
+
+void sq_quote_buf(struct strbuf *dst, const char *src)
+{
+       char *to_free = NULL;
+
+       if (dst->buf == src)
+               to_free = strbuf_detach(dst, NULL);
+
+       strbuf_addch(dst, '\'');
+       while (*src) {
+               size_t len = strcspn(src, "'!");
+               strbuf_add(dst, src, len);
+               src += len;
+               while (need_bs_quote(*src)) {
+                       strbuf_addstr(dst, "'\\");
+                       strbuf_addch(dst, *src++);
+                       strbuf_addch(dst, '\'');
+               }
+       }
+       strbuf_addch(dst, '\'');
+       free(to_free);
+}
+
+void sq_quote_print(FILE *stream, const char *src)
+{
+       char c;
+
+       fputc('\'', stream);
+       while ((c = *src++)) {
+               if (need_bs_quote(c)) {
+                       fputs("'\\", stream);
+                       fputc(c, stream);
+                       fputc('\'', stream);
+               } else {
+                       fputc(c, stream);
+               }
+       }
+       fputc('\'', stream);
+}
+
+void sq_quote_argv(struct strbuf *dst, const char** argv, size_t maxlen)
+{
+       int i;
+
+       /* Copy into destination buffer. */
+       strbuf_grow(dst, 255);
+       for (i = 0; argv[i]; ++i) {
+               strbuf_addch(dst, ' ');
+               sq_quote_buf(dst, argv[i]);
+               if (maxlen && dst->len > maxlen)
+                       die("Too many or long arguments");
+       }
+}
+
+char *sq_dequote(char *arg)
+{
+       char *dst = arg;
+       char *src = arg;
+       char c;
+
+       if (*src != '\'')
+               return NULL;
+       for (;;) {
+               c = *++src;
+               if (!c)
+                       return NULL;
+               if (c != '\'') {
+                       *dst++ = c;
+                       continue;
+               }
+               /* We stepped out of sq */
+               switch (*++src) {
+               case '\0':
+                       *dst = 0;
+                       return arg;
+               case '\\':
+                       c = *++src;
+                       if (need_bs_quote(c) && *++src == '\'') {
+                               *dst++ = c;
+                               continue;
+                       }
+               /* Fallthrough */
+               default:
+                       return NULL;
+               }
+       }
+}
+
+/* 1 means: quote as octal
+ * 0 means: quote as octal if (quote_path_fully)
+ * -1 means: never quote
+ * c: quote as "\\c"
+ */
+#define X8(x)   x, x, x, x, x, x, x, x
+#define X16(x)  X8(x), X8(x)
+static signed char const sq_lookup[256] = {
+       /*           0    1    2    3    4    5    6    7 */
+       /* 0x00 */   1,   1,   1,   1,   1,   1,   1, 'a',
+       /* 0x08 */ 'b', 't', 'n', 'v', 'f', 'r',   1,   1,
+       /* 0x10 */ X16(1),
+       /* 0x20 */  -1,  -1, '"',  -1,  -1,  -1,  -1,  -1,
+       /* 0x28 */ X16(-1), X16(-1), X16(-1),
+       /* 0x58 */  -1,  -1,  -1,  -1,'\\',  -1,  -1,  -1,
+       /* 0x60 */ X16(-1), X8(-1),
+       /* 0x78 */  -1,  -1,  -1,  -1,  -1,  -1,  -1,   1,
+       /* 0x80 */ /* set to 0 */
+};
+
+static inline int sq_must_quote(char c)
+{
+       return sq_lookup[(unsigned char)c] + quote_path_fully > 0;
+}
+
+/* returns the longest prefix not needing a quote up to maxlen if positive.
+   This stops at the first \0 because it's marked as a character needing an
+   escape */
+static size_t next_quote_pos(const char *s, ssize_t maxlen)
+{
+       size_t len;
+       if (maxlen < 0) {
+               for (len = 0; !sq_must_quote(s[len]); len++);
+       } else {
+               for (len = 0; len < maxlen && !sq_must_quote(s[len]); len++);
+       }
+       return len;
+}
+
+/*
+ * C-style name quoting.
+ *
+ * (1) if sb and fp are both NULL, inspect the input name and counts the
+ *     number of bytes that are needed to hold c_style quoted version of name,
+ *     counting the double quotes around it but not terminating NUL, and
+ *     returns it.
+ *     However, if name does not need c_style quoting, it returns 0.
+ *
+ * (2) if sb or fp are not NULL, it emits the c_style quoted version
+ *     of name, enclosed with double quotes if asked and needed only.
+ *     Return value is the same as in (1).
+ */
+static size_t quote_c_style_counted(const char *name, ssize_t maxlen,
+                                    struct strbuf *sb, FILE *fp, int no_dq)
+{
+#undef EMIT
+#define EMIT(c)                                 \
+       do {                                        \
+               if (sb) strbuf_addch(sb, (c));          \
+               if (fp) fputc((c), fp);                 \
+               count++;                                \
+       } while (0)
+#define EMITBUF(s, l)                           \
+       do {                                        \
+               if (sb) strbuf_add(sb, (s), (l));       \
+               if (fp) fwrite((s), (l), 1, fp);        \
+               count += (l);                           \
+       } while (0)
+
+       size_t len, count = 0;
+       const char *p = name;
+
+       for (;;) {
+               int ch;
+
+               len = next_quote_pos(p, maxlen);
+               if (len == maxlen || !p[len])
+                       break;
+
+               if (!no_dq && p == name)
+                       EMIT('"');
+
+               EMITBUF(p, len);
+               EMIT('\\');
+               p += len;
+               ch = (unsigned char)*p++;
+               if (sq_lookup[ch] >= ' ') {
+                       EMIT(sq_lookup[ch]);
+               } else {
+                       EMIT(((ch >> 6) & 03) + '0');
+                       EMIT(((ch >> 3) & 07) + '0');
+                       EMIT(((ch >> 0) & 07) + '0');
+               }
+       }
+
+       EMITBUF(p, len);
+       if (p == name)   /* no ending quote needed */
+               return 0;
+
+       if (!no_dq)
+               EMIT('"');
+       return count;
+}
+
+size_t quote_c_style(const char *name, struct strbuf *sb, FILE *fp, int nodq)
+{
+       return quote_c_style_counted(name, -1, sb, fp, nodq);
+}
+
+void write_name_quoted(const char *name, FILE *fp, int terminator)
+{
+       if (terminator) {
+               quote_c_style(name, NULL, fp, 0);
+       } else {
+               fputs(name, fp);
+       }
+       fputc(terminator, fp);
+}
+
+extern void write_name_quotedpfx(const char *pfx, size_t pfxlen,
+                                 const char *name, FILE *fp, int terminator)
+{
+       int needquote = 0;
+
+       if (terminator) {
+               needquote = next_quote_pos(pfx, pfxlen) < pfxlen
+                       || name[next_quote_pos(name, -1)];
+       }
+       if (needquote) {
+               fputc('"', fp);
+               quote_c_style_counted(pfx, pfxlen, NULL, fp, 1);
+               quote_c_style(name, NULL, fp, 1);
+               fputc('"', fp);
+       } else {
+               fwrite(pfx, pfxlen, 1, fp);
+               fputs(name, fp);
+       }
+       fputc(terminator, fp);
+}
+
+/*
+ * C-style name unquoting.
+ *
+ * Quoted should point at the opening double quote.
+ * + Returns 0 if it was able to unquote the string properly, and appends the
+ *   result in the strbuf `sb'.
+ * + Returns -1 in case of error, and doesn't touch the strbuf. Though note
+ *   that this function will allocate memory in the strbuf, so calling
+ *   strbuf_release is mandatory whichever result unquote_c_style returns.
+ *
+ * Updates endp pointer to point at one past the ending double quote if given.
+ */
+int unquote_c_style(struct strbuf *sb, const char *quoted, const char **endp)
+{
+       size_t oldlen = sb->len, len;
+       int ch, ac;
+
+       if (*quoted++ != '"')
+               return -1;
+
+       for (;;) {
+               len = strcspn(quoted, "\"\\");
+               strbuf_add(sb, quoted, len);
+               quoted += len;
+
+               switch (*quoted++) {
+                 case '"':
+                       if (endp)
+                               *endp = quoted + 1;
+                       return 0;
+                 case '\\':
+                       break;
+                 default:
+                       goto error;
+               }
+
+               switch ((ch = *quoted++)) {
+               case 'a': ch = '\a'; break;
+               case 'b': ch = '\b'; break;
+               case 'f': ch = '\f'; break;
+               case 'n': ch = '\n'; break;
+               case 'r': ch = '\r'; break;
+               case 't': ch = '\t'; break;
+               case 'v': ch = '\v'; break;
+
+               case '\\': case '"':
+                       break; /* verbatim */
+
+               /* octal values with first digit over 4 overflow */
+               case '0': case '1': case '2': case '3':
+                                       ac = ((ch - '0') << 6);
+                       if ((ch = *quoted++) < '0' || '7' < ch)
+                               goto error;
+                                       ac |= ((ch - '0') << 3);
+                       if ((ch = *quoted++) < '0' || '7' < ch)
+                               goto error;
+                                       ac |= (ch - '0');
+                                       ch = ac;
+                                       break;
+                               default:
+                       goto error;
+                       }
+               strbuf_addch(sb, ch);
+               }
+
+  error:
+       strbuf_setlen(sb, oldlen);
+       return -1;
+}
+
+/* quoting as a string literal for other languages */
+
+void perl_quote_print(FILE *stream, const char *src)
+{
+       const char sq = '\'';
+       const char bq = '\\';
+       char c;
+
+       fputc(sq, stream);
+       while ((c = *src++)) {
+               if (c == sq || c == bq)
+                       fputc(bq, stream);
+               fputc(c, stream);
+       }
+       fputc(sq, stream);
+}
+
+void python_quote_print(FILE *stream, const char *src)
+{
+       const char sq = '\'';
+       const char bq = '\\';
+       const char nl = '\n';
+       char c;
+
+       fputc(sq, stream);
+       while ((c = *src++)) {
+               if (c == nl) {
+                       fputc(bq, stream);
+                       fputc('n', stream);
+                       continue;
+               }
+               if (c == sq || c == bq)
+                       fputc(bq, stream);
+               fputc(c, stream);
+       }
+       fputc(sq, stream);
+}
+
+void tcl_quote_print(FILE *stream, const char *src)
+{
+       char c;
+
+       fputc('"', stream);
+       while ((c = *src++)) {
+               switch (c) {
+               case '[': case ']':
+               case '{': case '}':
+               case '$': case '\\': case '"':
+                       fputc('\\', stream);
+               default:
+                       fputc(c, stream);
+                       break;
+               case '\f':
+                       fputs("\\f", stream);
+                       break;
+               case '\r':
+                       fputs("\\r", stream);
+                       break;
+               case '\n':
+                       fputs("\\n", stream);
+                       break;
+               case '\t':
+                       fputs("\\t", stream);
+                       break;
+               case '\v':
+                       fputs("\\v", stream);
+                       break;
+               }
+       }
+       fputc('"', stream);
+}
diff --git a/quote.h b/quote.h
new file mode 100644 (file)
index 0000000..ab7596f
--- /dev/null
+++ b/quote.h
@@ -0,0 +1,54 @@
+#ifndef QUOTE_H
+#define QUOTE_H
+
+#include <stddef.h>
+#include <stdio.h>
+
+/* Help to copy the thing properly quoted for the shell safety.
+ * any single quote is replaced with '\'', any exclamation point
+ * is replaced with '\!', and the whole thing is enclosed in a
+ * single quote pair.
+ *
+ * For example, if you are passing the result to system() as an
+ * argument:
+ *
+ * sprintf(cmd, "foobar %s %s", sq_quote(arg0), sq_quote(arg1))
+ *
+ * would be appropriate.  If the system() is going to call ssh to
+ * run the command on the other side:
+ *
+ * sprintf(cmd, "git-diff-tree %s %s", sq_quote(arg0), sq_quote(arg1));
+ * sprintf(rcmd, "ssh %s %s", sq_quote(host), sq_quote(cmd));
+ *
+ * Note that the above examples leak memory!  Remember to free result from
+ * sq_quote() in a real application.
+ *
+ * sq_quote_buf() writes to an existing buffer of specified size; it
+ * will return the number of characters that would have been written
+ * excluding the final null regardless of the buffer size.
+ */
+
+extern void sq_quote_print(FILE *stream, const char *src);
+
+extern void sq_quote_buf(struct strbuf *, const char *src);
+extern void sq_quote_argv(struct strbuf *, const char **argv, size_t maxlen);
+
+/* This unwraps what sq_quote() produces in place, but returns
+ * NULL if the input does not look like what sq_quote would have
+ * produced.
+ */
+extern char *sq_dequote(char *);
+
+extern int unquote_c_style(struct strbuf *, const char *quoted, const char **endp);
+extern size_t quote_c_style(const char *name, struct strbuf *, FILE *, int no_dq);
+
+extern void write_name_quoted(const char *name, FILE *, int terminator);
+extern void write_name_quotedpfx(const char *pfx, size_t pfxlen,
+                                 const char *name, FILE *, int terminator);
+
+/* quoting as a string literal for other languages */
+extern void perl_quote_print(FILE *stream, const char *src);
+extern void python_quote_print(FILE *stream, const char *src);
+extern void tcl_quote_print(FILE *stream, const char *src);
+
+#endif
diff --git a/reachable.c b/reachable.c
new file mode 100644 (file)
index 0000000..6383401
--- /dev/null
@@ -0,0 +1,220 @@
+#include "cache.h"
+#include "refs.h"
+#include "tag.h"
+#include "commit.h"
+#include "blob.h"
+#include "diff.h"
+#include "revision.h"
+#include "reachable.h"
+#include "cache-tree.h"
+
+static void process_blob(struct blob *blob,
+                        struct object_array *p,
+                        struct name_path *path,
+                        const char *name)
+{
+       struct object *obj = &blob->object;
+
+       if (obj->flags & SEEN)
+               return;
+       obj->flags |= SEEN;
+       /* Nothing to do, really .. The blob lookup was the important part */
+}
+
+static void process_gitlink(const unsigned char *sha1,
+                           struct object_array *p,
+                           struct name_path *path,
+                           const char *name)
+{
+       /* I don't think we want to recurse into this, really. */
+}
+
+static void process_tree(struct tree *tree,
+                        struct object_array *p,
+                        struct name_path *path,
+                        const char *name)
+{
+       struct object *obj = &tree->object;
+       struct tree_desc desc;
+       struct name_entry entry;
+       struct name_path me;
+
+       if (obj->flags & SEEN)
+               return;
+       obj->flags |= SEEN;
+       if (parse_tree(tree) < 0)
+               die("bad tree object %s", sha1_to_hex(obj->sha1));
+       name = xstrdup(name);
+       add_object(obj, p, path, name);
+       me.up = path;
+       me.elem = name;
+       me.elem_len = strlen(name);
+
+       init_tree_desc(&desc, tree->buffer, tree->size);
+
+       while (tree_entry(&desc, &entry)) {
+               if (S_ISDIR(entry.mode))
+                       process_tree(lookup_tree(entry.sha1), p, &me, entry.path);
+               else if (S_ISGITLINK(entry.mode))
+                       process_gitlink(entry.sha1, p, &me, entry.path);
+               else
+                       process_blob(lookup_blob(entry.sha1), p, &me, entry.path);
+       }
+       free(tree->buffer);
+       tree->buffer = NULL;
+}
+
+static void process_tag(struct tag *tag, struct object_array *p, const char *name)
+{
+       struct object *obj = &tag->object;
+       struct name_path me;
+
+       if (obj->flags & SEEN)
+               return;
+       obj->flags |= SEEN;
+
+       me.up = NULL;
+       me.elem = "tag:/";
+       me.elem_len = 5;
+
+       if (parse_tag(tag) < 0)
+               die("bad tag object %s", sha1_to_hex(obj->sha1));
+       add_object(tag->tagged, p, NULL, name);
+}
+
+static void walk_commit_list(struct rev_info *revs)
+{
+       int i;
+       struct commit *commit;
+       struct object_array objects = { 0, 0, NULL };
+
+       /* Walk all commits, process their trees */
+       while ((commit = get_revision(revs)) != NULL)
+               process_tree(commit->tree, &objects, NULL, "");
+
+       /* Then walk all the pending objects, recursively processing them too */
+       for (i = 0; i < revs->pending.nr; i++) {
+               struct object_array_entry *pending = revs->pending.objects + i;
+               struct object *obj = pending->item;
+               const char *name = pending->name;
+               if (obj->type == OBJ_TAG) {
+                       process_tag((struct tag *) obj, &objects, name);
+                       continue;
+               }
+               if (obj->type == OBJ_TREE) {
+                       process_tree((struct tree *)obj, &objects, NULL, name);
+                       continue;
+               }
+               if (obj->type == OBJ_BLOB) {
+                       process_blob((struct blob *)obj, &objects, NULL, name);
+                       continue;
+               }
+               die("unknown pending object %s (%s)", sha1_to_hex(obj->sha1), name);
+       }
+}
+
+static int add_one_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
+               const char *email, unsigned long timestamp, int tz,
+               const char *message, void *cb_data)
+{
+       struct object *object;
+       struct rev_info *revs = (struct rev_info *)cb_data;
+
+       object = parse_object(osha1);
+       if (object)
+               add_pending_object(revs, object, "");
+       object = parse_object(nsha1);
+       if (object)
+               add_pending_object(revs, object, "");
+       return 0;
+}
+
+static int add_one_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct object *object = parse_object(sha1);
+       struct rev_info *revs = (struct rev_info *)cb_data;
+
+       if (!object)
+               die("bad object ref: %s:%s", path, sha1_to_hex(sha1));
+       add_pending_object(revs, object, "");
+
+       return 0;
+}
+
+static int add_one_reflog(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       for_each_reflog_ent(path, add_one_reflog_ent, cb_data);
+       return 0;
+}
+
+static void add_one_tree(const unsigned char *sha1, struct rev_info *revs)
+{
+       struct tree *tree = lookup_tree(sha1);
+       add_pending_object(revs, &tree->object, "");
+}
+
+static void add_cache_tree(struct cache_tree *it, struct rev_info *revs)
+{
+       int i;
+
+       if (it->entry_count >= 0)
+               add_one_tree(it->sha1, revs);
+       for (i = 0; i < it->subtree_nr; i++)
+               add_cache_tree(it->down[i]->cache_tree, revs);
+}
+
+static void add_cache_refs(struct rev_info *revs)
+{
+       int i;
+
+       read_cache();
+       for (i = 0; i < active_nr; i++) {
+               /*
+                * The index can contain blobs and GITLINKs, GITLINKs are hashes
+                * that don't actually point to objects in the repository, it's
+                * almost guaranteed that they are NOT blobs, so we don't call
+                * lookup_blob() on them, to avoid populating the hash table
+                * with invalid information
+                */
+               if (S_ISGITLINK(ntohl(active_cache[i]->ce_mode)))
+                       continue;
+
+               lookup_blob(active_cache[i]->sha1);
+               /*
+                * We could add the blobs to the pending list, but quite
+                * frankly, we don't care. Once we've looked them up, and
+                * added them as objects, we've really done everything
+                * there is to do for a blob
+                */
+       }
+       if (active_cache_tree)
+               add_cache_tree(active_cache_tree, revs);
+}
+
+void mark_reachable_objects(struct rev_info *revs, int mark_reflog)
+{
+       /*
+        * Set up revision parsing, and mark us as being interested
+        * in all object types, not just commits.
+        */
+       revs->tag_objects = 1;
+       revs->blob_objects = 1;
+       revs->tree_objects = 1;
+
+       /* Add all refs from the index file */
+       add_cache_refs(revs);
+
+       /* Add all external refs */
+       for_each_ref(add_one_ref, revs);
+
+       /* Add all reflog info */
+       if (mark_reflog)
+               for_each_reflog(add_one_reflog, revs);
+
+       /*
+        * Set up the revision walk - this will move all commits
+        * from the pending list to the commit walking list.
+        */
+       prepare_revision_walk(revs);
+       walk_commit_list(revs);
+}
diff --git a/reachable.h b/reachable.h
new file mode 100644 (file)
index 0000000..4075181
--- /dev/null
@@ -0,0 +1,6 @@
+#ifndef REACHEABLE_H
+#define REACHEABLE_H
+
+extern void mark_reachable_objects(struct rev_info *revs, int mark_reflog);
+
+#endif
diff --git a/read-cache.c b/read-cache.c
new file mode 100644 (file)
index 0000000..7db5588
--- /dev/null
@@ -0,0 +1,1195 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#define NO_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "cache-tree.h"
+#include "refs.h"
+#include "dir.h"
+
+/* Index extensions.
+ *
+ * The first letter should be 'A'..'Z' for extensions that are not
+ * necessary for a correct operation (i.e. optimization data).
+ * When new extensions are added that _needs_ to be understood in
+ * order to correctly interpret the index file, pick character that
+ * is outside the range, to cause the reader to abort.
+ */
+
+#define CACHE_EXT(s) ( (s[0]<<24)|(s[1]<<16)|(s[2]<<8)|(s[3]) )
+#define CACHE_EXT_TREE 0x54524545      /* "TREE" */
+
+struct index_state the_index;
+
+/*
+ * This only updates the "non-critical" parts of the directory
+ * cache, ie the parts that aren't tracked by GIT, and only used
+ * to validate the cache.
+ */
+void fill_stat_cache_info(struct cache_entry *ce, struct stat *st)
+{
+       ce->ce_ctime.sec = htonl(st->st_ctime);
+       ce->ce_mtime.sec = htonl(st->st_mtime);
+#ifdef USE_NSEC
+       ce->ce_ctime.nsec = htonl(st->st_ctim.tv_nsec);
+       ce->ce_mtime.nsec = htonl(st->st_mtim.tv_nsec);
+#endif
+       ce->ce_dev = htonl(st->st_dev);
+       ce->ce_ino = htonl(st->st_ino);
+       ce->ce_uid = htonl(st->st_uid);
+       ce->ce_gid = htonl(st->st_gid);
+       ce->ce_size = htonl(st->st_size);
+
+       if (assume_unchanged)
+               ce->ce_flags |= htons(CE_VALID);
+}
+
+static int ce_compare_data(struct cache_entry *ce, struct stat *st)
+{
+       int match = -1;
+       int fd = open(ce->name, O_RDONLY);
+
+       if (fd >= 0) {
+               unsigned char sha1[20];
+               if (!index_fd(sha1, fd, st, 0, OBJ_BLOB, ce->name))
+                       match = hashcmp(sha1, ce->sha1);
+               /* index_fd() closed the file descriptor already */
+       }
+       return match;
+}
+
+static int ce_compare_link(struct cache_entry *ce, size_t expected_size)
+{
+       int match = -1;
+       char *target;
+       void *buffer;
+       unsigned long size;
+       enum object_type type;
+       int len;
+
+       target = xmalloc(expected_size);
+       len = readlink(ce->name, target, expected_size);
+       if (len != expected_size) {
+               free(target);
+               return -1;
+       }
+       buffer = read_sha1_file(ce->sha1, &type, &size);
+       if (!buffer) {
+               free(target);
+               return -1;
+       }
+       if (size == expected_size)
+               match = memcmp(buffer, target, size);
+       free(buffer);
+       free(target);
+       return match;
+}
+
+static int ce_compare_gitlink(struct cache_entry *ce)
+{
+       unsigned char sha1[20];
+
+       /*
+        * We don't actually require that the .git directory
+        * under GITLINK directory be a valid git directory. It
+        * might even be missing (in case nobody populated that
+        * sub-project).
+        *
+        * If so, we consider it always to match.
+        */
+       if (resolve_gitlink_ref(ce->name, "HEAD", sha1) < 0)
+               return 0;
+       return hashcmp(sha1, ce->sha1);
+}
+
+static int ce_modified_check_fs(struct cache_entry *ce, struct stat *st)
+{
+       switch (st->st_mode & S_IFMT) {
+       case S_IFREG:
+               if (ce_compare_data(ce, st))
+                       return DATA_CHANGED;
+               break;
+       case S_IFLNK:
+               if (ce_compare_link(ce, xsize_t(st->st_size)))
+                       return DATA_CHANGED;
+               break;
+       case S_IFDIR:
+               if (S_ISGITLINK(ntohl(ce->ce_mode)))
+                       return 0;
+       default:
+               return TYPE_CHANGED;
+       }
+       return 0;
+}
+
+static int ce_match_stat_basic(struct cache_entry *ce, struct stat *st)
+{
+       unsigned int changed = 0;
+
+       switch (ntohl(ce->ce_mode) & S_IFMT) {
+       case S_IFREG:
+               changed |= !S_ISREG(st->st_mode) ? TYPE_CHANGED : 0;
+               /* We consider only the owner x bit to be relevant for
+                * "mode changes"
+                */
+               if (trust_executable_bit &&
+                   (0100 & (ntohl(ce->ce_mode) ^ st->st_mode)))
+                       changed |= MODE_CHANGED;
+               break;
+       case S_IFLNK:
+               if (!S_ISLNK(st->st_mode) &&
+                   (has_symlinks || !S_ISREG(st->st_mode)))
+                       changed |= TYPE_CHANGED;
+               break;
+       case S_IFGITLINK:
+               if (!S_ISDIR(st->st_mode))
+                       changed |= TYPE_CHANGED;
+               else if (ce_compare_gitlink(ce))
+                       changed |= DATA_CHANGED;
+               return changed;
+       case 0: /* Special case: unmerged file in index */
+               return MODE_CHANGED | DATA_CHANGED | TYPE_CHANGED;
+       default:
+               die("internal error: ce_mode is %o", ntohl(ce->ce_mode));
+       }
+       if (ce->ce_mtime.sec != htonl(st->st_mtime))
+               changed |= MTIME_CHANGED;
+       if (ce->ce_ctime.sec != htonl(st->st_ctime))
+               changed |= CTIME_CHANGED;
+
+#ifdef USE_NSEC
+       /*
+        * nsec seems unreliable - not all filesystems support it, so
+        * as long as it is in the inode cache you get right nsec
+        * but after it gets flushed, you get zero nsec.
+        */
+       if (ce->ce_mtime.nsec != htonl(st->st_mtim.tv_nsec))
+               changed |= MTIME_CHANGED;
+       if (ce->ce_ctime.nsec != htonl(st->st_ctim.tv_nsec))
+               changed |= CTIME_CHANGED;
+#endif
+
+       if (ce->ce_uid != htonl(st->st_uid) ||
+           ce->ce_gid != htonl(st->st_gid))
+               changed |= OWNER_CHANGED;
+       if (ce->ce_ino != htonl(st->st_ino))
+               changed |= INODE_CHANGED;
+
+#ifdef USE_STDEV
+       /*
+        * st_dev breaks on network filesystems where different
+        * clients will have different views of what "device"
+        * the filesystem is on
+        */
+       if (ce->ce_dev != htonl(st->st_dev))
+               changed |= INODE_CHANGED;
+#endif
+
+       if (ce->ce_size != htonl(st->st_size))
+               changed |= DATA_CHANGED;
+
+       return changed;
+}
+
+int ie_match_stat(struct index_state *istate,
+                 struct cache_entry *ce, struct stat *st,
+                 unsigned int options)
+{
+       unsigned int changed;
+       int ignore_valid = options & CE_MATCH_IGNORE_VALID;
+       int assume_racy_is_modified = options & CE_MATCH_RACY_IS_DIRTY;
+
+       /*
+        * If it's marked as always valid in the index, it's
+        * valid whatever the checked-out copy says.
+        */
+       if (!ignore_valid && (ce->ce_flags & htons(CE_VALID)))
+               return 0;
+
+       changed = ce_match_stat_basic(ce, st);
+
+       /*
+        * Within 1 second of this sequence:
+        *      echo xyzzy >file && git-update-index --add file
+        * running this command:
+        *      echo frotz >file
+        * would give a falsely clean cache entry.  The mtime and
+        * length match the cache, and other stat fields do not change.
+        *
+        * We could detect this at update-index time (the cache entry
+        * being registered/updated records the same time as "now")
+        * and delay the return from git-update-index, but that would
+        * effectively mean we can make at most one commit per second,
+        * which is not acceptable.  Instead, we check cache entries
+        * whose mtime are the same as the index file timestamp more
+        * carefully than others.
+        */
+       if (!changed &&
+           istate->timestamp &&
+           istate->timestamp <= ntohl(ce->ce_mtime.sec)) {
+               if (assume_racy_is_modified)
+                       changed |= DATA_CHANGED;
+               else
+                       changed |= ce_modified_check_fs(ce, st);
+       }
+
+       return changed;
+}
+
+int ie_modified(struct index_state *istate,
+               struct cache_entry *ce, struct stat *st, unsigned int options)
+{
+       int changed, changed_fs;
+
+       changed = ie_match_stat(istate, ce, st, options);
+       if (!changed)
+               return 0;
+       /*
+        * If the mode or type has changed, there's no point in trying
+        * to refresh the entry - it's not going to match
+        */
+       if (changed & (MODE_CHANGED | TYPE_CHANGED))
+               return changed;
+
+       /* Immediately after read-tree or update-index --cacheinfo,
+        * the length field is zero.  For other cases the ce_size
+        * should match the SHA1 recorded in the index entry.
+        */
+       if ((changed & DATA_CHANGED) && ce->ce_size != htonl(0))
+               return changed;
+
+       changed_fs = ce_modified_check_fs(ce, st);
+       if (changed_fs)
+               return changed | changed_fs;
+       return 0;
+}
+
+int base_name_compare(const char *name1, int len1, int mode1,
+                     const char *name2, int len2, int mode2)
+{
+       unsigned char c1, c2;
+       int len = len1 < len2 ? len1 : len2;
+       int cmp;
+
+       cmp = memcmp(name1, name2, len);
+       if (cmp)
+               return cmp;
+       c1 = name1[len];
+       c2 = name2[len];
+       if (!c1 && S_ISDIR(mode1))
+               c1 = '/';
+       if (!c2 && S_ISDIR(mode2))
+               c2 = '/';
+       return (c1 < c2) ? -1 : (c1 > c2) ? 1 : 0;
+}
+
+int cache_name_compare(const char *name1, int flags1, const char *name2, int flags2)
+{
+       int len1 = flags1 & CE_NAMEMASK;
+       int len2 = flags2 & CE_NAMEMASK;
+       int len = len1 < len2 ? len1 : len2;
+       int cmp;
+
+       cmp = memcmp(name1, name2, len);
+       if (cmp)
+               return cmp;
+       if (len1 < len2)
+               return -1;
+       if (len1 > len2)
+               return 1;
+
+       /* Compare stages  */
+       flags1 &= CE_STAGEMASK;
+       flags2 &= CE_STAGEMASK;
+
+       if (flags1 < flags2)
+               return -1;
+       if (flags1 > flags2)
+               return 1;
+       return 0;
+}
+
+int index_name_pos(struct index_state *istate, const char *name, int namelen)
+{
+       int first, last;
+
+       first = 0;
+       last = istate->cache_nr;
+       while (last > first) {
+               int next = (last + first) >> 1;
+               struct cache_entry *ce = istate->cache[next];
+               int cmp = cache_name_compare(name, namelen, ce->name, ntohs(ce->ce_flags));
+               if (!cmp)
+                       return next;
+               if (cmp < 0) {
+                       last = next;
+                       continue;
+               }
+               first = next+1;
+       }
+       return -first-1;
+}
+
+/* Remove entry, return true if there are more entries to go.. */
+int remove_index_entry_at(struct index_state *istate, int pos)
+{
+       istate->cache_changed = 1;
+       istate->cache_nr--;
+       if (pos >= istate->cache_nr)
+               return 0;
+       memmove(istate->cache + pos,
+               istate->cache + pos + 1,
+               (istate->cache_nr - pos) * sizeof(struct cache_entry *));
+       return 1;
+}
+
+int remove_file_from_index(struct index_state *istate, const char *path)
+{
+       int pos = index_name_pos(istate, path, strlen(path));
+       if (pos < 0)
+               pos = -pos-1;
+       cache_tree_invalidate_path(istate->cache_tree, path);
+       while (pos < istate->cache_nr && !strcmp(istate->cache[pos]->name, path))
+               remove_index_entry_at(istate, pos);
+       return 0;
+}
+
+static int compare_name(struct cache_entry *ce, const char *path, int namelen)
+{
+       return namelen != ce_namelen(ce) || memcmp(path, ce->name, namelen);
+}
+
+static int index_name_pos_also_unmerged(struct index_state *istate,
+       const char *path, int namelen)
+{
+       int pos = index_name_pos(istate, path, namelen);
+       struct cache_entry *ce;
+
+       if (pos >= 0)
+               return pos;
+
+       /* maybe unmerged? */
+       pos = -1 - pos;
+       if (pos >= istate->cache_nr ||
+                       compare_name((ce = istate->cache[pos]), path, namelen))
+               return -1;
+
+       /* order of preference: stage 2, 1, 3 */
+       if (ce_stage(ce) == 1 && pos + 1 < istate->cache_nr &&
+                       ce_stage((ce = istate->cache[pos + 1])) == 2 &&
+                       !compare_name(ce, path, namelen))
+               pos++;
+       return pos;
+}
+
+int add_file_to_index(struct index_state *istate, const char *path, int verbose)
+{
+       int size, namelen, pos;
+       struct stat st;
+       struct cache_entry *ce;
+       unsigned ce_option = CE_MATCH_IGNORE_VALID|CE_MATCH_RACY_IS_DIRTY;
+
+       if (lstat(path, &st))
+               die("%s: unable to stat (%s)", path, strerror(errno));
+
+       if (!S_ISREG(st.st_mode) && !S_ISLNK(st.st_mode) && !S_ISDIR(st.st_mode))
+               die("%s: can only add regular files, symbolic links or git-directories", path);
+
+       namelen = strlen(path);
+       if (S_ISDIR(st.st_mode)) {
+               while (namelen && path[namelen-1] == '/')
+                       namelen--;
+       }
+       size = cache_entry_size(namelen);
+       ce = xcalloc(1, size);
+       memcpy(ce->name, path, namelen);
+       ce->ce_flags = htons(namelen);
+       fill_stat_cache_info(ce, &st);
+
+       if (trust_executable_bit && has_symlinks)
+               ce->ce_mode = create_ce_mode(st.st_mode);
+       else {
+               /* If there is an existing entry, pick the mode bits and type
+                * from it, otherwise assume unexecutable regular file.
+                */
+               struct cache_entry *ent;
+               int pos = index_name_pos_also_unmerged(istate, path, namelen);
+
+               ent = (0 <= pos) ? istate->cache[pos] : NULL;
+               ce->ce_mode = ce_mode_from_stat(ent, st.st_mode);
+       }
+
+       pos = index_name_pos(istate, ce->name, namelen);
+       if (0 <= pos &&
+           !ce_stage(istate->cache[pos]) &&
+           !ie_match_stat(istate, istate->cache[pos], &st, ce_option)) {
+               /* Nothing changed, really */
+               free(ce);
+               return 0;
+       }
+
+       if (index_path(ce->sha1, path, &st, 1))
+               die("unable to index file %s", path);
+       if (add_index_entry(istate, ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE))
+               die("unable to add %s to index",path);
+       if (verbose)
+               printf("add '%s'\n", path);
+       return 0;
+}
+
+struct cache_entry *make_cache_entry(unsigned int mode,
+               const unsigned char *sha1, const char *path, int stage,
+               int refresh)
+{
+       int size, len;
+       struct cache_entry *ce;
+
+       if (!verify_path(path))
+               return NULL;
+
+       len = strlen(path);
+       size = cache_entry_size(len);
+       ce = xcalloc(1, size);
+
+       hashcpy(ce->sha1, sha1);
+       memcpy(ce->name, path, len);
+       ce->ce_flags = create_ce_flags(len, stage);
+       ce->ce_mode = create_ce_mode(mode);
+
+       if (refresh)
+               return refresh_cache_entry(ce, 0);
+
+       return ce;
+}
+
+int ce_same_name(struct cache_entry *a, struct cache_entry *b)
+{
+       int len = ce_namelen(a);
+       return ce_namelen(b) == len && !memcmp(a->name, b->name, len);
+}
+
+int ce_path_match(const struct cache_entry *ce, const char **pathspec)
+{
+       const char *match, *name;
+       int len;
+
+       if (!pathspec)
+               return 1;
+
+       len = ce_namelen(ce);
+       name = ce->name;
+       while ((match = *pathspec++) != NULL) {
+               int matchlen = strlen(match);
+               if (matchlen > len)
+                       continue;
+               if (memcmp(name, match, matchlen))
+                       continue;
+               if (matchlen && name[matchlen-1] == '/')
+                       return 1;
+               if (name[matchlen] == '/' || !name[matchlen])
+                       return 1;
+               if (!matchlen)
+                       return 1;
+       }
+       return 0;
+}
+
+/*
+ * We fundamentally don't like some paths: we don't want
+ * dot or dot-dot anywhere, and for obvious reasons don't
+ * want to recurse into ".git" either.
+ *
+ * Also, we don't want double slashes or slashes at the
+ * end that can make pathnames ambiguous.
+ */
+static int verify_dotfile(const char *rest)
+{
+       /*
+        * The first character was '.', but that
+        * has already been discarded, we now test
+        * the rest.
+        */
+       switch (*rest) {
+       /* "." is not allowed */
+       case '\0': case '/':
+               return 0;
+
+       /*
+        * ".git" followed by  NUL or slash is bad. This
+        * shares the path end test with the ".." case.
+        */
+       case 'g':
+               if (rest[1] != 'i')
+                       break;
+               if (rest[2] != 't')
+                       break;
+               rest += 2;
+       /* fallthrough */
+       case '.':
+               if (rest[1] == '\0' || rest[1] == '/')
+                       return 0;
+       }
+       return 1;
+}
+
+int verify_path(const char *path)
+{
+       char c;
+
+       goto inside;
+       for (;;) {
+               if (!c)
+                       return 1;
+               if (c == '/') {
+inside:
+                       c = *path++;
+                       switch (c) {
+                       default:
+                               continue;
+                       case '/': case '\0':
+                               break;
+                       case '.':
+                               if (verify_dotfile(path))
+                                       continue;
+                       }
+                       return 0;
+               }
+               c = *path++;
+       }
+}
+
+/*
+ * Do we have another file that has the beginning components being a
+ * proper superset of the name we're trying to add?
+ */
+static int has_file_name(struct index_state *istate,
+                        const struct cache_entry *ce, int pos, int ok_to_replace)
+{
+       int retval = 0;
+       int len = ce_namelen(ce);
+       int stage = ce_stage(ce);
+       const char *name = ce->name;
+
+       while (pos < istate->cache_nr) {
+               struct cache_entry *p = istate->cache[pos++];
+
+               if (len >= ce_namelen(p))
+                       break;
+               if (memcmp(name, p->name, len))
+                       break;
+               if (ce_stage(p) != stage)
+                       continue;
+               if (p->name[len] != '/')
+                       continue;
+               if (!ce_stage(p) && !p->ce_mode)
+                       continue;
+               retval = -1;
+               if (!ok_to_replace)
+                       break;
+               remove_index_entry_at(istate, --pos);
+       }
+       return retval;
+}
+
+/*
+ * Do we have another file with a pathname that is a proper
+ * subset of the name we're trying to add?
+ */
+static int has_dir_name(struct index_state *istate,
+                       const struct cache_entry *ce, int pos, int ok_to_replace)
+{
+       int retval = 0;
+       int stage = ce_stage(ce);
+       const char *name = ce->name;
+       const char *slash = name + ce_namelen(ce);
+
+       for (;;) {
+               int len;
+
+               for (;;) {
+                       if (*--slash == '/')
+                               break;
+                       if (slash <= ce->name)
+                               return retval;
+               }
+               len = slash - name;
+
+               pos = index_name_pos(istate, name, ntohs(create_ce_flags(len, stage)));
+               if (pos >= 0) {
+                       /*
+                        * Found one, but not so fast.  This could
+                        * be a marker that says "I was here, but
+                        * I am being removed".  Such an entry is
+                        * not a part of the resulting tree, and
+                        * it is Ok to have a directory at the same
+                        * path.
+                        */
+                       if (stage || istate->cache[pos]->ce_mode) {
+                               retval = -1;
+                               if (!ok_to_replace)
+                                       break;
+                               remove_index_entry_at(istate, pos);
+                               continue;
+                       }
+               }
+               else
+                       pos = -pos-1;
+
+               /*
+                * Trivial optimization: if we find an entry that
+                * already matches the sub-directory, then we know
+                * we're ok, and we can exit.
+                */
+               while (pos < istate->cache_nr) {
+                       struct cache_entry *p = istate->cache[pos];
+                       if ((ce_namelen(p) <= len) ||
+                           (p->name[len] != '/') ||
+                           memcmp(p->name, name, len))
+                               break; /* not our subdirectory */
+                       if (ce_stage(p) == stage && (stage || p->ce_mode))
+                               /* p is at the same stage as our entry, and
+                                * is a subdirectory of what we are looking
+                                * at, so we cannot have conflicts at our
+                                * level or anything shorter.
+                                */
+                               return retval;
+                       pos++;
+               }
+       }
+       return retval;
+}
+
+/* We may be in a situation where we already have path/file and path
+ * is being added, or we already have path and path/file is being
+ * added.  Either one would result in a nonsense tree that has path
+ * twice when git-write-tree tries to write it out.  Prevent it.
+ *
+ * If ok-to-replace is specified, we remove the conflicting entries
+ * from the cache so the caller should recompute the insert position.
+ * When this happens, we return non-zero.
+ */
+static int check_file_directory_conflict(struct index_state *istate,
+                                        const struct cache_entry *ce,
+                                        int pos, int ok_to_replace)
+{
+       int retval;
+
+       /*
+        * When ce is an "I am going away" entry, we allow it to be added
+        */
+       if (!ce_stage(ce) && !ce->ce_mode)
+               return 0;
+
+       /*
+        * We check if the path is a sub-path of a subsequent pathname
+        * first, since removing those will not change the position
+        * in the array.
+        */
+       retval = has_file_name(istate, ce, pos, ok_to_replace);
+
+       /*
+        * Then check if the path might have a clashing sub-directory
+        * before it.
+        */
+       return retval + has_dir_name(istate, ce, pos, ok_to_replace);
+}
+
+static int add_index_entry_with_check(struct index_state *istate, struct cache_entry *ce, int option)
+{
+       int pos;
+       int ok_to_add = option & ADD_CACHE_OK_TO_ADD;
+       int ok_to_replace = option & ADD_CACHE_OK_TO_REPLACE;
+       int skip_df_check = option & ADD_CACHE_SKIP_DFCHECK;
+
+       cache_tree_invalidate_path(istate->cache_tree, ce->name);
+       pos = index_name_pos(istate, ce->name, ntohs(ce->ce_flags));
+
+       /* existing match? Just replace it. */
+       if (pos >= 0) {
+               istate->cache_changed = 1;
+               istate->cache[pos] = ce;
+               return 0;
+       }
+       pos = -pos-1;
+
+       /*
+        * Inserting a merged entry ("stage 0") into the index
+        * will always replace all non-merged entries..
+        */
+       if (pos < istate->cache_nr && ce_stage(ce) == 0) {
+               while (ce_same_name(istate->cache[pos], ce)) {
+                       ok_to_add = 1;
+                       if (!remove_index_entry_at(istate, pos))
+                               break;
+               }
+       }
+
+       if (!ok_to_add)
+               return -1;
+       if (!verify_path(ce->name))
+               return -1;
+
+       if (!skip_df_check &&
+           check_file_directory_conflict(istate, ce, pos, ok_to_replace)) {
+               if (!ok_to_replace)
+                       return error("'%s' appears as both a file and as a directory",
+                                    ce->name);
+               pos = index_name_pos(istate, ce->name, ntohs(ce->ce_flags));
+               pos = -pos-1;
+       }
+       return pos + 1;
+}
+
+int add_index_entry(struct index_state *istate, struct cache_entry *ce, int option)
+{
+       int pos;
+
+       if (option & ADD_CACHE_JUST_APPEND)
+               pos = istate->cache_nr;
+       else {
+               int ret;
+               ret = add_index_entry_with_check(istate, ce, option);
+               if (ret <= 0)
+                       return ret;
+               pos = ret - 1;
+       }
+
+       /* Make sure the array is big enough .. */
+       if (istate->cache_nr == istate->cache_alloc) {
+               istate->cache_alloc = alloc_nr(istate->cache_alloc);
+               istate->cache = xrealloc(istate->cache,
+                                       istate->cache_alloc * sizeof(struct cache_entry *));
+       }
+
+       /* Add it in.. */
+       istate->cache_nr++;
+       if (istate->cache_nr > pos + 1)
+               memmove(istate->cache + pos + 1,
+                       istate->cache + pos,
+                       (istate->cache_nr - pos - 1) * sizeof(ce));
+       istate->cache[pos] = ce;
+       istate->cache_changed = 1;
+       return 0;
+}
+
+/*
+ * "refresh" does not calculate a new sha1 file or bring the
+ * cache up-to-date for mode/content changes. But what it
+ * _does_ do is to "re-match" the stat information of a file
+ * with the cache, so that you can refresh the cache for a
+ * file that hasn't been changed but where the stat entry is
+ * out of date.
+ *
+ * For example, you'd want to do this after doing a "git-read-tree",
+ * to link up the stat cache details with the proper files.
+ */
+static struct cache_entry *refresh_cache_ent(struct index_state *istate,
+                                            struct cache_entry *ce,
+                                            unsigned int options, int *err)
+{
+       struct stat st;
+       struct cache_entry *updated;
+       int changed, size;
+       int ignore_valid = options & CE_MATCH_IGNORE_VALID;
+
+       if (lstat(ce->name, &st) < 0) {
+               if (err)
+                       *err = errno;
+               return NULL;
+       }
+
+       changed = ie_match_stat(istate, ce, &st, options);
+       if (!changed) {
+               /*
+                * The path is unchanged.  If we were told to ignore
+                * valid bit, then we did the actual stat check and
+                * found that the entry is unmodified.  If the entry
+                * is not marked VALID, this is the place to mark it
+                * valid again, under "assume unchanged" mode.
+                */
+               if (ignore_valid && assume_unchanged &&
+                   !(ce->ce_flags & htons(CE_VALID)))
+                       ; /* mark this one VALID again */
+               else
+                       return ce;
+       }
+
+       if (ie_modified(istate, ce, &st, options)) {
+               if (err)
+                       *err = EINVAL;
+               return NULL;
+       }
+
+       size = ce_size(ce);
+       updated = xmalloc(size);
+       memcpy(updated, ce, size);
+       fill_stat_cache_info(updated, &st);
+
+       /*
+        * If ignore_valid is not set, we should leave CE_VALID bit
+        * alone.  Otherwise, paths marked with --no-assume-unchanged
+        * (i.e. things to be edited) will reacquire CE_VALID bit
+        * automatically, which is not really what we want.
+        */
+       if (!ignore_valid && assume_unchanged &&
+           !(ce->ce_flags & htons(CE_VALID)))
+               updated->ce_flags &= ~htons(CE_VALID);
+
+       return updated;
+}
+
+int refresh_index(struct index_state *istate, unsigned int flags, const char **pathspec, char *seen)
+{
+       int i;
+       int has_errors = 0;
+       int really = (flags & REFRESH_REALLY) != 0;
+       int allow_unmerged = (flags & REFRESH_UNMERGED) != 0;
+       int quiet = (flags & REFRESH_QUIET) != 0;
+       int not_new = (flags & REFRESH_IGNORE_MISSING) != 0;
+       unsigned int options = really ? CE_MATCH_IGNORE_VALID : 0;
+
+       for (i = 0; i < istate->cache_nr; i++) {
+               struct cache_entry *ce, *new;
+               int cache_errno = 0;
+
+               ce = istate->cache[i];
+               if (ce_stage(ce)) {
+                       while ((i < istate->cache_nr) &&
+                              ! strcmp(istate->cache[i]->name, ce->name))
+                               i++;
+                       i--;
+                       if (allow_unmerged)
+                               continue;
+                       printf("%s: needs merge\n", ce->name);
+                       has_errors = 1;
+                       continue;
+               }
+
+               if (pathspec && !match_pathspec(pathspec, ce->name, strlen(ce->name), 0, seen))
+                       continue;
+
+               new = refresh_cache_ent(istate, ce, options, &cache_errno);
+               if (new == ce)
+                       continue;
+               if (!new) {
+                       if (not_new && cache_errno == ENOENT)
+                               continue;
+                       if (really && cache_errno == EINVAL) {
+                               /* If we are doing --really-refresh that
+                                * means the index is not valid anymore.
+                                */
+                               ce->ce_flags &= ~htons(CE_VALID);
+                               istate->cache_changed = 1;
+                       }
+                       if (quiet)
+                               continue;
+                       printf("%s: needs update\n", ce->name);
+                       has_errors = 1;
+                       continue;
+               }
+               istate->cache_changed = 1;
+               /* You can NOT just free istate->cache[i] here, since it
+                * might not be necessarily malloc()ed but can also come
+                * from mmap(). */
+               istate->cache[i] = new;
+       }
+       return has_errors;
+}
+
+struct cache_entry *refresh_cache_entry(struct cache_entry *ce, int really)
+{
+       return refresh_cache_ent(&the_index, ce, really, NULL);
+}
+
+static int verify_hdr(struct cache_header *hdr, unsigned long size)
+{
+       SHA_CTX c;
+       unsigned char sha1[20];
+
+       if (hdr->hdr_signature != htonl(CACHE_SIGNATURE))
+               return error("bad signature");
+       if (hdr->hdr_version != htonl(2))
+               return error("bad index version");
+       SHA1_Init(&c);
+       SHA1_Update(&c, hdr, size - 20);
+       SHA1_Final(sha1, &c);
+       if (hashcmp(sha1, (unsigned char *)hdr + size - 20))
+               return error("bad index file sha1 signature");
+       return 0;
+}
+
+static int read_index_extension(struct index_state *istate,
+                               const char *ext, void *data, unsigned long sz)
+{
+       switch (CACHE_EXT(ext)) {
+       case CACHE_EXT_TREE:
+               istate->cache_tree = cache_tree_read(data, sz);
+               break;
+       default:
+               if (*ext < 'A' || 'Z' < *ext)
+                       return error("index uses %.4s extension, which we do not understand",
+                                    ext);
+               fprintf(stderr, "ignoring %.4s extension\n", ext);
+               break;
+       }
+       return 0;
+}
+
+int read_index(struct index_state *istate)
+{
+       return read_index_from(istate, get_index_file());
+}
+
+/* remember to discard_cache() before reading a different cache! */
+int read_index_from(struct index_state *istate, const char *path)
+{
+       int fd, i;
+       struct stat st;
+       unsigned long offset;
+       struct cache_header *hdr;
+
+       errno = EBUSY;
+       if (istate->mmap)
+               return istate->cache_nr;
+
+       errno = ENOENT;
+       istate->timestamp = 0;
+       fd = open(path, O_RDONLY);
+       if (fd < 0) {
+               if (errno == ENOENT)
+                       return 0;
+               die("index file open failed (%s)", strerror(errno));
+       }
+
+       if (fstat(fd, &st))
+               die("cannot stat the open index (%s)", strerror(errno));
+
+       errno = EINVAL;
+       istate->mmap_size = xsize_t(st.st_size);
+       if (istate->mmap_size < sizeof(struct cache_header) + 20)
+               die("index file smaller than expected");
+
+       istate->mmap = xmmap(NULL, istate->mmap_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+       close(fd);
+
+       hdr = istate->mmap;
+       if (verify_hdr(hdr, istate->mmap_size) < 0)
+               goto unmap;
+
+       istate->cache_nr = ntohl(hdr->hdr_entries);
+       istate->cache_alloc = alloc_nr(istate->cache_nr);
+       istate->cache = xcalloc(istate->cache_alloc, sizeof(struct cache_entry *));
+
+       offset = sizeof(*hdr);
+       for (i = 0; i < istate->cache_nr; i++) {
+               struct cache_entry *ce;
+
+               ce = (struct cache_entry *)((char *)(istate->mmap) + offset);
+               offset = offset + ce_size(ce);
+               istate->cache[i] = ce;
+       }
+       istate->timestamp = st.st_mtime;
+       while (offset <= istate->mmap_size - 20 - 8) {
+               /* After an array of active_nr index entries,
+                * there can be arbitrary number of extended
+                * sections, each of which is prefixed with
+                * extension name (4-byte) and section length
+                * in 4-byte network byte order.
+                */
+               unsigned long extsize;
+               memcpy(&extsize, (char *)(istate->mmap) + offset + 4, 4);
+               extsize = ntohl(extsize);
+               if (read_index_extension(istate,
+                                        ((const char *) (istate->mmap)) + offset,
+                                        (char *) (istate->mmap) + offset + 8,
+                                        extsize) < 0)
+                       goto unmap;
+               offset += 8;
+               offset += extsize;
+       }
+       return istate->cache_nr;
+
+unmap:
+       munmap(istate->mmap, istate->mmap_size);
+       errno = EINVAL;
+       die("index file corrupt");
+}
+
+int discard_index(struct index_state *istate)
+{
+       int ret;
+
+       istate->cache_nr = 0;
+       istate->cache_changed = 0;
+       istate->timestamp = 0;
+       cache_tree_free(&(istate->cache_tree));
+       if (istate->mmap == NULL)
+               return 0;
+       ret = munmap(istate->mmap, istate->mmap_size);
+       istate->mmap = NULL;
+       istate->mmap_size = 0;
+
+       /* no need to throw away allocated active_cache */
+       return ret;
+}
+
+#define WRITE_BUFFER_SIZE 8192
+static unsigned char write_buffer[WRITE_BUFFER_SIZE];
+static unsigned long write_buffer_len;
+
+static int ce_write_flush(SHA_CTX *context, int fd)
+{
+       unsigned int buffered = write_buffer_len;
+       if (buffered) {
+               SHA1_Update(context, write_buffer, buffered);
+               if (write_in_full(fd, write_buffer, buffered) != buffered)
+                       return -1;
+               write_buffer_len = 0;
+       }
+       return 0;
+}
+
+static int ce_write(SHA_CTX *context, int fd, void *data, unsigned int len)
+{
+       while (len) {
+               unsigned int buffered = write_buffer_len;
+               unsigned int partial = WRITE_BUFFER_SIZE - buffered;
+               if (partial > len)
+                       partial = len;
+               memcpy(write_buffer + buffered, data, partial);
+               buffered += partial;
+               if (buffered == WRITE_BUFFER_SIZE) {
+                       write_buffer_len = buffered;
+                       if (ce_write_flush(context, fd))
+                               return -1;
+                       buffered = 0;
+               }
+               write_buffer_len = buffered;
+               len -= partial;
+               data = (char *) data + partial;
+       }
+       return 0;
+}
+
+static int write_index_ext_header(SHA_CTX *context, int fd,
+                                 unsigned int ext, unsigned int sz)
+{
+       ext = htonl(ext);
+       sz = htonl(sz);
+       return ((ce_write(context, fd, &ext, 4) < 0) ||
+               (ce_write(context, fd, &sz, 4) < 0)) ? -1 : 0;
+}
+
+static int ce_flush(SHA_CTX *context, int fd)
+{
+       unsigned int left = write_buffer_len;
+
+       if (left) {
+               write_buffer_len = 0;
+               SHA1_Update(context, write_buffer, left);
+       }
+
+       /* Flush first if not enough space for SHA1 signature */
+       if (left + 20 > WRITE_BUFFER_SIZE) {
+               if (write_in_full(fd, write_buffer, left) != left)
+                       return -1;
+               left = 0;
+       }
+
+       /* Append the SHA1 signature at the end */
+       SHA1_Final(write_buffer + left, context);
+       left += 20;
+       return (write_in_full(fd, write_buffer, left) != left) ? -1 : 0;
+}
+
+static void ce_smudge_racily_clean_entry(struct cache_entry *ce)
+{
+       /*
+        * The only thing we care about in this function is to smudge the
+        * falsely clean entry due to touch-update-touch race, so we leave
+        * everything else as they are.  We are called for entries whose
+        * ce_mtime match the index file mtime.
+        */
+       struct stat st;
+
+       if (lstat(ce->name, &st) < 0)
+               return;
+       if (ce_match_stat_basic(ce, &st))
+               return;
+       if (ce_modified_check_fs(ce, &st)) {
+               /* This is "racily clean"; smudge it.  Note that this
+                * is a tricky code.  At first glance, it may appear
+                * that it can break with this sequence:
+                *
+                * $ echo xyzzy >frotz
+                * $ git-update-index --add frotz
+                * $ : >frotz
+                * $ sleep 3
+                * $ echo filfre >nitfol
+                * $ git-update-index --add nitfol
+                *
+                * but it does not.  When the second update-index runs,
+                * it notices that the entry "frotz" has the same timestamp
+                * as index, and if we were to smudge it by resetting its
+                * size to zero here, then the object name recorded
+                * in index is the 6-byte file but the cached stat information
+                * becomes zero --- which would then match what we would
+                * obtain from the filesystem next time we stat("frotz").
+                *
+                * However, the second update-index, before calling
+                * this function, notices that the cached size is 6
+                * bytes and what is on the filesystem is an empty
+                * file, and never calls us, so the cached size information
+                * for "frotz" stays 6 which does not match the filesystem.
+                */
+               ce->ce_size = htonl(0);
+       }
+}
+
+int write_index(struct index_state *istate, int newfd)
+{
+       SHA_CTX c;
+       struct cache_header hdr;
+       int i, err, removed;
+       struct cache_entry **cache = istate->cache;
+       int entries = istate->cache_nr;
+
+       for (i = removed = 0; i < entries; i++)
+               if (!cache[i]->ce_mode)
+                       removed++;
+
+       hdr.hdr_signature = htonl(CACHE_SIGNATURE);
+       hdr.hdr_version = htonl(2);
+       hdr.hdr_entries = htonl(entries - removed);
+
+       SHA1_Init(&c);
+       if (ce_write(&c, newfd, &hdr, sizeof(hdr)) < 0)
+               return -1;
+
+       for (i = 0; i < entries; i++) {
+               struct cache_entry *ce = cache[i];
+               if (!ce->ce_mode)
+                       continue;
+               if (istate->timestamp &&
+                   istate->timestamp <= ntohl(ce->ce_mtime.sec))
+                       ce_smudge_racily_clean_entry(ce);
+               if (ce_write(&c, newfd, ce, ce_size(ce)) < 0)
+                       return -1;
+       }
+
+       /* Write extension data here */
+       if (istate->cache_tree) {
+               struct strbuf sb;
+
+               strbuf_init(&sb, 0);
+               cache_tree_write(&sb, istate->cache_tree);
+               err = write_index_ext_header(&c, newfd, CACHE_EXT_TREE, sb.len) < 0
+                       || ce_write(&c, newfd, sb.buf, sb.len) < 0;
+               strbuf_release(&sb);
+               if (err)
+                       return -1;
+       }
+       return ce_flush(&c, newfd);
+}
diff --git a/receive-pack.c b/receive-pack.c
new file mode 100644 (file)
index 0000000..fba4cf8
--- /dev/null
@@ -0,0 +1,494 @@
+#include "cache.h"
+#include "pack.h"
+#include "refs.h"
+#include "pkt-line.h"
+#include "run-command.h"
+#include "exec_cmd.h"
+#include "commit.h"
+#include "object.h"
+
+static const char receive_pack_usage[] = "git-receive-pack <git-dir>";
+
+static int deny_non_fast_forwards = 0;
+static int receive_unpack_limit = -1;
+static int transfer_unpack_limit = -1;
+static int unpack_limit = 100;
+static int report_status;
+
+static char capabilities[] = " report-status delete-refs ";
+static int capabilities_sent;
+
+static int receive_pack_config(const char *var, const char *value)
+{
+       if (strcmp(var, "receive.denynonfastforwards") == 0) {
+               deny_non_fast_forwards = git_config_bool(var, value);
+               return 0;
+       }
+
+       if (strcmp(var, "receive.unpacklimit") == 0) {
+               receive_unpack_limit = git_config_int(var, value);
+               return 0;
+       }
+
+       if (strcmp(var, "transfer.unpacklimit") == 0) {
+               transfer_unpack_limit = git_config_int(var, value);
+               return 0;
+       }
+
+       return git_default_config(var, value);
+}
+
+static int show_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       if (capabilities_sent)
+               packet_write(1, "%s %s\n", sha1_to_hex(sha1), path);
+       else
+               packet_write(1, "%s %s%c%s\n",
+                            sha1_to_hex(sha1), path, 0, capabilities);
+       capabilities_sent = 1;
+       return 0;
+}
+
+static void write_head_info(void)
+{
+       for_each_ref(show_ref, NULL);
+       if (!capabilities_sent)
+               show_ref("capabilities^{}", null_sha1, 0, NULL);
+
+}
+
+struct command {
+       struct command *next;
+       const char *error_string;
+       unsigned char old_sha1[20];
+       unsigned char new_sha1[20];
+       char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static struct command *commands;
+
+static const char pre_receive_hook[] = "hooks/pre-receive";
+static const char post_receive_hook[] = "hooks/post-receive";
+
+static int hook_status(int code, const char *hook_name)
+{
+       switch (code) {
+       case 0:
+               return 0;
+       case -ERR_RUN_COMMAND_FORK:
+               return error("hook fork failed");
+       case -ERR_RUN_COMMAND_EXEC:
+               return error("hook execute failed");
+       case -ERR_RUN_COMMAND_PIPE:
+               return error("hook pipe failed");
+       case -ERR_RUN_COMMAND_WAITPID:
+               return error("waitpid failed");
+       case -ERR_RUN_COMMAND_WAITPID_WRONG_PID:
+               return error("waitpid is confused");
+       case -ERR_RUN_COMMAND_WAITPID_SIGNAL:
+               return error("%s died of signal", hook_name);
+       case -ERR_RUN_COMMAND_WAITPID_NOEXIT:
+               return error("%s died strangely", hook_name);
+       default:
+               error("%s exited with error code %d", hook_name, -code);
+               return -code;
+       }
+}
+
+static int run_hook(const char *hook_name)
+{
+       static char buf[sizeof(commands->old_sha1) * 2 + PATH_MAX + 4];
+       struct command *cmd;
+       struct child_process proc;
+       const char *argv[2];
+       int have_input = 0, code;
+
+       for (cmd = commands; !have_input && cmd; cmd = cmd->next) {
+               if (!cmd->error_string)
+                       have_input = 1;
+       }
+
+       if (!have_input || access(hook_name, X_OK) < 0)
+               return 0;
+
+       argv[0] = hook_name;
+       argv[1] = NULL;
+
+       memset(&proc, 0, sizeof(proc));
+       proc.argv = argv;
+       proc.in = -1;
+       proc.stdout_to_stderr = 1;
+
+       code = start_command(&proc);
+       if (code)
+               return hook_status(code, hook_name);
+       for (cmd = commands; cmd; cmd = cmd->next) {
+               if (!cmd->error_string) {
+                       size_t n = snprintf(buf, sizeof(buf), "%s %s %s\n",
+                               sha1_to_hex(cmd->old_sha1),
+                               sha1_to_hex(cmd->new_sha1),
+                               cmd->ref_name);
+                       if (write_in_full(proc.in, buf, n) != n)
+                               break;
+               }
+       }
+       return hook_status(finish_command(&proc), hook_name);
+}
+
+static int run_update_hook(struct command *cmd)
+{
+       static const char update_hook[] = "hooks/update";
+       struct child_process proc;
+       const char *argv[5];
+
+       if (access(update_hook, X_OK) < 0)
+               return 0;
+
+       argv[0] = update_hook;
+       argv[1] = cmd->ref_name;
+       argv[2] = sha1_to_hex(cmd->old_sha1);
+       argv[3] = sha1_to_hex(cmd->new_sha1);
+       argv[4] = NULL;
+
+       memset(&proc, 0, sizeof(proc));
+       proc.argv = argv;
+       proc.no_stdin = 1;
+       proc.stdout_to_stderr = 1;
+
+       return hook_status(run_command(&proc), update_hook);
+}
+
+static const char *update(struct command *cmd)
+{
+       const char *name = cmd->ref_name;
+       unsigned char *old_sha1 = cmd->old_sha1;
+       unsigned char *new_sha1 = cmd->new_sha1;
+       struct ref_lock *lock;
+
+       if (!prefixcmp(name, "refs/") && check_ref_format(name + 5)) {
+               error("refusing to create funny ref '%s' remotely", name);
+               return "funny refname";
+       }
+
+       if (!is_null_sha1(new_sha1) && !has_sha1_file(new_sha1)) {
+               error("unpack should have generated %s, "
+                     "but I can't find it!", sha1_to_hex(new_sha1));
+               return "bad pack";
+       }
+       if (deny_non_fast_forwards && !is_null_sha1(new_sha1) &&
+           !is_null_sha1(old_sha1) &&
+           !prefixcmp(name, "refs/heads/")) {
+               struct commit *old_commit, *new_commit;
+               struct commit_list *bases, *ent;
+
+               old_commit = (struct commit *)parse_object(old_sha1);
+               new_commit = (struct commit *)parse_object(new_sha1);
+               bases = get_merge_bases(old_commit, new_commit, 1);
+               for (ent = bases; ent; ent = ent->next)
+                       if (!hashcmp(old_sha1, ent->item->object.sha1))
+                               break;
+               free_commit_list(bases);
+               if (!ent) {
+                       error("denying non-fast forward %s"
+                             " (you should pull first)", name);
+                       return "non-fast forward";
+               }
+       }
+       if (run_update_hook(cmd)) {
+               error("hook declined to update %s", name);
+               return "hook declined";
+       }
+
+       if (is_null_sha1(new_sha1)) {
+               if (!parse_object(old_sha1)) {
+                       warning ("Allowing deletion of corrupt ref.");
+                       old_sha1 = NULL;
+               }
+               if (delete_ref(name, old_sha1)) {
+                       error("failed to delete %s", name);
+                       return "failed to delete";
+               }
+               return NULL; /* good */
+       }
+       else {
+               lock = lock_any_ref_for_update(name, old_sha1, 0);
+               if (!lock) {
+                       error("failed to lock %s", name);
+                       return "failed to lock";
+               }
+               if (write_ref_sha1(lock, new_sha1, "push")) {
+                       return "failed to write"; /* error() already called */
+               }
+               return NULL; /* good */
+       }
+}
+
+static char update_post_hook[] = "hooks/post-update";
+
+static void run_update_post_hook(struct command *cmd)
+{
+       struct command *cmd_p;
+       int argc;
+       const char **argv;
+
+       for (argc = 0, cmd_p = cmd; cmd_p; cmd_p = cmd_p->next) {
+               if (cmd_p->error_string)
+                       continue;
+               argc++;
+       }
+       if (!argc || access(update_post_hook, X_OK) < 0)
+               return;
+       argv = xmalloc(sizeof(*argv) * (2 + argc));
+       argv[0] = update_post_hook;
+
+       for (argc = 1, cmd_p = cmd; cmd_p; cmd_p = cmd_p->next) {
+               char *p;
+               if (cmd_p->error_string)
+                       continue;
+               p = xmalloc(strlen(cmd_p->ref_name) + 1);
+               strcpy(p, cmd_p->ref_name);
+               argv[argc] = p;
+               argc++;
+       }
+       argv[argc] = NULL;
+       run_command_v_opt(argv, RUN_COMMAND_NO_STDIN
+               | RUN_COMMAND_STDOUT_TO_STDERR);
+}
+
+static void execute_commands(const char *unpacker_error)
+{
+       struct command *cmd = commands;
+
+       if (unpacker_error) {
+               while (cmd) {
+                       cmd->error_string = "n/a (unpacker error)";
+                       cmd = cmd->next;
+               }
+               return;
+       }
+
+       if (run_hook(pre_receive_hook)) {
+               while (cmd) {
+                       cmd->error_string = "pre-receive hook declined";
+                       cmd = cmd->next;
+               }
+               return;
+       }
+
+       while (cmd) {
+               cmd->error_string = update(cmd);
+               cmd = cmd->next;
+       }
+}
+
+static void read_head_info(void)
+{
+       struct command **p = &commands;
+       for (;;) {
+               static char line[1000];
+               unsigned char old_sha1[20], new_sha1[20];
+               struct command *cmd;
+               char *refname;
+               int len, reflen;
+
+               len = packet_read_line(0, line, sizeof(line));
+               if (!len)
+                       break;
+               if (line[len-1] == '\n')
+                       line[--len] = 0;
+               if (len < 83 ||
+                   line[40] != ' ' ||
+                   line[81] != ' ' ||
+                   get_sha1_hex(line, old_sha1) ||
+                   get_sha1_hex(line + 41, new_sha1))
+                       die("protocol error: expected old/new/ref, got '%s'",
+                           line);
+
+               refname = line + 82;
+               reflen = strlen(refname);
+               if (reflen + 82 < len) {
+                       if (strstr(refname + reflen + 1, "report-status"))
+                               report_status = 1;
+               }
+               cmd = xmalloc(sizeof(struct command) + len - 80);
+               hashcpy(cmd->old_sha1, old_sha1);
+               hashcpy(cmd->new_sha1, new_sha1);
+               memcpy(cmd->ref_name, line + 82, len - 81);
+               cmd->error_string = NULL;
+               cmd->next = NULL;
+               *p = cmd;
+               p = &cmd->next;
+       }
+}
+
+static const char *parse_pack_header(struct pack_header *hdr)
+{
+       switch (read_pack_header(0, hdr)) {
+       case PH_ERROR_EOF:
+               return "eof before pack header was fully read";
+
+       case PH_ERROR_PACK_SIGNATURE:
+               return "protocol error (pack signature mismatch detected)";
+
+       case PH_ERROR_PROTOCOL:
+               return "protocol error (pack version unsupported)";
+
+       default:
+               return "unknown error in parse_pack_header";
+
+       case 0:
+               return NULL;
+       }
+}
+
+static const char *pack_lockfile;
+
+static const char *unpack(void)
+{
+       struct pack_header hdr;
+       const char *hdr_err;
+       char hdr_arg[38];
+
+       hdr_err = parse_pack_header(&hdr);
+       if (hdr_err)
+               return hdr_err;
+       snprintf(hdr_arg, sizeof(hdr_arg), "--pack_header=%u,%u",
+                       ntohl(hdr.hdr_version), ntohl(hdr.hdr_entries));
+
+       if (ntohl(hdr.hdr_entries) < unpack_limit) {
+               int code;
+               const char *unpacker[3];
+               unpacker[0] = "unpack-objects";
+               unpacker[1] = hdr_arg;
+               unpacker[2] = NULL;
+               code = run_command_v_opt(unpacker, RUN_GIT_CMD);
+               switch (code) {
+               case 0:
+                       return NULL;
+               case -ERR_RUN_COMMAND_FORK:
+                       return "unpack fork failed";
+               case -ERR_RUN_COMMAND_EXEC:
+                       return "unpack execute failed";
+               case -ERR_RUN_COMMAND_WAITPID:
+                       return "waitpid failed";
+               case -ERR_RUN_COMMAND_WAITPID_WRONG_PID:
+                       return "waitpid is confused";
+               case -ERR_RUN_COMMAND_WAITPID_SIGNAL:
+                       return "unpacker died of signal";
+               case -ERR_RUN_COMMAND_WAITPID_NOEXIT:
+                       return "unpacker died strangely";
+               default:
+                       return "unpacker exited with error code";
+               }
+       } else {
+               const char *keeper[6];
+               int s, status;
+               char keep_arg[256];
+               struct child_process ip;
+
+               s = sprintf(keep_arg, "--keep=receive-pack %i on ", getpid());
+               if (gethostname(keep_arg + s, sizeof(keep_arg) - s))
+                       strcpy(keep_arg + s, "localhost");
+
+               keeper[0] = "index-pack";
+               keeper[1] = "--stdin";
+               keeper[2] = "--fix-thin";
+               keeper[3] = hdr_arg;
+               keeper[4] = keep_arg;
+               keeper[5] = NULL;
+               memset(&ip, 0, sizeof(ip));
+               ip.argv = keeper;
+               ip.out = -1;
+               ip.git_cmd = 1;
+               if (start_command(&ip))
+                       return "index-pack fork failed";
+               pack_lockfile = index_pack_lockfile(ip.out);
+               status = finish_command(&ip);
+               if (!status) {
+                       reprepare_packed_git();
+                       return NULL;
+               }
+               return "index-pack abnormal exit";
+       }
+}
+
+static void report(const char *unpack_status)
+{
+       struct command *cmd;
+       packet_write(1, "unpack %s\n",
+                    unpack_status ? unpack_status : "ok");
+       for (cmd = commands; cmd; cmd = cmd->next) {
+               if (!cmd->error_string)
+                       packet_write(1, "ok %s\n",
+                                    cmd->ref_name);
+               else
+                       packet_write(1, "ng %s %s\n",
+                                    cmd->ref_name, cmd->error_string);
+       }
+       packet_flush(1);
+}
+
+static int delete_only(struct command *cmd)
+{
+       while (cmd) {
+               if (!is_null_sha1(cmd->new_sha1))
+                       return 0;
+               cmd = cmd->next;
+       }
+       return 1;
+}
+
+int main(int argc, char **argv)
+{
+       int i;
+       char *dir = NULL;
+
+       argv++;
+       for (i = 1; i < argc; i++) {
+               char *arg = *argv++;
+
+               if (*arg == '-') {
+                       /* Do flag handling here */
+                       usage(receive_pack_usage);
+               }
+               if (dir)
+                       usage(receive_pack_usage);
+               dir = arg;
+       }
+       if (!dir)
+               usage(receive_pack_usage);
+
+       if (!enter_repo(dir, 0))
+               die("'%s': unable to chdir or not a git archive", dir);
+
+       if (is_repository_shallow())
+               die("attempt to push into a shallow repository");
+
+       git_config(receive_pack_config);
+
+       if (0 <= transfer_unpack_limit)
+               unpack_limit = transfer_unpack_limit;
+       else if (0 <= receive_unpack_limit)
+               unpack_limit = receive_unpack_limit;
+
+       write_head_info();
+
+       /* EOF */
+       packet_flush(1);
+
+       read_head_info();
+       if (commands) {
+               const char *unpack_status = NULL;
+
+               if (!delete_only(commands))
+                       unpack_status = unpack();
+               execute_commands(unpack_status);
+               if (pack_lockfile)
+                       unlink(pack_lockfile);
+               if (report_status)
+                       report(unpack_status);
+               run_hook(post_receive_hook);
+               run_update_post_hook(commands);
+       }
+       return 0;
+}
diff --git a/reflog-walk.c b/reflog-walk.c
new file mode 100644 (file)
index 0000000..ee1456b
--- /dev/null
@@ -0,0 +1,274 @@
+#include "cache.h"
+#include "commit.h"
+#include "refs.h"
+#include "diff.h"
+#include "revision.h"
+#include "path-list.h"
+#include "reflog-walk.h"
+
+struct complete_reflogs {
+       char *ref;
+       struct reflog_info {
+               unsigned char osha1[20], nsha1[20];
+               char *email;
+               unsigned long timestamp;
+               int tz;
+               char *message;
+       } *items;
+       int nr, alloc;
+};
+
+static int read_one_reflog(unsigned char *osha1, unsigned char *nsha1,
+               const char *email, unsigned long timestamp, int tz,
+               const char *message, void *cb_data)
+{
+       struct complete_reflogs *array = cb_data;
+       struct reflog_info *item;
+
+       if (array->nr >= array->alloc) {
+               array->alloc = alloc_nr(array->nr + 1);
+               array->items = xrealloc(array->items, array->alloc *
+                       sizeof(struct reflog_info));
+       }
+       item = array->items + array->nr;
+       memcpy(item->osha1, osha1, 20);
+       memcpy(item->nsha1, nsha1, 20);
+       item->email = xstrdup(email);
+       item->timestamp = timestamp;
+       item->tz = tz;
+       item->message = xstrdup(message);
+       array->nr++;
+       return 0;
+}
+
+static struct complete_reflogs *read_complete_reflog(const char *ref)
+{
+       struct complete_reflogs *reflogs =
+               xcalloc(sizeof(struct complete_reflogs), 1);
+       reflogs->ref = xstrdup(ref);
+       for_each_reflog_ent(ref, read_one_reflog, reflogs);
+       if (reflogs->nr == 0) {
+               unsigned char sha1[20];
+               const char *name = resolve_ref(ref, sha1, 1, NULL);
+               if (name)
+                       for_each_reflog_ent(name, read_one_reflog, reflogs);
+       }
+       if (reflogs->nr == 0) {
+               int len = strlen(ref);
+               char *refname = xmalloc(len + 12);
+               sprintf(refname, "refs/%s", ref);
+               for_each_reflog_ent(refname, read_one_reflog, reflogs);
+               if (reflogs->nr == 0) {
+                       sprintf(refname, "refs/heads/%s", ref);
+                       for_each_reflog_ent(refname, read_one_reflog, reflogs);
+               }
+               free(refname);
+       }
+       return reflogs;
+}
+
+static int get_reflog_recno_by_time(struct complete_reflogs *array,
+       unsigned long timestamp)
+{
+       int i;
+       for (i = array->nr - 1; i >= 0; i--)
+               if (timestamp >= array->items[i].timestamp)
+                       return i;
+       return -1;
+}
+
+struct commit_info_lifo {
+       struct commit_info {
+               struct commit *commit;
+               void *util;
+       } *items;
+       int nr, alloc;
+};
+
+static struct commit_info *get_commit_info(struct commit *commit,
+               struct commit_info_lifo *lifo, int pop)
+{
+       int i;
+       for (i = 0; i < lifo->nr; i++)
+               if (lifo->items[i].commit == commit) {
+                       struct commit_info *result = &lifo->items[i];
+                       if (pop) {
+                               if (i + 1 < lifo->nr)
+                                       memmove(lifo->items + i,
+                                               lifo->items + i + 1,
+                                               (lifo->nr - i) *
+                                               sizeof(struct commit_info));
+                               lifo->nr--;
+                       }
+                       return result;
+               }
+       return NULL;
+}
+
+static void add_commit_info(struct commit *commit, void *util,
+               struct commit_info_lifo *lifo)
+{
+       struct commit_info *info;
+       if (lifo->nr >= lifo->alloc) {
+               lifo->alloc = alloc_nr(lifo->nr + 1);
+               lifo->items = xrealloc(lifo->items,
+                       lifo->alloc * sizeof(struct commit_info));
+       }
+       info = lifo->items + lifo->nr;
+       info->commit = commit;
+       info->util = util;
+       lifo->nr++;
+}
+
+struct commit_reflog {
+       int flag, recno;
+       struct complete_reflogs *reflogs;
+};
+
+struct reflog_walk_info {
+       struct commit_info_lifo reflogs;
+       struct path_list complete_reflogs;
+       struct commit_reflog *last_commit_reflog;
+};
+
+void init_reflog_walk(struct reflog_walk_info** info)
+{
+       *info = xcalloc(sizeof(struct reflog_walk_info), 1);
+}
+
+int add_reflog_for_walk(struct reflog_walk_info *info,
+               struct commit *commit, const char *name)
+{
+       unsigned long timestamp = 0;
+       int recno = -1;
+       struct path_list_item *item;
+       struct complete_reflogs *reflogs;
+       char *branch, *at = strchr(name, '@');
+       struct commit_reflog *commit_reflog;
+
+       if (commit->object.flags & UNINTERESTING)
+               die ("Cannot walk reflogs for %s", name);
+
+       branch = xstrdup(name);
+       if (at && at[1] == '{') {
+               char *ep;
+               branch[at - name] = '\0';
+               recno = strtoul(at + 2, &ep, 10);
+               if (*ep != '}') {
+                       recno = -1;
+                       timestamp = approxidate(at + 2);
+               }
+       } else
+               recno = 0;
+
+       item = path_list_lookup(branch, &info->complete_reflogs);
+       if (item)
+               reflogs = item->util;
+       else {
+               if (*branch == '\0') {
+                       unsigned char sha1[20];
+                       const char *head = resolve_ref("HEAD", sha1, 0, NULL);
+                       if (!head)
+                               die ("No current branch");
+                       free(branch);
+                       branch = xstrdup(head);
+               }
+               reflogs = read_complete_reflog(branch);
+               if (!reflogs || reflogs->nr == 0) {
+                       unsigned char sha1[20];
+                       char *b;
+                       if (dwim_log(branch, strlen(branch), sha1, &b) == 1) {
+                               if (reflogs) {
+                                       free(reflogs->ref);
+                                       free(reflogs);
+                               }
+                               free(branch);
+                               branch = b;
+                               reflogs = read_complete_reflog(branch);
+                       }
+               }
+               if (!reflogs || reflogs->nr == 0)
+                       return -1;
+               path_list_insert(branch, &info->complete_reflogs)->util
+                       = reflogs;
+       }
+
+       commit_reflog = xcalloc(sizeof(struct commit_reflog), 1);
+       if (recno < 0) {
+               commit_reflog->flag = 1;
+               commit_reflog->recno = get_reflog_recno_by_time(reflogs, timestamp);
+               if (commit_reflog->recno < 0) {
+                       free(branch);
+                       free(commit_reflog);
+                       return -1;
+               }
+       } else
+               commit_reflog->recno = reflogs->nr - recno - 1;
+       commit_reflog->reflogs = reflogs;
+
+       add_commit_info(commit, commit_reflog, &info->reflogs);
+       return 0;
+}
+
+void fake_reflog_parent(struct reflog_walk_info *info, struct commit *commit)
+{
+       struct commit_info *commit_info =
+               get_commit_info(commit, &info->reflogs, 0);
+       struct commit_reflog *commit_reflog;
+       struct reflog_info *reflog;
+
+       info->last_commit_reflog = NULL;
+       if (!commit_info)
+               return;
+
+       commit_reflog = commit_info->util;
+       if (commit_reflog->recno < 0) {
+               commit->parents = NULL;
+               return;
+       }
+
+       reflog = &commit_reflog->reflogs->items[commit_reflog->recno];
+       info->last_commit_reflog = commit_reflog;
+       commit_reflog->recno--;
+       commit_info->commit = (struct commit *)parse_object(reflog->osha1);
+       if (!commit_info->commit) {
+               commit->parents = NULL;
+               return;
+       }
+
+       commit->parents = xcalloc(sizeof(struct commit_list), 1);
+       commit->parents->item = commit_info->commit;
+       commit->object.flags &= ~(ADDED | SEEN | SHOWN);
+}
+
+void show_reflog_message(struct reflog_walk_info* info, int oneline,
+       int relative_date)
+{
+       if (info && info->last_commit_reflog) {
+               struct commit_reflog *commit_reflog = info->last_commit_reflog;
+               struct reflog_info *info;
+
+               info = &commit_reflog->reflogs->items[commit_reflog->recno+1];
+               if (oneline) {
+                       printf("%s@{", commit_reflog->reflogs->ref);
+                       if (commit_reflog->flag || relative_date)
+                               printf("%s", show_date(info->timestamp, 0, 1));
+                       else
+                               printf("%d", commit_reflog->reflogs->nr
+                                      - 2 - commit_reflog->recno);
+                       printf("}: %s", info->message);
+               }
+               else {
+                       printf("Reflog: %s@{", commit_reflog->reflogs->ref);
+                       if (commit_reflog->flag || relative_date)
+                               printf("%s", show_date(info->timestamp,
+                                                       info->tz,
+                                                       relative_date));
+                       else
+                               printf("%d", commit_reflog->reflogs->nr
+                                      - 2 - commit_reflog->recno);
+                       printf("} (%s)\nReflog message: %s",
+                              info->email, info->message);
+               }
+       }
+}
diff --git a/reflog-walk.h b/reflog-walk.h
new file mode 100644 (file)
index 0000000..7ca1438
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef REFLOG_WALK_H
+#define REFLOG_WALK_H
+
+extern void init_reflog_walk(struct reflog_walk_info** info);
+extern int add_reflog_for_walk(struct reflog_walk_info *info,
+               struct commit *commit, const char *name);
+extern void fake_reflog_parent(struct reflog_walk_info *info,
+               struct commit *commit);
+extern void show_reflog_message(struct reflog_walk_info *info, int, int);
+
+#endif
diff --git a/refs.c b/refs.c
new file mode 100644 (file)
index 0000000..3e6e98c
--- /dev/null
+++ b/refs.c
@@ -0,0 +1,1474 @@
+#include "cache.h"
+#include "refs.h"
+#include "object.h"
+#include "tag.h"
+#include "dir.h"
+
+/* ISSYMREF=01 and ISPACKED=02 are public interfaces */
+#define REF_KNOWS_PEELED 04
+
+struct ref_list {
+       struct ref_list *next;
+       unsigned char flag; /* ISSYMREF? ISPACKED? */
+       unsigned char sha1[20];
+       unsigned char peeled[20];
+       char name[FLEX_ARRAY];
+};
+
+static const char *parse_ref_line(char *line, unsigned char *sha1)
+{
+       /*
+        * 42: the answer to everything.
+        *
+        * In this case, it happens to be the answer to
+        *  40 (length of sha1 hex representation)
+        *  +1 (space in between hex and name)
+        *  +1 (newline at the end of the line)
+        */
+       int len = strlen(line) - 42;
+
+       if (len <= 0)
+               return NULL;
+       if (get_sha1_hex(line, sha1) < 0)
+               return NULL;
+       if (!isspace(line[40]))
+               return NULL;
+       line += 41;
+       if (isspace(*line))
+               return NULL;
+       if (line[len] != '\n')
+               return NULL;
+       line[len] = 0;
+
+       return line;
+}
+
+static struct ref_list *add_ref(const char *name, const unsigned char *sha1,
+                               int flag, struct ref_list *list,
+                               struct ref_list **new_entry)
+{
+       int len;
+       struct ref_list *entry;
+
+       /* Allocate it and add it in.. */
+       len = strlen(name) + 1;
+       entry = xmalloc(sizeof(struct ref_list) + len);
+       hashcpy(entry->sha1, sha1);
+       hashclr(entry->peeled);
+       memcpy(entry->name, name, len);
+       entry->flag = flag;
+       entry->next = list;
+       if (new_entry)
+               *new_entry = entry;
+       return entry;
+}
+
+/* merge sort the ref list */
+static struct ref_list *sort_ref_list(struct ref_list *list)
+{
+       int psize, qsize, last_merge_count, cmp;
+       struct ref_list *p, *q, *l, *e;
+       struct ref_list *new_list = list;
+       int k = 1;
+       int merge_count = 0;
+
+       if (!list)
+               return list;
+
+       do {
+               last_merge_count = merge_count;
+               merge_count = 0;
+
+               psize = 0;
+
+               p = new_list;
+               q = new_list;
+               new_list = NULL;
+               l = NULL;
+
+               while (p) {
+                       merge_count++;
+
+                       while (psize < k && q->next) {
+                               q = q->next;
+                               psize++;
+                       }
+                       qsize = k;
+
+                       while ((psize > 0) || (qsize > 0 && q)) {
+                               if (qsize == 0 || !q) {
+                                       e = p;
+                                       p = p->next;
+                                       psize--;
+                               } else if (psize == 0) {
+                                       e = q;
+                                       q = q->next;
+                                       qsize--;
+                               } else {
+                                       cmp = strcmp(q->name, p->name);
+                                       if (cmp < 0) {
+                                               e = q;
+                                               q = q->next;
+                                               qsize--;
+                                       } else if (cmp > 0) {
+                                               e = p;
+                                               p = p->next;
+                                               psize--;
+                                       } else {
+                                               if (hashcmp(q->sha1, p->sha1))
+                                                       die("Duplicated ref, and SHA1s don't match: %s",
+                                                           q->name);
+                                               warning("Duplicated ref: %s", q->name);
+                                               e = q;
+                                               q = q->next;
+                                               qsize--;
+                                               free(e);
+                                               e = p;
+                                               p = p->next;
+                                               psize--;
+                                       }
+                               }
+
+                               e->next = NULL;
+
+                               if (l)
+                                       l->next = e;
+                               if (!new_list)
+                                       new_list = e;
+                               l = e;
+                       }
+
+                       p = q;
+               };
+
+               k = k * 2;
+       } while ((last_merge_count != merge_count) || (last_merge_count != 1));
+
+       return new_list;
+}
+
+/*
+ * Future: need to be in "struct repository"
+ * when doing a full libification.
+ */
+static struct cached_refs {
+       char did_loose;
+       char did_packed;
+       struct ref_list *loose;
+       struct ref_list *packed;
+} cached_refs;
+
+static void free_ref_list(struct ref_list *list)
+{
+       struct ref_list *next;
+       for ( ; list; list = next) {
+               next = list->next;
+               free(list);
+       }
+}
+
+static void invalidate_cached_refs(void)
+{
+       struct cached_refs *ca = &cached_refs;
+
+       if (ca->did_loose && ca->loose)
+               free_ref_list(ca->loose);
+       if (ca->did_packed && ca->packed)
+               free_ref_list(ca->packed);
+       ca->loose = ca->packed = NULL;
+       ca->did_loose = ca->did_packed = 0;
+}
+
+static void read_packed_refs(FILE *f, struct cached_refs *cached_refs)
+{
+       struct ref_list *list = NULL;
+       struct ref_list *last = NULL;
+       char refline[PATH_MAX];
+       int flag = REF_ISPACKED;
+
+       while (fgets(refline, sizeof(refline), f)) {
+               unsigned char sha1[20];
+               const char *name;
+               static const char header[] = "# pack-refs with:";
+
+               if (!strncmp(refline, header, sizeof(header)-1)) {
+                       const char *traits = refline + sizeof(header) - 1;
+                       if (strstr(traits, " peeled "))
+                               flag |= REF_KNOWS_PEELED;
+                       /* perhaps other traits later as well */
+                       continue;
+               }
+
+               name = parse_ref_line(refline, sha1);
+               if (name) {
+                       list = add_ref(name, sha1, flag, list, &last);
+                       continue;
+               }
+               if (last &&
+                   refline[0] == '^' &&
+                   strlen(refline) == 42 &&
+                   refline[41] == '\n' &&
+                   !get_sha1_hex(refline + 1, sha1))
+                       hashcpy(last->peeled, sha1);
+       }
+       cached_refs->packed = sort_ref_list(list);
+}
+
+static struct ref_list *get_packed_refs(void)
+{
+       if (!cached_refs.did_packed) {
+               FILE *f = fopen(git_path("packed-refs"), "r");
+               cached_refs.packed = NULL;
+               if (f) {
+                       read_packed_refs(f, &cached_refs);
+                       fclose(f);
+               }
+               cached_refs.did_packed = 1;
+       }
+       return cached_refs.packed;
+}
+
+static struct ref_list *get_ref_dir(const char *base, struct ref_list *list)
+{
+       DIR *dir = opendir(git_path("%s", base));
+
+       if (dir) {
+               struct dirent *de;
+               int baselen = strlen(base);
+               char *ref = xmalloc(baselen + 257);
+
+               memcpy(ref, base, baselen);
+               if (baselen && base[baselen-1] != '/')
+                       ref[baselen++] = '/';
+
+               while ((de = readdir(dir)) != NULL) {
+                       unsigned char sha1[20];
+                       struct stat st;
+                       int flag;
+                       int namelen;
+
+                       if (de->d_name[0] == '.')
+                               continue;
+                       namelen = strlen(de->d_name);
+                       if (namelen > 255)
+                               continue;
+                       if (has_extension(de->d_name, ".lock"))
+                               continue;
+                       memcpy(ref + baselen, de->d_name, namelen+1);
+                       if (stat(git_path("%s", ref), &st) < 0)
+                               continue;
+                       if (S_ISDIR(st.st_mode)) {
+                               list = get_ref_dir(ref, list);
+                               continue;
+                       }
+                       if (!resolve_ref(ref, sha1, 1, &flag)) {
+                               error("%s points nowhere!", ref);
+                               continue;
+                       }
+                       list = add_ref(ref, sha1, flag, list, NULL);
+               }
+               free(ref);
+               closedir(dir);
+       }
+       return sort_ref_list(list);
+}
+
+static struct ref_list *get_loose_refs(void)
+{
+       if (!cached_refs.did_loose) {
+               cached_refs.loose = get_ref_dir("refs", NULL);
+               cached_refs.did_loose = 1;
+       }
+       return cached_refs.loose;
+}
+
+/* We allow "recursive" symbolic refs. Only within reason, though */
+#define MAXDEPTH 5
+#define MAXREFLEN (1024)
+
+static int resolve_gitlink_packed_ref(char *name, int pathlen, const char *refname, unsigned char *result)
+{
+       FILE *f;
+       struct cached_refs refs;
+       struct ref_list *ref;
+       int retval;
+
+       strcpy(name + pathlen, "packed-refs");
+       f = fopen(name, "r");
+       if (!f)
+               return -1;
+       read_packed_refs(f, &refs);
+       fclose(f);
+       ref = refs.packed;
+       retval = -1;
+       while (ref) {
+               if (!strcmp(ref->name, refname)) {
+                       retval = 0;
+                       memcpy(result, ref->sha1, 20);
+                       break;
+               }
+               ref = ref->next;
+       }
+       free_ref_list(refs.packed);
+       return retval;
+}
+
+static int resolve_gitlink_ref_recursive(char *name, int pathlen, const char *refname, unsigned char *result, int recursion)
+{
+       int fd, len = strlen(refname);
+       char buffer[128], *p;
+
+       if (recursion > MAXDEPTH || len > MAXREFLEN)
+               return -1;
+       memcpy(name + pathlen, refname, len+1);
+       fd = open(name, O_RDONLY);
+       if (fd < 0)
+               return resolve_gitlink_packed_ref(name, pathlen, refname, result);
+
+       len = read(fd, buffer, sizeof(buffer)-1);
+       close(fd);
+       if (len < 0)
+               return -1;
+       while (len && isspace(buffer[len-1]))
+               len--;
+       buffer[len] = 0;
+
+       /* Was it a detached head or an old-fashioned symlink? */
+       if (!get_sha1_hex(buffer, result))
+               return 0;
+
+       /* Symref? */
+       if (strncmp(buffer, "ref:", 4))
+               return -1;
+       p = buffer + 4;
+       while (isspace(*p))
+               p++;
+
+       return resolve_gitlink_ref_recursive(name, pathlen, p, result, recursion+1);
+}
+
+int resolve_gitlink_ref(const char *path, const char *refname, unsigned char *result)
+{
+       int len = strlen(path), retval;
+       char *gitdir;
+
+       while (len && path[len-1] == '/')
+               len--;
+       if (!len)
+               return -1;
+       gitdir = xmalloc(len + MAXREFLEN + 8);
+       memcpy(gitdir, path, len);
+       memcpy(gitdir + len, "/.git/", 7);
+
+       retval = resolve_gitlink_ref_recursive(gitdir, len+6, refname, result, 0);
+       free(gitdir);
+       return retval;
+}
+
+const char *resolve_ref(const char *ref, unsigned char *sha1, int reading, int *flag)
+{
+       int depth = MAXDEPTH, len;
+       char buffer[256];
+       static char ref_buffer[256];
+
+       if (flag)
+               *flag = 0;
+
+       for (;;) {
+               const char *path = git_path("%s", ref);
+               struct stat st;
+               char *buf;
+               int fd;
+
+               if (--depth < 0)
+                       return NULL;
+
+               /* Special case: non-existing file.
+                * Not having the refs/heads/new-branch is OK
+                * if we are writing into it, so is .git/HEAD
+                * that points at refs/heads/master still to be
+                * born.  It is NOT OK if we are resolving for
+                * reading.
+                */
+               if (lstat(path, &st) < 0) {
+                       struct ref_list *list = get_packed_refs();
+                       while (list) {
+                               if (!strcmp(ref, list->name)) {
+                                       hashcpy(sha1, list->sha1);
+                                       if (flag)
+                                               *flag |= REF_ISPACKED;
+                                       return ref;
+                               }
+                               list = list->next;
+                       }
+                       if (reading || errno != ENOENT)
+                               return NULL;
+                       hashclr(sha1);
+                       return ref;
+               }
+
+               /* Follow "normalized" - ie "refs/.." symlinks by hand */
+               if (S_ISLNK(st.st_mode)) {
+                       len = readlink(path, buffer, sizeof(buffer)-1);
+                       if (len >= 5 && !memcmp("refs/", buffer, 5)) {
+                               buffer[len] = 0;
+                               strcpy(ref_buffer, buffer);
+                               ref = ref_buffer;
+                               if (flag)
+                                       *flag |= REF_ISSYMREF;
+                               continue;
+                       }
+               }
+
+               /* Is it a directory? */
+               if (S_ISDIR(st.st_mode)) {
+                       errno = EISDIR;
+                       return NULL;
+               }
+
+               /*
+                * Anything else, just open it and try to use it as
+                * a ref
+                */
+               fd = open(path, O_RDONLY);
+               if (fd < 0)
+                       return NULL;
+               len = read_in_full(fd, buffer, sizeof(buffer)-1);
+               close(fd);
+
+               /*
+                * Is it a symbolic ref?
+                */
+               if (len < 4 || memcmp("ref:", buffer, 4))
+                       break;
+               buf = buffer + 4;
+               len -= 4;
+               while (len && isspace(*buf))
+                       buf++, len--;
+               while (len && isspace(buf[len-1]))
+                       len--;
+               buf[len] = 0;
+               memcpy(ref_buffer, buf, len + 1);
+               ref = ref_buffer;
+               if (flag)
+                       *flag |= REF_ISSYMREF;
+       }
+       if (len < 40 || get_sha1_hex(buffer, sha1))
+               return NULL;
+       return ref;
+}
+
+int read_ref(const char *ref, unsigned char *sha1)
+{
+       if (resolve_ref(ref, sha1, 1, NULL))
+               return 0;
+       return -1;
+}
+
+static int do_one_ref(const char *base, each_ref_fn fn, int trim,
+                     void *cb_data, struct ref_list *entry)
+{
+       if (strncmp(base, entry->name, trim))
+               return 0;
+       if (is_null_sha1(entry->sha1))
+               return 0;
+       if (!has_sha1_file(entry->sha1)) {
+               error("%s does not point to a valid object!", entry->name);
+               return 0;
+       }
+       return fn(entry->name + trim, entry->sha1, entry->flag, cb_data);
+}
+
+int peel_ref(const char *ref, unsigned char *sha1)
+{
+       int flag;
+       unsigned char base[20];
+       struct object *o;
+
+       if (!resolve_ref(ref, base, 1, &flag))
+               return -1;
+
+       if ((flag & REF_ISPACKED)) {
+               struct ref_list *list = get_packed_refs();
+
+               while (list) {
+                       if (!strcmp(list->name, ref)) {
+                               if (list->flag & REF_KNOWS_PEELED) {
+                                       hashcpy(sha1, list->peeled);
+                                       return 0;
+                               }
+                               /* older pack-refs did not leave peeled ones */
+                               break;
+                       }
+                       list = list->next;
+               }
+       }
+
+       /* fallback - callers should not call this for unpacked refs */
+       o = parse_object(base);
+       if (o->type == OBJ_TAG) {
+               o = deref_tag(o, ref, 0);
+               if (o) {
+                       hashcpy(sha1, o->sha1);
+                       return 0;
+               }
+       }
+       return -1;
+}
+
+static int do_for_each_ref(const char *base, each_ref_fn fn, int trim,
+                          void *cb_data)
+{
+       int retval;
+       struct ref_list *packed = get_packed_refs();
+       struct ref_list *loose = get_loose_refs();
+
+       while (packed && loose) {
+               struct ref_list *entry;
+               int cmp = strcmp(packed->name, loose->name);
+               if (!cmp) {
+                       packed = packed->next;
+                       continue;
+               }
+               if (cmp > 0) {
+                       entry = loose;
+                       loose = loose->next;
+               } else {
+                       entry = packed;
+                       packed = packed->next;
+               }
+               retval = do_one_ref(base, fn, trim, cb_data, entry);
+               if (retval)
+                       return retval;
+       }
+
+       for (packed = packed ? packed : loose; packed; packed = packed->next) {
+               retval = do_one_ref(base, fn, trim, cb_data, packed);
+               if (retval)
+                       return retval;
+       }
+       return 0;
+}
+
+int head_ref(each_ref_fn fn, void *cb_data)
+{
+       unsigned char sha1[20];
+       int flag;
+
+       if (resolve_ref("HEAD", sha1, 1, &flag))
+               return fn("HEAD", sha1, flag, cb_data);
+       return 0;
+}
+
+int for_each_ref(each_ref_fn fn, void *cb_data)
+{
+       return do_for_each_ref("refs/", fn, 0, cb_data);
+}
+
+int for_each_tag_ref(each_ref_fn fn, void *cb_data)
+{
+       return do_for_each_ref("refs/tags/", fn, 10, cb_data);
+}
+
+int for_each_branch_ref(each_ref_fn fn, void *cb_data)
+{
+       return do_for_each_ref("refs/heads/", fn, 11, cb_data);
+}
+
+int for_each_remote_ref(each_ref_fn fn, void *cb_data)
+{
+       return do_for_each_ref("refs/remotes/", fn, 13, cb_data);
+}
+
+/*
+ * Make sure "ref" is something reasonable to have under ".git/refs/";
+ * We do not like it if:
+ *
+ * - any path component of it begins with ".", or
+ * - it has double dots "..", or
+ * - it has ASCII control character, "~", "^", ":" or SP, anywhere, or
+ * - it ends with a "/".
+ */
+
+static inline int bad_ref_char(int ch)
+{
+       if (((unsigned) ch) <= ' ' ||
+           ch == '~' || ch == '^' || ch == ':')
+               return 1;
+       /* 2.13 Pattern Matching Notation */
+       if (ch == '?' || ch == '[') /* Unsupported */
+               return 1;
+       if (ch == '*') /* Supported at the end */
+               return 2;
+       return 0;
+}
+
+int check_ref_format(const char *ref)
+{
+       int ch, level, bad_type;
+       const char *cp = ref;
+
+       level = 0;
+       while (1) {
+               while ((ch = *cp++) == '/')
+                       ; /* tolerate duplicated slashes */
+               if (!ch)
+                       return -1; /* should not end with slashes */
+
+               /* we are at the beginning of the path component */
+               if (ch == '.')
+                       return -1;
+               bad_type = bad_ref_char(ch);
+               if (bad_type) {
+                       return (bad_type == 2 && !*cp) ? -3 : -1;
+               }
+
+               /* scan the rest of the path component */
+               while ((ch = *cp++) != 0) {
+                       bad_type = bad_ref_char(ch);
+                       if (bad_type) {
+                               return (bad_type == 2 && !*cp) ? -3 : -1;
+                       }
+                       if (ch == '/')
+                               break;
+                       if (ch == '.' && *cp == '.')
+                               return -1;
+               }
+               level++;
+               if (!ch) {
+                       if (level < 2)
+                               return -2; /* at least of form "heads/blah" */
+                       return 0;
+               }
+       }
+}
+
+const char *ref_rev_parse_rules[] = {
+       "%.*s",
+       "refs/%.*s",
+       "refs/tags/%.*s",
+       "refs/heads/%.*s",
+       "refs/remotes/%.*s",
+       "refs/remotes/%.*s/HEAD",
+       NULL
+};
+
+const char *ref_fetch_rules[] = {
+       "%.*s",
+       "refs/%.*s",
+       "refs/heads/%.*s",
+       NULL
+};
+
+int refname_match(const char *abbrev_name, const char *full_name, const char **rules)
+{
+       const char **p;
+       const int abbrev_name_len = strlen(abbrev_name);
+
+       for (p = rules; *p; p++) {
+               if (!strcmp(full_name, mkpath(*p, abbrev_name_len, abbrev_name))) {
+                       return 1;
+               }
+       }
+
+       return 0;
+}
+
+static struct ref_lock *verify_lock(struct ref_lock *lock,
+       const unsigned char *old_sha1, int mustexist)
+{
+       if (!resolve_ref(lock->ref_name, lock->old_sha1, mustexist, NULL)) {
+               error("Can't verify ref %s", lock->ref_name);
+               unlock_ref(lock);
+               return NULL;
+       }
+       if (hashcmp(lock->old_sha1, old_sha1)) {
+               error("Ref %s is at %s but expected %s", lock->ref_name,
+                       sha1_to_hex(lock->old_sha1), sha1_to_hex(old_sha1));
+               unlock_ref(lock);
+               return NULL;
+       }
+       return lock;
+}
+
+static int remove_empty_directories(const char *file)
+{
+       /* we want to create a file but there is a directory there;
+        * if that is an empty directory (or a directory that contains
+        * only empty directories), remove them.
+        */
+       struct strbuf path;
+       int result;
+
+       strbuf_init(&path, 20);
+       strbuf_addstr(&path, file);
+
+       result = remove_dir_recursively(&path, 1);
+
+       strbuf_release(&path);
+
+       return result;
+}
+
+static int is_refname_available(const char *ref, const char *oldref,
+                               struct ref_list *list, int quiet)
+{
+       int namlen = strlen(ref); /* e.g. 'foo/bar' */
+       while (list) {
+               /* list->name could be 'foo' or 'foo/bar/baz' */
+               if (!oldref || strcmp(oldref, list->name)) {
+                       int len = strlen(list->name);
+                       int cmplen = (namlen < len) ? namlen : len;
+                       const char *lead = (namlen < len) ? list->name : ref;
+                       if (!strncmp(ref, list->name, cmplen) &&
+                           lead[cmplen] == '/') {
+                               if (!quiet)
+                                       error("'%s' exists; cannot create '%s'",
+                                             list->name, ref);
+                               return 0;
+                       }
+               }
+               list = list->next;
+       }
+       return 1;
+}
+
+static struct ref_lock *lock_ref_sha1_basic(const char *ref, const unsigned char *old_sha1, int flags, int *type_p)
+{
+       char *ref_file;
+       const char *orig_ref = ref;
+       struct ref_lock *lock;
+       struct stat st;
+       int last_errno = 0;
+       int type;
+       int mustexist = (old_sha1 && !is_null_sha1(old_sha1));
+
+       lock = xcalloc(1, sizeof(struct ref_lock));
+       lock->lock_fd = -1;
+
+       ref = resolve_ref(ref, lock->old_sha1, mustexist, &type);
+       if (!ref && errno == EISDIR) {
+               /* we are trying to lock foo but we used to
+                * have foo/bar which now does not exist;
+                * it is normal for the empty directory 'foo'
+                * to remain.
+                */
+               ref_file = git_path("%s", orig_ref);
+               if (remove_empty_directories(ref_file)) {
+                       last_errno = errno;
+                       error("there are still refs under '%s'", orig_ref);
+                       goto error_return;
+               }
+               ref = resolve_ref(orig_ref, lock->old_sha1, mustexist, &type);
+       }
+       if (type_p)
+           *type_p = type;
+       if (!ref) {
+               last_errno = errno;
+               error("unable to resolve reference %s: %s",
+                       orig_ref, strerror(errno));
+               goto error_return;
+       }
+       /* When the ref did not exist and we are creating it,
+        * make sure there is no existing ref that is packed
+        * whose name begins with our refname, nor a ref whose
+        * name is a proper prefix of our refname.
+        */
+       if (is_null_sha1(lock->old_sha1) &&
+            !is_refname_available(ref, NULL, get_packed_refs(), 0))
+               goto error_return;
+
+       lock->lk = xcalloc(1, sizeof(struct lock_file));
+
+       if (flags & REF_NODEREF)
+               ref = orig_ref;
+       lock->ref_name = xstrdup(ref);
+       lock->orig_ref_name = xstrdup(orig_ref);
+       ref_file = git_path("%s", ref);
+       if (lstat(ref_file, &st) && errno == ENOENT)
+               lock->force_write = 1;
+       if ((flags & REF_NODEREF) && (type & REF_ISSYMREF))
+               lock->force_write = 1;
+
+       if (safe_create_leading_directories(ref_file)) {
+               last_errno = errno;
+               error("unable to create directory for %s", ref_file);
+               goto error_return;
+       }
+       lock->lock_fd = hold_lock_file_for_update(lock->lk, ref_file, 1);
+
+       return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock;
+
+ error_return:
+       unlock_ref(lock);
+       errno = last_errno;
+       return NULL;
+}
+
+struct ref_lock *lock_ref_sha1(const char *ref, const unsigned char *old_sha1)
+{
+       char refpath[PATH_MAX];
+       if (check_ref_format(ref))
+               return NULL;
+       strcpy(refpath, mkpath("refs/%s", ref));
+       return lock_ref_sha1_basic(refpath, old_sha1, 0, NULL);
+}
+
+struct ref_lock *lock_any_ref_for_update(const char *ref, const unsigned char *old_sha1, int flags)
+{
+       if (check_ref_format(ref) == -1)
+               return NULL;
+       return lock_ref_sha1_basic(ref, old_sha1, flags, NULL);
+}
+
+static struct lock_file packlock;
+
+static int repack_without_ref(const char *refname)
+{
+       struct ref_list *list, *packed_ref_list;
+       int fd;
+       int found = 0;
+
+       packed_ref_list = get_packed_refs();
+       for (list = packed_ref_list; list; list = list->next) {
+               if (!strcmp(refname, list->name)) {
+                       found = 1;
+                       break;
+               }
+       }
+       if (!found)
+               return 0;
+       fd = hold_lock_file_for_update(&packlock, git_path("packed-refs"), 0);
+       if (fd < 0)
+               return error("cannot delete '%s' from packed refs", refname);
+
+       for (list = packed_ref_list; list; list = list->next) {
+               char line[PATH_MAX + 100];
+               int len;
+
+               if (!strcmp(refname, list->name))
+                       continue;
+               len = snprintf(line, sizeof(line), "%s %s\n",
+                              sha1_to_hex(list->sha1), list->name);
+               /* this should not happen but just being defensive */
+               if (len > sizeof(line))
+                       die("too long a refname '%s'", list->name);
+               write_or_die(fd, line, len);
+       }
+       close(fd);
+       return commit_lock_file(&packlock);
+}
+
+int delete_ref(const char *refname, const unsigned char *sha1)
+{
+       struct ref_lock *lock;
+       int err, i, ret = 0, flag = 0;
+
+       lock = lock_ref_sha1_basic(refname, sha1, 0, &flag);
+       if (!lock)
+               return 1;
+       if (!(flag & REF_ISPACKED)) {
+               /* loose */
+               i = strlen(lock->lk->filename) - 5; /* .lock */
+               lock->lk->filename[i] = 0;
+               err = unlink(lock->lk->filename);
+               if (err) {
+                       ret = 1;
+                       error("unlink(%s) failed: %s",
+                             lock->lk->filename, strerror(errno));
+               }
+               lock->lk->filename[i] = '.';
+       }
+       /* removing the loose one could have resurrected an earlier
+        * packed one.  Also, if it was not loose we need to repack
+        * without it.
+        */
+       ret |= repack_without_ref(refname);
+
+       err = unlink(git_path("logs/%s", lock->ref_name));
+       if (err && errno != ENOENT)
+               fprintf(stderr, "warning: unlink(%s) failed: %s",
+                       git_path("logs/%s", lock->ref_name), strerror(errno));
+       invalidate_cached_refs();
+       unlock_ref(lock);
+       return ret;
+}
+
+int rename_ref(const char *oldref, const char *newref, const char *logmsg)
+{
+       static const char renamed_ref[] = "RENAMED-REF";
+       unsigned char sha1[20], orig_sha1[20];
+       int flag = 0, logmoved = 0;
+       struct ref_lock *lock;
+       struct stat loginfo;
+       int log = !lstat(git_path("logs/%s", oldref), &loginfo);
+
+       if (S_ISLNK(loginfo.st_mode))
+               return error("reflog for %s is a symlink", oldref);
+
+       if (!resolve_ref(oldref, orig_sha1, 1, &flag))
+               return error("refname %s not found", oldref);
+
+       if (!is_refname_available(newref, oldref, get_packed_refs(), 0))
+               return 1;
+
+       if (!is_refname_available(newref, oldref, get_loose_refs(), 0))
+               return 1;
+
+       lock = lock_ref_sha1_basic(renamed_ref, NULL, 0, NULL);
+       if (!lock)
+               return error("unable to lock %s", renamed_ref);
+       lock->force_write = 1;
+       if (write_ref_sha1(lock, orig_sha1, logmsg))
+               return error("unable to save current sha1 in %s", renamed_ref);
+
+       if (log && rename(git_path("logs/%s", oldref), git_path("tmp-renamed-log")))
+               return error("unable to move logfile logs/%s to tmp-renamed-log: %s",
+                       oldref, strerror(errno));
+
+       if (delete_ref(oldref, orig_sha1)) {
+               error("unable to delete old %s", oldref);
+               goto rollback;
+       }
+
+       if (resolve_ref(newref, sha1, 1, &flag) && delete_ref(newref, sha1)) {
+               if (errno==EISDIR) {
+                       if (remove_empty_directories(git_path("%s", newref))) {
+                               error("Directory not empty: %s", newref);
+                               goto rollback;
+                       }
+               } else {
+                       error("unable to delete existing %s", newref);
+                       goto rollback;
+               }
+       }
+
+       if (log && safe_create_leading_directories(git_path("logs/%s", newref))) {
+               error("unable to create directory for %s", newref);
+               goto rollback;
+       }
+
+ retry:
+       if (log && rename(git_path("tmp-renamed-log"), git_path("logs/%s", newref))) {
+               if (errno==EISDIR || errno==ENOTDIR) {
+                       /*
+                        * rename(a, b) when b is an existing
+                        * directory ought to result in ISDIR, but
+                        * Solaris 5.8 gives ENOTDIR.  Sheesh.
+                        */
+                       if (remove_empty_directories(git_path("logs/%s", newref))) {
+                               error("Directory not empty: logs/%s", newref);
+                               goto rollback;
+                       }
+                       goto retry;
+               } else {
+                       error("unable to move logfile tmp-renamed-log to logs/%s: %s",
+                               newref, strerror(errno));
+                       goto rollback;
+               }
+       }
+       logmoved = log;
+
+       lock = lock_ref_sha1_basic(newref, NULL, 0, NULL);
+       if (!lock) {
+               error("unable to lock %s for update", newref);
+               goto rollback;
+       }
+
+       lock->force_write = 1;
+       hashcpy(lock->old_sha1, orig_sha1);
+       if (write_ref_sha1(lock, orig_sha1, logmsg)) {
+               error("unable to write current sha1 into %s", newref);
+               goto rollback;
+       }
+
+       return 0;
+
+ rollback:
+       lock = lock_ref_sha1_basic(oldref, NULL, 0, NULL);
+       if (!lock) {
+               error("unable to lock %s for rollback", oldref);
+               goto rollbacklog;
+       }
+
+       lock->force_write = 1;
+       flag = log_all_ref_updates;
+       log_all_ref_updates = 0;
+       if (write_ref_sha1(lock, orig_sha1, NULL))
+               error("unable to write current sha1 into %s", oldref);
+       log_all_ref_updates = flag;
+
+ rollbacklog:
+       if (logmoved && rename(git_path("logs/%s", newref), git_path("logs/%s", oldref)))
+               error("unable to restore logfile %s from %s: %s",
+                       oldref, newref, strerror(errno));
+       if (!logmoved && log &&
+           rename(git_path("tmp-renamed-log"), git_path("logs/%s", oldref)))
+               error("unable to restore logfile %s from tmp-renamed-log: %s",
+                       oldref, strerror(errno));
+
+       return 1;
+}
+
+void unlock_ref(struct ref_lock *lock)
+{
+       if (lock->lock_fd >= 0) {
+               close(lock->lock_fd);
+               /* Do not free lock->lk -- atexit() still looks at them */
+               if (lock->lk)
+                       rollback_lock_file(lock->lk);
+       }
+       free(lock->ref_name);
+       free(lock->orig_ref_name);
+       free(lock);
+}
+
+/*
+ * copy the reflog message msg to buf, which has been allocated sufficiently
+ * large, while cleaning up the whitespaces.  Especially, convert LF to space,
+ * because reflog file is one line per entry.
+ */
+static int copy_msg(char *buf, const char *msg)
+{
+       char *cp = buf;
+       char c;
+       int wasspace = 1;
+
+       *cp++ = '\t';
+       while ((c = *msg++)) {
+               if (wasspace && isspace(c))
+                       continue;
+               wasspace = isspace(c);
+               if (wasspace)
+                       c = ' ';
+               *cp++ = c;
+       }
+       while (buf < cp && isspace(cp[-1]))
+               cp--;
+       *cp++ = '\n';
+       return cp - buf;
+}
+
+static int log_ref_write(const char *ref_name, const unsigned char *old_sha1,
+                        const unsigned char *new_sha1, const char *msg)
+{
+       int logfd, written, oflags = O_APPEND | O_WRONLY;
+       unsigned maxlen, len;
+       int msglen;
+       char *log_file, *logrec;
+       const char *committer;
+
+       if (log_all_ref_updates < 0)
+               log_all_ref_updates = !is_bare_repository();
+
+       log_file = git_path("logs/%s", ref_name);
+
+       if (log_all_ref_updates &&
+           (!prefixcmp(ref_name, "refs/heads/") ||
+            !prefixcmp(ref_name, "refs/remotes/") ||
+            !strcmp(ref_name, "HEAD"))) {
+               if (safe_create_leading_directories(log_file) < 0)
+                       return error("unable to create directory for %s",
+                                    log_file);
+               oflags |= O_CREAT;
+       }
+
+       logfd = open(log_file, oflags, 0666);
+       if (logfd < 0) {
+               if (!(oflags & O_CREAT) && errno == ENOENT)
+                       return 0;
+
+               if ((oflags & O_CREAT) && errno == EISDIR) {
+                       if (remove_empty_directories(log_file)) {
+                               return error("There are still logs under '%s'",
+                                            log_file);
+                       }
+                       logfd = open(log_file, oflags, 0666);
+               }
+
+               if (logfd < 0)
+                       return error("Unable to append to %s: %s",
+                                    log_file, strerror(errno));
+       }
+
+       adjust_shared_perm(log_file);
+
+       msglen = msg ? strlen(msg) : 0;
+       committer = git_committer_info(-1);
+       maxlen = strlen(committer) + msglen + 100;
+       logrec = xmalloc(maxlen);
+       len = sprintf(logrec, "%s %s %s\n",
+                     sha1_to_hex(old_sha1),
+                     sha1_to_hex(new_sha1),
+                     committer);
+       if (msglen)
+               len += copy_msg(logrec + len - 1, msg) - 1;
+       written = len <= maxlen ? write_in_full(logfd, logrec, len) : -1;
+       free(logrec);
+       if (close(logfd) != 0 || written != len)
+               return error("Unable to append to %s", log_file);
+       return 0;
+}
+
+int write_ref_sha1(struct ref_lock *lock,
+       const unsigned char *sha1, const char *logmsg)
+{
+       static char term = '\n';
+
+       if (!lock)
+               return -1;
+       if (!lock->force_write && !hashcmp(lock->old_sha1, sha1)) {
+               unlock_ref(lock);
+               return 0;
+       }
+       if (write_in_full(lock->lock_fd, sha1_to_hex(sha1), 40) != 40 ||
+           write_in_full(lock->lock_fd, &term, 1) != 1
+               || close(lock->lock_fd) < 0) {
+               error("Couldn't write %s", lock->lk->filename);
+               unlock_ref(lock);
+               return -1;
+       }
+       invalidate_cached_refs();
+       if (log_ref_write(lock->ref_name, lock->old_sha1, sha1, logmsg) < 0 ||
+           (strcmp(lock->ref_name, lock->orig_ref_name) &&
+            log_ref_write(lock->orig_ref_name, lock->old_sha1, sha1, logmsg) < 0)) {
+               unlock_ref(lock);
+               return -1;
+       }
+       if (strcmp(lock->orig_ref_name, "HEAD") != 0) {
+               /*
+                * Special hack: If a branch is updated directly and HEAD
+                * points to it (may happen on the remote side of a push
+                * for example) then logically the HEAD reflog should be
+                * updated too.
+                * A generic solution implies reverse symref information,
+                * but finding all symrefs pointing to the given branch
+                * would be rather costly for this rare event (the direct
+                * update of a branch) to be worth it.  So let's cheat and
+                * check with HEAD only which should cover 99% of all usage
+                * scenarios (even 100% of the default ones).
+                */
+               unsigned char head_sha1[20];
+               int head_flag;
+               const char *head_ref;
+               head_ref = resolve_ref("HEAD", head_sha1, 1, &head_flag);
+               if (head_ref && (head_flag & REF_ISSYMREF) &&
+                   !strcmp(head_ref, lock->ref_name))
+                       log_ref_write("HEAD", lock->old_sha1, sha1, logmsg);
+       }
+       if (commit_lock_file(lock->lk)) {
+               error("Couldn't set %s", lock->ref_name);
+               unlock_ref(lock);
+               return -1;
+       }
+       lock->lock_fd = -1;
+       unlock_ref(lock);
+       return 0;
+}
+
+int create_symref(const char *ref_target, const char *refs_heads_master,
+                 const char *logmsg)
+{
+       const char *lockpath;
+       char ref[1000];
+       int fd, len, written;
+       char *git_HEAD = xstrdup(git_path("%s", ref_target));
+       unsigned char old_sha1[20], new_sha1[20];
+
+       if (logmsg && read_ref(ref_target, old_sha1))
+               hashclr(old_sha1);
+
+       if (safe_create_leading_directories(git_HEAD) < 0)
+               return error("unable to create directory for %s", git_HEAD);
+
+#ifndef NO_SYMLINK_HEAD
+       if (prefer_symlink_refs) {
+               unlink(git_HEAD);
+               if (!symlink(refs_heads_master, git_HEAD))
+                       goto done;
+               fprintf(stderr, "no symlink - falling back to symbolic ref\n");
+       }
+#endif
+
+       len = snprintf(ref, sizeof(ref), "ref: %s\n", refs_heads_master);
+       if (sizeof(ref) <= len) {
+               error("refname too long: %s", refs_heads_master);
+               goto error_free_return;
+       }
+       lockpath = mkpath("%s.lock", git_HEAD);
+       fd = open(lockpath, O_CREAT | O_EXCL | O_WRONLY, 0666);
+       if (fd < 0) {
+               error("Unable to open %s for writing", lockpath);
+               goto error_free_return;
+       }
+       written = write_in_full(fd, ref, len);
+       if (close(fd) != 0 || written != len) {
+               error("Unable to write to %s", lockpath);
+               goto error_unlink_return;
+       }
+       if (rename(lockpath, git_HEAD) < 0) {
+               error("Unable to create %s", git_HEAD);
+               goto error_unlink_return;
+       }
+       if (adjust_shared_perm(git_HEAD)) {
+               error("Unable to fix permissions on %s", lockpath);
+       error_unlink_return:
+               unlink(lockpath);
+       error_free_return:
+               free(git_HEAD);
+               return -1;
+       }
+
+#ifndef NO_SYMLINK_HEAD
+       done:
+#endif
+       if (logmsg && !read_ref(refs_heads_master, new_sha1))
+               log_ref_write(ref_target, old_sha1, new_sha1, logmsg);
+
+       free(git_HEAD);
+       return 0;
+}
+
+static char *ref_msg(const char *line, const char *endp)
+{
+       const char *ep;
+       line += 82;
+       ep = memchr(line, '\n', endp - line);
+       if (!ep)
+               ep = endp;
+       return xmemdupz(line, ep - line);
+}
+
+int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *sha1, char **msg, unsigned long *cutoff_time, int *cutoff_tz, int *cutoff_cnt)
+{
+       const char *logfile, *logdata, *logend, *rec, *lastgt, *lastrec;
+       char *tz_c;
+       int logfd, tz, reccnt = 0;
+       struct stat st;
+       unsigned long date;
+       unsigned char logged_sha1[20];
+       void *log_mapped;
+       size_t mapsz;
+
+       logfile = git_path("logs/%s", ref);
+       logfd = open(logfile, O_RDONLY, 0);
+       if (logfd < 0)
+               die("Unable to read log %s: %s", logfile, strerror(errno));
+       fstat(logfd, &st);
+       if (!st.st_size)
+               die("Log %s is empty.", logfile);
+       mapsz = xsize_t(st.st_size);
+       log_mapped = xmmap(NULL, mapsz, PROT_READ, MAP_PRIVATE, logfd, 0);
+       logdata = log_mapped;
+       close(logfd);
+
+       lastrec = NULL;
+       rec = logend = logdata + st.st_size;
+       while (logdata < rec) {
+               reccnt++;
+               if (logdata < rec && *(rec-1) == '\n')
+                       rec--;
+               lastgt = NULL;
+               while (logdata < rec && *(rec-1) != '\n') {
+                       rec--;
+                       if (*rec == '>')
+                               lastgt = rec;
+               }
+               if (!lastgt)
+                       die("Log %s is corrupt.", logfile);
+               date = strtoul(lastgt + 1, &tz_c, 10);
+               if (date <= at_time || cnt == 0) {
+                       tz = strtoul(tz_c, NULL, 10);
+                       if (msg)
+                               *msg = ref_msg(rec, logend);
+                       if (cutoff_time)
+                               *cutoff_time = date;
+                       if (cutoff_tz)
+                               *cutoff_tz = tz;
+                       if (cutoff_cnt)
+                               *cutoff_cnt = reccnt - 1;
+                       if (lastrec) {
+                               if (get_sha1_hex(lastrec, logged_sha1))
+                                       die("Log %s is corrupt.", logfile);
+                               if (get_sha1_hex(rec + 41, sha1))
+                                       die("Log %s is corrupt.", logfile);
+                               if (hashcmp(logged_sha1, sha1)) {
+                                       fprintf(stderr,
+                                               "warning: Log %s has gap after %s.\n",
+                                               logfile, show_date(date, tz, DATE_RFC2822));
+                               }
+                       }
+                       else if (date == at_time) {
+                               if (get_sha1_hex(rec + 41, sha1))
+                                       die("Log %s is corrupt.", logfile);
+                       }
+                       else {
+                               if (get_sha1_hex(rec + 41, logged_sha1))
+                                       die("Log %s is corrupt.", logfile);
+                               if (hashcmp(logged_sha1, sha1)) {
+                                       fprintf(stderr,
+                                               "warning: Log %s unexpectedly ended on %s.\n",
+                                               logfile, show_date(date, tz, DATE_RFC2822));
+                               }
+                       }
+                       munmap(log_mapped, mapsz);
+                       return 0;
+               }
+               lastrec = rec;
+               if (cnt > 0)
+                       cnt--;
+       }
+
+       rec = logdata;
+       while (rec < logend && *rec != '>' && *rec != '\n')
+               rec++;
+       if (rec == logend || *rec == '\n')
+               die("Log %s is corrupt.", logfile);
+       date = strtoul(rec + 1, &tz_c, 10);
+       tz = strtoul(tz_c, NULL, 10);
+       if (get_sha1_hex(logdata, sha1))
+               die("Log %s is corrupt.", logfile);
+       if (msg)
+               *msg = ref_msg(logdata, logend);
+       munmap(log_mapped, mapsz);
+
+       if (cutoff_time)
+               *cutoff_time = date;
+       if (cutoff_tz)
+               *cutoff_tz = tz;
+       if (cutoff_cnt)
+               *cutoff_cnt = reccnt;
+       return 1;
+}
+
+int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
+{
+       const char *logfile;
+       FILE *logfp;
+       char buf[1024];
+       int ret = 0;
+
+       logfile = git_path("logs/%s", ref);
+       logfp = fopen(logfile, "r");
+       if (!logfp)
+               return -1;
+       while (fgets(buf, sizeof(buf), logfp)) {
+               unsigned char osha1[20], nsha1[20];
+               char *email_end, *message;
+               unsigned long timestamp;
+               int len, tz;
+
+               /* old SP new SP name <email> SP time TAB msg LF */
+               len = strlen(buf);
+               if (len < 83 || buf[len-1] != '\n' ||
+                   get_sha1_hex(buf, osha1) || buf[40] != ' ' ||
+                   get_sha1_hex(buf + 41, nsha1) || buf[81] != ' ' ||
+                   !(email_end = strchr(buf + 82, '>')) ||
+                   email_end[1] != ' ' ||
+                   !(timestamp = strtoul(email_end + 2, &message, 10)) ||
+                   !message || message[0] != ' ' ||
+                   (message[1] != '+' && message[1] != '-') ||
+                   !isdigit(message[2]) || !isdigit(message[3]) ||
+                   !isdigit(message[4]) || !isdigit(message[5]))
+                       continue; /* corrupt? */
+               email_end[1] = '\0';
+               tz = strtol(message + 1, NULL, 10);
+               if (message[6] != '\t')
+                       message += 6;
+               else
+                       message += 7;
+               ret = fn(osha1, nsha1, buf+82, timestamp, tz, message, cb_data);
+               if (ret)
+                       break;
+       }
+       fclose(logfp);
+       return ret;
+}
+
+static int do_for_each_reflog(const char *base, each_ref_fn fn, void *cb_data)
+{
+       DIR *dir = opendir(git_path("logs/%s", base));
+       int retval = 0;
+
+       if (dir) {
+               struct dirent *de;
+               int baselen = strlen(base);
+               char *log = xmalloc(baselen + 257);
+
+               memcpy(log, base, baselen);
+               if (baselen && base[baselen-1] != '/')
+                       log[baselen++] = '/';
+
+               while ((de = readdir(dir)) != NULL) {
+                       struct stat st;
+                       int namelen;
+
+                       if (de->d_name[0] == '.')
+                               continue;
+                       namelen = strlen(de->d_name);
+                       if (namelen > 255)
+                               continue;
+                       if (has_extension(de->d_name, ".lock"))
+                               continue;
+                       memcpy(log + baselen, de->d_name, namelen+1);
+                       if (stat(git_path("logs/%s", log), &st) < 0)
+                               continue;
+                       if (S_ISDIR(st.st_mode)) {
+                               retval = do_for_each_reflog(log, fn, cb_data);
+                       } else {
+                               unsigned char sha1[20];
+                               if (!resolve_ref(log, sha1, 0, NULL))
+                                       retval = error("bad ref for %s", log);
+                               else
+                                       retval = fn(log, sha1, 0, cb_data);
+                       }
+                       if (retval)
+                               break;
+               }
+               free(log);
+               closedir(dir);
+       }
+       else if (*base)
+               return errno;
+       return retval;
+}
+
+int for_each_reflog(each_ref_fn fn, void *cb_data)
+{
+       return do_for_each_reflog("", fn, cb_data);
+}
+
+int update_ref(const char *action, const char *refname,
+               const unsigned char *sha1, const unsigned char *oldval,
+               int flags, enum action_on_err onerr)
+{
+       static struct ref_lock *lock;
+       lock = lock_any_ref_for_update(refname, oldval, flags);
+       if (!lock) {
+               const char *str = "Cannot lock the ref '%s'.";
+               switch (onerr) {
+               case MSG_ON_ERR: error(str, refname); break;
+               case DIE_ON_ERR: die(str, refname); break;
+               case QUIET_ON_ERR: break;
+               }
+               return 1;
+       }
+       if (write_ref_sha1(lock, sha1, action) < 0) {
+               const char *str = "Cannot update the ref '%s'.";
+               switch (onerr) {
+               case MSG_ON_ERR: error(str, refname); break;
+               case DIE_ON_ERR: die(str, refname); break;
+               case QUIET_ON_ERR: break;
+               }
+               return 1;
+       }
+       return 0;
+}
+
+struct ref *find_ref_by_name(struct ref *list, const char *name)
+{
+       for ( ; list; list = list->next)
+               if (!strcmp(list->name, name))
+                       return list;
+       return NULL;
+}
diff --git a/refs.h b/refs.h
new file mode 100644 (file)
index 0000000..9dc8aa0
--- /dev/null
+++ b/refs.h
@@ -0,0 +1,70 @@
+#ifndef REFS_H
+#define REFS_H
+
+struct ref_lock {
+       char *ref_name;
+       char *orig_ref_name;
+       struct lock_file *lk;
+       unsigned char old_sha1[20];
+       int lock_fd;
+       int force_write;
+};
+
+#define REF_ISSYMREF 01
+#define REF_ISPACKED 02
+
+/*
+ * Calls the specified function for each ref file until it returns nonzero,
+ * and returns the value
+ */
+typedef int each_ref_fn(const char *refname, const unsigned char *sha1, int flags, void *cb_data);
+extern int head_ref(each_ref_fn, void *);
+extern int for_each_ref(each_ref_fn, void *);
+extern int for_each_tag_ref(each_ref_fn, void *);
+extern int for_each_branch_ref(each_ref_fn, void *);
+extern int for_each_remote_ref(each_ref_fn, void *);
+
+extern int peel_ref(const char *, unsigned char *);
+
+/** Locks a "refs/" ref returning the lock on success and NULL on failure. **/
+extern struct ref_lock *lock_ref_sha1(const char *ref, const unsigned char *old_sha1);
+
+/** Locks any ref (for 'HEAD' type refs). */
+#define REF_NODEREF    0x01
+extern struct ref_lock *lock_any_ref_for_update(const char *ref, const unsigned char *old_sha1, int flags);
+
+/** Release any lock taken but not written. **/
+extern void unlock_ref(struct ref_lock *lock);
+
+/** Writes sha1 into the ref specified by the lock. **/
+extern int write_ref_sha1(struct ref_lock *lock, const unsigned char *sha1, const char *msg);
+
+/** Reads log for the value of ref during at_time. **/
+extern int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *sha1, char **msg, unsigned long *cutoff_time, int *cutoff_tz, int *cutoff_cnt);
+
+/* iterate over reflog entries */
+typedef int each_reflog_ent_fn(unsigned char *osha1, unsigned char *nsha1, const char *, unsigned long, int, const char *, void *);
+int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data);
+
+/*
+ * Calls the specified function for each reflog file until it returns nonzero,
+ * and returns the value
+ */
+extern int for_each_reflog(each_ref_fn, void *);
+
+/** Returns 0 if target has the right format for a ref. **/
+extern int check_ref_format(const char *target);
+
+/** rename ref, return 0 on success **/
+extern int rename_ref(const char *oldref, const char *newref, const char *logmsg);
+
+/** resolve ref in nested "gitlink" repository */
+extern int resolve_gitlink_ref(const char *name, const char *refname, unsigned char *result);
+
+/** lock a ref and then write its file */
+enum action_on_err { MSG_ON_ERR, DIE_ON_ERR, QUIET_ON_ERR };
+int update_ref(const char *action, const char *refname,
+               const unsigned char *sha1, const unsigned char *oldval,
+               int flags, enum action_on_err onerr);
+
+#endif /* REFS_H */
diff --git a/remote.c b/remote.c
new file mode 100644 (file)
index 0000000..3fb0f99
--- /dev/null
+++ b/remote.c
@@ -0,0 +1,911 @@
+#include "cache.h"
+#include "remote.h"
+#include "refs.h"
+
+static struct remote **remotes;
+static int allocated_remotes;
+
+static struct branch **branches;
+static int allocated_branches;
+
+static struct branch *current_branch;
+static const char *default_remote_name;
+
+#define BUF_SIZE (2048)
+static char buffer[BUF_SIZE];
+
+static void add_push_refspec(struct remote *remote, const char *ref)
+{
+       int nr = remote->push_refspec_nr + 1;
+       remote->push_refspec =
+               xrealloc(remote->push_refspec, nr * sizeof(char *));
+       remote->push_refspec[nr-1] = ref;
+       remote->push_refspec_nr = nr;
+}
+
+static void add_fetch_refspec(struct remote *remote, const char *ref)
+{
+       int nr = remote->fetch_refspec_nr + 1;
+       remote->fetch_refspec =
+               xrealloc(remote->fetch_refspec, nr * sizeof(char *));
+       remote->fetch_refspec[nr-1] = ref;
+       remote->fetch_refspec_nr = nr;
+}
+
+static void add_url(struct remote *remote, const char *url)
+{
+       int nr = remote->url_nr + 1;
+       remote->url =
+               xrealloc(remote->url, nr * sizeof(char *));
+       remote->url[nr-1] = url;
+       remote->url_nr = nr;
+}
+
+static struct remote *make_remote(const char *name, int len)
+{
+       int i, empty = -1;
+
+       for (i = 0; i < allocated_remotes; i++) {
+               if (!remotes[i]) {
+                       if (empty < 0)
+                               empty = i;
+               } else {
+                       if (len ? (!strncmp(name, remotes[i]->name, len) &&
+                                  !remotes[i]->name[len]) :
+                           !strcmp(name, remotes[i]->name))
+                               return remotes[i];
+               }
+       }
+
+       if (empty < 0) {
+               empty = allocated_remotes;
+               allocated_remotes += allocated_remotes ? allocated_remotes : 1;
+               remotes = xrealloc(remotes,
+                                  sizeof(*remotes) * allocated_remotes);
+               memset(remotes + empty, 0,
+                      (allocated_remotes - empty) * sizeof(*remotes));
+       }
+       remotes[empty] = xcalloc(1, sizeof(struct remote));
+       if (len)
+               remotes[empty]->name = xstrndup(name, len);
+       else
+               remotes[empty]->name = xstrdup(name);
+       return remotes[empty];
+}
+
+static void add_merge(struct branch *branch, const char *name)
+{
+       int nr = branch->merge_nr + 1;
+       branch->merge_name =
+               xrealloc(branch->merge_name, nr * sizeof(char *));
+       branch->merge_name[nr-1] = name;
+       branch->merge_nr = nr;
+}
+
+static struct branch *make_branch(const char *name, int len)
+{
+       int i, empty = -1;
+       char *refname;
+
+       for (i = 0; i < allocated_branches; i++) {
+               if (!branches[i]) {
+                       if (empty < 0)
+                               empty = i;
+               } else {
+                       if (len ? (!strncmp(name, branches[i]->name, len) &&
+                                  !branches[i]->name[len]) :
+                           !strcmp(name, branches[i]->name))
+                               return branches[i];
+               }
+       }
+
+       if (empty < 0) {
+               empty = allocated_branches;
+               allocated_branches += allocated_branches ? allocated_branches : 1;
+               branches = xrealloc(branches,
+                                  sizeof(*branches) * allocated_branches);
+               memset(branches + empty, 0,
+                      (allocated_branches - empty) * sizeof(*branches));
+       }
+       branches[empty] = xcalloc(1, sizeof(struct branch));
+       if (len)
+               branches[empty]->name = xstrndup(name, len);
+       else
+               branches[empty]->name = xstrdup(name);
+       refname = malloc(strlen(name) + strlen("refs/heads/") + 1);
+       strcpy(refname, "refs/heads/");
+       strcpy(refname + strlen("refs/heads/"),
+              branches[empty]->name);
+       branches[empty]->refname = refname;
+
+       return branches[empty];
+}
+
+static void read_remotes_file(struct remote *remote)
+{
+       FILE *f = fopen(git_path("remotes/%s", remote->name), "r");
+
+       if (!f)
+               return;
+       while (fgets(buffer, BUF_SIZE, f)) {
+               int value_list;
+               char *s, *p;
+
+               if (!prefixcmp(buffer, "URL:")) {
+                       value_list = 0;
+                       s = buffer + 4;
+               } else if (!prefixcmp(buffer, "Push:")) {
+                       value_list = 1;
+                       s = buffer + 5;
+               } else if (!prefixcmp(buffer, "Pull:")) {
+                       value_list = 2;
+                       s = buffer + 5;
+               } else
+                       continue;
+
+               while (isspace(*s))
+                       s++;
+               if (!*s)
+                       continue;
+
+               p = s + strlen(s);
+               while (isspace(p[-1]))
+                       *--p = 0;
+
+               switch (value_list) {
+               case 0:
+                       add_url(remote, xstrdup(s));
+                       break;
+               case 1:
+                       add_push_refspec(remote, xstrdup(s));
+                       break;
+               case 2:
+                       add_fetch_refspec(remote, xstrdup(s));
+                       break;
+               }
+       }
+       fclose(f);
+}
+
+static void read_branches_file(struct remote *remote)
+{
+       const char *slash = strchr(remote->name, '/');
+       char *frag;
+       char *branch;
+       int n = slash ? slash - remote->name : 1000;
+       FILE *f = fopen(git_path("branches/%.*s", n, remote->name), "r");
+       char *s, *p;
+       int len;
+
+       if (!f)
+               return;
+       s = fgets(buffer, BUF_SIZE, f);
+       fclose(f);
+       if (!s)
+               return;
+       while (isspace(*s))
+               s++;
+       if (!*s)
+               return;
+       p = s + strlen(s);
+       while (isspace(p[-1]))
+               *--p = 0;
+       len = p - s;
+       if (slash)
+               len += strlen(slash);
+       p = xmalloc(len + 1);
+       strcpy(p, s);
+       if (slash)
+               strcat(p, slash);
+       frag = strchr(p, '#');
+       if (frag) {
+               *(frag++) = '\0';
+               branch = xmalloc(strlen(frag) + 12);
+               strcpy(branch, "refs/heads/");
+               strcat(branch, frag);
+       } else {
+               branch = "refs/heads/master";
+       }
+       add_url(remote, p);
+       add_fetch_refspec(remote, branch);
+       remote->fetch_tags = 1; /* always auto-follow */
+}
+
+static int handle_config(const char *key, const char *value)
+{
+       const char *name;
+       const char *subkey;
+       struct remote *remote;
+       struct branch *branch;
+       if (!prefixcmp(key, "branch.")) {
+               name = key + 7;
+               subkey = strrchr(name, '.');
+               branch = make_branch(name, subkey - name);
+               if (!subkey)
+                       return 0;
+               if (!value)
+                       return 0;
+               if (!strcmp(subkey, ".remote")) {
+                       branch->remote_name = xstrdup(value);
+                       if (branch == current_branch)
+                               default_remote_name = branch->remote_name;
+               } else if (!strcmp(subkey, ".merge"))
+                       add_merge(branch, xstrdup(value));
+               return 0;
+       }
+       if (prefixcmp(key,  "remote."))
+               return 0;
+       name = key + 7;
+       subkey = strrchr(name, '.');
+       if (!subkey)
+               return error("Config with no key for remote %s", name);
+       if (*subkey == '/') {
+               warning("Config remote shorthand cannot begin with '/': %s", name);
+               return 0;
+       }
+       remote = make_remote(name, subkey - name);
+       if (!value) {
+               /* if we ever have a boolean variable, e.g. "remote.*.disabled"
+                * [remote "frotz"]
+                *      disabled
+                * is a valid way to set it to true; we get NULL in value so
+                * we need to handle it here.
+                *
+                * if (!strcmp(subkey, ".disabled")) {
+                *      val = git_config_bool(key, value);
+                *      return 0;
+                * } else
+                *
+                */
+               return 0; /* ignore unknown booleans */
+       }
+       if (!strcmp(subkey, ".url")) {
+               add_url(remote, xstrdup(value));
+       } else if (!strcmp(subkey, ".push")) {
+               add_push_refspec(remote, xstrdup(value));
+       } else if (!strcmp(subkey, ".fetch")) {
+               add_fetch_refspec(remote, xstrdup(value));
+       } else if (!strcmp(subkey, ".receivepack")) {
+               if (!remote->receivepack)
+                       remote->receivepack = xstrdup(value);
+               else
+                       error("more than one receivepack given, using the first");
+       } else if (!strcmp(subkey, ".uploadpack")) {
+               if (!remote->uploadpack)
+                       remote->uploadpack = xstrdup(value);
+               else
+                       error("more than one uploadpack given, using the first");
+       } else if (!strcmp(subkey, ".tagopt")) {
+               if (!strcmp(value, "--no-tags"))
+                       remote->fetch_tags = -1;
+       } else if (!strcmp(subkey, ".proxy")) {
+               remote->http_proxy = xstrdup(value);
+       }
+       return 0;
+}
+
+static void read_config(void)
+{
+       unsigned char sha1[20];
+       const char *head_ref;
+       int flag;
+       if (default_remote_name) // did this already
+               return;
+       default_remote_name = xstrdup("origin");
+       current_branch = NULL;
+       head_ref = resolve_ref("HEAD", sha1, 0, &flag);
+       if (head_ref && (flag & REF_ISSYMREF) &&
+           !prefixcmp(head_ref, "refs/heads/")) {
+               current_branch =
+                       make_branch(head_ref + strlen("refs/heads/"), 0);
+       }
+       git_config(handle_config);
+}
+
+struct refspec *parse_ref_spec(int nr_refspec, const char **refspec)
+{
+       int i;
+       struct refspec *rs = xcalloc(sizeof(*rs), nr_refspec);
+       for (i = 0; i < nr_refspec; i++) {
+               const char *sp, *ep, *gp;
+               sp = refspec[i];
+               if (*sp == '+') {
+                       rs[i].force = 1;
+                       sp++;
+               }
+               gp = strchr(sp, '*');
+               ep = strchr(sp, ':');
+               if (gp && ep && gp > ep)
+                       gp = NULL;
+               if (ep) {
+                       if (ep[1]) {
+                               const char *glob = strchr(ep + 1, '*');
+                               if (!glob)
+                                       gp = NULL;
+                               if (gp)
+                                       rs[i].dst = xstrndup(ep + 1,
+                                                            glob - ep - 1);
+                               else
+                                       rs[i].dst = xstrdup(ep + 1);
+                       }
+               } else {
+                       ep = sp + strlen(sp);
+               }
+               if (gp) {
+                       rs[i].pattern = 1;
+                       ep = gp;
+               }
+               rs[i].src = xstrndup(sp, ep - sp);
+       }
+       return rs;
+}
+
+struct remote *remote_get(const char *name)
+{
+       struct remote *ret;
+
+       read_config();
+       if (!name)
+               name = default_remote_name;
+       ret = make_remote(name, 0);
+       if (name[0] != '/') {
+               if (!ret->url)
+                       read_remotes_file(ret);
+               if (!ret->url)
+                       read_branches_file(ret);
+       }
+       if (!ret->url)
+               add_url(ret, name);
+       if (!ret->url)
+               return NULL;
+       ret->fetch = parse_ref_spec(ret->fetch_refspec_nr, ret->fetch_refspec);
+       ret->push = parse_ref_spec(ret->push_refspec_nr, ret->push_refspec);
+       return ret;
+}
+
+int for_each_remote(each_remote_fn fn, void *priv)
+{
+       int i, result = 0;
+       read_config();
+       for (i = 0; i < allocated_remotes && !result; i++) {
+               struct remote *r = remotes[i];
+               if (!r)
+                       continue;
+               if (!r->fetch)
+                       r->fetch = parse_ref_spec(r->fetch_refspec_nr,
+                                       r->fetch_refspec);
+               if (!r->push)
+                       r->push = parse_ref_spec(r->push_refspec_nr,
+                                       r->push_refspec);
+               result = fn(r, priv);
+       }
+       return result;
+}
+
+void ref_remove_duplicates(struct ref *ref_map)
+{
+       struct ref **posn;
+       struct ref *next;
+       for (; ref_map; ref_map = ref_map->next) {
+               if (!ref_map->peer_ref)
+                       continue;
+               posn = &ref_map->next;
+               while (*posn) {
+                       if ((*posn)->peer_ref &&
+                           !strcmp((*posn)->peer_ref->name,
+                                   ref_map->peer_ref->name)) {
+                               if (strcmp((*posn)->name, ref_map->name))
+                                       die("%s tracks both %s and %s",
+                                           ref_map->peer_ref->name,
+                                           (*posn)->name, ref_map->name);
+                               next = (*posn)->next;
+                               free((*posn)->peer_ref);
+                               free(*posn);
+                               *posn = next;
+                       } else {
+                               posn = &(*posn)->next;
+                       }
+               }
+       }
+}
+
+int remote_has_url(struct remote *remote, const char *url)
+{
+       int i;
+       for (i = 0; i < remote->url_nr; i++) {
+               if (!strcmp(remote->url[i], url))
+                       return 1;
+       }
+       return 0;
+}
+
+int remote_find_tracking(struct remote *remote, struct refspec *refspec)
+{
+       int find_src = refspec->src == NULL;
+       char *needle, **result;
+       int i;
+
+       if (find_src) {
+               if (!refspec->dst)
+                       return error("find_tracking: need either src or dst");
+               needle = refspec->dst;
+               result = &refspec->src;
+       } else {
+               needle = refspec->src;
+               result = &refspec->dst;
+       }
+
+       for (i = 0; i < remote->fetch_refspec_nr; i++) {
+               struct refspec *fetch = &remote->fetch[i];
+               const char *key = find_src ? fetch->dst : fetch->src;
+               const char *value = find_src ? fetch->src : fetch->dst;
+               if (!fetch->dst)
+                       continue;
+               if (fetch->pattern) {
+                       if (!prefixcmp(needle, key)) {
+                               *result = xmalloc(strlen(value) +
+                                                 strlen(needle) -
+                                                 strlen(key) + 1);
+                               strcpy(*result, value);
+                               strcpy(*result + strlen(value),
+                                      needle + strlen(key));
+                               refspec->force = fetch->force;
+                               return 0;
+                       }
+               } else if (!strcmp(needle, key)) {
+                       *result = xstrdup(value);
+                       refspec->force = fetch->force;
+                       return 0;
+               }
+       }
+       return -1;
+}
+
+struct ref *alloc_ref(unsigned namelen)
+{
+       struct ref *ret = xmalloc(sizeof(struct ref) + namelen);
+       memset(ret, 0, sizeof(struct ref) + namelen);
+       return ret;
+}
+
+static struct ref *copy_ref(const struct ref *ref)
+{
+       struct ref *ret = xmalloc(sizeof(struct ref) + strlen(ref->name) + 1);
+       memcpy(ret, ref, sizeof(struct ref) + strlen(ref->name) + 1);
+       ret->next = NULL;
+       return ret;
+}
+
+struct ref *copy_ref_list(const struct ref *ref)
+{
+       struct ref *ret = NULL;
+       struct ref **tail = &ret;
+       while (ref) {
+               *tail = copy_ref(ref);
+               ref = ref->next;
+               tail = &((*tail)->next);
+       }
+       return ret;
+}
+
+void free_refs(struct ref *ref)
+{
+       struct ref *next;
+       while (ref) {
+               next = ref->next;
+               if (ref->peer_ref)
+                       free(ref->peer_ref);
+               free(ref);
+               ref = next;
+       }
+}
+
+static int count_refspec_match(const char *pattern,
+                              struct ref *refs,
+                              struct ref **matched_ref)
+{
+       int patlen = strlen(pattern);
+       struct ref *matched_weak = NULL;
+       struct ref *matched = NULL;
+       int weak_match = 0;
+       int match = 0;
+
+       for (weak_match = match = 0; refs; refs = refs->next) {
+               char *name = refs->name;
+               int namelen = strlen(name);
+
+               if (!refname_match(pattern, name, ref_rev_parse_rules))
+                       continue;
+
+               /* A match is "weak" if it is with refs outside
+                * heads or tags, and did not specify the pattern
+                * in full (e.g. "refs/remotes/origin/master") or at
+                * least from the toplevel (e.g. "remotes/origin/master");
+                * otherwise "git push $URL master" would result in
+                * ambiguity between remotes/origin/master and heads/master
+                * at the remote site.
+                */
+               if (namelen != patlen &&
+                   patlen != namelen - 5 &&
+                   prefixcmp(name, "refs/heads/") &&
+                   prefixcmp(name, "refs/tags/")) {
+                       /* We want to catch the case where only weak
+                        * matches are found and there are multiple
+                        * matches, and where more than one strong
+                        * matches are found, as ambiguous.  One
+                        * strong match with zero or more weak matches
+                        * are acceptable as a unique match.
+                        */
+                       matched_weak = refs;
+                       weak_match++;
+               }
+               else {
+                       matched = refs;
+                       match++;
+               }
+       }
+       if (!matched) {
+               *matched_ref = matched_weak;
+               return weak_match;
+       }
+       else {
+               *matched_ref = matched;
+               return match;
+       }
+}
+
+static void tail_link_ref(struct ref *ref, struct ref ***tail)
+{
+       **tail = ref;
+       while (ref->next)
+               ref = ref->next;
+       *tail = &ref->next;
+}
+
+static struct ref *try_explicit_object_name(const char *name)
+{
+       unsigned char sha1[20];
+       struct ref *ref;
+       int len;
+
+       if (!*name) {
+               ref = alloc_ref(20);
+               strcpy(ref->name, "(delete)");
+               hashclr(ref->new_sha1);
+               return ref;
+       }
+       if (get_sha1(name, sha1))
+               return NULL;
+       len = strlen(name) + 1;
+       ref = alloc_ref(len);
+       memcpy(ref->name, name, len);
+       hashcpy(ref->new_sha1, sha1);
+       return ref;
+}
+
+static struct ref *make_linked_ref(const char *name, struct ref ***tail)
+{
+       struct ref *ret;
+       size_t len;
+
+       len = strlen(name) + 1;
+       ret = alloc_ref(len);
+       memcpy(ret->name, name, len);
+       tail_link_ref(ret, tail);
+       return ret;
+}
+
+static int match_explicit(struct ref *src, struct ref *dst,
+                         struct ref ***dst_tail,
+                         struct refspec *rs,
+                         int errs)
+{
+       struct ref *matched_src, *matched_dst;
+
+       const char *dst_value = rs->dst;
+
+       if (rs->pattern)
+               return errs;
+
+       matched_src = matched_dst = NULL;
+       switch (count_refspec_match(rs->src, src, &matched_src)) {
+       case 1:
+               break;
+       case 0:
+               /* The source could be in the get_sha1() format
+                * not a reference name.  :refs/other is a
+                * way to delete 'other' ref at the remote end.
+                */
+               matched_src = try_explicit_object_name(rs->src);
+               if (!matched_src)
+                       error("src refspec %s does not match any.", rs->src);
+               break;
+       default:
+               matched_src = NULL;
+               error("src refspec %s matches more than one.", rs->src);
+               break;
+       }
+
+       if (!matched_src)
+               errs = 1;
+
+       if (!dst_value) {
+               if (!matched_src)
+                       return errs;
+               dst_value = matched_src->name;
+       }
+
+       switch (count_refspec_match(dst_value, dst, &matched_dst)) {
+       case 1:
+               break;
+       case 0:
+               if (!memcmp(dst_value, "refs/", 5))
+                       matched_dst = make_linked_ref(dst_value, dst_tail);
+               else
+                       error("dst refspec %s does not match any "
+                             "existing ref on the remote and does "
+                             "not start with refs/.", dst_value);
+               break;
+       default:
+               matched_dst = NULL;
+               error("dst refspec %s matches more than one.",
+                     dst_value);
+               break;
+       }
+       if (errs || !matched_dst)
+               return 1;
+       if (matched_dst->peer_ref) {
+               errs = 1;
+               error("dst ref %s receives from more than one src.",
+                     matched_dst->name);
+       }
+       else {
+               matched_dst->peer_ref = matched_src;
+               matched_dst->force = rs->force;
+       }
+       return errs;
+}
+
+static int match_explicit_refs(struct ref *src, struct ref *dst,
+                              struct ref ***dst_tail, struct refspec *rs,
+                              int rs_nr)
+{
+       int i, errs;
+       for (i = errs = 0; i < rs_nr; i++)
+               errs |= match_explicit(src, dst, dst_tail, &rs[i], errs);
+       return -errs;
+}
+
+static const struct refspec *check_pattern_match(const struct refspec *rs,
+                                                int rs_nr,
+                                                const struct ref *src)
+{
+       int i;
+       for (i = 0; i < rs_nr; i++) {
+               if (rs[i].pattern && !prefixcmp(src->name, rs[i].src))
+                       return rs + i;
+       }
+       return NULL;
+}
+
+/*
+ * Note. This is used only by "push"; refspec matching rules for
+ * push and fetch are subtly different, so do not try to reuse it
+ * without thinking.
+ */
+int match_refs(struct ref *src, struct ref *dst, struct ref ***dst_tail,
+              int nr_refspec, const char **refspec, int flags)
+{
+       struct refspec *rs =
+               parse_ref_spec(nr_refspec, (const char **) refspec);
+       int send_all = flags & MATCH_REFS_ALL;
+       int send_mirror = flags & MATCH_REFS_MIRROR;
+
+       if (match_explicit_refs(src, dst, dst_tail, rs, nr_refspec))
+               return -1;
+
+       /* pick the remainder */
+       for ( ; src; src = src->next) {
+               struct ref *dst_peer;
+               const struct refspec *pat = NULL;
+               char *dst_name;
+               if (src->peer_ref)
+                       continue;
+               if (nr_refspec) {
+                       pat = check_pattern_match(rs, nr_refspec, src);
+                       if (!pat)
+                               continue;
+               }
+               else if (!send_mirror && prefixcmp(src->name, "refs/heads/"))
+                       /*
+                        * "matching refs"; traditionally we pushed everything
+                        * including refs outside refs/heads/ hierarchy, but
+                        * that does not make much sense these days.
+                        */
+                       continue;
+
+               if (pat) {
+                       const char *dst_side = pat->dst ? pat->dst : pat->src;
+                       dst_name = xmalloc(strlen(dst_side) +
+                                          strlen(src->name) -
+                                          strlen(pat->src) + 2);
+                       strcpy(dst_name, dst_side);
+                       strcat(dst_name, src->name + strlen(pat->src));
+               } else
+                       dst_name = xstrdup(src->name);
+               dst_peer = find_ref_by_name(dst, dst_name);
+               if (dst_peer && dst_peer->peer_ref)
+                       /* We're already sending something to this ref. */
+                       goto free_name;
+
+               if (!dst_peer && !nr_refspec && !(send_all || send_mirror))
+                       /*
+                        * Remote doesn't have it, and we have no
+                        * explicit pattern, and we don't have
+                        * --all nor --mirror.
+                        */
+                       goto free_name;
+               if (!dst_peer) {
+                       /* Create a new one and link it */
+                       dst_peer = make_linked_ref(dst_name, dst_tail);
+                       hashcpy(dst_peer->new_sha1, src->new_sha1);
+               }
+               dst_peer->peer_ref = src;
+               if (pat)
+                       dst_peer->force = pat->force;
+       free_name:
+               free(dst_name);
+       }
+       return 0;
+}
+
+struct branch *branch_get(const char *name)
+{
+       struct branch *ret;
+
+       read_config();
+       if (!name || !*name || !strcmp(name, "HEAD"))
+               ret = current_branch;
+       else
+               ret = make_branch(name, 0);
+       if (ret && ret->remote_name) {
+               ret->remote = remote_get(ret->remote_name);
+               if (ret->merge_nr) {
+                       int i;
+                       ret->merge = xcalloc(sizeof(*ret->merge),
+                                            ret->merge_nr);
+                       for (i = 0; i < ret->merge_nr; i++) {
+                               ret->merge[i] = xcalloc(1, sizeof(**ret->merge));
+                               ret->merge[i]->src = xstrdup(ret->merge_name[i]);
+                               remote_find_tracking(ret->remote,
+                                                    ret->merge[i]);
+                       }
+               }
+       }
+       return ret;
+}
+
+int branch_has_merge_config(struct branch *branch)
+{
+       return branch && !!branch->merge;
+}
+
+int branch_merge_matches(struct branch *branch,
+                                int i,
+                                const char *refname)
+{
+       if (!branch || i < 0 || i >= branch->merge_nr)
+               return 0;
+       return refname_match(branch->merge[i]->src, refname, ref_fetch_rules);
+}
+
+static struct ref *get_expanded_map(const struct ref *remote_refs,
+                                   const struct refspec *refspec)
+{
+       const struct ref *ref;
+       struct ref *ret = NULL;
+       struct ref **tail = &ret;
+
+       int remote_prefix_len = strlen(refspec->src);
+       int local_prefix_len = strlen(refspec->dst);
+
+       for (ref = remote_refs; ref; ref = ref->next) {
+               if (strchr(ref->name, '^'))
+                       continue; /* a dereference item */
+               if (!prefixcmp(ref->name, refspec->src)) {
+                       const char *match;
+                       struct ref *cpy = copy_ref(ref);
+                       match = ref->name + remote_prefix_len;
+
+                       cpy->peer_ref = alloc_ref(local_prefix_len +
+                                                 strlen(match) + 1);
+                       sprintf(cpy->peer_ref->name, "%s%s",
+                               refspec->dst, match);
+                       if (refspec->force)
+                               cpy->peer_ref->force = 1;
+                       *tail = cpy;
+                       tail = &cpy->next;
+               }
+       }
+
+       return ret;
+}
+
+static const struct ref *find_ref_by_name_abbrev(const struct ref *refs, const char *name)
+{
+       const struct ref *ref;
+       for (ref = refs; ref; ref = ref->next) {
+               if (refname_match(name, ref->name, ref_fetch_rules))
+                       return ref;
+       }
+       return NULL;
+}
+
+struct ref *get_remote_ref(const struct ref *remote_refs, const char *name)
+{
+       const struct ref *ref = find_ref_by_name_abbrev(remote_refs, name);
+
+       if (!ref)
+               return NULL;
+
+       return copy_ref(ref);
+}
+
+static struct ref *get_local_ref(const char *name)
+{
+       struct ref *ret;
+       if (!name)
+               return NULL;
+
+       if (!prefixcmp(name, "refs/")) {
+               ret = alloc_ref(strlen(name) + 1);
+               strcpy(ret->name, name);
+               return ret;
+       }
+
+       if (!prefixcmp(name, "heads/") ||
+           !prefixcmp(name, "tags/") ||
+           !prefixcmp(name, "remotes/")) {
+               ret = alloc_ref(strlen(name) + 6);
+               sprintf(ret->name, "refs/%s", name);
+               return ret;
+       }
+
+       ret = alloc_ref(strlen(name) + 12);
+       sprintf(ret->name, "refs/heads/%s", name);
+       return ret;
+}
+
+int get_fetch_map(const struct ref *remote_refs,
+                 const struct refspec *refspec,
+                 struct ref ***tail,
+                 int missing_ok)
+{
+       struct ref *ref_map, *rm;
+
+       if (refspec->pattern) {
+               ref_map = get_expanded_map(remote_refs, refspec);
+       } else {
+               const char *name = refspec->src[0] ? refspec->src : "HEAD";
+
+               ref_map = get_remote_ref(remote_refs, name);
+               if (!missing_ok && !ref_map)
+                       die("Couldn't find remote ref %s", name);
+               if (ref_map) {
+                       ref_map->peer_ref = get_local_ref(refspec->dst);
+                       if (ref_map->peer_ref && refspec->force)
+                               ref_map->peer_ref->force = 1;
+               }
+       }
+
+       for (rm = ref_map; rm; rm = rm->next) {
+               if (rm->peer_ref && check_ref_format(rm->peer_ref->name + 5))
+                       die("* refusing to create funny ref '%s' locally",
+                           rm->peer_ref->name);
+       }
+
+       if (ref_map)
+               tail_link_ref(ref_map, tail);
+
+       return 0;
+}
diff --git a/remote.h b/remote.h
new file mode 100644 (file)
index 0000000..86e036d
--- /dev/null
+++ b/remote.h
@@ -0,0 +1,117 @@
+#ifndef REMOTE_H
+#define REMOTE_H
+
+struct remote {
+       const char *name;
+
+       const char **url;
+       int url_nr;
+
+       const char **push_refspec;
+       struct refspec *push;
+       int push_refspec_nr;
+
+       const char **fetch_refspec;
+       struct refspec *fetch;
+       int fetch_refspec_nr;
+
+       /*
+        * -1 to never fetch tags
+        * 0 to auto-follow tags on heuristic (default)
+        * 1 to always auto-follow tags
+        * 2 to always fetch tags
+        */
+       int fetch_tags;
+
+       const char *receivepack;
+       const char *uploadpack;
+
+       /*
+        * for curl remotes only
+        */
+       char *http_proxy;
+};
+
+struct remote *remote_get(const char *name);
+
+typedef int each_remote_fn(struct remote *remote, void *priv);
+int for_each_remote(each_remote_fn fn, void *priv);
+
+int remote_has_url(struct remote *remote, const char *url);
+
+struct refspec {
+       unsigned force : 1;
+       unsigned pattern : 1;
+
+       char *src;
+       char *dst;
+};
+
+struct ref *alloc_ref(unsigned namelen);
+
+struct ref *copy_ref_list(const struct ref *ref);
+
+int check_ref_type(const struct ref *ref, int flags);
+
+/*
+ * Frees the entire list and peers of elements.
+ */
+void free_refs(struct ref *ref);
+
+/*
+ * Removes and frees any duplicate refs in the map.
+ */
+void ref_remove_duplicates(struct ref *ref_map);
+
+struct refspec *parse_ref_spec(int nr_refspec, const char **refspec);
+
+int match_refs(struct ref *src, struct ref *dst, struct ref ***dst_tail,
+              int nr_refspec, const char **refspec, int all);
+
+/*
+ * Given a list of the remote refs and the specification of things to
+ * fetch, makes a (separate) list of the refs to fetch and the local
+ * refs to store into.
+ *
+ * *tail is the pointer to the tail pointer of the list of results
+ * beforehand, and will be set to the tail pointer of the list of
+ * results afterward.
+ *
+ * missing_ok is usually false, but when we are adding branch.$name.merge
+ * it is Ok if the branch is not at the remote anymore.
+ */
+int get_fetch_map(const struct ref *remote_refs, const struct refspec *refspec,
+                 struct ref ***tail, int missing_ok);
+
+struct ref *get_remote_ref(const struct ref *remote_refs, const char *name);
+
+/*
+ * For the given remote, reads the refspec's src and sets the other fields.
+ */
+int remote_find_tracking(struct remote *remote, struct refspec *refspec);
+
+struct branch {
+       const char *name;
+       const char *refname;
+
+       const char *remote_name;
+       struct remote *remote;
+
+       const char **merge_name;
+       struct refspec **merge;
+       int merge_nr;
+};
+
+struct branch *branch_get(const char *name);
+
+int branch_has_merge_config(struct branch *branch);
+int branch_merge_matches(struct branch *, int n, const char *);
+
+/* Flags to match_refs. */
+enum match_refs_flags {
+       MATCH_REFS_NONE         = 0,
+       MATCH_REFS_ALL          = (1 << 0),
+       MATCH_REFS_MIRROR       = (1 << 1),
+};
+
+#endif
diff --git a/revision.c b/revision.c
new file mode 100644 (file)
index 0000000..2a59035
--- /dev/null
@@ -0,0 +1,1618 @@
+#include "cache.h"
+#include "tag.h"
+#include "blob.h"
+#include "tree.h"
+#include "commit.h"
+#include "diff.h"
+#include "refs.h"
+#include "revision.h"
+#include "grep.h"
+#include "reflog-walk.h"
+#include "patch-ids.h"
+
+volatile show_early_output_fn_t show_early_output;
+
+static char *path_name(struct name_path *path, const char *name)
+{
+       struct name_path *p;
+       char *n, *m;
+       int nlen = strlen(name);
+       int len = nlen + 1;
+
+       for (p = path; p; p = p->up) {
+               if (p->elem_len)
+                       len += p->elem_len + 1;
+       }
+       n = xmalloc(len);
+       m = n + len - (nlen + 1);
+       strcpy(m, name);
+       for (p = path; p; p = p->up) {
+               if (p->elem_len) {
+                       m -= p->elem_len + 1;
+                       memcpy(m, p->elem, p->elem_len);
+                       m[p->elem_len] = '/';
+               }
+       }
+       return n;
+}
+
+void add_object(struct object *obj,
+               struct object_array *p,
+               struct name_path *path,
+               const char *name)
+{
+       add_object_array(obj, path_name(path, name), p);
+}
+
+static void mark_blob_uninteresting(struct blob *blob)
+{
+       if (blob->object.flags & UNINTERESTING)
+               return;
+       blob->object.flags |= UNINTERESTING;
+}
+
+void mark_tree_uninteresting(struct tree *tree)
+{
+       struct tree_desc desc;
+       struct name_entry entry;
+       struct object *obj = &tree->object;
+
+       if (obj->flags & UNINTERESTING)
+               return;
+       obj->flags |= UNINTERESTING;
+       if (!has_sha1_file(obj->sha1))
+               return;
+       if (parse_tree(tree) < 0)
+               die("bad tree %s", sha1_to_hex(obj->sha1));
+
+       init_tree_desc(&desc, tree->buffer, tree->size);
+       while (tree_entry(&desc, &entry)) {
+               switch (object_type(entry.mode)) {
+               case OBJ_TREE:
+                       mark_tree_uninteresting(lookup_tree(entry.sha1));
+                       break;
+               case OBJ_BLOB:
+                       mark_blob_uninteresting(lookup_blob(entry.sha1));
+                       break;
+               default:
+                       /* Subproject commit - not in this repository */
+                       break;
+               }
+       }
+
+       /*
+        * We don't care about the tree any more
+        * after it has been marked uninteresting.
+        */
+       free(tree->buffer);
+       tree->buffer = NULL;
+}
+
+void mark_parents_uninteresting(struct commit *commit)
+{
+       struct commit_list *parents = commit->parents;
+
+       while (parents) {
+               struct commit *commit = parents->item;
+               if (!(commit->object.flags & UNINTERESTING)) {
+                       commit->object.flags |= UNINTERESTING;
+
+                       /*
+                        * Normally we haven't parsed the parent
+                        * yet, so we won't have a parent of a parent
+                        * here. However, it may turn out that we've
+                        * reached this commit some other way (where it
+                        * wasn't uninteresting), in which case we need
+                        * to mark its parents recursively too..
+                        */
+                       if (commit->parents)
+                               mark_parents_uninteresting(commit);
+               }
+
+               /*
+                * A missing commit is ok iff its parent is marked
+                * uninteresting.
+                *
+                * We just mark such a thing parsed, so that when
+                * it is popped next time around, we won't be trying
+                * to parse it and get an error.
+                */
+               if (!has_sha1_file(commit->object.sha1))
+                       commit->object.parsed = 1;
+               parents = parents->next;
+       }
+}
+
+static void add_pending_object_with_mode(struct rev_info *revs, struct object *obj, const char *name, unsigned mode)
+{
+       if (revs->no_walk && (obj->flags & UNINTERESTING))
+               die("object ranges do not make sense when not walking revisions");
+       if (revs->reflog_info && obj->type == OBJ_COMMIT &&
+                       add_reflog_for_walk(revs->reflog_info,
+                               (struct commit *)obj, name))
+               return;
+       add_object_array_with_mode(obj, name, &revs->pending, mode);
+}
+
+void add_pending_object(struct rev_info *revs, struct object *obj, const char *name)
+{
+       add_pending_object_with_mode(revs, obj, name, S_IFINVALID);
+}
+
+static struct object *get_reference(struct rev_info *revs, const char *name, const unsigned char *sha1, unsigned int flags)
+{
+       struct object *object;
+
+       object = parse_object(sha1);
+       if (!object)
+               die("bad object %s", name);
+       object->flags |= flags;
+       return object;
+}
+
+static struct commit *handle_commit(struct rev_info *revs, struct object *object, const char *name)
+{
+       unsigned long flags = object->flags;
+
+       /*
+        * Tag object? Look what it points to..
+        */
+       while (object->type == OBJ_TAG) {
+               struct tag *tag = (struct tag *) object;
+               if (revs->tag_objects && !(flags & UNINTERESTING))
+                       add_pending_object(revs, object, tag->tag);
+               object = parse_object(tag->tagged->sha1);
+               if (!object)
+                       die("bad object %s", sha1_to_hex(tag->tagged->sha1));
+       }
+
+       /*
+        * Commit object? Just return it, we'll do all the complex
+        * reachability crud.
+        */
+       if (object->type == OBJ_COMMIT) {
+               struct commit *commit = (struct commit *)object;
+               if (parse_commit(commit) < 0)
+                       die("unable to parse commit %s", name);
+               if (flags & UNINTERESTING) {
+                       commit->object.flags |= UNINTERESTING;
+                       mark_parents_uninteresting(commit);
+                       revs->limited = 1;
+               }
+               return commit;
+       }
+
+       /*
+        * Tree object? Either mark it uniniteresting, or add it
+        * to the list of objects to look at later..
+        */
+       if (object->type == OBJ_TREE) {
+               struct tree *tree = (struct tree *)object;
+               if (!revs->tree_objects)
+                       return NULL;
+               if (flags & UNINTERESTING) {
+                       mark_tree_uninteresting(tree);
+                       return NULL;
+               }
+               add_pending_object(revs, object, "");
+               return NULL;
+       }
+
+       /*
+        * Blob object? You know the drill by now..
+        */
+       if (object->type == OBJ_BLOB) {
+               struct blob *blob = (struct blob *)object;
+               if (!revs->blob_objects)
+                       return NULL;
+               if (flags & UNINTERESTING) {
+                       mark_blob_uninteresting(blob);
+                       return NULL;
+               }
+               add_pending_object(revs, object, "");
+               return NULL;
+       }
+       die("%s is unknown object", name);
+}
+
+static int everybody_uninteresting(struct commit_list *orig)
+{
+       struct commit_list *list = orig;
+       while (list) {
+               struct commit *commit = list->item;
+               list = list->next;
+               if (commit->object.flags & UNINTERESTING)
+                       continue;
+               return 0;
+       }
+       return 1;
+}
+
+/*
+ * The goal is to get REV_TREE_NEW as the result only if the
+ * diff consists of all '+' (and no other changes), and
+ * REV_TREE_DIFFERENT otherwise (of course if the trees are
+ * the same we want REV_TREE_SAME).  That means that once we
+ * get to REV_TREE_DIFFERENT, we do not have to look any further.
+ */
+static int tree_difference = REV_TREE_SAME;
+
+static void file_add_remove(struct diff_options *options,
+                   int addremove, unsigned mode,
+                   const unsigned char *sha1,
+                   const char *base, const char *path)
+{
+       int diff = REV_TREE_DIFFERENT;
+
+       /*
+        * Is it an add of a new file? It means that the old tree
+        * didn't have it at all, so we will turn "REV_TREE_SAME" ->
+        * "REV_TREE_NEW", but leave any "REV_TREE_DIFFERENT" alone
+        * (and if it already was "REV_TREE_NEW", we'll keep it
+        * "REV_TREE_NEW" of course).
+        */
+       if (addremove == '+') {
+               diff = tree_difference;
+               if (diff != REV_TREE_SAME)
+                       return;
+               diff = REV_TREE_NEW;
+       }
+       tree_difference = diff;
+       if (tree_difference == REV_TREE_DIFFERENT)
+               DIFF_OPT_SET(options, HAS_CHANGES);
+}
+
+static void file_change(struct diff_options *options,
+                unsigned old_mode, unsigned new_mode,
+                const unsigned char *old_sha1,
+                const unsigned char *new_sha1,
+                const char *base, const char *path)
+{
+       tree_difference = REV_TREE_DIFFERENT;
+       DIFF_OPT_SET(options, HAS_CHANGES);
+}
+
+static int rev_compare_tree(struct rev_info *revs, struct tree *t1, struct tree *t2)
+{
+       if (!t1)
+               return REV_TREE_NEW;
+       if (!t2)
+               return REV_TREE_DIFFERENT;
+       tree_difference = REV_TREE_SAME;
+       DIFF_OPT_CLR(&revs->pruning, HAS_CHANGES);
+       if (diff_tree_sha1(t1->object.sha1, t2->object.sha1, "",
+                          &revs->pruning) < 0)
+               return REV_TREE_DIFFERENT;
+       return tree_difference;
+}
+
+static int rev_same_tree_as_empty(struct rev_info *revs, struct tree *t1)
+{
+       int retval;
+       void *tree;
+       unsigned long size;
+       struct tree_desc empty, real;
+
+       if (!t1)
+               return 0;
+
+       tree = read_object_with_reference(t1->object.sha1, tree_type, &size, NULL);
+       if (!tree)
+               return 0;
+       init_tree_desc(&real, tree, size);
+       init_tree_desc(&empty, "", 0);
+
+       tree_difference = REV_TREE_SAME;
+       DIFF_OPT_CLR(&revs->pruning, HAS_CHANGES);
+       retval = diff_tree(&empty, &real, "", &revs->pruning);
+       free(tree);
+
+       return retval >= 0 && (tree_difference == REV_TREE_SAME);
+}
+
+static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit)
+{
+       struct commit_list **pp, *parent;
+       int tree_changed = 0, tree_same = 0;
+
+       /*
+        * If we don't do pruning, everything is interesting
+        */
+       if (!revs->prune)
+               return;
+
+       if (!commit->tree)
+               return;
+
+       if (!commit->parents) {
+               if (rev_same_tree_as_empty(revs, commit->tree))
+                       commit->object.flags |= TREESAME;
+               return;
+       }
+
+       /*
+        * Normal non-merge commit? If we don't want to make the
+        * history dense, we consider it always to be a change..
+        */
+       if (!revs->dense && !commit->parents->next)
+               return;
+
+       pp = &commit->parents;
+       while ((parent = *pp) != NULL) {
+               struct commit *p = parent->item;
+
+               if (parse_commit(p) < 0)
+                       die("cannot simplify commit %s (because of %s)",
+                           sha1_to_hex(commit->object.sha1),
+                           sha1_to_hex(p->object.sha1));
+               switch (rev_compare_tree(revs, p->tree, commit->tree)) {
+               case REV_TREE_SAME:
+                       tree_same = 1;
+                       if (!revs->simplify_history || (p->object.flags & UNINTERESTING)) {
+                               /* Even if a merge with an uninteresting
+                                * side branch brought the entire change
+                                * we are interested in, we do not want
+                                * to lose the other branches of this
+                                * merge, so we just keep going.
+                                */
+                               pp = &parent->next;
+                               continue;
+                       }
+                       parent->next = NULL;
+                       commit->parents = parent;
+                       commit->object.flags |= TREESAME;
+                       return;
+
+               case REV_TREE_NEW:
+                       if (revs->remove_empty_trees &&
+                           rev_same_tree_as_empty(revs, p->tree)) {
+                               /* We are adding all the specified
+                                * paths from this parent, so the
+                                * history beyond this parent is not
+                                * interesting.  Remove its parents
+                                * (they are grandparents for us).
+                                * IOW, we pretend this parent is a
+                                * "root" commit.
+                                */
+                               if (parse_commit(p) < 0)
+                                       die("cannot simplify commit %s (invalid %s)",
+                                           sha1_to_hex(commit->object.sha1),
+                                           sha1_to_hex(p->object.sha1));
+                               p->parents = NULL;
+                       }
+               /* fallthrough */
+               case REV_TREE_DIFFERENT:
+                       tree_changed = 1;
+                       pp = &parent->next;
+                       continue;
+               }
+               die("bad tree compare for commit %s", sha1_to_hex(commit->object.sha1));
+       }
+       if (tree_changed && !tree_same)
+               return;
+       commit->object.flags |= TREESAME;
+}
+
+static int add_parents_to_list(struct rev_info *revs, struct commit *commit, struct commit_list **list)
+{
+       struct commit_list *parent = commit->parents;
+       unsigned left_flag;
+       int add, rest;
+
+       if (commit->object.flags & ADDED)
+               return 0;
+       commit->object.flags |= ADDED;
+
+       /*
+        * If the commit is uninteresting, don't try to
+        * prune parents - we want the maximal uninteresting
+        * set.
+        *
+        * Normally we haven't parsed the parent
+        * yet, so we won't have a parent of a parent
+        * here. However, it may turn out that we've
+        * reached this commit some other way (where it
+        * wasn't uninteresting), in which case we need
+        * to mark its parents recursively too..
+        */
+       if (commit->object.flags & UNINTERESTING) {
+               while (parent) {
+                       struct commit *p = parent->item;
+                       parent = parent->next;
+                       if (parse_commit(p) < 0)
+                               return -1;
+                       p->object.flags |= UNINTERESTING;
+                       if (p->parents)
+                               mark_parents_uninteresting(p);
+                       if (p->object.flags & SEEN)
+                               continue;
+                       p->object.flags |= SEEN;
+                       insert_by_date(p, list);
+               }
+               return 0;
+       }
+
+       /*
+        * Ok, the commit wasn't uninteresting. Try to
+        * simplify the commit history and find the parent
+        * that has no differences in the path set if one exists.
+        */
+       try_to_simplify_commit(revs, commit);
+
+       if (revs->no_walk)
+               return 0;
+
+       left_flag = (commit->object.flags & SYMMETRIC_LEFT);
+
+       rest = !revs->first_parent_only;
+       for (parent = commit->parents, add = 1; parent; add = rest) {
+               struct commit *p = parent->item;
+
+               parent = parent->next;
+               if (parse_commit(p) < 0)
+                       return -1;
+               p->object.flags |= left_flag;
+               if (p->object.flags & SEEN)
+                       continue;
+               p->object.flags |= SEEN;
+               if (add)
+                       insert_by_date(p, list);
+       }
+       return 0;
+}
+
+static void cherry_pick_list(struct commit_list *list, struct rev_info *revs)
+{
+       struct commit_list *p;
+       int left_count = 0, right_count = 0;
+       int left_first;
+       struct patch_ids ids;
+
+       /* First count the commits on the left and on the right */
+       for (p = list; p; p = p->next) {
+               struct commit *commit = p->item;
+               unsigned flags = commit->object.flags;
+               if (flags & BOUNDARY)
+                       ;
+               else if (flags & SYMMETRIC_LEFT)
+                       left_count++;
+               else
+                       right_count++;
+       }
+
+       left_first = left_count < right_count;
+       init_patch_ids(&ids);
+       if (revs->diffopt.nr_paths) {
+               ids.diffopts.nr_paths = revs->diffopt.nr_paths;
+               ids.diffopts.paths = revs->diffopt.paths;
+               ids.diffopts.pathlens = revs->diffopt.pathlens;
+       }
+
+       /* Compute patch-ids for one side */
+       for (p = list; p; p = p->next) {
+               struct commit *commit = p->item;
+               unsigned flags = commit->object.flags;
+
+               if (flags & BOUNDARY)
+                       continue;
+               /*
+                * If we have fewer left, left_first is set and we omit
+                * commits on the right branch in this loop.  If we have
+                * fewer right, we skip the left ones.
+                */
+               if (left_first != !!(flags & SYMMETRIC_LEFT))
+                       continue;
+               commit->util = add_commit_patch_id(commit, &ids);
+       }
+
+       /* Check the other side */
+       for (p = list; p; p = p->next) {
+               struct commit *commit = p->item;
+               struct patch_id *id;
+               unsigned flags = commit->object.flags;
+
+               if (flags & BOUNDARY)
+                       continue;
+               /*
+                * If we have fewer left, left_first is set and we omit
+                * commits on the left branch in this loop.
+                */
+               if (left_first == !!(flags & SYMMETRIC_LEFT))
+                       continue;
+
+               /*
+                * Have we seen the same patch id?
+                */
+               id = has_commit_patch_id(commit, &ids);
+               if (!id)
+                       continue;
+               id->seen = 1;
+               commit->object.flags |= SHOWN;
+       }
+
+       /* Now check the original side for seen ones */
+       for (p = list; p; p = p->next) {
+               struct commit *commit = p->item;
+               struct patch_id *ent;
+
+               ent = commit->util;
+               if (!ent)
+                       continue;
+               if (ent->seen)
+                       commit->object.flags |= SHOWN;
+               commit->util = NULL;
+       }
+
+       free_patch_ids(&ids);
+}
+
+static int limit_list(struct rev_info *revs)
+{
+       struct commit_list *list = revs->commits;
+       struct commit_list *newlist = NULL;
+       struct commit_list **p = &newlist;
+
+       while (list) {
+               struct commit_list *entry = list;
+               struct commit *commit = list->item;
+               struct object *obj = &commit->object;
+               show_early_output_fn_t show;
+
+               list = list->next;
+               free(entry);
+
+               if (revs->max_age != -1 && (commit->date < revs->max_age))
+                       obj->flags |= UNINTERESTING;
+               if (add_parents_to_list(revs, commit, &list) < 0)
+                       return -1;
+               if (obj->flags & UNINTERESTING) {
+                       mark_parents_uninteresting(commit);
+                       if (everybody_uninteresting(list))
+                               break;
+                       continue;
+               }
+               if (revs->min_age != -1 && (commit->date > revs->min_age))
+                       continue;
+               p = &commit_list_insert(commit, p)->next;
+
+               show = show_early_output;
+               if (!show)
+                       continue;
+
+               show(revs, newlist);
+               show_early_output = NULL;
+       }
+       if (revs->cherry_pick)
+               cherry_pick_list(newlist, revs);
+
+       revs->commits = newlist;
+       return 0;
+}
+
+struct all_refs_cb {
+       int all_flags;
+       int warned_bad_reflog;
+       struct rev_info *all_revs;
+       const char *name_for_errormsg;
+};
+
+static int handle_one_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct all_refs_cb *cb = cb_data;
+       struct object *object = get_reference(cb->all_revs, path, sha1,
+                                             cb->all_flags);
+       add_pending_object(cb->all_revs, object, path);
+       return 0;
+}
+
+static void handle_all(struct rev_info *revs, unsigned flags)
+{
+       struct all_refs_cb cb;
+       cb.all_revs = revs;
+       cb.all_flags = flags;
+       for_each_ref(handle_one_ref, &cb);
+}
+
+static void handle_one_reflog_commit(unsigned char *sha1, void *cb_data)
+{
+       struct all_refs_cb *cb = cb_data;
+       if (!is_null_sha1(sha1)) {
+               struct object *o = parse_object(sha1);
+               if (o) {
+                       o->flags |= cb->all_flags;
+                       add_pending_object(cb->all_revs, o, "");
+               }
+               else if (!cb->warned_bad_reflog) {
+                       warning("reflog of '%s' references pruned commits",
+                               cb->name_for_errormsg);
+                       cb->warned_bad_reflog = 1;
+               }
+       }
+}
+
+static int handle_one_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
+               const char *email, unsigned long timestamp, int tz,
+               const char *message, void *cb_data)
+{
+       handle_one_reflog_commit(osha1, cb_data);
+       handle_one_reflog_commit(nsha1, cb_data);
+       return 0;
+}
+
+static int handle_one_reflog(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct all_refs_cb *cb = cb_data;
+       cb->warned_bad_reflog = 0;
+       cb->name_for_errormsg = path;
+       for_each_reflog_ent(path, handle_one_reflog_ent, cb_data);
+       return 0;
+}
+
+static void handle_reflog(struct rev_info *revs, unsigned flags)
+{
+       struct all_refs_cb cb;
+       cb.all_revs = revs;
+       cb.all_flags = flags;
+       for_each_reflog(handle_one_reflog, &cb);
+}
+
+static int add_parents_only(struct rev_info *revs, const char *arg, int flags)
+{
+       unsigned char sha1[20];
+       struct object *it;
+       struct commit *commit;
+       struct commit_list *parents;
+
+       if (*arg == '^') {
+               flags ^= UNINTERESTING;
+               arg++;
+       }
+       if (get_sha1(arg, sha1))
+               return 0;
+       while (1) {
+               it = get_reference(revs, arg, sha1, 0);
+               if (it->type != OBJ_TAG)
+                       break;
+               hashcpy(sha1, ((struct tag*)it)->tagged->sha1);
+       }
+       if (it->type != OBJ_COMMIT)
+               return 0;
+       commit = (struct commit *)it;
+       for (parents = commit->parents; parents; parents = parents->next) {
+               it = &parents->item->object;
+               it->flags |= flags;
+               add_pending_object(revs, it, arg);
+       }
+       return 1;
+}
+
+void init_revisions(struct rev_info *revs, const char *prefix)
+{
+       memset(revs, 0, sizeof(*revs));
+
+       revs->abbrev = DEFAULT_ABBREV;
+       revs->ignore_merges = 1;
+       revs->simplify_history = 1;
+       DIFF_OPT_SET(&revs->pruning, RECURSIVE);
+       DIFF_OPT_SET(&revs->pruning, QUIET);
+       revs->pruning.add_remove = file_add_remove;
+       revs->pruning.change = file_change;
+       revs->lifo = 1;
+       revs->dense = 1;
+       revs->prefix = prefix;
+       revs->max_age = -1;
+       revs->min_age = -1;
+       revs->skip_count = -1;
+       revs->max_count = -1;
+
+       revs->commit_format = CMIT_FMT_DEFAULT;
+
+       diff_setup(&revs->diffopt);
+}
+
+static void add_pending_commit_list(struct rev_info *revs,
+                                    struct commit_list *commit_list,
+                                    unsigned int flags)
+{
+       while (commit_list) {
+               struct object *object = &commit_list->item->object;
+               object->flags |= flags;
+               add_pending_object(revs, object, sha1_to_hex(object->sha1));
+               commit_list = commit_list->next;
+       }
+}
+
+static void prepare_show_merge(struct rev_info *revs)
+{
+       struct commit_list *bases;
+       struct commit *head, *other;
+       unsigned char sha1[20];
+       const char **prune = NULL;
+       int i, prune_num = 1; /* counting terminating NULL */
+
+       if (get_sha1("HEAD", sha1) || !(head = lookup_commit(sha1)))
+               die("--merge without HEAD?");
+       if (get_sha1("MERGE_HEAD", sha1) || !(other = lookup_commit(sha1)))
+               die("--merge without MERGE_HEAD?");
+       add_pending_object(revs, &head->object, "HEAD");
+       add_pending_object(revs, &other->object, "MERGE_HEAD");
+       bases = get_merge_bases(head, other, 1);
+       while (bases) {
+               struct commit *it = bases->item;
+               struct commit_list *n = bases->next;
+               free(bases);
+               bases = n;
+               it->object.flags |= UNINTERESTING;
+               add_pending_object(revs, &it->object, "(merge-base)");
+       }
+
+       if (!active_nr)
+               read_cache();
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (!ce_stage(ce))
+                       continue;
+               if (ce_path_match(ce, revs->prune_data)) {
+                       prune_num++;
+                       prune = xrealloc(prune, sizeof(*prune) * prune_num);
+                       prune[prune_num-2] = ce->name;
+                       prune[prune_num-1] = NULL;
+               }
+               while ((i+1 < active_nr) &&
+                      ce_same_name(ce, active_cache[i+1]))
+                       i++;
+       }
+       revs->prune_data = prune;
+}
+
+int handle_revision_arg(const char *arg, struct rev_info *revs,
+                       int flags,
+                       int cant_be_filename)
+{
+       unsigned mode;
+       char *dotdot;
+       struct object *object;
+       unsigned char sha1[20];
+       int local_flags;
+
+       dotdot = strstr(arg, "..");
+       if (dotdot) {
+               unsigned char from_sha1[20];
+               const char *next = dotdot + 2;
+               const char *this = arg;
+               int symmetric = *next == '.';
+               unsigned int flags_exclude = flags ^ UNINTERESTING;
+
+               *dotdot = 0;
+               next += symmetric;
+
+               if (!*next)
+                       next = "HEAD";
+               if (dotdot == arg)
+                       this = "HEAD";
+               if (!get_sha1(this, from_sha1) &&
+                   !get_sha1(next, sha1)) {
+                       struct commit *a, *b;
+                       struct commit_list *exclude;
+
+                       a = lookup_commit_reference(from_sha1);
+                       b = lookup_commit_reference(sha1);
+                       if (!a || !b) {
+                               die(symmetric ?
+                                   "Invalid symmetric difference expression %s...%s" :
+                                   "Invalid revision range %s..%s",
+                                   arg, next);
+                       }
+
+                       if (!cant_be_filename) {
+                               *dotdot = '.';
+                               verify_non_filename(revs->prefix, arg);
+                       }
+
+                       if (symmetric) {
+                               exclude = get_merge_bases(a, b, 1);
+                               add_pending_commit_list(revs, exclude,
+                                                       flags_exclude);
+                               free_commit_list(exclude);
+                               a->object.flags |= flags | SYMMETRIC_LEFT;
+                       } else
+                               a->object.flags |= flags_exclude;
+                       b->object.flags |= flags;
+                       add_pending_object(revs, &a->object, this);
+                       add_pending_object(revs, &b->object, next);
+                       return 0;
+               }
+               *dotdot = '.';
+       }
+       dotdot = strstr(arg, "^@");
+       if (dotdot && !dotdot[2]) {
+               *dotdot = 0;
+               if (add_parents_only(revs, arg, flags))
+                       return 0;
+               *dotdot = '^';
+       }
+       dotdot = strstr(arg, "^!");
+       if (dotdot && !dotdot[2]) {
+               *dotdot = 0;
+               if (!add_parents_only(revs, arg, flags ^ UNINTERESTING))
+                       *dotdot = '^';
+       }
+
+       local_flags = 0;
+       if (*arg == '^') {
+               local_flags = UNINTERESTING;
+               arg++;
+       }
+       if (get_sha1_with_mode(arg, sha1, &mode))
+               return -1;
+       if (!cant_be_filename)
+               verify_non_filename(revs->prefix, arg);
+       object = get_reference(revs, arg, sha1, flags ^ local_flags);
+       add_pending_object_with_mode(revs, object, arg, mode);
+       return 0;
+}
+
+static void add_grep(struct rev_info *revs, const char *ptn, enum grep_pat_token what)
+{
+       if (!revs->grep_filter) {
+               struct grep_opt *opt = xcalloc(1, sizeof(*opt));
+               opt->status_only = 1;
+               opt->pattern_tail = &(opt->pattern_list);
+               opt->regflags = REG_NEWLINE;
+               revs->grep_filter = opt;
+       }
+       append_grep_pattern(revs->grep_filter, ptn,
+                           "command line", 0, what);
+}
+
+static void add_header_grep(struct rev_info *revs, const char *field, const char *pattern)
+{
+       char *pat;
+       const char *prefix;
+       int patlen, fldlen;
+
+       fldlen = strlen(field);
+       patlen = strlen(pattern);
+       pat = xmalloc(patlen + fldlen + 10);
+       prefix = ".*";
+       if (*pattern == '^') {
+               prefix = "";
+               pattern++;
+       }
+       sprintf(pat, "^%s %s%s", field, prefix, pattern);
+       add_grep(revs, pat, GREP_PATTERN_HEAD);
+}
+
+static void add_message_grep(struct rev_info *revs, const char *pattern)
+{
+       add_grep(revs, pattern, GREP_PATTERN_BODY);
+}
+
+static void add_ignore_packed(struct rev_info *revs, const char *name)
+{
+       int num = ++revs->num_ignore_packed;
+
+       revs->ignore_packed = xrealloc(revs->ignore_packed,
+                                      sizeof(const char **) * (num + 1));
+       revs->ignore_packed[num-1] = name;
+       revs->ignore_packed[num] = NULL;
+}
+
+/*
+ * Parse revision information, filling in the "rev_info" structure,
+ * and removing the used arguments from the argument list.
+ *
+ * Returns the number of arguments left that weren't recognized
+ * (which are also moved to the head of the argument list)
+ */
+int setup_revisions(int argc, const char **argv, struct rev_info *revs, const char *def)
+{
+       int i, flags, seen_dashdash, show_merge;
+       const char **unrecognized = argv + 1;
+       int left = 1;
+       int all_match = 0;
+       int regflags = 0;
+
+       /* First, search for "--" */
+       seen_dashdash = 0;
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (strcmp(arg, "--"))
+                       continue;
+               argv[i] = NULL;
+               argc = i;
+               if (argv[i + 1])
+                       revs->prune_data = get_pathspec(revs->prefix, argv + i + 1);
+               seen_dashdash = 1;
+               break;
+       }
+
+       flags = show_merge = 0;
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (*arg == '-') {
+                       int opts;
+                       if (!prefixcmp(arg, "--max-count=")) {
+                               revs->max_count = atoi(arg + 12);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--skip=")) {
+                               revs->skip_count = atoi(arg + 7);
+                               continue;
+                       }
+                       /* accept -<digit>, like traditional "head" */
+                       if ((*arg == '-') && isdigit(arg[1])) {
+                               revs->max_count = atoi(arg + 1);
+                               continue;
+                       }
+                       if (!strcmp(arg, "-n")) {
+                               if (argc <= i + 1)
+                                       die("-n requires an argument");
+                               revs->max_count = atoi(argv[++i]);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "-n")) {
+                               revs->max_count = atoi(arg + 2);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--max-age=")) {
+                               revs->max_age = atoi(arg + 10);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--since=")) {
+                               revs->max_age = approxidate(arg + 8);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--after=")) {
+                               revs->max_age = approxidate(arg + 8);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--min-age=")) {
+                               revs->min_age = atoi(arg + 10);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--before=")) {
+                               revs->min_age = approxidate(arg + 9);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--until=")) {
+                               revs->min_age = approxidate(arg + 8);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--all")) {
+                               handle_all(revs, flags);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--first-parent")) {
+                               revs->first_parent_only = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--reflog")) {
+                               handle_reflog(revs, flags);
+                               continue;
+                       }
+                       if (!strcmp(arg, "-g") ||
+                                       !strcmp(arg, "--walk-reflogs")) {
+                               init_reflog_walk(&revs->reflog_info);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--not")) {
+                               flags ^= UNINTERESTING;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--default")) {
+                               if (++i >= argc)
+                                       die("bad --default argument");
+                               def = argv[i];
+                               continue;
+                       }
+                       if (!strcmp(arg, "--merge")) {
+                               show_merge = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--topo-order")) {
+                               revs->topo_order = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--date-order")) {
+                               revs->lifo = 0;
+                               revs->topo_order = 1;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--early-output")) {
+                               int count = 100;
+                               switch (arg[14]) {
+                               case '=':
+                                       count = atoi(arg+15);
+                                       /* Fallthrough */
+                               case 0:
+                                       revs->topo_order = 1;
+                                       revs->early_output = count;
+                                       continue;
+                               }
+                       }
+                       if (!strcmp(arg, "--parents")) {
+                               revs->parents = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--dense")) {
+                               revs->dense = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--sparse")) {
+                               revs->dense = 0;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--remove-empty")) {
+                               revs->remove_empty_trees = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--no-merges")) {
+                               revs->no_merges = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--boundary")) {
+                               revs->boundary = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--left-right")) {
+                               revs->left_right = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--cherry-pick")) {
+                               revs->cherry_pick = 1;
+                               revs->limited = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--objects")) {
+                               revs->tag_objects = 1;
+                               revs->tree_objects = 1;
+                               revs->blob_objects = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--objects-edge")) {
+                               revs->tag_objects = 1;
+                               revs->tree_objects = 1;
+                               revs->blob_objects = 1;
+                               revs->edge_hint = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--unpacked")) {
+                               revs->unpacked = 1;
+                               free(revs->ignore_packed);
+                               revs->ignore_packed = NULL;
+                               revs->num_ignore_packed = 0;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--unpacked=")) {
+                               revs->unpacked = 1;
+                               add_ignore_packed(revs, arg+11);
+                               continue;
+                       }
+                       if (!strcmp(arg, "-r")) {
+                               revs->diff = 1;
+                               DIFF_OPT_SET(&revs->diffopt, RECURSIVE);
+                               continue;
+                       }
+                       if (!strcmp(arg, "-t")) {
+                               revs->diff = 1;
+                               DIFF_OPT_SET(&revs->diffopt, RECURSIVE);
+                               DIFF_OPT_SET(&revs->diffopt, TREE_IN_RECURSIVE);
+                               continue;
+                       }
+                       if (!strcmp(arg, "-m")) {
+                               revs->ignore_merges = 0;
+                               continue;
+                       }
+                       if (!strcmp(arg, "-c")) {
+                               revs->diff = 1;
+                               revs->dense_combined_merges = 0;
+                               revs->combine_merges = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--cc")) {
+                               revs->diff = 1;
+                               revs->dense_combined_merges = 1;
+                               revs->combine_merges = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "-v")) {
+                               revs->verbose_header = 1;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--pretty")) {
+                               revs->verbose_header = 1;
+                               revs->commit_format = get_commit_format(arg+8);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--root")) {
+                               revs->show_root_diff = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--no-commit-id")) {
+                               revs->no_commit_id = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--always")) {
+                               revs->always_show_header = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--no-abbrev")) {
+                               revs->abbrev = 0;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--abbrev")) {
+                               revs->abbrev = DEFAULT_ABBREV;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--abbrev=")) {
+                               revs->abbrev = strtoul(arg + 9, NULL, 10);
+                               if (revs->abbrev < MINIMUM_ABBREV)
+                                       revs->abbrev = MINIMUM_ABBREV;
+                               else if (revs->abbrev > 40)
+                                       revs->abbrev = 40;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--abbrev-commit")) {
+                               revs->abbrev_commit = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--full-diff")) {
+                               revs->diff = 1;
+                               revs->full_diff = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--full-history")) {
+                               revs->simplify_history = 0;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--relative-date")) {
+                               revs->date_mode = DATE_RELATIVE;
+                               continue;
+                       }
+                       if (!strncmp(arg, "--date=", 7)) {
+                               revs->date_mode = parse_date_format(arg + 7);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--log-size")) {
+                               revs->show_log_size = 1;
+                               continue;
+                       }
+
+                       /*
+                        * Grepping the commit log
+                        */
+                       if (!prefixcmp(arg, "--author=")) {
+                               add_header_grep(revs, "author", arg+9);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--committer=")) {
+                               add_header_grep(revs, "committer", arg+12);
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--grep=")) {
+                               add_message_grep(revs, arg+7);
+                               continue;
+                       }
+                       if (!strcmp(arg, "--extended-regexp") ||
+                           !strcmp(arg, "-E")) {
+                               regflags |= REG_EXTENDED;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--regexp-ignore-case") ||
+                           !strcmp(arg, "-i")) {
+                               regflags |= REG_ICASE;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--all-match")) {
+                               all_match = 1;
+                               continue;
+                       }
+                       if (!prefixcmp(arg, "--encoding=")) {
+                               arg += 11;
+                               if (strcmp(arg, "none"))
+                                       git_log_output_encoding = xstrdup(arg);
+                               else
+                                       git_log_output_encoding = "";
+                               continue;
+                       }
+                       if (!strcmp(arg, "--reverse")) {
+                               revs->reverse ^= 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--no-walk")) {
+                               revs->no_walk = 1;
+                               continue;
+                       }
+                       if (!strcmp(arg, "--do-walk")) {
+                               revs->no_walk = 0;
+                               continue;
+                       }
+
+                       opts = diff_opt_parse(&revs->diffopt, argv+i, argc-i);
+                       if (opts > 0) {
+                               i += opts - 1;
+                               continue;
+                       }
+                       *unrecognized++ = arg;
+                       left++;
+                       continue;
+               }
+
+               if (handle_revision_arg(arg, revs, flags, seen_dashdash)) {
+                       int j;
+                       if (seen_dashdash || *arg == '^')
+                               die("bad revision '%s'", arg);
+
+                       /* If we didn't have a "--":
+                        * (1) all filenames must exist;
+                        * (2) all rev-args must not be interpretable
+                        *     as a valid filename.
+                        * but the latter we have checked in the main loop.
+                        */
+                       for (j = i; j < argc; j++)
+                               verify_filename(revs->prefix, argv[j]);
+
+                       revs->prune_data = get_pathspec(revs->prefix,
+                                                       argv + i);
+                       break;
+               }
+       }
+
+       if (revs->grep_filter)
+               revs->grep_filter->regflags |= regflags;
+
+       if (show_merge)
+               prepare_show_merge(revs);
+       if (def && !revs->pending.nr) {
+               unsigned char sha1[20];
+               struct object *object;
+               unsigned mode;
+               if (get_sha1_with_mode(def, sha1, &mode))
+                       die("bad default revision '%s'", def);
+               object = get_reference(revs, def, sha1, 0);
+               add_pending_object_with_mode(revs, object, def, mode);
+       }
+
+       /* Did the user ask for any diff output? Run the diff! */
+       if (revs->diffopt.output_format & ~DIFF_FORMAT_NO_OUTPUT)
+               revs->diff = 1;
+
+       /* Pickaxe and rename following needs diffs */
+       if (revs->diffopt.pickaxe || DIFF_OPT_TST(&revs->diffopt, FOLLOW_RENAMES))
+               revs->diff = 1;
+
+       if (revs->topo_order)
+               revs->limited = 1;
+
+       if (revs->prune_data) {
+               diff_tree_setup_paths(revs->prune_data, &revs->pruning);
+               /* Can't prune commits with rename following: the paths change.. */
+               if (!DIFF_OPT_TST(&revs->diffopt, FOLLOW_RENAMES))
+                       revs->prune = 1;
+               if (!revs->full_diff)
+                       diff_tree_setup_paths(revs->prune_data, &revs->diffopt);
+       }
+       if (revs->combine_merges) {
+               revs->ignore_merges = 0;
+               if (revs->dense_combined_merges && !revs->diffopt.output_format)
+                       revs->diffopt.output_format = DIFF_FORMAT_PATCH;
+       }
+       revs->diffopt.abbrev = revs->abbrev;
+       if (diff_setup_done(&revs->diffopt) < 0)
+               die("diff_setup_done failed");
+
+       if (revs->grep_filter) {
+               revs->grep_filter->all_match = all_match;
+               compile_grep_patterns(revs->grep_filter);
+       }
+
+       if (revs->reverse && revs->reflog_info)
+               die("cannot combine --reverse with --walk-reflogs");
+
+       return left;
+}
+
+int prepare_revision_walk(struct rev_info *revs)
+{
+       int nr = revs->pending.nr;
+       struct object_array_entry *e, *list;
+
+       e = list = revs->pending.objects;
+       revs->pending.nr = 0;
+       revs->pending.alloc = 0;
+       revs->pending.objects = NULL;
+       while (--nr >= 0) {
+               struct commit *commit = handle_commit(revs, e->item, e->name);
+               if (commit) {
+                       if (!(commit->object.flags & SEEN)) {
+                               commit->object.flags |= SEEN;
+                               insert_by_date(commit, &revs->commits);
+                       }
+               }
+               e++;
+       }
+       free(list);
+
+       if (revs->no_walk)
+               return 0;
+       if (revs->limited)
+               if (limit_list(revs) < 0)
+                       return -1;
+       if (revs->topo_order)
+               sort_in_topological_order(&revs->commits, revs->lifo);
+       return 0;
+}
+
+enum rewrite_result {
+       rewrite_one_ok,
+       rewrite_one_noparents,
+       rewrite_one_error,
+};
+
+static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp)
+{
+       for (;;) {
+               struct commit *p = *pp;
+               if (!revs->limited)
+                       if (add_parents_to_list(revs, p, &revs->commits) < 0)
+                               return rewrite_one_error;
+               if (p->parents && p->parents->next)
+                       return rewrite_one_ok;
+               if (p->object.flags & UNINTERESTING)
+                       return rewrite_one_ok;
+               if (!(p->object.flags & TREESAME))
+                       return rewrite_one_ok;
+               if (!p->parents)
+                       return rewrite_one_noparents;
+               *pp = p->parents->item;
+       }
+}
+
+static void remove_duplicate_parents(struct commit *commit)
+{
+       struct commit_list **pp, *p;
+
+       /* Examine existing parents while marking ones we have seen... */
+       pp = &commit->parents;
+       while ((p = *pp) != NULL) {
+               struct commit *parent = p->item;
+               if (parent->object.flags & TMP_MARK) {
+                       *pp = p->next;
+                       continue;
+               }
+               parent->object.flags |= TMP_MARK;
+               pp = &p->next;
+       }
+       /* ... and clear the temporary mark */
+       for (p = commit->parents; p; p = p->next)
+               p->item->object.flags &= ~TMP_MARK;
+}
+
+static int rewrite_parents(struct rev_info *revs, struct commit *commit)
+{
+       struct commit_list **pp = &commit->parents;
+       while (*pp) {
+               struct commit_list *parent = *pp;
+               switch (rewrite_one(revs, &parent->item)) {
+               case rewrite_one_ok:
+                       break;
+               case rewrite_one_noparents:
+                       *pp = parent->next;
+                       continue;
+               case rewrite_one_error:
+                       return -1;
+               }
+               pp = &parent->next;
+       }
+       remove_duplicate_parents(commit);
+       return 0;
+}
+
+static int commit_match(struct commit *commit, struct rev_info *opt)
+{
+       if (!opt->grep_filter)
+               return 1;
+       return grep_buffer(opt->grep_filter,
+                          NULL, /* we say nothing, not even filename */
+                          commit->buffer, strlen(commit->buffer));
+}
+
+enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit)
+{
+       if (commit->object.flags & SHOWN)
+               return commit_ignore;
+       if (revs->unpacked && has_sha1_pack(commit->object.sha1, revs->ignore_packed))
+               return commit_ignore;
+       if (commit->object.flags & UNINTERESTING)
+               return commit_ignore;
+       if (revs->min_age != -1 && (commit->date > revs->min_age))
+               return commit_ignore;
+       if (revs->no_merges && commit->parents && commit->parents->next)
+               return commit_ignore;
+       if (!commit_match(commit, revs))
+               return commit_ignore;
+       if (revs->prune && revs->dense) {
+               /* Commit without changes? */
+               if (commit->object.flags & TREESAME) {
+                       /* drop merges unless we want parenthood */
+                       if (!revs->parents)
+                               return commit_ignore;
+                       /* non-merge - always ignore it */
+                       if (!commit->parents || !commit->parents->next)
+                               return commit_ignore;
+               }
+               if (revs->parents && rewrite_parents(revs, commit) < 0)
+                       return commit_error;
+       }
+       return commit_show;
+}
+
+static struct commit *get_revision_1(struct rev_info *revs)
+{
+       if (!revs->commits)
+               return NULL;
+
+       do {
+               struct commit_list *entry = revs->commits;
+               struct commit *commit = entry->item;
+
+               revs->commits = entry->next;
+               free(entry);
+
+               if (revs->reflog_info)
+                       fake_reflog_parent(revs->reflog_info, commit);
+
+               /*
+                * If we haven't done the list limiting, we need to look at
+                * the parents here. We also need to do the date-based limiting
+                * that we'd otherwise have done in limit_list().
+                */
+               if (!revs->limited) {
+                       if (revs->max_age != -1 &&
+                           (commit->date < revs->max_age))
+                               continue;
+                       if (add_parents_to_list(revs, commit, &revs->commits) < 0)
+                               return NULL;
+               }
+
+               switch (simplify_commit(revs, commit)) {
+               case commit_ignore:
+                       continue;
+               case commit_error:
+                       return NULL;
+               default:
+                       return commit;
+               }
+       } while (revs->commits);
+       return NULL;
+}
+
+static void gc_boundary(struct object_array *array)
+{
+       unsigned nr = array->nr;
+       unsigned alloc = array->alloc;
+       struct object_array_entry *objects = array->objects;
+
+       if (alloc <= nr) {
+               unsigned i, j;
+               for (i = j = 0; i < nr; i++) {
+                       if (objects[i].item->flags & SHOWN)
+                               continue;
+                       if (i != j)
+                               objects[j] = objects[i];
+                       j++;
+               }
+               for (i = j; i < nr; i++)
+                       objects[i].item = NULL;
+               array->nr = j;
+       }
+}
+
+struct commit *get_revision(struct rev_info *revs)
+{
+       struct commit *c = NULL;
+       struct commit_list *l;
+
+       if (revs->boundary == 2) {
+               unsigned i;
+               struct object_array *array = &revs->boundary_commits;
+               struct object_array_entry *objects = array->objects;
+               for (i = 0; i < array->nr; i++) {
+                       c = (struct commit *)(objects[i].item);
+                       if (!c)
+                               continue;
+                       if (!(c->object.flags & CHILD_SHOWN))
+                               continue;
+                       if (!(c->object.flags & SHOWN))
+                               break;
+               }
+               if (array->nr <= i)
+                       return NULL;
+
+               c->object.flags |= SHOWN | BOUNDARY;
+               return c;
+       }
+
+       if (revs->reverse) {
+               int limit = -1;
+
+               if (0 <= revs->max_count) {
+                       limit = revs->max_count;
+                       if (0 < revs->skip_count)
+                               limit += revs->skip_count;
+               }
+               l = NULL;
+               while ((c = get_revision_1(revs))) {
+                       commit_list_insert(c, &l);
+                       if ((0 < limit) && !--limit)
+                               break;
+               }
+               revs->commits = l;
+               revs->reverse = 0;
+               revs->max_count = -1;
+               c = NULL;
+       }
+
+       /*
+        * Now pick up what they want to give us
+        */
+       c = get_revision_1(revs);
+       if (c) {
+               while (0 < revs->skip_count) {
+                       revs->skip_count--;
+                       c = get_revision_1(revs);
+                       if (!c)
+                               break;
+               }
+       }
+
+       /*
+        * Check the max_count.
+        */
+       switch (revs->max_count) {
+       case -1:
+               break;
+       case 0:
+               c = NULL;
+               break;
+       default:
+               revs->max_count--;
+       }
+
+       if (c)
+               c->object.flags |= SHOWN;
+
+       if (!revs->boundary) {
+               return c;
+       }
+
+       if (!c) {
+               /*
+                * get_revision_1() runs out the commits, and
+                * we are done computing the boundaries.
+                * switch to boundary commits output mode.
+                */
+               revs->boundary = 2;
+               return get_revision(revs);
+       }
+
+       /*
+        * boundary commits are the commits that are parents of the
+        * ones we got from get_revision_1() but they themselves are
+        * not returned from get_revision_1().  Before returning
+        * 'c', we need to mark its parents that they could be boundaries.
+        */
+
+       for (l = c->parents; l; l = l->next) {
+               struct object *p;
+               p = &(l->item->object);
+               if (p->flags & (CHILD_SHOWN | SHOWN))
+                       continue;
+               p->flags |= CHILD_SHOWN;
+               gc_boundary(&revs->boundary_commits);
+               add_object_array(p, NULL, &revs->boundary_commits);
+       }
+
+       return c;
+}
diff --git a/revision.h b/revision.h
new file mode 100644 (file)
index 0000000..992e1e9
--- /dev/null
@@ -0,0 +1,141 @@
+#ifndef REVISION_H
+#define REVISION_H
+
+#define SEEN           (1u<<0)
+#define UNINTERESTING   (1u<<1)
+#define TREESAME       (1u<<2)
+#define SHOWN          (1u<<3)
+#define TMP_MARK       (1u<<4) /* for isolated cases; clean after use */
+#define BOUNDARY       (1u<<5)
+#define CHILD_SHOWN    (1u<<6)
+#define ADDED          (1u<<7) /* Parents already parsed and added? */
+#define SYMMETRIC_LEFT (1u<<8)
+#define TOPOSORT       (1u<<9) /* In the active toposort list.. */
+
+struct rev_info;
+struct log_info;
+
+struct rev_info {
+       /* Starting list */
+       struct commit_list *commits;
+       struct object_array pending;
+
+       /* Parents of shown commits */
+       struct object_array boundary_commits;
+
+       /* Basic information */
+       const char *prefix;
+       void *prune_data;
+       unsigned int early_output;
+
+       /* Traversal flags */
+       unsigned int    dense:1,
+                       prune:1,
+                       no_merges:1,
+                       no_walk:1,
+                       remove_empty_trees:1,
+                       simplify_history:1,
+                       lifo:1,
+                       topo_order:1,
+                       tag_objects:1,
+                       tree_objects:1,
+                       blob_objects:1,
+                       edge_hint:1,
+                       limited:1,
+                       unpacked:1, /* see also ignore_packed below */
+                       boundary:2,
+                       left_right:1,
+                       parents:1,
+                       reverse:1,
+                       cherry_pick:1,
+                       first_parent_only:1;
+
+       /* Diff flags */
+       unsigned int    diff:1,
+                       full_diff:1,
+                       show_root_diff:1,
+                       no_commit_id:1,
+                       verbose_header:1,
+                       ignore_merges:1,
+                       combine_merges:1,
+                       dense_combined_merges:1,
+                       always_show_header:1;
+
+       /* Format info */
+       unsigned int    shown_one:1,
+                       abbrev_commit:1;
+       enum date_mode date_mode;
+
+       const char **ignore_packed; /* pretend objects in these are unpacked */
+       int num_ignore_packed;
+
+       unsigned int    abbrev;
+       enum cmit_fmt   commit_format;
+       struct log_info *loginfo;
+       int             nr, total;
+       const char      *mime_boundary;
+       const char      *message_id;
+       const char      *ref_message_id;
+       const char      *add_signoff;
+       const char      *extra_headers;
+       const char      *log_reencode;
+       const char      *subject_prefix;
+       int             no_inline;
+       int             show_log_size;
+
+       /* Filter by commit log message */
+       struct grep_opt *grep_filter;
+
+       /* special limits */
+       int skip_count;
+       int max_count;
+       unsigned long max_age;
+       unsigned long min_age;
+
+       /* diff info for patches and for paths limiting */
+       struct diff_options diffopt;
+       struct diff_options pruning;
+
+       struct reflog_walk_info *reflog_info;
+};
+
+#define REV_TREE_SAME          0
+#define REV_TREE_NEW           1
+#define REV_TREE_DIFFERENT     2
+
+/* revision.c */
+typedef void (*show_early_output_fn_t)(struct rev_info *, struct commit_list *);
+volatile show_early_output_fn_t show_early_output;
+
+extern void init_revisions(struct rev_info *revs, const char *prefix);
+extern int setup_revisions(int argc, const char **argv, struct rev_info *revs, const char *def);
+extern int handle_revision_arg(const char *arg, struct rev_info *revs,int flags,int cant_be_filename);
+
+extern int prepare_revision_walk(struct rev_info *revs);
+extern struct commit *get_revision(struct rev_info *revs);
+
+extern void mark_parents_uninteresting(struct commit *commit);
+extern void mark_tree_uninteresting(struct tree *tree);
+
+struct name_path {
+       struct name_path *up;
+       int elem_len;
+       const char *elem;
+};
+
+extern void add_object(struct object *obj,
+                      struct object_array *p,
+                      struct name_path *path,
+                      const char *name);
+
+extern void add_pending_object(struct rev_info *revs, struct object *obj, const char *name);
+
+enum commit_action {
+       commit_ignore,
+       commit_show,
+       commit_error
+};
+
+extern enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit);
+
+#endif
diff --git a/run-command.c b/run-command.c
new file mode 100644 (file)
index 0000000..476d00c
--- /dev/null
@@ -0,0 +1,239 @@
+#include "cache.h"
+#include "run-command.h"
+#include "exec_cmd.h"
+
+static inline void close_pair(int fd[2])
+{
+       close(fd[0]);
+       close(fd[1]);
+}
+
+static inline void dup_devnull(int to)
+{
+       int fd = open("/dev/null", O_RDWR);
+       dup2(fd, to);
+       close(fd);
+}
+
+int start_command(struct child_process *cmd)
+{
+       int need_in, need_out, need_err;
+       int fdin[2], fdout[2], fderr[2];
+
+       need_in = !cmd->no_stdin && cmd->in < 0;
+       if (need_in) {
+               if (pipe(fdin) < 0)
+                       return -ERR_RUN_COMMAND_PIPE;
+               cmd->in = fdin[1];
+               cmd->close_in = 1;
+       }
+
+       need_out = !cmd->no_stdout
+               && !cmd->stdout_to_stderr
+               && cmd->out < 0;
+       if (need_out) {
+               if (pipe(fdout) < 0) {
+                       if (need_in)
+                               close_pair(fdin);
+                       return -ERR_RUN_COMMAND_PIPE;
+               }
+               cmd->out = fdout[0];
+               cmd->close_out = 1;
+       }
+
+       need_err = !cmd->no_stderr && cmd->err < 0;
+       if (need_err) {
+               if (pipe(fderr) < 0) {
+                       if (need_in)
+                               close_pair(fdin);
+                       if (need_out)
+                               close_pair(fdout);
+                       return -ERR_RUN_COMMAND_PIPE;
+               }
+               cmd->err = fderr[0];
+       }
+
+       cmd->pid = fork();
+       if (cmd->pid < 0) {
+               if (need_in)
+                       close_pair(fdin);
+               if (need_out)
+                       close_pair(fdout);
+               if (need_err)
+                       close_pair(fderr);
+               return -ERR_RUN_COMMAND_FORK;
+       }
+
+       if (!cmd->pid) {
+               if (cmd->no_stdin)
+                       dup_devnull(0);
+               else if (need_in) {
+                       dup2(fdin[0], 0);
+                       close_pair(fdin);
+               } else if (cmd->in) {
+                       dup2(cmd->in, 0);
+                       close(cmd->in);
+               }
+
+               if (cmd->no_stdout)
+                       dup_devnull(1);
+               else if (cmd->stdout_to_stderr)
+                       dup2(2, 1);
+               else if (need_out) {
+                       dup2(fdout[1], 1);
+                       close_pair(fdout);
+               } else if (cmd->out > 1) {
+                       dup2(cmd->out, 1);
+                       close(cmd->out);
+               }
+
+               if (cmd->no_stderr)
+                       dup_devnull(2);
+               else if (need_err) {
+                       dup2(fderr[1], 2);
+                       close_pair(fderr);
+               }
+
+               if (cmd->dir && chdir(cmd->dir))
+                       die("exec %s: cd to %s failed (%s)", cmd->argv[0],
+                           cmd->dir, strerror(errno));
+               if (cmd->env) {
+                       for (; *cmd->env; cmd->env++) {
+                               if (strchr(*cmd->env, '='))
+                                       putenv((char*)*cmd->env);
+                               else
+                                       unsetenv(*cmd->env);
+                       }
+               }
+               if (cmd->git_cmd) {
+                       execv_git_cmd(cmd->argv);
+               } else {
+                       execvp(cmd->argv[0], (char *const*) cmd->argv);
+               }
+               die("exec %s failed.", cmd->argv[0]);
+       }
+
+       if (need_in)
+               close(fdin[0]);
+       else if (cmd->in)
+               close(cmd->in);
+
+       if (need_out)
+               close(fdout[1]);
+       else if (cmd->out > 1)
+               close(cmd->out);
+
+       if (need_err)
+               close(fderr[1]);
+
+       return 0;
+}
+
+static int wait_or_whine(pid_t pid)
+{
+       for (;;) {
+               int status, code;
+               pid_t waiting = waitpid(pid, &status, 0);
+
+               if (waiting < 0) {
+                       if (errno == EINTR)
+                               continue;
+                       error("waitpid failed (%s)", strerror(errno));
+                       return -ERR_RUN_COMMAND_WAITPID;
+               }
+               if (waiting != pid)
+                       return -ERR_RUN_COMMAND_WAITPID_WRONG_PID;
+               if (WIFSIGNALED(status))
+                       return -ERR_RUN_COMMAND_WAITPID_SIGNAL;
+
+               if (!WIFEXITED(status))
+                       return -ERR_RUN_COMMAND_WAITPID_NOEXIT;
+               code = WEXITSTATUS(status);
+               if (code)
+                       return -code;
+               return 0;
+       }
+}
+
+int finish_command(struct child_process *cmd)
+{
+       if (cmd->close_in)
+               close(cmd->in);
+       if (cmd->close_out)
+               close(cmd->out);
+       return wait_or_whine(cmd->pid);
+}
+
+int run_command(struct child_process *cmd)
+{
+       int code = start_command(cmd);
+       if (code)
+               return code;
+       return finish_command(cmd);
+}
+
+static void prepare_run_command_v_opt(struct child_process *cmd,
+                                     const char **argv,
+                                     int opt)
+{
+       memset(cmd, 0, sizeof(*cmd));
+       cmd->argv = argv;
+       cmd->no_stdin = opt & RUN_COMMAND_NO_STDIN ? 1 : 0;
+       cmd->git_cmd = opt & RUN_GIT_CMD ? 1 : 0;
+       cmd->stdout_to_stderr = opt & RUN_COMMAND_STDOUT_TO_STDERR ? 1 : 0;
+}
+
+int run_command_v_opt(const char **argv, int opt)
+{
+       struct child_process cmd;
+       prepare_run_command_v_opt(&cmd, argv, opt);
+       return run_command(&cmd);
+}
+
+int run_command_v_opt_cd(const char **argv, int opt, const char *dir)
+{
+       struct child_process cmd;
+       prepare_run_command_v_opt(&cmd, argv, opt);
+       cmd.dir = dir;
+       return run_command(&cmd);
+}
+
+int run_command_v_opt_cd_env(const char **argv, int opt, const char *dir, const char *const *env)
+{
+       struct child_process cmd;
+       prepare_run_command_v_opt(&cmd, argv, opt);
+       cmd.dir = dir;
+       cmd.env = env;
+       return run_command(&cmd);
+}
+
+int start_async(struct async *async)
+{
+       int pipe_out[2];
+
+       if (pipe(pipe_out) < 0)
+               return error("cannot create pipe: %s", strerror(errno));
+
+       async->pid = fork();
+       if (async->pid < 0) {
+               error("fork (async) failed: %s", strerror(errno));
+               close_pair(pipe_out);
+               return -1;
+       }
+       if (!async->pid) {
+               close(pipe_out[0]);
+               exit(!!async->proc(pipe_out[1], async->data));
+       }
+       async->out = pipe_out[0];
+       close(pipe_out[1]);
+       return 0;
+}
+
+int finish_async(struct async *async)
+{
+       int ret = 0;
+
+       if (wait_or_whine(async->pid))
+               ret = error("waitpid (async) failed");
+       return ret;
+}
diff --git a/run-command.h b/run-command.h
new file mode 100644 (file)
index 0000000..1fc781d
--- /dev/null
@@ -0,0 +1,69 @@
+#ifndef RUN_COMMAND_H
+#define RUN_COMMAND_H
+
+enum {
+       ERR_RUN_COMMAND_FORK = 10000,
+       ERR_RUN_COMMAND_EXEC,
+       ERR_RUN_COMMAND_PIPE,
+       ERR_RUN_COMMAND_WAITPID,
+       ERR_RUN_COMMAND_WAITPID_WRONG_PID,
+       ERR_RUN_COMMAND_WAITPID_SIGNAL,
+       ERR_RUN_COMMAND_WAITPID_NOEXIT,
+};
+
+struct child_process {
+       const char **argv;
+       pid_t pid;
+       int in;
+       int out;
+       int err;
+       const char *dir;
+       const char *const *env;
+       unsigned close_in:1;
+       unsigned close_out:1;
+       unsigned no_stdin:1;
+       unsigned no_stdout:1;
+       unsigned no_stderr:1;
+       unsigned git_cmd:1; /* if this is to be git sub-command */
+       unsigned stdout_to_stderr:1;
+};
+
+int start_command(struct child_process *);
+int finish_command(struct child_process *);
+int run_command(struct child_process *);
+
+#define RUN_COMMAND_NO_STDIN 1
+#define RUN_GIT_CMD         2  /*If this is to be git sub-command */
+#define RUN_COMMAND_STDOUT_TO_STDERR 4
+int run_command_v_opt(const char **argv, int opt);
+int run_command_v_opt_cd(const char **argv, int opt, const char *dir);
+
+/*
+ * env (the environment) is to be formatted like environ: "VAR=VALUE".
+ * To unset an environment variable use just "VAR".
+ */
+int run_command_v_opt_cd_env(const char **argv, int opt, const char *dir, const char *const *env);
+
+/*
+ * The purpose of the following functions is to feed a pipe by running
+ * a function asynchronously and providing output that the caller reads.
+ *
+ * It is expected that no synchronization and mutual exclusion between
+ * the caller and the feed function is necessary so that the function
+ * can run in a thread without interfering with the caller.
+ */
+struct async {
+       /*
+        * proc writes to fd and closes it;
+        * returns 0 on success, non-zero on failure
+        */
+       int (*proc)(int fd, void *data);
+       void *data;
+       int out;        /* caller reads from here and closes it */
+       pid_t pid;
+};
+
+int start_async(struct async *async);
+int finish_async(struct async *async);
+
+#endif
diff --git a/send-pack.h b/send-pack.h
new file mode 100644 (file)
index 0000000..8ff1dc3
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef SEND_PACK_H
+#define SEND_PACK_H
+
+struct send_pack_args {
+       const char *receivepack;
+       unsigned verbose:1,
+               send_all:1,
+               send_mirror:1,
+               force_update:1,
+               use_thin_pack:1,
+               dry_run:1;
+};
+
+int send_pack(struct send_pack_args *args,
+             const char *dest, struct remote *remote,
+             int nr_heads, const char **heads);
+
+#endif
diff --git a/server-info.c b/server-info.c
new file mode 100644 (file)
index 0000000..a051e49
--- /dev/null
@@ -0,0 +1,252 @@
+#include "cache.h"
+#include "refs.h"
+#include "object.h"
+#include "commit.h"
+#include "tag.h"
+
+/* refs */
+static FILE *info_ref_fp;
+
+static int add_info_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct object *o = parse_object(sha1);
+       if (!o)
+               return -1;
+
+       fprintf(info_ref_fp, "%s        %s\n", sha1_to_hex(sha1), path);
+       if (o->type == OBJ_TAG) {
+               o = deref_tag(o, path, 0);
+               if (o)
+                       fprintf(info_ref_fp, "%s        %s^{}\n",
+                               sha1_to_hex(o->sha1), path);
+       }
+       return 0;
+}
+
+static int update_info_refs(int force)
+{
+       char *path0 = xstrdup(git_path("info/refs"));
+       int len = strlen(path0);
+       char *path1 = xmalloc(len + 2);
+
+       strcpy(path1, path0);
+       strcpy(path1 + len, "+");
+
+       safe_create_leading_directories(path0);
+       info_ref_fp = fopen(path1, "w");
+       if (!info_ref_fp)
+               return error("unable to update %s", path1);
+       for_each_ref(add_info_ref, NULL);
+       fclose(info_ref_fp);
+       adjust_shared_perm(path1);
+       rename(path1, path0);
+       free(path0);
+       free(path1);
+       return 0;
+}
+
+/* packs */
+static struct pack_info {
+       struct packed_git *p;
+       int old_num;
+       int new_num;
+       int nr_alloc;
+       int nr_heads;
+       unsigned char (*head)[20];
+} **info;
+static int num_pack;
+static const char *objdir;
+static int objdirlen;
+
+static struct pack_info *find_pack_by_name(const char *name)
+{
+       int i;
+       for (i = 0; i < num_pack; i++) {
+               struct packed_git *p = info[i]->p;
+               /* skip "/pack/" after ".git/objects" */
+               if (!strcmp(p->pack_name + objdirlen + 6, name))
+                       return info[i];
+       }
+       return NULL;
+}
+
+/* Returns non-zero when we detect that the info in the
+ * old file is useless.
+ */
+static int parse_pack_def(const char *line, int old_cnt)
+{
+       struct pack_info *i = find_pack_by_name(line + 2);
+       if (i) {
+               i->old_num = old_cnt;
+               return 0;
+       }
+       else {
+               /* The file describes a pack that is no longer here */
+               return 1;
+       }
+}
+
+/* Returns non-zero when we detect that the info in the
+ * old file is useless.
+ */
+static int read_pack_info_file(const char *infofile)
+{
+       FILE *fp;
+       char line[1000];
+       int old_cnt = 0;
+
+       fp = fopen(infofile, "r");
+       if (!fp)
+               return 1; /* nonexistent is not an error. */
+
+       while (fgets(line, sizeof(line), fp)) {
+               int len = strlen(line);
+               if (line[len-1] == '\n')
+                       line[--len] = 0;
+
+               if (!len)
+                       continue;
+
+               switch (line[0]) {
+               case 'P': /* P name */
+                       if (parse_pack_def(line, old_cnt++))
+                               goto out_stale;
+                       break;
+               case 'D': /* we used to emit D but that was misguided. */
+                       goto out_stale;
+                       break;
+               case 'T': /* we used to emit T but nobody uses it. */
+                       goto out_stale;
+                       break;
+               default:
+                       error("unrecognized: %s", line);
+                       break;
+               }
+       }
+       fclose(fp);
+       return 0;
+ out_stale:
+       fclose(fp);
+       return 1;
+}
+
+static int compare_info(const void *a_, const void *b_)
+{
+       struct pack_info * const* a = a_;
+       struct pack_info * const* b = b_;
+
+       if (0 <= (*a)->old_num && 0 <= (*b)->old_num)
+               /* Keep the order in the original */
+               return (*a)->old_num - (*b)->old_num;
+       else if (0 <= (*a)->old_num)
+               /* Only A existed in the original so B is obviously newer */
+               return -1;
+       else if (0 <= (*b)->old_num)
+               /* The other way around. */
+               return 1;
+
+       /* then it does not matter but at least keep the comparison stable */
+       if ((*a)->p == (*b)->p)
+               return 0;
+       else if ((*a)->p < (*b)->p)
+               return -1;
+       else
+               return 1;
+}
+
+static void init_pack_info(const char *infofile, int force)
+{
+       struct packed_git *p;
+       int stale;
+       int i = 0;
+
+       objdir = get_object_directory();
+       objdirlen = strlen(objdir);
+
+       prepare_packed_git();
+       for (p = packed_git; p; p = p->next) {
+               /* we ignore things on alternate path since they are
+                * not available to the pullers in general.
+                */
+               if (!p->pack_local)
+                       continue;
+               i++;
+       }
+       num_pack = i;
+       info = xcalloc(num_pack, sizeof(struct pack_info *));
+       for (i = 0, p = packed_git; p; p = p->next) {
+               if (!p->pack_local)
+                       continue;
+               info[i] = xcalloc(1, sizeof(struct pack_info));
+               info[i]->p = p;
+               info[i]->old_num = -1;
+               i++;
+       }
+
+       if (infofile && !force)
+               stale = read_pack_info_file(infofile);
+       else
+               stale = 1;
+
+       for (i = 0; i < num_pack; i++) {
+               if (stale) {
+                       info[i]->old_num = -1;
+                       info[i]->nr_heads = 0;
+               }
+       }
+
+       /* renumber them */
+       qsort(info, num_pack, sizeof(info[0]), compare_info);
+       for (i = 0; i < num_pack; i++)
+               info[i]->new_num = i;
+}
+
+static void write_pack_info_file(FILE *fp)
+{
+       int i;
+       for (i = 0; i < num_pack; i++)
+               fprintf(fp, "P %s\n", info[i]->p->pack_name + objdirlen + 6);
+       fputc('\n', fp);
+}
+
+static int update_info_packs(int force)
+{
+       char infofile[PATH_MAX];
+       char name[PATH_MAX];
+       int namelen;
+       FILE *fp;
+
+       namelen = sprintf(infofile, "%s/info/packs", get_object_directory());
+       strcpy(name, infofile);
+       strcpy(name + namelen, "+");
+
+       init_pack_info(infofile, force);
+
+       safe_create_leading_directories(name);
+       fp = fopen(name, "w");
+       if (!fp)
+               return error("cannot open %s", name);
+       write_pack_info_file(fp);
+       fclose(fp);
+       adjust_shared_perm(name);
+       rename(name, infofile);
+       return 0;
+}
+
+/* public */
+int update_server_info(int force)
+{
+       /* We would add more dumb-server support files later,
+        * including index of available pack files and their
+        * intended audiences.
+        */
+       int errs = 0;
+
+       errs = errs | update_info_refs(force);
+       errs = errs | update_info_packs(force);
+
+       /* remove leftover rev-cache file if there is any */
+       unlink(git_path("info/rev-cache"));
+
+       return errs;
+}
diff --git a/setup.c b/setup.c
new file mode 100644 (file)
index 0000000..e96a316
--- /dev/null
+++ b/setup.c
@@ -0,0 +1,404 @@
+#include "cache.h"
+#include "dir.h"
+
+static int inside_git_dir = -1;
+static int inside_work_tree = -1;
+
+const char *prefix_path(const char *prefix, int len, const char *path)
+{
+       const char *orig = path;
+       for (;;) {
+               char c;
+               if (*path != '.')
+                       break;
+               c = path[1];
+               /* "." */
+               if (!c) {
+                       path++;
+                       break;
+               }
+               /* "./" */
+               if (c == '/') {
+                       path += 2;
+                       continue;
+               }
+               if (c != '.')
+                       break;
+               c = path[2];
+               if (!c)
+                       path += 2;
+               else if (c == '/')
+                       path += 3;
+               else
+                       break;
+               /* ".." and "../" */
+               /* Remove last component of the prefix */
+               do {
+                       if (!len)
+                               die("'%s' is outside repository", orig);
+                       len--;
+               } while (len && prefix[len-1] != '/');
+               continue;
+       }
+       if (len) {
+               int speclen = strlen(path);
+               char *n = xmalloc(speclen + len + 1);
+
+               memcpy(n, prefix, len);
+               memcpy(n + len, path, speclen+1);
+               path = n;
+       }
+       return path;
+}
+
+/*
+ * Unlike prefix_path, this should be used if the named file does
+ * not have to interact with index entry; i.e. name of a random file
+ * on the filesystem.
+ */
+const char *prefix_filename(const char *pfx, int pfx_len, const char *arg)
+{
+       static char path[PATH_MAX];
+       if (!pfx || !*pfx || is_absolute_path(arg))
+               return arg;
+       memcpy(path, pfx, pfx_len);
+       strcpy(path + pfx_len, arg);
+       return path;
+}
+
+/*
+ * Verify a filename that we got as an argument for a pathspec
+ * entry. Note that a filename that begins with "-" never verifies
+ * as true, because even if such a filename were to exist, we want
+ * it to be preceded by the "--" marker (or we want the user to
+ * use a format like "./-filename")
+ */
+void verify_filename(const char *prefix, const char *arg)
+{
+       const char *name;
+       struct stat st;
+
+       if (*arg == '-')
+               die("bad flag '%s' used after filename", arg);
+       name = prefix ? prefix_filename(prefix, strlen(prefix), arg) : arg;
+       if (!lstat(name, &st))
+               return;
+       if (errno == ENOENT)
+               die("ambiguous argument '%s': unknown revision or path not in the working tree.\n"
+                   "Use '--' to separate paths from revisions", arg);
+       die("'%s': %s", arg, strerror(errno));
+}
+
+/*
+ * Opposite of the above: the command line did not have -- marker
+ * and we parsed the arg as a refname.  It should not be interpretable
+ * as a filename.
+ */
+void verify_non_filename(const char *prefix, const char *arg)
+{
+       const char *name;
+       struct stat st;
+
+       if (!is_inside_work_tree() || is_inside_git_dir())
+               return;
+       if (*arg == '-')
+               return; /* flag */
+       name = prefix ? prefix_filename(prefix, strlen(prefix), arg) : arg;
+       if (!lstat(name, &st))
+               die("ambiguous argument '%s': both revision and filename\n"
+                   "Use '--' to separate filenames from revisions", arg);
+       if (errno != ENOENT && errno != ENOTDIR)
+               die("'%s': %s", arg, strerror(errno));
+}
+
+const char **get_pathspec(const char *prefix, const char **pathspec)
+{
+       const char *entry = *pathspec;
+       const char **p;
+       int prefixlen;
+
+       if (!prefix && !entry)
+               return NULL;
+
+       if (!entry) {
+               static const char *spec[2];
+               spec[0] = prefix;
+               spec[1] = NULL;
+               return spec;
+       }
+
+       /* Otherwise we have to re-write the entries.. */
+       p = pathspec;
+       prefixlen = prefix ? strlen(prefix) : 0;
+       do {
+               *p = prefix_path(prefix, prefixlen, entry);
+       } while ((entry = *++p) != NULL);
+       return (const char **) pathspec;
+}
+
+/*
+ * Test if it looks like we're at a git directory.
+ * We want to see:
+ *
+ *  - either a objects/ directory _or_ the proper
+ *    GIT_OBJECT_DIRECTORY environment variable
+ *  - a refs/ directory
+ *  - either a HEAD symlink or a HEAD file that is formatted as
+ *    a proper "ref:", or a regular file HEAD that has a properly
+ *    formatted sha1 object name.
+ */
+static int is_git_directory(const char *suspect)
+{
+       char path[PATH_MAX];
+       size_t len = strlen(suspect);
+
+       strcpy(path, suspect);
+       if (getenv(DB_ENVIRONMENT)) {
+               if (access(getenv(DB_ENVIRONMENT), X_OK))
+                       return 0;
+       }
+       else {
+               strcpy(path + len, "/objects");
+               if (access(path, X_OK))
+                       return 0;
+       }
+
+       strcpy(path + len, "/refs");
+       if (access(path, X_OK))
+               return 0;
+
+       strcpy(path + len, "/HEAD");
+       if (validate_headref(path))
+               return 0;
+
+       return 1;
+}
+
+int is_inside_git_dir(void)
+{
+       if (inside_git_dir < 0)
+               inside_git_dir = is_inside_dir(get_git_dir());
+       return inside_git_dir;
+}
+
+int is_inside_work_tree(void)
+{
+       if (inside_work_tree < 0)
+               inside_work_tree = is_inside_dir(get_git_work_tree());
+       return inside_work_tree;
+}
+
+/*
+ * set_work_tree() is only ever called if you set GIT_DIR explicitely.
+ * The old behaviour (which we retain here) is to set the work tree root
+ * to the cwd, unless overridden by the config, the command line, or
+ * GIT_WORK_TREE.
+ */
+static const char *set_work_tree(const char *dir)
+{
+       char buffer[PATH_MAX + 1];
+
+       if (!getcwd(buffer, sizeof(buffer)))
+               die ("Could not get the current working directory");
+       git_work_tree_cfg = xstrdup(buffer);
+       inside_work_tree = 1;
+
+       return NULL;
+}
+
+void setup_work_tree(void)
+{
+       const char *work_tree, *git_dir;
+       static int initialized = 0;
+
+       if (initialized)
+               return;
+       work_tree = get_git_work_tree();
+       git_dir = get_git_dir();
+       if (!is_absolute_path(git_dir))
+               set_git_dir(make_absolute_path(git_dir));
+       if (!work_tree || chdir(work_tree))
+               die("This operation must be run in a work tree");
+       initialized = 1;
+}
+
+static int check_repository_format_gently(int *nongit_ok)
+{
+       git_config(check_repository_format_version);
+       if (GIT_REPO_VERSION < repository_format_version) {
+               if (!nongit_ok)
+                       die ("Expected git repo version <= %d, found %d",
+                            GIT_REPO_VERSION, repository_format_version);
+               warning("Expected git repo version <= %d, found %d",
+                       GIT_REPO_VERSION, repository_format_version);
+               warning("Please upgrade Git");
+               *nongit_ok = -1;
+               return -1;
+       }
+       return 0;
+}
+
+/*
+ * We cannot decide in this function whether we are in the work tree or
+ * not, since the config can only be read _after_ this function was called.
+ */
+const char *setup_git_directory_gently(int *nongit_ok)
+{
+       const char *work_tree_env = getenv(GIT_WORK_TREE_ENVIRONMENT);
+       static char cwd[PATH_MAX+1];
+       const char *gitdirenv;
+       int len, offset;
+
+       /*
+        * If GIT_DIR is set explicitly, we're not going
+        * to do any discovery, but we still do repository
+        * validation.
+        */
+       gitdirenv = getenv(GIT_DIR_ENVIRONMENT);
+       if (gitdirenv) {
+               if (PATH_MAX - 40 < strlen(gitdirenv))
+                       die("'$%s' too big", GIT_DIR_ENVIRONMENT);
+               if (is_git_directory(gitdirenv)) {
+                       static char buffer[1024 + 1];
+                       const char *retval;
+
+                       if (!work_tree_env) {
+                               retval = set_work_tree(gitdirenv);
+                               /* config may override worktree */
+                               if (check_repository_format_gently(nongit_ok))
+                                       return NULL;
+                               return retval;
+                       }
+                       if (check_repository_format_gently(nongit_ok))
+                               return NULL;
+                       retval = get_relative_cwd(buffer, sizeof(buffer) - 1,
+                                       get_git_work_tree());
+                       if (!retval || !*retval)
+                               return NULL;
+                       set_git_dir(make_absolute_path(gitdirenv));
+                       if (chdir(work_tree_env) < 0)
+                               die ("Could not chdir to %s", work_tree_env);
+                       strcat(buffer, "/");
+                       return retval;
+               }
+               if (nongit_ok) {
+                       *nongit_ok = 1;
+                       return NULL;
+               }
+               die("Not a git repository: '%s'", gitdirenv);
+       }
+
+       if (!getcwd(cwd, sizeof(cwd)-1))
+               die("Unable to read current working directory");
+
+       /*
+        * Test in the following order (relative to the cwd):
+        * - .git/
+        * - ./ (bare)
+        * - ../.git/
+        * - ../ (bare)
+        * - ../../.git/
+        *   etc.
+        */
+       offset = len = strlen(cwd);
+       for (;;) {
+               if (is_git_directory(DEFAULT_GIT_DIR_ENVIRONMENT))
+                       break;
+               if (is_git_directory(".")) {
+                       inside_git_dir = 1;
+                       if (!work_tree_env)
+                               inside_work_tree = 0;
+                       setenv(GIT_DIR_ENVIRONMENT, ".", 1);
+                       check_repository_format_gently(nongit_ok);
+                       return NULL;
+               }
+               chdir("..");
+               do {
+                       if (!offset) {
+                               if (nongit_ok) {
+                                       if (chdir(cwd))
+                                               die("Cannot come back to cwd");
+                                       *nongit_ok = 1;
+                                       return NULL;
+                               }
+                               die("Not a git repository");
+                       }
+               } while (cwd[--offset] != '/');
+       }
+
+       inside_git_dir = 0;
+       if (!work_tree_env)
+               inside_work_tree = 1;
+       git_work_tree_cfg = xstrndup(cwd, offset);
+       if (check_repository_format_gently(nongit_ok))
+               return NULL;
+       if (offset == len)
+               return NULL;
+
+       /* Make "offset" point to past the '/', and add a '/' at the end */
+       offset++;
+       cwd[len++] = '/';
+       cwd[len] = 0;
+       return cwd + offset;
+}
+
+int git_config_perm(const char *var, const char *value)
+{
+       if (value) {
+               int i;
+               if (!strcmp(value, "umask"))
+                       return PERM_UMASK;
+               if (!strcmp(value, "group"))
+                       return PERM_GROUP;
+               if (!strcmp(value, "all") ||
+                   !strcmp(value, "world") ||
+                   !strcmp(value, "everybody"))
+                       return PERM_EVERYBODY;
+               i = atoi(value);
+               if (i > 1)
+                       return i;
+       }
+       return git_config_bool(var, value);
+}
+
+int check_repository_format_version(const char *var, const char *value)
+{
+       if (strcmp(var, "core.repositoryformatversion") == 0)
+               repository_format_version = git_config_int(var, value);
+       else if (strcmp(var, "core.sharedrepository") == 0)
+               shared_repository = git_config_perm(var, value);
+       else if (strcmp(var, "core.bare") == 0) {
+               is_bare_repository_cfg = git_config_bool(var, value);
+               if (is_bare_repository_cfg == 1)
+                       inside_work_tree = -1;
+       } else if (strcmp(var, "core.worktree") == 0) {
+               if (git_work_tree_cfg)
+                       free(git_work_tree_cfg);
+               git_work_tree_cfg = xstrdup(value);
+               inside_work_tree = -1;
+       }
+       return 0;
+}
+
+int check_repository_format(void)
+{
+       return check_repository_format_gently(NULL);
+}
+
+const char *setup_git_directory(void)
+{
+       const char *retval = setup_git_directory_gently(NULL);
+       check_repository_format();
+
+       /* If the work tree is not the default one, recompute prefix */
+       if (inside_work_tree < 0) {
+               static char buffer[PATH_MAX + 1];
+               char *rel;
+               if (retval && chdir(retval))
+                       die ("Could not jump back into original cwd");
+               rel = get_relative_cwd(buffer, PATH_MAX, get_git_work_tree());
+               return rel && *rel ? strcat(rel, "/") : NULL;
+       }
+
+       return retval;
+}
diff --git a/sha1_file.c b/sha1_file.c
new file mode 100644 (file)
index 0000000..b0c2435
--- /dev/null
@@ -0,0 +1,2424 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ *
+ * This handles basic git sha1 object files - packing, unpacking,
+ * creation etc.
+ */
+#include "cache.h"
+#include "delta.h"
+#include "pack.h"
+#include "blob.h"
+#include "commit.h"
+#include "tag.h"
+#include "tree.h"
+#include "refs.h"
+
+#ifndef O_NOATIME
+#if defined(__linux__) && (defined(__i386__) || defined(__PPC__))
+#define O_NOATIME 01000000
+#else
+#define O_NOATIME 0
+#endif
+#endif
+
+#ifdef NO_C99_FORMAT
+#define SZ_FMT "lu"
+static unsigned long sz_fmt(size_t s) { return (unsigned long)s; }
+#else
+#define SZ_FMT "zu"
+static size_t sz_fmt(size_t s) { return s; }
+#endif
+
+const unsigned char null_sha1[20];
+
+static unsigned int sha1_file_open_flag = O_NOATIME;
+
+const signed char hexval_table[256] = {
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 00-07 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 08-0f */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 10-17 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 18-1f */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 20-27 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 28-2f */
+         0,  1,  2,  3,  4,  5,  6,  7,                /* 30-37 */
+         8,  9, -1, -1, -1, -1, -1, -1,                /* 38-3f */
+        -1, 10, 11, 12, 13, 14, 15, -1,                /* 40-47 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 48-4f */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 50-57 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 58-5f */
+        -1, 10, 11, 12, 13, 14, 15, -1,                /* 60-67 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 68-67 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 70-77 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 78-7f */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 80-87 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 88-8f */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 90-97 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* 98-9f */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* a0-a7 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* a8-af */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* b0-b7 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* b8-bf */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* c0-c7 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* c8-cf */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* d0-d7 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* d8-df */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* e0-e7 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* e8-ef */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* f0-f7 */
+        -1, -1, -1, -1, -1, -1, -1, -1,                /* f8-ff */
+};
+
+int get_sha1_hex(const char *hex, unsigned char *sha1)
+{
+       int i;
+       for (i = 0; i < 20; i++) {
+               unsigned int val = (hexval(hex[0]) << 4) | hexval(hex[1]);
+               if (val & ~0xff)
+                       return -1;
+               *sha1++ = val;
+               hex += 2;
+       }
+       return 0;
+}
+
+int safe_create_leading_directories(char *path)
+{
+       char *pos = path;
+       struct stat st;
+
+       if (is_absolute_path(path))
+               pos++;
+
+       while (pos) {
+               pos = strchr(pos, '/');
+               if (!pos)
+                       break;
+               *pos = 0;
+               if (!stat(path, &st)) {
+                       /* path exists */
+                       if (!S_ISDIR(st.st_mode)) {
+                               *pos = '/';
+                               return -3;
+                       }
+               }
+               else if (mkdir(path, 0777)) {
+                       *pos = '/';
+                       return -1;
+               }
+               else if (adjust_shared_perm(path)) {
+                       *pos = '/';
+                       return -2;
+               }
+               *pos++ = '/';
+       }
+       return 0;
+}
+
+char * sha1_to_hex(const unsigned char *sha1)
+{
+       static int bufno;
+       static char hexbuffer[4][50];
+       static const char hex[] = "0123456789abcdef";
+       char *buffer = hexbuffer[3 & ++bufno], *buf = buffer;
+       int i;
+
+       for (i = 0; i < 20; i++) {
+               unsigned int val = *sha1++;
+               *buf++ = hex[val >> 4];
+               *buf++ = hex[val & 0xf];
+       }
+       *buf = '\0';
+
+       return buffer;
+}
+
+static void fill_sha1_path(char *pathbuf, const unsigned char *sha1)
+{
+       int i;
+       for (i = 0; i < 20; i++) {
+               static char hex[] = "0123456789abcdef";
+               unsigned int val = sha1[i];
+               char *pos = pathbuf + i*2 + (i > 0);
+               *pos++ = hex[val >> 4];
+               *pos = hex[val & 0xf];
+       }
+}
+
+/*
+ * NOTE! This returns a statically allocated buffer, so you have to be
+ * careful about using it. Do a "xstrdup()" if you need to save the
+ * filename.
+ *
+ * Also note that this returns the location for creating.  Reading
+ * SHA1 file can happen from any alternate directory listed in the
+ * DB_ENVIRONMENT environment variable if it is not found in
+ * the primary object database.
+ */
+char *sha1_file_name(const unsigned char *sha1)
+{
+       static char *name, *base;
+
+       if (!base) {
+               const char *sha1_file_directory = get_object_directory();
+               int len = strlen(sha1_file_directory);
+               base = xmalloc(len + 60);
+               memcpy(base, sha1_file_directory, len);
+               memset(base+len, 0, 60);
+               base[len] = '/';
+               base[len+3] = '/';
+               name = base + len + 1;
+       }
+       fill_sha1_path(name, sha1);
+       return base;
+}
+
+char *sha1_pack_name(const unsigned char *sha1)
+{
+       static const char hex[] = "0123456789abcdef";
+       static char *name, *base, *buf;
+       int i;
+
+       if (!base) {
+               const char *sha1_file_directory = get_object_directory();
+               int len = strlen(sha1_file_directory);
+               base = xmalloc(len + 60);
+               sprintf(base, "%s/pack/pack-1234567890123456789012345678901234567890.pack", sha1_file_directory);
+               name = base + len + 11;
+       }
+
+       buf = name;
+
+       for (i = 0; i < 20; i++) {
+               unsigned int val = *sha1++;
+               *buf++ = hex[val >> 4];
+               *buf++ = hex[val & 0xf];
+       }
+
+       return base;
+}
+
+char *sha1_pack_index_name(const unsigned char *sha1)
+{
+       static const char hex[] = "0123456789abcdef";
+       static char *name, *base, *buf;
+       int i;
+
+       if (!base) {
+               const char *sha1_file_directory = get_object_directory();
+               int len = strlen(sha1_file_directory);
+               base = xmalloc(len + 60);
+               sprintf(base, "%s/pack/pack-1234567890123456789012345678901234567890.idx", sha1_file_directory);
+               name = base + len + 11;
+       }
+
+       buf = name;
+
+       for (i = 0; i < 20; i++) {
+               unsigned int val = *sha1++;
+               *buf++ = hex[val >> 4];
+               *buf++ = hex[val & 0xf];
+       }
+
+       return base;
+}
+
+struct alternate_object_database *alt_odb_list;
+static struct alternate_object_database **alt_odb_tail;
+
+static void read_info_alternates(const char * alternates, int depth);
+
+/*
+ * Prepare alternate object database registry.
+ *
+ * The variable alt_odb_list points at the list of struct
+ * alternate_object_database.  The elements on this list come from
+ * non-empty elements from colon separated ALTERNATE_DB_ENVIRONMENT
+ * environment variable, and $GIT_OBJECT_DIRECTORY/info/alternates,
+ * whose contents is similar to that environment variable but can be
+ * LF separated.  Its base points at a statically allocated buffer that
+ * contains "/the/directory/corresponding/to/.git/objects/...", while
+ * its name points just after the slash at the end of ".git/objects/"
+ * in the example above, and has enough space to hold 40-byte hex
+ * SHA1, an extra slash for the first level indirection, and the
+ * terminating NUL.
+ */
+static int link_alt_odb_entry(const char * entry, int len, const char * relative_base, int depth)
+{
+       struct stat st;
+       const char *objdir = get_object_directory();
+       struct alternate_object_database *ent;
+       struct alternate_object_database *alt;
+       /* 43 = 40-byte + 2 '/' + terminating NUL */
+       int pfxlen = len;
+       int entlen = pfxlen + 43;
+       int base_len = -1;
+
+       if (!is_absolute_path(entry) && relative_base) {
+               /* Relative alt-odb */
+               if (base_len < 0)
+                       base_len = strlen(relative_base) + 1;
+               entlen += base_len;
+               pfxlen += base_len;
+       }
+       ent = xmalloc(sizeof(*ent) + entlen);
+
+       if (!is_absolute_path(entry) && relative_base) {
+               memcpy(ent->base, relative_base, base_len - 1);
+               ent->base[base_len - 1] = '/';
+               memcpy(ent->base + base_len, entry, len);
+       }
+       else
+               memcpy(ent->base, entry, pfxlen);
+
+       ent->name = ent->base + pfxlen + 1;
+       ent->base[pfxlen + 3] = '/';
+       ent->base[pfxlen] = ent->base[entlen-1] = 0;
+
+       /* Detect cases where alternate disappeared */
+       if (stat(ent->base, &st) || !S_ISDIR(st.st_mode)) {
+               error("object directory %s does not exist; "
+                     "check .git/objects/info/alternates.",
+                     ent->base);
+               free(ent);
+               return -1;
+       }
+
+       /* Prevent the common mistake of listing the same
+        * thing twice, or object directory itself.
+        */
+       for (alt = alt_odb_list; alt; alt = alt->next) {
+               if (!memcmp(ent->base, alt->base, pfxlen)) {
+                       free(ent);
+                       return -1;
+               }
+       }
+       if (!memcmp(ent->base, objdir, pfxlen)) {
+               free(ent);
+               return -1;
+       }
+
+       /* add the alternate entry */
+       *alt_odb_tail = ent;
+       alt_odb_tail = &(ent->next);
+       ent->next = NULL;
+
+       /* recursively add alternates */
+       read_info_alternates(ent->base, depth + 1);
+
+       ent->base[pfxlen] = '/';
+
+       return 0;
+}
+
+static void link_alt_odb_entries(const char *alt, const char *ep, int sep,
+                                const char *relative_base, int depth)
+{
+       const char *cp, *last;
+
+       if (depth > 5) {
+               error("%s: ignoring alternate object stores, nesting too deep.",
+                               relative_base);
+               return;
+       }
+
+       last = alt;
+       while (last < ep) {
+               cp = last;
+               if (cp < ep && *cp == '#') {
+                       while (cp < ep && *cp != sep)
+                               cp++;
+                       last = cp + 1;
+                       continue;
+               }
+               while (cp < ep && *cp != sep)
+                       cp++;
+               if (last != cp) {
+                       if (!is_absolute_path(last) && depth) {
+                               error("%s: ignoring relative alternate object store %s",
+                                               relative_base, last);
+                       } else {
+                               link_alt_odb_entry(last, cp - last,
+                                               relative_base, depth);
+                       }
+               }
+               while (cp < ep && *cp == sep)
+                       cp++;
+               last = cp;
+       }
+}
+
+static void read_info_alternates(const char * relative_base, int depth)
+{
+       char *map;
+       size_t mapsz;
+       struct stat st;
+       const char alt_file_name[] = "info/alternates";
+       /* Given that relative_base is no longer than PATH_MAX,
+          ensure that "path" has enough space to append "/", the
+          file name, "info/alternates", and a trailing NUL.  */
+       char path[PATH_MAX + 1 + sizeof alt_file_name];
+       int fd;
+
+       sprintf(path, "%s/%s", relative_base, alt_file_name);
+       fd = open(path, O_RDONLY);
+       if (fd < 0)
+               return;
+       if (fstat(fd, &st) || (st.st_size == 0)) {
+               close(fd);
+               return;
+       }
+       mapsz = xsize_t(st.st_size);
+       map = xmmap(NULL, mapsz, PROT_READ, MAP_PRIVATE, fd, 0);
+       close(fd);
+
+       link_alt_odb_entries(map, map + mapsz, '\n', relative_base, depth);
+
+       munmap(map, mapsz);
+}
+
+void prepare_alt_odb(void)
+{
+       const char *alt;
+
+       if (alt_odb_tail)
+               return;
+
+       alt = getenv(ALTERNATE_DB_ENVIRONMENT);
+       if (!alt) alt = "";
+
+       alt_odb_tail = &alt_odb_list;
+       link_alt_odb_entries(alt, alt + strlen(alt), ':', NULL, 0);
+
+       read_info_alternates(get_object_directory(), 0);
+}
+
+static char *find_sha1_file(const unsigned char *sha1, struct stat *st)
+{
+       char *name = sha1_file_name(sha1);
+       struct alternate_object_database *alt;
+
+       if (!stat(name, st))
+               return name;
+       prepare_alt_odb();
+       for (alt = alt_odb_list; alt; alt = alt->next) {
+               name = alt->name;
+               fill_sha1_path(name, sha1);
+               if (!stat(alt->base, st))
+                       return alt->base;
+       }
+       return NULL;
+}
+
+static unsigned int pack_used_ctr;
+static unsigned int pack_mmap_calls;
+static unsigned int peak_pack_open_windows;
+static unsigned int pack_open_windows;
+static size_t peak_pack_mapped;
+static size_t pack_mapped;
+struct packed_git *packed_git;
+
+void pack_report(void)
+{
+       fprintf(stderr,
+               "pack_report: getpagesize()            = %10" SZ_FMT "\n"
+               "pack_report: core.packedGitWindowSize = %10" SZ_FMT "\n"
+               "pack_report: core.packedGitLimit      = %10" SZ_FMT "\n",
+               sz_fmt(getpagesize()),
+               sz_fmt(packed_git_window_size),
+               sz_fmt(packed_git_limit));
+       fprintf(stderr,
+               "pack_report: pack_used_ctr            = %10u\n"
+               "pack_report: pack_mmap_calls          = %10u\n"
+               "pack_report: pack_open_windows        = %10u / %10u\n"
+               "pack_report: pack_mapped              = "
+                       "%10" SZ_FMT " / %10" SZ_FMT "\n",
+               pack_used_ctr,
+               pack_mmap_calls,
+               pack_open_windows, peak_pack_open_windows,
+               sz_fmt(pack_mapped), sz_fmt(peak_pack_mapped));
+}
+
+static int check_packed_git_idx(const char *path,  struct packed_git *p)
+{
+       void *idx_map;
+       struct pack_idx_header *hdr;
+       size_t idx_size;
+       uint32_t version, nr, i, *index;
+       int fd = open(path, O_RDONLY);
+       struct stat st;
+
+       if (fd < 0)
+               return -1;
+       if (fstat(fd, &st)) {
+               close(fd);
+               return -1;
+       }
+       idx_size = xsize_t(st.st_size);
+       if (idx_size < 4 * 256 + 20 + 20) {
+               close(fd);
+               return error("index file %s is too small", path);
+       }
+       idx_map = xmmap(NULL, idx_size, PROT_READ, MAP_PRIVATE, fd, 0);
+       close(fd);
+
+       hdr = idx_map;
+       if (hdr->idx_signature == htonl(PACK_IDX_SIGNATURE)) {
+               version = ntohl(hdr->idx_version);
+               if (version < 2 || version > 2) {
+                       munmap(idx_map, idx_size);
+                       return error("index file %s is version %d"
+                                    " and is not supported by this binary"
+                                    " (try upgrading GIT to a newer version)",
+                                    path, version);
+               }
+       } else
+               version = 1;
+
+       nr = 0;
+       index = idx_map;
+       if (version > 1)
+               index += 2;  /* skip index header */
+       for (i = 0; i < 256; i++) {
+               uint32_t n = ntohl(index[i]);
+               if (n < nr) {
+                       munmap(idx_map, idx_size);
+                       return error("non-monotonic index %s", path);
+               }
+               nr = n;
+       }
+
+       if (version == 1) {
+               /*
+                * Total size:
+                *  - 256 index entries 4 bytes each
+                *  - 24-byte entries * nr (20-byte sha1 + 4-byte offset)
+                *  - 20-byte SHA1 of the packfile
+                *  - 20-byte SHA1 file checksum
+                */
+               if (idx_size != 4*256 + nr * 24 + 20 + 20) {
+                       munmap(idx_map, idx_size);
+                       return error("wrong index v1 file size in %s", path);
+               }
+       } else if (version == 2) {
+               /*
+                * Minimum size:
+                *  - 8 bytes of header
+                *  - 256 index entries 4 bytes each
+                *  - 20-byte sha1 entry * nr
+                *  - 4-byte crc entry * nr
+                *  - 4-byte offset entry * nr
+                *  - 20-byte SHA1 of the packfile
+                *  - 20-byte SHA1 file checksum
+                * And after the 4-byte offset table might be a
+                * variable sized table containing 8-byte entries
+                * for offsets larger than 2^31.
+                */
+               unsigned long min_size = 8 + 4*256 + nr*(20 + 4 + 4) + 20 + 20;
+               unsigned long max_size = min_size;
+               if (nr)
+                       max_size += (nr - 1)*8;
+               if (idx_size < min_size || idx_size > max_size) {
+                       munmap(idx_map, idx_size);
+                       return error("wrong index v2 file size in %s", path);
+               }
+               if (idx_size != min_size &&
+                   /*
+                    * make sure we can deal with large pack offsets.
+                    * 31-bit signed offset won't be enough, neither
+                    * 32-bit unsigned one will be.
+                    */
+                   (sizeof(off_t) <= 4)) {
+                       munmap(idx_map, idx_size);
+                       return error("pack too large for current definition of off_t in %s", path);
+               }
+       }
+
+       p->index_version = version;
+       p->index_data = idx_map;
+       p->index_size = idx_size;
+       p->num_objects = nr;
+       return 0;
+}
+
+int open_pack_index(struct packed_git *p)
+{
+       char *idx_name;
+       int ret;
+
+       if (p->index_data)
+               return 0;
+
+       idx_name = xstrdup(p->pack_name);
+       strcpy(idx_name + strlen(idx_name) - strlen(".pack"), ".idx");
+       ret = check_packed_git_idx(idx_name, p);
+       free(idx_name);
+       return ret;
+}
+
+static void scan_windows(struct packed_git *p,
+       struct packed_git **lru_p,
+       struct pack_window **lru_w,
+       struct pack_window **lru_l)
+{
+       struct pack_window *w, *w_l;
+
+       for (w_l = NULL, w = p->windows; w; w = w->next) {
+               if (!w->inuse_cnt) {
+                       if (!*lru_w || w->last_used < (*lru_w)->last_used) {
+                               *lru_p = p;
+                               *lru_w = w;
+                               *lru_l = w_l;
+                       }
+               }
+               w_l = w;
+       }
+}
+
+static int unuse_one_window(struct packed_git *current, int keep_fd)
+{
+       struct packed_git *p, *lru_p = NULL;
+       struct pack_window *lru_w = NULL, *lru_l = NULL;
+
+       if (current)
+               scan_windows(current, &lru_p, &lru_w, &lru_l);
+       for (p = packed_git; p; p = p->next)
+               scan_windows(p, &lru_p, &lru_w, &lru_l);
+       if (lru_p) {
+               munmap(lru_w->base, lru_w->len);
+               pack_mapped -= lru_w->len;
+               if (lru_l)
+                       lru_l->next = lru_w->next;
+               else {
+                       lru_p->windows = lru_w->next;
+                       if (!lru_p->windows && lru_p->pack_fd != keep_fd) {
+                               close(lru_p->pack_fd);
+                               lru_p->pack_fd = -1;
+                       }
+               }
+               free(lru_w);
+               pack_open_windows--;
+               return 1;
+       }
+       return 0;
+}
+
+void release_pack_memory(size_t need, int fd)
+{
+       size_t cur = pack_mapped;
+       while (need >= (cur - pack_mapped) && unuse_one_window(NULL, fd))
+               ; /* nothing */
+}
+
+void unuse_pack(struct pack_window **w_cursor)
+{
+       struct pack_window *w = *w_cursor;
+       if (w) {
+               w->inuse_cnt--;
+               *w_cursor = NULL;
+       }
+}
+
+/*
+ * Do not call this directly as this leaks p->pack_fd on error return;
+ * call open_packed_git() instead.
+ */
+static int open_packed_git_1(struct packed_git *p)
+{
+       struct stat st;
+       struct pack_header hdr;
+       unsigned char sha1[20];
+       unsigned char *idx_sha1;
+       long fd_flag;
+
+       if (!p->index_data && open_pack_index(p))
+               return error("packfile %s index unavailable", p->pack_name);
+
+       p->pack_fd = open(p->pack_name, O_RDONLY);
+       if (p->pack_fd < 0 || fstat(p->pack_fd, &st))
+               return -1;
+
+       /* If we created the struct before we had the pack we lack size. */
+       if (!p->pack_size) {
+               if (!S_ISREG(st.st_mode))
+                       return error("packfile %s not a regular file", p->pack_name);
+               p->pack_size = st.st_size;
+       } else if (p->pack_size != st.st_size)
+               return error("packfile %s size changed", p->pack_name);
+
+       /* We leave these file descriptors open with sliding mmap;
+        * there is no point keeping them open across exec(), though.
+        */
+       fd_flag = fcntl(p->pack_fd, F_GETFD, 0);
+       if (fd_flag < 0)
+               return error("cannot determine file descriptor flags");
+       fd_flag |= FD_CLOEXEC;
+       if (fcntl(p->pack_fd, F_SETFD, fd_flag) == -1)
+               return error("cannot set FD_CLOEXEC");
+
+       /* Verify we recognize this pack file format. */
+       if (read_in_full(p->pack_fd, &hdr, sizeof(hdr)) != sizeof(hdr))
+               return error("file %s is far too short to be a packfile", p->pack_name);
+       if (hdr.hdr_signature != htonl(PACK_SIGNATURE))
+               return error("file %s is not a GIT packfile", p->pack_name);
+       if (!pack_version_ok(hdr.hdr_version))
+               return error("packfile %s is version %u and not supported"
+                       " (try upgrading GIT to a newer version)",
+                       p->pack_name, ntohl(hdr.hdr_version));
+
+       /* Verify the pack matches its index. */
+       if (p->num_objects != ntohl(hdr.hdr_entries))
+               return error("packfile %s claims to have %u objects"
+                            " while index indicates %u objects",
+                            p->pack_name, ntohl(hdr.hdr_entries),
+                            p->num_objects);
+       if (lseek(p->pack_fd, p->pack_size - sizeof(sha1), SEEK_SET) == -1)
+               return error("end of packfile %s is unavailable", p->pack_name);
+       if (read_in_full(p->pack_fd, sha1, sizeof(sha1)) != sizeof(sha1))
+               return error("packfile %s signature is unavailable", p->pack_name);
+       idx_sha1 = ((unsigned char *)p->index_data) + p->index_size - 40;
+       if (hashcmp(sha1, idx_sha1))
+               return error("packfile %s does not match index", p->pack_name);
+       return 0;
+}
+
+static int open_packed_git(struct packed_git *p)
+{
+       if (!open_packed_git_1(p))
+               return 0;
+       if (p->pack_fd != -1) {
+               close(p->pack_fd);
+               p->pack_fd = -1;
+       }
+       return -1;
+}
+
+static int in_window(struct pack_window *win, off_t offset)
+{
+       /* We must promise at least 20 bytes (one hash) after the
+        * offset is available from this window, otherwise the offset
+        * is not actually in this window and a different window (which
+        * has that one hash excess) must be used.  This is to support
+        * the object header and delta base parsing routines below.
+        */
+       off_t win_off = win->offset;
+       return win_off <= offset
+               && (offset + 20) <= (win_off + win->len);
+}
+
+unsigned char* use_pack(struct packed_git *p,
+               struct pack_window **w_cursor,
+               off_t offset,
+               unsigned int *left)
+{
+       struct pack_window *win = *w_cursor;
+
+       if (p->pack_fd == -1 && open_packed_git(p))
+               die("packfile %s cannot be accessed", p->pack_name);
+
+       /* Since packfiles end in a hash of their content and its
+        * pointless to ask for an offset into the middle of that
+        * hash, and the in_window function above wouldn't match
+        * don't allow an offset too close to the end of the file.
+        */
+       if (offset > (p->pack_size - 20))
+               die("offset beyond end of packfile (truncated pack?)");
+
+       if (!win || !in_window(win, offset)) {
+               if (win)
+                       win->inuse_cnt--;
+               for (win = p->windows; win; win = win->next) {
+                       if (in_window(win, offset))
+                               break;
+               }
+               if (!win) {
+                       size_t window_align = packed_git_window_size / 2;
+                       off_t len;
+                       win = xcalloc(1, sizeof(*win));
+                       win->offset = (offset / window_align) * window_align;
+                       len = p->pack_size - win->offset;
+                       if (len > packed_git_window_size)
+                               len = packed_git_window_size;
+                       win->len = (size_t)len;
+                       pack_mapped += win->len;
+                       while (packed_git_limit < pack_mapped
+                               && unuse_one_window(p, p->pack_fd))
+                               ; /* nothing */
+                       win->base = xmmap(NULL, win->len,
+                               PROT_READ, MAP_PRIVATE,
+                               p->pack_fd, win->offset);
+                       if (win->base == MAP_FAILED)
+                               die("packfile %s cannot be mapped: %s",
+                                       p->pack_name,
+                                       strerror(errno));
+                       pack_mmap_calls++;
+                       pack_open_windows++;
+                       if (pack_mapped > peak_pack_mapped)
+                               peak_pack_mapped = pack_mapped;
+                       if (pack_open_windows > peak_pack_open_windows)
+                               peak_pack_open_windows = pack_open_windows;
+                       win->next = p->windows;
+                       p->windows = win;
+               }
+       }
+       if (win != *w_cursor) {
+               win->last_used = pack_used_ctr++;
+               win->inuse_cnt++;
+               *w_cursor = win;
+       }
+       offset -= win->offset;
+       if (left)
+               *left = win->len - xsize_t(offset);
+       return win->base + offset;
+}
+
+struct packed_git *add_packed_git(const char *path, int path_len, int local)
+{
+       struct stat st;
+       struct packed_git *p = xmalloc(sizeof(*p) + path_len + 2);
+
+       /*
+        * Make sure a corresponding .pack file exists and that
+        * the index looks sane.
+        */
+       path_len -= strlen(".idx");
+       if (path_len < 1)
+               return NULL;
+       memcpy(p->pack_name, path, path_len);
+       strcpy(p->pack_name + path_len, ".pack");
+       if (stat(p->pack_name, &st) || !S_ISREG(st.st_mode)) {
+               free(p);
+               return NULL;
+       }
+
+       /* ok, it looks sane as far as we can check without
+        * actually mapping the pack file.
+        */
+       p->index_version = 0;
+       p->index_data = NULL;
+       p->index_size = 0;
+       p->num_objects = 0;
+       p->pack_size = st.st_size;
+       p->next = NULL;
+       p->windows = NULL;
+       p->pack_fd = -1;
+       p->pack_local = local;
+       p->mtime = st.st_mtime;
+       if (path_len < 40 || get_sha1_hex(path + path_len - 40, p->sha1))
+               hashclr(p->sha1);
+       return p;
+}
+
+struct packed_git *parse_pack_index(unsigned char *sha1)
+{
+       char *path = sha1_pack_index_name(sha1);
+       return parse_pack_index_file(sha1, path);
+}
+
+struct packed_git *parse_pack_index_file(const unsigned char *sha1,
+                                        const char *idx_path)
+{
+       const char *path = sha1_pack_name(sha1);
+       struct packed_git *p = xmalloc(sizeof(*p) + strlen(path) + 2);
+
+       if (check_packed_git_idx(idx_path, p)) {
+               free(p);
+               return NULL;
+       }
+
+       strcpy(p->pack_name, path);
+       p->pack_size = 0;
+       p->next = NULL;
+       p->windows = NULL;
+       p->pack_fd = -1;
+       hashcpy(p->sha1, sha1);
+       return p;
+}
+
+void install_packed_git(struct packed_git *pack)
+{
+       pack->next = packed_git;
+       packed_git = pack;
+}
+
+static void prepare_packed_git_one(char *objdir, int local)
+{
+       /* Ensure that this buffer is large enough so that we can
+          append "/pack/" without clobbering the stack even if
+          strlen(objdir) were PATH_MAX.  */
+       char path[PATH_MAX + 1 + 4 + 1 + 1];
+       int len;
+       DIR *dir;
+       struct dirent *de;
+
+       sprintf(path, "%s/pack", objdir);
+       len = strlen(path);
+       dir = opendir(path);
+       if (!dir) {
+               if (errno != ENOENT)
+                       error("unable to open object pack directory: %s: %s",
+                             path, strerror(errno));
+               return;
+       }
+       path[len++] = '/';
+       while ((de = readdir(dir)) != NULL) {
+               int namelen = strlen(de->d_name);
+               struct packed_git *p;
+
+               if (!has_extension(de->d_name, ".idx"))
+                       continue;
+
+               if (len + namelen + 1 > sizeof(path))
+                       continue;
+
+               /* Don't reopen a pack we already have. */
+               strcpy(path + len, de->d_name);
+               for (p = packed_git; p; p = p->next) {
+                       if (!memcmp(path, p->pack_name, len + namelen - 4))
+                               break;
+               }
+               if (p)
+                       continue;
+               /* See if it really is a valid .idx file with corresponding
+                * .pack file that we can map.
+                */
+               p = add_packed_git(path, len + namelen, local);
+               if (!p)
+                       continue;
+               install_packed_git(p);
+       }
+       closedir(dir);
+}
+
+static int sort_pack(const void *a_, const void *b_)
+{
+       struct packed_git *a = *((struct packed_git **)a_);
+       struct packed_git *b = *((struct packed_git **)b_);
+       int st;
+
+       /*
+        * Local packs tend to contain objects specific to our
+        * variant of the project than remote ones.  In addition,
+        * remote ones could be on a network mounted filesystem.
+        * Favor local ones for these reasons.
+        */
+       st = a->pack_local - b->pack_local;
+       if (st)
+               return -st;
+
+       /*
+        * Younger packs tend to contain more recent objects,
+        * and more recent objects tend to get accessed more
+        * often.
+        */
+       if (a->mtime < b->mtime)
+               return 1;
+       else if (a->mtime == b->mtime)
+               return 0;
+       return -1;
+}
+
+static void rearrange_packed_git(void)
+{
+       struct packed_git **ary, *p;
+       int i, n;
+
+       for (n = 0, p = packed_git; p; p = p->next)
+               n++;
+       if (n < 2)
+               return;
+
+       /* prepare an array of packed_git for easier sorting */
+       ary = xcalloc(n, sizeof(struct packed_git *));
+       for (n = 0, p = packed_git; p; p = p->next)
+               ary[n++] = p;
+
+       qsort(ary, n, sizeof(struct packed_git *), sort_pack);
+
+       /* link them back again */
+       for (i = 0; i < n - 1; i++)
+               ary[i]->next = ary[i + 1];
+       ary[n - 1]->next = NULL;
+       packed_git = ary[0];
+
+       free(ary);
+}
+
+static int prepare_packed_git_run_once = 0;
+void prepare_packed_git(void)
+{
+       struct alternate_object_database *alt;
+
+       if (prepare_packed_git_run_once)
+               return;
+       prepare_packed_git_one(get_object_directory(), 1);
+       prepare_alt_odb();
+       for (alt = alt_odb_list; alt; alt = alt->next) {
+               alt->name[-1] = 0;
+               prepare_packed_git_one(alt->base, 0);
+               alt->name[-1] = '/';
+       }
+       rearrange_packed_git();
+       prepare_packed_git_run_once = 1;
+}
+
+void reprepare_packed_git(void)
+{
+       prepare_packed_git_run_once = 0;
+       prepare_packed_git();
+}
+
+int check_sha1_signature(const unsigned char *sha1, void *map, unsigned long size, const char *type)
+{
+       unsigned char real_sha1[20];
+       hash_sha1_file(map, size, type, real_sha1);
+       return hashcmp(sha1, real_sha1) ? -1 : 0;
+}
+
+static void *map_sha1_file(const unsigned char *sha1, unsigned long *size)
+{
+       struct stat st;
+       void *map;
+       int fd;
+       char *filename = find_sha1_file(sha1, &st);
+
+       if (!filename) {
+               return NULL;
+       }
+
+       fd = open(filename, O_RDONLY | sha1_file_open_flag);
+       if (fd < 0) {
+               /* See if it works without O_NOATIME */
+               switch (sha1_file_open_flag) {
+               default:
+                       fd = open(filename, O_RDONLY);
+                       if (fd >= 0)
+                               break;
+               /* Fallthrough */
+               case 0:
+                       return NULL;
+               }
+
+               /* If it failed once, it will probably fail again.
+                * Stop using O_NOATIME
+                */
+               sha1_file_open_flag = 0;
+       }
+       *size = xsize_t(st.st_size);
+       map = xmmap(NULL, *size, PROT_READ, MAP_PRIVATE, fd, 0);
+       close(fd);
+       return map;
+}
+
+static int legacy_loose_object(unsigned char *map)
+{
+       unsigned int word;
+
+       /*
+        * Is it a zlib-compressed buffer? If so, the first byte
+        * must be 0x78 (15-bit window size, deflated), and the
+        * first 16-bit word is evenly divisible by 31
+        */
+       word = (map[0] << 8) + map[1];
+       if (map[0] == 0x78 && !(word % 31))
+               return 1;
+       else
+               return 0;
+}
+
+unsigned long unpack_object_header_gently(const unsigned char *buf, unsigned long len, enum object_type *type, unsigned long *sizep)
+{
+       unsigned shift;
+       unsigned char c;
+       unsigned long size;
+       unsigned long used = 0;
+
+       c = buf[used++];
+       *type = (c >> 4) & 7;
+       size = c & 15;
+       shift = 4;
+       while (c & 0x80) {
+               if (len <= used)
+                       return 0;
+               if (sizeof(long) * 8 <= shift)
+                       return 0;
+               c = buf[used++];
+               size += (c & 0x7f) << shift;
+               shift += 7;
+       }
+       *sizep = size;
+       return used;
+}
+
+static int unpack_sha1_header(z_stream *stream, unsigned char *map, unsigned long mapsize, void *buffer, unsigned long bufsiz)
+{
+       unsigned long size, used;
+       static const char valid_loose_object_type[8] = {
+               0, /* OBJ_EXT */
+               1, 1, 1, 1, /* "commit", "tree", "blob", "tag" */
+               0, /* "delta" and others are invalid in a loose object */
+       };
+       enum object_type type;
+
+       /* Get the data stream */
+       memset(stream, 0, sizeof(*stream));
+       stream->next_in = map;
+       stream->avail_in = mapsize;
+       stream->next_out = buffer;
+       stream->avail_out = bufsiz;
+
+       if (legacy_loose_object(map)) {
+               inflateInit(stream);
+               return inflate(stream, 0);
+       }
+
+
+       /*
+        * There used to be a second loose object header format which
+        * was meant to mimic the in-pack format, allowing for direct
+        * copy of the object data.  This format turned up not to be
+        * really worth it and we don't write it any longer.  But we
+        * can still read it.
+        */
+       used = unpack_object_header_gently(map, mapsize, &type, &size);
+       if (!used || !valid_loose_object_type[type])
+               return -1;
+       map += used;
+       mapsize -= used;
+
+       /* Set up the stream for the rest.. */
+       stream->next_in = map;
+       stream->avail_in = mapsize;
+       inflateInit(stream);
+
+       /* And generate the fake traditional header */
+       stream->total_out = 1 + snprintf(buffer, bufsiz, "%s %lu",
+                                        typename(type), size);
+       return 0;
+}
+
+static void *unpack_sha1_rest(z_stream *stream, void *buffer, unsigned long size, const unsigned char *sha1)
+{
+       int bytes = strlen(buffer) + 1;
+       unsigned char *buf = xmalloc(1+size);
+       unsigned long n;
+       int status = Z_OK;
+
+       n = stream->total_out - bytes;
+       if (n > size)
+               n = size;
+       memcpy(buf, (char *) buffer + bytes, n);
+       bytes = n;
+       if (bytes <= size) {
+               /*
+                * The above condition must be (bytes <= size), not
+                * (bytes < size).  In other words, even though we
+                * expect no more output and set avail_out to zer0,
+                * the input zlib stream may have bytes that express
+                * "this concludes the stream", and we *do* want to
+                * eat that input.
+                *
+                * Otherwise we would not be able to test that we
+                * consumed all the input to reach the expected size;
+                * we also want to check that zlib tells us that all
+                * went well with status == Z_STREAM_END at the end.
+                */
+               stream->next_out = buf + bytes;
+               stream->avail_out = size - bytes;
+               while (status == Z_OK)
+                       status = inflate(stream, Z_FINISH);
+       }
+       buf[size] = 0;
+       if (status == Z_STREAM_END && !stream->avail_in) {
+               inflateEnd(stream);
+               return buf;
+       }
+
+       if (status < 0)
+               error("corrupt loose object '%s'", sha1_to_hex(sha1));
+       else if (stream->avail_in)
+               error("garbage at end of loose object '%s'",
+                     sha1_to_hex(sha1));
+       free(buf);
+       return NULL;
+}
+
+/*
+ * We used to just use "sscanf()", but that's actually way
+ * too permissive for what we want to check. So do an anal
+ * object header parse by hand.
+ */
+static int parse_sha1_header(const char *hdr, unsigned long *sizep)
+{
+       char type[10];
+       int i;
+       unsigned long size;
+
+       /*
+        * The type can be at most ten bytes (including the
+        * terminating '\0' that we add), and is followed by
+        * a space.
+        */
+       i = 0;
+       for (;;) {
+               char c = *hdr++;
+               if (c == ' ')
+                       break;
+               type[i++] = c;
+               if (i >= sizeof(type))
+                       return -1;
+       }
+       type[i] = 0;
+
+       /*
+        * The length must follow immediately, and be in canonical
+        * decimal format (ie "010" is not valid).
+        */
+       size = *hdr++ - '0';
+       if (size > 9)
+               return -1;
+       if (size) {
+               for (;;) {
+                       unsigned long c = *hdr - '0';
+                       if (c > 9)
+                               break;
+                       hdr++;
+                       size = size * 10 + c;
+               }
+       }
+       *sizep = size;
+
+       /*
+        * The length must be followed by a zero byte
+        */
+       return *hdr ? -1 : type_from_string(type);
+}
+
+static void *unpack_sha1_file(void *map, unsigned long mapsize, enum object_type *type, unsigned long *size, const unsigned char *sha1)
+{
+       int ret;
+       z_stream stream;
+       char hdr[8192];
+
+       ret = unpack_sha1_header(&stream, map, mapsize, hdr, sizeof(hdr));
+       if (ret < Z_OK || (*type = parse_sha1_header(hdr, size)) < 0)
+               return NULL;
+
+       return unpack_sha1_rest(&stream, hdr, *size, sha1);
+}
+
+unsigned long get_size_from_delta(struct packed_git *p,
+                                 struct pack_window **w_curs,
+                                 off_t curpos)
+{
+       const unsigned char *data;
+       unsigned char delta_head[20], *in;
+       z_stream stream;
+       int st;
+
+       memset(&stream, 0, sizeof(stream));
+       stream.next_out = delta_head;
+       stream.avail_out = sizeof(delta_head);
+
+       inflateInit(&stream);
+       do {
+               in = use_pack(p, w_curs, curpos, &stream.avail_in);
+               stream.next_in = in;
+               st = inflate(&stream, Z_FINISH);
+               curpos += stream.next_in - in;
+       } while ((st == Z_OK || st == Z_BUF_ERROR) &&
+                stream.total_out < sizeof(delta_head));
+       inflateEnd(&stream);
+       if ((st != Z_STREAM_END) && stream.total_out != sizeof(delta_head))
+               die("delta data unpack-initial failed");
+
+       /* Examine the initial part of the delta to figure out
+        * the result size.
+        */
+       data = delta_head;
+
+       /* ignore base size */
+       get_delta_hdr_size(&data, delta_head+sizeof(delta_head));
+
+       /* Read the result size */
+       return get_delta_hdr_size(&data, delta_head+sizeof(delta_head));
+}
+
+static off_t get_delta_base(struct packed_git *p,
+                                   struct pack_window **w_curs,
+                                   off_t *curpos,
+                                   enum object_type type,
+                                   off_t delta_obj_offset)
+{
+       unsigned char *base_info = use_pack(p, w_curs, *curpos, NULL);
+       off_t base_offset;
+
+       /* use_pack() assured us we have [base_info, base_info + 20)
+        * as a range that we can look at without walking off the
+        * end of the mapped window.  Its actually the hash size
+        * that is assured.  An OFS_DELTA longer than the hash size
+        * is stupid, as then a REF_DELTA would be smaller to store.
+        */
+       if (type == OBJ_OFS_DELTA) {
+               unsigned used = 0;
+               unsigned char c = base_info[used++];
+               base_offset = c & 127;
+               while (c & 128) {
+                       base_offset += 1;
+                       if (!base_offset || MSB(base_offset, 7))
+                               die("offset value overflow for delta base object");
+                       c = base_info[used++];
+                       base_offset = (base_offset << 7) + (c & 127);
+               }
+               base_offset = delta_obj_offset - base_offset;
+               if (base_offset >= delta_obj_offset)
+                       die("delta base offset out of bound");
+               *curpos += used;
+       } else if (type == OBJ_REF_DELTA) {
+               /* The base entry _must_ be in the same pack */
+               base_offset = find_pack_entry_one(base_info, p);
+               if (!base_offset)
+                       die("failed to find delta-pack base object %s",
+                               sha1_to_hex(base_info));
+               *curpos += 20;
+       } else
+               die("I am totally screwed");
+       return base_offset;
+}
+
+/* forward declaration for a mutually recursive function */
+static int packed_object_info(struct packed_git *p, off_t offset,
+                             unsigned long *sizep);
+
+static int packed_delta_info(struct packed_git *p,
+                            struct pack_window **w_curs,
+                            off_t curpos,
+                            enum object_type type,
+                            off_t obj_offset,
+                            unsigned long *sizep)
+{
+       off_t base_offset;
+
+       base_offset = get_delta_base(p, w_curs, &curpos, type, obj_offset);
+       type = packed_object_info(p, base_offset, NULL);
+
+       /* We choose to only get the type of the base object and
+        * ignore potentially corrupt pack file that expects the delta
+        * based on a base with a wrong size.  This saves tons of
+        * inflate() calls.
+        */
+       if (sizep)
+               *sizep = get_size_from_delta(p, w_curs, curpos);
+
+       return type;
+}
+
+static int unpack_object_header(struct packed_git *p,
+                               struct pack_window **w_curs,
+                               off_t *curpos,
+                               unsigned long *sizep)
+{
+       unsigned char *base;
+       unsigned int left;
+       unsigned long used;
+       enum object_type type;
+
+       /* use_pack() assures us we have [base, base + 20) available
+        * as a range that we can look at at.  (Its actually the hash
+        * size that is assured.)  With our object header encoding
+        * the maximum deflated object size is 2^137, which is just
+        * insane, so we know won't exceed what we have been given.
+        */
+       base = use_pack(p, w_curs, *curpos, &left);
+       used = unpack_object_header_gently(base, left, &type, sizep);
+       if (!used)
+               die("object offset outside of pack file");
+       *curpos += used;
+
+       return type;
+}
+
+const char *packed_object_info_detail(struct packed_git *p,
+                                     off_t obj_offset,
+                                     unsigned long *size,
+                                     unsigned long *store_size,
+                                     unsigned int *delta_chain_length,
+                                     unsigned char *base_sha1)
+{
+       struct pack_window *w_curs = NULL;
+       off_t curpos;
+       unsigned long dummy;
+       unsigned char *next_sha1;
+       enum object_type type;
+
+       *delta_chain_length = 0;
+       curpos = obj_offset;
+       type = unpack_object_header(p, &w_curs, &curpos, size);
+
+       for (;;) {
+               switch (type) {
+               default:
+                       die("pack %s contains unknown object type %d",
+                           p->pack_name, type);
+               case OBJ_COMMIT:
+               case OBJ_TREE:
+               case OBJ_BLOB:
+               case OBJ_TAG:
+                       *store_size = 0; /* notyet */
+                       unuse_pack(&w_curs);
+                       return typename(type);
+               case OBJ_OFS_DELTA:
+                       obj_offset = get_delta_base(p, &w_curs, &curpos, type, obj_offset);
+                       if (*delta_chain_length == 0) {
+                               /* TODO: find base_sha1 as pointed by curpos */
+                               hashclr(base_sha1);
+                       }
+                       break;
+               case OBJ_REF_DELTA:
+                       next_sha1 = use_pack(p, &w_curs, curpos, NULL);
+                       if (*delta_chain_length == 0)
+                               hashcpy(base_sha1, next_sha1);
+                       obj_offset = find_pack_entry_one(next_sha1, p);
+                       break;
+               }
+               (*delta_chain_length)++;
+               curpos = obj_offset;
+               type = unpack_object_header(p, &w_curs, &curpos, &dummy);
+       }
+}
+
+static int packed_object_info(struct packed_git *p, off_t obj_offset,
+                             unsigned long *sizep)
+{
+       struct pack_window *w_curs = NULL;
+       unsigned long size;
+       off_t curpos = obj_offset;
+       enum object_type type;
+
+       type = unpack_object_header(p, &w_curs, &curpos, &size);
+
+       switch (type) {
+       case OBJ_OFS_DELTA:
+       case OBJ_REF_DELTA:
+               type = packed_delta_info(p, &w_curs, curpos,
+                                        type, obj_offset, sizep);
+               break;
+       case OBJ_COMMIT:
+       case OBJ_TREE:
+       case OBJ_BLOB:
+       case OBJ_TAG:
+               if (sizep)
+                       *sizep = size;
+               break;
+       default:
+               die("pack %s contains unknown object type %d",
+                   p->pack_name, type);
+       }
+       unuse_pack(&w_curs);
+       return type;
+}
+
+static void *unpack_compressed_entry(struct packed_git *p,
+                                   struct pack_window **w_curs,
+                                   off_t curpos,
+                                   unsigned long size)
+{
+       int st;
+       z_stream stream;
+       unsigned char *buffer, *in;
+
+       buffer = xmalloc(size + 1);
+       buffer[size] = 0;
+       memset(&stream, 0, sizeof(stream));
+       stream.next_out = buffer;
+       stream.avail_out = size;
+
+       inflateInit(&stream);
+       do {
+               in = use_pack(p, w_curs, curpos, &stream.avail_in);
+               stream.next_in = in;
+               st = inflate(&stream, Z_FINISH);
+               curpos += stream.next_in - in;
+       } while (st == Z_OK || st == Z_BUF_ERROR);
+       inflateEnd(&stream);
+       if ((st != Z_STREAM_END) || stream.total_out != size) {
+               free(buffer);
+               return NULL;
+       }
+
+       return buffer;
+}
+
+#define MAX_DELTA_CACHE (256)
+
+static size_t delta_base_cached;
+
+static struct delta_base_cache_lru_list {
+       struct delta_base_cache_lru_list *prev;
+       struct delta_base_cache_lru_list *next;
+} delta_base_cache_lru = { &delta_base_cache_lru, &delta_base_cache_lru };
+
+static struct delta_base_cache_entry {
+       struct delta_base_cache_lru_list lru;
+       void *data;
+       struct packed_git *p;
+       off_t base_offset;
+       unsigned long size;
+       enum object_type type;
+} delta_base_cache[MAX_DELTA_CACHE];
+
+static unsigned long pack_entry_hash(struct packed_git *p, off_t base_offset)
+{
+       unsigned long hash;
+
+       hash = (unsigned long)p + (unsigned long)base_offset;
+       hash += (hash >> 8) + (hash >> 16);
+       return hash % MAX_DELTA_CACHE;
+}
+
+static void *cache_or_unpack_entry(struct packed_git *p, off_t base_offset,
+       unsigned long *base_size, enum object_type *type, int keep_cache)
+{
+       void *ret;
+       unsigned long hash = pack_entry_hash(p, base_offset);
+       struct delta_base_cache_entry *ent = delta_base_cache + hash;
+
+       ret = ent->data;
+       if (ret && ent->p == p && ent->base_offset == base_offset)
+               goto found_cache_entry;
+       return unpack_entry(p, base_offset, type, base_size);
+
+found_cache_entry:
+       if (!keep_cache) {
+               ent->data = NULL;
+               ent->lru.next->prev = ent->lru.prev;
+               ent->lru.prev->next = ent->lru.next;
+               delta_base_cached -= ent->size;
+       } else {
+               ret = xmemdupz(ent->data, ent->size);
+       }
+       *type = ent->type;
+       *base_size = ent->size;
+       return ret;
+}
+
+static inline void release_delta_base_cache(struct delta_base_cache_entry *ent)
+{
+       if (ent->data) {
+               free(ent->data);
+               ent->data = NULL;
+               ent->lru.next->prev = ent->lru.prev;
+               ent->lru.prev->next = ent->lru.next;
+               delta_base_cached -= ent->size;
+       }
+}
+
+static void add_delta_base_cache(struct packed_git *p, off_t base_offset,
+       void *base, unsigned long base_size, enum object_type type)
+{
+       unsigned long hash = pack_entry_hash(p, base_offset);
+       struct delta_base_cache_entry *ent = delta_base_cache + hash;
+       struct delta_base_cache_lru_list *lru;
+
+       release_delta_base_cache(ent);
+       delta_base_cached += base_size;
+
+       for (lru = delta_base_cache_lru.next;
+            delta_base_cached > delta_base_cache_limit
+            && lru != &delta_base_cache_lru;
+            lru = lru->next) {
+               struct delta_base_cache_entry *f = (void *)lru;
+               if (f->type == OBJ_BLOB)
+                       release_delta_base_cache(f);
+       }
+       for (lru = delta_base_cache_lru.next;
+            delta_base_cached > delta_base_cache_limit
+            && lru != &delta_base_cache_lru;
+            lru = lru->next) {
+               struct delta_base_cache_entry *f = (void *)lru;
+               release_delta_base_cache(f);
+       }
+
+       ent->p = p;
+       ent->base_offset = base_offset;
+       ent->type = type;
+       ent->data = base;
+       ent->size = base_size;
+       ent->lru.next = &delta_base_cache_lru;
+       ent->lru.prev = delta_base_cache_lru.prev;
+       delta_base_cache_lru.prev->next = &ent->lru;
+       delta_base_cache_lru.prev = &ent->lru;
+}
+
+static void *unpack_delta_entry(struct packed_git *p,
+                               struct pack_window **w_curs,
+                               off_t curpos,
+                               unsigned long delta_size,
+                               off_t obj_offset,
+                               enum object_type *type,
+                               unsigned long *sizep)
+{
+       void *delta_data, *result, *base;
+       unsigned long base_size;
+       off_t base_offset;
+
+       base_offset = get_delta_base(p, w_curs, &curpos, *type, obj_offset);
+       base = cache_or_unpack_entry(p, base_offset, &base_size, type, 0);
+       if (!base)
+               die("failed to read delta base object"
+                   " at %"PRIuMAX" from %s",
+                   (uintmax_t)base_offset, p->pack_name);
+
+       delta_data = unpack_compressed_entry(p, w_curs, curpos, delta_size);
+       if (!delta_data)
+               die("failed to unpack compressed delta"
+                   " at %"PRIuMAX" from %s",
+                   (uintmax_t)curpos, p->pack_name);
+       result = patch_delta(base, base_size,
+                            delta_data, delta_size,
+                            sizep);
+       if (!result)
+               die("failed to apply delta");
+       free(delta_data);
+       add_delta_base_cache(p, base_offset, base, base_size, *type);
+       return result;
+}
+
+void *unpack_entry(struct packed_git *p, off_t obj_offset,
+                  enum object_type *type, unsigned long *sizep)
+{
+       struct pack_window *w_curs = NULL;
+       off_t curpos = obj_offset;
+       void *data;
+
+       *type = unpack_object_header(p, &w_curs, &curpos, sizep);
+       switch (*type) {
+       case OBJ_OFS_DELTA:
+       case OBJ_REF_DELTA:
+               data = unpack_delta_entry(p, &w_curs, curpos, *sizep,
+                                         obj_offset, type, sizep);
+               break;
+       case OBJ_COMMIT:
+       case OBJ_TREE:
+       case OBJ_BLOB:
+       case OBJ_TAG:
+               data = unpack_compressed_entry(p, &w_curs, curpos, *sizep);
+               break;
+       default:
+               die("unknown object type %i in %s", *type, p->pack_name);
+       }
+       unuse_pack(&w_curs);
+       return data;
+}
+
+const unsigned char *nth_packed_object_sha1(struct packed_git *p,
+                                           uint32_t n)
+{
+       const unsigned char *index = p->index_data;
+       if (!index) {
+               if (open_pack_index(p))
+                       return NULL;
+               index = p->index_data;
+       }
+       if (n >= p->num_objects)
+               return NULL;
+       index += 4 * 256;
+       if (p->index_version == 1) {
+               return index + 24 * n + 4;
+       } else {
+               index += 8;
+               return index + 20 * n;
+       }
+}
+
+static off_t nth_packed_object_offset(const struct packed_git *p, uint32_t n)
+{
+       const unsigned char *index = p->index_data;
+       index += 4 * 256;
+       if (p->index_version == 1) {
+               return ntohl(*((uint32_t *)(index + 24 * n)));
+       } else {
+               uint32_t off;
+               index += 8 + p->num_objects * (20 + 4);
+               off = ntohl(*((uint32_t *)(index + 4 * n)));
+               if (!(off & 0x80000000))
+                       return off;
+               index += p->num_objects * 4 + (off & 0x7fffffff) * 8;
+               return (((uint64_t)ntohl(*((uint32_t *)(index + 0)))) << 32) |
+                                  ntohl(*((uint32_t *)(index + 4)));
+       }
+}
+
+off_t find_pack_entry_one(const unsigned char *sha1,
+                                 struct packed_git *p)
+{
+       const uint32_t *level1_ofs = p->index_data;
+       const unsigned char *index = p->index_data;
+       unsigned hi, lo;
+
+       if (!index) {
+               if (open_pack_index(p))
+                       return 0;
+               level1_ofs = p->index_data;
+               index = p->index_data;
+       }
+       if (p->index_version > 1) {
+               level1_ofs += 2;
+               index += 8;
+       }
+       index += 4 * 256;
+       hi = ntohl(level1_ofs[*sha1]);
+       lo = ((*sha1 == 0x0) ? 0 : ntohl(level1_ofs[*sha1 - 1]));
+
+       do {
+               unsigned mi = (lo + hi) / 2;
+               unsigned x = (p->index_version > 1) ? (mi * 20) : (mi * 24 + 4);
+               int cmp = hashcmp(index + x, sha1);
+               if (!cmp)
+                       return nth_packed_object_offset(p, mi);
+               if (cmp > 0)
+                       hi = mi;
+               else
+                       lo = mi+1;
+       } while (lo < hi);
+       return 0;
+}
+
+int matches_pack_name(struct packed_git *p, const char *name)
+{
+       const char *last_c, *c;
+
+       if (!strcmp(p->pack_name, name))
+               return 1;
+
+       for (c = p->pack_name, last_c = c; *c;)
+               if (*c == '/')
+                       last_c = ++c;
+               else
+                       ++c;
+       if (!strcmp(last_c, name))
+               return 1;
+
+       return 0;
+}
+
+static int find_pack_entry(const unsigned char *sha1, struct pack_entry *e, const char **ignore_packed)
+{
+       static struct packed_git *last_found = (void *)1;
+       struct packed_git *p;
+       off_t offset;
+
+       prepare_packed_git();
+       if (!packed_git)
+               return 0;
+       p = (last_found == (void *)1) ? packed_git : last_found;
+
+       do {
+               if (ignore_packed) {
+                       const char **ig;
+                       for (ig = ignore_packed; *ig; ig++)
+                               if (matches_pack_name(p, *ig))
+                                       break;
+                       if (*ig)
+                               goto next;
+               }
+
+               offset = find_pack_entry_one(sha1, p);
+               if (offset) {
+                       /*
+                        * We are about to tell the caller where they can
+                        * locate the requested object.  We better make
+                        * sure the packfile is still here and can be
+                        * accessed before supplying that answer, as
+                        * it may have been deleted since the index
+                        * was loaded!
+                        */
+                       if (p->pack_fd == -1 && open_packed_git(p)) {
+                               error("packfile %s cannot be accessed", p->pack_name);
+                               goto next;
+                       }
+                       e->offset = offset;
+                       e->p = p;
+                       hashcpy(e->sha1, sha1);
+                       last_found = p;
+                       return 1;
+               }
+
+               next:
+               if (p == last_found)
+                       p = packed_git;
+               else
+                       p = p->next;
+               if (p == last_found)
+                       p = p->next;
+       } while (p);
+       return 0;
+}
+
+struct packed_git *find_sha1_pack(const unsigned char *sha1,
+                                 struct packed_git *packs)
+{
+       struct packed_git *p;
+
+       for (p = packs; p; p = p->next) {
+               if (find_pack_entry_one(sha1, p))
+                       return p;
+       }
+       return NULL;
+
+}
+
+static int sha1_loose_object_info(const unsigned char *sha1, unsigned long *sizep)
+{
+       int status;
+       unsigned long mapsize, size;
+       void *map;
+       z_stream stream;
+       char hdr[32];
+
+       map = map_sha1_file(sha1, &mapsize);
+       if (!map)
+               return error("unable to find %s", sha1_to_hex(sha1));
+       if (unpack_sha1_header(&stream, map, mapsize, hdr, sizeof(hdr)) < 0)
+               status = error("unable to unpack %s header",
+                              sha1_to_hex(sha1));
+       else if ((status = parse_sha1_header(hdr, &size)) < 0)
+               status = error("unable to parse %s header", sha1_to_hex(sha1));
+       else if (sizep)
+               *sizep = size;
+       inflateEnd(&stream);
+       munmap(map, mapsize);
+       return status;
+}
+
+int sha1_object_info(const unsigned char *sha1, unsigned long *sizep)
+{
+       struct pack_entry e;
+
+       if (!find_pack_entry(sha1, &e, NULL)) {
+               reprepare_packed_git();
+               if (!find_pack_entry(sha1, &e, NULL))
+                       return sha1_loose_object_info(sha1, sizep);
+       }
+       return packed_object_info(e.p, e.offset, sizep);
+}
+
+static void *read_packed_sha1(const unsigned char *sha1,
+                             enum object_type *type, unsigned long *size)
+{
+       struct pack_entry e;
+
+       if (!find_pack_entry(sha1, &e, NULL))
+               return NULL;
+       else
+               return cache_or_unpack_entry(e.p, e.offset, size, type, 1);
+}
+
+/*
+ * This is meant to hold a *small* number of objects that you would
+ * want read_sha1_file() to be able to return, but yet you do not want
+ * to write them into the object store (e.g. a browse-only
+ * application).
+ */
+static struct cached_object {
+       unsigned char sha1[20];
+       enum object_type type;
+       void *buf;
+       unsigned long size;
+} *cached_objects;
+static int cached_object_nr, cached_object_alloc;
+
+static struct cached_object *find_cached_object(const unsigned char *sha1)
+{
+       int i;
+       struct cached_object *co = cached_objects;
+
+       for (i = 0; i < cached_object_nr; i++, co++) {
+               if (!hashcmp(co->sha1, sha1))
+                       return co;
+       }
+       return NULL;
+}
+
+int pretend_sha1_file(void *buf, unsigned long len, enum object_type type,
+                     unsigned char *sha1)
+{
+       struct cached_object *co;
+
+       hash_sha1_file(buf, len, typename(type), sha1);
+       if (has_sha1_file(sha1) || find_cached_object(sha1))
+               return 0;
+       if (cached_object_alloc <= cached_object_nr) {
+               cached_object_alloc = alloc_nr(cached_object_alloc);
+               cached_objects = xrealloc(cached_objects,
+                                         sizeof(*cached_objects) *
+                                         cached_object_alloc);
+       }
+       co = &cached_objects[cached_object_nr++];
+       co->size = len;
+       co->type = type;
+       co->buf = xmalloc(len);
+       memcpy(co->buf, buf, len);
+       hashcpy(co->sha1, sha1);
+       return 0;
+}
+
+void *read_sha1_file(const unsigned char *sha1, enum object_type *type,
+                    unsigned long *size)
+{
+       unsigned long mapsize;
+       void *map, *buf;
+       struct cached_object *co;
+
+       co = find_cached_object(sha1);
+       if (co) {
+               *type = co->type;
+               *size = co->size;
+               return xmemdupz(co->buf, co->size);
+       }
+
+       buf = read_packed_sha1(sha1, type, size);
+       if (buf)
+               return buf;
+       map = map_sha1_file(sha1, &mapsize);
+       if (map) {
+               buf = unpack_sha1_file(map, mapsize, type, size, sha1);
+               munmap(map, mapsize);
+               return buf;
+       }
+       reprepare_packed_git();
+       return read_packed_sha1(sha1, type, size);
+}
+
+void *read_object_with_reference(const unsigned char *sha1,
+                                const char *required_type_name,
+                                unsigned long *size,
+                                unsigned char *actual_sha1_return)
+{
+       enum object_type type, required_type;
+       void *buffer;
+       unsigned long isize;
+       unsigned char actual_sha1[20];
+
+       required_type = type_from_string(required_type_name);
+       hashcpy(actual_sha1, sha1);
+       while (1) {
+               int ref_length = -1;
+               const char *ref_type = NULL;
+
+               buffer = read_sha1_file(actual_sha1, &type, &isize);
+               if (!buffer)
+                       return NULL;
+               if (type == required_type) {
+                       *size = isize;
+                       if (actual_sha1_return)
+                               hashcpy(actual_sha1_return, actual_sha1);
+                       return buffer;
+               }
+               /* Handle references */
+               else if (type == OBJ_COMMIT)
+                       ref_type = "tree ";
+               else if (type == OBJ_TAG)
+                       ref_type = "object ";
+               else {
+                       free(buffer);
+                       return NULL;
+               }
+               ref_length = strlen(ref_type);
+
+               if (memcmp(buffer, ref_type, ref_length) ||
+                   get_sha1_hex((char *) buffer + ref_length, actual_sha1)) {
+                       free(buffer);
+                       return NULL;
+               }
+               free(buffer);
+               /* Now we have the ID of the referred-to object in
+                * actual_sha1.  Check again. */
+       }
+}
+
+static void write_sha1_file_prepare(const void *buf, unsigned long len,
+                                    const char *type, unsigned char *sha1,
+                                    char *hdr, int *hdrlen)
+{
+       SHA_CTX c;
+
+       /* Generate the header */
+       *hdrlen = sprintf(hdr, "%s %lu", type, len)+1;
+
+       /* Sha1.. */
+       SHA1_Init(&c);
+       SHA1_Update(&c, hdr, *hdrlen);
+       SHA1_Update(&c, buf, len);
+       SHA1_Final(sha1, &c);
+}
+
+/*
+ * Link the tempfile to the final place, possibly creating the
+ * last directory level as you do so.
+ *
+ * Returns the errno on failure, 0 on success.
+ */
+static int link_temp_to_file(const char *tmpfile, const char *filename)
+{
+       int ret;
+       char *dir;
+
+       if (!link(tmpfile, filename))
+               return 0;
+
+       /*
+        * Try to mkdir the last path component if that failed.
+        *
+        * Re-try the "link()" regardless of whether the mkdir
+        * succeeds, since a race might mean that somebody
+        * else succeeded.
+        */
+       ret = errno;
+       dir = strrchr(filename, '/');
+       if (dir) {
+               *dir = 0;
+               if (!mkdir(filename, 0777) && adjust_shared_perm(filename)) {
+                       *dir = '/';
+                       return -2;
+               }
+               *dir = '/';
+               if (!link(tmpfile, filename))
+                       return 0;
+               ret = errno;
+       }
+       return ret;
+}
+
+/*
+ * Move the just written object into its final resting place
+ */
+int move_temp_to_file(const char *tmpfile, const char *filename)
+{
+       int ret = link_temp_to_file(tmpfile, filename);
+
+       /*
+        * Coda hack - coda doesn't like cross-directory links,
+        * so we fall back to a rename, which will mean that it
+        * won't be able to check collisions, but that's not a
+        * big deal.
+        *
+        * The same holds for FAT formatted media.
+        *
+        * When this succeeds, we just return 0. We have nothing
+        * left to unlink.
+        */
+       if (ret && ret != EEXIST) {
+               if (!rename(tmpfile, filename))
+                       return 0;
+               ret = errno;
+       }
+       unlink(tmpfile);
+       if (ret) {
+               if (ret != EEXIST) {
+                       return error("unable to write sha1 filename %s: %s\n", filename, strerror(ret));
+               }
+               /* FIXME!!! Collision check here ? */
+       }
+
+       return 0;
+}
+
+static int write_buffer(int fd, const void *buf, size_t len)
+{
+       if (write_in_full(fd, buf, len) < 0)
+               return error("file write error (%s)", strerror(errno));
+       return 0;
+}
+
+int hash_sha1_file(const void *buf, unsigned long len, const char *type,
+                   unsigned char *sha1)
+{
+       char hdr[32];
+       int hdrlen;
+       write_sha1_file_prepare(buf, len, type, sha1, hdr, &hdrlen);
+       return 0;
+}
+
+int write_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *returnsha1)
+{
+       int size, ret;
+       unsigned char *compressed;
+       z_stream stream;
+       unsigned char sha1[20];
+       char *filename;
+       static char tmpfile[PATH_MAX];
+       char hdr[32];
+       int fd, hdrlen;
+
+       /* Normally if we have it in the pack then we do not bother writing
+        * it out into .git/objects/??/?{38} file.
+        */
+       write_sha1_file_prepare(buf, len, type, sha1, hdr, &hdrlen);
+       filename = sha1_file_name(sha1);
+       if (returnsha1)
+               hashcpy(returnsha1, sha1);
+       if (has_sha1_file(sha1))
+               return 0;
+       fd = open(filename, O_RDONLY);
+       if (fd >= 0) {
+               /*
+                * FIXME!!! We might do collision checking here, but we'd
+                * need to uncompress the old file and check it. Later.
+                */
+               close(fd);
+               return 0;
+       }
+
+       if (errno != ENOENT) {
+               return error("sha1 file %s: %s\n", filename, strerror(errno));
+       }
+
+       snprintf(tmpfile, sizeof(tmpfile), "%s/tmp_obj_XXXXXX", get_object_directory());
+
+       fd = mkstemp(tmpfile);
+       if (fd < 0) {
+               if (errno == EPERM)
+                       return error("insufficient permission for adding an object to repository database %s\n", get_object_directory());
+               else
+                       return error("unable to create temporary sha1 filename %s: %s\n", tmpfile, strerror(errno));
+       }
+
+       /* Set it up */
+       memset(&stream, 0, sizeof(stream));
+       deflateInit(&stream, zlib_compression_level);
+       size = 8 + deflateBound(&stream, len+hdrlen);
+       compressed = xmalloc(size);
+
+       /* Compress it */
+       stream.next_out = compressed;
+       stream.avail_out = size;
+
+       /* First header.. */
+       stream.next_in = (unsigned char *)hdr;
+       stream.avail_in = hdrlen;
+       while (deflate(&stream, 0) == Z_OK)
+               /* nothing */;
+
+       /* Then the data itself.. */
+       stream.next_in = buf;
+       stream.avail_in = len;
+       ret = deflate(&stream, Z_FINISH);
+       if (ret != Z_STREAM_END)
+               die("unable to deflate new object %s (%d)", sha1_to_hex(sha1), ret);
+
+       ret = deflateEnd(&stream);
+       if (ret != Z_OK)
+               die("deflateEnd on object %s failed (%d)", sha1_to_hex(sha1), ret);
+
+       size = stream.total_out;
+
+       if (write_buffer(fd, compressed, size) < 0)
+               die("unable to write sha1 file");
+       fchmod(fd, 0444);
+       if (close(fd))
+               die("unable to write sha1 file");
+       free(compressed);
+
+       return move_temp_to_file(tmpfile, filename);
+}
+
+/*
+ * We need to unpack and recompress the object for writing
+ * it out to a different file.
+ */
+static void *repack_object(const unsigned char *sha1, unsigned long *objsize)
+{
+       size_t size;
+       z_stream stream;
+       unsigned char *unpacked;
+       unsigned long len;
+       enum object_type type;
+       char hdr[32];
+       int hdrlen;
+       void *buf;
+
+       /* need to unpack and recompress it by itself */
+       unpacked = read_packed_sha1(sha1, &type, &len);
+       if (!unpacked)
+               error("cannot read sha1_file for %s", sha1_to_hex(sha1));
+
+       hdrlen = sprintf(hdr, "%s %lu", typename(type), len) + 1;
+
+       /* Set it up */
+       memset(&stream, 0, sizeof(stream));
+       deflateInit(&stream, zlib_compression_level);
+       size = deflateBound(&stream, len + hdrlen);
+       buf = xmalloc(size);
+
+       /* Compress it */
+       stream.next_out = buf;
+       stream.avail_out = size;
+
+       /* First header.. */
+       stream.next_in = (void *)hdr;
+       stream.avail_in = hdrlen;
+       while (deflate(&stream, 0) == Z_OK)
+               /* nothing */;
+
+       /* Then the data itself.. */
+       stream.next_in = unpacked;
+       stream.avail_in = len;
+       while (deflate(&stream, Z_FINISH) == Z_OK)
+               /* nothing */;
+       deflateEnd(&stream);
+       free(unpacked);
+
+       *objsize = stream.total_out;
+       return buf;
+}
+
+int write_sha1_to_fd(int fd, const unsigned char *sha1)
+{
+       int retval;
+       unsigned long objsize;
+       void *buf = map_sha1_file(sha1, &objsize);
+
+       if (buf) {
+               retval = write_buffer(fd, buf, objsize);
+               munmap(buf, objsize);
+               return retval;
+       }
+
+       buf = repack_object(sha1, &objsize);
+       retval = write_buffer(fd, buf, objsize);
+       free(buf);
+       return retval;
+}
+
+int write_sha1_from_fd(const unsigned char *sha1, int fd, char *buffer,
+                      size_t bufsize, size_t *bufposn)
+{
+       char tmpfile[PATH_MAX];
+       int local;
+       z_stream stream;
+       unsigned char real_sha1[20];
+       unsigned char discard[4096];
+       int ret;
+       SHA_CTX c;
+
+       snprintf(tmpfile, sizeof(tmpfile), "%s/tmp_obj_XXXXXX", get_object_directory());
+
+       local = mkstemp(tmpfile);
+       if (local < 0) {
+               if (errno == EPERM)
+                       return error("insufficient permission for adding an object to repository database %s\n", get_object_directory());
+               else
+                       return error("unable to create temporary sha1 filename %s: %s\n", tmpfile, strerror(errno));
+       }
+
+       memset(&stream, 0, sizeof(stream));
+
+       inflateInit(&stream);
+
+       SHA1_Init(&c);
+
+       do {
+               ssize_t size;
+               if (*bufposn) {
+                       stream.avail_in = *bufposn;
+                       stream.next_in = (unsigned char *) buffer;
+                       do {
+                               stream.next_out = discard;
+                               stream.avail_out = sizeof(discard);
+                               ret = inflate(&stream, Z_SYNC_FLUSH);
+                               SHA1_Update(&c, discard, sizeof(discard) -
+                                           stream.avail_out);
+                       } while (stream.avail_in && ret == Z_OK);
+                       if (write_buffer(local, buffer, *bufposn - stream.avail_in) < 0)
+                               die("unable to write sha1 file");
+                       memmove(buffer, buffer + *bufposn - stream.avail_in,
+                               stream.avail_in);
+                       *bufposn = stream.avail_in;
+                       if (ret != Z_OK)
+                               break;
+               }
+               size = xread(fd, buffer + *bufposn, bufsize - *bufposn);
+               if (size <= 0) {
+                       close(local);
+                       unlink(tmpfile);
+                       if (!size)
+                               return error("Connection closed?");
+                       perror("Reading from connection");
+                       return -1;
+               }
+               *bufposn += size;
+       } while (1);
+       inflateEnd(&stream);
+
+       fchmod(local, 0444);
+       if (close(local) != 0)
+               die("unable to write sha1 file");
+       SHA1_Final(real_sha1, &c);
+       if (ret != Z_STREAM_END) {
+               unlink(tmpfile);
+               return error("File %s corrupted", sha1_to_hex(sha1));
+       }
+       if (hashcmp(sha1, real_sha1)) {
+               unlink(tmpfile);
+               return error("File %s has bad hash", sha1_to_hex(sha1));
+       }
+
+       return move_temp_to_file(tmpfile, sha1_file_name(sha1));
+}
+
+int has_pack_index(const unsigned char *sha1)
+{
+       struct stat st;
+       if (stat(sha1_pack_index_name(sha1), &st))
+               return 0;
+       return 1;
+}
+
+int has_pack_file(const unsigned char *sha1)
+{
+       struct stat st;
+       if (stat(sha1_pack_name(sha1), &st))
+               return 0;
+       return 1;
+}
+
+int has_sha1_pack(const unsigned char *sha1, const char **ignore_packed)
+{
+       struct pack_entry e;
+       return find_pack_entry(sha1, &e, ignore_packed);
+}
+
+int has_sha1_file(const unsigned char *sha1)
+{
+       struct stat st;
+       struct pack_entry e;
+
+       if (find_pack_entry(sha1, &e, NULL))
+               return 1;
+       return find_sha1_file(sha1, &st) ? 1 : 0;
+}
+
+int index_pipe(unsigned char *sha1, int fd, const char *type, int write_object)
+{
+       struct strbuf buf;
+       int ret;
+
+       strbuf_init(&buf, 0);
+       if (strbuf_read(&buf, fd, 4096) < 0) {
+               strbuf_release(&buf);
+               return -1;
+       }
+
+       if (!type)
+               type = blob_type;
+       if (write_object)
+               ret = write_sha1_file(buf.buf, buf.len, type, sha1);
+       else
+               ret = hash_sha1_file(buf.buf, buf.len, type, sha1);
+       strbuf_release(&buf);
+
+       return ret;
+}
+
+int index_fd(unsigned char *sha1, int fd, struct stat *st, int write_object,
+            enum object_type type, const char *path)
+{
+       size_t size = xsize_t(st->st_size);
+       void *buf = NULL;
+       int ret, re_allocated = 0;
+
+       if (size)
+               buf = xmmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+       close(fd);
+
+       if (!type)
+               type = OBJ_BLOB;
+
+       /*
+        * Convert blobs to git internal format
+        */
+       if ((type == OBJ_BLOB) && S_ISREG(st->st_mode)) {
+               struct strbuf nbuf;
+               strbuf_init(&nbuf, 0);
+               if (convert_to_git(path, buf, size, &nbuf)) {
+                       munmap(buf, size);
+                       buf = strbuf_detach(&nbuf, &size);
+                       re_allocated = 1;
+               }
+       }
+
+       if (write_object)
+               ret = write_sha1_file(buf, size, typename(type), sha1);
+       else
+               ret = hash_sha1_file(buf, size, typename(type), sha1);
+       if (re_allocated) {
+               free(buf);
+               return ret;
+       }
+       if (size)
+               munmap(buf, size);
+       return ret;
+}
+
+int index_path(unsigned char *sha1, const char *path, struct stat *st, int write_object)
+{
+       int fd;
+       char *target;
+       size_t len;
+
+       switch (st->st_mode & S_IFMT) {
+       case S_IFREG:
+               fd = open(path, O_RDONLY);
+               if (fd < 0)
+                       return error("open(\"%s\"): %s", path,
+                                    strerror(errno));
+               if (index_fd(sha1, fd, st, write_object, OBJ_BLOB, path) < 0)
+                       return error("%s: failed to insert into database",
+                                    path);
+               break;
+       case S_IFLNK:
+               len = xsize_t(st->st_size);
+               target = xmalloc(len + 1);
+               if (readlink(path, target, len + 1) != st->st_size) {
+                       char *errstr = strerror(errno);
+                       free(target);
+                       return error("readlink(\"%s\"): %s", path,
+                                    errstr);
+               }
+               if (!write_object)
+                       hash_sha1_file(target, len, blob_type, sha1);
+               else if (write_sha1_file(target, len, blob_type, sha1))
+                       return error("%s: failed to insert into database",
+                                    path);
+               free(target);
+               break;
+       case S_IFDIR:
+               return resolve_gitlink_ref(path, "HEAD", sha1);
+       default:
+               return error("%s: unsupported file type", path);
+       }
+       return 0;
+}
+
+int read_pack_header(int fd, struct pack_header *header)
+{
+       char *c = (char*)header;
+       ssize_t remaining = sizeof(struct pack_header);
+       do {
+               ssize_t r = xread(fd, c, remaining);
+               if (r <= 0)
+                       /* "eof before pack header was fully read" */
+                       return PH_ERROR_EOF;
+               remaining -= r;
+               c += r;
+       } while (remaining > 0);
+       if (header->hdr_signature != htonl(PACK_SIGNATURE))
+               /* "protocol error (pack signature mismatch detected)" */
+               return PH_ERROR_PACK_SIGNATURE;
+       if (!pack_version_ok(header->hdr_version))
+               /* "protocol error (pack version unsupported)" */
+               return PH_ERROR_PROTOCOL;
+       return 0;
+}
diff --git a/sha1_name.c b/sha1_name.c
new file mode 100644 (file)
index 0000000..13e1164
--- /dev/null
@@ -0,0 +1,720 @@
+#include "cache.h"
+#include "tag.h"
+#include "commit.h"
+#include "tree.h"
+#include "blob.h"
+#include "tree-walk.h"
+#include "refs.h"
+
+static int find_short_object_filename(int len, const char *name, unsigned char *sha1)
+{
+       struct alternate_object_database *alt;
+       char hex[40];
+       int found = 0;
+       static struct alternate_object_database *fakeent;
+
+       if (!fakeent) {
+               const char *objdir = get_object_directory();
+               int objdir_len = strlen(objdir);
+               int entlen = objdir_len + 43;
+               fakeent = xmalloc(sizeof(*fakeent) + entlen);
+               memcpy(fakeent->base, objdir, objdir_len);
+               fakeent->name = fakeent->base + objdir_len + 1;
+               fakeent->name[-1] = '/';
+       }
+       fakeent->next = alt_odb_list;
+
+       sprintf(hex, "%.2s", name);
+       for (alt = fakeent; alt && found < 2; alt = alt->next) {
+               struct dirent *de;
+               DIR *dir;
+               sprintf(alt->name, "%.2s/", name);
+               dir = opendir(alt->base);
+               if (!dir)
+                       continue;
+               while ((de = readdir(dir)) != NULL) {
+                       if (strlen(de->d_name) != 38)
+                               continue;
+                       if (memcmp(de->d_name, name + 2, len - 2))
+                               continue;
+                       if (!found) {
+                               memcpy(hex + 2, de->d_name, 38);
+                               found++;
+                       }
+                       else if (memcmp(hex + 2, de->d_name, 38)) {
+                               found = 2;
+                               break;
+                       }
+               }
+               closedir(dir);
+       }
+       if (found == 1)
+               return get_sha1_hex(hex, sha1) == 0;
+       return found;
+}
+
+static int match_sha(unsigned len, const unsigned char *a, const unsigned char *b)
+{
+       do {
+               if (*a != *b)
+                       return 0;
+               a++;
+               b++;
+               len -= 2;
+       } while (len > 1);
+       if (len)
+               if ((*a ^ *b) & 0xf0)
+                       return 0;
+       return 1;
+}
+
+static int find_short_packed_object(int len, const unsigned char *match, unsigned char *sha1)
+{
+       struct packed_git *p;
+       const unsigned char *found_sha1 = NULL;
+       int found = 0;
+
+       prepare_packed_git();
+       for (p = packed_git; p && found < 2; p = p->next) {
+               uint32_t num, last;
+               uint32_t first = 0;
+               open_pack_index(p);
+               num = p->num_objects;
+               last = num;
+               while (first < last) {
+                       uint32_t mid = (first + last) / 2;
+                       const unsigned char *now;
+                       int cmp;
+
+                       now = nth_packed_object_sha1(p, mid);
+                       cmp = hashcmp(match, now);
+                       if (!cmp) {
+                               first = mid;
+                               break;
+                       }
+                       if (cmp > 0) {
+                               first = mid+1;
+                               continue;
+                       }
+                       last = mid;
+               }
+               if (first < num) {
+                       const unsigned char *now, *next;
+                      now = nth_packed_object_sha1(p, first);
+                       if (match_sha(len, match, now)) {
+                               next = nth_packed_object_sha1(p, first+1);
+                              if (!next|| !match_sha(len, match, next)) {
+                                       /* unique within this pack */
+                                       if (!found) {
+                                               found_sha1 = now;
+                                               found++;
+                                       }
+                                       else if (hashcmp(found_sha1, now)) {
+                                               found = 2;
+                                               break;
+                                       }
+                               }
+                               else {
+                                       /* not even unique within this pack */
+                                       found = 2;
+                                       break;
+                               }
+                       }
+               }
+       }
+       if (found == 1)
+               hashcpy(sha1, found_sha1);
+       return found;
+}
+
+#define SHORT_NAME_NOT_FOUND (-1)
+#define SHORT_NAME_AMBIGUOUS (-2)
+
+static int find_unique_short_object(int len, char *canonical,
+                                   unsigned char *res, unsigned char *sha1)
+{
+       int has_unpacked, has_packed;
+       unsigned char unpacked_sha1[20], packed_sha1[20];
+
+       prepare_alt_odb();
+       has_unpacked = find_short_object_filename(len, canonical, unpacked_sha1);
+       has_packed = find_short_packed_object(len, res, packed_sha1);
+       if (!has_unpacked && !has_packed)
+               return SHORT_NAME_NOT_FOUND;
+       if (1 < has_unpacked || 1 < has_packed)
+               return SHORT_NAME_AMBIGUOUS;
+       if (has_unpacked != has_packed) {
+               hashcpy(sha1, (has_packed ? packed_sha1 : unpacked_sha1));
+               return 0;
+       }
+       /* Both have unique ones -- do they match? */
+       if (hashcmp(packed_sha1, unpacked_sha1))
+               return SHORT_NAME_AMBIGUOUS;
+       hashcpy(sha1, packed_sha1);
+       return 0;
+}
+
+static int get_short_sha1(const char *name, int len, unsigned char *sha1,
+                         int quietly)
+{
+       int i, status;
+       char canonical[40];
+       unsigned char res[20];
+
+       if (len < MINIMUM_ABBREV || len > 40)
+               return -1;
+       hashclr(res);
+       memset(canonical, 'x', 40);
+       for (i = 0; i < len ;i++) {
+               unsigned char c = name[i];
+               unsigned char val;
+               if (c >= '0' && c <= '9')
+                       val = c - '0';
+               else if (c >= 'a' && c <= 'f')
+                       val = c - 'a' + 10;
+               else if (c >= 'A' && c <='F') {
+                       val = c - 'A' + 10;
+                       c -= 'A' - 'a';
+               }
+               else
+                       return -1;
+               canonical[i] = c;
+               if (!(i & 1))
+                       val <<= 4;
+               res[i >> 1] |= val;
+       }
+
+       status = find_unique_short_object(i, canonical, res, sha1);
+       if (!quietly && (status == SHORT_NAME_AMBIGUOUS))
+               return error("short SHA1 %.*s is ambiguous.", len, canonical);
+       return status;
+}
+
+const char *find_unique_abbrev(const unsigned char *sha1, int len)
+{
+       int status, is_null;
+       static char hex[41];
+
+       is_null = is_null_sha1(sha1);
+       memcpy(hex, sha1_to_hex(sha1), 40);
+       if (len == 40 || !len)
+               return hex;
+       while (len < 40) {
+               unsigned char sha1_ret[20];
+               status = get_short_sha1(hex, len, sha1_ret, 1);
+               if (!status ||
+                   (is_null && status != SHORT_NAME_AMBIGUOUS)) {
+                       hex[len] = 0;
+                       return hex;
+               }
+               if (status != SHORT_NAME_AMBIGUOUS)
+                       return NULL;
+               len++;
+       }
+       return NULL;
+}
+
+static int ambiguous_path(const char *path, int len)
+{
+       int slash = 1;
+       int cnt;
+
+       for (cnt = 0; cnt < len; cnt++) {
+               switch (*path++) {
+               case '\0':
+                       break;
+               case '/':
+                       if (slash)
+                               break;
+                       slash = 1;
+                       continue;
+               case '.':
+                       continue;
+               default:
+                       slash = 0;
+                       continue;
+               }
+               break;
+       }
+       return slash;
+}
+
+int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref)
+{
+       const char **p, *r;
+       int refs_found = 0;
+
+       *ref = NULL;
+       for (p = ref_rev_parse_rules; *p; p++) {
+               unsigned char sha1_from_ref[20];
+               unsigned char *this_result;
+
+               this_result = refs_found ? sha1_from_ref : sha1;
+               r = resolve_ref(mkpath(*p, len, str), this_result, 1, NULL);
+               if (r) {
+                       if (!refs_found++)
+                               *ref = xstrdup(r);
+                       if (!warn_ambiguous_refs)
+                               break;
+               }
+       }
+       return refs_found;
+}
+
+int dwim_log(const char *str, int len, unsigned char *sha1, char **log)
+{
+       const char **p;
+       int logs_found = 0;
+
+       *log = NULL;
+       for (p = ref_rev_parse_rules; *p; p++) {
+               struct stat st;
+               unsigned char hash[20];
+               char path[PATH_MAX];
+               const char *ref, *it;
+
+               strcpy(path, mkpath(*p, len, str));
+               ref = resolve_ref(path, hash, 0, NULL);
+               if (!ref)
+                       continue;
+               if (!stat(git_path("logs/%s", path), &st) &&
+                   S_ISREG(st.st_mode))
+                       it = path;
+               else if (strcmp(ref, path) &&
+                        !stat(git_path("logs/%s", ref), &st) &&
+                        S_ISREG(st.st_mode))
+                       it = ref;
+               else
+                       continue;
+               if (!logs_found++) {
+                       *log = xstrdup(it);
+                       hashcpy(sha1, hash);
+               }
+               if (!warn_ambiguous_refs)
+                       break;
+       }
+       return logs_found;
+}
+
+static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
+{
+       static const char *warning = "warning: refname '%.*s' is ambiguous.\n";
+       char *real_ref = NULL;
+       int refs_found = 0;
+       int at, reflog_len;
+
+       if (len == 40 && !get_sha1_hex(str, sha1))
+               return 0;
+
+       /* basic@{time or number} format to query ref-log */
+       reflog_len = at = 0;
+       if (str[len-1] == '}') {
+               for (at = 0; at < len - 1; at++) {
+                       if (str[at] == '@' && str[at+1] == '{') {
+                               reflog_len = (len-1) - (at+2);
+                               len = at;
+                               break;
+                       }
+               }
+       }
+
+       /* Accept only unambiguous ref paths. */
+       if (len && ambiguous_path(str, len))
+               return -1;
+
+       if (!len && reflog_len) {
+               /* allow "@{...}" to mean the current branch reflog */
+               refs_found = dwim_ref("HEAD", 4, sha1, &real_ref);
+       } else if (reflog_len)
+               refs_found = dwim_log(str, len, sha1, &real_ref);
+       else
+               refs_found = dwim_ref(str, len, sha1, &real_ref);
+
+       if (!refs_found)
+               return -1;
+
+       if (warn_ambiguous_refs && refs_found > 1)
+               fprintf(stderr, warning, len, str);
+
+       if (reflog_len) {
+               int nth, i;
+               unsigned long at_time;
+               unsigned long co_time;
+               int co_tz, co_cnt;
+
+               /* Is it asking for N-th entry, or approxidate? */
+               for (i = nth = 0; 0 <= nth && i < reflog_len; i++) {
+                       char ch = str[at+2+i];
+                       if ('0' <= ch && ch <= '9')
+                               nth = nth * 10 + ch - '0';
+                       else
+                               nth = -1;
+               }
+               if (0 <= nth)
+                       at_time = 0;
+               else
+                       at_time = approxidate(str + at + 2);
+               if (read_ref_at(real_ref, at_time, nth, sha1, NULL,
+                               &co_time, &co_tz, &co_cnt)) {
+                       if (at_time)
+                               fprintf(stderr,
+                                       "warning: Log for '%.*s' only goes "
+                                       "back to %s.\n", len, str,
+                                       show_date(co_time, co_tz, DATE_RFC2822));
+                       else
+                               fprintf(stderr,
+                                       "warning: Log for '%.*s' only has "
+                                       "%d entries.\n", len, str, co_cnt);
+               }
+       }
+
+       free(real_ref);
+       return 0;
+}
+
+static int get_sha1_1(const char *name, int len, unsigned char *sha1);
+
+static int get_parent(const char *name, int len,
+                     unsigned char *result, int idx)
+{
+       unsigned char sha1[20];
+       int ret = get_sha1_1(name, len, sha1);
+       struct commit *commit;
+       struct commit_list *p;
+
+       if (ret)
+               return ret;
+       commit = lookup_commit_reference(sha1);
+       if (!commit)
+               return -1;
+       if (parse_commit(commit))
+               return -1;
+       if (!idx) {
+               hashcpy(result, commit->object.sha1);
+               return 0;
+       }
+       p = commit->parents;
+       while (p) {
+               if (!--idx) {
+                       hashcpy(result, p->item->object.sha1);
+                       return 0;
+               }
+               p = p->next;
+       }
+       return -1;
+}
+
+static int get_nth_ancestor(const char *name, int len,
+                           unsigned char *result, int generation)
+{
+       unsigned char sha1[20];
+       int ret = get_sha1_1(name, len, sha1);
+       if (ret)
+               return ret;
+
+       while (generation--) {
+               struct commit *commit = lookup_commit_reference(sha1);
+
+               if (!commit || parse_commit(commit) || !commit->parents)
+                       return -1;
+               hashcpy(sha1, commit->parents->item->object.sha1);
+       }
+       hashcpy(result, sha1);
+       return 0;
+}
+
+static int peel_onion(const char *name, int len, unsigned char *sha1)
+{
+       unsigned char outer[20];
+       const char *sp;
+       unsigned int expected_type = 0;
+       struct object *o;
+
+       /*
+        * "ref^{type}" dereferences ref repeatedly until you cannot
+        * dereference anymore, or you get an object of given type,
+        * whichever comes first.  "ref^{}" means just dereference
+        * tags until you get a non-tag.  "ref^0" is a shorthand for
+        * "ref^{commit}".  "commit^{tree}" could be used to find the
+        * top-level tree of the given commit.
+        */
+       if (len < 4 || name[len-1] != '}')
+               return -1;
+
+       for (sp = name + len - 1; name <= sp; sp--) {
+               int ch = *sp;
+               if (ch == '{' && name < sp && sp[-1] == '^')
+                       break;
+       }
+       if (sp <= name)
+               return -1;
+
+       sp++; /* beginning of type name, or closing brace for empty */
+       if (!strncmp(commit_type, sp, 6) && sp[6] == '}')
+               expected_type = OBJ_COMMIT;
+       else if (!strncmp(tree_type, sp, 4) && sp[4] == '}')
+               expected_type = OBJ_TREE;
+       else if (!strncmp(blob_type, sp, 4) && sp[4] == '}')
+               expected_type = OBJ_BLOB;
+       else if (sp[0] == '}')
+               expected_type = OBJ_NONE;
+       else
+               return -1;
+
+       if (get_sha1_1(name, sp - name - 2, outer))
+               return -1;
+
+       o = parse_object(outer);
+       if (!o)
+               return -1;
+       if (!expected_type) {
+               o = deref_tag(o, name, sp - name - 2);
+               if (!o || (!o->parsed && !parse_object(o->sha1)))
+                       return -1;
+               hashcpy(sha1, o->sha1);
+       }
+       else {
+               /* At this point, the syntax look correct, so
+                * if we do not get the needed object, we should
+                * barf.
+                */
+
+               while (1) {
+                       if (!o || (!o->parsed && !parse_object(o->sha1)))
+                               return -1;
+                       if (o->type == expected_type) {
+                               hashcpy(sha1, o->sha1);
+                               return 0;
+                       }
+                       if (o->type == OBJ_TAG)
+                               o = ((struct tag*) o)->tagged;
+                       else if (o->type == OBJ_COMMIT)
+                               o = &(((struct commit *) o)->tree->object);
+                       else
+                               return error("%.*s: expected %s type, but the object dereferences to %s type",
+                                            len, name, typename(expected_type),
+                                            typename(o->type));
+                       if (!o->parsed)
+                               parse_object(o->sha1);
+               }
+       }
+       return 0;
+}
+
+static int get_describe_name(const char *name, int len, unsigned char *sha1)
+{
+       const char *cp;
+
+       for (cp = name + len - 1; name + 2 <= cp; cp--) {
+               char ch = *cp;
+               if (hexval(ch) & ~0377) {
+                       /* We must be looking at g in "SOMETHING-g"
+                        * for it to be describe output.
+                        */
+                       if (ch == 'g' && cp[-1] == '-') {
+                               cp++;
+                               len -= cp - name;
+                               return get_short_sha1(cp, len, sha1, 1);
+                       }
+               }
+       }
+       return -1;
+}
+
+static int get_sha1_1(const char *name, int len, unsigned char *sha1)
+{
+       int ret, has_suffix;
+       const char *cp;
+
+       /* "name~3" is "name^^^",
+        * "name~" and "name~0" are name -- not "name^0"!
+        * "name^" is not "name^0"; it is "name^1".
+        */
+       has_suffix = 0;
+       for (cp = name + len - 1; name <= cp; cp--) {
+               int ch = *cp;
+               if ('0' <= ch && ch <= '9')
+                       continue;
+               if (ch == '~' || ch == '^')
+                       has_suffix = ch;
+               break;
+       }
+
+       if (has_suffix) {
+               int num = 0;
+               int len1 = cp - name;
+               cp++;
+               while (cp < name + len)
+                       num = num * 10 + *cp++ - '0';
+               if (has_suffix == '^') {
+                       if (!num && len1 == len - 1)
+                               num = 1;
+                       return get_parent(name, len1, sha1, num);
+               }
+               /* else if (has_suffix == '~') -- goes without saying */
+               return get_nth_ancestor(name, len1, sha1, num);
+       }
+
+       ret = peel_onion(name, len, sha1);
+       if (!ret)
+               return 0;
+
+       ret = get_sha1_basic(name, len, sha1);
+       if (!ret)
+               return 0;
+
+       /* It could be describe output that is "SOMETHING-gXXXX" */
+       ret = get_describe_name(name, len, sha1);
+       if (!ret)
+               return 0;
+
+       return get_short_sha1(name, len, sha1, 0);
+}
+
+static int handle_one_ref(const char *path,
+               const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct commit_list **list = cb_data;
+       struct object *object = parse_object(sha1);
+       if (!object)
+               return 0;
+       if (object->type == OBJ_TAG)
+               object = deref_tag(object, path, strlen(path));
+       if (object->type != OBJ_COMMIT)
+               return 0;
+       insert_by_date((struct commit *)object, list);
+       return 0;
+}
+
+/*
+ * This interprets names like ':/Initial revision of "git"' by searching
+ * through history and returning the first commit whose message starts
+ * with the given string.
+ *
+ * For future extension, ':/!' is reserved. If you want to match a message
+ * beginning with a '!', you have to repeat the exclamation mark.
+ */
+
+#define ONELINE_SEEN (1u<<20)
+static int get_sha1_oneline(const char *prefix, unsigned char *sha1)
+{
+       struct commit_list *list = NULL, *backup = NULL, *l;
+       int retval = -1;
+       char *temp_commit_buffer = NULL;
+
+       if (prefix[0] == '!') {
+               if (prefix[1] != '!')
+                       die ("Invalid search pattern: %s", prefix);
+               prefix++;
+       }
+       for_each_ref(handle_one_ref, &list);
+       for (l = list; l; l = l->next)
+               commit_list_insert(l->item, &backup);
+       while (list) {
+               char *p;
+               struct commit *commit;
+               enum object_type type;
+               unsigned long size;
+
+               commit = pop_most_recent_commit(&list, ONELINE_SEEN);
+               parse_object(commit->object.sha1);
+               if (temp_commit_buffer)
+                       free(temp_commit_buffer);
+               if (commit->buffer)
+                       p = commit->buffer;
+               else {
+                       p = read_sha1_file(commit->object.sha1, &type, &size);
+                       if (!p)
+                               continue;
+                       temp_commit_buffer = p;
+               }
+               if (!(p = strstr(p, "\n\n")))
+                       continue;
+               if (!prefixcmp(p + 2, prefix)) {
+                       hashcpy(sha1, commit->object.sha1);
+                       retval = 0;
+                       break;
+               }
+       }
+       if (temp_commit_buffer)
+               free(temp_commit_buffer);
+       free_commit_list(list);
+       for (l = backup; l; l = l->next)
+               clear_commit_marks(l->item, ONELINE_SEEN);
+       return retval;
+}
+
+/*
+ * This is like "get_sha1_basic()", except it allows "sha1 expressions",
+ * notably "xyz^" for "parent of xyz"
+ */
+int get_sha1(const char *name, unsigned char *sha1)
+{
+       unsigned unused;
+       return get_sha1_with_mode(name, sha1, &unused);
+}
+
+int get_sha1_with_mode(const char *name, unsigned char *sha1, unsigned *mode)
+{
+       int ret, bracket_depth;
+       int namelen = strlen(name);
+       const char *cp;
+
+       *mode = S_IFINVALID;
+       ret = get_sha1_1(name, namelen, sha1);
+       if (!ret)
+               return ret;
+       /* sha1:path --> object name of path in ent sha1
+        * :path -> object name of path in index
+        * :[0-3]:path -> object name of path in index at stage
+        */
+       if (name[0] == ':') {
+               int stage = 0;
+               struct cache_entry *ce;
+               int pos;
+               if (namelen > 2 && name[1] == '/')
+                       return get_sha1_oneline(name + 2, sha1);
+               if (namelen < 3 ||
+                   name[2] != ':' ||
+                   name[1] < '0' || '3' < name[1])
+                       cp = name + 1;
+               else {
+                       stage = name[1] - '0';
+                       cp = name + 3;
+               }
+               namelen = namelen - (cp - name);
+               if (!active_cache)
+                       read_cache();
+               pos = cache_name_pos(cp, namelen);
+               if (pos < 0)
+                       pos = -pos - 1;
+               while (pos < active_nr) {
+                       ce = active_cache[pos];
+                       if (ce_namelen(ce) != namelen ||
+                           memcmp(ce->name, cp, namelen))
+                               break;
+                       if (ce_stage(ce) == stage) {
+                               hashcpy(sha1, ce->sha1);
+                               *mode = ntohl(ce->ce_mode);
+                               return 0;
+                       }
+                       pos++;
+               }
+               return -1;
+       }
+       for (cp = name, bracket_depth = 0; *cp; cp++) {
+               if (*cp == '{')
+                       bracket_depth++;
+               else if (bracket_depth && *cp == '}')
+                       bracket_depth--;
+               else if (!bracket_depth && *cp == ':')
+                       break;
+       }
+       if (*cp == ':') {
+               unsigned char tree_sha1[20];
+               if (!get_sha1_1(name, cp-name, tree_sha1))
+                       return get_tree_entry(tree_sha1, cp+1, sha1,
+                                             mode);
+       }
+       return ret;
+}
diff --git a/shallow.c b/shallow.c
new file mode 100644 (file)
index 0000000..dbd9f5a
--- /dev/null
+++ b/shallow.c
@@ -0,0 +1,103 @@
+#include "cache.h"
+#include "commit.h"
+#include "tag.h"
+
+static int is_shallow = -1;
+
+int register_shallow(const unsigned char *sha1)
+{
+       struct commit_graft *graft =
+               xmalloc(sizeof(struct commit_graft));
+       struct commit *commit = lookup_commit(sha1);
+
+       hashcpy(graft->sha1, sha1);
+       graft->nr_parent = -1;
+       if (commit && commit->object.parsed)
+               commit->parents = NULL;
+       return register_commit_graft(graft, 0);
+}
+
+int is_repository_shallow(void)
+{
+       FILE *fp;
+       char buf[1024];
+
+       if (is_shallow >= 0)
+               return is_shallow;
+
+       fp = fopen(git_path("shallow"), "r");
+       if (!fp) {
+               is_shallow = 0;
+               return is_shallow;
+       }
+       is_shallow = 1;
+
+       while (fgets(buf, sizeof(buf), fp)) {
+               unsigned char sha1[20];
+               if (get_sha1_hex(buf, sha1))
+                       die("bad shallow line: %s", buf);
+               register_shallow(sha1);
+       }
+       fclose(fp);
+       return is_shallow;
+}
+
+struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
+               int shallow_flag, int not_shallow_flag)
+{
+       int i = 0, cur_depth = 0;
+       struct commit_list *result = NULL;
+       struct object_array stack = {0, 0, NULL};
+       struct commit *commit = NULL;
+
+       while (commit || i < heads->nr || stack.nr) {
+               struct commit_list *p;
+               if (!commit) {
+                       if (i < heads->nr) {
+                               commit = (struct commit *)
+                                       deref_tag(heads->objects[i++].item, NULL, 0);
+                               if (commit->object.type != OBJ_COMMIT) {
+                                       commit = NULL;
+                                       continue;
+                               }
+                               if (!commit->util)
+                                       commit->util = xmalloc(sizeof(int));
+                               *(int *)commit->util = 0;
+                               cur_depth = 0;
+                       } else {
+                               commit = (struct commit *)
+                                       stack.objects[--stack.nr].item;
+                               cur_depth = *(int *)commit->util;
+                       }
+               }
+               parse_commit(commit);
+               commit->object.flags |= not_shallow_flag;
+               cur_depth++;
+               for (p = commit->parents, commit = NULL; p; p = p->next) {
+                       if (!p->item->util) {
+                               int *pointer = xmalloc(sizeof(int));
+                               p->item->util = pointer;
+                               *pointer =  cur_depth;
+                       } else {
+                               int *pointer = p->item->util;
+                               if (cur_depth >= *pointer)
+                                       continue;
+                               *pointer = cur_depth;
+                       }
+                       if (cur_depth < depth) {
+                               if (p->next)
+                                       add_object_array(&p->item->object,
+                                                       NULL, &stack);
+                               else {
+                                       commit = p->item;
+                                       cur_depth = *(int *)commit->util;
+                               }
+                       } else {
+                               commit_list_insert(p->item, &result);
+                               p->item->object.flags |= shallow_flag;
+                       }
+               }
+       }
+
+       return result;
+}
diff --git a/shell.c b/shell.c
new file mode 100644 (file)
index 0000000..9826109
--- /dev/null
+++ b/shell.c
@@ -0,0 +1,80 @@
+#include "cache.h"
+#include "quote.h"
+#include "exec_cmd.h"
+#include "strbuf.h"
+
+static int do_generic_cmd(const char *me, char *arg)
+{
+       const char *my_argv[4];
+
+       if (!arg || !(arg = sq_dequote(arg)))
+               die("bad argument");
+       if (prefixcmp(me, "git-"))
+               die("bad command");
+
+       my_argv[0] = me + 4;
+       my_argv[1] = arg;
+       my_argv[2] = NULL;
+
+       return execv_git_cmd(my_argv);
+}
+
+static int do_cvs_cmd(const char *me, char *arg)
+{
+       const char *cvsserver_argv[3] = {
+               "cvsserver", "server", NULL
+       };
+
+       if (!arg || strcmp(arg, "server"))
+               die("git-cvsserver only handles server: %s", arg);
+
+       setup_path(NULL);
+
+       return execv_git_cmd(cvsserver_argv);
+}
+
+
+static struct commands {
+       const char *name;
+       int (*exec)(const char *me, char *arg);
+} cmd_list[] = {
+       { "git-receive-pack", do_generic_cmd },
+       { "git-upload-pack", do_generic_cmd },
+       { "cvs", do_cvs_cmd },
+       { NULL },
+};
+
+int main(int argc, char **argv)
+{
+       char *prog;
+       struct commands *cmd;
+
+       if (argc == 2 && !strcmp(argv[1], "cvs server"))
+               argv--;
+       /* We want to see "-c cmd args", and nothing else */
+       else if (argc != 3 || strcmp(argv[1], "-c"))
+               die("What do you think I am? A shell?");
+
+       prog = argv[2];
+       argv += 2;
+       argc -= 2;
+       for (cmd = cmd_list ; cmd->name ; cmd++) {
+               int len = strlen(cmd->name);
+               char *arg;
+               if (strncmp(cmd->name, prog, len))
+                       continue;
+               arg = NULL;
+               switch (prog[len]) {
+               case '\0':
+                       arg = NULL;
+                       break;
+               case ' ':
+                       arg = prog + len + 1;
+                       break;
+               default:
+                       continue;
+               }
+               exit(cmd->exec(cmd->name, arg));
+       }
+       die("unrecognized command '%s'", prog);
+}
diff --git a/show-index.c b/show-index.c
new file mode 100644 (file)
index 0000000..7253991
--- /dev/null
@@ -0,0 +1,78 @@
+#include "cache.h"
+#include "pack.h"
+
+int main(int argc, char **argv)
+{
+       int i;
+       unsigned nr;
+       unsigned int version;
+       static unsigned int top_index[256];
+
+       if (fread(top_index, 2 * 4, 1, stdin) != 1)
+               die("unable to read header");
+       if (top_index[0] == htonl(PACK_IDX_SIGNATURE)) {
+               version = ntohl(top_index[1]);
+               if (version < 2 || version > 2)
+                       die("unknown index version");
+               if (fread(top_index, 256 * 4, 1, stdin) != 1)
+                       die("unable to read index");
+       } else {
+               version = 1;
+               if (fread(&top_index[2], 254 * 4, 1, stdin) != 1)
+                       die("unable to read index");
+       }
+       nr = 0;
+       for (i = 0; i < 256; i++) {
+               unsigned n = ntohl(top_index[i]);
+               if (n < nr)
+                       die("corrupt index file");
+               nr = n;
+       }
+       if (version == 1) {
+               for (i = 0; i < nr; i++) {
+                       unsigned int offset, entry[6];
+
+                       if (fread(entry, 4 + 20, 1, stdin) != 1)
+                               die("unable to read entry %u/%u", i, nr);
+                       offset = ntohl(entry[0]);
+                       printf("%u %s\n", offset, sha1_to_hex((void *)(entry+1)));
+               }
+       } else {
+               unsigned off64_nr = 0;
+               struct {
+                       unsigned char sha1[20];
+                       uint32_t crc;
+                       uint32_t off;
+               } *entries = xmalloc(nr * sizeof(entries[0]));
+               for (i = 0; i < nr; i++)
+                       if (fread(entries[i].sha1, 20, 1, stdin) != 1)
+                               die("unable to read sha1 %u/%u", i, nr);
+               for (i = 0; i < nr; i++)
+                       if (fread(&entries[i].crc, 4, 1, stdin) != 1)
+                               die("unable to read crc %u/%u", i, nr);
+               for (i = 0; i < nr; i++)
+                       if (fread(&entries[i].off, 4, 1, stdin) != 1)
+                               die("unable to read 32b offset %u/%u", i, nr);
+               for (i = 0; i < nr; i++) {
+                       uint64_t offset;
+                       uint32_t off = ntohl(entries[i].off);
+                       if (!(off & 0x80000000)) {
+                               offset = off;
+                       } else {
+                               uint32_t off64[2];
+                               if ((off & 0x7fffffff) != off64_nr)
+                                       die("inconsistent 64b offset index");
+                               if (fread(off64, 8, 1, stdin) != 1)
+                                       die("unable to read 64b offset %u", off64_nr);
+                               offset = (((uint64_t)ntohl(off64[0])) << 32) |
+                                                    ntohl(off64[1]);
+                               off64_nr++;
+                       }
+                       printf("%" PRIuMAX " %s (%08x)\n", (uintmax_t) offset,
+                              sha1_to_hex(entries[i].sha1),
+                              ntohl(entries[i].crc));
+               }
+               free(entries);
+       }
+       return 0;
+}
diff --git a/sideband.c b/sideband.c
new file mode 100644 (file)
index 0000000..756bbc2
--- /dev/null
@@ -0,0 +1,116 @@
+#include "pkt-line.h"
+#include "sideband.h"
+
+/*
+ * Receive multiplexed output stream over git native protocol.
+ * in_stream is the input stream from the remote, which carries data
+ * in pkt_line format with band designator.  Demultiplex it into out
+ * and err and return error appropriately.  Band #1 carries the
+ * primary payload.  Things coming over band #2 is not necessarily
+ * error; they are usually informative message on the standard error
+ * stream, aka "verbose").  A message over band #3 is a signal that
+ * the remote died unexpectedly.  A flush() concludes the stream.
+ */
+
+#define PREFIX "remote:"
+#define SUFFIX "\033[K"  /* change to "        " if ANSI sequences don't work */
+
+int recv_sideband(const char *me, int in_stream, int out, int err)
+{
+       unsigned pf = strlen(PREFIX);
+       unsigned sf = strlen(SUFFIX);
+       char buf[pf + LARGE_PACKET_MAX + sf + 1];
+       memcpy(buf, PREFIX, pf);
+       while (1) {
+               int band, len;
+               len = packet_read_line(in_stream, buf + pf, LARGE_PACKET_MAX);
+               if (len == 0)
+                       break;
+               if (len < 1) {
+                       len = sprintf(buf, "%s: protocol error: no band designator\n", me);
+                       safe_write(err, buf, len);
+                       return SIDEBAND_PROTOCOL_ERROR;
+               }
+               band = buf[pf] & 0xff;
+               len--;
+               switch (band) {
+               case 3:
+                       buf[pf] = ' ';
+                       buf[pf+1+len] = '\n';
+                       safe_write(err, buf, pf+1+len+1);
+                       return SIDEBAND_REMOTE_ERROR;
+               case 2:
+                       buf[pf] = ' ';
+                       len += pf+1;
+                       while (1) {
+                               int brk = pf+1;
+
+                               /* Break the buffer into separate lines. */
+                               while (brk < len) {
+                                       brk++;
+                                       if (buf[brk-1] == '\n' ||
+                                           buf[brk-1] == '\r')
+                                               break;
+                               }
+
+                               /*
+                                * Let's insert a suffix to clear the end
+                                * of the screen line, but only if current
+                                * line data actually contains something.
+                                */
+                               if (brk > pf+1 + 1) {
+                                       char save[sf];
+                                       memcpy(save, buf + brk, sf);
+                                       buf[brk + sf - 1] = buf[brk - 1];
+                                       memcpy(buf + brk - 1, SUFFIX, sf);
+                                       safe_write(err, buf, brk + sf);
+                                       memcpy(buf + brk, save, sf);
+                               } else
+                                       safe_write(err, buf, brk);
+
+                               if (brk < len) {
+                                       memmove(buf + pf+1, buf + brk, len - brk);
+                                       len = len - brk + pf+1;
+                               } else
+                                       break;
+                       }
+                       continue;
+               case 1:
+                       safe_write(out, buf + pf+1, len);
+                       continue;
+               default:
+                       len = sprintf(buf,
+                                     "%s: protocol error: bad band #%d\n",
+                                     me, band);
+                       safe_write(err, buf, len);
+                       return SIDEBAND_PROTOCOL_ERROR;
+               }
+       }
+       return 0;
+}
+
+/*
+ * fd is connected to the remote side; send the sideband data
+ * over multiplexed packet stream.
+ */
+ssize_t send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_max)
+{
+       ssize_t ssz = sz;
+       const char *p = data;
+
+       while (sz) {
+               unsigned n;
+               char hdr[5];
+
+               n = sz;
+               if (packet_max - 5 < n)
+                       n = packet_max - 5;
+               sprintf(hdr, "%04x", n + 5);
+               hdr[4] = band;
+               safe_write(fd, hdr, 5);
+               safe_write(fd, p, n);
+               p += n;
+               sz -= n;
+       }
+       return ssz;
+}
diff --git a/sideband.h b/sideband.h
new file mode 100644 (file)
index 0000000..a84b691
--- /dev/null
@@ -0,0 +1,13 @@
+#ifndef SIDEBAND_H
+#define SIDEBAND_H
+
+#define SIDEBAND_PROTOCOL_ERROR -2
+#define SIDEBAND_REMOTE_ERROR -1
+
+#define DEFAULT_PACKET_MAX 1000
+#define LARGE_PACKET_MAX 65520
+
+int recv_sideband(const char *me, int in_stream, int out, int err);
+ssize_t send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_max);
+
+#endif
diff --git a/strbuf.c b/strbuf.c
new file mode 100644 (file)
index 0000000..b9b194b
--- /dev/null
+++ b/strbuf.c
@@ -0,0 +1,234 @@
+#include "cache.h"
+
+/*
+ * Used as the default ->buf value, so that people can always assume
+ * buf is non NULL and ->buf is NUL terminated even for a freshly
+ * initialized strbuf.
+ */
+char strbuf_slopbuf[1];
+
+void strbuf_init(struct strbuf *sb, size_t hint)
+{
+       sb->alloc = sb->len = 0;
+       sb->buf = strbuf_slopbuf;
+       if (hint)
+               strbuf_grow(sb, hint);
+}
+
+void strbuf_release(struct strbuf *sb)
+{
+       if (sb->alloc) {
+               free(sb->buf);
+               strbuf_init(sb, 0);
+       }
+}
+
+char *strbuf_detach(struct strbuf *sb, size_t *sz)
+{
+       char *res = sb->alloc ? sb->buf : NULL;
+       if (sz)
+               *sz = sb->len;
+       strbuf_init(sb, 0);
+       return res;
+}
+
+void strbuf_attach(struct strbuf *sb, void *buf, size_t len, size_t alloc)
+{
+       strbuf_release(sb);
+       sb->buf   = buf;
+       sb->len   = len;
+       sb->alloc = alloc;
+       strbuf_grow(sb, 0);
+       sb->buf[sb->len] = '\0';
+}
+
+void strbuf_grow(struct strbuf *sb, size_t extra)
+{
+       if (sb->len + extra + 1 <= sb->len)
+               die("you want to use way too much memory");
+       if (!sb->alloc)
+               sb->buf = NULL;
+       ALLOC_GROW(sb->buf, sb->len + extra + 1, sb->alloc);
+}
+
+void strbuf_rtrim(struct strbuf *sb)
+{
+       while (sb->len > 0 && isspace((unsigned char)sb->buf[sb->len - 1]))
+               sb->len--;
+       sb->buf[sb->len] = '\0';
+}
+
+int strbuf_cmp(struct strbuf *a, struct strbuf *b)
+{
+       int cmp;
+       if (a->len < b->len) {
+               cmp = memcmp(a->buf, b->buf, a->len);
+               return cmp ? cmp : -1;
+       } else {
+               cmp = memcmp(a->buf, b->buf, b->len);
+               return cmp ? cmp : a->len != b->len;
+       }
+}
+
+void strbuf_splice(struct strbuf *sb, size_t pos, size_t len,
+                                  const void *data, size_t dlen)
+{
+       if (pos + len < pos)
+               die("you want to use way too much memory");
+       if (pos > sb->len)
+               die("`pos' is too far after the end of the buffer");
+       if (pos + len > sb->len)
+               die("`pos + len' is too far after the end of the buffer");
+
+       if (dlen >= len)
+               strbuf_grow(sb, dlen - len);
+       memmove(sb->buf + pos + dlen,
+                       sb->buf + pos + len,
+                       sb->len - pos - len);
+       memcpy(sb->buf + pos, data, dlen);
+       strbuf_setlen(sb, sb->len + dlen - len);
+}
+
+void strbuf_insert(struct strbuf *sb, size_t pos, const void *data, size_t len)
+{
+       strbuf_splice(sb, pos, 0, data, len);
+}
+
+void strbuf_remove(struct strbuf *sb, size_t pos, size_t len)
+{
+       strbuf_splice(sb, pos, len, NULL, 0);
+}
+
+void strbuf_add(struct strbuf *sb, const void *data, size_t len)
+{
+       strbuf_grow(sb, len);
+       memcpy(sb->buf + sb->len, data, len);
+       strbuf_setlen(sb, sb->len + len);
+}
+
+void strbuf_adddup(struct strbuf *sb, size_t pos, size_t len)
+{
+       strbuf_grow(sb, len);
+       memcpy(sb->buf + sb->len, sb->buf + pos, len);
+       strbuf_setlen(sb, sb->len + len);
+}
+
+void strbuf_addf(struct strbuf *sb, const char *fmt, ...)
+{
+       int len;
+       va_list ap;
+
+       if (!strbuf_avail(sb))
+               strbuf_grow(sb, 64);
+       va_start(ap, fmt);
+       len = vsnprintf(sb->buf + sb->len, sb->alloc - sb->len, fmt, ap);
+       va_end(ap);
+       if (len < 0)
+               die("your vsnprintf is broken");
+       if (len > strbuf_avail(sb)) {
+               strbuf_grow(sb, len);
+               va_start(ap, fmt);
+               len = vsnprintf(sb->buf + sb->len, sb->alloc - sb->len, fmt, ap);
+               va_end(ap);
+               if (len > strbuf_avail(sb)) {
+                       die("this should not happen, your snprintf is broken");
+               }
+       }
+       strbuf_setlen(sb, sb->len + len);
+}
+
+void strbuf_expand(struct strbuf *sb, const char *format,
+                   const char **placeholders, expand_fn_t fn, void *context)
+{
+       for (;;) {
+               const char *percent, **p;
+
+               percent = strchrnul(format, '%');
+               strbuf_add(sb, format, percent - format);
+               if (!*percent)
+                       break;
+               format = percent + 1;
+
+               for (p = placeholders; *p; p++) {
+                       if (!prefixcmp(format, *p))
+                               break;
+               }
+               if (*p) {
+                       fn(sb, *p, context);
+                       format += strlen(*p);
+               } else
+                       strbuf_addch(sb, '%');
+       }
+}
+
+size_t strbuf_fread(struct strbuf *sb, size_t size, FILE *f)
+{
+       size_t res;
+
+       strbuf_grow(sb, size);
+       res = fread(sb->buf + sb->len, 1, size, f);
+       if (res > 0) {
+               strbuf_setlen(sb, sb->len + res);
+       }
+       return res;
+}
+
+ssize_t strbuf_read(struct strbuf *sb, int fd, size_t hint)
+{
+       size_t oldlen = sb->len;
+
+       strbuf_grow(sb, hint ? hint : 8192);
+       for (;;) {
+               ssize_t cnt;
+
+               cnt = xread(fd, sb->buf + sb->len, sb->alloc - sb->len - 1);
+               if (cnt < 0) {
+                       strbuf_setlen(sb, oldlen);
+                       return -1;
+               }
+               if (!cnt)
+                       break;
+               sb->len += cnt;
+               strbuf_grow(sb, 8192);
+       }
+
+       sb->buf[sb->len] = '\0';
+       return sb->len - oldlen;
+}
+
+int strbuf_getline(struct strbuf *sb, FILE *fp, int term)
+{
+       int ch;
+
+       strbuf_grow(sb, 0);
+       if (feof(fp))
+               return EOF;
+
+       strbuf_reset(sb);
+       while ((ch = fgetc(fp)) != EOF) {
+               if (ch == term)
+                       break;
+               strbuf_grow(sb, 1);
+               sb->buf[sb->len++] = ch;
+       }
+       if (ch == EOF && sb->len == 0)
+               return EOF;
+
+       sb->buf[sb->len] = '\0';
+       return 0;
+}
+
+int strbuf_read_file(struct strbuf *sb, const char *path, size_t hint)
+{
+       int fd, len;
+
+       fd = open(path, O_RDONLY);
+       if (fd < 0)
+               return -1;
+       len = strbuf_read(sb, fd, hint);
+       close(fd);
+       if (len < 0)
+               return -1;
+
+       return len;
+}
diff --git a/strbuf.h b/strbuf.h
new file mode 100644 (file)
index 0000000..36d61db
--- /dev/null
+++ b/strbuf.h
@@ -0,0 +1,122 @@
+#ifndef STRBUF_H
+#define STRBUF_H
+
+/*
+ * Strbuf's can be use in many ways: as a byte array, or to store arbitrary
+ * long, overflow safe strings.
+ *
+ * Strbufs has some invariants that are very important to keep in mind:
+ *
+ * 1. the ->buf member is always malloc-ed, hence strbuf's can be used to
+ *    build complex strings/buffers whose final size isn't easily known.
+ *
+ *    It is NOT legal to copy the ->buf pointer away.
+ *    `strbuf_detach' is the operation that detachs a buffer from its shell
+ *    while keeping the shell valid wrt its invariants.
+ *
+ * 2. the ->buf member is a byte array that has at least ->len + 1 bytes
+ *    allocated. The extra byte is used to store a '\0', allowing the ->buf
+ *    member to be a valid C-string. Every strbuf function ensure this
+ *    invariant is preserved.
+ *
+ *    Note that it is OK to "play" with the buffer directly if you work it
+ *    that way:
+ *
+ *    strbuf_grow(sb, SOME_SIZE);
+ *       ... Here, the memory array starting at sb->buf, and of length
+ *       ... strbuf_avail(sb) is all yours, and you are sure that
+ *       ... strbuf_avail(sb) is at least SOME_SIZE.
+ *    strbuf_setlen(sb, sb->len + SOME_OTHER_SIZE);
+ *
+ *    Of course, SOME_OTHER_SIZE must be smaller or equal to strbuf_avail(sb).
+ *
+ *    Doing so is safe, though if it has to be done in many places, adding the
+ *    missing API to the strbuf module is the way to go.
+ *
+ *    XXX: do _not_ assume that the area that is yours is of size ->alloc - 1
+ *         even if it's true in the current implementation. Alloc is somehow a
+ *         "private" member that should not be messed with.
+ */
+
+#include <assert.h>
+
+extern char strbuf_slopbuf[];
+struct strbuf {
+       size_t alloc;
+       size_t len;
+       char *buf;
+};
+
+#define STRBUF_INIT  { 0, 0, strbuf_slopbuf }
+
+/*----- strbuf life cycle -----*/
+extern void strbuf_init(struct strbuf *, size_t);
+extern void strbuf_release(struct strbuf *);
+extern char *strbuf_detach(struct strbuf *, size_t *);
+extern void strbuf_attach(struct strbuf *, void *, size_t, size_t);
+static inline void strbuf_swap(struct strbuf *a, struct strbuf *b) {
+       struct strbuf tmp = *a;
+       *a = *b;
+       *b = tmp;
+}
+
+/*----- strbuf size related -----*/
+static inline size_t strbuf_avail(struct strbuf *sb) {
+       return sb->alloc ? sb->alloc - sb->len - 1 : 0;
+}
+
+extern void strbuf_grow(struct strbuf *, size_t);
+
+static inline void strbuf_setlen(struct strbuf *sb, size_t len) {
+       if (!sb->alloc)
+               strbuf_grow(sb, 0);
+       assert(len < sb->alloc);
+       sb->len = len;
+       sb->buf[len] = '\0';
+}
+#define strbuf_reset(sb)  strbuf_setlen(sb, 0)
+
+/*----- content related -----*/
+extern void strbuf_rtrim(struct strbuf *);
+extern int strbuf_cmp(struct strbuf *, struct strbuf *);
+
+/*----- add data in your buffer -----*/
+static inline void strbuf_addch(struct strbuf *sb, int c) {
+       strbuf_grow(sb, 1);
+       sb->buf[sb->len++] = c;
+       sb->buf[sb->len] = '\0';
+}
+
+extern void strbuf_insert(struct strbuf *, size_t pos, const void *, size_t);
+extern void strbuf_remove(struct strbuf *, size_t pos, size_t len);
+
+/* splice pos..pos+len with given data */
+extern void strbuf_splice(struct strbuf *, size_t pos, size_t len,
+                          const void *, size_t);
+
+extern void strbuf_add(struct strbuf *, const void *, size_t);
+static inline void strbuf_addstr(struct strbuf *sb, const char *s) {
+       strbuf_add(sb, s, strlen(s));
+}
+static inline void strbuf_addbuf(struct strbuf *sb, struct strbuf *sb2) {
+       strbuf_add(sb, sb2->buf, sb2->len);
+}
+extern void strbuf_adddup(struct strbuf *sb, size_t pos, size_t len);
+
+typedef void (*expand_fn_t) (struct strbuf *sb, const char *placeholder, void *context);
+extern void strbuf_expand(struct strbuf *sb, const char *format, const char **placeholders, expand_fn_t fn, void *context);
+
+__attribute__((format(printf,2,3)))
+extern void strbuf_addf(struct strbuf *sb, const char *fmt, ...);
+
+extern size_t strbuf_fread(struct strbuf *, size_t, FILE *);
+/* XXX: if read fails, any partial read is undone */
+extern ssize_t strbuf_read(struct strbuf *, int fd, size_t hint);
+extern int strbuf_read_file(struct strbuf *sb, const char *path, size_t hint);
+
+extern int strbuf_getline(struct strbuf *, FILE *, int);
+
+extern void stripspace(struct strbuf *buf, int skip_comments);
+extern void launch_editor(const char *path, struct strbuf *buffer, const char *const *env);
+
+#endif /* STRBUF_H */
diff --git a/symlinks.c b/symlinks.c
new file mode 100644 (file)
index 0000000..be9ace6
--- /dev/null
@@ -0,0 +1,48 @@
+#include "cache.h"
+
+int has_symlink_leading_path(const char *name, char *last_symlink)
+{
+       char path[PATH_MAX];
+       const char *sp, *ep;
+       char *dp;
+
+       sp = name;
+       dp = path;
+
+       if (last_symlink && *last_symlink) {
+               size_t last_len = strlen(last_symlink);
+               size_t len = strlen(name);
+               if (last_len < len &&
+                   !strncmp(name, last_symlink, last_len) &&
+                   name[last_len] == '/')
+                       return 1;
+               *last_symlink = '\0';
+       }
+
+       while (1) {
+               size_t len;
+               struct stat st;
+
+               ep = strchr(sp, '/');
+               if (!ep)
+                       break;
+               len = ep - sp;
+               if (PATH_MAX <= dp + len - path + 2)
+                       return 0; /* new name is longer than that??? */
+               memcpy(dp, sp, len);
+               dp[len] = 0;
+
+               if (lstat(path, &st))
+                       return 0;
+               if (S_ISLNK(st.st_mode)) {
+                       if (last_symlink)
+                               strcpy(last_symlink, path);
+                       return 1;
+               }
+
+               dp[len++] = '/';
+               dp = dp + len;
+               sp = ep + 1;
+       }
+       return 0;
+}
diff --git a/t/.gitignore b/t/.gitignore
new file mode 100644 (file)
index 0000000..fad67c0
--- /dev/null
@@ -0,0 +1 @@
+trash
diff --git a/t/Makefile b/t/Makefile
new file mode 100644 (file)
index 0000000..72d7884
--- /dev/null
@@ -0,0 +1,31 @@
+# Run tests
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+#GIT_TEST_OPTS=--verbose --debug
+SHELL_PATH ?= $(SHELL)
+TAR ?= $(TAR)
+RM ?= rm -f
+
+# Shell quote;
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+
+T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
+TSVN = $(wildcard t91[0-9][0-9]-*.sh)
+
+all: $(T) clean
+
+$(T):
+       @echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS)
+
+clean:
+       $(RM) -r trash
+
+# we can test NO_OPTIMIZE_COMMITS independently of LC_ALL
+full-svn-test:
+       $(MAKE) $(TSVN) GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C
+       $(MAKE) $(TSVN) GIT_SVN_NO_OPTIMIZE_COMMITS=0 LC_ALL=en_US.UTF-8
+
+.PHONY: $(T) clean
+.NOTPARALLEL:
diff --git a/t/README b/t/README
new file mode 100644 (file)
index 0000000..36f2517
--- /dev/null
+++ b/t/README
@@ -0,0 +1,211 @@
+Core GIT Tests
+==============
+
+This directory holds many test scripts for core GIT tools.  The
+first part of this short document describes how to run the tests
+and read their output.
+
+When fixing the tools or adding enhancements, you are strongly
+encouraged to add tests in this directory to cover what you are
+trying to fix or enhance.  The later part of this short document
+describes how your test scripts should be organized.
+
+
+Running Tests
+-------------
+
+The easiest way to run tests is to say "make".  This runs all
+the tests.
+
+    *** t0000-basic.sh ***
+    *   ok 1: .git/objects should be empty after git-init in an empty repo.
+    *   ok 2: .git/objects should have 256 subdirectories.
+    *   ok 3: git-update-index without --add should fail adding.
+    ...
+    *   ok 23: no diff after checkout and git-update-index --refresh.
+    * passed all 23 test(s)
+    *** t0100-environment-names.sh ***
+    *   ok 1: using old names should issue warnings.
+    *   ok 2: using old names but having new names should not issue warnings.
+    ...
+
+Or you can run each test individually from command line, like
+this:
+
+    $ sh ./t3001-ls-files-killed.sh
+    *   ok 1: git-update-index --add to add various paths.
+    *   ok 2: git-ls-files -k to show killed files.
+    *   ok 3: validate git-ls-files -k output.
+    * passed all 3 test(s)
+
+You can pass --verbose (or -v), --debug (or -d), and --immediate
+(or -i) command line argument to the test.
+
+--verbose::
+       This makes the test more verbose.  Specifically, the
+       command being run and their output if any are also
+       output.
+
+--debug::
+       This may help the person who is developing a new test.
+       It causes the command defined with test_debug to run.
+
+--immediate::
+       This causes the test to immediately exit upon the first
+       failed test.
+
+
+Naming Tests
+------------
+
+The test files are named as:
+
+       tNNNN-commandname-details.sh
+
+where N is a decimal digit.
+
+First digit tells the family:
+
+       0 - the absolute basics and global stuff
+       1 - the basic commands concerning database
+       2 - the basic commands concerning the working tree
+       3 - the other basic commands (e.g. ls-files)
+       4 - the diff commands
+       5 - the pull and exporting commands
+       6 - the revision tree commands (even e.g. merge-base)
+       7 - the porcelainish commands concerning the working tree
+       8 - the porcelainish commands concerning forensics
+       9 - the git tools
+
+Second digit tells the particular command we are testing.
+
+Third digit (optionally) tells the particular switch or group of switches
+we are testing.
+
+If you create files under t/ directory (i.e. here) that is not
+the top-level test script, never name the file to match the above
+pattern.  The Makefile here considers all such files as the
+top-level test script and tries to run all of them.  A care is
+especially needed if you are creating a common test library
+file, similar to test-lib.sh, because such a library file may
+not be suitable for standalone execution.
+
+
+Writing Tests
+-------------
+
+The test script is written as a shell script.  It should start
+with the standard "#!/bin/sh" with copyright notices, and an
+assignment to variable 'test_description', like this:
+
+       #!/bin/sh
+       #
+       # Copyright (c) 2005 Junio C Hamano
+       #
+
+       test_description='xxx test (option --frotz)
+
+       This test registers the following structure in the cache
+       and tries to run git-ls-files with option --frotz.'
+
+
+Source 'test-lib.sh'
+--------------------
+
+After assigning test_description, the test script should source
+test-lib.sh like this:
+
+       . ./test-lib.sh
+
+This test harness library does the following things:
+
+ - If the script is invoked with command line argument --help
+   (or -h), it shows the test_description and exits.
+
+ - Creates an empty test directory with an empty .git/objects
+   database and chdir(2) into it.  This directory is 't/trash'
+   if you must know, but I do not think you care.
+
+ - Defines standard test helper functions for your scripts to
+   use.  These functions are designed to make all scripts behave
+   consistently when command line arguments --verbose (or -v),
+   --debug (or -d), and --immediate (or -i) is given.
+
+
+End with test_done
+------------------
+
+Your script will be a sequence of tests, using helper functions
+from the test harness library.  At the end of the script, call
+'test_done'.
+
+
+Test harness library
+--------------------
+
+There are a handful helper functions defined in the test harness
+library for your script to use.
+
+ - test_expect_success <message> <script>
+
+   This takes two strings as parameter, and evaluates the
+   <script>.  If it yields success, test is considered
+   successful.  <message> should state what it is testing.
+
+   Example:
+
+       test_expect_success \
+           'git-write-tree should be able to write an empty tree.' \
+           'tree=$(git-write-tree)'
+
+ - test_expect_failure <message> <script>
+
+   This is the opposite of test_expect_success.  If <script>
+   yields success, test is considered a failure.
+
+   Example:
+
+       test_expect_failure \
+           'git-update-index without --add should fail adding.' \
+           'git-update-index should-be-empty'
+
+ - test_debug <script>
+
+   This takes a single argument, <script>, and evaluates it only
+   when the test script is started with --debug command line
+   argument.  This is primarily meant for use during the
+   development of a new test script.
+
+ - test_done
+
+   Your test script must have test_done at the end.  Its purpose
+   is to summarize successes and failures in the test script and
+   exit with an appropriate error code.
+
+
+Tips for Writing Tests
+----------------------
+
+As with any programming projects, existing programs are the best
+source of the information.  However, do _not_ emulate
+t0000-basic.sh when writing your tests.  The test is special in
+that it tries to validate the very core of GIT.  For example, it
+knows that there will be 256 subdirectories under .git/objects/,
+and it knows that the object ID of an empty tree is a certain
+40-byte string.  This is deliberately done so in t0000-basic.sh
+because the things the very basic core test tries to achieve is
+to serve as a basis for people who are changing the GIT internal
+drastically.  For these people, after making certain changes,
+not seeing failures from the basic test _is_ a failure.  And
+such drastic changes to the core GIT that even changes these
+otherwise supposedly stable object IDs should be accompanied by
+an update to t0000-basic.sh.
+
+However, other tests that simply rely on basic parts of the core
+GIT working properly should not have that level of intimate
+knowledge of the core GIT internals.  If all the test scripts
+hardcoded the object IDs like t0000-basic.sh does, that defeats
+the purpose of t0000-basic.sh, which is to isolate that level of
+validation in one place.  Your test also ends up needing
+updating when such a change to the internal happens, so do _not_
+do it and leave the low level of validation to t0000-basic.sh.
diff --git a/t/annotate-tests.sh b/t/annotate-tests.sh
new file mode 100644 (file)
index 0000000..cacb273
--- /dev/null
@@ -0,0 +1,123 @@
+# This file isn't used as a test script directly, instead it is
+# sourced from t8001-annotate.sh and t8001-blame.sh.
+
+check_count () {
+       head=
+       case "$1" in -h) head="$2"; shift; shift ;; esac
+       echo "$PROG file $head" >&4
+       $PROG file $head >.result || return 1
+       cat .result | perl -e '
+               my %expect = (@ARGV);
+               my %count = ();
+               while (<STDIN>) {
+                       if (/^[0-9a-f]+\t\(([^\t]+)\t/) {
+                               my $author = $1;
+                               for ($author) { s/^\s*//; s/\s*$//; }
+                               if (exists $expect{$author}) {
+                                       $count{$author}++;
+                               }
+                       }
+               }
+               my $bad = 0;
+               while (my ($author, $count) = each %count) {
+                       my $ok;
+                       if ($expect{$author} != $count) {
+                               $bad = 1;
+                               $ok = "bad";
+                       }
+                       else {
+                               $ok = "good";
+                       }
+                       print STDERR "Author $author (expected $expect{$author}, attributed $count) $ok\n";
+               }
+               exit($bad);
+       ' "$@"
+}
+
+test_expect_success \
+    'prepare reference tree' \
+    'echo "1A quick brown fox jumps over the" >file &&
+     echo "lazy dog" >>file &&
+     git add file
+     GIT_AUTHOR_NAME="A" git commit -a -m "Initial."'
+
+test_expect_success \
+    'check all lines blamed on A' \
+    'check_count A 2'
+
+test_expect_success \
+    'Setup new lines blamed on B' \
+    'echo "2A quick brown fox jumps over the" >>file &&
+     echo "lazy dog" >> file &&
+     GIT_AUTHOR_NAME="B" git commit -a -m "Second."'
+
+test_expect_success \
+    'Two lines blamed on A, two on B' \
+    'check_count A 2 B 2'
+
+test_expect_success \
+    'merge-setup part 1' \
+    'git checkout -b branch1 master &&
+     echo "3A slow green fox jumps into the" >> file &&
+     echo "well." >> file &&
+     GIT_AUTHOR_NAME="B1" git commit -a -m "Branch1-1"'
+
+test_expect_success \
+    'Two lines blamed on A, two on B, two on B1' \
+    'check_count A 2 B 2 B1 2'
+
+test_expect_success \
+    'merge-setup part 2' \
+    'git checkout -b branch2 master &&
+     sed -e "s/2A quick brown/4A quick brown lazy dog/" < file > file.new &&
+     mv file.new file &&
+     GIT_AUTHOR_NAME="B2" git commit -a -m "Branch2-1"'
+
+test_expect_success \
+    'Two lines blamed on A, one on B, one on B2' \
+    'check_count A 2 B 1 B2 1'
+
+test_expect_success \
+    'merge-setup part 3' \
+    'git pull . branch1'
+
+test_expect_success \
+    'Two lines blamed on A, one on B, two on B1, one on B2' \
+    'check_count A 2 B 1 B1 2 B2 1'
+
+test_expect_success \
+    'Annotating an old revision works' \
+    'check_count -h master A 2 B 2'
+
+test_expect_success \
+    'Annotating an old revision works' \
+    'check_count -h master^ A 2'
+
+test_expect_success \
+    'merge-setup part 4' \
+    'echo "evil merge." >>file &&
+     git commit -a --amend'
+
+test_expect_success \
+    'Two lines blamed on A, one on B, two on B1, one on B2, one on A U Thor' \
+    'check_count A 2 B 1 B1 2 B2 1 "A U Thor" 1'
+
+test_expect_success \
+    'an incomplete line added' \
+    'echo "incomplete" | tr -d "\\012" >>file &&
+    GIT_AUTHOR_NAME="C" git commit -a -m "Incomplete"'
+
+test_expect_success \
+    'With incomplete lines.' \
+    'check_count A 2 B 1 B1 2 B2 1 "A U Thor" 1 C 1'
+
+test_expect_success \
+    'some edit' \
+    'mv file file.orig &&
+    sed -e "s/^3A/99/" -e "/^1A/d" -e "/^incomplete/d" < file.orig > file &&
+    echo "incomplete" | tr -d "\\012" >>file &&
+    GIT_AUTHOR_NAME="D" git commit -a -m "edit"'
+
+test_expect_success \
+    'some edit' \
+    'check_count A 1 B 1 B1 1 B2 1 "A U Thor" 1 C 1 D 1'
diff --git a/t/diff-lib.sh b/t/diff-lib.sh
new file mode 100644 (file)
index 0000000..4624fe6
--- /dev/null
@@ -0,0 +1,41 @@
+:
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+sanitize_diff_raw='/^:/s/ '"$_x40"' '"$_x40"' \([A-Z]\)[0-9]*  / X X \1#       /'
+compare_diff_raw () {
+    # When heuristics are improved, the score numbers would change.
+    # Ignore them while comparing.
+    # Also we do not check SHA1 hash generation in this test, which
+    # is a job for t0000-basic.sh
+
+    sed -e "$sanitize_diff_raw" <"$1" >.tmp-1
+    sed -e "$sanitize_diff_raw" <"$2" >.tmp-2
+    git diff .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2
+}
+
+sanitize_diff_raw_z='/^:/s/ '"$_x40"' '"$_x40"' \([A-Z]\)[0-9]*$/ X X \1#/'
+compare_diff_raw_z () {
+    # When heuristics are improved, the score numbers would change.
+    # Ignore them while comparing.
+    # Also we do not check SHA1 hash generation in this test, which
+    # is a job for t0000-basic.sh
+
+    tr '\0' '\012' <"$1" | sed -e "$sanitize_diff_raw_z" >.tmp-1
+    tr '\0' '\012' <"$2" | sed -e "$sanitize_diff_raw_z" >.tmp-2
+    git diff .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2
+}
+
+compare_diff_patch () {
+    # When heuristics are improved, the score numbers would change.
+    # Ignore them while comparing.
+    sed -e '
+       /^[dis]*imilarity index [0-9]*%$/d
+       /^index [0-9a-f]*\.\.[0-9a-f]/d
+    ' <"$1" >.tmp-1
+    sed -e '
+       /^[dis]*imilarity index [0-9]*%$/d
+       /^index [0-9a-f]*\.\.[0-9a-f]/d
+    ' <"$2" >.tmp-2
+    git diff .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2
+}
diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh
new file mode 100644 (file)
index 0000000..8d4a447
--- /dev/null
@@ -0,0 +1,84 @@
+. ./test-lib.sh
+
+if test -n "$NO_SVN_TESTS"
+then
+       test_expect_success 'skipping git-svn tests, NO_SVN_TESTS defined' :
+       test_done
+       exit
+fi
+
+GIT_DIR=$PWD/.git
+GIT_SVN_DIR=$GIT_DIR/svn/git-svn
+SVN_TREE=$GIT_SVN_DIR/svn-tree
+
+svn >/dev/null 2>&1
+if test $? -ne 1
+then
+    test_expect_success 'skipping git-svn tests, svn not found' :
+    test_done
+    exit
+fi
+
+svnrepo=$PWD/svnrepo
+
+perl -w -e "
+use SVN::Core;
+use SVN::Repos;
+\$SVN::Core::VERSION gt '1.1.0' or exit(42);
+system(qw/svnadmin create --fs-type fsfs/, '$svnrepo') == 0 or exit(41);
+" >&3 2>&4
+x=$?
+if test $x -ne 0
+then
+       if test $x -eq 42; then
+               err='Perl SVN libraries must be >= 1.1.0'
+       elif test $x -eq 41; then
+               err='svnadmin failed to create fsfs repository'
+       else
+               err='Perl SVN libraries not found or unusable, skipping test'
+       fi
+       test_expect_success "$err" :
+       test_done
+       exit
+fi
+
+rawsvnrepo="$svnrepo"
+svnrepo="file://$svnrepo"
+
+poke() {
+       test-chmtime +1 "$1"
+}
+
+SVN_HTTPD_MODULE_PATH=${SVN_HTTPD_MODULE_PATH-'/usr/lib/apache2/modules'}
+SVN_HTTPD_PATH=${SVN_HTTPD_PATH-'/usr/sbin/apache2'}
+
+start_httpd () {
+       if test -z "$SVN_HTTPD_PORT"
+       then
+               echo >&2 'SVN_HTTPD_PORT is not defined!'
+               return
+       fi
+
+       mkdir "$GIT_DIR"/logs
+
+       cat > "$GIT_DIR/httpd.conf" <<EOF
+ServerName "git-svn test"
+ServerRoot "$GIT_DIR"
+DocumentRoot "$GIT_DIR"
+PidFile "$GIT_DIR/httpd.pid"
+Listen 127.0.0.1:$SVN_HTTPD_PORT
+LoadModule dav_module $SVN_HTTPD_MODULE_PATH/mod_dav.so
+LoadModule dav_svn_module $SVN_HTTPD_MODULE_PATH/mod_dav_svn.so
+<Location /svn>
+       DAV svn
+       SVNPath $rawsvnrepo
+</Location>
+EOF
+       "$SVN_HTTPD_PATH" -f "$GIT_DIR"/httpd.conf -k start
+       svnrepo=http://127.0.0.1:$SVN_HTTPD_PORT/svn
+}
+
+stop_httpd () {
+       test -z "$SVN_HTTPD_PORT" && return
+       "$SVN_HTTPD_PATH" -f "$GIT_DIR"/httpd.conf -k stop
+}
diff --git a/t/lib-read-tree-m-3way.sh b/t/lib-read-tree-m-3way.sh
new file mode 100644 (file)
index 0000000..168329a
--- /dev/null
@@ -0,0 +1,158 @@
+: Included from t1000-read-tree-m-3way.sh and others
+# Original tree.
+mkdir Z
+for a in N D M
+do
+    for b in N D M
+    do
+        p=$a$b
+       echo This is $p from the original tree. >$p
+       echo This is Z/$p from the original tree. >Z/$p
+       test_expect_success \
+           "adding test file $p and Z/$p" \
+           'git update-index --add $p &&
+           git update-index --add Z/$p'
+    done
+done
+echo This is SS from the original tree. >SS
+test_expect_success \
+    'adding test file SS' \
+    'git update-index --add SS'
+cat >TT <<\EOF
+This is a trivial merge sample text.
+Branch A is expected to upcase this word, here.
+There are some filler lines to avoid diff context
+conflicts here,
+like this one,
+and this one,
+and this one is yet another one of them.
+At the very end, here comes another line, that is
+the word, expected to be upcased by Branch B.
+This concludes the trivial merge sample file.
+EOF
+test_expect_success \
+    'adding test file TT' \
+    'git update-index --add TT'
+test_expect_success \
+    'prepare initial tree' \
+    'tree_O=$(git write-tree)'
+
+################################################################
+# Branch A and B makes the changes according to the above matrix.
+
+################################################################
+# Branch A
+
+to_remove=$(echo D? Z/D?)
+rm -f $to_remove
+test_expect_success \
+    'change in branch A (removal)' \
+    'git update-index --remove $to_remove'
+
+for p in M? Z/M?
+do
+    echo This is modified $p in the branch A. >$p
+    test_expect_success \
+       'change in branch A (modification)' \
+        "git update-index $p"
+done
+
+for p in AN AA Z/AN Z/AA
+do
+    echo This is added $p in the branch A. >$p
+    test_expect_success \
+       'change in branch A (addition)' \
+       "git update-index --add $p"
+done
+
+echo This is SS from the modified tree. >SS
+echo This is LL from the modified tree. >LL
+test_expect_success \
+    'change in branch A (addition)' \
+    'git update-index --add LL &&
+     git update-index SS'
+mv TT TT-
+sed -e '/Branch A/s/word/WORD/g' <TT- >TT
+rm -f TT-
+test_expect_success \
+    'change in branch A (edit)' \
+    'git update-index TT'
+
+mkdir DF
+echo Branch A makes a file at DF/DF, creating a directory DF. >DF/DF
+test_expect_success \
+    'change in branch A (change file to directory)' \
+    'git update-index --add DF/DF'
+
+test_expect_success \
+    'recording branch A tree' \
+    'tree_A=$(git write-tree)'
+
+################################################################
+# Branch B
+# Start from O
+
+rm -rf [NDMASLT][NDMASLT] Z DF
+mkdir Z
+test_expect_success \
+    'reading original tree and checking out' \
+    'git read-tree $tree_O &&
+     git checkout-index -a'
+
+to_remove=$(echo ?D Z/?D)
+rm -f $to_remove
+test_expect_success \
+    'change in branch B (removal)' \
+    "git update-index --remove $to_remove"
+
+for p in ?M Z/?M
+do
+    echo This is modified $p in the branch B. >$p
+    test_expect_success \
+       'change in branch B (modification)' \
+       "git update-index $p"
+done
+
+for p in NA AA Z/NA Z/AA
+do
+    echo This is added $p in the branch B. >$p
+    test_expect_success \
+       'change in branch B (addition)' \
+       "git update-index --add $p"
+done
+echo This is SS from the modified tree. >SS
+echo This is LL from the modified tree. >LL
+test_expect_success \
+    'change in branch B (addition and modification)' \
+    'git update-index --add LL &&
+     git update-index SS'
+mv TT TT-
+sed -e '/Branch B/s/word/WORD/g' <TT- >TT
+rm -f TT-
+test_expect_success \
+    'change in branch B (modification)' \
+    'git update-index TT'
+
+echo Branch B makes a file at DF. >DF
+test_expect_success \
+    'change in branch B (addition of a file to conflict with directory)' \
+    'git update-index --add DF'
+
+test_expect_success \
+    'recording branch B tree' \
+    'tree_B=$(git write-tree)'
+
+test_expect_success \
+    'keep contents of 3 trees for easy access' \
+    'rm -f .git/index &&
+     git read-tree $tree_O &&
+     mkdir .orig-O &&
+     git checkout-index --prefix=.orig-O/ -f -q -a &&
+     rm -f .git/index &&
+     git read-tree $tree_A &&
+     mkdir .orig-A &&
+     git checkout-index --prefix=.orig-A/ -f -q -a &&
+     rm -f .git/index &&
+     git read-tree $tree_B &&
+     mkdir .orig-B &&
+     git checkout-index --prefix=.orig-B/ -f -q -a'
diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh
new file mode 100755 (executable)
index 0000000..4e49d59
--- /dev/null
@@ -0,0 +1,300 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Test the very basics part #1.
+
+The rest of the test suite does not check the basic operation of git
+plumbing commands to work very carefully.  Their job is to concentrate
+on tricky features that caused bugs in the past to detect regression.
+
+This test runs very basic features, like registering things in cache,
+writing tree, etc.
+
+Note that this test *deliberately* hard-codes many expected object
+IDs.  When object ID computation changes, like in the previous case of
+swapping compression and hashing order, the person who is making the
+modification *should* take notice and update the test vectors here.
+'
+
+################################################################
+# It appears that people try to run tests without building...
+
+../git >/dev/null
+if test $? != 1
+then
+       echo >&2 'You do not seem to have built git yet.'
+       exit 1
+fi
+
+. ./test-lib.sh
+
+################################################################
+# git init has been done in an empty repository.
+# make sure it is empty.
+
+find .git/objects -type f -print >should-be-empty
+test_expect_success \
+    '.git/objects should be empty after git init in an empty repo.' \
+    'cmp -s /dev/null should-be-empty'
+
+# also it should have 2 subdirectories; no fan-out anymore, pack, and info.
+# 3 is counting "objects" itself
+find .git/objects -type d -print >full-of-directories
+test_expect_success \
+    '.git/objects should have 3 subdirectories.' \
+    'test $(wc -l < full-of-directories) = 3'
+
+################################################################
+# Basics of the basics
+
+# updating a new file without --add should fail.
+test_expect_failure \
+    'git update-index without --add should fail adding.' \
+    'git update-index should-be-empty'
+
+# and with --add it should succeed, even if it is empty (it used to fail).
+test_expect_success \
+    'git update-index with --add should succeed.' \
+    'git update-index --add should-be-empty'
+
+test_expect_success \
+    'writing tree out with git write-tree' \
+    'tree=$(git write-tree)'
+
+# we know the shape and contents of the tree and know the object ID for it.
+test_expect_success \
+    'validate object ID of a known tree.' \
+    'test "$tree" = 7bb943559a305bdd6bdee2cef6e5df2413c3d30a'
+
+# Removing paths.
+rm -f should-be-empty full-of-directories
+test_expect_failure \
+    'git update-index without --remove should fail removing.' \
+    'git update-index should-be-empty'
+
+test_expect_success \
+    'git update-index with --remove should be able to remove.' \
+    'git update-index --remove should-be-empty'
+
+# Empty tree can be written with recent write-tree.
+test_expect_success \
+    'git write-tree should be able to write an empty tree.' \
+    'tree=$(git write-tree)'
+
+test_expect_success \
+    'validate object ID of a known tree.' \
+    'test "$tree" = 4b825dc642cb6eb9a060e54bf8d69288fbee4904'
+
+# Various types of objects
+mkdir path2 path3 path3/subp3
+for p in path0 path2/file2 path3/file3 path3/subp3/file3
+do
+    echo "hello $p" >$p
+    ln -s "hello $p" ${p}sym
+done
+test_expect_success \
+    'adding various types of objects with git update-index --add.' \
+    'find path* ! -type d -print | xargs git update-index --add'
+
+# Show them and see that matches what we expect.
+test_expect_success \
+    'showing stage with git ls-files --stage' \
+    'git ls-files --stage >current'
+
+cat >expected <<\EOF
+100644 f87290f8eb2cbbea7857214459a0739927eab154 0      path0
+120000 15a98433ae33114b085f3eb3bb03b832b3180a01 0      path0sym
+100644 3feff949ed00a62d9f7af97c15cd8a30595e7ac7 0      path2/file2
+120000 d8ce161addc5173867a3c3c730924388daedbc38 0      path2/file2sym
+100644 0aa34cae68d0878578ad119c86ca2b5ed5b28376 0      path3/file3
+120000 8599103969b43aff7e430efea79ca4636466794f 0      path3/file3sym
+100644 00fb5908cb97c2564a9783c0c64087333b3b464f 0      path3/subp3/file3
+120000 6649a1ebe9e9f1c553b66f5a6e74136a07ccc57c 0      path3/subp3/file3sym
+EOF
+test_expect_success \
+    'validate git ls-files output for a known tree.' \
+    'diff current expected'
+
+test_expect_success \
+    'writing tree out with git write-tree.' \
+    'tree=$(git write-tree)'
+test_expect_success \
+    'validate object ID for a known tree.' \
+    'test "$tree" = 087704a96baf1c2d1c869a8b084481e121c88b5b'
+
+test_expect_success \
+    'showing tree with git ls-tree' \
+    'git ls-tree $tree >current'
+cat >expected <<\EOF
+100644 blob f87290f8eb2cbbea7857214459a0739927eab154   path0
+120000 blob 15a98433ae33114b085f3eb3bb03b832b3180a01   path0sym
+040000 tree 58a09c23e2ca152193f2786e06986b7b6712bdbe   path2
+040000 tree 21ae8269cacbe57ae09138dcc3a2887f904d02b3   path3
+EOF
+test_expect_success \
+    'git ls-tree output for a known tree.' \
+    'diff current expected'
+
+# This changed in ls-tree pathspec change -- recursive does
+# not show tree nodes anymore.
+test_expect_success \
+    'showing tree with git ls-tree -r' \
+    'git ls-tree -r $tree >current'
+cat >expected <<\EOF
+100644 blob f87290f8eb2cbbea7857214459a0739927eab154   path0
+120000 blob 15a98433ae33114b085f3eb3bb03b832b3180a01   path0sym
+100644 blob 3feff949ed00a62d9f7af97c15cd8a30595e7ac7   path2/file2
+120000 blob d8ce161addc5173867a3c3c730924388daedbc38   path2/file2sym
+100644 blob 0aa34cae68d0878578ad119c86ca2b5ed5b28376   path3/file3
+120000 blob 8599103969b43aff7e430efea79ca4636466794f   path3/file3sym
+100644 blob 00fb5908cb97c2564a9783c0c64087333b3b464f   path3/subp3/file3
+120000 blob 6649a1ebe9e9f1c553b66f5a6e74136a07ccc57c   path3/subp3/file3sym
+EOF
+test_expect_success \
+    'git ls-tree -r output for a known tree.' \
+    'diff current expected'
+
+# But with -r -t we can have both.
+test_expect_success \
+    'showing tree with git ls-tree -r -t' \
+    'git ls-tree -r -t $tree >current'
+cat >expected <<\EOF
+100644 blob f87290f8eb2cbbea7857214459a0739927eab154   path0
+120000 blob 15a98433ae33114b085f3eb3bb03b832b3180a01   path0sym
+040000 tree 58a09c23e2ca152193f2786e06986b7b6712bdbe   path2
+100644 blob 3feff949ed00a62d9f7af97c15cd8a30595e7ac7   path2/file2
+120000 blob d8ce161addc5173867a3c3c730924388daedbc38   path2/file2sym
+040000 tree 21ae8269cacbe57ae09138dcc3a2887f904d02b3   path3
+100644 blob 0aa34cae68d0878578ad119c86ca2b5ed5b28376   path3/file3
+120000 blob 8599103969b43aff7e430efea79ca4636466794f   path3/file3sym
+040000 tree 3c5e5399f3a333eddecce7a9b9465b63f65f51e2   path3/subp3
+100644 blob 00fb5908cb97c2564a9783c0c64087333b3b464f   path3/subp3/file3
+120000 blob 6649a1ebe9e9f1c553b66f5a6e74136a07ccc57c   path3/subp3/file3sym
+EOF
+test_expect_success \
+    'git ls-tree -r output for a known tree.' \
+    'diff current expected'
+
+test_expect_success \
+    'writing partial tree out with git write-tree --prefix.' \
+    'ptree=$(git write-tree --prefix=path3)'
+test_expect_success \
+    'validate object ID for a known tree.' \
+    'test "$ptree" = 21ae8269cacbe57ae09138dcc3a2887f904d02b3'
+
+test_expect_success \
+    'writing partial tree out with git write-tree --prefix.' \
+    'ptree=$(git write-tree --prefix=path3/subp3)'
+test_expect_success \
+    'validate object ID for a known tree.' \
+    'test "$ptree" = 3c5e5399f3a333eddecce7a9b9465b63f65f51e2'
+
+cat >badobjects <<EOF
+100644 blob 1000000000000000000000000000000000000000   dir/file1
+100644 blob 2000000000000000000000000000000000000000   dir/file2
+100644 blob 3000000000000000000000000000000000000000   dir/file3
+100644 blob 4000000000000000000000000000000000000000   dir/file4
+100644 blob 5000000000000000000000000000000000000000   dir/file5
+EOF
+
+rm .git/index
+test_expect_success \
+    'put invalid objects into the index.' \
+    'git update-index --index-info < badobjects'
+
+test_expect_failure \
+    'writing this tree without --missing-ok.' \
+    'git write-tree'
+
+test_expect_success \
+    'writing this tree with --missing-ok.' \
+    'git write-tree --missing-ok'
+
+
+################################################################
+rm .git/index
+test_expect_success \
+    'git read-tree followed by write-tree should be idempotent.' \
+    'git read-tree $tree &&
+     test -f .git/index &&
+     newtree=$(git write-tree) &&
+     test "$newtree" = "$tree"'
+
+cat >expected <<\EOF
+:100644 100644 f87290f8eb2cbbea7857214459a0739927eab154 0000000000000000000000000000000000000000 M     path0
+:120000 120000 15a98433ae33114b085f3eb3bb03b832b3180a01 0000000000000000000000000000000000000000 M     path0sym
+:100644 100644 3feff949ed00a62d9f7af97c15cd8a30595e7ac7 0000000000000000000000000000000000000000 M     path2/file2
+:120000 120000 d8ce161addc5173867a3c3c730924388daedbc38 0000000000000000000000000000000000000000 M     path2/file2sym
+:100644 100644 0aa34cae68d0878578ad119c86ca2b5ed5b28376 0000000000000000000000000000000000000000 M     path3/file3
+:120000 120000 8599103969b43aff7e430efea79ca4636466794f 0000000000000000000000000000000000000000 M     path3/file3sym
+:100644 100644 00fb5908cb97c2564a9783c0c64087333b3b464f 0000000000000000000000000000000000000000 M     path3/subp3/file3
+:120000 120000 6649a1ebe9e9f1c553b66f5a6e74136a07ccc57c 0000000000000000000000000000000000000000 M     path3/subp3/file3sym
+EOF
+test_expect_success \
+    'validate git diff-files output for a know cache/work tree state.' \
+    'git diff-files >current && diff >/dev/null -b current expected'
+
+test_expect_success \
+    'git update-index --refresh should succeed.' \
+    'git update-index --refresh'
+
+test_expect_success \
+    'no diff after checkout and git update-index --refresh.' \
+    'git diff-files >current && cmp -s current /dev/null'
+
+################################################################
+P=087704a96baf1c2d1c869a8b084481e121c88b5b
+test_expect_success \
+    'git commit-tree records the correct tree in a commit.' \
+    'commit0=$(echo NO | git commit-tree $P) &&
+     tree=$(git show --pretty=raw $commit0 |
+        sed -n -e "s/^tree //p" -e "/^author /q") &&
+     test "z$tree" = "z$P"'
+
+test_expect_success \
+    'git commit-tree records the correct parent in a commit.' \
+    'commit1=$(echo NO | git commit-tree $P -p $commit0) &&
+     parent=$(git show --pretty=raw $commit1 |
+        sed -n -e "s/^parent //p" -e "/^author /q") &&
+     test "z$commit0" = "z$parent"'
+
+test_expect_success \
+    'git commit-tree omits duplicated parent in a commit.' \
+    'commit2=$(echo NO | git commit-tree $P -p $commit0 -p $commit0) &&
+     parent=$(git show --pretty=raw $commit2 |
+        sed -n -e "s/^parent //p" -e "/^author /q" |
+        sort -u) &&
+     test "z$commit0" = "z$parent" &&
+     numparent=$(git show --pretty=raw $commit2 |
+        sed -n -e "s/^parent //p" -e "/^author /q" |
+        wc -l) &&
+     test $numparent = 1'
+
+test_expect_success 'update-index D/F conflict' '
+       mv path0 tmp &&
+       mv path2 path0 &&
+       mv tmp path2 &&
+       git update-index --add --replace path2 path0/file2 &&
+       numpath0=$(git ls-files path0 | wc -l) &&
+       test $numpath0 = 1
+'
+
+test_expect_success 'absolute path works as expected' '
+       mkdir first &&
+       ln -s ../.git first/.git &&
+       mkdir second &&
+       ln -s ../first second/other &&
+       mkdir third &&
+       dir="$(cd .git; pwd -P)" &&
+       dir2=third/../second/other/.git &&
+       test "$dir" = "$(test-absolute-path $dir2)" &&
+       file="$dir"/index &&
+       test "$file" = "$(test-absolute-path $dir2/index)" &&
+       ln -s ../first/file .git/syml &&
+       sym="$(cd first; pwd -P)"/file &&
+       test "$sym" = "$(test-absolute-path $dir2/syml)"
+'
+
+test_done
diff --git a/t/t0001-init.sh b/t/t0001-init.sh
new file mode 100755 (executable)
index 0000000..c015405
--- /dev/null
@@ -0,0 +1,116 @@
+#!/bin/sh
+
+test_description='git init'
+
+. ./test-lib.sh
+
+check_config () {
+       if test -d "$1" && test -f "$1/config" && test -d "$1/refs"
+       then
+               : happy
+       else
+               echo "expected a directory $1, a file $1/config and $1/refs"
+               return 1
+       fi
+       bare=$(GIT_CONFIG="$1/config" git config --bool core.bare)
+       worktree=$(GIT_CONFIG="$1/config" git config core.worktree) ||
+       worktree=unset
+
+       test "$bare" = "$2" && test "$worktree" = "$3" || {
+               echo "expected bare=$2 worktree=$3"
+               echo "     got bare=$bare worktree=$worktree"
+               return 1
+       }
+}
+
+test_expect_success 'plain' '
+       (
+               unset GIT_DIR GIT_WORK_TREE
+               mkdir plain &&
+               cd plain &&
+               git init
+       ) &&
+       check_config plain/.git false unset
+'
+
+test_expect_success 'plain with GIT_WORK_TREE' '
+       if (
+               unset GIT_DIR
+               mkdir plain-wt &&
+               cd plain-wt &&
+               GIT_WORK_TREE=$(pwd) git init
+       )
+       then
+               echo Should have failed -- GIT_WORK_TREE should not be used
+               false
+       fi
+'
+
+test_expect_success 'plain bare' '
+       (
+               unset GIT_DIR GIT_WORK_TREE GIT_CONFIG
+               mkdir plain-bare-1 &&
+               cd plain-bare-1 &&
+               git --bare init
+       ) &&
+       check_config plain-bare-1 true unset
+'
+
+test_expect_success 'plain bare with GIT_WORK_TREE' '
+       if (
+               unset GIT_DIR GIT_CONFIG
+               mkdir plain-bare-2 &&
+               cd plain-bare-2 &&
+               GIT_WORK_TREE=$(pwd) git --bare init
+       )
+       then
+               echo Should have failed -- GIT_WORK_TREE should not be used
+               false
+       fi
+'
+
+test_expect_success 'GIT_DIR bare' '
+
+       (
+               unset GIT_CONFIG
+               mkdir git-dir-bare.git &&
+               GIT_DIR=git-dir-bare.git git init
+       ) &&
+       check_config git-dir-bare.git true unset
+'
+
+test_expect_success 'GIT_DIR non-bare' '
+
+       (
+               unset GIT_CONFIG
+               mkdir non-bare &&
+               cd non-bare &&
+               GIT_DIR=.git git init
+       ) &&
+       check_config non-bare/.git false unset
+'
+
+test_expect_success 'GIT_DIR & GIT_WORK_TREE (1)' '
+
+       (
+               unset GIT_CONFIG
+               mkdir git-dir-wt-1.git &&
+               GIT_WORK_TREE=$(pwd) GIT_DIR=git-dir-wt-1.git git init
+       ) &&
+       check_config git-dir-wt-1.git false "$(pwd)"
+'
+
+test_expect_success 'GIT_DIR & GIT_WORK_TREE (2)' '
+
+       if (
+               unset GIT_CONFIG
+               mkdir git-dir-wt-2.git &&
+               GIT_WORK_TREE=$(pwd) GIT_DIR=git-dir-wt-2.git git --bare init
+       )
+       then
+               echo Should have failed -- --bare should not be used
+               false
+       fi
+'
+
+test_done
diff --git a/t/t0010-racy-git.sh b/t/t0010-racy-git.sh
new file mode 100755 (executable)
index 0000000..e45a9e4
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+test_description='racy GIT'
+
+. ./test-lib.sh
+
+# This test can give false success if your machine is sufficiently
+# slow or your trial happened to happen on second boundary.
+
+for trial in 0 1 2 3 4
+do
+       rm -f .git/index
+       echo frotz >infocom
+       git update-index --add infocom
+       echo xyzzy >infocom
+
+       files=`git diff-files -p`
+       test_expect_success \
+       "Racy GIT trial #$trial part A" \
+       'test "" != "$files"'
+
+       sleep 1
+       echo xyzzy >cornerstone
+       git update-index --add cornerstone
+
+       files=`git diff-files -p`
+       test_expect_success \
+       "Racy GIT trial #$trial part B" \
+       'test "" != "$files"'
+
+done
+
+test_done
diff --git a/t/t0020-crlf.sh b/t/t0020-crlf.sh
new file mode 100755 (executable)
index 0000000..62bc4bb
--- /dev/null
@@ -0,0 +1,381 @@
+#!/bin/sh
+
+test_description='CRLF conversion'
+
+. ./test-lib.sh
+
+q_to_nul () {
+       tr Q '\0'
+}
+
+append_cr () {
+       sed -e 's/$/Q/' | tr Q '\015'
+}
+
+remove_cr () {
+       tr '\015' Q <"$1" | grep Q >/dev/null &&
+       tr '\015' Q <"$1" | sed -ne 's/Q$//p'
+}
+
+test_expect_success setup '
+
+       git repo-config core.autocrlf false &&
+
+       for w in Hello world how are you; do echo $w; done >one &&
+       mkdir dir &&
+       for w in I am very very fine thank you; do echo $w; done >dir/two &&
+       for w in Oh here is NULQin text here; do echo $w; done | q_to_nul >three &&
+       git add . &&
+
+       git commit -m initial &&
+
+       one=`git rev-parse HEAD:one` &&
+       dir=`git rev-parse HEAD:dir` &&
+       two=`git rev-parse HEAD:dir/two` &&
+       three=`git rev-parse HEAD:three` &&
+
+       for w in Some extra lines here; do echo $w; done >>one &&
+       git diff >patch.file &&
+       patched=`git hash-object --stdin <one` &&
+       git read-tree --reset -u HEAD &&
+
+       echo happy.
+'
+
+test_expect_success 'update with autocrlf=input' '
+
+       rm -f tmp one dir/two three &&
+       git read-tree --reset -u HEAD &&
+       git repo-config core.autocrlf input &&
+
+       for f in one dir/two
+       do
+               append_cr <$f >tmp && mv -f tmp $f &&
+               git update-index -- $f || {
+                       echo Oops
+                       false
+                       break
+               }
+       done &&
+
+       differs=`git diff-index --cached HEAD` &&
+       test -z "$differs" || {
+               echo Oops "$differs"
+               false
+       }
+
+'
+
+test_expect_success 'update with autocrlf=true' '
+
+       rm -f tmp one dir/two three &&
+       git read-tree --reset -u HEAD &&
+       git repo-config core.autocrlf true &&
+
+       for f in one dir/two
+       do
+               append_cr <$f >tmp && mv -f tmp $f &&
+               git update-index -- $f || {
+                       echo "Oops $f"
+                       false
+                       break
+               }
+       done &&
+
+       differs=`git diff-index --cached HEAD` &&
+       test -z "$differs" || {
+               echo Oops "$differs"
+               false
+       }
+
+'
+
+test_expect_success 'checkout with autocrlf=true' '
+
+       rm -f tmp one dir/two three &&
+       git repo-config core.autocrlf true &&
+       git read-tree --reset -u HEAD &&
+
+       for f in one dir/two
+       do
+               remove_cr "$f" >tmp && mv -f tmp $f &&
+               git update-index -- $f || {
+                       echo "Eh? $f"
+                       false
+                       break
+               }
+       done &&
+       test "$one" = `git hash-object --stdin <one` &&
+       test "$two" = `git hash-object --stdin <dir/two` &&
+       differs=`git diff-index --cached HEAD` &&
+       test -z "$differs" || {
+               echo Oops "$differs"
+               false
+       }
+'
+
+test_expect_success 'checkout with autocrlf=input' '
+
+       rm -f tmp one dir/two three &&
+       git repo-config core.autocrlf input &&
+       git read-tree --reset -u HEAD &&
+
+       for f in one dir/two
+       do
+               if remove_cr "$f" >/dev/null
+               then
+                       echo "Eh? $f"
+                       false
+                       break
+               else
+                       git update-index -- $f
+               fi
+       done &&
+       test "$one" = `git hash-object --stdin <one` &&
+       test "$two" = `git hash-object --stdin <dir/two` &&
+       differs=`git diff-index --cached HEAD` &&
+       test -z "$differs" || {
+               echo Oops "$differs"
+               false
+       }
+'
+
+test_expect_success 'apply patch (autocrlf=input)' '
+
+       rm -f tmp one dir/two three &&
+       git repo-config core.autocrlf input &&
+       git read-tree --reset -u HEAD &&
+
+       git apply patch.file &&
+       test "$patched" = "`git hash-object --stdin <one`" || {
+               echo "Eh?  apply without index"
+               false
+       }
+'
+
+test_expect_success 'apply patch --cached (autocrlf=input)' '
+
+       rm -f tmp one dir/two three &&
+       git repo-config core.autocrlf input &&
+       git read-tree --reset -u HEAD &&
+
+       git apply --cached patch.file &&
+       test "$patched" = `git rev-parse :one` || {
+               echo "Eh?  apply with --cached"
+               false
+       }
+'
+
+test_expect_success 'apply patch --index (autocrlf=input)' '
+
+       rm -f tmp one dir/two three &&
+       git repo-config core.autocrlf input &&
+       git read-tree --reset -u HEAD &&
+
+       git apply --index patch.file &&
+       test "$patched" = `git rev-parse :one` &&
+       test "$patched" = `git hash-object --stdin <one` || {
+               echo "Eh?  apply with --index"
+               false
+       }
+'
+
+test_expect_success 'apply patch (autocrlf=true)' '
+
+       rm -f tmp one dir/two three &&
+       git repo-config core.autocrlf true &&
+       git read-tree --reset -u HEAD &&
+
+       git apply patch.file &&
+       test "$patched" = "`remove_cr one | git hash-object --stdin`" || {
+               echo "Eh?  apply without index"
+               false
+       }
+'
+
+test_expect_success 'apply patch --cached (autocrlf=true)' '
+
+       rm -f tmp one dir/two three &&
+       git repo-config core.autocrlf true &&
+       git read-tree --reset -u HEAD &&
+
+       git apply --cached patch.file &&
+       test "$patched" = `git rev-parse :one` || {
+               echo "Eh?  apply without index"
+               false
+       }
+'
+
+test_expect_success 'apply patch --index (autocrlf=true)' '
+
+       rm -f tmp one dir/two three &&
+       git repo-config core.autocrlf true &&
+       git read-tree --reset -u HEAD &&
+
+       git apply --index patch.file &&
+       test "$patched" = `git rev-parse :one` &&
+       test "$patched" = "`remove_cr one | git hash-object --stdin`" || {
+               echo "Eh?  apply with --index"
+               false
+       }
+'
+
+test_expect_success '.gitattributes says two is binary' '
+
+       rm -f tmp one dir/two three &&
+       echo "two -crlf" >.gitattributes &&
+       git repo-config core.autocrlf true &&
+       git read-tree --reset -u HEAD &&
+
+       if remove_cr dir/two >/dev/null
+       then
+               echo "Huh?"
+               false
+       else
+               : happy
+       fi &&
+
+       if remove_cr one >/dev/null
+       then
+               : happy
+       else
+               echo "Huh?"
+               false
+       fi &&
+
+       if remove_cr three >/dev/null
+       then
+               echo "Huh?"
+               false
+       else
+               : happy
+       fi
+'
+
+test_expect_success '.gitattributes says two is input' '
+
+       rm -f tmp one dir/two three &&
+       echo "two crlf=input" >.gitattributes &&
+       git read-tree --reset -u HEAD &&
+
+       if remove_cr dir/two >/dev/null
+       then
+               echo "Huh?"
+               false
+       else
+               : happy
+       fi
+'
+
+test_expect_success '.gitattributes says two and three are text' '
+
+       rm -f tmp one dir/two three &&
+       echo "t* crlf" >.gitattributes &&
+       git read-tree --reset -u HEAD &&
+
+       if remove_cr dir/two >/dev/null
+       then
+               : happy
+       else
+               echo "Huh?"
+               false
+       fi &&
+
+       if remove_cr three >/dev/null
+       then
+               : happy
+       else
+               echo "Huh?"
+               false
+       fi
+'
+
+test_expect_success 'in-tree .gitattributes (1)' '
+
+       echo "one -crlf" >>.gitattributes &&
+       git add .gitattributes &&
+       git commit -m "Add .gitattributes" &&
+
+       rm -rf tmp one dir .gitattributes patch.file three &&
+       git read-tree --reset -u HEAD &&
+
+       if remove_cr one >/dev/null
+       then
+               echo "Eh? one should not have CRLF"
+               false
+       else
+               : happy
+       fi &&
+       remove_cr three >/dev/null || {
+               echo "Eh? three should still have CRLF"
+               false
+       }
+'
+
+test_expect_success 'in-tree .gitattributes (2)' '
+
+       rm -rf tmp one dir .gitattributes patch.file three &&
+       git read-tree --reset HEAD &&
+       git checkout-index -f -q -u -a &&
+
+       if remove_cr one >/dev/null
+       then
+               echo "Eh? one should not have CRLF"
+               false
+       else
+               : happy
+       fi &&
+       remove_cr three >/dev/null || {
+               echo "Eh? three should still have CRLF"
+               false
+       }
+'
+
+test_expect_success 'in-tree .gitattributes (3)' '
+
+       rm -rf tmp one dir .gitattributes patch.file three &&
+       git read-tree --reset HEAD &&
+       git checkout-index -u .gitattributes &&
+       git checkout-index -u one dir/two three &&
+
+       if remove_cr one >/dev/null
+       then
+               echo "Eh? one should not have CRLF"
+               false
+       else
+               : happy
+       fi &&
+       remove_cr three >/dev/null || {
+               echo "Eh? three should still have CRLF"
+               false
+       }
+'
+
+test_expect_success 'in-tree .gitattributes (4)' '
+
+       rm -rf tmp one dir .gitattributes patch.file three &&
+       git read-tree --reset HEAD &&
+       git checkout-index -u one dir/two three &&
+       git checkout-index -u .gitattributes &&
+
+       if remove_cr one >/dev/null
+       then
+               echo "Eh? one should not have CRLF"
+               false
+       else
+               : happy
+       fi &&
+       remove_cr three >/dev/null || {
+               echo "Eh? three should still have CRLF"
+               false
+       }
+'
+
+test_expect_success 'invalid .gitattributes (must not crash)' '
+
+       echo "three +crlf" >>.gitattributes &&
+       git diff
+
+'
+
+test_done
diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh
new file mode 100755 (executable)
index 0000000..cb86029
--- /dev/null
@@ -0,0 +1,89 @@
+#!/bin/sh
+
+test_description='blob conversion via gitattributes'
+
+. ./test-lib.sh
+
+cat <<\EOF >rot13.sh
+tr '[a-zA-Z]' '[n-za-mN-ZA-M]'
+EOF
+chmod +x rot13.sh
+
+test_expect_success setup '
+       git config filter.rot13.smudge ./rot13.sh &&
+       git config filter.rot13.clean ./rot13.sh &&
+
+       {
+           echo "*.t filter=rot13"
+           echo "*.i ident"
+       } >.gitattributes &&
+
+       {
+           echo a b c d e f g h i j k l m
+           echo n o p q r s t u v w x y z
+           echo '\''$Id$'\''
+       } >test &&
+       cat test >test.t &&
+       cat test >test.o &&
+       cat test >test.i &&
+       git add test test.t test.i &&
+       rm -f test test.t test.i &&
+       git checkout -- test test.t test.i
+'
+
+script='s/^\$Id: \([0-9a-f]*\) \$/\1/p'
+
+test_expect_success check '
+
+       cmp test.o test &&
+       cmp test.o test.t &&
+
+       # ident should be stripped in the repository
+       git diff --raw --exit-code :test :test.i &&
+       id=$(git rev-parse --verify :test) &&
+       embedded=$(sed -ne "$script" test.i) &&
+       test "z$id" = "z$embedded" &&
+
+       git cat-file blob :test.t > test.r &&
+
+       ./rot13.sh < test.o > test.t &&
+       cmp test.r test.t
+'
+
+# If an expanded ident ever gets into the repository, we want to make sure that
+# it is collapsed before being expanded again on checkout
+test_expect_success expanded_in_repo '
+       {
+               echo "File with expanded keywords"
+               echo "\$Id\$"
+               echo "\$Id:\$"
+               echo "\$Id: 0000000000000000000000000000000000000000 \$"
+               echo "\$Id: NoSpaceAtEnd\$"
+               echo "\$Id:NoSpaceAtFront \$"
+               echo "\$Id:NoSpaceAtEitherEnd\$"
+               echo "\$Id: NoTerminatingSymbol"
+       } > expanded-keywords &&
+
+       {
+               echo "File with expanded keywords"
+               echo "\$Id: 4f21723e7b15065df7de95bd46c8ba6fb1818f4c \$"
+               echo "\$Id: 4f21723e7b15065df7de95bd46c8ba6fb1818f4c \$"
+               echo "\$Id: 4f21723e7b15065df7de95bd46c8ba6fb1818f4c \$"
+               echo "\$Id: 4f21723e7b15065df7de95bd46c8ba6fb1818f4c \$"
+               echo "\$Id: 4f21723e7b15065df7de95bd46c8ba6fb1818f4c \$"
+               echo "\$Id: 4f21723e7b15065df7de95bd46c8ba6fb1818f4c \$"
+               echo "\$Id: NoTerminatingSymbol"
+       } > expected-output &&
+
+       git add expanded-keywords &&
+       git commit -m "File with keywords expanded" &&
+
+       echo "expanded-keywords ident" >> .gitattributes &&
+
+       rm -f expanded-keywords &&
+       git checkout -- expanded-keywords &&
+       cat expanded-keywords &&
+       cmp expanded-keywords expected-output
+'
+
+test_done
diff --git a/t/t0022-crlf-rename.sh b/t/t0022-crlf-rename.sh
new file mode 100755 (executable)
index 0000000..430a1d1
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+test_description='ignore CR in CRLF sequence while computing similiarity'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       cat ../t0022-crlf-rename.sh >sample &&
+       git add sample &&
+
+       test_tick &&
+       git commit -m Initial &&
+
+       sed -e "s/\$/\r/" ../t0022-crlf-rename.sh >elpmas &&
+       git add elpmas &&
+       rm -f sample &&
+
+       test_tick &&
+       git commit -a -m Second
+
+'
+
+test_expect_success 'diff -M' '
+
+       git diff-tree -M -r --name-status HEAD^ HEAD |
+       sed -e "s/R[0-9]*/RNUM/" >actual &&
+       echo "RNUM      sample  elpmas" >expect &&
+       diff -u expect actual
+
+'
+
+test_done
diff --git a/t/t0023-crlf-am.sh b/t/t0023-crlf-am.sh
new file mode 100755 (executable)
index 0000000..6f8a434
--- /dev/null
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+test_description='Test am with auto.crlf'
+
+. ./test-lib.sh
+
+cat >patchfile <<\EOF
+From 38be10072e45dd6b08ce40851e3fca60a31a340b Mon Sep 17 00:00:00 2001
+From: Marius Storm-Olsen <x@y.com>
+Date: Thu, 23 Aug 2007 13:00:00 +0200
+Subject: test1
+
+---
+ foo |    1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
+ create mode 100644 foo
+
+diff --git a/foo b/foo
+new file mode 100644
+index 0000000000000000000000000000000000000000..5716ca5987cbf97d6bb54920bea6adde242d87e6
+--- /dev/null
++++ b/foo
+@@ -0,0 +1 @@
++bar
+EOF
+
+test_expect_success 'setup' '
+
+       git config core.autocrlf true &&
+       echo foo >bar &&
+       git add bar &&
+       test_tick &&
+       git commit -m initial
+
+'
+
+test_expect_success 'am' '
+
+       git am --binary -3 <patchfile &&
+       git diff-files --name-status --exit-code
+
+'
+
+test_done
diff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh
new file mode 100755 (executable)
index 0000000..cad95f3
--- /dev/null
@@ -0,0 +1,400 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Carlos Rica
+#
+
+test_description='git stripspace'
+
+. ./test-lib.sh
+
+t40='A quick brown fox jumps over the lazy do'
+s40='                                        '
+sss="$s40$s40$s40$s40$s40$s40$s40$s40$s40$s40" # 400
+ttt="$t40$t40$t40$t40$t40$t40$t40$t40$t40$t40" # 400
+
+test_expect_success \
+    'long lines without spaces should be unchanged' '
+    echo "$ttt" >expect &&
+    git stripspace <expect >actual &&
+    git diff expect actual &&
+
+    echo "$ttt$ttt" >expect &&
+    git stripspace <expect >actual &&
+    git diff expect actual &&
+
+    echo "$ttt$ttt$ttt" >expect &&
+    git stripspace <expect >actual &&
+    git diff expect actual &&
+
+    echo "$ttt$ttt$ttt$ttt" >expect &&
+    git stripspace <expect >actual &&
+    git diff expect actual
+'
+
+test_expect_success \
+    'lines with spaces at the beginning should be unchanged' '
+    echo "$sss$ttt" >expect &&
+    git stripspace <expect >actual &&
+    git diff expect actual &&
+
+    echo "$sss$sss$ttt" >expect &&
+    git stripspace <expect >actual &&
+    git diff expect actual &&
+
+    echo "$sss$sss$sss$ttt" >expect &&
+    git stripspace <expect >actual &&
+    git diff expect actual
+'
+
+test_expect_success \
+    'lines with intermediate spaces should be unchanged' '
+    echo "$ttt$sss$ttt" >expect &&
+    git stripspace <expect >actual &&
+    git diff expect actual &&
+
+    echo "$ttt$sss$sss$ttt" >expect &&
+    git stripspace <expect >actual &&
+    git diff expect actual
+'
+
+test_expect_success \
+    'consecutive blank lines should be unified' '
+    printf "$ttt\n\n$ttt\n" > expect &&
+    printf "$ttt\n\n\n\n\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt\n\n$ttt\n" > expect &&
+    printf "$ttt$ttt\n\n\n\n\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt$ttt\n\n$ttt\n" > expect &&
+    printf "$ttt$ttt$ttt\n\n\n\n\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n$ttt\n" > expect &&
+    printf "$ttt\n\n\n\n\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n$ttt$ttt\n" > expect &&
+    printf "$ttt\n\n\n\n\n$ttt$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n$ttt$ttt$ttt\n" > expect &&
+    printf "$ttt\n\n\n\n\n$ttt$ttt$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n$ttt\n" > expect &&
+    printf "$ttt\n\t\n \n\n  \t\t\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt\n\n$ttt\n" > expect &&
+    printf "$ttt$ttt\n\t\n \n\n  \t\t\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt$ttt\n\n$ttt\n" > expect &&
+    printf "$ttt$ttt$ttt\n\t\n \n\n  \t\t\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n$ttt\n" > expect &&
+    printf "$ttt\n\t\n \n\n  \t\t\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n$ttt$ttt\n" > expect &&
+    printf "$ttt\n\t\n \n\n  \t\t\n$ttt$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n$ttt$ttt$ttt\n" > expect &&
+    printf "$ttt\n\t\n \n\n  \t\t\n$ttt$ttt$ttt\n" | git stripspace >actual &&
+    git diff expect actual
+'
+
+test_expect_success \
+    'only consecutive blank lines should be completely removed' '
+    > expect &&
+
+    printf "\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "\n\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$sss\n$sss\n$sss\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$sss$sss\n$sss\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "\n$sss\n$sss$sss\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$sss$sss$sss$sss\n\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "\n$sss$sss$sss$sss\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "\n\n$sss$sss$sss$sss\n" | git stripspace >actual &&
+    git diff expect actual
+'
+
+test_expect_success \
+    'consecutive blank lines at the beginning should be removed' '
+    printf "$ttt\n" > expect &&
+    printf "\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n" > expect &&
+    printf "\n\n\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt\n" > expect &&
+    printf "\n\n\n$ttt$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt$ttt\n" > expect &&
+    printf "\n\n\n$ttt$ttt$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt$ttt$ttt\n" > expect &&
+    printf "\n\n\n$ttt$ttt$ttt$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n" > expect &&
+
+    printf "$sss\n$sss\n$sss\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "\n$sss\n$sss$sss\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$sss$sss\n$sss\n\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$sss$sss$sss\n\n\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "\n$sss$sss$sss\n\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "\n\n$sss$sss$sss\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual
+'
+
+test_expect_success \
+    'consecutive blank lines at the end should be removed' '
+    printf "$ttt\n" > expect &&
+    printf "$ttt\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n" > expect &&
+    printf "$ttt\n\n\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt\n" > expect &&
+    printf "$ttt$ttt\n\n\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt$ttt\n" > expect &&
+    printf "$ttt$ttt$ttt\n\n\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt$ttt$ttt\n" > expect &&
+    printf "$ttt$ttt$ttt$ttt\n\n\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n" > expect &&
+
+    printf "$ttt\n$sss\n$sss\n$sss\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n$sss\n$sss$sss\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n$sss$sss\n$sss\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n$sss$sss$sss\n\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n$sss$sss$sss\n\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n\n\n$sss$sss$sss\n" | git stripspace >actual &&
+    git diff expect actual
+'
+
+test_expect_success \
+    'text without newline at end should end with newline' '
+    test `printf "$ttt" | git stripspace | wc -l` -gt 0 &&
+    test `printf "$ttt$ttt" | git stripspace | wc -l` -gt 0 &&
+    test `printf "$ttt$ttt$ttt" | git stripspace | wc -l` -gt 0 &&
+    test `printf "$ttt$ttt$ttt$ttt" | git stripspace | wc -l` -gt 0
+'
+
+# text plus spaces at the end:
+
+test_expect_success \
+    'text plus spaces without newline at end should end with newline' '
+    test `printf "$ttt$sss" | git stripspace | wc -l` -gt 0 &&
+    test `printf "$ttt$ttt$sss" | git stripspace | wc -l` -gt 0 &&
+    test `printf "$ttt$ttt$ttt$sss" | git stripspace | wc -l` -gt 0 &&
+    test `printf "$ttt$sss$sss" | git stripspace | wc -l` -gt 0 &&
+    test `printf "$ttt$ttt$sss$sss" | git stripspace | wc -l` -gt 0 &&
+    test `printf "$ttt$sss$sss$sss" | git stripspace | wc -l` -gt 0
+'
+
+test_expect_failure \
+    'text plus spaces without newline at end should not show spaces' '
+    printf "$ttt$sss" | git stripspace | grep -q "  " ||
+    printf "$ttt$ttt$sss" | git stripspace | grep -q "  " ||
+    printf "$ttt$ttt$ttt$sss" | git stripspace | grep -q "  " ||
+    printf "$ttt$sss$sss" | git stripspace | grep -q "  " ||
+    printf "$ttt$ttt$sss$sss" | git stripspace | grep -q "  " ||
+    printf "$ttt$sss$sss$sss" | git stripspace | grep -q "  "
+'
+
+test_expect_success \
+    'text plus spaces without newline should show the correct lines' '
+    printf "$ttt\n" >expect &&
+    printf "$ttt$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n" >expect &&
+    printf "$ttt$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n" >expect &&
+    printf "$ttt$sss$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt\n" >expect &&
+    printf "$ttt$ttt$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt\n" >expect &&
+    printf "$ttt$ttt$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt$ttt\n" >expect &&
+    printf "$ttt$ttt$ttt$sss" | git stripspace >actual &&
+    git diff expect actual
+'
+
+test_expect_failure \
+    'text plus spaces at end should not show spaces' '
+    echo "$ttt$sss" | git stripspace | grep -q "  " ||
+    echo "$ttt$ttt$sss" | git stripspace | grep -q "  " ||
+    echo "$ttt$ttt$ttt$sss" | git stripspace | grep -q "  " ||
+    echo "$ttt$sss$sss" | git stripspace | grep -q "  " ||
+    echo "$ttt$ttt$sss$sss" | git stripspace | grep -q "  " ||
+    echo "$ttt$sss$sss$sss" | git stripspace | grep -q "  "
+'
+
+test_expect_success \
+    'text plus spaces at end should be cleaned and newline must remain' '
+    echo "$ttt" >expect &&
+    echo "$ttt$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    echo "$ttt" >expect &&
+    echo "$ttt$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    echo "$ttt" >expect &&
+    echo "$ttt$sss$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    echo "$ttt$ttt" >expect &&
+    echo "$ttt$ttt$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    echo "$ttt$ttt" >expect &&
+    echo "$ttt$ttt$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    echo "$ttt$ttt$ttt" >expect &&
+    echo "$ttt$ttt$ttt$sss" | git stripspace >actual &&
+    git diff expect actual
+'
+
+# spaces only:
+
+test_expect_success \
+    'spaces with newline at end should be replaced with empty string' '
+    printf "" >expect &&
+
+    echo | git stripspace >actual &&
+    git diff expect actual &&
+
+    echo "$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    echo "$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    echo "$sss$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    echo "$sss$sss$sss$sss" | git stripspace >actual &&
+    git diff expect actual
+'
+
+test_expect_failure \
+    'spaces without newline at end should not show spaces' '
+    printf "" | git stripspace | grep -q " " ||
+    printf "$sss" | git stripspace | grep -q " " ||
+    printf "$sss$sss" | git stripspace | grep -q " " ||
+    printf "$sss$sss$sss" | git stripspace | grep -q " " ||
+    printf "$sss$sss$sss$sss" | git stripspace | grep -q " "
+'
+
+test_expect_success \
+    'spaces without newline at end should be replaced with empty string' '
+    printf "" >expect &&
+
+    printf "" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$sss$sss$sss" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$sss$sss$sss$sss" | git stripspace >actual &&
+    git diff expect actual
+'
+
+test_expect_success \
+    'consecutive text lines should be unchanged' '
+    printf "$ttt$ttt\n$ttt\n" >expect &&
+    printf "$ttt$ttt\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n$ttt$ttt\n$ttt\n" >expect &&
+    printf "$ttt\n$ttt$ttt\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n$ttt\n$ttt\n$ttt$ttt\n" >expect &&
+    printf "$ttt\n$ttt\n$ttt\n$ttt$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n$ttt\n\n$ttt$ttt\n$ttt\n" >expect &&
+    printf "$ttt\n$ttt\n\n$ttt$ttt\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt$ttt\n\n$ttt\n$ttt$ttt\n" >expect &&
+    printf "$ttt$ttt\n\n$ttt\n$ttt$ttt\n" | git stripspace >actual &&
+    git diff expect actual &&
+
+    printf "$ttt\n$ttt$ttt\n\n$ttt\n" >expect &&
+    printf "$ttt\n$ttt$ttt\n\n$ttt\n" | git stripspace >actual &&
+    git diff expect actual
+'
+
+test_expect_success 'strip comments, too' '
+       test ! -z "$(echo "# comment" | git stripspace)" &&
+       test -z "$(echo "# comment" | git stripspace -s)"
+'
+
+test_done
diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh
new file mode 100755 (executable)
index 0000000..462fdf2
--- /dev/null
@@ -0,0 +1,106 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes Schindelin
+#
+
+test_description='our own option parser'
+
+. ./test-lib.sh
+
+cat > expect.err << EOF
+usage: test-parse-options <options>
+
+    -b, --boolean         get a boolean
+    -i, --integer <n>     get a integer
+    -j <n>                get a integer, too
+
+string options
+    -s, --string <string>
+                          get a string
+    --string2 <str>       get another string
+    --st <st>             get another string (pervert ordering)
+
+EOF
+
+test_expect_success 'test help' '
+       ! test-parse-options -h > output 2> output.err &&
+       test ! -s output &&
+       git diff expect.err output.err
+'
+
+cat > expect << EOF
+boolean: 2
+integer: 1729
+string: 123
+EOF
+
+test_expect_success 'short options' '
+       test-parse-options -s123 -b -i 1729 -b > output 2> output.err &&
+       git diff expect output &&
+       test ! -s output.err
+'
+cat > expect << EOF
+boolean: 2
+integer: 1729
+string: 321
+EOF
+
+test_expect_success 'long options' '
+       test-parse-options --boolean --integer 1729 --boolean --string2=321 \
+               > output 2> output.err &&
+       test ! -s output.err &&
+       git diff expect output
+'
+
+cat > expect << EOF
+boolean: 1
+integer: 13
+string: 123
+arg 00: a1
+arg 01: b1
+arg 02: --boolean
+EOF
+
+test_expect_success 'intermingled arguments' '
+       test-parse-options a1 --string 123 b1 --boolean -j 13 -- --boolean \
+               > output 2> output.err &&
+       test ! -s output.err &&
+       git diff expect output
+'
+
+cat > expect << EOF
+boolean: 0
+integer: 2
+string: (not set)
+EOF
+
+test_expect_success 'unambiguously abbreviated option' '
+       test-parse-options --int 2 --boolean --no-bo > output 2> output.err &&
+       test ! -s output.err &&
+       git diff expect output
+'
+
+test_expect_success 'unambiguously abbreviated option with "="' '
+       test-parse-options --int=2 > output 2> output.err &&
+       test ! -s output.err &&
+       git diff expect output
+'
+
+test_expect_failure 'ambiguously abbreviated option' '
+       test-parse-options --strin 123;
+        test $? != 129
+'
+
+cat > expect << EOF
+boolean: 0
+integer: 0
+string: 123
+EOF
+
+test_expect_success 'non ambiguous option (after two options it abbreviates)' '
+       test-parse-options --st 123 > output 2> output.err &&
+       test ! -s output.err &&
+       git diff expect output
+'
+
+test_done
diff --git a/t/t1000-read-tree-m-3way.sh b/t/t1000-read-tree-m-3way.sh
new file mode 100755 (executable)
index 0000000..37add1b
--- /dev/null
@@ -0,0 +1,514 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Three way merge with read-tree -m
+
+This test tries three-way merge with read-tree -m
+
+There is one ancestor (called O for Original) and two branches A
+and B derived from it.  We want to do a 3-way merge between A and
+B, using O as the common ancestor.
+
+    merge A O B
+
+Decisions are made by comparing contents of O, A and B pathname
+by pathname.  The result is determined by the following guiding
+principle:
+
+ - If only A does something to it and B does not touch it, take
+   whatever A does.
+
+ - If only B does something to it and A does not touch it, take
+   whatever B does.
+
+ - If both A and B does something but in the same way, take
+   whatever they do.
+
+ - If A and B does something but different things, we need a
+   3-way merge:
+
+   - We cannot do anything about the following cases:
+
+     * O does not have it.  A and B both must be adding to the
+       same path independently.
+
+     * A deletes it.  B must be modifying.
+
+   - Otherwise, A and B are modifying.  Run 3-way merge.
+
+First, the case matrix.
+
+ - Vertical axis is for A'\''s actions.
+ - Horizontal axis is for B'\''s actions.
+
+.----------------------------------------------------------------.
+| A        B | No Action  |   Delete   |   Modify   |    Add     |
+|------------+------------+------------+------------+------------|
+| No Action  |            |            |            |            |
+|            | select O   | delete     | select B   | select B   |
+|            |            |            |            |            |
+|------------+------------+------------+------------+------------|
+| Delete     |            |            | ********** |    can     |
+|            | delete     | delete     | merge      |    not     |
+|            |            |            |            |  happen    |
+|------------+------------+------------+------------+------------|
+| Modify     |            | ********** | ?????????? |    can     |
+|            | select A   | merge      | select A=B |    not     |
+|            |            |            | merge      |  happen    |
+|------------+------------+------------+------------+------------|
+| Add        |            |    can     |    can     | ?????????? |
+|            | select A   |    not     |    not     | select A=B |
+|            |            |  happen    |  happen    | merge      |
+.----------------------------------------------------------------.
+
+In addition:
+
+ SS: a special case of MM, where A and B makes the same modification.
+ LL: a special case of AA, where A and B creates the same file.
+ TT: a special case of MM, where A and B makes mergeable changes.
+ DF: a special case, where A makes a directory and B makes a file.
+
+'
+. ./test-lib.sh
+. ../lib-read-tree-m-3way.sh
+
+################################################################
+# Trivial "majority when 3 stages exist" merge plus #2ALT, #3ALT
+# and #5ALT trivial merges.
+
+cat >expected <<\EOF
+100644 X 2     AA
+100644 X 3     AA
+100644 X 0     AN
+100644 X 1     DD
+100644 X 3     DF
+100644 X 2     DF/DF
+100644 X 1     DM
+100644 X 3     DM
+100644 X 1     DN
+100644 X 3     DN
+100644 X 0     LL
+100644 X 1     MD
+100644 X 2     MD
+100644 X 1     MM
+100644 X 2     MM
+100644 X 3     MM
+100644 X 0     MN
+100644 X 0     NA
+100644 X 1     ND
+100644 X 2     ND
+100644 X 0     NM
+100644 X 0     NN
+100644 X 0     SS
+100644 X 1     TT
+100644 X 2     TT
+100644 X 3     TT
+100644 X 2     Z/AA
+100644 X 3     Z/AA
+100644 X 0     Z/AN
+100644 X 1     Z/DD
+100644 X 1     Z/DM
+100644 X 3     Z/DM
+100644 X 1     Z/DN
+100644 X 3     Z/DN
+100644 X 1     Z/MD
+100644 X 2     Z/MD
+100644 X 1     Z/MM
+100644 X 2     Z/MM
+100644 X 3     Z/MM
+100644 X 0     Z/MN
+100644 X 0     Z/NA
+100644 X 1     Z/ND
+100644 X 2     Z/ND
+100644 X 0     Z/NM
+100644 X 0     Z/NN
+EOF
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+
+check_result () {
+    git ls-files --stage | sed -e 's/ '"$_x40"' / X /' >current &&
+    git diff expected current
+}
+
+# This is done on an empty work directory, which is the normal
+# merge person behaviour.
+test_expect_success \
+    '3-way merge with git read-tree -m, empty cache' \
+    "rm -fr [NDMALTS][NDMALTSF] Z &&
+     rm .git/index &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+# This starts out with the first head, which is the normal
+# patch submitter behaviour.
+test_expect_success \
+    '3-way merge with git read-tree -m, match H' \
+    "rm -fr [NDMALTS][NDMALTSF] Z &&
+     rm .git/index &&
+     git read-tree $tree_A &&
+     git checkout-index -f -u -a &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+: <<\END_OF_CASE_TABLE
+
+We have so far tested only empty index and clean-and-matching-A index
+case which are trivial.  Make sure index requirements are also
+checked.
+
+"git read-tree -m O A B"
+
+     O       A       B         result      index requirements
+-------------------------------------------------------------------
+  1  missing missing missing   -           must not exist.
+ ------------------------------------------------------------------
+  2  missing missing exists    take B*     must match B, if exists.
+ ------------------------------------------------------------------
+  3  missing exists  missing   take A*     must match A, if exists.
+ ------------------------------------------------------------------
+  4  missing exists  A!=B      no merge    must match A and be
+                                           up-to-date, if exists.
+ ------------------------------------------------------------------
+  5  missing exists  A==B      take A      must match A, if exists.
+ ------------------------------------------------------------------
+  6  exists  missing missing   remove      must not exist.
+ ------------------------------------------------------------------
+  7  exists  missing O!=B      no merge    must not exist.
+ ------------------------------------------------------------------
+  8  exists  missing O==B      remove      must not exist.
+ ------------------------------------------------------------------
+  9  exists  O!=A    missing   no merge    must match A and be
+                                           up-to-date, if exists.
+ ------------------------------------------------------------------
+ 10  exists  O==A    missing   no merge    must match A
+ ------------------------------------------------------------------
+ 11  exists  O!=A    O!=B      no merge    must match A and be
+                     A!=B                  up-to-date, if exists.
+ ------------------------------------------------------------------
+ 12  exists  O!=A    O!=B      take A      must match A, if exists.
+                     A==B
+ ------------------------------------------------------------------
+ 13  exists  O!=A    O==B      take A      must match A, if exists.
+ ------------------------------------------------------------------
+ 14  exists  O==A    O!=B      take B      if exists, must either (1)
+                                           match A and be up-to-date,
+                                           or (2) match B.
+ ------------------------------------------------------------------
+ 15  exists  O==A    O==B      take B      must match A if exists.
+ ------------------------------------------------------------------
+ 16  exists  O==A    O==B      barf        must match A if exists.
+     *multi* in one  in another
+-------------------------------------------------------------------
+
+Note: we need to be careful in case 2 and 3.  The tree A may contain
+DF (file) when tree B require DF to be a directory by having DF/DF
+(file).
+
+END_OF_CASE_TABLE
+
+test_expect_failure \
+    '1 - must not have an entry not in A.' \
+    "rm -f .git/index XX &&
+     echo XX >XX &&
+     git update-index --add XX &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '2 - must match B in !O && !A && B case.' \
+    "rm -f .git/index NA &&
+     cp .orig-B/NA NA &&
+     git update-index --add NA &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '2 - matching B alone is OK in !O && !A && B case.' \
+    "rm -f .git/index NA &&
+     cp .orig-B/NA NA &&
+     git update-index --add NA &&
+     echo extra >>NA &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '3 - must match A in !O && A && !B case.' \
+    "rm -f .git/index AN &&
+     cp .orig-A/AN AN &&
+     git update-index --add AN &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_success \
+    '3 - matching A alone is OK in !O && A && !B case.' \
+    "rm -f .git/index AN &&
+     cp .orig-A/AN AN &&
+     git update-index --add AN &&
+     echo extra >>AN &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_failure \
+    '3 (fail) - must match A in !O && A && !B case.' \
+    "rm -f .git/index AN &&
+     cp .orig-A/AN AN &&
+     echo extra >>AN &&
+     git update-index --add AN &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '4 - must match and be up-to-date in !O && A && B && A!=B case.' \
+    "rm -f .git/index AA &&
+     cp .orig-A/AA AA &&
+     git update-index --add AA &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_failure \
+    '4 (fail) - must match and be up-to-date in !O && A && B && A!=B case.' \
+    "rm -f .git/index AA &&
+     cp .orig-A/AA AA &&
+     git update-index --add AA &&
+     echo extra >>AA &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_failure \
+    '4 (fail) - must match and be up-to-date in !O && A && B && A!=B case.' \
+    "rm -f .git/index AA &&
+     cp .orig-A/AA AA &&
+     echo extra >>AA &&
+     git update-index --add AA &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '5 - must match in !O && A && B && A==B case.' \
+    "rm -f .git/index LL &&
+     cp .orig-A/LL LL &&
+     git update-index --add LL &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_success \
+    '5 - must match in !O && A && B && A==B case.' \
+    "rm -f .git/index LL &&
+     cp .orig-A/LL LL &&
+     git update-index --add LL &&
+     echo extra >>LL &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_failure \
+    '5 (fail) - must match A in !O && A && B && A==B case.' \
+    "rm -f .git/index LL &&
+     cp .orig-A/LL LL &&
+     echo extra >>LL &&
+     git update-index --add LL &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_failure \
+    '6 - must not exist in O && !A && !B case' \
+    "rm -f .git/index DD &&
+     echo DD >DD
+     git update-index --add DD &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_failure \
+    '7 - must not exist in O && !A && B && O!=B case' \
+    "rm -f .git/index DM &&
+     cp .orig-B/DM DM &&
+     git update-index --add DM &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_failure \
+    '8 - must not exist in O && !A && B && O==B case' \
+    "rm -f .git/index DN &&
+     cp .orig-B/DN DN &&
+     git update-index --add DN &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '9 - must match and be up-to-date in O && A && !B && O!=A case' \
+    "rm -f .git/index MD &&
+     cp .orig-A/MD MD &&
+     git update-index --add MD &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_failure \
+    '9 (fail) - must match and be up-to-date in O && A && !B && O!=A case' \
+    "rm -f .git/index MD &&
+     cp .orig-A/MD MD &&
+     git update-index --add MD &&
+     echo extra >>MD &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_failure \
+    '9 (fail) - must match and be up-to-date in O && A && !B && O!=A case' \
+    "rm -f .git/index MD &&
+     cp .orig-A/MD MD &&
+     echo extra >>MD &&
+     git update-index --add MD &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '10 - must match and be up-to-date in O && A && !B && O==A case' \
+    "rm -f .git/index ND &&
+     cp .orig-A/ND ND &&
+     git update-index --add ND &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_failure \
+    '10 (fail) - must match and be up-to-date in O && A && !B && O==A case' \
+    "rm -f .git/index ND &&
+     cp .orig-A/ND ND &&
+     git update-index --add ND &&
+     echo extra >>ND &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_failure \
+    '10 (fail) - must match and be up-to-date in O && A && !B && O==A case' \
+    "rm -f .git/index ND &&
+     cp .orig-A/ND ND &&
+     echo extra >>ND &&
+     git update-index --add ND &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '11 - must match and be up-to-date in O && A && B && O!=A && O!=B && A!=B case' \
+    "rm -f .git/index MM &&
+     cp .orig-A/MM MM &&
+     git update-index --add MM &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_failure \
+    '11 (fail) - must match and be up-to-date in O && A && B && O!=A && O!=B && A!=B case' \
+    "rm -f .git/index MM &&
+     cp .orig-A/MM MM &&
+     git update-index --add MM &&
+     echo extra >>MM &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_failure \
+    '11 (fail) - must match and be up-to-date in O && A && B && O!=A && O!=B && A!=B case' \
+    "rm -f .git/index MM &&
+     cp .orig-A/MM MM &&
+     echo extra >>MM &&
+     git update-index --add MM &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '12 - must match A in O && A && B && O!=A && A==B case' \
+    "rm -f .git/index SS &&
+     cp .orig-A/SS SS &&
+     git update-index --add SS &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_success \
+    '12 - must match A in O && A && B && O!=A && A==B case' \
+    "rm -f .git/index SS &&
+     cp .orig-A/SS SS &&
+     git update-index --add SS &&
+     echo extra >>SS &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_failure \
+    '12 (fail) - must match A in O && A && B && O!=A && A==B case' \
+    "rm -f .git/index SS &&
+     cp .orig-A/SS SS &&
+     echo extra >>SS &&
+     git update-index --add SS &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '13 - must match A in O && A && B && O!=A && O==B case' \
+    "rm -f .git/index MN &&
+     cp .orig-A/MN MN &&
+     git update-index --add MN &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_success \
+    '13 - must match A in O && A && B && O!=A && O==B case' \
+    "rm -f .git/index MN &&
+     cp .orig-A/MN MN &&
+     git update-index --add MN &&
+     echo extra >>MN &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_success \
+    '14 - must match and be up-to-date in O && A && B && O==A && O!=B case' \
+    "rm -f .git/index NM &&
+     cp .orig-A/NM NM &&
+     git update-index --add NM &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_success \
+    '14 - may match B in O && A && B && O==A && O!=B case' \
+    "rm -f .git/index NM &&
+     cp .orig-B/NM NM &&
+     git update-index --add NM &&
+     echo extra >>NM &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_failure \
+    '14 (fail) - must match and be up-to-date in O && A && B && O==A && O!=B case' \
+    "rm -f .git/index NM &&
+     cp .orig-A/NM NM &&
+     git update-index --add NM &&
+     echo extra >>NM &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_failure \
+    '14 (fail) - must match and be up-to-date in O && A && B && O==A && O!=B case' \
+    "rm -f .git/index NM &&
+     cp .orig-A/NM NM &&
+     echo extra >>NM &&
+     git update-index --add NM &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+test_expect_success \
+    '15 - must match A in O && A && B && O==A && O==B case' \
+    "rm -f .git/index NN &&
+     cp .orig-A/NN NN &&
+     git update-index --add NN &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_success \
+    '15 - must match A in O && A && B && O==A && O==B case' \
+    "rm -f .git/index NN &&
+     cp .orig-A/NN NN &&
+     git update-index --add NN &&
+     echo extra >>NN &&
+     git read-tree -m $tree_O $tree_A $tree_B &&
+     check_result"
+
+test_expect_failure \
+    '15 (fail) - must match A in O && A && B && O==A && O==B case' \
+    "rm -f .git/index NN &&
+     cp .orig-A/NN NN &&
+     echo extra >>NN &&
+     git update-index --add NN &&
+     git read-tree -m $tree_O $tree_A $tree_B"
+
+# #16
+test_expect_success \
+    '16 - A matches in one and B matches in another.' \
+    'rm -f .git/index F16 &&
+    echo F16 >F16 &&
+    git update-index --add F16 &&
+    tree0=`git write-tree` &&
+    echo E16 >F16 &&
+    git update-index F16 &&
+    tree1=`git write-tree` &&
+    git read-tree -m $tree0 $tree1 $tree1 $tree0 &&
+    git ls-files --stage'
+
+test_done
diff --git a/t/t1001-read-tree-m-2way.sh b/t/t1001-read-tree-m-2way.sh
new file mode 100755 (executable)
index 0000000..b01b003
--- /dev/null
@@ -0,0 +1,344 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Two way merge with read-tree -m $H $M
+
+This test tries two-way merge (aka fast forward with carry forward).
+
+There is the head (called H) and another commit (called M), which is
+simply ahead of H.  The index and the work tree contains a state that
+is derived from H, but may also have local changes.  This test checks
+all the combinations described in the two-tree merge "carry forward"
+rules, found in <Documentation/git read-tree.txt>.
+
+In the test, these paths are used:
+        bozbar  - in H, stays in M, modified from bozbar to gnusto
+        frotz   - not in H added in M
+        nitfol  - in H, stays in M unmodified
+        rezrov  - in H, deleted in M
+        yomin   - not in H nor M
+'
+. ./test-lib.sh
+
+read_tree_twoway () {
+    git read-tree -m "$1" "$2" && git ls-files --stage
+}
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+compare_change () {
+       sed -n >current \
+           -e '/^--- /d; /^+++ /d; /^@@ /d;' \
+           -e 's/^\([-+][0-7][0-7][0-7][0-7][0-7][0-7]\) '"$_x40"' /\1 X /p' \
+           "$1"
+       git diff expected current
+}
+
+check_cache_at () {
+       clean_if_empty=`git diff-files -- "$1"`
+       case "$clean_if_empty" in
+       '')  echo "$1: clean" ;;
+       ?*)  echo "$1: dirty" ;;
+       esac
+       case "$2,$clean_if_empty" in
+       clean,)         :     ;;
+       clean,?*)       false ;;
+       dirty,)         false ;;
+       dirty,?*)       :     ;;
+       esac
+}
+
+cat >bozbar-old <<\EOF
+This is a sample file used in two-way fast forward merge
+tests.  Its second line ends with a magic word bozbar
+which will be modified by the merged head to gnusto.
+It has some extra lines so that external tools can
+successfully merge independent changes made to later
+lines (such as this one), avoiding line conflicts.
+EOF
+
+sed -e 's/bozbar/gnusto (earlier bozbar)/' bozbar-old >bozbar-new
+
+test_expect_success \
+    setup \
+    'echo frotz >frotz &&
+     echo nitfol >nitfol &&
+     cat bozbar-old >bozbar &&
+     echo rezrov >rezrov &&
+     echo yomin >yomin &&
+     git update-index --add nitfol bozbar rezrov &&
+     treeH=`git write-tree` &&
+     echo treeH $treeH &&
+     git ls-tree $treeH &&
+
+     cat bozbar-new >bozbar &&
+     git update-index --add frotz bozbar --force-remove rezrov &&
+     git ls-files --stage >M.out &&
+     treeM=`git write-tree` &&
+     echo treeM $treeM &&
+     git ls-tree $treeM &&
+     git diff-tree $treeH $treeM'
+
+test_expect_success \
+    '1, 2, 3 - no carry forward' \
+    'rm -f .git/index &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >1-3.out &&
+     git diff M.out 1-3.out &&
+     check_cache_at bozbar dirty &&
+     check_cache_at frotz dirty &&
+     check_cache_at nitfol dirty'
+
+echo '+100644 X 0      yomin' >expected
+
+test_expect_success \
+    '4 - carry forward local addition.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     git update-index --add yomin &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >4.out || return 1
+     git diff M.out 4.out >4diff.out
+     compare_change 4diff.out expected &&
+     check_cache_at yomin clean'
+
+test_expect_success \
+    '5 - carry forward local addition.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo yomin >yomin &&
+     git update-index --add yomin &&
+     echo yomin yomin >yomin &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >5.out || return 1
+     git diff M.out 5.out >5diff.out
+     compare_change 5diff.out expected &&
+     check_cache_at yomin dirty'
+
+test_expect_success \
+    '6 - local addition already has the same.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     git update-index --add frotz &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >6.out &&
+     git diff M.out 6.out &&
+     check_cache_at frotz clean'
+
+test_expect_success \
+    '7 - local addition already has the same.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo frotz >frotz &&
+     git update-index --add frotz &&
+     echo frotz frotz >frotz &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >7.out &&
+     git diff M.out 7.out &&
+     check_cache_at frotz dirty'
+
+test_expect_success \
+    '8 - conflicting addition.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo frotz frotz >frotz &&
+     git update-index --add frotz &&
+     if read_tree_twoway $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '9 - conflicting addition.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo frotz frotz >frotz &&
+     git update-index --add frotz &&
+     echo frotz >frotz &&
+     if read_tree_twoway $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '10 - path removed.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo rezrov >rezrov &&
+     git update-index --add rezrov &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >10.out &&
+     git diff M.out 10.out'
+
+test_expect_success \
+    '11 - dirty path removed.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo rezrov >rezrov &&
+     git update-index --add rezrov &&
+     echo rezrov rezrov >rezrov &&
+     if read_tree_twoway $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '12 - unmatching local changes being removed.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo rezrov rezrov >rezrov &&
+     git update-index --add rezrov &&
+     if read_tree_twoway $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '13 - unmatching local changes being removed.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo rezrov rezrov >rezrov &&
+     git update-index --add rezrov &&
+     echo rezrov >rezrov &&
+     if read_tree_twoway $treeH $treeM; then false; else :; fi'
+
+cat >expected <<EOF
+-100644 X 0    nitfol
++100644 X 0    nitfol
+EOF
+
+test_expect_success \
+    '14 - unchanged in two heads.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo nitfol nitfol >nitfol &&
+     git update-index --add nitfol &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >14.out || return 1
+     git diff M.out 14.out >14diff.out
+     compare_change 14diff.out expected &&
+     check_cache_at nitfol clean'
+
+test_expect_success \
+    '15 - unchanged in two heads.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo nitfol nitfol >nitfol &&
+     git update-index --add nitfol &&
+     echo nitfol nitfol nitfol >nitfol &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >15.out || return 1
+     git diff M.out 15.out >15diff.out
+     compare_change 15diff.out expected &&
+     check_cache_at nitfol dirty'
+
+test_expect_success \
+    '16 - conflicting local change.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo bozbar bozbar >bozbar &&
+     git update-index --add bozbar &&
+     if read_tree_twoway $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '17 - conflicting local change.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     echo bozbar bozbar >bozbar &&
+     git update-index --add bozbar &&
+     echo bozbar bozbar bozbar >bozbar &&
+     if read_tree_twoway $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '18 - local change already having a good result.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     cat bozbar-new >bozbar &&
+     git update-index --add bozbar &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >18.out &&
+     git diff M.out 18.out &&
+     check_cache_at bozbar clean'
+
+test_expect_success \
+    '19 - local change already having a good result, further modified.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     cat bozbar-new >bozbar &&
+     git update-index --add bozbar &&
+     echo gnusto gnusto >bozbar &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >19.out &&
+     git diff M.out 19.out &&
+     check_cache_at bozbar dirty'
+
+test_expect_success \
+    '20 - no local change, use new tree.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     cat bozbar-old >bozbar &&
+     git update-index --add bozbar &&
+     read_tree_twoway $treeH $treeM &&
+     git ls-files --stage >20.out &&
+     git diff M.out 20.out &&
+     check_cache_at bozbar dirty'
+
+test_expect_success \
+    '21 - no local change, dirty cache.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     cat bozbar-old >bozbar &&
+     git update-index --add bozbar &&
+     echo gnusto gnusto >bozbar &&
+     if read_tree_twoway $treeH $treeM; then false; else :; fi'
+
+# This fails with straight two-way fast forward.
+test_expect_success \
+    '22 - local change cache updated.' \
+    'rm -f .git/index &&
+     git read-tree $treeH &&
+     git checkout-index -u -f -q -a &&
+     sed -e "s/such as/SUCH AS/" bozbar-old >bozbar &&
+     git update-index --add bozbar &&
+     if read_tree_twoway $treeH $treeM; then false; else :; fi'
+
+# Also make sure we did not break DF vs DF/DF case.
+test_expect_success \
+    'DF vs DF/DF case setup.' \
+    'rm -f .git/index &&
+     echo DF >DF &&
+     git update-index --add DF &&
+     treeDF=`git write-tree` &&
+     echo treeDF $treeDF &&
+     git ls-tree $treeDF &&
+
+     rm -f DF &&
+     mkdir DF &&
+     echo DF/DF >DF/DF &&
+     git update-index --add --remove DF DF/DF &&
+     treeDFDF=`git write-tree` &&
+     echo treeDFDF $treeDFDF &&
+     git ls-tree $treeDFDF &&
+     git ls-files --stage >DFDF.out'
+
+test_expect_success \
+    'DF vs DF/DF case test.' \
+    'rm -f .git/index &&
+     rm -fr DF &&
+     echo DF >DF &&
+     git update-index --add DF &&
+     read_tree_twoway $treeDF $treeDFDF &&
+     git ls-files --stage >DFDFcheck.out &&
+     git diff DFDF.out DFDFcheck.out &&
+     check_cache_at DF/DF dirty &&
+     :'
+
+test_done
diff --git a/t/t1002-read-tree-m-u-2way.sh b/t/t1002-read-tree-m-u-2way.sh
new file mode 100755 (executable)
index 0000000..42e5cf8
--- /dev/null
@@ -0,0 +1,344 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Two way merge with read-tree -m -u $H $M
+
+This is identical to t1001, but uses -u to update the work tree as well.
+
+'
+. ./test-lib.sh
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+compare_change () {
+       sed >current \
+           -e '/^--- /d; /^+++ /d; /^@@ /d;' \
+           -e 's/^\(.[0-7][0-7][0-7][0-7][0-7][0-7]\) '"$_x40"' /\1 X /' "$1"
+       git diff expected current
+}
+
+check_cache_at () {
+       clean_if_empty=`git diff-files -- "$1"`
+       case "$clean_if_empty" in
+       '')  echo "$1: clean" ;;
+       ?*)  echo "$1: dirty" ;;
+       esac
+       case "$2,$clean_if_empty" in
+       clean,)         :     ;;
+       clean,?*)       false ;;
+       dirty,)         false ;;
+       dirty,?*)       :     ;;
+       esac
+}
+
+test_expect_success \
+    setup \
+    'echo frotz >frotz &&
+     echo nitfol >nitfol &&
+     echo bozbar >bozbar &&
+     echo rezrov >rezrov &&
+     git update-index --add nitfol bozbar rezrov &&
+     treeH=`git write-tree` &&
+     echo treeH $treeH &&
+     git ls-tree $treeH &&
+
+     echo gnusto >bozbar &&
+     git update-index --add frotz bozbar --force-remove rezrov &&
+     git ls-files --stage >M.out &&
+     treeM=`git write-tree` &&
+     echo treeM $treeM &&
+     git ls-tree $treeM &&
+     sum bozbar frotz nitfol >M.sum &&
+     git diff-tree $treeH $treeM'
+
+test_expect_success \
+    '1, 2, 3 - no carry forward' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >1-3.out &&
+     cmp M.out 1-3.out &&
+     sum bozbar frotz nitfol >actual3.sum &&
+     cmp M.sum actual3.sum &&
+     check_cache_at bozbar clean &&
+     check_cache_at frotz clean &&
+     check_cache_at nitfol clean'
+
+test_expect_success \
+    '4 - carry forward local addition.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo "+100644 X 0 yomin" >expected &&
+     echo yomin >yomin &&
+     git update-index --add yomin &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >4.out || return 1
+     diff -U0 M.out 4.out >4diff.out
+     compare_change 4diff.out expected &&
+     check_cache_at yomin clean &&
+     sum bozbar frotz nitfol >actual4.sum &&
+     cmp M.sum actual4.sum &&
+     echo yomin >yomin1 &&
+     diff yomin yomin1 &&
+     rm -f yomin1'
+
+test_expect_success \
+    '5 - carry forward local addition.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     git read-tree -m -u $treeH &&
+     echo yomin >yomin &&
+     git update-index --add yomin &&
+     echo yomin yomin >yomin &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >5.out || return 1
+     diff -U0 M.out 5.out >5diff.out
+     compare_change 5diff.out expected &&
+     check_cache_at yomin dirty &&
+     sum bozbar frotz nitfol >actual5.sum &&
+     cmp M.sum actual5.sum &&
+     : dirty index should have prevented -u from checking it out. &&
+     echo yomin yomin >yomin1 &&
+     diff yomin yomin1 &&
+     rm -f yomin1'
+
+test_expect_success \
+    '6 - local addition already has the same.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo frotz >frotz &&
+     git update-index --add frotz &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >6.out &&
+     diff -U0 M.out 6.out &&
+     check_cache_at frotz clean &&
+     sum bozbar frotz nitfol >actual3.sum &&
+     cmp M.sum actual3.sum &&
+     echo frotz >frotz1 &&
+     diff frotz frotz1 &&
+     rm -f frotz1'
+
+test_expect_success \
+    '7 - local addition already has the same.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo frotz >frotz &&
+     git update-index --add frotz &&
+     echo frotz frotz >frotz &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >7.out &&
+     diff -U0 M.out 7.out &&
+     check_cache_at frotz dirty &&
+     sum bozbar frotz nitfol >actual7.sum &&
+     if cmp M.sum actual7.sum; then false; else :; fi &&
+     : dirty index should have prevented -u from checking it out. &&
+     echo frotz frotz >frotz1 &&
+     diff frotz frotz1 &&
+     rm -f frotz1'
+
+test_expect_success \
+    '8 - conflicting addition.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo frotz frotz >frotz &&
+     git update-index --add frotz &&
+     if git read-tree -m -u $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '9 - conflicting addition.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo frotz frotz >frotz &&
+     git update-index --add frotz &&
+     echo frotz >frotz &&
+     if git read-tree -m -u $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '10 - path removed.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo rezrov >rezrov &&
+     git update-index --add rezrov &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >10.out &&
+     cmp M.out 10.out &&
+     sum bozbar frotz nitfol >actual10.sum &&
+     cmp M.sum actual10.sum'
+
+test_expect_success \
+    '11 - dirty path removed.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo rezrov >rezrov &&
+     git update-index --add rezrov &&
+     echo rezrov rezrov >rezrov &&
+     if git read-tree -m -u $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '12 - unmatching local changes being removed.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo rezrov rezrov >rezrov &&
+     git update-index --add rezrov &&
+     if git read-tree -m -u $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '13 - unmatching local changes being removed.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo rezrov rezrov >rezrov &&
+     git update-index --add rezrov &&
+     echo rezrov >rezrov &&
+     if git read-tree -m -u $treeH $treeM; then false; else :; fi'
+
+cat >expected <<EOF
+-100644 X 0    nitfol
++100644 X 0    nitfol
+EOF
+
+test_expect_success \
+    '14 - unchanged in two heads.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo nitfol nitfol >nitfol &&
+     git update-index --add nitfol &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >14.out || return 1
+     diff -U0 M.out 14.out >14diff.out
+     compare_change 14diff.out expected &&
+     sum bozbar frotz >actual14.sum &&
+     grep -v nitfol M.sum > expected14.sum &&
+     cmp expected14.sum actual14.sum &&
+     sum bozbar frotz nitfol >actual14a.sum &&
+     if cmp M.sum actual14a.sum; then false; else :; fi &&
+     check_cache_at nitfol clean &&
+     echo nitfol nitfol >nitfol1 &&
+     diff nitfol nitfol1 &&
+     rm -f nitfol1'
+
+test_expect_success \
+    '15 - unchanged in two heads.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo nitfol nitfol >nitfol &&
+     git update-index --add nitfol &&
+     echo nitfol nitfol nitfol >nitfol &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >15.out || return 1
+     diff -U0 M.out 15.out >15diff.out
+     compare_change 15diff.out expected &&
+     check_cache_at nitfol dirty &&
+     sum bozbar frotz >actual15.sum &&
+     grep -v nitfol M.sum > expected15.sum &&
+     cmp expected15.sum actual15.sum &&
+     sum bozbar frotz nitfol >actual15a.sum &&
+     if cmp M.sum actual15a.sum; then false; else :; fi &&
+     echo nitfol nitfol nitfol >nitfol1 &&
+     diff nitfol nitfol1 &&
+     rm -f nitfol1'
+
+test_expect_success \
+    '16 - conflicting local change.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo bozbar bozbar >bozbar &&
+     git update-index --add bozbar &&
+     if git read-tree -m -u $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '17 - conflicting local change.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo bozbar bozbar >bozbar &&
+     git update-index --add bozbar &&
+     echo bozbar bozbar bozbar >bozbar &&
+     if git read-tree -m -u $treeH $treeM; then false; else :; fi'
+
+test_expect_success \
+    '18 - local change already having a good result.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo gnusto >bozbar &&
+     git update-index --add bozbar &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >18.out &&
+     diff -U0 M.out 18.out &&
+     check_cache_at bozbar clean &&
+     sum bozbar frotz nitfol >actual18.sum &&
+     cmp M.sum actual18.sum'
+
+test_expect_success \
+    '19 - local change already having a good result, further modified.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo gnusto >bozbar &&
+     git update-index --add bozbar &&
+     echo gnusto gnusto >bozbar &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >19.out &&
+     diff -U0 M.out 19.out &&
+     check_cache_at bozbar dirty &&
+     sum frotz nitfol >actual19.sum &&
+     grep -v bozbar  M.sum > expected19.sum &&
+     cmp expected19.sum actual19.sum &&
+     sum bozbar frotz nitfol >actual19a.sum &&
+     if cmp M.sum actual19a.sum; then false; else :; fi &&
+     echo gnusto gnusto >bozbar1 &&
+     diff bozbar bozbar1 &&
+     rm -f bozbar1'
+
+test_expect_success \
+    '20 - no local change, use new tree.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo bozbar >bozbar &&
+     git update-index --add bozbar &&
+     git read-tree -m -u $treeH $treeM &&
+     git ls-files --stage >20.out &&
+     diff -U0 M.out 20.out &&
+     check_cache_at bozbar clean &&
+     sum bozbar frotz nitfol >actual20.sum &&
+     cmp M.sum actual20.sum'
+
+test_expect_success \
+    '21 - no local change, dirty cache.' \
+    'rm -f .git/index nitfol bozbar rezrov frotz &&
+     git read-tree --reset -u $treeH &&
+     echo bozbar >bozbar &&
+     git update-index --add bozbar &&
+     echo gnusto gnusto >bozbar &&
+     if git read-tree -m -u $treeH $treeM; then false; else :; fi'
+
+# Also make sure we did not break DF vs DF/DF case.
+test_expect_success \
+    'DF vs DF/DF case setup.' \
+    'rm -f .git/index
+     echo DF >DF &&
+     git update-index --add DF &&
+     treeDF=`git write-tree` &&
+     echo treeDF $treeDF &&
+     git ls-tree $treeDF &&
+
+     rm -f DF &&
+     mkdir DF &&
+     echo DF/DF >DF/DF &&
+     git update-index --add --remove DF DF/DF &&
+     treeDFDF=`git write-tree` &&
+     echo treeDFDF $treeDFDF &&
+     git ls-tree $treeDFDF &&
+     git ls-files --stage >DFDF.out'
+
+test_expect_success \
+    'DF vs DF/DF case test.' \
+    'rm -f .git/index &&
+     rm -fr DF &&
+     echo DF >DF &&
+     git update-index --add DF &&
+     git read-tree -m -u $treeDF $treeDFDF &&
+     git ls-files --stage >DFDFcheck.out &&
+     diff -U0 DFDF.out DFDFcheck.out &&
+     check_cache_at DF/DF clean'
+
+test_done
diff --git a/t/t1003-read-tree-prefix.sh b/t/t1003-read-tree-prefix.sh
new file mode 100755 (executable)
index 0000000..8c6d67e
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='git read-tree --prefix test.
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       echo hello >one &&
+       git update-index --add one &&
+       tree=`git write-tree` &&
+       echo tree is $tree
+'
+
+echo 'one
+two/one' >expect
+
+test_expect_success 'read-tree --prefix' '
+       git read-tree --prefix=two/ $tree &&
+       git ls-files >actual &&
+       cmp expect actual
+'
+
+test_done
diff --git a/t/t1004-read-tree-m-u-wf.sh b/t/t1004-read-tree-m-u-wf.sh
new file mode 100755 (executable)
index 0000000..d609a55
--- /dev/null
@@ -0,0 +1,119 @@
+#!/bin/sh
+
+test_description='read-tree -m -u checks working tree files'
+
+. ./test-lib.sh
+
+# two-tree test
+
+test_expect_success 'two-way setup' '
+
+       mkdir subdir &&
+       echo >file1 file one &&
+       echo >file2 file two &&
+       echo >subdir/file1 file one in subdirectory &&
+       echo >subdir/file2 file two in subdirectory &&
+       git update-index --add file1 file2 subdir/file1 subdir/file2 &&
+       git commit -m initial &&
+
+       git branch side &&
+       git tag -f branch-point &&
+
+       echo file2 is not tracked on the master anymore &&
+       rm -f file2 subdir/file2 &&
+       git update-index --remove file2 subdir/file2 &&
+       git commit -a -m "master removes file2 and subdir/file2"
+'
+
+test_expect_success 'two-way not clobbering' '
+
+       echo >file2 master creates untracked file2 &&
+       echo >subdir/file2 master creates untracked subdir/file2 &&
+       if err=`git read-tree -m -u master side 2>&1`
+       then
+               echo should have complained
+               false
+       else
+               echo "happy to see $err"
+       fi
+'
+
+echo file2 >.gitignore
+
+test_expect_success 'two-way with incorrect --exclude-per-directory (1)' '
+
+       if err=`git read-tree -m --exclude-per-directory=.gitignore master side 2>&1`
+       then
+               echo should have complained
+               false
+       else
+               echo "happy to see $err"
+       fi
+'
+
+test_expect_success 'two-way with incorrect --exclude-per-directory (2)' '
+
+       if err=`git read-tree -m -u --exclude-per-directory=foo --exclude-per-directory=.gitignore master side 2>&1`
+       then
+               echo should have complained
+               false
+       else
+               echo "happy to see $err"
+       fi
+'
+
+test_expect_success 'two-way clobbering a ignored file' '
+
+       git read-tree -m -u --exclude-per-directory=.gitignore master side
+'
+
+rm -f .gitignore
+
+# three-tree test
+
+test_expect_success 'three-way not complaining on an untracked path in both' '
+
+       rm -f file2 subdir/file2 &&
+       git checkout side &&
+       echo >file3 file three &&
+       echo >subdir/file3 file three &&
+       git update-index --add file3 subdir/file3 &&
+       git commit -a -m "side adds file3 and removes file2" &&
+
+       git checkout master &&
+       echo >file2 file two is untracked on the master side &&
+       echo >subdir/file2 file two is untracked on the master side &&
+
+       git read-tree -m -u branch-point master side
+'
+
+test_expect_success 'three-way not clobbering a working tree file' '
+
+       git reset --hard &&
+       rm -f file2 subdir/file2 file3 subdir/file3 &&
+       git checkout master &&
+       echo >file3 file three created in master, untracked &&
+       echo >subdir/file3 file three created in master, untracked &&
+       if err=`git read-tree -m -u branch-point master side 2>&1`
+       then
+               echo should have complained
+               false
+       else
+               echo "happy to see $err"
+       fi
+'
+
+echo >.gitignore file3
+
+test_expect_success 'three-way not complaining on an untracked file' '
+
+       git reset --hard &&
+       rm -f file2 subdir/file2 file3 subdir/file3 &&
+       git checkout master &&
+       echo >file3 file three created in master, untracked &&
+       echo >subdir/file3 file three created in master, untracked &&
+
+       git read-tree -m -u --exclude-per-directory=.gitignore branch-point master side
+'
+
+test_done
diff --git a/t/t1020-subdirectory.sh b/t/t1020-subdirectory.sh
new file mode 100755 (executable)
index 0000000..b9cef34
--- /dev/null
@@ -0,0 +1,138 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='Try various core-level commands in subdirectory.
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       long="a b c d e f g h i j k l m n o p q r s t u v w x y z" &&
+       for c in $long; do echo $c; done >one &&
+       mkdir dir &&
+       for c in x y z $long a b c; do echo $c; done >dir/two &&
+       cp one original.one &&
+       cp dir/two original.two
+'
+HERE=`pwd`
+LF='
+'
+
+test_expect_success 'update-index and ls-files' '
+       cd $HERE &&
+       git update-index --add one &&
+       case "`git ls-files`" in
+       one) echo ok one ;;
+       *) echo bad one; exit 1 ;;
+       esac &&
+       cd dir &&
+       git update-index --add two &&
+       case "`git ls-files`" in
+       two) echo ok two ;;
+       *) echo bad two; exit 1 ;;
+       esac &&
+       cd .. &&
+       case "`git ls-files`" in
+       dir/two"$LF"one) echo ok both ;;
+       *) echo bad; exit 1 ;;
+       esac
+'
+
+test_expect_success 'cat-file' '
+       cd $HERE &&
+       two=`git ls-files -s dir/two` &&
+       two=`expr "$two" : "[0-7]* \\([0-9a-f]*\\)"` &&
+       echo "$two" &&
+       git cat-file -p "$two" >actual &&
+       cmp dir/two actual &&
+       cd dir &&
+       git cat-file -p "$two" >actual &&
+       cmp two actual
+'
+rm -f actual dir/actual
+
+test_expect_success 'diff-files' '
+       cd $HERE &&
+       echo a >>one &&
+       echo d >>dir/two &&
+       case "`git diff-files --name-only`" in
+       dir/two"$LF"one) echo ok top ;;
+       *) echo bad top; exit 1 ;;
+       esac &&
+       # diff should not omit leading paths
+       cd dir &&
+       case "`git diff-files --name-only`" in
+       dir/two"$LF"one) echo ok subdir ;;
+       *) echo bad subdir; exit 1 ;;
+       esac &&
+       case "`git diff-files --name-only .`" in
+       dir/two) echo ok subdir limited ;;
+       *) echo bad subdir limited; exit 1 ;;
+       esac
+'
+
+test_expect_success 'write-tree' '
+       cd $HERE &&
+       top=`git write-tree` &&
+       echo $top &&
+       cd dir &&
+       sub=`git write-tree` &&
+       echo $sub &&
+       test "z$top" = "z$sub"
+'
+
+test_expect_success 'checkout-index' '
+       cd $HERE &&
+       git checkout-index -f -u one &&
+       cmp one original.one &&
+       cd dir &&
+       git checkout-index -f -u two &&
+       cmp two ../original.two
+'
+
+test_expect_success 'read-tree' '
+       cd $HERE &&
+       rm -f one dir/two &&
+       tree=`git write-tree` &&
+       git read-tree --reset -u "$tree" &&
+       cmp one original.one &&
+       cmp dir/two original.two &&
+       cd dir &&
+       rm -f two &&
+       git read-tree --reset -u "$tree" &&
+       cmp two ../original.two &&
+       cmp ../one ../original.one
+'
+
+test_expect_success 'no file/rev ambiguity check inside .git' '
+       cd $HERE &&
+       git commit -a -m 1 &&
+       cd $HERE/.git &&
+       git show -s HEAD
+'
+
+test_expect_success 'no file/rev ambiguity check inside a bare repo' '
+       cd $HERE &&
+       git clone -s --bare .git foo.git &&
+       cd foo.git && GIT_DIR=. git show -s HEAD
+'
+
+# This still does not work as it should...
+: test_expect_success 'no file/rev ambiguity check inside a bare repo' '
+       cd $HERE &&
+       git clone -s --bare .git foo.git &&
+       cd foo.git && git show -s HEAD
+'
+
+test_expect_success 'detection should not be fooled by a symlink' '
+       cd $HERE &&
+       rm -fr foo.git &&
+       git clone -s .git another &&
+       ln -s another yetanother &&
+       cd yetanother/.git &&
+       git show -s HEAD
+'
+
+test_done
diff --git a/t/t1100-commit-tree-options.sh b/t/t1100-commit-tree-options.sh
new file mode 100755 (executable)
index 0000000..7f7fc36
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/sh
+#
+# Copyright (C) 2005 Rene Scharfe
+#
+
+test_description='git commit-tree options test
+
+This test checks that git commit-tree can create a specific commit
+object by defining all environment variables that it understands.
+'
+
+. ./test-lib.sh
+
+cat >expected <<EOF
+tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
+author Author Name <author@email> 1117148400 +0000
+committer Committer Name <committer@email> 1117150200 +0000
+
+comment text
+EOF
+
+test_expect_success \
+    'test preparation: write empty tree' \
+    'git write-tree >treeid'
+
+test_expect_success \
+    'construct commit' \
+    'echo comment text |
+     GIT_AUTHOR_NAME="Author Name" \
+     GIT_AUTHOR_EMAIL="author@email" \
+     GIT_AUTHOR_DATE="2005-05-26 23:00" \
+     GIT_COMMITTER_NAME="Committer Name" \
+     GIT_COMMITTER_EMAIL="committer@email" \
+     GIT_COMMITTER_DATE="2005-05-26 23:30" \
+     TZ=GMT git commit-tree `cat treeid` >commitid 2>/dev/null'
+
+test_expect_success \
+    'read commit' \
+    'git cat-file commit `cat commitid` >commit'
+
+test_expect_success \
+    'compare commit' \
+    'diff expected commit'
+
+test_done
diff --git a/t/t1200-tutorial.sh b/t/t1200-tutorial.sh
new file mode 100755 (executable)
index 0000000..991d3c5
--- /dev/null
@@ -0,0 +1,161 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Johannes Schindelin
+#
+
+test_description='A simple turial in the form of a test case'
+
+. ./test-lib.sh
+
+echo "Hello World" > hello
+echo "Silly example" > example
+
+git update-index --add hello example
+
+test_expect_success 'blob' "test blob = \"$(git cat-file -t 557db03)\""
+
+test_expect_success 'blob 557db03' "test \"Hello World\" = \"$(git cat-file blob 557db03)\""
+
+echo "It's a new day for git" >>hello
+cat > diff.expect << EOF
+diff --git a/hello b/hello
+index 557db03..263414f 100644
+--- a/hello
++++ b/hello
+@@ -1 +1,2 @@
+ Hello World
++It's a new day for git
+EOF
+git diff-files -p > diff.output
+test_expect_success 'git diff-files -p' 'cmp diff.expect diff.output'
+git diff > diff.output
+test_expect_success 'git diff' 'cmp diff.expect diff.output'
+
+tree=$(git write-tree 2>/dev/null)
+
+test_expect_success 'tree' "test 8988da15d077d4829fc51d8544c097def6644dbb = $tree"
+
+output="$(echo "Initial commit" | git commit-tree $(git write-tree) 2>&1 > .git/refs/heads/master)"
+
+git diff-index -p HEAD > diff.output
+test_expect_success 'git diff-index -p HEAD' 'cmp diff.expect diff.output'
+
+git diff HEAD > diff.output
+test_expect_success 'git diff HEAD' 'cmp diff.expect diff.output'
+
+#rm hello
+#test_expect_success 'git read-tree --reset HEAD' "git read-tree --reset HEAD ; test \"hello: needs update\" = \"$(git update-index --refresh)\""
+
+cat > whatchanged.expect << EOF
+commit VARIABLE
+Author: VARIABLE
+Date:   VARIABLE
+
+    Initial commit
+
+diff --git a/example b/example
+new file mode 100644
+index 0000000..f24c74a
+--- /dev/null
++++ b/example
+@@ -0,0 +1 @@
++Silly example
+diff --git a/hello b/hello
+new file mode 100644
+index 0000000..557db03
+--- /dev/null
++++ b/hello
+@@ -0,0 +1 @@
++Hello World
+EOF
+
+git whatchanged -p --root | \
+       sed -e "1s/^\(.\{7\}\).\{40\}/\1VARIABLE/" \
+               -e "2,3s/^\(.\{8\}\).*$/\1VARIABLE/" \
+> whatchanged.output
+test_expect_success 'git whatchanged -p --root' 'cmp whatchanged.expect whatchanged.output'
+
+git tag my-first-tag
+test_expect_success 'git tag my-first-tag' 'cmp .git/refs/heads/master .git/refs/tags/my-first-tag'
+
+# TODO: test git-clone
+
+git checkout -b mybranch
+test_expect_success 'git checkout -b mybranch' 'cmp .git/refs/heads/master .git/refs/heads/mybranch'
+
+cat > branch.expect <<EOF
+  master
+* mybranch
+EOF
+
+git branch > branch.output
+test_expect_success 'git branch' 'cmp branch.expect branch.output'
+
+git checkout mybranch
+echo "Work, work, work" >>hello
+git commit -m 'Some work.' -i hello
+
+git checkout master
+
+echo "Play, play, play" >>hello
+echo "Lots of fun" >>example
+git commit -m 'Some fun.' -i hello example
+
+test_expect_failure 'git resolve now fails' '
+       git merge -m "Merge work in mybranch" mybranch
+'
+
+cat > hello << EOF
+Hello World
+It's a new day for git
+Play, play, play
+Work, work, work
+EOF
+
+git commit -m 'Merged "mybranch" changes.' -i hello
+
+test_done
+
+cat > show-branch.expect << EOF
+* [master] Merged "mybranch" changes.
+ ! [mybranch] Some work.
+--
+-  [master] Merged "mybranch" changes.
+*+ [mybranch] Some work.
+EOF
+
+git show-branch --topo-order master mybranch > show-branch.output
+test_expect_success 'git show-branch' 'cmp show-branch.expect show-branch.output'
+
+git checkout mybranch
+
+cat > resolve.expect << EOF
+Updating from VARIABLE to VARIABLE
+ example |    1 +
+ hello   |    1 +
+ 2 files changed, 2 insertions(+), 0 deletions(-)
+EOF
+
+git merge -s "Merge upstream changes." master | \
+       sed -e "1s/[0-9a-f]\{40\}/VARIABLE/g" >resolve.output
+test_expect_success 'git resolve' 'cmp resolve.expect resolve.output'
+
+cat > show-branch2.expect << EOF
+! [master] Merged "mybranch" changes.
+ * [mybranch] Merged "mybranch" changes.
+--
+-- [master] Merged "mybranch" changes.
+EOF
+
+git show-branch --topo-order master mybranch > show-branch2.output
+test_expect_success 'git show-branch' 'cmp show-branch2.expect show-branch2.output'
+
+# TODO: test git fetch
+
+# TODO: test git push
+
+test_expect_success 'git repack' 'git repack'
+test_expect_success 'git prune-packed' 'git prune-packed'
+test_expect_failure '-> only packed objects' 'find -type f .git/objects/[0-9a-f][0-9a-f]'
+
+test_done
diff --git a/t/t1300-repo-config.sh b/t/t1300-repo-config.sh
new file mode 100755 (executable)
index 0000000..1d2bf2c
--- /dev/null
@@ -0,0 +1,619 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Johannes Schindelin
+#
+
+test_description='Test git config in different settings'
+
+. ./test-lib.sh
+
+test -f .git/config && rm .git/config
+
+git config core.penguin "little blue"
+
+cat > expect << EOF
+[core]
+       penguin = little blue
+EOF
+
+test_expect_success 'initial' 'cmp .git/config expect'
+
+git config Core.Movie BadPhysics
+
+cat > expect << EOF
+[core]
+       penguin = little blue
+       Movie = BadPhysics
+EOF
+
+test_expect_success 'mixed case' 'cmp .git/config expect'
+
+git config Cores.WhatEver Second
+
+cat > expect << EOF
+[core]
+       penguin = little blue
+       Movie = BadPhysics
+[Cores]
+       WhatEver = Second
+EOF
+
+test_expect_success 'similar section' 'cmp .git/config expect'
+
+git config CORE.UPPERCASE true
+
+cat > expect << EOF
+[core]
+       penguin = little blue
+       Movie = BadPhysics
+       UPPERCASE = true
+[Cores]
+       WhatEver = Second
+EOF
+
+test_expect_success 'similar section' 'cmp .git/config expect'
+
+test_expect_success 'replace with non-match' \
+       'git config core.penguin kingpin !blue'
+
+test_expect_success 'replace with non-match (actually matching)' \
+       'git config core.penguin "very blue" !kingpin'
+
+cat > expect << EOF
+[core]
+       penguin = very blue
+       Movie = BadPhysics
+       UPPERCASE = true
+       penguin = kingpin
+[Cores]
+       WhatEver = Second
+EOF
+
+test_expect_success 'non-match result' 'cmp .git/config expect'
+
+cat > .git/config << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+               haha   ="beta" # last silly comment
+haha = hello
+       haha = bello
+[nextSection] noNewline = ouch
+EOF
+
+cp .git/config .git/config2
+
+test_expect_success 'multiple unset' \
+       'git config --unset-all beta.haha'
+
+cat > expect << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+[nextSection] noNewline = ouch
+EOF
+
+test_expect_success 'multiple unset is correct' 'cmp .git/config expect'
+
+mv .git/config2 .git/config
+
+test_expect_success '--replace-all' \
+       'git config --replace-all beta.haha gamma'
+
+cat > expect << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+       haha = gamma
+[nextSection] noNewline = ouch
+EOF
+
+test_expect_success 'all replaced' 'cmp .git/config expect'
+
+git config beta.haha alpha
+
+cat > expect << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+       haha = alpha
+[nextSection] noNewline = ouch
+EOF
+
+test_expect_success 'really mean test' 'cmp .git/config expect'
+
+git config nextsection.nonewline wow
+
+cat > expect << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+       haha = alpha
+[nextSection]
+       nonewline = wow
+EOF
+
+test_expect_success 'really really mean test' 'cmp .git/config expect'
+
+test_expect_success 'get value' 'test alpha = $(git config beta.haha)'
+git config --unset beta.haha
+
+cat > expect << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+[nextSection]
+       nonewline = wow
+EOF
+
+test_expect_success 'unset' 'cmp .git/config expect'
+
+git config nextsection.NoNewLine "wow2 for me" "for me$"
+
+cat > expect << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+[nextSection]
+       nonewline = wow
+       NoNewLine = wow2 for me
+EOF
+
+test_expect_success 'multivar' 'cmp .git/config expect'
+
+test_expect_success 'non-match' \
+       'git config --get nextsection.nonewline !for'
+
+test_expect_success 'non-match value' \
+       'test wow = $(git config --get nextsection.nonewline !for)'
+
+test_expect_failure 'ambiguous get' \
+       'git config --get nextsection.nonewline'
+
+test_expect_success 'get multivar' \
+       'git config --get-all nextsection.nonewline'
+
+git config nextsection.nonewline "wow3" "wow$"
+
+cat > expect << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+[nextSection]
+       nonewline = wow3
+       NoNewLine = wow2 for me
+EOF
+
+test_expect_success 'multivar replace' 'cmp .git/config expect'
+
+test_expect_failure 'ambiguous value' 'git config nextsection.nonewline'
+
+test_expect_failure 'ambiguous unset' \
+       'git config --unset nextsection.nonewline'
+
+test_expect_failure 'invalid unset' \
+       'git config --unset somesection.nonewline'
+
+git config --unset nextsection.nonewline "wow3$"
+
+cat > expect << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+[nextSection]
+       NoNewLine = wow2 for me
+EOF
+
+test_expect_success 'multivar unset' 'cmp .git/config expect'
+
+test_expect_failure 'invalid key' 'git config inval.2key blabla'
+
+test_expect_success 'correct key' 'git config 123456.a123 987'
+
+test_expect_success 'hierarchical section' \
+       'git config Version.1.2.3eX.Alpha beta'
+
+cat > expect << EOF
+[beta] ; silly comment # another comment
+noIndent= sillyValue ; 'nother silly comment
+
+# empty line
+               ; comment
+[nextSection]
+       NoNewLine = wow2 for me
+[123456]
+       a123 = 987
+[Version "1.2.3eX"]
+       Alpha = beta
+EOF
+
+test_expect_success 'hierarchical section value' 'cmp .git/config expect'
+
+cat > expect << EOF
+beta.noindent=sillyValue
+nextsection.nonewline=wow2 for me
+123456.a123=987
+version.1.2.3eX.alpha=beta
+EOF
+
+test_expect_success 'working --list' \
+       'git config --list > output && cmp output expect'
+
+cat > expect << EOF
+beta.noindent sillyValue
+nextsection.nonewline wow2 for me
+EOF
+
+test_expect_success '--get-regexp' \
+       'git config --get-regexp in > output && cmp output expect'
+
+git config --add nextsection.nonewline "wow4 for you"
+
+cat > expect << EOF
+wow2 for me
+wow4 for you
+EOF
+
+test_expect_success '--add' \
+       'git config --get-all nextsection.nonewline > output && cmp output expect'
+
+cat > .git/config << EOF
+[novalue]
+       variable
+EOF
+
+test_expect_success 'get variable with no value' \
+       'git config --get novalue.variable ^$'
+
+echo novalue.variable > expect
+
+test_expect_success 'get-regexp variable with no value' \
+       'git config --get-regexp novalue > output &&
+        cmp output expect'
+
+git config > output 2>&1
+
+test_expect_success 'no arguments, but no crash' \
+       "test $? = 129 && grep usage output"
+
+cat > .git/config << EOF
+[a.b]
+       c = d
+EOF
+
+git config a.x y
+
+cat > expect << EOF
+[a.b]
+       c = d
+[a]
+       x = y
+EOF
+
+test_expect_success 'new section is partial match of another' 'cmp .git/config expect'
+
+git config b.x y
+git config a.b c
+
+cat > expect << EOF
+[a.b]
+       c = d
+[a]
+       x = y
+       b = c
+[b]
+       x = y
+EOF
+
+test_expect_success 'new variable inserts into proper section' 'cmp .git/config expect'
+
+test_expect_success 'alternative GIT_CONFIG (non-existing file should fail)' \
+       'git config --file non-existing-config -l; test $? != 0'
+
+cat > other-config << EOF
+[ein]
+       bahn = strasse
+EOF
+
+cat > expect << EOF
+ein.bahn=strasse
+EOF
+
+GIT_CONFIG=other-config git config -l > output
+
+test_expect_success 'alternative GIT_CONFIG' 'cmp output expect'
+
+test_expect_success 'alternative GIT_CONFIG (--file)' \
+       'git config --file other-config -l > output && cmp output expect'
+
+GIT_CONFIG=other-config git config anwohner.park ausweis
+
+cat > expect << EOF
+[ein]
+       bahn = strasse
+[anwohner]
+       park = ausweis
+EOF
+
+test_expect_success '--set in alternative GIT_CONFIG' 'cmp other-config expect'
+
+cat > .git/config << EOF
+# Hallo
+       #Bello
+[branch "eins"]
+       x = 1
+[branch.eins]
+       y = 1
+       [branch "1 234 blabl/a"]
+weird
+EOF
+
+test_expect_success "rename section" \
+       "git config --rename-section branch.eins branch.zwei"
+
+cat > expect << EOF
+# Hallo
+       #Bello
+[branch "zwei"]
+       x = 1
+[branch "zwei"]
+       y = 1
+       [branch "1 234 blabl/a"]
+weird
+EOF
+
+test_expect_success "rename succeeded" "git diff expect .git/config"
+
+test_expect_failure "rename non-existing section" \
+       'git config --rename-section branch."world domination" branch.drei'
+
+test_expect_success "rename succeeded" "git diff expect .git/config"
+
+test_expect_success "rename another section" \
+       'git config --rename-section branch."1 234 blabl/a" branch.drei'
+
+cat > expect << EOF
+# Hallo
+       #Bello
+[branch "zwei"]
+       x = 1
+[branch "zwei"]
+       y = 1
+[branch "drei"]
+weird
+EOF
+
+test_expect_success "rename succeeded" "git diff expect .git/config"
+
+cat >> .git/config << EOF
+  [branch "zwei"] a = 1 [branch "vier"]
+EOF
+
+test_expect_success "remove section" "git config --remove-section branch.zwei"
+
+cat > expect << EOF
+# Hallo
+       #Bello
+[branch "drei"]
+weird
+EOF
+
+test_expect_success "section was removed properly" \
+       "git diff -u expect .git/config"
+
+rm .git/config
+
+cat > expect << EOF
+[gitcvs]
+       enabled = true
+       dbname = %Ggitcvs2.%a.%m.sqlite
+[gitcvs "ext"]
+       dbname = %Ggitcvs1.%a.%m.sqlite
+EOF
+
+test_expect_success 'section ending' '
+
+       git config gitcvs.enabled true &&
+       git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
+       git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
+       cmp .git/config expect
+
+'
+
+test_expect_success numbers '
+
+       git config kilo.gram 1k &&
+       git config mega.ton 1m &&
+       k=$(git config --int --get kilo.gram) &&
+       test z1024 = "z$k" &&
+       m=$(git config --int --get mega.ton) &&
+       test z1048576 = "z$m"
+'
+
+cat > expect << EOF
+true
+false
+true
+false
+true
+false
+true
+false
+EOF
+
+test_expect_success bool '
+
+       git config bool.true1 01 &&
+       git config bool.true2 -1 &&
+       git config bool.true3 YeS &&
+       git config bool.true4 true &&
+       git config bool.false1 000 &&
+       git config bool.false2 "" &&
+       git config bool.false3 nO &&
+       git config bool.false4 FALSE &&
+       rm -f result &&
+       for i in 1 2 3 4
+       do
+           git config --bool --get bool.true$i >>result
+           git config --bool --get bool.false$i >>result
+        done &&
+       cmp expect result'
+
+test_expect_failure 'invalid bool (--get)' '
+
+       git config bool.nobool foobar &&
+       git config --bool --get bool.nobool'
+
+test_expect_failure 'invalid bool (set)' '
+
+       git config --bool bool.nobool foobar'
+
+rm .git/config
+
+cat > expect <<\EOF
+[bool]
+       true1 = true
+       true2 = true
+       true3 = true
+       true4 = true
+       false1 = false
+       false2 = false
+       false3 = false
+       false4 = false
+EOF
+
+test_expect_success 'set --bool' '
+
+       git config --bool bool.true1 01 &&
+       git config --bool bool.true2 -1 &&
+       git config --bool bool.true3 YeS &&
+       git config --bool bool.true4 true &&
+       git config --bool bool.false1 000 &&
+       git config --bool bool.false2 "" &&
+       git config --bool bool.false3 nO &&
+       git config --bool bool.false4 FALSE &&
+       cmp expect .git/config'
+
+rm .git/config
+
+cat > expect <<\EOF
+[int]
+       val1 = 1
+       val2 = -1
+       val3 = 5242880
+EOF
+
+test_expect_success 'set --int' '
+
+       git config --int int.val1 01 &&
+       git config --int int.val2 -1 &&
+       git config --int int.val3 5m &&
+       cmp expect .git/config'
+
+rm .git/config
+
+git config quote.leading " test"
+git config quote.ending "test "
+git config quote.semicolon "test;test"
+git config quote.hash "test#test"
+
+cat > expect << EOF
+[quote]
+       leading = " test"
+       ending = "test "
+       semicolon = "test;test"
+       hash = "test#test"
+EOF
+
+test_expect_success 'quoting' 'cmp .git/config expect'
+
+test_expect_failure 'key with newline' 'git config key.with\\\
+newline 123'
+
+test_expect_success 'value with newline' 'git config key.sub value.with\\\
+newline'
+
+cat > .git/config <<\EOF
+[section]
+       ; comment \
+       continued = cont\
+inued
+       noncont   = not continued ; \
+       quotecont = "cont;\
+inued"
+EOF
+
+cat > expect <<\EOF
+section.continued=continued
+section.noncont=not continued
+section.quotecont=cont;inued
+EOF
+
+git config --list > result
+
+test_expect_success 'value continued on next line' 'cmp result expect'
+
+cat > .git/config <<\EOF
+[section "sub=section"]
+       val1 = foo=bar
+       val2 = foo\nbar
+       val3 = \n\n
+       val4 =
+       val5
+EOF
+
+cat > expect <<\EOF
+section.sub=section.val1
+foo=barQsection.sub=section.val2
+foo
+barQsection.sub=section.val3
+
+
+Qsection.sub=section.val4
+Qsection.sub=section.val5Q
+EOF
+
+git config --null --list | tr '[\000]' 'Q' > result
+echo >>result
+
+test_expect_success '--null --list' 'cmp result expect'
+
+git config --null --get-regexp 'val[0-9]' | tr '[\000]' 'Q' > result
+echo >>result
+
+test_expect_success '--null --get-regexp' 'cmp result expect'
+
+test_expect_success 'symlinked configuration' '
+
+       ln -s notyet myconfig &&
+       GIT_CONFIG=myconfig git config test.frotz nitfol &&
+       test -h myconfig &&
+       test -f notyet &&
+       test "z$(GIT_CONFIG=notyet git config test.frotz)" = znitfol &&
+       GIT_CONFIG=myconfig git config test.xyzzy rezrov &&
+       test -h myconfig &&
+       test -f notyet &&
+       test "z$(GIT_CONFIG=notyet git config test.frotz)" = znitfol &&
+       test "z$(GIT_CONFIG=notyet git config test.xyzzy)" = zrezrov
+
+'
+
+test_done
diff --git a/t/t1301-shared-repo.sh b/t/t1301-shared-repo.sh
new file mode 100755 (executable)
index 0000000..6bfe19a
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes Schindelin
+#
+
+test_description='Test shared repository initialization'
+
+. ./test-lib.sh
+
+test_expect_success 'shared=all' '
+       mkdir sub &&
+       cd sub &&
+       git init --shared=all &&
+       test 2 = $(git config core.sharedrepository)
+'
+
+test_expect_success 'update-server-info honors core.sharedRepository' '
+       : > a1 &&
+       git add a1 &&
+       test_tick &&
+       git commit -m a1 &&
+       umask 0277 &&
+       git update-server-info &&
+       actual="$(ls -l .git/info/refs)" &&
+       case "$actual" in
+       -r--r--r--*)
+               : happy
+               ;;
+       *)
+               echo Oops, .git/info/refs is not 0444
+               false
+               ;;
+       esac
+'
+
+test_done
diff --git a/t/t1302-repo-version.sh b/t/t1302-repo-version.sh
new file mode 100755 (executable)
index 0000000..37fc1c8
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Nguyễn Thái Ngọc Duy
+#
+
+test_description='Test repository version check'
+
+. ./test-lib.sh
+
+cat >test.patch <<EOF
+diff --git a/test.txt b/test.txt
+new file mode 100644
+--- /dev/null
++++ b/test.txt
+@@ -0,0 +1 @@
++123
+EOF
+
+test_create_repo "test"
+test_create_repo "test2"
+
+GIT_CONFIG=test2/.git/config git config core.repositoryformatversion 99 || exit 1
+
+test_expect_success 'gitdir selection on normal repos' '
+       (test "$(git config core.repositoryformatversion)" = 0 &&
+       cd test &&
+       test "$(git config core.repositoryformatversion)" = 0)'
+
+# Make sure it would stop at test2, not trash
+test_expect_success 'gitdir selection on unsupported repo' '
+       (cd test2 &&
+       test "$(git config core.repositoryformatversion)" = 99)'
+
+test_expect_success 'gitdir not required mode' '
+       (git apply --stat test.patch &&
+       cd test && git apply --stat ../test.patch &&
+       cd ../test2 && git apply --stat ../test.patch)'
+
+test_expect_success 'gitdir required mode on normal repos' '
+       (git apply --check --index test.patch &&
+       cd test && git apply --check --index ../test.patch)'
+
+test_expect_failure 'gitdir required mode on unsupported repo' '
+       (cd test2 && git apply --check --index ../test.patch)'
+
+test_done
diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh
new file mode 100755 (executable)
index 0000000..a90824b
--- /dev/null
@@ -0,0 +1,232 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Shawn Pearce
+#
+
+test_description='Test git update-ref and basic ref logging'
+. ./test-lib.sh
+
+Z=0000000000000000000000000000000000000000
+A=1111111111111111111111111111111111111111
+B=2222222222222222222222222222222222222222
+C=3333333333333333333333333333333333333333
+D=4444444444444444444444444444444444444444
+E=5555555555555555555555555555555555555555
+F=6666666666666666666666666666666666666666
+m=refs/heads/master
+n_dir=refs/heads/gu
+n=$n_dir/fixes
+
+test_expect_success \
+       "create $m" \
+       "git update-ref $m $A &&
+        test $A"' = $(cat .git/'"$m"')'
+test_expect_success \
+       "create $m" \
+       "git update-ref $m $B $A &&
+        test $B"' = $(cat .git/'"$m"')'
+rm -f .git/$m
+
+test_expect_success \
+       "fail to create $n" \
+       "touch .git/$n_dir
+        git update-ref $n $A >out 2>err"'
+        test $? != 0'
+rm -f .git/$n_dir out err
+
+test_expect_success \
+       "create $m (by HEAD)" \
+       "git update-ref HEAD $A &&
+        test $A"' = $(cat .git/'"$m"')'
+test_expect_success \
+       "create $m (by HEAD)" \
+       "git update-ref HEAD $B $A &&
+        test $B"' = $(cat .git/'"$m"')'
+rm -f .git/$m
+
+test_expect_failure \
+       '(not) create HEAD with old sha1' \
+       "git update-ref HEAD $A $B"
+test_expect_failure \
+       "(not) prior created .git/$m" \
+       "test -f .git/$m"
+rm -f .git/$m
+
+test_expect_success \
+       "create HEAD" \
+       "git update-ref HEAD $A"
+test_expect_failure \
+       '(not) change HEAD with wrong SHA1' \
+       "git update-ref HEAD $B $Z"
+test_expect_failure \
+       "(not) changed .git/$m" \
+       "test $B"' = $(cat .git/'"$m"')'
+rm -f .git/$m
+
+: a repository with working tree always has reflog these days...
+: >.git/logs/refs/heads/master
+test_expect_success \
+       "create $m (logged by touch)" \
+       'GIT_COMMITTER_DATE="2005-05-26 23:30" \
+        git update-ref HEAD '"$A"' -m "Initial Creation" &&
+        test '"$A"' = $(cat .git/'"$m"')'
+test_expect_success \
+       "update $m (logged by touch)" \
+       'GIT_COMMITTER_DATE="2005-05-26 23:31" \
+        git update-ref HEAD'" $B $A "'-m "Switch" &&
+        test '"$B"' = $(cat .git/'"$m"')'
+test_expect_success \
+       "set $m (logged by touch)" \
+       'GIT_COMMITTER_DATE="2005-05-26 23:41" \
+        git update-ref HEAD'" $A &&
+        test $A"' = $(cat .git/'"$m"')'
+
+cat >expect <<EOF
+$Z $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000      Initial Creation
+$A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150260 +0000      Switch
+$B $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150860 +0000
+EOF
+test_expect_success \
+       "verifying $m's log" \
+       "diff expect .git/logs/$m"
+rm -rf .git/$m .git/logs expect
+
+test_expect_success \
+       'enable core.logAllRefUpdates' \
+       'git config core.logAllRefUpdates true &&
+        test true = $(git config --bool --get core.logAllRefUpdates)'
+
+test_expect_success \
+       "create $m (logged by config)" \
+       'GIT_COMMITTER_DATE="2005-05-26 23:32" \
+        git update-ref HEAD'" $A "'-m "Initial Creation" &&
+        test '"$A"' = $(cat .git/'"$m"')'
+test_expect_success \
+       "update $m (logged by config)" \
+       'GIT_COMMITTER_DATE="2005-05-26 23:33" \
+        git update-ref HEAD'" $B $A "'-m "Switch" &&
+        test '"$B"' = $(cat .git/'"$m"')'
+test_expect_success \
+       "set $m (logged by config)" \
+       'GIT_COMMITTER_DATE="2005-05-26 23:43" \
+        git update-ref HEAD '"$A &&
+        test $A"' = $(cat .git/'"$m"')'
+
+cat >expect <<EOF
+$Z $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150320 +0000      Initial Creation
+$A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150380 +0000      Switch
+$B $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150980 +0000
+EOF
+test_expect_success \
+       "verifying $m's log" \
+       'diff expect .git/logs/$m'
+rm -f .git/$m .git/logs/$m expect
+
+git update-ref $m $D
+cat >.git/logs/$m <<EOF
+$C $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150320 -0500
+$A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150380 -0500
+$F $Z $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150680 -0500
+$Z $E $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150980 -0500
+EOF
+
+ed="Thu, 26 May 2005 18:32:00 -0500"
+gd="Thu, 26 May 2005 18:33:00 -0500"
+ld="Thu, 26 May 2005 18:43:00 -0500"
+test_expect_success \
+       'Query "master@{May 25 2005}" (before history)' \
+       'rm -f o e
+        git rev-parse --verify "master@{May 25 2005}" >o 2>e &&
+        test '"$C"' = $(cat o) &&
+        test "warning: Log for '\'master\'' only goes back to $ed." = "$(cat e)"'
+test_expect_success \
+       "Query master@{2005-05-25} (before history)" \
+       'rm -f o e
+        git rev-parse --verify master@{2005-05-25} >o 2>e &&
+        test '"$C"' = $(cat o) &&
+        echo test "warning: Log for '\'master\'' only goes back to $ed." = "$(cat e)"'
+test_expect_success \
+       'Query "master@{May 26 2005 23:31:59}" (1 second before history)' \
+       'rm -f o e
+        git rev-parse --verify "master@{May 26 2005 23:31:59}" >o 2>e &&
+        test '"$C"' = $(cat o) &&
+        test "warning: Log for '\''master'\'' only goes back to $ed." = "$(cat e)"'
+test_expect_success \
+       'Query "master@{May 26 2005 23:32:00}" (exactly history start)' \
+       'rm -f o e
+        git rev-parse --verify "master@{May 26 2005 23:32:00}" >o 2>e &&
+        test '"$A"' = $(cat o) &&
+        test "" = "$(cat e)"'
+test_expect_success \
+       'Query "master@{2005-05-26 23:33:01}" (middle of history with gap)' \
+       'rm -f o e
+        git rev-parse --verify "master@{2005-05-26 23:33:01}" >o 2>e &&
+        test '"$B"' = $(cat o) &&
+        test "warning: Log .git/logs/'"$m has gap after $gd"'." = "$(cat e)"'
+test_expect_success \
+       'Query "master@{2005-05-26 23:38:00}" (middle of history)' \
+       'rm -f o e
+        git rev-parse --verify "master@{2005-05-26 23:38:00}" >o 2>e &&
+        test '"$Z"' = $(cat o) &&
+        test "" = "$(cat e)"'
+test_expect_success \
+       'Query "master@{2005-05-26 23:43:00}" (exact end of history)' \
+       'rm -f o e
+        git rev-parse --verify "master@{2005-05-26 23:43:00}" >o 2>e &&
+        test '"$E"' = $(cat o) &&
+        test "" = "$(cat e)"'
+test_expect_success \
+       'Query "master@{2005-05-28}" (past end of history)' \
+       'rm -f o e
+        git rev-parse --verify "master@{2005-05-28}" >o 2>e &&
+        test '"$D"' = $(cat o) &&
+        test "warning: Log .git/logs/'"$m unexpectedly ended on $ld"'." = "$(cat e)"'
+
+
+rm -f .git/$m .git/logs/$m expect
+
+test_expect_success \
+    'creating initial files' \
+    'echo TEST >F &&
+     git add F &&
+        GIT_AUTHOR_DATE="2005-05-26 23:30" \
+        GIT_COMMITTER_DATE="2005-05-26 23:30" git-commit -m add -a &&
+        h_TEST=$(git rev-parse --verify HEAD)
+        echo The other day this did not work. >M &&
+        echo And then Bob told me how to fix it. >>M &&
+        echo OTHER >F &&
+        GIT_AUTHOR_DATE="2005-05-26 23:41" \
+        GIT_COMMITTER_DATE="2005-05-26 23:41" git-commit -F M -a &&
+        h_OTHER=$(git rev-parse --verify HEAD) &&
+        GIT_AUTHOR_DATE="2005-05-26 23:44" \
+        GIT_COMMITTER_DATE="2005-05-26 23:44" git-commit --amend &&
+        h_FIXED=$(git rev-parse --verify HEAD) &&
+        echo Merged initial commit and a later commit. >M &&
+        echo $h_TEST >.git/MERGE_HEAD &&
+        GIT_AUTHOR_DATE="2005-05-26 23:45" \
+        GIT_COMMITTER_DATE="2005-05-26 23:45" git-commit -F M &&
+        h_MERGED=$(git rev-parse --verify HEAD) &&
+        rm -f M'
+
+cat >expect <<EOF
+$Z $h_TEST $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 commit (initial): add
+$h_TEST $h_OTHER $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150860 +0000   commit: The other day this did not work.
+$h_OTHER $h_FIXED $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117151040 +0000  commit (amend): The other day this did not work.
+$h_FIXED $h_MERGED $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117151100 +0000 commit (merge): Merged initial commit and a later commit.
+EOF
+test_expect_success \
+       'git-commit logged updates' \
+       "diff expect .git/logs/$m"
+unset h_TEST h_OTHER h_FIXED h_MERGED
+
+test_expect_success \
+       'git cat-file blob master:F (expect OTHER)' \
+       'test OTHER = $(git cat-file blob master:F)'
+test_expect_success \
+       'git cat-file blob master@{2005-05-26 23:30}:F (expect TEST)' \
+       'test TEST = $(git cat-file blob "master@{2005-05-26 23:30}:F")'
+test_expect_success \
+       'git cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)' \
+       'test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")'
+
+test_done
diff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh
new file mode 100755 (executable)
index 0000000..f959aae
--- /dev/null
@@ -0,0 +1,196 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Junio C Hamano
+#
+
+test_description='Test prune and reflog expiration'
+. ./test-lib.sh
+
+check_have () {
+       gaah= &&
+       for N in "$@"
+       do
+               eval "o=\$$N" && git cat-file -t $o || {
+                       echo Gaah $N
+                       gaah=$N
+                       break
+               }
+       done &&
+       test -z "$gaah"
+}
+
+check_fsck () {
+       output=$(git fsck --full)
+       case "$1" in
+       '')
+               test -z "$output" ;;
+       *)
+               echo "$output" | grep "$1" ;;
+       esac
+}
+
+corrupt () {
+       aa=${1%??????????????????????????????????????} zz=${1#??}
+       mv .git/objects/$aa/$zz .git/$aa$zz
+}
+
+recover () {
+       aa=${1%??????????????????????????????????????} zz=${1#??}
+       mkdir -p .git/objects/$aa
+       mv .git/$aa$zz .git/objects/$aa/$zz
+}
+
+check_dont_have () {
+       gaah= &&
+       for N in "$@"
+       do
+               eval "o=\$$N"
+               git cat-file -t $o && {
+                       echo Gaah $N
+                       gaah=$N
+                       break
+               }
+       done
+       test -z "$gaah"
+}
+
+test_expect_success setup '
+       mkdir -p A/B &&
+       echo rat >C &&
+       echo ox >A/D &&
+       echo tiger >A/B/E &&
+       git add . &&
+
+       test_tick && git commit -m rabbit &&
+       H=`git rev-parse --verify HEAD` &&
+       A=`git rev-parse --verify HEAD:A` &&
+       B=`git rev-parse --verify HEAD:A/B` &&
+       C=`git rev-parse --verify HEAD:C` &&
+       D=`git rev-parse --verify HEAD:A/D` &&
+       E=`git rev-parse --verify HEAD:A/B/E` &&
+       check_fsck &&
+
+       chmod +x C &&
+       ( test "`git config --bool core.filemode`" != false ||
+         echo executable >>C ) &&
+       git add C &&
+       test_tick && git commit -m dragon &&
+       L=`git rev-parse --verify HEAD` &&
+       check_fsck &&
+
+       rm -f C A/B/E &&
+       echo snake >F &&
+       echo horse >A/G &&
+       git add F A/G &&
+       test_tick && git commit -a -m sheep &&
+       F=`git rev-parse --verify HEAD:F` &&
+       G=`git rev-parse --verify HEAD:A/G` &&
+       I=`git rev-parse --verify HEAD:A` &&
+       J=`git rev-parse --verify HEAD` &&
+       check_fsck &&
+
+       rm -f A/G &&
+       test_tick && git commit -a -m monkey &&
+       K=`git rev-parse --verify HEAD` &&
+       check_fsck &&
+
+       check_have A B C D E F G H I J K L &&
+
+       git prune &&
+
+       check_have A B C D E F G H I J K L &&
+
+       check_fsck &&
+
+       loglen=$(wc -l <.git/logs/refs/heads/master) &&
+       test $loglen = 4
+'
+
+test_expect_success rewind '
+       test_tick && git reset --hard HEAD~2 &&
+       test -f C &&
+       test -f A/B/E &&
+       ! test -f F &&
+       ! test -f A/G &&
+
+       check_have A B C D E F G H I J K L &&
+
+       git prune &&
+
+       check_have A B C D E F G H I J K L &&
+
+       loglen=$(wc -l <.git/logs/refs/heads/master) &&
+       test $loglen = 5
+'
+
+test_expect_success 'corrupt and check' '
+
+       corrupt $F &&
+       check_fsck "missing blob $F"
+
+'
+
+test_expect_success 'reflog expire --dry-run should not touch reflog' '
+
+       git reflog expire --dry-run \
+               --expire=$(($test_tick - 10000)) \
+               --expire-unreachable=$(($test_tick - 10000)) \
+               --stale-fix \
+               --all &&
+
+       loglen=$(wc -l <.git/logs/refs/heads/master) &&
+       test $loglen = 5 &&
+
+       check_fsck "missing blob $F"
+'
+
+test_expect_success 'reflog expire' '
+
+       git reflog expire --verbose \
+               --expire=$(($test_tick - 10000)) \
+               --expire-unreachable=$(($test_tick - 10000)) \
+               --stale-fix \
+               --all &&
+
+       loglen=$(wc -l <.git/logs/refs/heads/master) &&
+       test $loglen = 2 &&
+
+       check_fsck "dangling commit $K"
+'
+
+test_expect_success 'prune and fsck' '
+
+       git prune &&
+       check_fsck &&
+
+       check_have A B C D E H L &&
+       check_dont_have F G I J K
+
+'
+
+test_expect_success 'recover and check' '
+
+       recover $F &&
+       check_fsck "dangling blob $F"
+
+'
+
+test_expect_success 'prune --expire' '
+
+       before=$(git count-objects | sed "s/ .*//") &&
+       BLOB=$(echo aleph | git hash-object -w --stdin) &&
+       BLOB_FILE=.git/objects/$(echo $BLOB | sed "s/^../&\//") &&
+       test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
+       test -f $BLOB_FILE &&
+       git reset --hard &&
+       git prune --expire=1.hour.ago &&
+       test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
+       test -f $BLOB_FILE &&
+       test-chmtime -86500 $BLOB_FILE &&
+       git prune --expire 1.day &&
+       test $before = $(git count-objects | sed "s/ .*//") &&
+       ! test -f $BLOB_FILE
+
+'
+
+test_done
diff --git a/t/t1420-lost-found.sh b/t/t1420-lost-found.sh
new file mode 100755 (executable)
index 0000000..dc9e402
--- /dev/null
@@ -0,0 +1,35 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes E. Schindelin
+#
+
+test_description='Test fsck --lost-found'
+. ./test-lib.sh
+
+test_expect_success setup '
+       git config core.logAllRefUpdates 0 &&
+       : > file1 &&
+       git add file1 &&
+       test_tick &&
+       git commit -m initial &&
+       echo 1 > file1 &&
+       echo 2 > file2 &&
+       git add file1 file2 &&
+       test_tick &&
+       git commit -m second &&
+       echo 3 > file3 &&
+       git add file3
+'
+
+test_expect_success 'lost and found something' '
+       git rev-parse HEAD > lost-commit &&
+       git rev-parse :file3 > lost-other &&
+       test_tick &&
+       git reset --hard HEAD^ &&
+       git fsck --lost-found &&
+       test 2 = $(ls .git/lost-found/*/* | wc -l) &&
+       test -f .git/lost-found/commit/$(cat lost-commit) &&
+       test -f .git/lost-found/other/$(cat lost-other)
+'
+
+test_done
diff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh
new file mode 100755 (executable)
index 0000000..e474b3f
--- /dev/null
@@ -0,0 +1,79 @@
+#!/bin/sh
+
+test_description='test git rev-parse'
+. ./test-lib.sh
+
+test_rev_parse() {
+       name=$1
+       shift
+
+       test_expect_success "$name: is-bare-repository" \
+       "test '$1' = \"\$(git rev-parse --is-bare-repository)\""
+       shift
+       [ $# -eq 0 ] && return
+
+       test_expect_success "$name: is-inside-git-dir" \
+       "test '$1' = \"\$(git rev-parse --is-inside-git-dir)\""
+       shift
+       [ $# -eq 0 ] && return
+
+       test_expect_success "$name: is-inside-work-tree" \
+       "test '$1' = \"\$(git rev-parse --is-inside-work-tree)\""
+       shift
+       [ $# -eq 0 ] && return
+
+       test_expect_success "$name: prefix" \
+       "test '$1' = \"\$(git rev-parse --show-prefix)\""
+       shift
+       [ $# -eq 0 ] && return
+}
+
+# label is-bare is-inside-git is-inside-work prefix
+
+test_rev_parse toplevel false false true ''
+
+cd .git || exit 1
+test_rev_parse .git/ true true false ''
+cd objects || exit 1
+test_rev_parse .git/objects/ true true false ''
+cd ../.. || exit 1
+
+mkdir -p sub/dir || exit 1
+cd sub/dir || exit 1
+test_rev_parse subdirectory false false true sub/dir/
+cd ../.. || exit 1
+
+git config core.bare true
+test_rev_parse 'core.bare = true' true false false
+
+git config --unset core.bare
+test_rev_parse 'core.bare undefined' false false true
+
+mkdir work || exit 1
+cd work || exit 1
+export GIT_DIR=../.git
+export GIT_CONFIG="$(pwd)"/../.git/config
+
+git config core.bare false
+test_rev_parse 'GIT_DIR=../.git, core.bare = false' false false true ''
+
+git config core.bare true
+test_rev_parse 'GIT_DIR=../.git, core.bare = true' true false false ''
+
+git config --unset core.bare
+test_rev_parse 'GIT_DIR=../.git, core.bare undefined' false false true ''
+
+mv ../.git ../repo.git || exit 1
+export GIT_DIR=../repo.git
+export GIT_CONFIG="$(pwd)"/../repo.git/config
+
+git config core.bare false
+test_rev_parse 'GIT_DIR=../repo.git, core.bare = false' false false true ''
+
+git config core.bare true
+test_rev_parse 'GIT_DIR=../repo.git, core.bare = true' true false false ''
+
+git config --unset core.bare
+test_rev_parse 'GIT_DIR=../repo.git, core.bare undefined' false false true ''
+
+test_done
diff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh
new file mode 100755 (executable)
index 0000000..7ee3820
--- /dev/null
@@ -0,0 +1,115 @@
+#!/bin/sh
+
+test_description='test separate work tree'
+. ./test-lib.sh
+
+test_rev_parse() {
+       name=$1
+       shift
+
+       test_expect_success "$name: is-bare-repository" \
+       "test '$1' = \"\$(git rev-parse --is-bare-repository)\""
+       shift
+       [ $# -eq 0 ] && return
+
+       test_expect_success "$name: is-inside-git-dir" \
+       "test '$1' = \"\$(git rev-parse --is-inside-git-dir)\""
+       shift
+       [ $# -eq 0 ] && return
+
+       test_expect_success "$name: is-inside-work-tree" \
+       "test '$1' = \"\$(git rev-parse --is-inside-work-tree)\""
+       shift
+       [ $# -eq 0 ] && return
+
+       test_expect_success "$name: prefix" \
+       "test '$1' = \"\$(git rev-parse --show-prefix)\""
+       shift
+       [ $# -eq 0 ] && return
+}
+
+mkdir -p work/sub/dir || exit 1
+mv .git repo.git || exit 1
+
+say "core.worktree = relative path"
+export GIT_DIR=repo.git
+export GIT_CONFIG="$(pwd)"/$GIT_DIR/config
+unset GIT_WORK_TREE
+git config core.worktree ../work
+test_rev_parse 'outside'      false false false
+cd work || exit 1
+export GIT_DIR=../repo.git
+export GIT_CONFIG="$(pwd)"/$GIT_DIR/config
+test_rev_parse 'inside'       false false true ''
+cd sub/dir || exit 1
+export GIT_DIR=../../../repo.git
+export GIT_CONFIG="$(pwd)"/$GIT_DIR/config
+test_rev_parse 'subdirectory' false false true sub/dir/
+cd ../../.. || exit 1
+
+say "core.worktree = absolute path"
+export GIT_DIR=$(pwd)/repo.git
+export GIT_CONFIG=$GIT_DIR/config
+git config core.worktree "$(pwd)/work"
+test_rev_parse 'outside'      false false false
+cd work || exit 1
+test_rev_parse 'inside'       false false true ''
+cd sub/dir || exit 1
+test_rev_parse 'subdirectory' false false true sub/dir/
+cd ../../.. || exit 1
+
+say "GIT_WORK_TREE=relative path (override core.worktree)"
+export GIT_DIR=$(pwd)/repo.git
+export GIT_CONFIG=$GIT_DIR/config
+git config core.worktree non-existent
+export GIT_WORK_TREE=work
+test_rev_parse 'outside'      false false false
+cd work || exit 1
+export GIT_WORK_TREE=.
+test_rev_parse 'inside'       false false true ''
+cd sub/dir || exit 1
+export GIT_WORK_TREE=../..
+test_rev_parse 'subdirectory' false false true sub/dir/
+cd ../../.. || exit 1
+
+mv work repo.git/work
+
+say "GIT_WORK_TREE=absolute path, work tree below git dir"
+export GIT_DIR=$(pwd)/repo.git
+export GIT_CONFIG=$GIT_DIR/config
+export GIT_WORK_TREE=$(pwd)/repo.git/work
+test_rev_parse 'outside'              false false false
+cd repo.git || exit 1
+test_rev_parse 'in repo.git'              false true  false
+cd objects || exit 1
+test_rev_parse 'in repo.git/objects'      false true  false
+cd ../work || exit 1
+test_rev_parse 'in repo.git/work'         false true true ''
+cd sub/dir || exit 1
+test_rev_parse 'in repo.git/sub/dir' false true true sub/dir/
+cd ../../../.. || exit 1
+
+test_expect_success 'repo finds its work tree' '
+       (cd repo.git &&
+        : > work/sub/dir/untracked &&
+        test sub/dir/untracked = "$(git ls-files --others)")
+'
+
+test_expect_success 'repo finds its work tree from work tree, too' '
+       (cd repo.git/work/sub/dir &&
+        : > tracked &&
+        git --git-dir=../../.. add tracked &&
+        cd ../../.. &&
+        test sub/dir/tracked = "$(git ls-files)")
+'
+
+test_expect_success '_gently() groks relative GIT_DIR & GIT_WORK_TREE' '
+       cd repo.git/work/sub/dir &&
+       GIT_DIR=../../.. GIT_WORK_TREE=../.. GIT_PAGER= \
+               git diff --exit-code tracked &&
+       echo changed > tracked &&
+       ! GIT_DIR=../../.. GIT_WORK_TREE=../.. GIT_PAGER= \
+               git diff --exit-code tracked
+'
+
+test_done
diff --git a/t/t2000-checkout-cache-clash.sh b/t/t2000-checkout-cache-clash.sh
new file mode 100755 (executable)
index 0000000..ac84335
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git checkout-index test.
+
+This test registers the following filesystem structure in the
+cache:
+
+    path0       - a file
+    path1/file1 - a file in a directory
+
+And then tries to checkout in a work tree that has the following:
+
+    path0/file0 - a file in a directory
+    path1       - a file
+
+The git checkout-index command should fail when attempting to checkout
+path0, finding it is occupied by a directory, and path1/file1, finding
+path1 is occupied by a non-directory.  With "-f" flag, it should remove
+the conflicting paths and succeed.
+'
+. ./test-lib.sh
+
+date >path0
+mkdir path1
+date >path1/file1
+
+test_expect_success \
+    'git update-index --add various paths.' \
+    'git update-index --add path0 path1/file1'
+
+rm -fr path0 path1
+mkdir path0
+date >path0/file0
+date >path1
+
+test_expect_failure \
+    'git checkout-index without -f should fail on conflicting work tree.' \
+    'git checkout-index -a'
+
+test_expect_success \
+    'git checkout-index with -f should succeed.' \
+    'git checkout-index -f -a'
+
+test_expect_success \
+    'git checkout-index conflicting paths.' \
+    'test -f path0 && test -d path1 && test -f path1/file1'
+
+test_done
diff --git a/t/t2001-checkout-cache-clash.sh b/t/t2001-checkout-cache-clash.sh
new file mode 100755 (executable)
index 0000000..ef00753
--- /dev/null
@@ -0,0 +1,86 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git checkout-index test.
+
+This test registers the following filesystem structure in the cache:
+
+    path0/file0        - a file in a directory
+    path1/file1 - a file in a directory
+
+and attempts to check it out when the work tree has:
+
+    path0/file0 - a file in a directory
+    path1       - a symlink pointing at "path0"
+
+Checkout cache should fail to extract path1/file1 because the leading
+path path1 is occupied by a non-directory.  With "-f" it should remove
+the symlink path1 and create directory path1 and file path1/file1.
+'
+. ./test-lib.sh
+
+show_files() {
+       # show filesystem files, just [-dl] for type and name
+       find path? -ls |
+       sed -e 's/^[0-9]* * [0-9]* * \([-bcdl]\)[^ ]* *[0-9]* *[^ ]* *[^ ]* *[0-9]* [A-Z][a-z][a-z] [0-9][0-9] [^ ]* /fs: \1 /'
+       # what's in the cache, just mode and name
+       git ls-files --stage |
+       sed -e 's/^\([0-9]*\) [0-9a-f]* [0-3] /ca: \1 /'
+       # what's in the tree, just mode and name.
+       git ls-tree -r "$1" |
+       sed -e 's/^\([0-9]*\)   [^ ]*   [0-9a-f]*       /tr: \1 /'
+}
+
+mkdir path0
+date >path0/file0
+test_expect_success \
+    'git update-index --add path0/file0' \
+    'git update-index --add path0/file0'
+test_expect_success \
+    'writing tree out with git write-tree' \
+    'tree1=$(git write-tree)'
+test_debug 'show_files $tree1'
+
+mkdir path1
+date >path1/file1
+test_expect_success \
+    'git update-index --add path1/file1' \
+    'git update-index --add path1/file1'
+test_expect_success \
+    'writing tree out with git write-tree' \
+    'tree2=$(git write-tree)'
+test_debug 'show_files $tree2'
+
+rm -fr path1
+test_expect_success \
+    'read previously written tree and checkout.' \
+    'git read-tree -m $tree1 && git checkout-index -f -a'
+test_debug 'show_files $tree1'
+
+ln -s path0 path1
+test_expect_success \
+    'git update-index --add a symlink.' \
+    'git update-index --add path1'
+test_expect_success \
+    'writing tree out with git write-tree' \
+    'tree3=$(git write-tree)'
+test_debug 'show_files $tree3'
+
+# Morten says "Got that?" here.
+# Test begins.
+
+test_expect_success \
+    'read previously written tree and checkout.' \
+    'git read-tree $tree2 && git checkout-index -f -a'
+test_debug 'show_files $tree2'
+
+test_expect_success \
+    'checking out conflicting path with -f' \
+    'test ! -h path0 && test -d path0 &&
+     test ! -h path1 && test -d path1 &&
+     test ! -h path0/file0 && test -f path0/file0 &&
+     test ! -h path1/file1 && test -f path1/file1'
+
+test_done
diff --git a/t/t2002-checkout-cache-u.sh b/t/t2002-checkout-cache-u.sh
new file mode 100755 (executable)
index 0000000..f7a0055
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git checkout-index -u test.
+
+With -u flag, git checkout-index internally runs the equivalent of
+git update-index --refresh on the checked out entry.'
+
+. ./test-lib.sh
+
+test_expect_success \
+'preparation' '
+echo frotz >path0 &&
+git update-index --add path0 &&
+t=$(git write-tree)'
+
+test_expect_failure \
+'without -u, git checkout-index smudges stat information.' '
+rm -f path0 &&
+git read-tree $t &&
+git checkout-index -f -a &&
+git diff-files | diff - /dev/null'
+
+test_expect_success \
+'with -u, git checkout-index picks up stat information from new files.' '
+rm -f path0 &&
+git read-tree $t &&
+git checkout-index -u -f -a &&
+git diff-files | diff - /dev/null'
+
+test_done
diff --git a/t/t2003-checkout-cache-mkdir.sh b/t/t2003-checkout-cache-mkdir.sh
new file mode 100755 (executable)
index 0000000..71894b3
--- /dev/null
@@ -0,0 +1,96 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git checkout-index --prefix test.
+
+This test makes sure that --prefix option works as advertised, and
+also verifies that such leading path may contain symlinks, unlike
+the GIT controlled paths.
+'
+
+. ./test-lib.sh
+
+test_expect_success \
+    'setup' \
+    'mkdir path1 &&
+    echo frotz >path0 &&
+    echo rezrov >path1/file1 &&
+    git update-index --add path0 path1/file1'
+
+test_expect_success \
+    'have symlink in place where dir is expected.' \
+    'rm -fr path0 path1 &&
+     mkdir path2 &&
+     ln -s path2 path1 &&
+     git checkout-index -f -a &&
+     test ! -h path1 && test -d path1 &&
+     test -f path1/file1 && test ! -f path2/file1'
+
+test_expect_success \
+    'use --prefix=path2/' \
+    'rm -fr path0 path1 path2 &&
+     mkdir path2 &&
+     git checkout-index --prefix=path2/ -f -a &&
+     test -f path2/path0 &&
+     test -f path2/path1/file1 &&
+     test ! -f path0 &&
+     test ! -f path1/file1'
+
+test_expect_success \
+    'use --prefix=tmp-' \
+    'rm -fr path0 path1 path2 tmp* &&
+     git checkout-index --prefix=tmp- -f -a &&
+     test -f tmp-path0 &&
+     test -f tmp-path1/file1 &&
+     test ! -f path0 &&
+     test ! -f path1/file1'
+
+test_expect_success \
+    'use --prefix=tmp- but with a conflicting file and dir' \
+    'rm -fr path0 path1 path2 tmp* &&
+     echo nitfol >tmp-path1 &&
+     mkdir tmp-path0 &&
+     git checkout-index --prefix=tmp- -f -a &&
+     test -f tmp-path0 &&
+     test -f tmp-path1/file1 &&
+     test ! -f path0 &&
+     test ! -f path1/file1'
+
+# Linus fix #1
+test_expect_success \
+    'use --prefix=tmp/orary/ where tmp is a symlink' \
+    'rm -fr path0 path1 path2 tmp* &&
+     mkdir tmp1 tmp1/orary &&
+     ln -s tmp1 tmp &&
+     git checkout-index --prefix=tmp/orary/ -f -a &&
+     test -d tmp1/orary &&
+     test -f tmp1/orary/path0 &&
+     test -f tmp1/orary/path1/file1 &&
+     test -h tmp'
+
+# Linus fix #2
+test_expect_success \
+    'use --prefix=tmp/orary- where tmp is a symlink' \
+    'rm -fr path0 path1 path2 tmp* &&
+     mkdir tmp1 &&
+     ln -s tmp1 tmp &&
+     git checkout-index --prefix=tmp/orary- -f -a &&
+     test -f tmp1/orary-path0 &&
+     test -f tmp1/orary-path1/file1 &&
+     test -h tmp'
+
+# Linus fix #3
+test_expect_success \
+    'use --prefix=tmp- where tmp-path1 is a symlink' \
+    'rm -fr path0 path1 path2 tmp* &&
+     mkdir tmp1 &&
+     ln -s tmp1 tmp-path1 &&
+     git checkout-index --prefix=tmp- -f -a &&
+     test -f tmp-path0 &&
+     test ! -h tmp-path1 &&
+     test -d tmp-path1 &&
+     test -f tmp-path1/file1'
+
+test_done
diff --git a/t/t2004-checkout-cache-temp.sh b/t/t2004-checkout-cache-temp.sh
new file mode 100755 (executable)
index 0000000..39133b8
--- /dev/null
@@ -0,0 +1,212 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Shawn Pearce
+#
+
+test_description='git checkout-index --temp test.
+
+With --temp flag, git checkout-index writes to temporary merge files
+rather than the tracked path.'
+
+. ./test-lib.sh
+
+test_expect_success \
+'preparation' '
+mkdir asubdir &&
+echo tree1path0 >path0 &&
+echo tree1path1 >path1 &&
+echo tree1path3 >path3 &&
+echo tree1path4 >path4 &&
+echo tree1asubdir/path5 >asubdir/path5 &&
+git update-index --add path0 path1 path3 path4 asubdir/path5 &&
+t1=$(git write-tree) &&
+rm -f path* .merge_* out .git/index &&
+echo tree2path0 >path0 &&
+echo tree2path1 >path1 &&
+echo tree2path2 >path2 &&
+echo tree2path4 >path4 &&
+git update-index --add path0 path1 path2 path4 &&
+t2=$(git write-tree) &&
+rm -f path* .merge_* out .git/index &&
+echo tree2path0 >path0 &&
+echo tree3path1 >path1 &&
+echo tree3path2 >path2 &&
+echo tree3path3 >path3 &&
+git update-index --add path0 path1 path2 path3 &&
+t3=$(git write-tree)'
+
+test_expect_success \
+'checkout one stage 0 to temporary file' '
+rm -f path* .merge_* out .git/index &&
+git read-tree $t1 &&
+git checkout-index --temp -- path1 >out &&
+test $(wc -l <out) = 1 &&
+test $(cut "-d " -f2 out) = path1 &&
+p=$(cut "-d    " -f1 out) &&
+test -f $p &&
+test $(cat $p) = tree1path1'
+
+test_expect_success \
+'checkout all stage 0 to temporary files' '
+rm -f path* .merge_* out .git/index &&
+git read-tree $t1 &&
+git checkout-index -a --temp >out &&
+test $(wc -l <out) = 5 &&
+for f in path0 path1 path3 path4 asubdir/path5
+do
+       test $(grep $f out | cut "-d    " -f2) = $f &&
+       p=$(grep $f out | cut "-d       " -f1) &&
+       test -f $p &&
+       test $(cat $p) = tree1$f
+done'
+
+test_expect_success \
+'prepare 3-way merge' '
+rm -f path* .merge_* out .git/index &&
+git read-tree -m $t1 $t2 $t3'
+
+test_expect_success \
+'checkout one stage 2 to temporary file' '
+rm -f path* .merge_* out &&
+git checkout-index --stage=2 --temp -- path1 >out &&
+test $(wc -l <out) = 1 &&
+test $(cut "-d " -f2 out) = path1 &&
+p=$(cut "-d    " -f1 out) &&
+test -f $p &&
+test $(cat $p) = tree2path1'
+
+test_expect_success \
+'checkout all stage 2 to temporary files' '
+rm -f path* .merge_* out &&
+git checkout-index --all --stage=2 --temp >out &&
+test $(wc -l <out) = 3 &&
+for f in path1 path2 path4
+do
+       test $(grep $f out | cut "-d    " -f2) = $f &&
+       p=$(grep $f out | cut "-d       " -f1) &&
+       test -f $p &&
+       test $(cat $p) = tree2$f
+done'
+
+test_expect_success \
+'checkout all stages/one file to nothing' '
+rm -f path* .merge_* out &&
+git checkout-index --stage=all --temp -- path0 >out &&
+test $(wc -l <out) = 0'
+
+test_expect_success \
+'checkout all stages/one file to temporary files' '
+rm -f path* .merge_* out &&
+git checkout-index --stage=all --temp -- path1 >out &&
+test $(wc -l <out) = 1 &&
+test $(cut "-d " -f2 out) = path1 &&
+cut "-d        " -f1 out | (read s1 s2 s3 &&
+test -f $s1 &&
+test -f $s2 &&
+test -f $s3 &&
+test $(cat $s1) = tree1path1 &&
+test $(cat $s2) = tree2path1 &&
+test $(cat $s3) = tree3path1)'
+
+test_expect_success \
+'checkout some stages/one file to temporary files' '
+rm -f path* .merge_* out &&
+git checkout-index --stage=all --temp -- path2 >out &&
+test $(wc -l <out) = 1 &&
+test $(cut "-d " -f2 out) = path2 &&
+cut "-d        " -f1 out | (read s1 s2 s3 &&
+test $s1 = . &&
+test -f $s2 &&
+test -f $s3 &&
+test $(cat $s2) = tree2path2 &&
+test $(cat $s3) = tree3path2)'
+
+test_expect_success \
+'checkout all stages/all files to temporary files' '
+rm -f path* .merge_* out &&
+git checkout-index -a --stage=all --temp >out &&
+test $(wc -l <out) = 5'
+
+test_expect_success \
+'-- path0: no entry' '
+test x$(grep path0 out | cut "-d       " -f2) = x'
+
+test_expect_success \
+'-- path1: all 3 stages' '
+test $(grep path1 out | cut "-d        " -f2) = path1 &&
+grep path1 out | cut "-d       " -f1 | (read s1 s2 s3 &&
+test -f $s1 &&
+test -f $s2 &&
+test -f $s3 &&
+test $(cat $s1) = tree1path1 &&
+test $(cat $s2) = tree2path1 &&
+test $(cat $s3) = tree3path1)'
+
+test_expect_success \
+'-- path2: no stage 1, have stage 2 and 3' '
+test $(grep path2 out | cut "-d        " -f2) = path2 &&
+grep path2 out | cut "-d       " -f1 | (read s1 s2 s3 &&
+test $s1 = . &&
+test -f $s2 &&
+test -f $s3 &&
+test $(cat $s2) = tree2path2 &&
+test $(cat $s3) = tree3path2)'
+
+test_expect_success \
+'-- path3: no stage 2, have stage 1 and 3' '
+test $(grep path3 out | cut "-d        " -f2) = path3 &&
+grep path3 out | cut "-d       " -f1 | (read s1 s2 s3 &&
+test -f $s1 &&
+test $s2 = . &&
+test -f $s3 &&
+test $(cat $s1) = tree1path3 &&
+test $(cat $s3) = tree3path3)'
+
+test_expect_success \
+'-- path4: no stage 3, have stage 1 and 3' '
+test $(grep path4 out | cut "-d        " -f2) = path4 &&
+grep path4 out | cut "-d       " -f1 | (read s1 s2 s3 &&
+test -f $s1 &&
+test -f $s2 &&
+test $s3 = . &&
+test $(cat $s1) = tree1path4 &&
+test $(cat $s2) = tree2path4)'
+
+test_expect_success \
+'-- asubdir/path5: no stage 2 and 3 have stage 1' '
+test $(grep asubdir/path5 out | cut "-d        " -f2) = asubdir/path5 &&
+grep asubdir/path5 out | cut "-d       " -f1 | (read s1 s2 s3 &&
+test -f $s1 &&
+test $s2 = . &&
+test $s3 = . &&
+test $(cat $s1) = tree1asubdir/path5)'
+
+test_expect_success \
+'checkout --temp within subdir' '
+(cd asubdir &&
+ git checkout-index -a --stage=all >out &&
+ test $(wc -l <out) = 1 &&
+ test $(grep path5 out | cut "-d       " -f2) = path5 &&
+ grep path5 out | cut "-d      " -f1 | (read s1 s2 s3 &&
+ test -f ../$s1 &&
+ test $s2 = . &&
+ test $s3 = . &&
+ test $(cat ../$s1) = tree1asubdir/path5)
+)'
+
+test_expect_success \
+'checkout --temp symlink' '
+rm -f path* .merge_* out .git/index &&
+ln -s b a &&
+git update-index --add a &&
+t4=$(git write-tree) &&
+rm -f .git/index &&
+git read-tree $t4 &&
+git checkout-index --temp -a >out &&
+test $(wc -l <out) = 1 &&
+test $(cut "-d " -f2 out) = a &&
+p=$(cut "-d    " -f1 out) &&
+test -f $p &&
+test $(cat $p) = b'
+
+test_done
diff --git a/t/t2005-checkout-index-symlinks.sh b/t/t2005-checkout-index-symlinks.sh
new file mode 100755 (executable)
index 0000000..a84c5a6
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes Sixt
+#
+
+test_description='git checkout-index on filesystem w/o symlinks test.
+
+This tests that git checkout-index creates a symbolic link as a plain
+file if core.symlinks is false.'
+
+. ./test-lib.sh
+
+test_expect_success \
+'preparation' '
+git config core.symlinks false &&
+l=$(echo -n file | git-hash-object -t blob -w --stdin) &&
+echo "120000 $l        symlink" | git update-index --index-info'
+
+test_expect_success \
+'the checked-out symlink must be a file' '
+git checkout-index symlink &&
+test -f symlink'
+
+test_expect_success \
+'the file must be the blob we added during the setup' '
+test "$(git-hash-object -t blob symlink)" = $l'
+
+test_done
diff --git a/t/t2007-checkout-symlink.sh b/t/t2007-checkout-symlink.sh
new file mode 100755 (executable)
index 0000000..0526fce
--- /dev/null
@@ -0,0 +1,50 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Junio C Hamano
+
+test_description='git checkout to switch between branches with symlink<->dir'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       mkdir frotz &&
+       echo hello >frotz/filfre &&
+       git add frotz/filfre &&
+       test_tick &&
+       git commit -m "master has file frotz/filfre" &&
+
+       git branch side &&
+
+       echo goodbye >nitfol &&
+       git add nitfol
+       test_tick &&
+       git commit -m "master adds file nitfol" &&
+
+       git checkout side &&
+
+       git rm --cached frotz/filfre &&
+       mv frotz xyzzy &&
+       ln -s xyzzy frotz &&
+       git add xyzzy/filfre frotz &&
+       test_tick &&
+       git commit -m "side moves frotz/ to xyzzy/ and adds frotz->xyzzy/"
+
+'
+
+test_expect_success 'switch from symlink to dir' '
+
+       git checkout master
+
+'
+
+rm -fr frotz xyzzy nitfol &&
+git checkout -f master || exit
+
+test_expect_success 'switch from dir to symlink' '
+
+       git checkout side
+
+'
+
+test_done
diff --git a/t/t2008-checkout-subdir.sh b/t/t2008-checkout-subdir.sh
new file mode 100755 (executable)
index 0000000..f78945e
--- /dev/null
@@ -0,0 +1,82 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 David Symonds
+
+test_description='git checkout from subdirectories'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       echo "base" > file0 &&
+       git add file0 &&
+       mkdir dir1 &&
+       echo "hello" > dir1/file1 &&
+       git add dir1/file1 &&
+       mkdir dir2 &&
+       echo "bonjour" > dir2/file2 &&
+       git add dir2/file2 &&
+       test_tick &&
+       git commit -m "populate tree"
+
+'
+
+test_expect_success 'remove and restore with relative path' '
+
+       (
+               cd dir1 &&
+               rm ../file0 &&
+               git checkout HEAD -- ../file0 &&
+               test "base" = "$(cat ../file0)" &&
+               rm ../dir2/file2 &&
+               git checkout HEAD -- ../dir2/file2 &&
+               test "bonjour" = "$(cat ../dir2/file2)" &&
+               rm ../file0 ./file1 &&
+               git checkout HEAD -- .. &&
+               test "base" = "$(cat ../file0)" &&
+               test "hello" = "$(cat file1)"
+       )
+
+'
+
+test_expect_success 'checkout with empty prefix' '
+
+       rm file0 &&
+       git checkout HEAD -- file0 &&
+       test "base" = "$(cat file0)"
+
+'
+
+test_expect_success 'checkout with simple prefix' '
+
+       rm dir1/file1 &&
+       git checkout HEAD -- dir1 &&
+       test "hello" = "$(cat dir1/file1)" &&
+       rm dir1/file1 &&
+       git checkout HEAD -- dir1/file1 &&
+       test "hello" = "$(cat dir1/file1)"
+
+'
+
+# This is not expected to work as ls-files was not designed
+# to deal with such.  Enable it when ls-files is updated.
+: test_expect_success 'checkout with complex relative path' '
+
+       rm file1 &&
+       git checkout HEAD -- ../dir1/../dir1/file1 && test -f ./file1
+
+'
+
+test_expect_failure 'relative path outside tree should fail' \
+       'git checkout HEAD -- ../../Makefile'
+
+test_expect_failure 'incorrect relative path to file should fail (1)' \
+       'git checkout HEAD -- ../file0'
+
+test_expect_failure 'incorrect relative path should fail (2)' \
+       '( cd dir1 && git checkout HEAD -- ./file0 )'
+
+test_expect_failure 'incorrect relative path should fail (3)' \
+       '( cd dir1 && git checkout HEAD -- ../../file0 )'
+
+test_done
diff --git a/t/t2050-git-dir-relative.sh b/t/t2050-git-dir-relative.sh
new file mode 100755 (executable)
index 0000000..88f268b
--- /dev/null
@@ -0,0 +1,55 @@
+#!/bin/sh
+
+test_description='check problems with relative GIT_DIR
+
+This test creates a working tree state with a file and subdir:
+
+  top (committed several times)
+  subdir (a subdirectory)
+
+It creates a commit-hook and tests it, then moves .git
+into the subdir while keeping the worktree location,
+and tries commits from the top and the subdir, checking
+that the commit-hook still gets called.'
+
+. ./test-lib.sh
+
+COMMIT_FILE="$(pwd)/output"
+export COMMIT_FILE
+
+test_expect_success 'Setting up post-commit hook' '
+mkdir -p .git/hooks &&
+echo >.git/hooks/post-commit "#!/bin/sh
+touch \"\${COMMIT_FILE}\"
+echo Post commit hook was called." &&
+chmod +x .git/hooks/post-commit'
+
+test_expect_success 'post-commit hook used ordinarily' '
+echo initial >top &&
+git-add top
+git-commit -m initial &&
+test -r "${COMMIT_FILE}"
+'
+
+rm -rf "${COMMIT_FILE}"
+mkdir subdir
+mv .git subdir
+
+test_expect_success 'post-commit-hook created and used from top dir' '
+echo changed >top &&
+git --git-dir subdir/.git add top &&
+git --git-dir subdir/.git commit -m topcommit &&
+test -r "${COMMIT_FILE}"
+'
+
+rm -rf "${COMMIT_FILE}"
+
+test_expect_success 'post-commit-hook from sub dir' '
+echo changed again >top
+cd subdir &&
+git --git-dir .git --work-tree .. add ../top &&
+git --git-dir .git --work-tree .. commit -m subcommit &&
+test -r "${COMMIT_FILE}"
+'
+
+test_done
diff --git a/t/t2100-update-cache-badpath.sh b/t/t2100-update-cache-badpath.sh
new file mode 100755 (executable)
index 0000000..04a1ed1
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git update-index nonsense-path test.
+
+This test creates the following structure in the cache:
+
+    path0       - a file
+    path1       - a symlink
+    path2/file2 - a file in a directory
+    path3/file3 - a file in a directory
+
+and tries to git update-index --add the following:
+
+    path0/file0 - a file in a directory
+    path1/file1 - a file in a directory
+    path2       - a file
+    path3       - a symlink
+
+All of the attempts should fail.
+'
+
+. ./test-lib.sh
+
+mkdir path2 path3
+date >path0
+ln -s xyzzy path1
+date >path2/file2
+date >path3/file3
+
+test_expect_success \
+    'git update-index --add to add various paths.' \
+    'git update-index --add -- path0 path1 path2/file2 path3/file3'
+
+rm -fr path?
+
+mkdir path0 path1
+date >path2
+ln -s frotz path3
+date >path0/file0
+date >path1/file1
+
+for p in path0/file0 path1/file1 path2 path3
+do
+       test_expect_failure \
+           "git update-index to add conflicting path $p should fail." \
+           "git update-index --add -- $p"
+done
+test_done
diff --git a/t/t2101-update-index-reupdate.sh b/t/t2101-update-index-reupdate.sh
new file mode 100755 (executable)
index 0000000..59b560b
--- /dev/null
@@ -0,0 +1,84 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='git update-index --again test.
+'
+
+. ./test-lib.sh
+
+cat > expected <<\EOF
+100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0      file1
+100644 9db8893856a8a02eaa73470054b7c1c5a7c82e47 0      file2
+EOF
+test_expect_success 'update-index --add' \
+       'echo hello world >file1 &&
+        echo goodbye people >file2 &&
+        git update-index --add file1 file2 &&
+        git ls-files -s >current &&
+        cmp current expected'
+
+test_expect_success 'update-index --again' \
+       'rm -f file1 &&
+       echo hello everybody >file2 &&
+       if git update-index --again
+       then
+               echo should have refused to remove file1
+               exit 1
+       else
+               echo happy - failed as expected
+       fi &&
+        git ls-files -s >current &&
+        cmp current expected'
+
+cat > expected <<\EOF
+100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0      file2
+EOF
+test_expect_success 'update-index --remove --again' \
+       'git update-index --remove --again &&
+        git ls-files -s >current &&
+        cmp current expected'
+
+test_expect_success 'first commit' 'git-commit -m initial'
+
+cat > expected <<\EOF
+100644 53ab446c3f4e42ce9bb728a0ccb283a101be4979 0      dir1/file3
+100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0      file2
+EOF
+test_expect_success 'update-index again' \
+       'mkdir -p dir1 &&
+       echo hello world >dir1/file3 &&
+       echo goodbye people >file2 &&
+       git update-index --add file2 dir1/file3 &&
+       echo hello everybody >file2
+       echo happy >dir1/file3 &&
+       git update-index --again &&
+       git ls-files -s >current &&
+       cmp current expected'
+
+cat > expected <<\EOF
+100644 d7fb3f695f06c759dbf3ab00046e7cc2da22d10f 0      dir1/file3
+100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0      file2
+EOF
+test_expect_success 'update-index --update from subdir' \
+       'echo not so happy >file2 &&
+       cd dir1 &&
+       cat ../file2 >file3 &&
+       git update-index --again &&
+       cd .. &&
+       git ls-files -s >current &&
+       cmp current expected'
+
+cat > expected <<\EOF
+100644 594fb5bb1759d90998e2bf2a38261ae8e243c760 0      dir1/file3
+100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0      file2
+EOF
+test_expect_success 'update-index --update with pathspec' \
+       'echo very happy >file2 &&
+       cat file2 >dir1/file3 &&
+       git update-index --again dir1/ &&
+       git ls-files -s >current &&
+       cmp current expected'
+
+test_done
diff --git a/t/t2102-update-index-symlinks.sh b/t/t2102-update-index-symlinks.sh
new file mode 100755 (executable)
index 0000000..19d0894
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes Sixt
+#
+
+test_description='git update-index on filesystem w/o symlinks test.
+
+This tests that git update-index keeps the symbolic link property
+even if a plain file is in the working tree if core.symlinks is false.'
+
+. ./test-lib.sh
+
+test_expect_success \
+'preparation' '
+git config core.symlinks false &&
+l=$(echo -n file | git-hash-object -t blob -w --stdin) &&
+echo "120000 $l        symlink" | git update-index --index-info'
+
+test_expect_success \
+'modify the symbolic link' '
+echo -n new-file > symlink &&
+git update-index symlink'
+
+test_expect_success \
+'the index entry must still be a symbolic link' '
+case "`git ls-files --stage --cached symlink`" in
+120000" "*symlink) echo ok;;
+*) echo fail; git ls-files --stage --cached symlink; (exit 1);;
+esac'
+
+test_done
diff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh
new file mode 100755 (executable)
index 0000000..24f892f
--- /dev/null
@@ -0,0 +1,114 @@
+#!/bin/sh
+
+test_description='git add -u
+
+This test creates a working tree state with three files:
+
+  top (previously committed, modified)
+  dir/sub (previously committed, modified)
+  dir/other (untracked)
+
+and issues a git add -u with path limiting on "dir" to add
+only the updates to dir/sub.
+
+Also tested are "git add -u" without limiting, and "git add -u"
+without contents changes.'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       echo initial >check &&
+       echo initial >top &&
+       echo initial >foo &&
+       mkdir dir1 dir2 &&
+       echo initial >dir1/sub1 &&
+       echo initial >dir1/sub2 &&
+       echo initial >dir2/sub3 &&
+       git add check dir1 dir2 top foo &&
+       test_tick
+       git-commit -m initial &&
+
+       echo changed >check &&
+       echo changed >top &&
+       echo changed >dir2/sub3 &&
+       rm -f dir1/sub1 &&
+       echo other >dir2/other
+'
+
+test_expect_success update '
+       git add -u dir1 dir2
+'
+
+test_expect_success 'update noticed a removal' '
+       test "$(git-ls-files dir1/sub1)" = ""
+'
+
+test_expect_success 'update touched correct path' '
+       test "$(git-diff-files --name-status dir2/sub3)" = ""
+'
+
+test_expect_success 'update did not touch other tracked files' '
+       test "$(git-diff-files --name-status check)" = "M       check" &&
+       test "$(git-diff-files --name-status top)" = "M top"
+'
+
+test_expect_success 'update did not touch untracked files' '
+       test "$(git-ls-files dir2/other)" = ""
+'
+
+test_expect_success 'cache tree has not been corrupted' '
+
+       git ls-files -s |
+       sed -e "s/ 0    /       /" >expect &&
+       git ls-tree -r $(git write-tree) |
+       sed -e "s/ blob / /" >current &&
+       diff -u expect current
+
+'
+
+test_expect_success 'update from a subdirectory' '
+       (
+               cd dir1 &&
+               echo more >sub2 &&
+               git add -u sub2
+       )
+'
+
+test_expect_success 'change gets noticed' '
+
+       test "$(git diff-files --name-status dir1)" = ""
+
+'
+
+test_expect_success 'replace a file with a symlink' '
+
+       rm foo &&
+       ln -s top foo &&
+       git add -u -- foo
+
+'
+
+test_expect_success 'add everything changed' '
+
+       git add -u &&
+       test -z "$(git diff-files)"
+
+'
+
+test_expect_success 'touch and then add -u' '
+
+       touch check &&
+       git add -u &&
+       test -z "$(git diff-files)"
+
+'
+
+test_expect_success 'touch and then add explicitly' '
+
+       touch check &&
+       git add check &&
+       test -z "$(git diff-files)"
+
+'
+
+test_done
diff --git a/t/t3000-ls-files-others.sh b/t/t3000-ls-files-others.sh
new file mode 100755 (executable)
index 0000000..bc0a351
--- /dev/null
@@ -0,0 +1,56 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git ls-files test (--others should pick up symlinks).
+
+This test runs git ls-files --others with the following on the
+filesystem.
+
+    path0       - a file
+    path1      - a symlink
+    path2/file2 - a file in a directory
+    path3-junk  - a file to confuse things
+    path3/file3 - a file in a directory
+'
+. ./test-lib.sh
+
+date >path0
+ln -s xyzzy path1
+mkdir path2 path3
+date >path2/file2
+date >path2-junk
+date >path3/file3
+date >path3-junk
+git update-index --add path3-junk path3/file3
+
+cat >expected1 <<EOF
+expected1
+expected2
+output
+path0
+path1
+path2-junk
+path2/file2
+EOF
+sed -e 's|path2/file2|path2/|' <expected1 >expected2
+
+test_expect_success \
+    'git ls-files --others to show output.' \
+    'git ls-files --others >output'
+
+test_expect_success \
+    'git ls-files --others should pick up symlinks.' \
+    'diff output expected1'
+
+test_expect_success \
+    'git ls-files --others --directory to show output.' \
+    'git ls-files --others --directory >output'
+
+
+test_expect_success \
+    'git ls-files --others --directory should not get confused.' \
+    'diff output expected2'
+
+test_done
diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh
new file mode 100755 (executable)
index 0000000..e25b255
--- /dev/null
@@ -0,0 +1,102 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git ls-files --others --exclude
+
+This test runs git ls-files --others and tests --exclude patterns.
+'
+
+. ./test-lib.sh
+
+rm -fr one three
+for dir in . one one/two three
+do
+  mkdir -p $dir &&
+  for i in 1 2 3 4 5 6 7 8
+  do
+    >$dir/a.$i
+  done
+done
+
+cat >expect <<EOF
+a.2
+a.4
+a.5
+a.8
+one/a.3
+one/a.4
+one/a.5
+one/a.7
+one/two/a.2
+one/two/a.3
+one/two/a.5
+one/two/a.7
+one/two/a.8
+three/a.2
+three/a.3
+three/a.4
+three/a.5
+three/a.8
+EOF
+
+echo '.gitignore
+output
+expect
+.gitignore
+*.7
+!*.8' >.git/ignore
+
+echo '*.1
+/*.3
+!*.6' >.gitignore
+echo '*.2
+two/*.4
+!*.7
+*.8' >one/.gitignore
+echo '!*.2
+!*.8' >one/two/.gitignore
+
+test_expect_success \
+    'git ls-files --others with various exclude options.' \
+    'git ls-files --others \
+       --exclude=\*.6 \
+       --exclude-per-directory=.gitignore \
+       --exclude-from=.git/ignore \
+       >output &&
+     git diff expect output'
+
+# Test \r\n (MSDOS-like systems)
+printf '*.1\r\n/*.3\r\n!*.6\r\n' >.gitignore
+
+test_expect_success \
+    'git ls-files --others with \r\n line endings.' \
+    'git ls-files --others \
+       --exclude=\*.6 \
+       --exclude-per-directory=.gitignore \
+       --exclude-from=.git/ignore \
+       >output &&
+     git diff expect output'
+
+cat > excludes-file << EOF
+*.[1-8]
+e*
+EOF
+
+git config core.excludesFile excludes-file
+
+git status | grep "^#  " > output
+
+cat > expect << EOF
+#      .gitignore
+#      a.6
+#      one/
+#      output
+#      three/
+EOF
+
+test_expect_success 'git-status honours core.excludesfile' \
+       'diff -u expect output'
+
+test_done
diff --git a/t/t3002-ls-files-dashpath.sh b/t/t3002-ls-files-dashpath.sh
new file mode 100755 (executable)
index 0000000..8687a01
--- /dev/null
@@ -0,0 +1,69 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git ls-files test (-- to terminate the path list).
+
+This test runs git ls-files --others with the following on the
+filesystem.
+
+    path0       - a file
+    -foo       - a file with a funny name.
+    --         - another file with a funny name.
+'
+. ./test-lib.sh
+
+test_expect_success \
+       setup \
+       'echo frotz >path0 &&
+       echo frotz >./-foo &&
+       echo frotz >./--'
+
+test_expect_success \
+    'git ls-files without path restriction.' \
+    'git ls-files --others >output &&
+     git diff output - <<EOF
+--
+-foo
+output
+path0
+EOF
+'
+
+test_expect_success \
+    'git ls-files with path restriction.' \
+    'git ls-files --others path0 >output &&
+       git diff output - <<EOF
+path0
+EOF
+'
+
+test_expect_success \
+    'git ls-files with path restriction with --.' \
+    'git ls-files --others -- path0 >output &&
+       git diff output - <<EOF
+path0
+EOF
+'
+
+test_expect_success \
+    'git ls-files with path restriction with -- --.' \
+    'git ls-files --others -- -- >output &&
+       git diff output - <<EOF
+--
+EOF
+'
+
+test_expect_success \
+    'git ls-files with no path restriction.' \
+    'git ls-files --others -- >output &&
+       git diff output - <<EOF
+--
+-foo
+output
+path0
+EOF
+'
+
+test_done
diff --git a/t/t3010-ls-files-killed-modified.sh b/t/t3010-ls-files-killed-modified.sh
new file mode 100755 (executable)
index 0000000..ec14040
--- /dev/null
@@ -0,0 +1,96 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git ls-files -k and -m flags test.
+
+This test prepares the following in the cache:
+
+    path0       - a file
+    path1       - a symlink
+    path2/file2 - a file in a directory
+    path3/file3 - a file in a directory
+
+and the following on the filesystem:
+
+    path0/file0 - a file in a directory
+    path1/file1 - a file in a directory
+    path2       - a file
+    path3       - a symlink
+    path4      - a file
+    path5      - a symlink
+    path6/file6 - a file in a directory
+
+git ls-files -k should report that existing filesystem
+objects except path4, path5 and path6/file6 to be killed.
+
+Also for modification test, the cache and working tree have:
+
+    path7       - an empty file, modified to a non-empty file.
+    path8       - a non-empty file, modified to an empty file.
+    path9      - an empty file, cache dirtied.
+    path10     - a non-empty file, cache dirtied.
+
+We should report path0, path1, path2/file2, path3/file3, path7 and path8
+modified without reporting path9 and path10.
+'
+. ./test-lib.sh
+
+date >path0
+ln -s xyzzy path1
+mkdir path2 path3
+date >path2/file2
+date >path3/file3
+: >path7
+date >path8
+: >path9
+date >path10
+test_expect_success \
+    'git update-index --add to add various paths.' \
+    "git update-index --add -- path0 path1 path?/file? path7 path8 path9 path10"
+
+rm -fr path? ;# leave path10 alone
+date >path2
+ln -s frotz path3
+ln -s nitfol path5
+mkdir path0 path1 path6
+date >path0/file0
+date >path1/file1
+date >path6/file6
+date >path7
+: >path8
+: >path9
+touch path10
+
+test_expect_success \
+    'git ls-files -k to show killed files.' \
+    'git ls-files -k >.output'
+cat >.expected <<EOF
+path0/file0
+path1/file1
+path2
+path3
+EOF
+
+test_expect_success \
+    'validate git ls-files -k output.' \
+    'diff .output .expected'
+
+test_expect_success \
+    'git ls-files -m to show modified files.' \
+    'git ls-files -m >.output'
+cat >.expected <<EOF
+path0
+path1
+path2/file2
+path3/file3
+path7
+path8
+EOF
+
+test_expect_success \
+    'validate git ls-files -m output.' \
+    'diff .output .expected'
+
+test_done
diff --git a/t/t3020-ls-files-error-unmatch.sh b/t/t3020-ls-files-error-unmatch.sh
new file mode 100755 (executable)
index 0000000..c83f820
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Carl D. Worth
+#
+
+test_description='git ls-files test for --error-unmatch option
+
+This test runs git ls-files --error-unmatch to ensure it correctly
+returns an error when a non-existent path is provided on the command
+line.
+'
+. ./test-lib.sh
+
+touch foo bar
+git update-index --add foo bar
+git-commit -m "add foo bar"
+
+test_expect_failure \
+    'git ls-files --error-unmatch should fail with unmatched path.' \
+    'git ls-files --error-unmatch foo bar-does-not-match'
+
+test_expect_success \
+    'git ls-files --error-unmatch should succeed eith matched paths.' \
+    'git ls-files --error-unmatch foo bar'
+
+test_done
+1
diff --git a/t/t3030-merge-recursive.sh b/t/t3030-merge-recursive.sh
new file mode 100755 (executable)
index 0000000..607f57f
--- /dev/null
@@ -0,0 +1,527 @@
+#!/bin/sh
+
+test_description='merge-recursive backend test'
+
+. ./test-lib.sh
+
+test_expect_success 'setup 1' '
+
+       echo hello >a &&
+       o0=$(git hash-object a) &&
+       cp a b &&
+       cp a c &&
+       mkdir d &&
+       cp a d/e &&
+
+       test_tick &&
+       git add a b c d/e &&
+       git commit -m initial &&
+       c0=$(git rev-parse --verify HEAD) &&
+       git branch side &&
+       git branch df-1 &&
+       git branch df-2 &&
+       git branch df-3 &&
+       git branch remove &&
+
+       echo hello >>a &&
+       cp a d/e &&
+       o1=$(git hash-object a) &&
+
+       git add a d/e &&
+
+       test_tick &&
+       git commit -m "master modifies a and d/e" &&
+       c1=$(git rev-parse --verify HEAD) &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o1   a"
+               echo "100644 blob $o0   b"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o1   d/e"
+               echo "100644 $o1 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o1 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+'
+
+test_expect_success 'setup 2' '
+
+       rm -rf [abcd] &&
+       git checkout side &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o0   a"
+               echo "100644 blob $o0   b"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o0   d/e"
+               echo "100644 $o0 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o0 0      d/e"
+       ) >expected &&
+       git diff -u expected actual &&
+
+       echo goodbye >>a &&
+       o2=$(git hash-object a) &&
+
+       git add a &&
+
+       test_tick &&
+       git commit -m "side modifies a" &&
+       c2=$(git rev-parse --verify HEAD) &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o2   a"
+               echo "100644 blob $o0   b"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o0   d/e"
+               echo "100644 $o2 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o0 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+'
+
+test_expect_success 'setup 3' '
+
+       rm -rf [abcd] &&
+       git checkout df-1 &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o0   a"
+               echo "100644 blob $o0   b"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o0   d/e"
+               echo "100644 $o0 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o0 0      d/e"
+       ) >expected &&
+       git diff -u expected actual &&
+
+       rm -f b && mkdir b && echo df-1 >b/c && git add b/c &&
+       o3=$(git hash-object b/c) &&
+
+       test_tick &&
+       git commit -m "df-1 makes b/c" &&
+       c3=$(git rev-parse --verify HEAD) &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o0   a"
+               echo "100644 blob $o3   b/c"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o0   d/e"
+               echo "100644 $o0 0      a"
+               echo "100644 $o3 0      b/c"
+               echo "100644 $o0 0      c"
+               echo "100644 $o0 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+'
+
+test_expect_success 'setup 4' '
+
+       rm -rf [abcd] &&
+       git checkout df-2 &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o0   a"
+               echo "100644 blob $o0   b"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o0   d/e"
+               echo "100644 $o0 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o0 0      d/e"
+       ) >expected &&
+       git diff -u expected actual &&
+
+       rm -f a && mkdir a && echo df-2 >a/c && git add a/c &&
+       o4=$(git hash-object a/c) &&
+
+       test_tick &&
+       git commit -m "df-2 makes a/c" &&
+       c4=$(git rev-parse --verify HEAD) &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o4   a/c"
+               echo "100644 blob $o0   b"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o0   d/e"
+               echo "100644 $o4 0      a/c"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o0 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+'
+
+test_expect_success 'setup 5' '
+
+       rm -rf [abcd] &&
+       git checkout remove &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o0   a"
+               echo "100644 blob $o0   b"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o0   d/e"
+               echo "100644 $o0 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o0 0      d/e"
+       ) >expected &&
+       git diff -u expected actual &&
+
+       rm -f b &&
+       echo remove-conflict >a &&
+
+       git add a &&
+       git rm b &&
+       o5=$(git hash-object a) &&
+
+       test_tick &&
+       git commit -m "remove removes b and modifies a" &&
+       c5=$(git rev-parse --verify HEAD) &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o5   a"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o0   d/e"
+               echo "100644 $o5 0      a"
+               echo "100644 $o0 0      c"
+               echo "100644 $o0 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+'
+
+test_expect_success 'setup 6' '
+
+       rm -rf [abcd] &&
+       git checkout df-3 &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o0   a"
+               echo "100644 blob $o0   b"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o0   d/e"
+               echo "100644 $o0 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o0 0      d/e"
+       ) >expected &&
+       git diff -u expected actual &&
+
+       rm -fr d && echo df-3 >d && git add d &&
+       o6=$(git hash-object d) &&
+
+       test_tick &&
+       git commit -m "df-3 makes d" &&
+       c6=$(git rev-parse --verify HEAD) &&
+       ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+       (
+               echo "100644 blob $o0   a"
+               echo "100644 blob $o0   b"
+               echo "100644 blob $o0   c"
+               echo "100644 blob $o6   d"
+               echo "100644 $o0 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o6 0      d"
+       ) >expected &&
+       git diff -u expected actual
+'
+
+test_expect_success 'merge-recursive simple' '
+
+       rm -fr [abcd] &&
+       git checkout -f "$c2" &&
+
+       git-merge-recursive "$c0" -- "$c2" "$c1"
+       status=$?
+       case "$status" in
+       1)
+               : happy
+               ;;
+       *)
+               echo >&2 "why status $status!!!"
+               false
+               ;;
+       esac
+'
+
+test_expect_success 'merge-recursive result' '
+
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o0 1      a"
+               echo "100644 $o2 2      a"
+               echo "100644 $o1 3      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o1 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+'
+
+test_expect_success 'merge-recursive remove conflict' '
+
+       rm -fr [abcd] &&
+       git checkout -f "$c1" &&
+
+       git-merge-recursive "$c0" -- "$c1" "$c5"
+       status=$?
+       case "$status" in
+       1)
+               : happy
+               ;;
+       *)
+               echo >&2 "why status $status!!!"
+               false
+               ;;
+       esac
+'
+
+test_expect_success 'merge-recursive remove conflict' '
+
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o0 1      a"
+               echo "100644 $o1 2      a"
+               echo "100644 $o5 3      a"
+               echo "100644 $o0 0      c"
+               echo "100644 $o1 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+'
+
+test_expect_success 'merge-recursive d/f simple' '
+       rm -fr [abcd] &&
+       git reset --hard &&
+       git checkout -f "$c1" &&
+
+       git-merge-recursive "$c0" -- "$c1" "$c3"
+'
+
+test_expect_success 'merge-recursive result' '
+
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o1 0      a"
+               echo "100644 $o3 0      b/c"
+               echo "100644 $o0 0      c"
+               echo "100644 $o1 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+'
+
+test_expect_success 'merge-recursive d/f conflict' '
+
+       rm -fr [abcd] &&
+       git reset --hard &&
+       git checkout -f "$c1" &&
+
+       git-merge-recursive "$c0" -- "$c1" "$c4"
+       status=$?
+       case "$status" in
+       1)
+               : happy
+               ;;
+       *)
+               echo >&2 "why status $status!!!"
+               false
+               ;;
+       esac
+'
+
+test_expect_success 'merge-recursive d/f conflict result' '
+
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o0 1      a"
+               echo "100644 $o1 2      a"
+               echo "100644 $o4 0      a/c"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o1 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+'
+
+test_expect_success 'merge-recursive d/f conflict the other way' '
+
+       rm -fr [abcd] &&
+       git reset --hard &&
+       git checkout -f "$c4" &&
+
+       git-merge-recursive "$c0" -- "$c4" "$c1"
+       status=$?
+       case "$status" in
+       1)
+               : happy
+               ;;
+       *)
+               echo >&2 "why status $status!!!"
+               false
+               ;;
+       esac
+'
+
+test_expect_success 'merge-recursive d/f conflict result the other way' '
+
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o0 1      a"
+               echo "100644 $o1 3      a"
+               echo "100644 $o4 0      a/c"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o1 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+'
+
+test_expect_success 'merge-recursive d/f conflict' '
+
+       rm -fr [abcd] &&
+       git reset --hard &&
+       git checkout -f "$c1" &&
+
+       git-merge-recursive "$c0" -- "$c1" "$c6"
+       status=$?
+       case "$status" in
+       1)
+               : happy
+               ;;
+       *)
+               echo >&2 "why status $status!!!"
+               false
+               ;;
+       esac
+'
+
+test_expect_success 'merge-recursive d/f conflict result' '
+
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o1 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o6 3      d"
+               echo "100644 $o0 1      d/e"
+               echo "100644 $o1 2      d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+'
+
+test_expect_success 'merge-recursive d/f conflict' '
+
+       rm -fr [abcd] &&
+       git reset --hard &&
+       git checkout -f "$c6" &&
+
+       git-merge-recursive "$c0" -- "$c6" "$c1"
+       status=$?
+       case "$status" in
+       1)
+               : happy
+               ;;
+       *)
+               echo >&2 "why status $status!!!"
+               false
+               ;;
+       esac
+'
+
+test_expect_success 'merge-recursive d/f conflict result' '
+
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o1 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o6 2      d"
+               echo "100644 $o0 1      d/e"
+               echo "100644 $o1 3      d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+'
+
+test_expect_success 'reset and 3-way merge' '
+
+       git reset --hard "$c2" &&
+       git read-tree -m "$c0" "$c2" "$c1"
+
+'
+
+test_expect_success 'reset and bind merge' '
+
+       git reset --hard master &&
+       git read-tree --prefix=M/ master &&
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o1 0      M/a"
+               echo "100644 $o0 0      M/b"
+               echo "100644 $o0 0      M/c"
+               echo "100644 $o1 0      M/d/e"
+               echo "100644 $o1 0      a"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o1 0      d/e"
+       ) >expected &&
+       git diff -u expected actual &&
+
+       git read-tree --prefix=a1/ master &&
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o1 0      M/a"
+               echo "100644 $o0 0      M/b"
+               echo "100644 $o0 0      M/c"
+               echo "100644 $o1 0      M/d/e"
+               echo "100644 $o1 0      a"
+               echo "100644 $o1 0      a1/a"
+               echo "100644 $o0 0      a1/b"
+               echo "100644 $o0 0      a1/c"
+               echo "100644 $o1 0      a1/d/e"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o1 0      d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+       git read-tree --prefix=z/ master &&
+       git ls-files -s >actual &&
+       (
+               echo "100644 $o1 0      M/a"
+               echo "100644 $o0 0      M/b"
+               echo "100644 $o0 0      M/c"
+               echo "100644 $o1 0      M/d/e"
+               echo "100644 $o1 0      a"
+               echo "100644 $o1 0      a1/a"
+               echo "100644 $o0 0      a1/b"
+               echo "100644 $o0 0      a1/c"
+               echo "100644 $o1 0      a1/d/e"
+               echo "100644 $o0 0      b"
+               echo "100644 $o0 0      c"
+               echo "100644 $o1 0      d/e"
+               echo "100644 $o1 0      z/a"
+               echo "100644 $o0 0      z/b"
+               echo "100644 $o0 0      z/c"
+               echo "100644 $o1 0      z/d/e"
+       ) >expected &&
+       git diff -u expected actual
+
+'
+
+test_done
diff --git a/t/t3040-subprojects-basic.sh b/t/t3040-subprojects-basic.sh
new file mode 100755 (executable)
index 0000000..79b9f23
--- /dev/null
@@ -0,0 +1,85 @@
+#!/bin/sh
+
+test_description='Basic subproject functionality'
+. ./test-lib.sh
+
+test_expect_success 'Super project creation' \
+    ': >Makefile &&
+    git add Makefile &&
+    git commit -m "Superproject created"'
+
+
+cat >expected <<EOF
+:000000 160000 00000... A      sub1
+:000000 160000 00000... A      sub2
+EOF
+test_expect_success 'create subprojects' \
+    'mkdir sub1 &&
+    ( cd sub1 && git init && : >Makefile && git add * &&
+    git commit -q -m "subproject 1" ) &&
+    mkdir sub2 &&
+    ( cd sub2 && git init && : >Makefile && git add * &&
+    git commit -q -m "subproject 2" ) &&
+    git update-index --add sub1 &&
+    git add sub2 &&
+    git commit -q -m "subprojects added" &&
+    git diff-tree --abbrev=5 HEAD^ HEAD |cut -d" " -f-3,5- >current &&
+    git diff expected current'
+
+git branch save HEAD
+
+test_expect_success 'check if fsck ignores the subprojects' \
+    'git fsck --full'
+
+test_expect_success 'check if commit in a subproject detected' \
+    '( cd sub1 &&
+    echo "all:" >>Makefile &&
+    echo "     true" >>Makefile &&
+    git commit -q -a -m "make all" ) && {
+        git diff-files --exit-code
+       test $? = 1
+    }'
+
+test_expect_success 'check if a changed subproject HEAD can be committed' \
+    'git commit -q -a -m "sub1 changed" && {
+       git diff-tree --exit-code HEAD^ HEAD
+       test $? = 1
+    }'
+
+test_expect_success 'check if diff-index works for subproject elements' \
+    'git diff-index --exit-code --cached save -- sub1
+    test $? = 1'
+
+test_expect_success 'check if diff-tree works for subproject elements' \
+    'git diff-tree --exit-code HEAD^ HEAD -- sub1
+    test $? = 1'
+
+test_expect_success 'check if git diff works for subproject elements' \
+    'git diff --exit-code HEAD^ HEAD
+    test $? = 1'
+
+test_expect_success 'check if clone works' \
+    'git ls-files -s >expected &&
+    git clone -l -s . cloned &&
+    ( cd cloned && git ls-files -s ) >current &&
+    git diff expected current'
+
+test_expect_success 'removing and adding subproject' \
+    'git update-index --force-remove -- sub2 &&
+    mv sub2 sub3 &&
+    git add sub3 &&
+    git commit -q -m "renaming a subproject" && {
+       git diff -M --name-status --exit-code HEAD^ HEAD
+       test $? = 1
+    }'
+
+# the index must contain the object name the HEAD of the
+# subproject sub1 was at the point "save"
+test_expect_success 'checkout in superproject' \
+    'git checkout save &&
+    git diff-index --exit-code --raw --cached save -- sub1'
+
+# just interesting what happened...
+# git diff --name-status -M save master
+
+test_done
diff --git a/t/t3050-subprojects-fetch.sh b/t/t3050-subprojects-fetch.sh
new file mode 100755 (executable)
index 0000000..34f26a8
--- /dev/null
@@ -0,0 +1,52 @@
+#!/bin/sh
+
+test_description='fetching and pushing project with subproject'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       test_tick &&
+       mkdir -p sub && (
+               cd sub &&
+               git init &&
+               >subfile &&
+               git add subfile
+               git commit -m "subproject commit #1"
+       ) &&
+       >mainfile
+       git add sub mainfile &&
+       test_tick &&
+       git commit -m "superproject commit #1"
+'
+
+test_expect_success clone '
+       git clone file://`pwd`/.git cloned &&
+       (git rev-parse HEAD; git ls-files -s) >expected &&
+       (
+               cd cloned &&
+               (git rev-parse HEAD; git ls-files -s) >../actual
+       ) &&
+       diff -u expected actual
+'
+
+test_expect_success advance '
+       echo more >mainfile &&
+       git update-index --force-remove sub &&
+       mv sub/.git sub/.git-disabled &&
+       git add sub/subfile mainfile &&
+       mv sub/.git-disabled sub/.git &&
+       test_tick &&
+       git commit -m "superproject commit #2"
+'
+
+test_expect_success fetch '
+       (git rev-parse HEAD; git ls-files -s) >expected &&
+       (
+               cd cloned &&
+               git pull &&
+               (git rev-parse HEAD; git ls-files -s) >../actual
+       ) &&
+       diff -u expected actual
+'
+
+test_done
diff --git a/t/t3060-ls-files-with-tree.sh b/t/t3060-ls-files-with-tree.sh
new file mode 100755 (executable)
index 0000000..68eb266
--- /dev/null
@@ -0,0 +1,71 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Carl D. Worth
+#
+
+test_description='git ls-files test (--with-tree).
+
+This test runs git ls-files --with-tree and in particular in
+a scenario known to trigger a crash with some versions of git.
+'
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       # The bug we are exercising requires a fair number of entries
+       # in a sub-directory so that add_index_entry will trigger a
+       # realloc.
+
+       echo file >expected &&
+       mkdir sub &&
+       bad= &&
+       for n in 0 1 2 3 4 5
+       do
+               for m in 0 1 2 3 4 5 6 7 8 9
+               do
+                       num=00$n$m &&
+                       >sub/file-$num &&
+                       echo file-$num >>expected || {
+                               bad=t
+                               break
+                       }
+               done && test -z "$bad" || {
+                       bad=t
+                       break
+               }
+       done && test -z "$bad" &&
+       git add . &&
+       git commit -m "add a bunch of files" &&
+
+       # We remove them all so that we will have something to add
+       # back with --with-tree and so that we will definitely be
+       # under the realloc size to trigger the bug.
+       rm -rf sub &&
+       git commit -a -m "remove them all" &&
+
+       # The bug also requires some entry before our directory so that
+       # prune_path will modify the_index.cache
+
+       mkdir a_directory_that_sorts_before_sub &&
+       >a_directory_that_sorts_before_sub/file &&
+       mkdir sub &&
+       >sub/file &&
+       git add .
+'
+
+# We have to run from a sub-directory to trigger prune_path
+# Then we finally get to run our --with-tree test
+cd sub
+
+test_expect_success 'git -ls-files --with-tree should succeed from subdir' '
+
+       git ls-files --with-tree=HEAD~1 >../output
+
+'
+
+cd ..
+test_expect_success \
+    'git -ls-files --with-tree should add entries from named tree.' \
+    'diff -u expected output'
+
+test_done
diff --git a/t/t3100-ls-tree-restrict.sh b/t/t3100-ls-tree-restrict.sh
new file mode 100755 (executable)
index 0000000..46427e3
--- /dev/null
@@ -0,0 +1,158 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git ls-tree test.
+
+This test runs git ls-tree with the following in a tree.
+
+    path0       - a file
+    path1      - a symlink
+    path2/foo   - a file in a directory
+    path2/bazbo - a symlink in a directory
+    path2/baz/b - a file in a directory in a directory
+
+The new path restriction code should do the right thing for path2 and
+path2/baz.  Also path0/ should snow nothing.
+'
+. ./test-lib.sh
+
+test_expect_success \
+    'setup' \
+    'mkdir path2 path2/baz &&
+     echo Hi >path0 &&
+     ln -s path0 path1 &&
+     echo Lo >path2/foo &&
+     ln -s ../path1 path2/bazbo &&
+     echo Mi >path2/baz/b &&
+     find path? \( -type f -o -type l \) -print |
+     xargs git update-index --add &&
+     tree=`git write-tree` &&
+     echo $tree'
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+test_output () {
+    sed -e "s/ $_x40   / X     /" <current >check
+    git diff expected check
+}
+
+test_expect_success \
+    'ls-tree plain' \
+    'git ls-tree $tree >current &&
+     cat >expected <<\EOF &&
+100644 blob X  path0
+120000 blob X  path1
+040000 tree X  path2
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree recursive' \
+    'git ls-tree -r $tree >current &&
+     cat >expected <<\EOF &&
+100644 blob X  path0
+120000 blob X  path1
+100644 blob X  path2/baz/b
+120000 blob X  path2/bazbo
+100644 blob X  path2/foo
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree recursive with -t' \
+    'git ls-tree -r -t $tree >current &&
+     cat >expected <<\EOF &&
+100644 blob X  path0
+120000 blob X  path1
+040000 tree X  path2
+040000 tree X  path2/baz
+100644 blob X  path2/baz/b
+120000 blob X  path2/bazbo
+100644 blob X  path2/foo
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree recursive with -d' \
+    'git ls-tree -r -d $tree >current &&
+     cat >expected <<\EOF &&
+040000 tree X  path2
+040000 tree X  path2/baz
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree filtered with path' \
+    'git ls-tree $tree path >current &&
+     cat >expected <<\EOF &&
+EOF
+     test_output'
+
+
+# it used to be path1 and then path0, but with pathspec semantics
+# they are shown in canonical order.
+test_expect_success \
+    'ls-tree filtered with path1 path0' \
+    'git ls-tree $tree path1 path0 >current &&
+     cat >expected <<\EOF &&
+100644 blob X  path0
+120000 blob X  path1
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree filtered with path0/' \
+    'git ls-tree $tree path0/ >current &&
+     cat >expected <<\EOF &&
+EOF
+     test_output'
+
+# It used to show path2 and its immediate children but
+# with pathspec semantics it shows only path2
+test_expect_success \
+    'ls-tree filtered with path2' \
+    'git ls-tree $tree path2 >current &&
+     cat >expected <<\EOF &&
+040000 tree X  path2
+EOF
+     test_output'
+
+# ... and path2/ shows the children.
+test_expect_success \
+    'ls-tree filtered with path2/' \
+    'git ls-tree $tree path2/ >current &&
+     cat >expected <<\EOF &&
+040000 tree X  path2/baz
+120000 blob X  path2/bazbo
+100644 blob X  path2/foo
+EOF
+     test_output'
+
+# The same change -- exact match does not show children of
+# path2/baz
+test_expect_success \
+    'ls-tree filtered with path2/baz' \
+    'git ls-tree $tree path2/baz >current &&
+     cat >expected <<\EOF &&
+040000 tree X  path2/baz
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree filtered with path2/bak' \
+    'git ls-tree $tree path2/bak >current &&
+     cat >expected <<\EOF &&
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree -t filtered with path2/bak' \
+    'git ls-tree -t $tree path2/bak >current &&
+     cat >expected <<\EOF &&
+040000 tree X  path2
+EOF
+     test_output'
+
+test_done
diff --git a/t/t3101-ls-tree-dirname.sh b/t/t3101-ls-tree-dirname.sh
new file mode 100755 (executable)
index 0000000..39fe267
--- /dev/null
@@ -0,0 +1,138 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+# Copyright (c) 2005 Robert Fitzsimons
+#
+
+test_description='git ls-tree directory and filenames handling.
+
+This test runs git ls-tree with the following in a tree.
+
+    1.txt              - a file
+    2.txt              - a file
+    path0/a/b/c/1.txt  - a file in a directory
+    path1/b/c/1.txt    - a file in a directory
+    path2/1.txt        - a file in a directory
+    path3/1.txt        - a file in a directory
+    path3/2.txt        - a file in a directory
+
+Test the handling of mulitple directories which have matching file
+entries.  Also test odd filename and missing entries handling.
+'
+. ./test-lib.sh
+
+test_expect_success \
+    'setup' \
+    'echo 111 >1.txt &&
+     echo 222 >2.txt &&
+     mkdir path0 path0/a path0/a/b path0/a/b/c &&
+     echo 111 >path0/a/b/c/1.txt &&
+     mkdir path1 path1/b path1/b/c &&
+     echo 111 >path1/b/c/1.txt &&
+     mkdir path2 &&
+     echo 111 >path2/1.txt &&
+     mkdir path3 &&
+     echo 111 >path3/1.txt &&
+     echo 222 >path3/2.txt &&
+     find *.txt path* \( -type f -o -type l \) -print |
+     xargs git update-index --add &&
+     tree=`git write-tree` &&
+     echo $tree'
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+test_output () {
+    sed -e "s/ $_x40   / X     /" <current >check
+    git diff expected check
+}
+
+test_expect_success \
+    'ls-tree plain' \
+    'git ls-tree $tree >current &&
+     cat >expected <<\EOF &&
+100644 blob X  1.txt
+100644 blob X  2.txt
+040000 tree X  path0
+040000 tree X  path1
+040000 tree X  path2
+040000 tree X  path3
+EOF
+     test_output'
+
+# Recursive does not show tree nodes anymore...
+test_expect_success \
+    'ls-tree recursive' \
+    'git ls-tree -r $tree >current &&
+     cat >expected <<\EOF &&
+100644 blob X  1.txt
+100644 blob X  2.txt
+100644 blob X  path0/a/b/c/1.txt
+100644 blob X  path1/b/c/1.txt
+100644 blob X  path2/1.txt
+100644 blob X  path3/1.txt
+100644 blob X  path3/2.txt
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree filter 1.txt' \
+    'git ls-tree $tree 1.txt >current &&
+     cat >expected <<\EOF &&
+100644 blob X  1.txt
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree filter path1/b/c/1.txt' \
+    'git ls-tree $tree path1/b/c/1.txt >current &&
+     cat >expected <<\EOF &&
+100644 blob X  path1/b/c/1.txt
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree filter all 1.txt files' \
+    'git ls-tree $tree 1.txt path0/a/b/c/1.txt path1/b/c/1.txt path2/1.txt path3/1.txt >current &&
+     cat >expected <<\EOF &&
+100644 blob X  1.txt
+100644 blob X  path0/a/b/c/1.txt
+100644 blob X  path1/b/c/1.txt
+100644 blob X  path2/1.txt
+100644 blob X  path3/1.txt
+EOF
+     test_output'
+
+# I am not so sure about this one after ls-tree doing pathspec match.
+# Having both path0/a and path0/a/b/c makes path0/a redundant, and
+# it behaves as if path0/a/b/c, path1/b/c, path2 and path3 are specified.
+test_expect_success \
+    'ls-tree filter directories' \
+    'git ls-tree $tree path3 path2 path0/a/b/c path1/b/c path0/a >current &&
+     cat >expected <<\EOF &&
+040000 tree X  path0/a/b/c
+040000 tree X  path1/b/c
+040000 tree X  path2
+040000 tree X  path3
+EOF
+     test_output'
+
+# Again, duplicates are filtered away so this is equivalent to
+# having 1.txt and path3
+test_expect_success \
+    'ls-tree filter odd names' \
+    'git ls-tree $tree 1.txt /1.txt //1.txt path3/1.txt /path3/1.txt //path3//1.txt path3 /path3/ path3// >current &&
+     cat >expected <<\EOF &&
+100644 blob X  1.txt
+100644 blob X  path3/1.txt
+100644 blob X  path3/2.txt
+EOF
+     test_output'
+
+test_expect_success \
+    'ls-tree filter missing files and extra slashes' \
+    'git ls-tree $tree 1.txt/ abc.txt path3//23.txt path3/2.txt/// >current &&
+     cat >expected <<\EOF &&
+EOF
+     test_output'
+
+test_done
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
new file mode 100755 (executable)
index 0000000..ef1eeb7
--- /dev/null
@@ -0,0 +1,205 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Amos Waterland
+#
+
+test_description='git branch --foo should not create bogus branch
+
+This test runs git branch --help and checks that the argument is properly
+handled.  Specifically, that a bogus branch is not created.
+'
+. ./test-lib.sh
+
+test_expect_success \
+    'prepare a trivial repository' \
+    'echo Hello > A &&
+     git update-index --add A &&
+     git-commit -m "Initial commit." &&
+     HEAD=$(git rev-parse --verify HEAD)'
+
+test_expect_failure \
+    'git branch --help should not have created a bogus branch' \
+    'git branch --help </dev/null >/dev/null 2>/dev/null || :
+     test -f .git/refs/heads/--help'
+
+test_expect_success \
+    'git branch abc should create a branch' \
+    'git branch abc && test -f .git/refs/heads/abc'
+
+test_expect_success \
+    'git branch a/b/c should create a branch' \
+    'git branch a/b/c && test -f .git/refs/heads/a/b/c'
+
+cat >expect <<EOF
+0000000000000000000000000000000000000000 $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000     branch: Created from master
+EOF
+test_expect_success \
+    'git branch -l d/e/f should create a branch and a log' \
+       'GIT_COMMITTER_DATE="2005-05-26 23:30" \
+     git branch -l d/e/f &&
+        test -f .git/refs/heads/d/e/f &&
+        test -f .git/logs/refs/heads/d/e/f &&
+        diff expect .git/logs/refs/heads/d/e/f'
+
+test_expect_success \
+    'git branch -d d/e/f should delete a branch and a log' \
+       'git branch -d d/e/f &&
+        test ! -f .git/refs/heads/d/e/f &&
+        test ! -f .git/logs/refs/heads/d/e/f'
+
+test_expect_success \
+    'git branch j/k should work after branch j has been deleted' \
+       'git branch j &&
+        git branch -d j &&
+        git branch j/k'
+
+test_expect_success \
+    'git branch l should work after branch l/m has been deleted' \
+       'git branch l/m &&
+        git branch -d l/m &&
+        git branch l'
+
+test_expect_success \
+    'git branch -m m m/m should work' \
+       'git branch -l m &&
+        git branch -m m m/m &&
+        test -f .git/logs/refs/heads/m/m'
+
+test_expect_success \
+    'git branch -m n/n n should work' \
+       'git branch -l n/n &&
+        git branch -m n/n n
+        test -f .git/logs/refs/heads/n'
+
+test_expect_failure \
+    'git branch -m o/o o should fail when o/p exists' \
+       'git branch o/o &&
+        git branch o/p &&
+        git branch -m o/o o'
+
+test_expect_failure \
+    'git branch -m q r/q should fail when r exists' \
+       'git branch q &&
+         git branch r &&
+         git branch -m q r/q'
+
+mv .git/config .git/config-saved
+
+test_expect_success 'git branch -m q q2 without config should succeed' '
+       git branch -m q q2 &&
+       git branch -m q2 q
+'
+
+mv .git/config-saved .git/config
+
+git config branch.s/s.dummy Hello
+
+test_expect_success \
+    'git branch -m s/s s should work when s/t is deleted' \
+       'git branch -l s/s &&
+        test -f .git/logs/refs/heads/s/s &&
+        git branch -l s/t &&
+        test -f .git/logs/refs/heads/s/t &&
+        git branch -d s/t &&
+        git branch -m s/s s &&
+        test -f .git/logs/refs/heads/s'
+
+test_expect_success 'config information was renamed, too' \
+       "test $(git config branch.s.dummy) = Hello &&
+        ! git config branch.s/s/dummy"
+
+test_expect_failure \
+    'git branch -m u v should fail when the reflog for u is a symlink' \
+    'git branch -l u &&
+     mv .git/logs/refs/heads/u real-u &&
+     ln -s real-u .git/logs/refs/heads/u &&
+     git branch -m u v'
+
+test_expect_success 'test tracking setup via --track' \
+    'git config remote.local.url . &&
+     git config remote.local.fetch refs/heads/*:refs/remotes/local/* &&
+     (git show-ref -q refs/remotes/local/master || git-fetch local) &&
+     git branch --track my1 local/master &&
+     test $(git config branch.my1.remote) = local &&
+     test $(git config branch.my1.merge) = refs/heads/master'
+
+test_expect_success 'test tracking setup (non-wildcard, matching)' \
+    'git config remote.local.url . &&
+     git config remote.local.fetch refs/heads/master:refs/remotes/local/master &&
+     (git show-ref -q refs/remotes/local/master || git-fetch local) &&
+     git branch --track my4 local/master &&
+     test $(git config branch.my4.remote) = local &&
+     test $(git config branch.my4.merge) = refs/heads/master'
+
+test_expect_success 'test tracking setup (non-wildcard, not matching)' \
+    'git config remote.local.url . &&
+     git config remote.local.fetch refs/heads/s:refs/remotes/local/s &&
+     (git show-ref -q refs/remotes/local/master || git-fetch local) &&
+     git branch --track my5 local/master &&
+     ! test "$(git config branch.my5.remote)" = local &&
+     ! test "$(git config branch.my5.merge)" = refs/heads/master'
+
+test_expect_success 'test tracking setup via config' \
+    'git config branch.autosetupmerge true &&
+     git config remote.local.url . &&
+     git config remote.local.fetch refs/heads/*:refs/remotes/local/* &&
+     (git show-ref -q refs/remotes/local/master || git-fetch local) &&
+     git branch my3 local/master &&
+     test $(git config branch.my3.remote) = local &&
+     test $(git config branch.my3.merge) = refs/heads/master'
+
+test_expect_success 'avoid ambiguous track' '
+       git config branch.autosetupmerge true &&
+       git config remote.ambi1.url = lalala &&
+       git config remote.ambi1.fetch = refs/heads/lalala:refs/heads/master &&
+       git config remote.ambi2.url = lilili &&
+       git config remote.ambi2.fetch = refs/heads/lilili:refs/heads/master &&
+       git branch all1 master &&
+       test -z "$(git config branch.all1.merge)"
+'
+
+test_expect_success 'test overriding tracking setup via --no-track' \
+    'git config branch.autosetupmerge true &&
+     git config remote.local.url . &&
+     git config remote.local.fetch refs/heads/*:refs/remotes/local/* &&
+     (git show-ref -q refs/remotes/local/master || git-fetch local) &&
+     git branch --no-track my2 local/master &&
+     git config branch.autosetupmerge false &&
+     ! test "$(git config branch.my2.remote)" = local &&
+     ! test "$(git config branch.my2.merge)" = refs/heads/master'
+
+test_expect_success 'no tracking without .fetch entries' \
+    'git branch --track my6 s &&
+     test -z "$(git config branch.my6.remote)" &&
+     test -z "$(git config branch.my6.merge)"'
+
+test_expect_success 'test tracking setup via --track but deeper' \
+    'git config remote.local.url . &&
+     git config remote.local.fetch refs/heads/*:refs/remotes/local/* &&
+     (git show-ref -q refs/remotes/local/o/o || git-fetch local) &&
+     git branch --track my7 local/o/o &&
+     test "$(git config branch.my7.remote)" = local &&
+     test "$(git config branch.my7.merge)" = refs/heads/o/o'
+
+test_expect_success 'test deleting branch deletes branch config' \
+    'git branch -d my7 &&
+     test -z "$(git config branch.my7.remote)" &&
+     test -z "$(git config branch.my7.merge)"'
+
+test_expect_success 'test deleting branch without config' \
+    'git branch my7 s &&
+     test "$(git branch -d my7 2>&1)" = "Deleted branch my7."'
+
+# Keep this test last, as it changes the current branch
+cat >expect <<EOF
+0000000000000000000000000000000000000000 $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000     branch: Created from master
+EOF
+test_expect_success \
+    'git checkout -b g/h/i -l should create a branch and a log' \
+       'GIT_COMMITTER_DATE="2005-05-26 23:30" \
+     git-checkout -b g/h/i -l master &&
+        test -f .git/refs/heads/g/h/i &&
+        test -f .git/logs/refs/heads/g/h/i &&
+        diff expect .git/logs/refs/heads/g/h/i'
+
+test_done
diff --git a/t/t3201-branch-contains.sh b/t/t3201-branch-contains.sh
new file mode 100755 (executable)
index 0000000..9ef593f
--- /dev/null
@@ -0,0 +1,58 @@
+#!/bin/sh
+
+test_description='branch --contains <commit>'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       >file &&
+       git add file &&
+       test_tick &&
+       git commit -m initial &&
+       git branch side &&
+
+       echo 1 >file &&
+       test_tick &&
+       git commit -a -m "second on master" &&
+
+       git checkout side &&
+       echo 1 >file &&
+       test_tick &&
+       git commit -a -m "second on side" &&
+
+       git merge master
+
+'
+
+test_expect_success 'branch --contains=master' '
+
+       git branch --contains=master >actual &&
+       {
+               echo "  master" && echo "* side"
+       } >expect &&
+       diff -u expect actual
+
+'
+
+test_expect_success 'branch --contains master' '
+
+       git branch --contains master >actual &&
+       {
+               echo "  master" && echo "* side"
+       } >expect &&
+       diff -u expect actual
+
+'
+
+test_expect_success 'branch --contains=side' '
+
+       git branch --contains=side >actual &&
+       {
+               echo "* side"
+       } >expect &&
+       diff -u expect actual
+
+'
+
+test_done
diff --git a/t/t3210-pack-refs.sh b/t/t3210-pack-refs.sh
new file mode 100755 (executable)
index 0000000..4ddc634
--- /dev/null
@@ -0,0 +1,108 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Amos Waterland
+# Copyright (c) 2006 Christian Couder
+#
+
+test_description='git pack-refs should not change the branch semantic
+
+This test runs git pack-refs and git show-ref and checks that the branch
+semantic is still the same.
+'
+. ./test-lib.sh
+
+echo '[core] logallrefupdates = true' >>.git/config
+
+test_expect_success \
+    'prepare a trivial repository' \
+    'echo Hello > A &&
+     git update-index --add A &&
+     git-commit -m "Initial commit." &&
+     HEAD=$(git rev-parse --verify HEAD)'
+
+SHA1=
+
+test_expect_success \
+    'see if git show-ref works as expected' \
+    'git branch a &&
+     SHA1=`cat .git/refs/heads/a` &&
+     echo "$SHA1 refs/heads/a" >expect &&
+     git show-ref a >result &&
+     diff expect result'
+
+test_expect_success \
+    'see if a branch still exists when packed' \
+    'git branch b &&
+     git pack-refs --all &&
+     rm -f .git/refs/heads/b &&
+     echo "$SHA1 refs/heads/b" >expect &&
+     git show-ref b >result &&
+     diff expect result'
+
+test_expect_failure \
+    'git branch c/d should barf if branch c exists' \
+    'git branch c &&
+     git pack-refs --all &&
+     rm .git/refs/heads/c &&
+     git branch c/d'
+
+test_expect_success \
+    'see if a branch still exists after git pack-refs --prune' \
+    'git branch e &&
+     git pack-refs --all --prune &&
+     echo "$SHA1 refs/heads/e" >expect &&
+     git show-ref e >result &&
+     diff expect result'
+
+test_expect_failure \
+    'see if git pack-refs --prune remove ref files' \
+    'git branch f &&
+     git pack-refs --all --prune &&
+     ls .git/refs/heads/f'
+
+test_expect_success \
+    'git branch g should work when git branch g/h has been deleted' \
+    'git branch g/h &&
+     git pack-refs --all --prune &&
+     git branch -d g/h &&
+     git branch g &&
+     git pack-refs --all &&
+     git branch -d g'
+
+test_expect_failure \
+    'git branch i/j/k should barf if branch i exists' \
+    'git branch i &&
+     git pack-refs --all --prune &&
+     git branch i/j/k'
+
+test_expect_success \
+    'test git branch k after branch k/l/m and k/lm have been deleted' \
+    'git branch k/l &&
+     git branch k/lm &&
+     git branch -d k/l &&
+     git branch k/l/m &&
+     git branch -d k/l/m &&
+     git branch -d k/lm &&
+     git branch k'
+
+test_expect_success \
+    'test git branch n after some branch deletion and pruning' \
+    'git branch n/o &&
+     git branch n/op &&
+     git branch -d n/o &&
+     git branch n/o/p &&
+     git branch -d n/op &&
+     git pack-refs --all --prune &&
+     git branch -d n/o/p &&
+     git branch n'
+
+test_expect_success 'pack, prune and repack' '
+       git-tag foo &&
+       git pack-refs --all --prune &&
+       git show-ref >all-of-them &&
+       git pack-refs &&
+       git show-ref >again &&
+       diff all-of-them again
+'
+
+test_done
diff --git a/t/t3300-funny-names.sh b/t/t3300-funny-names.sh
new file mode 100755 (executable)
index 0000000..dc8c369
--- /dev/null
@@ -0,0 +1,160 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Pathnames with funny characters.
+
+This test tries pathnames with funny characters in the working
+tree, index, and tree objects.
+'
+
+. ./test-lib.sh
+
+p0='no-funny'
+p1='tabs       ," (dq) and spaces'
+p2='just space'
+
+cat >"$p0" <<\EOF
+1. A quick brown fox jumps over the lazy cat, oops dog.
+2. A quick brown fox jumps over the lazy cat, oops dog.
+3. A quick brown fox jumps over the lazy cat, oops dog.
+EOF
+
+cat >"$p1" "$p0"
+echo 'Foo Bar Baz' >"$p2"
+
+test -f "$p1" && cmp "$p0" "$p1" || {
+       # since FAT/NTFS does not allow tabs in filenames, skip this test
+       say 'Your filesystem does not allow tabs in filenames, test skipped.'
+       test_done
+}
+
+echo 'just space
+no-funny' >expected
+test_expect_success 'git ls-files no-funny' \
+       'git update-index --add "$p0" "$p2" &&
+       git ls-files >current &&
+       git diff expected current'
+
+t0=`git write-tree`
+echo "$t0" >t0
+
+cat > expected <<\EOF
+just space
+no-funny
+"tabs\t,\" (dq) and spaces"
+EOF
+test_expect_success 'git ls-files with-funny' \
+       'git update-index --add "$p1" &&
+       git ls-files >current &&
+       git diff expected current'
+
+echo 'just space
+no-funny
+tabs   ," (dq) and spaces' >expected
+test_expect_success 'git ls-files -z with-funny' \
+       'git ls-files -z | tr \\0 \\012 >current &&
+       git diff expected current'
+
+t1=`git write-tree`
+echo "$t1" >t1
+
+cat > expected <<\EOF
+just space
+no-funny
+"tabs\t,\" (dq) and spaces"
+EOF
+test_expect_success 'git ls-tree with funny' \
+       'git ls-tree -r $t1 | sed -e "s/^[^     ]*      //" >current &&
+        git diff expected current'
+
+cat > expected <<\EOF
+A      "tabs\t,\" (dq) and spaces"
+EOF
+test_expect_success 'git diff-index with-funny' \
+       'git diff-index --name-status $t0 >current &&
+       git diff expected current'
+
+test_expect_success 'git diff-tree with-funny' \
+       'git diff-tree --name-status $t0 $t1 >current &&
+       git diff expected current'
+
+echo 'A
+tabs   ," (dq) and spaces' >expected
+test_expect_success 'git diff-index -z with-funny' \
+       'git diff-index -z --name-status $t0 | tr \\0 \\012 >current &&
+       git diff expected current'
+
+test_expect_success 'git diff-tree -z with-funny' \
+       'git diff-tree -z --name-status $t0 $t1 | tr \\0 \\012 >current &&
+       git diff expected current'
+
+cat > expected <<\EOF
+CNUM   no-funny        "tabs\t,\" (dq) and spaces"
+EOF
+test_expect_success 'git diff-tree -C with-funny' \
+       'git diff-tree -C --find-copies-harder --name-status \
+               $t0 $t1 | sed -e 's/^C[0-9]*/CNUM/' >current &&
+       git diff expected current'
+
+cat > expected <<\EOF
+RNUM   no-funny        "tabs\t,\" (dq) and spaces"
+EOF
+test_expect_success 'git diff-tree delete with-funny' \
+       'git update-index --force-remove "$p0" &&
+       git diff-index -M --name-status \
+               $t0 | sed -e 's/^R[0-9]*/RNUM/' >current &&
+       git diff expected current'
+
+cat > expected <<\EOF
+diff --git a/no-funny "b/tabs\t,\" (dq) and spaces"
+similarity index NUM%
+rename from no-funny
+rename to "tabs\t,\" (dq) and spaces"
+EOF
+test_expect_success 'git diff-tree delete with-funny' \
+       'git diff-index -M -p $t0 |
+        sed -e "s/index [0-9]*%/index NUM%/" >current &&
+        git diff expected current'
+
+chmod +x "$p1"
+cat > expected <<\EOF
+diff --git a/no-funny "b/tabs\t,\" (dq) and spaces"
+old mode 100644
+new mode 100755
+similarity index NUM%
+rename from no-funny
+rename to "tabs\t,\" (dq) and spaces"
+EOF
+test_expect_success 'git diff-tree delete with-funny' \
+       'git diff-index -M -p $t0 |
+        sed -e "s/index [0-9]*%/index NUM%/" >current &&
+        git diff expected current'
+
+cat >expected <<\EOF
+ "tabs\t,\" (dq) and spaces"
+ 1 files changed, 0 insertions(+), 0 deletions(-)
+EOF
+test_expect_success 'git diff-tree rename with-funny applied' \
+       'git diff-index -M -p $t0 |
+        git apply --stat | sed -e "s/|.*//" -e "s/ *\$//" >current &&
+        git diff expected current'
+
+cat > expected <<\EOF
+ no-funny
+ "tabs\t,\" (dq) and spaces"
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+EOF
+test_expect_success 'git diff-tree delete with-funny applied' \
+       'git diff-index -p $t0 |
+        git apply --stat | sed -e "s/|.*//" -e "s/ *\$//" >current &&
+        git diff expected current'
+
+test_expect_success 'git apply non-git diff' \
+       'git diff-index -p $t0 |
+        sed -ne "/^[-+@]/p" |
+        git apply --stat | sed -e "s/|.*//" -e "s/ *\$//" >current &&
+        git diff expected current'
+
+test_done
diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh
new file mode 100755 (executable)
index 0000000..95e33b5
--- /dev/null
@@ -0,0 +1,86 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Amos Waterland
+#
+
+test_description='git rebase should not destroy author information
+
+This test runs git rebase and checks that the author information is not lost.
+'
+. ./test-lib.sh
+
+export GIT_AUTHOR_EMAIL=bogus_email_address
+
+test_expect_success \
+    'prepare repository with topic branches' \
+    'echo First > A &&
+     git update-index --add A &&
+     git-commit -m "Add A." &&
+     git checkout -b my-topic-branch &&
+     echo Second > B &&
+     git update-index --add B &&
+     git-commit -m "Add B." &&
+     git checkout -f master &&
+     echo Third >> A &&
+     git update-index A &&
+     git-commit -m "Modify A." &&
+     git checkout -b side my-topic-branch &&
+     echo Side >> C &&
+     git add C &&
+     git commit -m "Add C" &&
+     git checkout -b nonlinear my-topic-branch &&
+     echo Edit >> B &&
+     git add B &&
+     git commit -m "Modify B" &&
+     git merge side &&
+     git checkout -b upstream-merged-nonlinear &&
+     git merge master &&
+     git checkout -f my-topic-branch &&
+     git tag topic
+'
+
+test_expect_success 'rebase against master' '
+     git rebase master'
+
+test_expect_failure \
+    'the rebase operation should not have destroyed author information' \
+    'git log | grep "Author:" | grep "<>"'
+
+test_expect_success 'rebase after merge master' '
+     git reset --hard topic &&
+     git merge master &&
+     git rebase master &&
+     ! git show | grep "^Merge:"
+'
+
+test_expect_success 'rebase of history with merges is linearized' '
+     git checkout nonlinear &&
+     test 4 = $(git rev-list master.. | wc -l) &&
+     git rebase master &&
+     test 3 = $(git rev-list master.. | wc -l)
+'
+
+test_expect_success \
+    'rebase of history with merges after upstream merge is linearized' '
+     git checkout upstream-merged-nonlinear &&
+     test 5 = $(git rev-list master.. | wc -l) &&
+     git rebase master &&
+     test 3 = $(git rev-list master.. | wc -l)
+'
+
+test_expect_success 'rebase a single mode change' '
+     git checkout master &&
+     echo 1 > X &&
+     git add X &&
+     test_tick &&
+     git commit -m prepare &&
+     git checkout -b modechange HEAD^ &&
+     echo 1 > X &&
+     git add X &&
+     chmod a+x A &&
+     test_tick &&
+     git commit -m modechange A X &&
+     GIT_TRACE=1 git rebase master
+'
+
+test_done
diff --git a/t/t3401-rebase-partial.sh b/t/t3401-rebase-partial.sh
new file mode 100755 (executable)
index 0000000..4934a4e
--- /dev/null
@@ -0,0 +1,61 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Yann Dirson, based on t3400 by Amos Waterland
+#
+
+test_description='git rebase should detect patches integrated upstream
+
+This test cherry-picks one local change of two into master branch, and
+checks that git rebase succeeds with only the second patch in the
+local branch.
+'
+. ./test-lib.sh
+
+test_expect_success \
+    'prepare repository with topic branch' \
+    'echo First > A &&
+     git update-index --add A &&
+     git-commit -m "Add A." &&
+
+     git-checkout -b my-topic-branch &&
+
+     echo Second > B &&
+     git update-index --add B &&
+     git-commit -m "Add B." &&
+
+     echo AnotherSecond > C &&
+     git update-index --add C &&
+     git-commit -m "Add C." &&
+
+     git-checkout -f master &&
+
+     echo Third >> A &&
+     git update-index A &&
+     git-commit -m "Modify A."
+'
+
+test_expect_success \
+    'pick top patch from topic branch into master' \
+    'git cherry-pick my-topic-branch^0 &&
+     git-checkout -f my-topic-branch &&
+     git branch master-merge master &&
+     git branch my-topic-branch-merge my-topic-branch
+'
+
+test_debug \
+    'git cherry master &&
+     git format-patch -k --stdout --full-index master >/dev/null &&
+     gitk --all & sleep 1
+'
+
+test_expect_success \
+    'rebase topic branch against new master and check git-am did not get halted' \
+    'git-rebase master && test ! -d .dotest'
+
+test_expect_success \
+       'rebase --merge topic branch that was partially merged upstream' \
+       'git-checkout -f my-topic-branch-merge &&
+        git-rebase --merge master-merge &&
+        test ! -d .git/.dotest-merge'
+
+test_done
diff --git a/t/t3402-rebase-merge.sh b/t/t3402-rebase-merge.sh
new file mode 100755 (executable)
index 0000000..7b7d072
--- /dev/null
@@ -0,0 +1,111 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='git rebase --merge test'
+
+. ./test-lib.sh
+
+T="A quick brown fox
+jumps over the lazy dog."
+for i in 1 2 3 4 5 6 7 8 9 10
+do
+       echo "$i $T"
+done >original
+
+test_expect_success setup '
+       git add original &&
+       git commit -m"initial" &&
+       git branch side &&
+       echo "11 $T" >>original &&
+       git commit -a -m"master updates a bit." &&
+
+       echo "12 $T" >>original &&
+       git commit -a -m"master updates a bit more." &&
+
+       git checkout side &&
+       (echo "0 $T" ; cat original) >renamed &&
+       git add renamed &&
+       git update-index --force-remove original &&
+       git commit -a -m"side renames and edits." &&
+
+       tr "[a-z]" "[A-Z]" <original >newfile &&
+       git add newfile &&
+       git commit -a -m"side edits further." &&
+
+       tr "[a-m]" "[A-M]" <original >newfile &&
+       rm -f original &&
+       git commit -a -m"side edits once again." &&
+
+       git branch test-rebase side &&
+       git branch test-rebase-pick side &&
+       git branch test-reference-pick side &&
+       git checkout -b test-merge side
+'
+
+test_expect_success 'reference merge' '
+       git merge -s recursive "reference merge" HEAD master
+'
+
+PRE_REBASE=$(git rev-parse test-rebase)
+test_expect_success rebase '
+       git checkout test-rebase &&
+       GIT_TRACE=1 git rebase --merge master
+'
+
+test_expect_success 'test-rebase@{1} is pre rebase' '
+       test $PRE_REBASE = $(git rev-parse test-rebase@{1})
+'
+
+test_expect_success 'merge and rebase should match' '
+       git diff-tree -r test-rebase test-merge >difference &&
+       if test -s difference
+       then
+               cat difference
+               (exit 1)
+       else
+               echo happy
+       fi
+'
+
+test_expect_success 'rebase the other way' '
+       git reset --hard master &&
+       git rebase --merge side
+'
+
+test_expect_success 'merge and rebase should match' '
+       git diff-tree -r test-rebase test-merge >difference &&
+       if test -s difference
+       then
+               cat difference
+               (exit 1)
+       else
+               echo happy
+       fi
+'
+
+test_expect_success 'picking rebase' '
+       git reset --hard side &&
+       git rebase --merge --onto master side^^ &&
+       mb=$(git merge-base master HEAD) &&
+       if test "$mb" = "$(git rev-parse master)"
+       then
+               echo happy
+       else
+               git show-branch
+               (exit 1)
+       fi &&
+       f=$(git diff-tree --name-only HEAD^ HEAD) &&
+       g=$(git diff-tree --name-only HEAD^^ HEAD^) &&
+       case "$f,$g" in
+       newfile,newfile)
+               echo happy ;;
+       *)
+               echo "$f"
+               echo "$g"
+               (exit 1)
+       esac
+'
+
+test_done
diff --git a/t/t3403-rebase-skip.sh b/t/t3403-rebase-skip.sh
new file mode 100755 (executable)
index 0000000..657f681
--- /dev/null
@@ -0,0 +1,71 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+#
+
+test_description='git rebase --merge --skip tests'
+
+. ./test-lib.sh
+
+# we assume the default git-am -3 --skip strategy is tested independently
+# and always works :)
+
+test_expect_success setup '
+       echo hello > hello &&
+       git add hello &&
+       git commit -m "hello" &&
+       git branch skip-reference &&
+
+       echo world >> hello &&
+       git commit -a -m "hello world" &&
+       echo goodbye >> hello &&
+       git commit -a -m "goodbye" &&
+
+       git checkout -f skip-reference &&
+       echo moo > hello &&
+       git commit -a -m "we should skip this" &&
+       echo moo > cow &&
+       git add cow &&
+       git commit -m "this should not be skipped" &&
+       git branch pre-rebase skip-reference &&
+       git branch skip-merge skip-reference
+       '
+
+test_expect_failure 'rebase with git am -3 (default)' '
+       git rebase master
+'
+
+test_expect_success 'rebase --skip with am -3' '
+       git rebase --skip
+       '
+
+test_expect_success 'rebase moves back to skip-reference' '
+       test refs/heads/skip-reference = $(git symbolic-ref HEAD) &&
+       git branch post-rebase &&
+       git reset --hard pre-rebase &&
+       ! git rebase master &&
+       echo "hello" > hello &&
+       git add hello &&
+       git rebase --continue &&
+       test refs/heads/skip-reference = $(git symbolic-ref HEAD) &&
+       git reset --hard post-rebase
+'
+
+test_expect_success 'checkout skip-merge' 'git checkout -f skip-merge'
+
+test_expect_failure 'rebase with --merge' 'git rebase --merge master'
+
+test_expect_success 'rebase --skip with --merge' '
+       git rebase --skip
+       '
+
+test_expect_success 'merge and reference trees equal' \
+       'test -z "`git diff-tree skip-merge skip-reference`"'
+
+test_expect_success 'moved back to branch correctly' '
+       test refs/heads/skip-merge = $(git symbolic-ref HEAD)
+'
+
+test_debug 'gitk --all & sleep 1'
+
+test_done
diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
new file mode 100755 (executable)
index 0000000..907c7f9
--- /dev/null
@@ -0,0 +1,321 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes E. Schindelin
+#
+
+test_description='git rebase interactive
+
+This test runs git rebase "interactively", by faking an edit, and verifies
+that the result still makes sense.
+'
+. ./test-lib.sh
+
+# set up two branches like this:
+#
+# A - B - C - D - E
+#   \
+#     F - G - H
+#       \
+#         I
+#
+# where B, D and G touch the same file.
+
+test_expect_success 'setup' '
+       : > file1 &&
+       git add file1 &&
+       test_tick &&
+       git commit -m A &&
+       git tag A &&
+       echo 1 > file1 &&
+       test_tick &&
+       git commit -m B file1 &&
+       : > file2 &&
+       git add file2 &&
+       test_tick &&
+       git commit -m C &&
+       echo 2 > file1 &&
+       test_tick &&
+       git commit -m D file1 &&
+       : > file3 &&
+       git add file3 &&
+       test_tick &&
+       git commit -m E &&
+       git checkout -b branch1 A &&
+       : > file4 &&
+       git add file4 &&
+       test_tick &&
+       git commit -m F &&
+       git tag F &&
+       echo 3 > file1 &&
+       test_tick &&
+       git commit -m G file1 &&
+       : > file5 &&
+       git add file5 &&
+       test_tick &&
+       git commit -m H &&
+       git checkout -b branch2 F &&
+       : > file6 &&
+       git add file6 &&
+       test_tick &&
+       git commit -m I &&
+       git tag I
+'
+
+cat > fake-editor.sh <<\EOF
+#!/bin/sh
+case "$1" in
+*/COMMIT_EDITMSG)
+       test -z "$FAKE_COMMIT_MESSAGE" || echo "$FAKE_COMMIT_MESSAGE" > "$1"
+       test -z "$FAKE_COMMIT_AMEND" || echo "$FAKE_COMMIT_AMEND" >> "$1"
+       exit
+       ;;
+esac
+test -z "$EXPECT_COUNT" ||
+       test "$EXPECT_COUNT" = $(sed -e '/^#/d' -e '/^$/d' < "$1" | wc -l) ||
+       exit
+test -z "$FAKE_LINES" && exit
+grep -v '^#' < "$1" > "$1".tmp
+rm -f "$1"
+cat "$1".tmp
+action=pick
+for line in $FAKE_LINES; do
+       case $line in
+       squash|edit)
+               action="$line";;
+       *)
+               echo sed -n "${line}s/^pick/$action/p"
+               sed -n "${line}p" < "$1".tmp
+               sed -n "${line}s/^pick/$action/p" < "$1".tmp >> "$1"
+               action=pick;;
+       esac
+done
+EOF
+
+chmod a+x fake-editor.sh
+VISUAL="$(pwd)/fake-editor.sh"
+export VISUAL
+
+test_expect_success 'no changes are a nop' '
+       git rebase -i F &&
+       test $(git rev-parse I) = $(git rev-parse HEAD)
+'
+
+test_expect_success 'test the [branch] option' '
+       git checkout -b dead-end &&
+       git rm file6 &&
+       git commit -m "stop here" &&
+       git rebase -i F branch2 &&
+       test $(git rev-parse I) = $(git rev-parse HEAD)
+'
+
+test_expect_success 'rebase on top of a non-conflicting commit' '
+       git checkout branch1 &&
+       git tag original-branch1 &&
+       git rebase -i branch2 &&
+       test file6 = $(git diff --name-only original-branch1) &&
+       test $(git rev-parse I) = $(git rev-parse HEAD~2)
+'
+
+test_expect_success 'reflog for the branch shows state before rebase' '
+       test $(git rev-parse branch1@{1}) = $(git rev-parse original-branch1)
+'
+
+test_expect_success 'exchange two commits' '
+       FAKE_LINES="2 1" git rebase -i HEAD~2 &&
+       test H = $(git cat-file commit HEAD^ | tail -n 1) &&
+       test G = $(git cat-file commit HEAD | tail -n 1)
+'
+
+cat > expect << EOF
+diff --git a/file1 b/file1
+index e69de29..00750ed 100644
+--- a/file1
++++ b/file1
+@@ -0,0 +1 @@
++3
+EOF
+
+cat > expect2 << EOF
+<<<<<<< HEAD:file1
+2
+=======
+3
+>>>>>>> b7ca976... G:file1
+EOF
+
+test_expect_success 'stop on conflicting pick' '
+       git tag new-branch1 &&
+       ! git rebase -i master &&
+       diff -u expect .git/.dotest-merge/patch &&
+       diff -u expect2 file1 &&
+       test 4 = $(grep -v "^#" < .git/.dotest-merge/done | wc -l) &&
+       test 0 = $(grep -ve "^#" -e "^$" < .git/.dotest-merge/git-rebase-todo |
+               wc -l)
+'
+
+test_expect_success 'abort' '
+       git rebase --abort &&
+       test $(git rev-parse new-branch1) = $(git rev-parse HEAD) &&
+       ! test -d .git/.dotest-merge
+'
+
+test_expect_success 'retain authorship' '
+       echo A > file7 &&
+       git add file7 &&
+       test_tick &&
+       GIT_AUTHOR_NAME="Twerp Snog" git commit -m "different author" &&
+       git tag twerp &&
+       git rebase -i --onto master HEAD^ &&
+       git show HEAD | grep "^Author: Twerp Snog"
+'
+
+test_expect_success 'squash' '
+       git reset --hard twerp &&
+       echo B > file7 &&
+       test_tick &&
+       GIT_AUTHOR_NAME="Nitfol" git commit -m "nitfol" file7 &&
+       echo "******************************" &&
+       FAKE_LINES="1 squash 2" git rebase -i --onto master HEAD~2 &&
+       test B = $(cat file7) &&
+       test $(git rev-parse HEAD^) = $(git rev-parse master)
+'
+
+test_expect_success 'retain authorship when squashing' '
+       git show HEAD | grep "^Author: Twerp Snog"
+'
+
+test_expect_success 'preserve merges with -p' '
+       git checkout -b to-be-preserved master^ &&
+       : > unrelated-file &&
+       git add unrelated-file &&
+       test_tick &&
+       git commit -m "unrelated" &&
+       git checkout -b to-be-rebased master &&
+       echo B > file1 &&
+       test_tick &&
+       git commit -m J file1 &&
+       test_tick &&
+       git merge to-be-preserved &&
+       echo C > file1 &&
+       test_tick &&
+       git commit -m K file1 &&
+       test_tick &&
+       git rebase -i -p --onto branch1 master &&
+       test $(git rev-parse HEAD^^2) = $(git rev-parse to-be-preserved) &&
+       test $(git rev-parse HEAD~3) = $(git rev-parse branch1) &&
+       test $(git show HEAD:file1) = C &&
+       test $(git show HEAD~2:file1) = B
+'
+
+test_expect_success '--continue tries to commit' '
+       test_tick &&
+       ! git rebase -i --onto new-branch1 HEAD^ &&
+       echo resolved > file1 &&
+       git add file1 &&
+       FAKE_COMMIT_MESSAGE="chouette!" git rebase --continue &&
+       test $(git rev-parse HEAD^) = $(git rev-parse new-branch1) &&
+       git show HEAD | grep chouette
+'
+
+test_expect_success 'verbose flag is heeded, even after --continue' '
+       git reset --hard HEAD@{1} &&
+       test_tick &&
+       ! git rebase -v -i --onto new-branch1 HEAD^ &&
+       echo resolved > file1 &&
+       git add file1 &&
+       git rebase --continue > output &&
+       grep "^ file1 |    2 +-$" output
+'
+
+test_expect_success 'multi-squash only fires up editor once' '
+       base=$(git rev-parse HEAD~4) &&
+       FAKE_COMMIT_AMEND="ONCE" FAKE_LINES="1 squash 2 squash 3 squash 4" \
+               git rebase -i $base &&
+       test $base = $(git rev-parse HEAD^) &&
+       test 1 = $(git show | grep ONCE | wc -l)
+'
+
+test_expect_success 'squash works as expected' '
+       for n in one two three four
+       do
+               echo $n >> file$n &&
+               git add file$n &&
+               git commit -m $n
+       done &&
+       one=$(git rev-parse HEAD~3) &&
+       FAKE_LINES="1 squash 3 2" git rebase -i HEAD~3 &&
+       test $one = $(git rev-parse HEAD~2)
+'
+
+test_expect_success 'interrupted squash works as expected' '
+       for n in one two three four
+       do
+               echo $n >> conflict &&
+               git add conflict &&
+               git commit -m $n
+       done &&
+       one=$(git rev-parse HEAD~3) &&
+       ! FAKE_LINES="1 squash 3 2" git rebase -i HEAD~3 &&
+       (echo one; echo two; echo four) > conflict &&
+       git add conflict &&
+       ! git rebase --continue &&
+       echo resolved > conflict &&
+       git add conflict &&
+       git rebase --continue &&
+       test $one = $(git rev-parse HEAD~2)
+'
+
+test_expect_success 'interrupted squash works as expected (case 2)' '
+       for n in one two three four
+       do
+               echo $n >> conflict &&
+               git add conflict &&
+               git commit -m $n
+       done &&
+       one=$(git rev-parse HEAD~3) &&
+       ! FAKE_LINES="3 squash 1 2" git rebase -i HEAD~3 &&
+       (echo one; echo four) > conflict &&
+       git add conflict &&
+       ! git rebase --continue &&
+       (echo one; echo two; echo four) > conflict &&
+       git add conflict &&
+       ! git rebase --continue &&
+       echo resolved > conflict &&
+       git add conflict &&
+       git rebase --continue &&
+       test $one = $(git rev-parse HEAD~2)
+'
+
+test_expect_success 'ignore patch if in upstream' '
+       HEAD=$(git rev-parse HEAD) &&
+       git checkout -b has-cherry-picked HEAD^ &&
+       echo unrelated > file7 &&
+       git add file7 &&
+       test_tick &&
+       git commit -m "unrelated change" &&
+       git cherry-pick $HEAD &&
+       EXPECT_COUNT=1 git rebase -i $HEAD &&
+       test $HEAD = $(git rev-parse HEAD^)
+'
+
+test_expect_success '--continue tries to commit, even for "edit"' '
+       parent=$(git rev-parse HEAD^) &&
+       test_tick &&
+       FAKE_LINES="edit 1" git rebase -i HEAD^ &&
+       echo edited > file7 &&
+       git add file7 &&
+       FAKE_COMMIT_MESSAGE="chouette!" git rebase --continue &&
+       test edited = $(git show HEAD:file7) &&
+       git show HEAD | grep chouette &&
+       test $parent = $(git rev-parse HEAD^)
+'
+
+test_expect_success 'rebase a detached HEAD' '
+       grandparent=$(git rev-parse HEAD~2) &&
+       git checkout $(git rev-parse HEAD) &&
+       test_tick &&
+       FAKE_LINES="2 1" git rebase -i HEAD~2 &&
+       test $grandparent = $(git rev-parse HEAD~2)
+'
+
+test_done
diff --git a/t/t3405-rebase-malformed.sh b/t/t3405-rebase-malformed.sh
new file mode 100755 (executable)
index 0000000..e4e2e64
--- /dev/null
@@ -0,0 +1,48 @@
+#!/bin/sh
+
+test_description='rebase should not insist on git message convention'
+
+. ./test-lib.sh
+
+cat >F <<\EOF
+This is an example of a commit log message
+that does not  conform to git commit convention.
+
+It has two paragraphs, but its first paragraph is not friendly
+to oneline summary format.
+EOF
+
+test_expect_success setup '
+
+       >file1 &&
+       >file2 &&
+       git add file1 file2 &&
+       test_tick &&
+       git commit -m "Initial commit" &&
+
+       git checkout -b side &&
+       cat F >file2 &&
+       git add file2 &&
+       test_tick &&
+       git commit -F F &&
+
+       git cat-file commit HEAD | sed -e "1,/^\$/d" >F0 &&
+
+       git checkout master &&
+
+       echo One >file1 &&
+       test_tick &&
+       git add file1 &&
+       git commit -m "Second commit"
+'
+
+test_expect_success rebase '
+
+       git rebase master side &&
+       git cat-file commit HEAD | sed -e "1,/^\$/d" >F1 &&
+
+       diff -u F0 F1 &&
+       diff -u F F0
+'
+
+test_done
diff --git a/t/t3406-rebase-message.sh b/t/t3406-rebase-message.sh
new file mode 100755 (executable)
index 0000000..332b2b2
--- /dev/null
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+test_description='messages from rebase operation'
+
+. ./test-lib.sh
+
+quick_one () {
+       echo "$1" >"file$1" &&
+       git add "file$1" &&
+       test_tick &&
+       git commit -m "$1"
+}
+
+test_expect_success setup '
+       quick_one O &&
+       git branch topic &&
+       quick_one X &&
+       quick_one A &&
+       quick_one B &&
+       quick_one Y &&
+
+       git checkout topic &&
+       quick_one A &&
+       quick_one B &&
+       quick_one Z
+
+'
+
+cat >expect <<\EOF
+Already applied: 0001 A
+Already applied: 0002 B
+Committed: 0003 Z
+EOF
+
+test_expect_success 'rebase -m' '
+
+       git rebase -m master >report &&
+       sed -n -e "/^Already applied: /p" \
+               -e "/^Committed: /p" report >actual &&
+       diff -u expect actual
+
+'
+
+test_done
diff --git a/t/t3500-cherry.sh b/t/t3500-cherry.sh
new file mode 100755 (executable)
index 0000000..d0a440f
--- /dev/null
@@ -0,0 +1,54 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Yann Dirson, based on t3400 by Amos Waterland
+#
+
+test_description='git cherry should detect patches integrated upstream
+
+This test cherry-picks one local change of two into master branch, and
+checks that git cherry only returns the second patch in the local branch
+'
+. ./test-lib.sh
+
+export GIT_AUTHOR_EMAIL=bogus_email_address
+
+test_expect_success \
+    'prepare repository with topic branch, and check cherry finds the 2 patches from there' \
+    'echo First > A &&
+     git update-index --add A &&
+     git-commit -m "Add A." &&
+
+     git-checkout -b my-topic-branch &&
+
+     echo Second > B &&
+     git update-index --add B &&
+     git-commit -m "Add B." &&
+
+     sleep 2 &&
+     echo AnotherSecond > C &&
+     git update-index --add C &&
+     git-commit -m "Add C." &&
+
+     git-checkout -f master &&
+     rm -f B C &&
+
+     echo Third >> A &&
+     git update-index A &&
+     git-commit -m "Modify A." &&
+
+     expr "$(echo $(git cherry master my-topic-branch) )" : "+ [^ ]* + .*"
+'
+
+test_expect_success \
+    'check that cherry with limit returns only the top patch'\
+    'expr "$(echo $(git cherry master my-topic-branch my-topic-branch^1) )" : "+ [^ ]*"
+'
+
+test_expect_success \
+    'cherry-pick one of the 2 patches, and check cherry recognized one and only one as new' \
+    'git cherry-pick my-topic-branch^0 &&
+     echo $(git cherry master my-topic-branch) &&
+     expr "$(echo $(git cherry master my-topic-branch) )" : "+ [^ ]* - .*"
+'
+
+test_done
diff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh
new file mode 100755 (executable)
index 0000000..2dbe04f
--- /dev/null
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+test_description='test cherry-pick and revert with renames
+
+  --
+   + rename2: renames oops to opos
+  +  rename1: renames oops to spoo
+  +  added:   adds extra line to oops
+  ++ initial: has lines in oops
+
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       for l in a b c d e f g h i j k l m n o
+       do
+               echo $l$l$l$l$l$l$l$l$l
+       done >oops &&
+
+       test_tick &&
+       git add oops &&
+       git commit -m initial &&
+       git tag initial &&
+
+       test_tick &&
+       echo "Add extra line at the end" >>oops &&
+       git commit -a -m added &&
+       git tag added &&
+
+       test_tick &&
+       git mv oops spoo &&
+       git commit -m rename1 &&
+       git tag rename1 &&
+
+       test_tick &&
+       git checkout -b side initial &&
+       git mv oops opos &&
+       git commit -m rename2 &&
+       git tag rename2
+'
+
+test_expect_success 'cherry-pick after renaming branch' '
+
+       git checkout rename2 &&
+       git cherry-pick added &&
+       test -f opos &&
+       grep "Add extra line at the end" opos
+
+'
+
+test_expect_success 'revert after renaming branch' '
+
+       git checkout rename1 &&
+       git revert added &&
+       test -f spoo &&
+       ! grep "Add extra line at the end" spoo
+
+'
+
+test_done
diff --git a/t/t3502-cherry-pick-merge.sh b/t/t3502-cherry-pick-merge.sh
new file mode 100755 (executable)
index 0000000..7c92e26
--- /dev/null
@@ -0,0 +1,123 @@
+#!/bin/sh
+
+test_description='cherry picking and reverting a merge
+
+               b---c
+              /   /
+       initial---a
+
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       >A &&
+       >B &&
+       git add A B &&
+       git commit -m "Initial" &&
+       git tag initial &&
+       git branch side &&
+       echo new line >A &&
+       git commit -m "add line to A" A &&
+       git tag a &&
+       git checkout side &&
+       echo new line >B &&
+       git commit -m "add line to B" B &&
+       git tag b &&
+       git checkout master &&
+       git merge side &&
+       git tag c
+
+'
+
+test_expect_success 'cherry-pick a non-merge with -m should fail' '
+
+       git reset --hard &&
+       git checkout a^0 &&
+       ! git cherry-pick -m 1 b &&
+       git diff --exit-code a --
+
+'
+
+test_expect_success 'cherry pick a merge without -m should fail' '
+
+       git reset --hard &&
+       git checkout a^0 &&
+       ! git cherry-pick c &&
+       git diff --exit-code a --
+
+'
+
+test_expect_success 'cherry pick a merge (1)' '
+
+       git reset --hard &&
+       git checkout a^0 &&
+       git cherry-pick -m 1 c &&
+       git diff --exit-code c
+
+'
+
+test_expect_success 'cherry pick a merge (2)' '
+
+       git reset --hard &&
+       git checkout b^0 &&
+       git cherry-pick -m 2 c &&
+       git diff --exit-code c
+
+'
+
+test_expect_success 'cherry pick a merge relative to nonexistent parent should fail' '
+
+       git reset --hard &&
+       git checkout b^0 &&
+       ! git cherry-pick -m 3 c
+
+'
+
+test_expect_success 'revert a non-merge with -m should fail' '
+
+       git reset --hard &&
+       git checkout c^0 &&
+       ! git revert -m 1 b &&
+       git diff --exit-code c
+
+'
+
+test_expect_success 'revert a merge without -m should fail' '
+
+       git reset --hard &&
+       git checkout c^0 &&
+       ! git revert c &&
+       git diff --exit-code c
+
+'
+
+test_expect_success 'revert a merge (1)' '
+
+       git reset --hard &&
+       git checkout c^0 &&
+       git revert -m 1 c &&
+       git diff --exit-code a --
+
+'
+
+test_expect_success 'revert a merge (2)' '
+
+       git reset --hard &&
+       git checkout c^0 &&
+       git revert -m 2 c &&
+       git diff --exit-code b --
+
+'
+
+test_expect_success 'revert a merge relative to nonexistent parent should fail' '
+
+       git reset --hard &&
+       git checkout c^0 &&
+       ! git revert -m 3 c &&
+       git diff --exit-code c
+
+'
+
+test_done
diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh
new file mode 100755 (executable)
index 0000000..5c001aa
--- /dev/null
@@ -0,0 +1,219 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Carl D. Worth
+#
+
+test_description='Test of the various options to git rm.'
+
+. ./test-lib.sh
+
+# Setup some files to be removed, some with funny characters
+test_expect_success \
+    'Initialize test directory' \
+    "touch -- foo bar baz 'space embedded' -q &&
+     git add -- foo bar baz 'space embedded' -q &&
+     git-commit -m 'add normal files' &&
+     test_tabs=y &&
+     if touch -- 'tab  embedded' 'newline
+embedded'
+     then
+     git add -- 'tab   embedded' 'newline
+embedded' &&
+     git-commit -m 'add files with tabs and newlines'
+     else
+         say 'Your filesystem does not allow tabs in filenames.'
+         test_tabs=n
+     fi"
+
+# Later we will try removing an unremovable path to make sure
+# git rm barfs, but if the test is run as root that cannot be
+# arranged.
+test_expect_success \
+    'Determine rm behavior' \
+    ': >test-file
+     chmod a-w .
+     rm -f test-file
+     test -f test-file && test_failed_remove=y
+     chmod 775 .
+     rm -f test-file'
+
+test_expect_success \
+    'Pre-check that foo exists and is in index before git rm foo' \
+    '[ -f foo ] && git ls-files --error-unmatch foo'
+
+test_expect_success \
+    'Test that git rm foo succeeds' \
+    'git rm --cached foo'
+
+test_expect_success \
+    'Test that git rm --cached foo succeeds if the index matches the file' \
+    'echo content > foo
+     git add foo
+     git rm --cached foo'
+
+test_expect_success \
+    'Test that git rm --cached foo succeeds if the index matches the file' \
+    'echo content > foo
+     git add foo
+     git commit -m foo
+     echo "other content" > foo
+     git rm --cached foo'
+
+test_expect_failure \
+    'Test that git rm --cached foo fails if the index matches neither the file nor HEAD' \
+    'echo content > foo
+     git add foo
+     git commit -m foo
+     echo "other content" > foo
+     git add foo
+     echo "yet another content" > foo
+     git rm --cached foo'
+
+test_expect_success \
+    'Test that git rm --cached -f foo works in case where --cached only did not' \
+    'echo content > foo
+     git add foo
+     git commit -m foo
+     echo "other content" > foo
+     git add foo
+     echo "yet another content" > foo
+     git rm --cached -f foo'
+
+test_expect_success \
+    'Post-check that foo exists but is not in index after git rm foo' \
+    '[ -f foo ] && ! git ls-files --error-unmatch foo'
+
+test_expect_success \
+    'Pre-check that bar exists and is in index before "git rm bar"' \
+    '[ -f bar ] && git ls-files --error-unmatch bar'
+
+test_expect_success \
+    'Test that "git rm bar" succeeds' \
+    'git rm bar'
+
+test_expect_success \
+    'Post-check that bar does not exist and is not in index after "git rm -f bar"' \
+    '! [ -f bar ] && ! git ls-files --error-unmatch bar'
+
+test_expect_success \
+    'Test that "git rm -- -q" succeeds (remove a file that looks like an option)' \
+    'git rm -- -q'
+
+test "$test_tabs" = y && test_expect_success \
+    "Test that \"git rm -f\" succeeds with embedded space, tab, or newline characters." \
+    "git rm -f 'space embedded' 'tab   embedded' 'newline
+embedded'"
+
+if test "$test_failed_remove" = y; then
+chmod a-w .
+test_expect_failure \
+    'Test that "git rm -f" fails if its rm fails' \
+    'git rm -f baz'
+chmod 775 .
+else
+    test_expect_success 'skipping removal failure (perhaps running as root?)' :
+fi
+
+test_expect_success \
+    'When the rm in "git rm -f" fails, it should not remove the file from the index' \
+    'git ls-files --error-unmatch baz'
+
+test_expect_success 'Remove nonexistent file with --ignore-unmatch' '
+       git rm --ignore-unmatch nonexistent
+'
+
+test_expect_success '"rm" command printed' '
+       echo frotz > test-file &&
+       git add test-file &&
+       git commit -m "add file for rm test" &&
+       git rm test-file > rm-output &&
+       test `egrep "^rm " rm-output | wc -l` = 1 &&
+       rm -f test-file rm-output &&
+       git commit -m "remove file from rm test"
+'
+
+test_expect_success '"rm" command suppressed with --quiet' '
+       echo frotz > test-file &&
+       git add test-file &&
+       git commit -m "add file for rm --quiet test" &&
+       git rm --quiet test-file > rm-output &&
+       test `wc -l < rm-output` = 0 &&
+       rm -f test-file rm-output &&
+       git commit -m "remove file from rm --quiet test"
+'
+
+# Now, failure cases.
+test_expect_success 'Re-add foo and baz' '
+       git add foo baz &&
+       git ls-files --error-unmatch foo baz
+'
+
+test_expect_success 'Modify foo -- rm should refuse' '
+       echo >>foo &&
+       ! git rm foo baz &&
+       test -f foo &&
+       test -f baz &&
+       git ls-files --error-unmatch foo baz
+'
+
+test_expect_success 'Modified foo -- rm -f should work' '
+       git rm -f foo baz &&
+       test ! -f foo &&
+       test ! -f baz &&
+       ! git ls-files --error-unmatch foo &&
+       ! git ls-files --error-unmatch bar
+'
+
+test_expect_success 'Re-add foo and baz for HEAD tests' '
+       echo frotz >foo &&
+       git checkout HEAD -- baz &&
+       git add foo baz &&
+       git ls-files --error-unmatch foo baz
+'
+
+test_expect_success 'foo is different in index from HEAD -- rm should refuse' '
+       ! git rm foo baz &&
+       test -f foo &&
+       test -f baz &&
+       git ls-files --error-unmatch foo baz
+'
+
+test_expect_success 'but with -f it should work.' '
+       git rm -f foo baz &&
+       test ! -f foo &&
+       test ! -f baz &&
+       ! git ls-files --error-unmatch foo
+       ! git ls-files --error-unmatch baz
+'
+
+test_expect_success 'Recursive test setup' '
+       mkdir -p frotz &&
+       echo qfwfq >frotz/nitfol &&
+       git add frotz &&
+       git commit -m "subdir test"
+'
+
+test_expect_success 'Recursive without -r fails' '
+       ! git rm frotz &&
+       test -d frotz &&
+       test -f frotz/nitfol
+'
+
+test_expect_success 'Recursive with -r but dirty' '
+       echo qfwfq >>frotz/nitfol
+       ! git rm -r frotz &&
+       test -d frotz &&
+       test -f frotz/nitfol
+'
+
+test_expect_success 'Recursive with -r -f' '
+       git rm -f -r frotz &&
+       ! test -f frotz/nitfol &&
+       ! test -d frotz
+'
+
+test_expect_failure 'Remove nonexistent file returns nonzero exit status' '
+       git rm nonexistent
+'
+
+test_done
diff --git a/t/t3700-add.sh b/t/t3700-add.sh
new file mode 100755 (executable)
index 0000000..287e058
--- /dev/null
@@ -0,0 +1,182 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Carl D. Worth
+#
+
+test_description='Test of git add, including the -- option.'
+
+. ./test-lib.sh
+
+test_expect_success \
+    'Test of git add' \
+    'touch foo && git add foo'
+
+test_expect_success \
+    'Post-check that foo is in the index' \
+    'git ls-files foo | grep foo'
+
+test_expect_success \
+    'Test that "git add -- -q" works' \
+    'touch -- -q && git add -- -q'
+
+test_expect_success \
+       'git add: Test that executable bit is not used if core.filemode=0' \
+       'git config core.filemode 0 &&
+        echo foo >xfoo1 &&
+        chmod 755 xfoo1 &&
+        git add xfoo1 &&
+        case "`git ls-files --stage xfoo1`" in
+        100644" "*xfoo1) echo ok;;
+        *) echo fail; git ls-files --stage xfoo1; (exit 1);;
+        esac'
+
+test_expect_success 'git add: filemode=0 should not get confused by symlink' '
+       rm -f xfoo1 &&
+       ln -s foo xfoo1 &&
+       git add xfoo1 &&
+       case "`git ls-files --stage xfoo1`" in
+       120000" "*xfoo1) echo ok;;
+       *) echo fail; git ls-files --stage xfoo1; (exit 1);;
+       esac
+'
+
+test_expect_success \
+       'git update-index --add: Test that executable bit is not used...' \
+       'git config core.filemode 0 &&
+        echo foo >xfoo2 &&
+        chmod 755 xfoo2 &&
+        git update-index --add xfoo2 &&
+        case "`git ls-files --stage xfoo2`" in
+        100644" "*xfoo2) echo ok;;
+        *) echo fail; git ls-files --stage xfoo2; (exit 1);;
+        esac'
+
+test_expect_success 'git add: filemode=0 should not get confused by symlink' '
+       rm -f xfoo2 &&
+       ln -s foo xfoo2 &&
+       git update-index --add xfoo2 &&
+       case "`git ls-files --stage xfoo2`" in
+       120000" "*xfoo2) echo ok;;
+       *) echo fail; git ls-files --stage xfoo2; (exit 1);;
+       esac
+'
+
+test_expect_success \
+       'git update-index --add: Test that executable bit is not used...' \
+       'git config core.filemode 0 &&
+        ln -s xfoo2 xfoo3 &&
+        git update-index --add xfoo3 &&
+        case "`git ls-files --stage xfoo3`" in
+        120000" "*xfoo3) echo ok;;
+        *) echo fail; git ls-files --stage xfoo3; (exit 1);;
+        esac'
+
+test_expect_success '.gitignore test setup' '
+       echo "*.ig" >.gitignore &&
+       mkdir c.if d.ig &&
+       >a.ig && >b.if &&
+       >c.if/c.if && >c.if/c.ig &&
+       >d.ig/d.if && >d.ig/d.ig
+'
+
+test_expect_success '.gitignore is honored' '
+       git add . &&
+       ! git ls-files | grep "\\.ig"
+'
+
+test_expect_success 'error out when attempting to add ignored ones without -f' '
+       ! git add a.?? &&
+       ! git ls-files | grep "\\.ig"
+'
+
+test_expect_success 'error out when attempting to add ignored ones without -f' '
+       ! git add d.?? &&
+       ! git ls-files | grep "\\.ig"
+'
+
+test_expect_success 'add ignored ones with -f' '
+       git add -f a.?? &&
+       git ls-files --error-unmatch a.ig
+'
+
+test_expect_success 'add ignored ones with -f' '
+       git add -f d.??/* &&
+       git ls-files --error-unmatch d.ig/d.if d.ig/d.ig
+'
+
+test_expect_success 'add ignored ones with -f' '
+       rm -f .git/index &&
+       git add -f d.?? &&
+       git ls-files --error-unmatch d.ig/d.if d.ig/d.ig
+'
+
+test_expect_success '.gitignore with subdirectory' '
+
+       rm -f .git/index &&
+       mkdir -p sub/dir &&
+       echo "!dir/a.*" >sub/.gitignore &&
+       >sub/a.ig &&
+       >sub/dir/a.ig &&
+       git add sub/dir &&
+       git ls-files --error-unmatch sub/dir/a.ig &&
+       rm -f .git/index &&
+       (
+               cd sub/dir &&
+               git add .
+       ) &&
+       git ls-files --error-unmatch sub/dir/a.ig
+'
+
+mkdir 1 1/2 1/3
+touch 1/2/a 1/3/b 1/2/c
+test_expect_success 'check correct prefix detection' '
+       rm -f .git/index &&
+       git add 1/2/a 1/3/b 1/2/c
+'
+
+test_expect_success 'git add with filemode=0, symlinks=0, and unmerged entries' '
+       for s in 1 2 3
+       do
+               echo $s > stage$s
+               echo "100755 $(git hash-object -w stage$s) $s   file"
+               echo "120000 $(printf $s | git hash-object -w -t blob --stdin) $s       symlink"
+       done | git update-index --index-info &&
+       git config core.filemode 0 &&
+       git config core.symlinks 0 &&
+       echo new > file &&
+       echo new > symlink &&
+       git add file symlink &&
+       git ls-files --stage | grep "^100755 .* 0       file$" &&
+       git ls-files --stage | grep "^120000 .* 0       symlink$"
+'
+
+test_expect_success 'git add with filemode=0, symlinks=0 prefers stage 2 over stage 1' '
+       git rm --cached -f file symlink &&
+       (
+               echo "100644 $(git hash-object -w stage1) 1     file"
+               echo "100755 $(git hash-object -w stage2) 2     file"
+               echo "100644 $(printf 1 | git hash-object -w -t blob --stdin) 1 symlink"
+               echo "120000 $(printf 2 | git hash-object -w -t blob --stdin) 2 symlink"
+       ) | git update-index --index-info &&
+       git config core.filemode 0 &&
+       git config core.symlinks 0 &&
+       echo new > file &&
+       echo new > symlink &&
+       git add file symlink &&
+       git ls-files --stage | grep "^100755 .* 0       file$" &&
+       git ls-files --stage | grep "^120000 .* 0       symlink$"
+'
+
+test_expect_success 'git add --refresh' '
+       >foo && git add foo && git commit -a -m "commit all" &&
+       test -z "`git diff-index HEAD -- foo`" &&
+       git read-tree HEAD &&
+       case "`git diff-index HEAD -- foo`" in
+       :100644" "*"M   foo") echo ok;;
+       *) echo fail; (exit 1);;
+       esac &&
+       git add --refresh -- foo &&
+       test -z "`git diff-index HEAD -- foo`"
+'
+
+test_done
diff --git a/t/t3800-mktag.sh b/t/t3800-mktag.sh
new file mode 100755 (executable)
index 0000000..261f199
--- /dev/null
@@ -0,0 +1,227 @@
+#!/bin/sh
+#
+#
+
+test_description='git-mktag: tag object verify test'
+
+. ./test-lib.sh
+
+###########################################################
+# check the tag.sig file, expecting verify_tag() to fail,
+# and checking that the error message matches the pattern
+# given in the expect.pat file.
+
+check_verify_failure () {
+    test_expect_success \
+        "$1" \
+        'git-mktag <tag.sig 2>message ||
+         egrep -q -f expect.pat message'
+}
+
+###########################################################
+# first create a commit, so we have a valid object/type
+# for the tag.
+echo Hello >A
+git update-index --add A
+git-commit -m "Initial commit"
+head=$(git rev-parse --verify HEAD)
+
+############################################################
+#  1. length check
+
+cat >tag.sig <<EOF
+too short for a tag
+EOF
+
+cat >expect.pat <<EOF
+^error: .*size wrong.*$
+EOF
+
+check_verify_failure 'Tag object length check'
+
+############################################################
+#  2. object line label check
+
+cat >tag.sig <<EOF
+xxxxxx 139e9b33986b1c2670fff52c5067603117b3e895
+type tag
+tag mytag
+EOF
+
+cat >expect.pat <<EOF
+^error: char0: .*"object "$
+EOF
+
+check_verify_failure '"object" line label check'
+
+############################################################
+#  3. object line SHA1 check
+
+cat >tag.sig <<EOF
+object zz9e9b33986b1c2670fff52c5067603117b3e895
+type tag
+tag mytag
+EOF
+
+cat >expect.pat <<EOF
+^error: char7: .*SHA1 hash$
+EOF
+
+check_verify_failure '"object" line SHA1 check'
+
+############################################################
+#  4. type line label check
+
+cat >tag.sig <<EOF
+object 779e9b33986b1c2670fff52c5067603117b3e895
+xxxx tag
+tag mytag
+EOF
+
+cat >expect.pat <<EOF
+^error: char47: .*"[\]ntype "$
+EOF
+
+check_verify_failure '"type" line label check'
+
+############################################################
+#  5. type line eol check
+
+echo "object 779e9b33986b1c2670fff52c5067603117b3e895" >tag.sig
+printf "type tagsssssssssssssssssssssssssssssss" >>tag.sig
+
+cat >expect.pat <<EOF
+^error: char48: .*"[\]n"$
+EOF
+
+check_verify_failure '"type" line eol check'
+
+############################################################
+#  6. tag line label check #1
+
+cat >tag.sig <<EOF
+object 779e9b33986b1c2670fff52c5067603117b3e895
+type tag
+xxx mytag
+EOF
+
+cat >expect.pat <<EOF
+^error: char57: no "tag " found$
+EOF
+
+check_verify_failure '"tag" line label check #1'
+
+############################################################
+#  7. tag line label check #2
+
+cat >tag.sig <<EOF
+object 779e9b33986b1c2670fff52c5067603117b3e895
+type taggggggggggggggggggggggggggggggg
+tag
+EOF
+
+cat >expect.pat <<EOF
+^error: char87: no "tag " found$
+EOF
+
+check_verify_failure '"tag" line label check #2'
+
+############################################################
+#  8. type line type-name length check
+
+cat >tag.sig <<EOF
+object 779e9b33986b1c2670fff52c5067603117b3e895
+type taggggggggggggggggggggggggggggggg
+tag mytag
+EOF
+
+cat >expect.pat <<EOF
+^error: char53: type too long$
+EOF
+
+check_verify_failure '"type" line type-name length check'
+
+############################################################
+#  9. verify object (SHA1/type) check
+
+cat >tag.sig <<EOF
+object 779e9b33986b1c2670fff52c5067603117b3e895
+type tagggg
+tag mytag
+EOF
+
+cat >expect.pat <<EOF
+^error: char7: could not verify object.*$
+EOF
+
+check_verify_failure 'verify object (SHA1/type) check'
+
+############################################################
+# 10. verify tag-name check
+
+cat >tag.sig <<EOF
+object $head
+type commit
+tag my tag
+EOF
+
+cat >expect.pat <<EOF
+^error: char67: could not verify tag name$
+EOF
+
+check_verify_failure 'verify tag-name check'
+
+############################################################
+# 11. tagger line label check #1
+
+cat >tag.sig <<EOF
+object $head
+type commit
+tag mytag
+EOF
+
+cat >expect.pat <<EOF
+^error: char70: could not find "tagger"$
+EOF
+
+check_verify_failure '"tagger" line label check #1'
+
+############################################################
+# 12. tagger line label check #2
+
+cat >tag.sig <<EOF
+object $head
+type commit
+tag mytag
+tagger
+EOF
+
+cat >expect.pat <<EOF
+^error: char70: could not find "tagger"$
+EOF
+
+check_verify_failure '"tagger" line label check #2'
+
+############################################################
+# 13. create valid tag
+
+cat >tag.sig <<EOF
+object $head
+type commit
+tag mytag
+tagger another@example.com
+EOF
+
+test_expect_success \
+    'create valid tag' \
+    'git-mktag <tag.sig >.git/refs/tags/mytag 2>message'
+
+############################################################
+# 14. check mytag
+
+test_expect_success \
+    'check mytag' \
+    'git-tag -l | grep mytag'
+
+
+test_done
diff --git a/t/t3900-i18n-commit.sh b/t/t3900-i18n-commit.sh
new file mode 100755 (executable)
index 0000000..94b1c24
--- /dev/null
@@ -0,0 +1,122 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='commit and log output encodings'
+
+. ./test-lib.sh
+
+compare_with () {
+       git show -s $1 | sed -e '1,/^$/d' -e 's/^    //' >current &&
+       git diff current "$2"
+}
+
+test_expect_success setup '
+       : >F &&
+       git add F &&
+       T=$(git write-tree) &&
+       C=$(git commit-tree $T <../t3900/1-UTF-8.txt) &&
+       git update-ref HEAD $C &&
+       git-tag C0
+'
+
+test_expect_success 'no encoding header for base case' '
+       E=$(git cat-file commit C0 | sed -ne "s/^encoding //p") &&
+       test z = "z$E"
+'
+
+for H in ISO-8859-1 EUCJP ISO-2022-JP
+do
+       test_expect_success "$H setup" '
+               git config i18n.commitencoding $H &&
+               git-checkout -b $H C0 &&
+               echo $H >F &&
+               git-commit -a -F ../t3900/$H.txt
+       '
+done
+
+for H in ISO-8859-1 EUCJP ISO-2022-JP
+do
+       test_expect_success "check encoding header for $H" '
+               E=$(git cat-file commit '$H' | sed -ne "s/^encoding //p") &&
+               test "z$E" = "z'$H'"
+       '
+done
+
+test_expect_success 'config to remove customization' '
+       git config --unset-all i18n.commitencoding &&
+       if Z=$(git config --get-all i18n.commitencoding)
+       then
+               echo Oops, should have failed.
+               false
+       else
+               test z = "z$Z"
+       fi &&
+       git config i18n.commitencoding utf-8
+'
+
+test_expect_success 'ISO-8859-1 should be shown in UTF-8 now' '
+       compare_with ISO-8859-1 ../t3900/1-UTF-8.txt
+'
+
+for H in EUCJP ISO-2022-JP
+do
+       test_expect_success "$H should be shown in UTF-8 now" '
+               compare_with '$H' ../t3900/2-UTF-8.txt
+       '
+done
+
+test_expect_success 'config to add customization' '
+       git config --unset-all i18n.commitencoding &&
+       if Z=$(git config --get-all i18n.commitencoding)
+       then
+               echo Oops, should have failed.
+               false
+       else
+               test z = "z$Z"
+       fi
+'
+
+for H in ISO-8859-1 EUCJP ISO-2022-JP
+do
+       test_expect_success "$H should be shown in itself now" '
+               git config i18n.commitencoding '$H' &&
+               compare_with '$H' ../t3900/'$H'.txt
+       '
+done
+
+test_expect_success 'config to tweak customization' '
+       git config i18n.logoutputencoding utf-8
+'
+
+test_expect_success 'ISO-8859-1 should be shown in UTF-8 now' '
+       compare_with ISO-8859-1 ../t3900/1-UTF-8.txt
+'
+
+for H in EUCJP ISO-2022-JP
+do
+       test_expect_success "$H should be shown in UTF-8 now" '
+               compare_with '$H' ../t3900/2-UTF-8.txt
+       '
+done
+
+for J in EUCJP ISO-2022-JP
+do
+       git config i18n.logoutputencoding $J
+       for H in EUCJP ISO-2022-JP
+       do
+               test_expect_success "$H should be shown in $J now" '
+                       compare_with '$H' ../t3900/'$J'.txt
+               '
+       done
+done
+
+for H in ISO-8859-1 EUCJP ISO-2022-JP
+do
+       test_expect_success "No conversion with $H" '
+               compare_with "--encoding=none '$H'" ../t3900/'$H'.txt
+       '
+done
+
+test_done
diff --git a/t/t3900/1-UTF-8.txt b/t/t3900/1-UTF-8.txt
new file mode 100644 (file)
index 0000000..ee31e19
--- /dev/null
@@ -0,0 +1,3 @@
+ÄËÑÏÖ
+
+Ábçdèfg
diff --git a/t/t3900/2-UTF-8.txt b/t/t3900/2-UTF-8.txt
new file mode 100644 (file)
index 0000000..63f4f8f
--- /dev/null
@@ -0,0 +1,4 @@
+はれひほふ
+
+しているのが、いるので。
+濱浜ほれぷりぽれまびぐりろへ。
diff --git a/t/t3900/EUCJP.txt b/t/t3900/EUCJP.txt
new file mode 100644 (file)
index 0000000..546f2aa
--- /dev/null
@@ -0,0 +1,4 @@
+¤Ï¤ì¤Ò¤Û¤Õ
+
+¤·¤Æ¤¤¤ë¤Î¤¬¡¢¤¤¤ë¤Î¤Ç¡£
+ßÀÉͤۤì¤×¤ê¤Ý¤ì¤Þ¤Ó¤°¤ê¤í¤Ø¡£
diff --git a/t/t3900/ISO-2022-JP.txt b/t/t3900/ISO-2022-JP.txt
new file mode 100644 (file)
index 0000000..74b5330
--- /dev/null
@@ -0,0 +1,4 @@
+\e$B$O$l$R$[$U\e(B
+
+\e$B$7$F$$$k$N$,!"$$$k$N$G!#\e(B
+\e$B_@IM$[$l$W$j$]$l$^$S$0$j$m$X!#\e(B
diff --git a/t/t3900/ISO-8859-1.txt b/t/t3900/ISO-8859-1.txt
new file mode 100644 (file)
index 0000000..7cbef0e
--- /dev/null
@@ -0,0 +1,3 @@
+ÄËÑÏÖ
+
+Ábçdèfg
diff --git a/t/t3901-8859-1.txt b/t/t3901-8859-1.txt
new file mode 100755 (executable)
index 0000000..38c21a6
--- /dev/null
@@ -0,0 +1,4 @@
+: to be sourced in t3901 -- this is latin-1
+GIT_AUTHOR_NAME="Áéí óú" &&
+GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME &&
+export GIT_AUTHOR_NAME GIT_COMMITTER_NAME
diff --git a/t/t3901-i18n-patch.sh b/t/t3901-i18n-patch.sh
new file mode 100755 (executable)
index 0000000..235f372
--- /dev/null
@@ -0,0 +1,255 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='i18n settings and format-patch | am pipe'
+
+. ./test-lib.sh
+
+check_encoding () {
+       # Make sure characters are not corrupted
+       cnt="$1" header="$2" i=1 j=0 bad=0
+       while test "$i" -le $cnt
+       do
+               git format-patch --encoding=UTF-8 --stdout HEAD~$i..HEAD~$j |
+               grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD=20=C3=B3=C3=BA?=" &&
+               git cat-file commit HEAD~$j |
+               case "$header" in
+               8859)
+                       grep "^encoding ISO-8859-1" ;;
+               *)
+                       ! grep "^encoding ISO-8859-1" ;;
+               esac || {
+                       bad=1
+                       break
+               }
+               j=$i
+               i=$(($i+1))
+       done
+       (exit $bad)
+}
+
+test_expect_success setup '
+       git config i18n.commitencoding UTF-8 &&
+
+       # use UTF-8 in author and committer name to match the
+       # i18n.commitencoding settings
+       . ../t3901-utf8.txt &&
+
+       test_tick &&
+       echo "$GIT_AUTHOR_NAME" >mine &&
+       git add mine &&
+       git commit -s -m "Initial commit" &&
+
+       test_tick &&
+       echo Hello world >mine &&
+       git add mine &&
+       git commit -s -m "Second on main" &&
+
+       # the first commit on the side branch is UTF-8
+       test_tick &&
+       git checkout -b side master^ &&
+       echo Another file >yours &&
+       git add yours &&
+       git commit -s -m "Second on side" &&
+
+       # the second one on the side branch is ISO-8859-1
+       git config i18n.commitencoding ISO-8859-1 &&
+       # use author and committer name in ISO-8859-1 to match it.
+       . ../t3901-8859-1.txt &&
+       test_tick &&
+       echo Yet another >theirs &&
+       git add theirs &&
+       git commit -s -m "Third on side" &&
+
+       # Back to default
+       git config i18n.commitencoding UTF-8
+'
+
+test_expect_success 'format-patch output (ISO-8859-1)' '
+       git config i18n.logoutputencoding ISO-8859-1 &&
+
+       git format-patch --stdout master..HEAD^ >out-l1 &&
+       git format-patch --stdout HEAD^ >out-l2 &&
+       grep "^Content-Type: text/plain; charset=ISO-8859-1" out-l1 &&
+       grep "^From: =?ISO-8859-1?q?=C1=E9=ED=20=F3=FA?=" out-l1 &&
+       grep "^Content-Type: text/plain; charset=ISO-8859-1" out-l2 &&
+       grep "^From: =?ISO-8859-1?q?=C1=E9=ED=20=F3=FA?=" out-l2
+'
+
+test_expect_success 'format-patch output (UTF-8)' '
+       git config i18n.logoutputencoding UTF-8 &&
+
+       git format-patch --stdout master..HEAD^ >out-u1 &&
+       git format-patch --stdout HEAD^ >out-u2 &&
+       grep "^Content-Type: text/plain; charset=UTF-8" out-u1 &&
+       grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD=20=C3=B3=C3=BA?=" out-u1 &&
+       grep "^Content-Type: text/plain; charset=UTF-8" out-u2 &&
+       grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD=20=C3=B3=C3=BA?=" out-u2
+'
+
+test_expect_success 'rebase (U/U)' '
+       # We want the result of rebase in UTF-8
+       git config i18n.commitencoding UTF-8 &&
+
+       # The test is about logoutputencoding not affecting the
+       # final outcome -- it is used internally to generate the
+       # patch and the log.
+
+       git config i18n.logoutputencoding UTF-8 &&
+
+       # The result will be committed by GIT_COMMITTER_NAME --
+       # we want UTF-8 encoded name.
+       . ../t3901-utf8.txt &&
+       git checkout -b test &&
+       git-rebase master &&
+
+       check_encoding 2
+'
+
+test_expect_success 'rebase (U/L)' '
+       git config i18n.commitencoding UTF-8 &&
+       git config i18n.logoutputencoding ISO-8859-1 &&
+       . ../t3901-utf8.txt &&
+
+       git reset --hard side &&
+       git-rebase master &&
+
+       check_encoding 2
+'
+
+test_expect_success 'rebase (L/L)' '
+       # In this test we want ISO-8859-1 encoded commits as the result
+       git config i18n.commitencoding ISO-8859-1 &&
+       git config i18n.logoutputencoding ISO-8859-1 &&
+       . ../t3901-8859-1.txt &&
+
+       git reset --hard side &&
+       git-rebase master &&
+
+       check_encoding 2 8859
+'
+
+test_expect_success 'rebase (L/U)' '
+       # This is pathological -- use UTF-8 as intermediate form
+       # to get ISO-8859-1 results.
+       git config i18n.commitencoding ISO-8859-1 &&
+       git config i18n.logoutputencoding UTF-8 &&
+       . ../t3901-8859-1.txt &&
+
+       git reset --hard side &&
+       git-rebase master &&
+
+       check_encoding 2 8859
+'
+
+test_expect_success 'cherry-pick(U/U)' '
+       # Both the commitencoding and logoutputencoding is set to UTF-8.
+
+       git config i18n.commitencoding UTF-8 &&
+       git config i18n.logoutputencoding UTF-8 &&
+       . ../t3901-utf8.txt &&
+
+       git reset --hard master &&
+       git cherry-pick side^ &&
+       git cherry-pick side &&
+       git revert HEAD &&
+
+       check_encoding 3
+'
+
+test_expect_success 'cherry-pick(L/L)' '
+       # Both the commitencoding and logoutputencoding is set to ISO-8859-1
+
+       git config i18n.commitencoding ISO-8859-1 &&
+       git config i18n.logoutputencoding ISO-8859-1 &&
+       . ../t3901-8859-1.txt &&
+
+       git reset --hard master &&
+       git cherry-pick side^ &&
+       git cherry-pick side &&
+       git revert HEAD &&
+
+       check_encoding 3 8859
+'
+
+test_expect_success 'cherry-pick(U/L)' '
+       # Commitencoding is set to UTF-8 but logoutputencoding is ISO-8859-1
+
+       git config i18n.commitencoding UTF-8 &&
+       git config i18n.logoutputencoding ISO-8859-1 &&
+       . ../t3901-utf8.txt &&
+
+       git reset --hard master &&
+       git cherry-pick side^ &&
+       git cherry-pick side &&
+       git revert HEAD &&
+
+       check_encoding 3
+'
+
+test_expect_success 'cherry-pick(L/U)' '
+       # Again, the commitencoding is set to ISO-8859-1 but
+       # logoutputencoding is set to UTF-8.
+
+       git config i18n.commitencoding ISO-8859-1 &&
+       git config i18n.logoutputencoding UTF-8 &&
+       . ../t3901-8859-1.txt &&
+
+       git reset --hard master &&
+       git cherry-pick side^ &&
+       git cherry-pick side &&
+       git revert HEAD &&
+
+       check_encoding 3 8859
+'
+
+test_expect_success 'rebase --merge (U/U)' '
+       git config i18n.commitencoding UTF-8 &&
+       git config i18n.logoutputencoding UTF-8 &&
+       . ../t3901-utf8.txt &&
+
+       git reset --hard side &&
+       git-rebase --merge master &&
+
+       check_encoding 2
+'
+
+test_expect_success 'rebase --merge (U/L)' '
+       git config i18n.commitencoding UTF-8 &&
+       git config i18n.logoutputencoding ISO-8859-1 &&
+       . ../t3901-utf8.txt &&
+
+       git reset --hard side &&
+       git-rebase --merge master &&
+
+       check_encoding 2
+'
+
+test_expect_success 'rebase --merge (L/L)' '
+       # In this test we want ISO-8859-1 encoded commits as the result
+       git config i18n.commitencoding ISO-8859-1 &&
+       git config i18n.logoutputencoding ISO-8859-1 &&
+       . ../t3901-8859-1.txt &&
+
+       git reset --hard side &&
+       git-rebase --merge master &&
+
+       check_encoding 2 8859
+'
+
+test_expect_success 'rebase --merge (L/U)' '
+       # This is pathological -- use UTF-8 as intermediate form
+       # to get ISO-8859-1 results.
+       git config i18n.commitencoding ISO-8859-1 &&
+       git config i18n.logoutputencoding UTF-8 &&
+       . ../t3901-8859-1.txt &&
+
+       git reset --hard side &&
+       git-rebase --merge master &&
+
+       check_encoding 2 8859
+'
+
+test_done
diff --git a/t/t3901-utf8.txt b/t/t3901-utf8.txt
new file mode 100755 (executable)
index 0000000..5f5205c
--- /dev/null
@@ -0,0 +1,4 @@
+: to be sourced in t3901 -- this is utf8
+GIT_AUTHOR_NAME="Áéí óú" &&
+GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME &&
+export GIT_AUTHOR_NAME GIT_COMMITTER_NAME
diff --git a/t/t3902-quoted.sh b/t/t3902-quoted.sh
new file mode 100755 (executable)
index 0000000..73da45f
--- /dev/null
@@ -0,0 +1,139 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='quoted output'
+
+. ./test-lib.sh
+
+P1='pathname   with HT'
+: >"$P1" 2>&1 && test -f "$P1" && rm -f "$P1" || {
+       echo >&2 'Filesystem does not support HT in names'
+       test_done
+}
+
+FN='濱野'
+GN='純'
+HT='   '
+LF='
+'
+DQ='"'
+
+echo foo > "Name and an${HT}HT"
+test -f "Name and an${HT}HT" || {
+       # since FAT/NTFS does not allow tabs in filenames, skip this test
+       say 'Your filesystem does not allow tabs in filenames, test skipped.'
+       test_done
+}
+
+for_each_name () {
+       for name in \
+           Name "Name and a${LF}LF" "Name and an${HT}HT" "Name${DQ}" \
+           "$FN$HT$GN" "$FN$LF$GN" "$FN $GN" "$FN$GN" "$FN$DQ$GN" \
+           "With SP in it"
+       do
+               eval "$1"
+       done
+}
+
+test_expect_success setup '
+
+       for_each_name "echo initial >\"\$name\""
+       git add . &&
+       git commit -q -m Initial &&
+
+       for_each_name "echo second >\"\$name\"" &&
+       git commit -a -m Second
+
+       for_each_name "echo modified >\"\$name\""
+
+'
+
+cat >expect.quoted <<\EOF
+Name
+"Name and a\nLF"
+"Name and an\tHT"
+"Name\""
+With SP in it
+"\346\277\261\351\207\216\t\347\264\224"
+"\346\277\261\351\207\216\n\347\264\224"
+"\346\277\261\351\207\216 \347\264\224"
+"\346\277\261\351\207\216\"\347\264\224"
+"\346\277\261\351\207\216\347\264\224"
+EOF
+
+cat >expect.raw <<\EOF
+Name
+"Name and a\nLF"
+"Name and an\tHT"
+"Name\""
+With SP in it
+"濱野\t純"
+"濱野\n純"
+濱野 純
+"濱野\"純"
+濱野純
+EOF
+
+test_expect_success 'check fully quoted output from ls-files' '
+
+       git ls-files >current && diff -u expect.quoted current
+
+'
+
+test_expect_success 'check fully quoted output from diff-files' '
+
+       git diff --name-only >current &&
+       diff -u expect.quoted current
+
+'
+
+test_expect_success 'check fully quoted output from diff-index' '
+
+       git diff --name-only HEAD >current &&
+       diff -u expect.quoted current
+
+'
+
+test_expect_success 'check fully quoted output from diff-tree' '
+
+       git diff --name-only HEAD^ HEAD >current &&
+       diff -u expect.quoted current
+
+'
+
+test_expect_success 'setting core.quotepath' '
+
+       git config --bool core.quotepath false
+
+'
+
+test_expect_success 'check fully quoted output from ls-files' '
+
+       git ls-files >current && diff -u expect.raw current
+
+'
+
+test_expect_success 'check fully quoted output from diff-files' '
+
+       git diff --name-only >current &&
+       diff -u expect.raw current
+
+'
+
+test_expect_success 'check fully quoted output from diff-index' '
+
+       git diff --name-only HEAD >current &&
+       diff -u expect.raw current
+
+'
+
+test_expect_success 'check fully quoted output from diff-tree' '
+
+       git diff --name-only HEAD^ HEAD >current &&
+       diff -u expect.raw current
+
+'
+
+test_done
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
new file mode 100755 (executable)
index 0000000..9a9a250
--- /dev/null
@@ -0,0 +1,76 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes E Schindelin
+#
+
+test_description='Test git-stash'
+
+. ./test-lib.sh
+
+test_expect_success 'stash some dirty working directory' '
+       echo 1 > file &&
+       git add file &&
+       test_tick &&
+       git commit -m initial &&
+       echo 2 > file &&
+       git add file &&
+       echo 3 > file &&
+       test_tick &&
+       git stash &&
+       git diff-files --quiet &&
+       git diff-index --cached --quiet HEAD
+'
+
+cat > expect << EOF
+diff --git a/file b/file
+index 0cfbf08..00750ed 100644
+--- a/file
++++ b/file
+@@ -1 +1 @@
+-2
++3
+EOF
+
+test_expect_success 'parents of stash' '
+       test $(git rev-parse stash^) = $(git rev-parse HEAD) &&
+       git diff stash^2..stash > output &&
+       diff -u output expect
+'
+
+test_expect_success 'apply needs clean working directory' '
+       echo 4 > other-file &&
+       git add other-file &&
+       echo 5 > other-file
+       ! git stash apply
+'
+
+test_expect_success 'apply stashed changes' '
+       git add other-file &&
+       test_tick &&
+       git commit -m other-file &&
+       git stash apply &&
+       test 3 = $(cat file) &&
+       test 1 = $(git show :file) &&
+       test 1 = $(git show HEAD:file)
+'
+
+test_expect_success 'apply stashed changes (including index)' '
+       git reset --hard HEAD^ &&
+       echo 6 > other-file &&
+       git add other-file &&
+       test_tick &&
+       git commit -m other-file &&
+       git stash apply --index &&
+       test 3 = $(cat file) &&
+       test 2 = $(git show :file) &&
+       test 1 = $(git show HEAD:file)
+'
+
+test_expect_success 'unstashing in a subdirectory' '
+       git reset --hard HEAD &&
+       mkdir subdir &&
+       cd subdir &&
+       git stash apply
+'
+
+test_done
diff --git a/t/t4000-diff-format.sh b/t/t4000-diff-format.sh
new file mode 100755 (executable)
index 0000000..c44b27a
--- /dev/null
@@ -0,0 +1,62 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Test built-in diff output engine.
+
+'
+. ./test-lib.sh
+. ../diff-lib.sh
+
+echo >path0 'Line 1
+Line 2
+line 3'
+cat path0 >path1
+chmod +x path1
+
+test_expect_success \
+    'update-index --add two files with and without +x.' \
+    'git update-index --add path0 path1'
+
+mv path0 path0-
+sed -e 's/line/Line/' <path0- >path0
+chmod +x path0
+rm -f path1
+test_expect_success \
+    'git diff-files -p after editing work tree.' \
+    'git diff-files -p >current'
+
+# that's as far as it comes
+if [ "$(git config --get core.filemode)" = false ]
+then
+       say 'filemode disabled on the filesystem'
+       test_done
+fi
+
+cat >expected <<\EOF
+diff --git a/path0 b/path0
+old mode 100644
+new mode 100755
+--- a/path0
++++ b/path0
+@@ -1,3 +1,3 @@
+ Line 1
+ Line 2
+-line 3
++Line 3
+diff --git a/path1 b/path1
+deleted file mode 100755
+--- a/path1
++++ /dev/null
+@@ -1,3 +0,0 @@
+-Line 1
+-Line 2
+-line 3
+EOF
+
+test_expect_success \
+    'validate git diff-files -p output.' \
+    'compare_diff_patch current expected'
+
+test_done
diff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh
new file mode 100755 (executable)
index 0000000..a326924
--- /dev/null
@@ -0,0 +1,80 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Test rename detection in diff engine.
+
+'
+. ./test-lib.sh
+. ../diff-lib.sh
+
+echo >path0 'Line 1
+Line 2
+Line 3
+Line 4
+Line 5
+Line 6
+Line 7
+Line 8
+Line 9
+Line 10
+line 11
+Line 12
+Line 13
+Line 14
+Line 15
+'
+
+test_expect_success \
+    'update-index --add a file.' \
+    'git update-index --add path0'
+
+test_expect_success \
+    'write that tree.' \
+    'tree=$(git write-tree) && echo $tree'
+
+sed -e 's/line/Line/' <path0 >path1
+rm -f path0
+test_expect_success \
+    'renamed and edited the file.' \
+    'git update-index --add --remove path0 path1'
+
+test_expect_success \
+    'git diff-index -p -M after rename and editing.' \
+    'git diff-index -p -M $tree >current'
+cat >expected <<\EOF
+diff --git a/path0 b/path1
+rename from path0
+rename to path1
+--- a/path0
++++ b/path1
+@@ -8,7 +8,7 @@ Line 7
+ Line 8
+ Line 9
+ Line 10
+-line 11
++Line 11
+ Line 12
+ Line 13
+ Line 14
+EOF
+
+test_expect_success \
+    'validate the output.' \
+    'compare_diff_patch current expected'
+
+test_expect_success 'favour same basenames over different ones' '
+       cp path1 another-path &&
+       git add another-path &&
+       git commit -m 1 &&
+       git rm path1 &&
+       mkdir subdir &&
+       git mv another-path subdir/path1 &&
+       git status | grep "renamed: .*path1 -> subdir/path1"'
+
+test_expect_success  'favour same basenames even with minor differences' '
+       git show HEAD:path1 | sed "s/15/16/" > subdir/path1 &&
+       git status | grep "renamed: .*path1 -> subdir/path1"'
+
+test_done
diff --git a/t/t4002-diff-basic.sh b/t/t4002-diff-basic.sh
new file mode 100755 (executable)
index 0000000..a4cfde6
--- /dev/null
@@ -0,0 +1,247 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Test diff raw-output.
+
+'
+. ./test-lib.sh
+. ../lib-read-tree-m-3way.sh
+
+cat >.test-plain-OA <<\EOF
+:000000 100644 0000000000000000000000000000000000000000 ccba72ad3888a3520b39efcf780b9ee64167535d A     AA
+:000000 100644 0000000000000000000000000000000000000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 A     AN
+:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D     DD
+:000000 040000 0000000000000000000000000000000000000000 6d50f65d3bdab91c63444294d38f08aeff328e42 A     DF
+:100644 000000 141c1f1642328e4bc46a7d801a71da392e66791e 0000000000000000000000000000000000000000 D     DM
+:100644 000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 0000000000000000000000000000000000000000 D     DN
+:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 A     LL
+:100644 100644 03f24c8c4700babccfd28b654e7e8eac402ad6cd 103d9f89b50b9aad03054b579be5e7aa665f2d57 M     MD
+:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 M     MM
+:100644 100644 bd084b0c27c7b6cc34f11d6d0509a29be3caf970 a716d58de4a570e0038f5c307bd8db34daea021f M     MN
+:100644 100644 40c959f984c8b89a2b02520d17f00d717f024397 2ac547ae9614a00d1b28275de608131f7a0e259f M     SS
+:100644 100644 4ac13458899ab908ef3b1128fa378daefc88d356 4c86f9a85fbc5e6804ee2e17a797538fbe785bca M     TT
+:040000 040000 7d670fdcdb9929f6c7dac196ff78689cd1c566a1 5e5f22072bb39f6e12cf663a57cb634c76eefb49 M     Z
+EOF
+
+cat >.test-recursive-OA <<\EOF
+:000000 100644 0000000000000000000000000000000000000000 ccba72ad3888a3520b39efcf780b9ee64167535d A     AA
+:000000 100644 0000000000000000000000000000000000000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 A     AN
+:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D     DD
+:000000 100644 0000000000000000000000000000000000000000 68a6d8b91da11045cf4aa3a5ab9f2a781c701249 A     DF/DF
+:100644 000000 141c1f1642328e4bc46a7d801a71da392e66791e 0000000000000000000000000000000000000000 D     DM
+:100644 000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 0000000000000000000000000000000000000000 D     DN
+:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 A     LL
+:100644 100644 03f24c8c4700babccfd28b654e7e8eac402ad6cd 103d9f89b50b9aad03054b579be5e7aa665f2d57 M     MD
+:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 M     MM
+:100644 100644 bd084b0c27c7b6cc34f11d6d0509a29be3caf970 a716d58de4a570e0038f5c307bd8db34daea021f M     MN
+:100644 100644 40c959f984c8b89a2b02520d17f00d717f024397 2ac547ae9614a00d1b28275de608131f7a0e259f M     SS
+:100644 100644 4ac13458899ab908ef3b1128fa378daefc88d356 4c86f9a85fbc5e6804ee2e17a797538fbe785bca M     TT
+:000000 100644 0000000000000000000000000000000000000000 8acb8e9750e3f644bf323fcf3d338849db106c77 A     Z/AA
+:000000 100644 0000000000000000000000000000000000000000 087494262084cefee7ed484d20c8dc0580791272 A     Z/AN
+:100644 000000 879007efae624d2b1307214b24a956f0a8d686a8 0000000000000000000000000000000000000000 D     Z/DD
+:100644 000000 9b541b2275c06e3a7b13f28badf5294e2ae63df4 0000000000000000000000000000000000000000 D     Z/DM
+:100644 000000 beb5d38c55283d280685ea21a0e50cfcc0ca064a 0000000000000000000000000000000000000000 D     Z/DN
+:100644 100644 d41fda41b7ec4de46b43cb7ea42a45001ae393d5 a79ac3be9377639e1c7d1edf1ae1b3a5f0ccd8a9 M     Z/MD
+:100644 100644 4ca22bae2527d3d9e1676498a0fba3b355bd1278 61422ba9c2c873416061a88cd40a59a35b576474 M     Z/MM
+:100644 100644 b16d7b25b869f2beb124efa53467d8a1550ad694 a5c544c21cfcb07eb80a4d89a5b7d1570002edfd M     Z/MN
+EOF
+cat >.test-plain-OB <<\EOF
+:000000 100644 0000000000000000000000000000000000000000 6aa2b5335b16431a0ef71e5c0a28be69183cf6a2 A     AA
+:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D     DD
+:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 A     DF
+:100644 100644 141c1f1642328e4bc46a7d801a71da392e66791e 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 M     DM
+:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 A     LL
+:100644 000000 03f24c8c4700babccfd28b654e7e8eac402ad6cd 0000000000000000000000000000000000000000 D     MD
+:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 19989d4559aae417fedee240ccf2ba315ea4dc2b M     MM
+:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 A     NA
+:100644 000000 a4e179e4291e5536a5e1c82e091052772d2c5a93 0000000000000000000000000000000000000000 D     ND
+:100644 100644 c8f25781e8f1792e3e40b74225e20553041b5226 cdb9a8c3da571502ac30225e9c17beccb8387983 M     NM
+:100644 100644 40c959f984c8b89a2b02520d17f00d717f024397 2ac547ae9614a00d1b28275de608131f7a0e259f M     SS
+:100644 100644 4ac13458899ab908ef3b1128fa378daefc88d356 c4e4a12231b9fa79a0053cb6077fcb21bb5b135a M     TT
+:040000 040000 7d670fdcdb9929f6c7dac196ff78689cd1c566a1 1ba523955d5160681af65cb776411f574c1e8155 M     Z
+EOF
+cat >.test-recursive-OB <<\EOF
+:000000 100644 0000000000000000000000000000000000000000 6aa2b5335b16431a0ef71e5c0a28be69183cf6a2 A     AA
+:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D     DD
+:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 A     DF
+:100644 100644 141c1f1642328e4bc46a7d801a71da392e66791e 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 M     DM
+:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 A     LL
+:100644 000000 03f24c8c4700babccfd28b654e7e8eac402ad6cd 0000000000000000000000000000000000000000 D     MD
+:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 19989d4559aae417fedee240ccf2ba315ea4dc2b M     MM
+:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 A     NA
+:100644 000000 a4e179e4291e5536a5e1c82e091052772d2c5a93 0000000000000000000000000000000000000000 D     ND
+:100644 100644 c8f25781e8f1792e3e40b74225e20553041b5226 cdb9a8c3da571502ac30225e9c17beccb8387983 M     NM
+:100644 100644 40c959f984c8b89a2b02520d17f00d717f024397 2ac547ae9614a00d1b28275de608131f7a0e259f M     SS
+:100644 100644 4ac13458899ab908ef3b1128fa378daefc88d356 c4e4a12231b9fa79a0053cb6077fcb21bb5b135a M     TT
+:000000 100644 0000000000000000000000000000000000000000 6c0b99286d0bce551ac4a7b3dff8b706edff3715 A     Z/AA
+:100644 000000 879007efae624d2b1307214b24a956f0a8d686a8 0000000000000000000000000000000000000000 D     Z/DD
+:100644 100644 9b541b2275c06e3a7b13f28badf5294e2ae63df4 d77371d15817fcaa57eeec27f770c505ba974ec1 M     Z/DM
+:100644 000000 d41fda41b7ec4de46b43cb7ea42a45001ae393d5 0000000000000000000000000000000000000000 D     Z/MD
+:100644 100644 4ca22bae2527d3d9e1676498a0fba3b355bd1278 697aad7715a1e7306ca76290a3dd4208fbaeddfa M     Z/MM
+:000000 100644 0000000000000000000000000000000000000000 d12979c22fff69c59ca9409e7a8fe3ee25eaee80 A     Z/NA
+:100644 000000 a18393c636b98e9bd7296b8b437ea4992b72440c 0000000000000000000000000000000000000000 D     Z/ND
+:100644 100644 3fdbe17fd013303a2e981e1ca1c6cd6e72789087 7e09d6a3a14bd630913e8c75693cea32157b606d M     Z/NM
+EOF
+cat >.test-plain-AB <<\EOF
+:100644 100644 ccba72ad3888a3520b39efcf780b9ee64167535d 6aa2b5335b16431a0ef71e5c0a28be69183cf6a2 M     AA
+:100644 000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 0000000000000000000000000000000000000000 D     AN
+:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 A     DF
+:040000 000000 6d50f65d3bdab91c63444294d38f08aeff328e42 0000000000000000000000000000000000000000 D     DF
+:000000 100644 0000000000000000000000000000000000000000 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 A     DM
+:000000 100644 0000000000000000000000000000000000000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 A     DN
+:100644 000000 103d9f89b50b9aad03054b579be5e7aa665f2d57 0000000000000000000000000000000000000000 D     MD
+:100644 100644 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 19989d4559aae417fedee240ccf2ba315ea4dc2b M     MM
+:100644 100644 a716d58de4a570e0038f5c307bd8db34daea021f bd084b0c27c7b6cc34f11d6d0509a29be3caf970 M     MN
+:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 A     NA
+:100644 000000 a4e179e4291e5536a5e1c82e091052772d2c5a93 0000000000000000000000000000000000000000 D     ND
+:100644 100644 c8f25781e8f1792e3e40b74225e20553041b5226 cdb9a8c3da571502ac30225e9c17beccb8387983 M     NM
+:100644 100644 4c86f9a85fbc5e6804ee2e17a797538fbe785bca c4e4a12231b9fa79a0053cb6077fcb21bb5b135a M     TT
+:040000 040000 5e5f22072bb39f6e12cf663a57cb634c76eefb49 1ba523955d5160681af65cb776411f574c1e8155 M     Z
+EOF
+cat >.test-recursive-AB <<\EOF
+:100644 100644 ccba72ad3888a3520b39efcf780b9ee64167535d 6aa2b5335b16431a0ef71e5c0a28be69183cf6a2 M     AA
+:100644 000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 0000000000000000000000000000000000000000 D     AN
+:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 A     DF
+:100644 000000 68a6d8b91da11045cf4aa3a5ab9f2a781c701249 0000000000000000000000000000000000000000 D     DF/DF
+:000000 100644 0000000000000000000000000000000000000000 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 A     DM
+:000000 100644 0000000000000000000000000000000000000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 A     DN
+:100644 000000 103d9f89b50b9aad03054b579be5e7aa665f2d57 0000000000000000000000000000000000000000 D     MD
+:100644 100644 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 19989d4559aae417fedee240ccf2ba315ea4dc2b M     MM
+:100644 100644 a716d58de4a570e0038f5c307bd8db34daea021f bd084b0c27c7b6cc34f11d6d0509a29be3caf970 M     MN
+:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 A     NA
+:100644 000000 a4e179e4291e5536a5e1c82e091052772d2c5a93 0000000000000000000000000000000000000000 D     ND
+:100644 100644 c8f25781e8f1792e3e40b74225e20553041b5226 cdb9a8c3da571502ac30225e9c17beccb8387983 M     NM
+:100644 100644 4c86f9a85fbc5e6804ee2e17a797538fbe785bca c4e4a12231b9fa79a0053cb6077fcb21bb5b135a M     TT
+:100644 100644 8acb8e9750e3f644bf323fcf3d338849db106c77 6c0b99286d0bce551ac4a7b3dff8b706edff3715 M     Z/AA
+:100644 000000 087494262084cefee7ed484d20c8dc0580791272 0000000000000000000000000000000000000000 D     Z/AN
+:000000 100644 0000000000000000000000000000000000000000 d77371d15817fcaa57eeec27f770c505ba974ec1 A     Z/DM
+:000000 100644 0000000000000000000000000000000000000000 beb5d38c55283d280685ea21a0e50cfcc0ca064a A     Z/DN
+:100644 000000 a79ac3be9377639e1c7d1edf1ae1b3a5f0ccd8a9 0000000000000000000000000000000000000000 D     Z/MD
+:100644 100644 61422ba9c2c873416061a88cd40a59a35b576474 697aad7715a1e7306ca76290a3dd4208fbaeddfa M     Z/MM
+:100644 100644 a5c544c21cfcb07eb80a4d89a5b7d1570002edfd b16d7b25b869f2beb124efa53467d8a1550ad694 M     Z/MN
+:000000 100644 0000000000000000000000000000000000000000 d12979c22fff69c59ca9409e7a8fe3ee25eaee80 A     Z/NA
+:100644 000000 a18393c636b98e9bd7296b8b437ea4992b72440c 0000000000000000000000000000000000000000 D     Z/ND
+:100644 100644 3fdbe17fd013303a2e981e1ca1c6cd6e72789087 7e09d6a3a14bd630913e8c75693cea32157b606d M     Z/NM
+EOF
+
+x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+x40="$x40$x40$x40$x40$x40$x40$x40$x40"
+z40='0000000000000000000000000000000000000000'
+cmp_diff_files_output () {
+    # diff-files never reports additions.  Also it does not fill in the
+    # object ID for the changed files because it wants you to look at the
+    # filesystem.
+    sed <"$2" >.test-tmp \
+       -e '/^:000000 /d;s/'$x40'\( [MCRNDU][0-9]*\)    /'$z40'\1       /' &&
+    diff "$1" .test-tmp
+}
+
+test_expect_success \
+    'diff-tree of known trees.' \
+    'git diff-tree $tree_O $tree_A >.test-a &&
+     cmp -s .test-a .test-plain-OA'
+
+test_expect_success \
+    'diff-tree of known trees.' \
+    'git diff-tree -r $tree_O $tree_A >.test-a &&
+     cmp -s .test-a .test-recursive-OA'
+
+test_expect_success \
+    'diff-tree of known trees.' \
+    'git diff-tree $tree_O $tree_B >.test-a &&
+     cmp -s .test-a .test-plain-OB'
+
+test_expect_success \
+    'diff-tree of known trees.' \
+    'git diff-tree -r $tree_O $tree_B >.test-a &&
+     cmp -s .test-a .test-recursive-OB'
+
+test_expect_success \
+    'diff-tree of known trees.' \
+    'git diff-tree $tree_A $tree_B >.test-a &&
+     cmp -s .test-a .test-plain-AB'
+
+test_expect_success \
+    'diff-tree of known trees.' \
+    'git diff-tree -r $tree_A $tree_B >.test-a &&
+     cmp -s .test-a .test-recursive-AB'
+
+test_expect_success \
+    'diff-cache O with A in cache' \
+    'git read-tree $tree_A &&
+     git diff-index --cached $tree_O >.test-a &&
+     cmp -s .test-a .test-recursive-OA'
+
+test_expect_success \
+    'diff-cache O with B in cache' \
+    'git read-tree $tree_B &&
+     git diff-index --cached $tree_O >.test-a &&
+     cmp -s .test-a .test-recursive-OB'
+
+test_expect_success \
+    'diff-cache A with B in cache' \
+    'git read-tree $tree_B &&
+     git diff-index --cached $tree_A >.test-a &&
+     cmp -s .test-a .test-recursive-AB'
+
+test_expect_success \
+    'diff-files with O in cache and A checked out' \
+    'rm -fr Z [A-Z][A-Z] &&
+     git read-tree $tree_A &&
+     git checkout-index -f -a &&
+     git read-tree --reset $tree_O || return 1
+     git update-index --refresh >/dev/null ;# this can exit non-zero
+     git diff-files >.test-a &&
+     cmp_diff_files_output .test-a .test-recursive-OA'
+
+test_expect_success \
+    'diff-files with O in cache and B checked out' \
+    'rm -fr Z [A-Z][A-Z] &&
+     git read-tree $tree_B &&
+     git checkout-index -f -a &&
+     git read-tree --reset $tree_O || return 1
+     git update-index --refresh >/dev/null ;# this can exit non-zero
+     git diff-files >.test-a &&
+     cmp_diff_files_output .test-a .test-recursive-OB'
+
+test_expect_success \
+    'diff-files with A in cache and B checked out' \
+    'rm -fr Z [A-Z][A-Z] &&
+     git read-tree $tree_B &&
+     git checkout-index -f -a &&
+     git read-tree --reset $tree_A || return 1
+     git update-index --refresh >/dev/null ;# this can exit non-zero
+     git diff-files >.test-a &&
+     cmp_diff_files_output .test-a .test-recursive-AB'
+
+################################################################
+# Now we have established the baseline, we do not have to
+# rely on individual object ID values that much.
+
+test_expect_success \
+    'diff-tree O A == diff-tree -R A O' \
+    'git diff-tree $tree_O $tree_A >.test-a &&
+    git diff-tree -R $tree_A $tree_O >.test-b &&
+    cmp -s .test-a .test-b'
+
+test_expect_success \
+    'diff-tree -r O A == diff-tree -r -R A O' \
+    'git diff-tree -r $tree_O $tree_A >.test-a &&
+    git diff-tree -r -R $tree_A $tree_O >.test-b &&
+    cmp -s .test-a .test-b'
+
+test_expect_success \
+    'diff-tree B A == diff-tree -R A B' \
+    'git diff-tree $tree_B $tree_A >.test-a &&
+    git diff-tree -R $tree_A $tree_B >.test-b &&
+    cmp -s .test-a .test-b'
+
+test_expect_success \
+    'diff-tree -r B A == diff-tree -r -R A B' \
+    'git diff-tree -r $tree_B $tree_A >.test-a &&
+    git diff-tree -r -R $tree_A $tree_B >.test-b &&
+    cmp -s .test-a .test-b'
+
+test_done
diff --git a/t/t4003-diff-rename-1.sh b/t/t4003-diff-rename-1.sh
new file mode 100755 (executable)
index 0000000..8b1f875
--- /dev/null
@@ -0,0 +1,128 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='More rename detection
+
+'
+. ./test-lib.sh
+. ../diff-lib.sh ;# test-lib chdir's into trash
+
+test_expect_success \
+    'prepare reference tree' \
+    'cat ../../COPYING >COPYING &&
+     echo frotz >rezrov &&
+    git update-index --add COPYING rezrov &&
+    tree=$(git write-tree) &&
+    echo $tree'
+
+test_expect_success \
+    'prepare work tree' \
+    'sed -e 's/HOWEVER/However/' <COPYING >COPYING.1 &&
+    sed -e 's/GPL/G.P.L/g' <COPYING >COPYING.2 &&
+    rm -f COPYING &&
+    git update-index --add --remove COPYING COPYING.?'
+
+# tree has COPYING and rezrov.  work tree has COPYING.1 and COPYING.2,
+# both are slightly edited, and unchanged rezrov.  So we say you
+# copy-and-edit one, and rename-and-edit the other.  We do not say
+# anything about rezrov.
+
+GIT_DIFF_OPTS=--unified=0 git diff-index -M -p $tree >current
+cat >expected <<\EOF
+diff --git a/COPYING b/COPYING.1
+copy from COPYING
+copy to COPYING.1
+--- a/COPYING
++++ b/COPYING.1
+@@ -6 +6 @@
+- HOWEVER, in order to allow a migration to GPLv3 if that seems like
++ However, in order to allow a migration to GPLv3 if that seems like
+diff --git a/COPYING b/COPYING.2
+rename from COPYING
+rename to COPYING.2
+--- a/COPYING
++++ b/COPYING.2
+@@ -2 +2 @@
+- Note that the only valid version of the GPL as far as this project
++ Note that the only valid version of the G.P.L as far as this project
+@@ -6 +6 @@
+- HOWEVER, in order to allow a migration to GPLv3 if that seems like
++ HOWEVER, in order to allow a migration to G.P.Lv3 if that seems like
+@@ -12 +12 @@
+-      This file is licensed under the GPL v2, or a later version
++      This file is licensed under the G.P.L v2, or a later version
+EOF
+
+test_expect_success \
+    'validate output from rename/copy detection (#1)' \
+    'compare_diff_patch current expected'
+
+test_expect_success \
+    'prepare work tree again' \
+    'mv COPYING.2 COPYING &&
+     git update-index --add --remove COPYING COPYING.1 COPYING.2'
+
+# tree has COPYING and rezrov.  work tree has COPYING and COPYING.1,
+# both are slightly edited, and unchanged rezrov.  So we say you
+# edited one, and copy-and-edit the other.  We do not say
+# anything about rezrov.
+
+GIT_DIFF_OPTS=--unified=0 git diff-index -C -p $tree >current
+cat >expected <<\EOF
+diff --git a/COPYING b/COPYING
+--- a/COPYING
++++ b/COPYING
+@@ -2 +2 @@
+- Note that the only valid version of the GPL as far as this project
++ Note that the only valid version of the G.P.L as far as this project
+@@ -6 +6 @@
+- HOWEVER, in order to allow a migration to GPLv3 if that seems like
++ HOWEVER, in order to allow a migration to G.P.Lv3 if that seems like
+@@ -12 +12 @@
+-      This file is licensed under the GPL v2, or a later version
++      This file is licensed under the G.P.L v2, or a later version
+diff --git a/COPYING b/COPYING.1
+copy from COPYING
+copy to COPYING.1
+--- a/COPYING
++++ b/COPYING.1
+@@ -6 +6 @@
+- HOWEVER, in order to allow a migration to GPLv3 if that seems like
++ However, in order to allow a migration to GPLv3 if that seems like
+EOF
+
+test_expect_success \
+    'validate output from rename/copy detection (#2)' \
+    'compare_diff_patch current expected'
+
+test_expect_success \
+    'prepare work tree once again' \
+    'cat ../../COPYING >COPYING &&
+     git update-index --add --remove COPYING COPYING.1'
+
+# tree has COPYING and rezrov.  work tree has COPYING and COPYING.1,
+# but COPYING is not edited.  We say you copy-and-edit COPYING.1; this
+# is only possible because -C mode now reports the unmodified file to
+# the diff-core.  Unchanged rezrov, although being fed to
+# git diff-index as well, should not be mentioned.
+
+GIT_DIFF_OPTS=--unified=0 \
+    git diff-index -C --find-copies-harder -p $tree >current
+cat >expected <<\EOF
+diff --git a/COPYING b/COPYING.1
+copy from COPYING
+copy to COPYING.1
+--- a/COPYING
++++ b/COPYING.1
+@@ -6 +6 @@
+- HOWEVER, in order to allow a migration to GPLv3 if that seems like
++ However, in order to allow a migration to GPLv3 if that seems like
+EOF
+
+test_expect_success \
+    'validate output from rename/copy detection (#3)' \
+    'compare_diff_patch current expected'
+
+test_done
diff --git a/t/t4004-diff-rename-symlink.sh b/t/t4004-diff-rename-symlink.sh
new file mode 100755 (executable)
index 0000000..3d25be7
--- /dev/null
@@ -0,0 +1,67 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='More rename detection tests.
+
+The rename detection logic should be able to detect pure rename or
+copy of symbolic links, but should not produce rename/copy followed
+by an edit for them.
+'
+. ./test-lib.sh
+. ../diff-lib.sh
+
+test_expect_success \
+    'prepare reference tree' \
+    'echo xyzzy | tr -d '\\\\'012 >yomin &&
+     ln -s xyzzy frotz &&
+    git update-index --add frotz yomin &&
+    tree=$(git write-tree) &&
+    echo $tree'
+
+test_expect_success \
+    'prepare work tree' \
+    'mv frotz rezrov &&
+     rm -f yomin &&
+     ln -s xyzzy nitfol &&
+     ln -s xzzzy bozbar &&
+    git update-index --add --remove frotz rezrov nitfol bozbar yomin'
+
+# tree has frotz pointing at xyzzy, and yomin that contains xyzzy to
+# confuse things.  work tree has rezrov (xyzzy) nitfol (xyzzy) and
+# bozbar (xzzzy).
+# rezrov and nitfol are rename/copy of frotz and bozbar should be
+# a new creation.
+
+GIT_DIFF_OPTS=--unified=0 git diff-index -M -p $tree >current
+cat >expected <<\EOF
+diff --git a/bozbar b/bozbar
+new file mode 120000
+--- /dev/null
++++ b/bozbar
+@@ -0,0 +1 @@
++xzzzy
+\ No newline at end of file
+diff --git a/frotz b/nitfol
+similarity index 100%
+copy from frotz
+copy to nitfol
+diff --git a/frotz b/rezrov
+similarity index 100%
+rename from frotz
+rename to rezrov
+diff --git a/yomin b/yomin
+deleted file mode 100644
+--- a/yomin
++++ /dev/null
+@@ -1 +0,0 @@
+-xyzzy
+\ No newline at end of file
+EOF
+
+test_expect_success \
+    'validate diff output' \
+    'compare_diff_patch current expected'
+
+test_done
diff --git a/t/t4005-diff-rename-2.sh b/t/t4005-diff-rename-2.sh
new file mode 100755 (executable)
index 0000000..6630017
--- /dev/null
@@ -0,0 +1,86 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Same rename detection as t4003 but testing diff-raw.
+
+'
+. ./test-lib.sh
+. ../diff-lib.sh ;# test-lib chdir's into trash
+
+test_expect_success \
+    'prepare reference tree' \
+    'cat ../../COPYING >COPYING &&
+     echo frotz >rezrov &&
+    git update-index --add COPYING rezrov &&
+    tree=$(git write-tree) &&
+    echo $tree'
+
+test_expect_success \
+    'prepare work tree' \
+    'sed -e 's/HOWEVER/However/' <COPYING >COPYING.1 &&
+    sed -e 's/GPL/G.P.L/g' <COPYING >COPYING.2 &&
+    rm -f COPYING &&
+    git update-index --add --remove COPYING COPYING.?'
+
+# tree has COPYING and rezrov.  work tree has COPYING.1 and COPYING.2,
+# both are slightly edited, and unchanged rezrov.  We say COPYING.1
+# and COPYING.2 are based on COPYING, and do not say anything about
+# rezrov.
+
+git diff-index -M $tree >current
+
+cat >expected <<\EOF
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234 COPYING COPYING.1
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 06c67961bbaed34a127f76d261f4c0bf73eda471 R1234 COPYING COPYING.2
+EOF
+
+test_expect_success \
+    'validate output from rename/copy detection (#1)' \
+    'compare_diff_raw current expected'
+
+################################################################
+
+test_expect_success \
+    'prepare work tree again' \
+    'mv COPYING.2 COPYING &&
+     git update-index --add --remove COPYING COPYING.1 COPYING.2'
+
+# tree has COPYING and rezrov.  work tree has COPYING and COPYING.1,
+# both are slightly edited, and unchanged rezrov.  We say COPYING.1
+# is based on COPYING and COPYING is still there, and do not say anything
+# about rezrov.
+
+git diff-index -C $tree >current
+cat >expected <<\EOF
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 06c67961bbaed34a127f76d261f4c0bf73eda471 M     COPYING
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234 COPYING COPYING.1
+EOF
+
+test_expect_success \
+    'validate output from rename/copy detection (#2)' \
+    'compare_diff_raw current expected'
+
+################################################################
+
+# tree has COPYING and rezrov.  work tree has the same COPYING and
+# copy-edited COPYING.1, and unchanged rezrov.  We should not say
+# anything about rezrov nor COPYING, since the revised again diff-raw
+# nows how to say Copy.
+
+test_expect_success \
+    'prepare work tree once again' \
+    'cat ../../COPYING >COPYING &&
+     git update-index --add --remove COPYING COPYING.1'
+
+git diff-index -C --find-copies-harder $tree >current
+cat >expected <<\EOF
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234 COPYING COPYING.1
+EOF
+
+test_expect_success \
+    'validate output from rename/copy detection (#3)' \
+    'compare_diff_raw current expected'
+
+test_done
diff --git a/t/t4006-diff-mode.sh b/t/t4006-diff-mode.sh
new file mode 100755 (executable)
index 0000000..ab5406d
--- /dev/null
@@ -0,0 +1,43 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Test mode change diffs.
+
+'
+. ./test-lib.sh
+
+test_expect_success \
+    'setup' \
+    'echo frotz >rezrov &&
+     git update-index --add rezrov &&
+     tree=`git write-tree` &&
+     echo $tree'
+
+if [ "$(git config --get core.filemode)" = false ]
+then
+       say 'filemode disabled on the filesystem, using update-index --chmod=+x'
+       test_expect_success \
+           'git update-index --chmod=+x' \
+           'git update-index rezrov &&
+            git update-index --chmod=+x rezrov &&
+            git diff-index $tree >current'
+else
+       test_expect_success \
+           'chmod' \
+           'chmod +x rezrov &&
+            git update-index rezrov &&
+            git diff-index $tree >current'
+fi
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+sed -e 's/\(:100644 100755\) \('"$_x40"'\) \2 /\1 X X /' <current >check
+echo ":100644 100755 X X M     rezrov" >expected
+
+test_expect_success \
+    'verify' \
+    'git diff expected check'
+
+test_done
diff --git a/t/t4007-rename-3.sh b/t/t4007-rename-3.sh
new file mode 100755 (executable)
index 0000000..104a4e1
--- /dev/null
@@ -0,0 +1,90 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Rename interaction with pathspec.
+
+'
+. ./test-lib.sh
+. ../diff-lib.sh ;# test-lib chdir's into trash
+
+test_expect_success \
+    'prepare reference tree' \
+    'mkdir path0 path1 &&
+     cp ../../COPYING path0/COPYING &&
+     git update-index --add path0/COPYING &&
+    tree=$(git write-tree) &&
+    echo $tree'
+
+test_expect_success \
+    'prepare work tree' \
+    'cp path0/COPYING path1/COPYING &&
+     git update-index --add --remove path0/COPYING path1/COPYING'
+
+# In the tree, there is only path0/COPYING.  In the cache, path0 and
+# path1 both have COPYING and the latter is a copy of path0/COPYING.
+# Comparing the full tree with cache should tell us so.
+
+git diff-index -C --find-copies-harder $tree >current
+
+cat >expected <<\EOF
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 6ff87c4664981e4397625791c8ea3bbb5f2279a3 C100  path0/COPYING   path1/COPYING
+EOF
+
+test_expect_success \
+    'validate the result (#1)' \
+    'compare_diff_raw current expected'
+
+# In the tree, there is only path0/COPYING.  In the cache, path0 and
+# path1 both have COPYING and the latter is a copy of path0/COPYING.
+# However when we say we care only about path1, we should just see
+# path1/COPYING suddenly appearing from nowhere, not detected as
+# a copy from path0/COPYING.
+
+git diff-index -C $tree path1 >current
+
+cat >expected <<\EOF
+:000000 100644 0000000000000000000000000000000000000000 6ff87c4664981e4397625791c8ea3bbb5f2279a3 A     path1/COPYING
+EOF
+
+test_expect_success \
+    'validate the result (#2)' \
+    'compare_diff_raw current expected'
+
+test_expect_success \
+    'tweak work tree' \
+    'rm -f path0/COPYING &&
+     git update-index --remove path0/COPYING'
+
+# In the tree, there is only path0/COPYING.  In the cache, path0 does
+# not have COPYING anymore and path1 has COPYING which is a copy of
+# path0/COPYING.  Showing the full tree with cache should tell us about
+# the rename.
+
+git diff-index -C $tree >current
+
+cat >expected <<\EOF
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 6ff87c4664981e4397625791c8ea3bbb5f2279a3 R100  path0/COPYING   path1/COPYING
+EOF
+
+test_expect_success \
+    'validate the result (#3)' \
+    'compare_diff_raw current expected'
+
+# In the tree, there is only path0/COPYING.  In the cache, path0 does
+# not have COPYING anymore and path1 has COPYING which is a copy of
+# path0/COPYING.  When we say we care only about path1, we should just
+# see path1/COPYING appearing from nowhere.
+
+git diff-index -C $tree path1 >current
+
+cat >expected <<\EOF
+:000000 100644 0000000000000000000000000000000000000000 6ff87c4664981e4397625791c8ea3bbb5f2279a3 A     path1/COPYING
+EOF
+
+test_expect_success \
+    'validate the result (#4)' \
+    'compare_diff_raw current expected'
+
+test_done
diff --git a/t/t4008-diff-break-rewrite.sh b/t/t4008-diff-break-rewrite.sh
new file mode 100755 (executable)
index 0000000..26c2e4a
--- /dev/null
@@ -0,0 +1,188 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Break and then rename
+
+We have two very different files, file0 and file1, registered in a tree.
+
+We update file1 so drastically that it is more similar to file0, and
+then remove file0.  With -B, changes to file1 should be broken into
+separate delete and create, resulting in removal of file0, removal of
+original file1 and creation of completely rewritten file1.
+
+Further, with -B and -M together, these three modifications should
+turn into rename-edit of file0 into file1.
+
+Starting from the same two files in the tree, we swap file0 and file1.
+With -B, this should be detected as two complete rewrites, resulting in
+four changes in total.
+
+Further, with -B and -M together, these should turn into two renames.
+'
+. ./test-lib.sh
+. ../diff-lib.sh ;# test-lib chdir's into trash
+
+test_expect_success \
+    setup \
+    'cat ../../README >file0 &&
+     cat ../../COPYING >file1 &&
+    git update-index --add file0 file1 &&
+    tree=$(git write-tree) &&
+    echo "$tree"'
+
+test_expect_success \
+    'change file1 with copy-edit of file0 and remove file0' \
+    'sed -e "s/git/GIT/" file0 >file1 &&
+     rm -f file0 &&
+    git update-index --remove file0 file1'
+
+test_expect_success \
+    'run diff with -B' \
+    'git diff-index -B --cached "$tree" >current'
+
+cat >expected <<\EOF
+:100644 000000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 0000000000000000000000000000000000000000 D     file0
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 11e331465a89c394dc25c780de230043750c1ec8 M100  file1
+EOF
+
+test_expect_success \
+    'validate result of -B (#1)' \
+    'compare_diff_raw expected current'
+
+test_expect_success \
+    'run diff with -B and -M' \
+    'git diff-index -B -M "$tree" >current'
+
+cat >expected <<\EOF
+:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 08bb2fb671deff4c03a4d4a0a1315dff98d5732c R100  file0   file1
+EOF
+
+test_expect_success \
+    'validate result of -B -M (#2)' \
+    'compare_diff_raw expected current'
+
+test_expect_success \
+    'swap file0 and file1' \
+    'rm -f file0 file1 &&
+     git read-tree -m $tree &&
+     git checkout-index -f -u -a &&
+     mv file0 tmp &&
+     mv file1 file0 &&
+     mv tmp file1 &&
+     git update-index file0 file1'
+
+test_expect_success \
+    'run diff with -B' \
+    'git diff-index -B "$tree" >current'
+
+cat >expected <<\EOF
+:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 6ff87c4664981e4397625791c8ea3bbb5f2279a3 M100  file0
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 M100  file1
+EOF
+
+test_expect_success \
+    'validate result of -B (#3)' \
+    'compare_diff_raw expected current'
+
+test_expect_success \
+    'run diff with -B and -M' \
+    'git diff-index -B -M "$tree" >current'
+
+cat >expected <<\EOF
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 6ff87c4664981e4397625791c8ea3bbb5f2279a3 R100  file1   file0
+:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 R100  file0   file1
+EOF
+
+test_expect_success \
+    'validate result of -B -M (#4)' \
+    'compare_diff_raw expected current'
+
+test_expect_success \
+    'make file0 into something completely different' \
+    'rm -f file0 &&
+     ln -s frotz file0 &&
+     git update-index file0 file1'
+
+test_expect_success \
+    'run diff with -B' \
+    'git diff-index -B "$tree" >current'
+
+cat >expected <<\EOF
+:100644 120000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 67be421f88824578857624f7b3dc75e99a8a1481 T     file0
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 M100  file1
+EOF
+
+test_expect_success \
+    'validate result of -B (#5)' \
+    'compare_diff_raw expected current'
+
+test_expect_success \
+    'run diff with -B -M' \
+    'git diff-index -B -M "$tree" >current'
+
+# file0 changed from regular to symlink.  file1 is very close to the preimage of file0.
+# because we break file0, file1 can become a rename of it.
+cat >expected <<\EOF
+:100644 120000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 67be421f88824578857624f7b3dc75e99a8a1481 T     file0
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 R     file0   file1
+EOF
+
+test_expect_success \
+    'validate result of -B -M (#6)' \
+    'compare_diff_raw expected current'
+
+test_expect_success \
+    'run diff with -M' \
+    'git diff-index -M "$tree" >current'
+
+# This should not mistake file0 as the copy source of new file1
+# due to type differences.
+cat >expected <<\EOF
+:100644 120000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 67be421f88824578857624f7b3dc75e99a8a1481 T     file0
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 M     file1
+EOF
+
+test_expect_success \
+    'validate result of -M (#7)' \
+    'compare_diff_raw expected current'
+
+test_expect_success \
+    'file1 edited to look like file0 and file0 rename-edited to file2' \
+    'rm -f file0 file1 &&
+     git read-tree -m $tree &&
+     git checkout-index -f -u -a &&
+     sed -e "s/git/GIT/" file0 >file1 &&
+     sed -e "s/git/GET/" file0 >file2 &&
+     rm -f file0
+     git update-index --add --remove file0 file1 file2'
+
+test_expect_success \
+    'run diff with -B' \
+    'git diff-index -B "$tree" >current'
+
+cat >expected <<\EOF
+:100644 000000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 0000000000000000000000000000000000000000 D     file0
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 08bb2fb671deff4c03a4d4a0a1315dff98d5732c M100  file1
+:000000 100644 0000000000000000000000000000000000000000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 A     file2
+EOF
+
+test_expect_success \
+    'validate result of -B (#8)' \
+    'compare_diff_raw expected current'
+
+test_expect_success \
+    'run diff with -B -M' \
+    'git diff-index -B -M "$tree" >current'
+
+cat >expected <<\EOF
+:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 08bb2fb671deff4c03a4d4a0a1315dff98d5732c C095  file0   file1
+:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 59f832e5c8b3f7e486be15ad0cd3e95ba9af8998 R095  file0   file2
+EOF
+
+test_expect_success \
+    'validate result of -B -M (#9)' \
+    'compare_diff_raw expected current'
+
+test_done
diff --git a/t/t4009-diff-rename-4.sh b/t/t4009-diff-rename-4.sh
new file mode 100755 (executable)
index 0000000..d2b45e7
--- /dev/null
@@ -0,0 +1,95 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Same rename detection as t4003 but testing diff-raw -z.
+
+'
+. ./test-lib.sh
+. ../diff-lib.sh ;# test-lib chdir's into trash
+
+test_expect_success \
+    'prepare reference tree' \
+    'cat ../../COPYING >COPYING &&
+     echo frotz >rezrov &&
+    git update-index --add COPYING rezrov &&
+    tree=$(git write-tree) &&
+    echo $tree'
+
+test_expect_success \
+    'prepare work tree' \
+    'sed -e 's/HOWEVER/However/' <COPYING >COPYING.1 &&
+    sed -e 's/GPL/G.P.L/g' <COPYING >COPYING.2 &&
+    rm -f COPYING &&
+    git update-index --add --remove COPYING COPYING.?'
+
+# tree has COPYING and rezrov.  work tree has COPYING.1 and COPYING.2,
+# both are slightly edited, and unchanged rezrov.  We say COPYING.1
+# and COPYING.2 are based on COPYING, and do not say anything about
+# rezrov.
+
+git diff-index -z -M $tree >current
+
+cat >expected <<\EOF
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234
+COPYING
+COPYING.1
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 06c67961bbaed34a127f76d261f4c0bf73eda471 R1234
+COPYING
+COPYING.2
+EOF
+
+test_expect_success \
+    'validate output from rename/copy detection (#1)' \
+    'compare_diff_raw_z current expected'
+
+################################################################
+
+test_expect_success \
+    'prepare work tree again' \
+    'mv COPYING.2 COPYING &&
+     git update-index --add --remove COPYING COPYING.1 COPYING.2'
+
+# tree has COPYING and rezrov.  work tree has COPYING and COPYING.1,
+# both are slightly edited, and unchanged rezrov.  We say COPYING.1
+# is based on COPYING and COPYING is still there, and do not say anything
+# about rezrov.
+
+git diff-index -z -C $tree >current
+cat >expected <<\EOF
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 06c67961bbaed34a127f76d261f4c0bf73eda471 M
+COPYING
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234
+COPYING
+COPYING.1
+EOF
+
+test_expect_success \
+    'validate output from rename/copy detection (#2)' \
+    'compare_diff_raw_z current expected'
+
+################################################################
+
+# tree has COPYING and rezrov.  work tree has the same COPYING and
+# copy-edited COPYING.1, and unchanged rezrov.  We should not say
+# anything about rezrov nor COPYING, since the revised again diff-raw
+# nows how to say Copy.
+
+test_expect_success \
+    'prepare work tree once again' \
+    'cat ../../COPYING >COPYING &&
+     git update-index --add --remove COPYING COPYING.1'
+
+git diff-index -z -C --find-copies-harder $tree >current
+cat >expected <<\EOF
+:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234
+COPYING
+COPYING.1
+EOF
+
+test_expect_success \
+    'validate output from rename/copy detection (#3)' \
+    'compare_diff_raw_z current expected'
+
+test_done
diff --git a/t/t4010-diff-pathspec.sh b/t/t4010-diff-pathspec.sh
new file mode 100755 (executable)
index 0000000..ad3d9e4
--- /dev/null
@@ -0,0 +1,65 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Pathspec restrictions
+
+Prepare:
+        file0
+        path1/file1
+'
+. ./test-lib.sh
+. ../diff-lib.sh ;# test-lib chdir's into trash
+
+test_expect_success \
+    setup \
+    'echo frotz >file0 &&
+     mkdir path1 &&
+     echo rezrov >path1/file1 &&
+     git update-index --add file0 path1/file1 &&
+     tree=`git write-tree` &&
+     echo "$tree" &&
+     echo nitfol >file0 &&
+     echo yomin >path1/file1 &&
+     git update-index file0 path1/file1'
+
+cat >expected <<\EOF
+EOF
+test_expect_success \
+    'limit to path should show nothing' \
+    'git diff-index --cached $tree -- path >current &&
+     compare_diff_raw current expected'
+
+cat >expected <<\EOF
+:100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M     path1/file1
+EOF
+test_expect_success \
+    'limit to path1 should show path1/file1' \
+    'git diff-index --cached $tree -- path1 >current &&
+     compare_diff_raw current expected'
+
+cat >expected <<\EOF
+:100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M     path1/file1
+EOF
+test_expect_success \
+    'limit to path1/ should show path1/file1' \
+    'git diff-index --cached $tree -- path1/ >current &&
+     compare_diff_raw current expected'
+
+cat >expected <<\EOF
+:100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M     file0
+EOF
+test_expect_success \
+    'limit to file0 should show file0' \
+    'git diff-index --cached $tree -- file0 >current &&
+     compare_diff_raw current expected'
+
+cat >expected <<\EOF
+EOF
+test_expect_success \
+    'limit to file0/ should emit nothing.' \
+    'git diff-index --cached $tree -- file0/ >current &&
+     compare_diff_raw current expected'
+
+test_done
diff --git a/t/t4011-diff-symlink.sh b/t/t4011-diff-symlink.sh
new file mode 100755 (executable)
index 0000000..c6d1369
--- /dev/null
@@ -0,0 +1,85 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Johannes Schindelin
+#
+
+test_description='Test diff of symlinks.
+
+'
+. ./test-lib.sh
+. ../diff-lib.sh
+
+cat > expected << EOF
+diff --git a/frotz b/frotz
+new file mode 120000
+index 0000000..7c465af
+--- /dev/null
++++ b/frotz
+@@ -0,0 +1 @@
++xyzzy
+\ No newline at end of file
+EOF
+
+test_expect_success \
+    'diff new symlink' \
+    'ln -s xyzzy frotz &&
+    git update-index &&
+    tree=$(git write-tree) &&
+    git update-index --add frotz &&
+    GIT_DIFF_OPTS=--unified=0 git diff-index -M -p $tree > current &&
+    compare_diff_patch current expected'
+
+test_expect_success \
+    'diff unchanged symlink' \
+    'tree=$(git write-tree) &&
+    git update-index frotz &&
+    test -z "$(git diff-index --name-only $tree)"'
+
+cat > expected << EOF
+diff --git a/frotz b/frotz
+deleted file mode 120000
+index 7c465af..0000000
+--- a/frotz
++++ /dev/null
+@@ -1 +0,0 @@
+-xyzzy
+\ No newline at end of file
+EOF
+
+test_expect_success \
+    'diff removed symlink' \
+    'rm frotz &&
+    git diff-index -M -p $tree > current &&
+    compare_diff_patch current expected'
+
+cat > expected << EOF
+diff --git a/frotz b/frotz
+EOF
+
+test_expect_success \
+    'diff identical, but newly created symlink' \
+    'sleep 3 &&
+    ln -s xyzzy frotz &&
+    git diff-index -M -p $tree > current &&
+    compare_diff_patch current expected'
+
+cat > expected << EOF
+diff --git a/frotz b/frotz
+index 7c465af..df1db54 120000
+--- a/frotz
++++ b/frotz
+@@ -1 +1 @@
+-xyzzy
+\ No newline at end of file
++yxyyz
+\ No newline at end of file
+EOF
+
+test_expect_success \
+    'diff different symlink' \
+    'rm frotz &&
+    ln -s yxyyz frotz &&
+    git diff-index -M -p $tree > current &&
+    compare_diff_patch current expected'
+
+test_done
diff --git a/t/t4012-diff-binary.sh b/t/t4012-diff-binary.sh
new file mode 100755 (executable)
index 0000000..eced1f3
--- /dev/null
@@ -0,0 +1,80 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='Binary diff and apply
+'
+
+. ./test-lib.sh
+
+test_expect_success 'prepare repository' \
+       'echo AIT >a && echo BIT >b && echo CIT >c && echo DIT >d &&
+        git update-index --add a b c d &&
+        echo git >a &&
+        cat ../test4012.png >b &&
+        echo git >c &&
+        cat b b >d'
+
+cat > expected <<\EOF
+ a |    2 +-
+ b |  Bin
+ c |    2 +-
+ d |  Bin
+ 4 files changed, 2 insertions(+), 2 deletions(-)
+EOF
+test_expect_success 'diff without --binary' \
+       'git diff | git apply --stat --summary >current &&
+        cmp current expected'
+
+test_expect_success 'diff with --binary' \
+       'git diff --binary | git apply --stat --summary >current &&
+        cmp current expected'
+
+# apply needs to be able to skip the binary material correctly
+# in order to report the line number of a corrupt patch.
+test_expect_success 'apply detecting corrupt patch correctly' \
+       'git diff | sed -e 's/-CIT/xCIT/' >broken &&
+        if git apply --stat --summary broken 2>detected
+        then
+               echo unhappy - should have detected an error
+               (exit 1)
+        else
+               echo happy
+        fi &&
+        detected=`cat detected` &&
+        detected=`expr "$detected" : "fatal.*at line \\([0-9]*\\)\$"` &&
+        detected=`sed -ne "${detected}p" broken` &&
+        test "$detected" = xCIT'
+
+test_expect_success 'apply detecting corrupt patch correctly' \
+       'git diff --binary | sed -e 's/-CIT/xCIT/' >broken &&
+        if git apply --stat --summary broken 2>detected
+        then
+               echo unhappy - should have detected an error
+               (exit 1)
+        else
+               echo happy
+        fi &&
+        detected=`cat detected` &&
+        detected=`expr "$detected" : "fatal.*at line \\([0-9]*\\)\$"` &&
+        detected=`sed -ne "${detected}p" broken` &&
+        test "$detected" = xCIT'
+
+test_expect_success 'initial commit' 'git-commit -a -m initial'
+
+# Try removal (b), modification (d), and creation (e).
+test_expect_success 'diff-index with --binary' \
+       'echo AIT >a && mv b e && echo CIT >c && cat e >d &&
+        git update-index --add --remove a b c d e &&
+        tree0=`git write-tree` &&
+        git diff --cached --binary >current &&
+        git apply --stat --summary current'
+
+test_expect_success 'apply binary patch' \
+       'git-reset --hard &&
+        git apply --binary --index <current &&
+        tree1=`git write-tree` &&
+        test "$tree1" = "$tree0"'
+
+test_done
diff --git a/t/t4013-diff-various.sh b/t/t4013-diff-various.sh
new file mode 100755 (executable)
index 0000000..9eec754
--- /dev/null
@@ -0,0 +1,261 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='Various diff formatting options'
+
+. ./test-lib.sh
+
+LF='
+'
+
+test_expect_success setup '
+
+       GIT_AUTHOR_DATE="2006-06-26 00:00:00 +0000" &&
+       GIT_COMMITTER_DATE="2006-06-26 00:00:00 +0000" &&
+       export GIT_AUTHOR_DATE GIT_COMMITTER_DATE &&
+
+       mkdir dir &&
+       mkdir dir2 &&
+       for i in 1 2 3; do echo $i; done >file0 &&
+       for i in A B; do echo $i; done >dir/sub &&
+       cat file0 >file2 &&
+       git add file0 file2 dir/sub &&
+       git commit -m Initial &&
+
+       git branch initial &&
+       git branch side &&
+
+       GIT_AUTHOR_DATE="2006-06-26 00:01:00 +0000" &&
+       GIT_COMMITTER_DATE="2006-06-26 00:01:00 +0000" &&
+       export GIT_AUTHOR_DATE GIT_COMMITTER_DATE &&
+
+       for i in 4 5 6; do echo $i; done >>file0 &&
+       for i in C D; do echo $i; done >>dir/sub &&
+       rm -f file2 &&
+       git update-index --remove file0 file2 dir/sub &&
+       git commit -m "Second${LF}${LF}This is the second commit." &&
+
+       GIT_AUTHOR_DATE="2006-06-26 00:02:00 +0000" &&
+       GIT_COMMITTER_DATE="2006-06-26 00:02:00 +0000" &&
+       export GIT_AUTHOR_DATE GIT_COMMITTER_DATE &&
+
+       for i in A B C; do echo $i; done >file1 &&
+       git add file1 &&
+       for i in E F; do echo $i; done >>dir/sub &&
+       git update-index dir/sub &&
+       git commit -m Third &&
+
+       GIT_AUTHOR_DATE="2006-06-26 00:03:00 +0000" &&
+       GIT_COMMITTER_DATE="2006-06-26 00:03:00 +0000" &&
+       export GIT_AUTHOR_DATE GIT_COMMITTER_DATE &&
+
+       git checkout side &&
+       for i in A B C; do echo $i; done >>file0 &&
+       for i in 1 2; do echo $i; done >>dir/sub &&
+       cat dir/sub >file3 &&
+       git add file3 &&
+       git update-index file0 dir/sub &&
+       git commit -m Side &&
+
+       GIT_AUTHOR_DATE="2006-06-26 00:04:00 +0000" &&
+       GIT_COMMITTER_DATE="2006-06-26 00:04:00 +0000" &&
+       export GIT_AUTHOR_DATE GIT_COMMITTER_DATE &&
+
+       git checkout master &&
+       git pull -s ours . side &&
+
+       GIT_AUTHOR_DATE="2006-06-26 00:05:00 +0000" &&
+       GIT_COMMITTER_DATE="2006-06-26 00:05:00 +0000" &&
+       export GIT_AUTHOR_DATE GIT_COMMITTER_DATE &&
+
+       for i in A B C; do echo $i; done >>file0 &&
+       for i in 1 2; do echo $i; done >>dir/sub &&
+       git update-index file0 dir/sub &&
+
+       git config log.showroot false &&
+       git commit --amend &&
+       git show-branch
+'
+
+: <<\EOF
+! [initial] Initial
+ * [master] Merge branch 'side'
+  ! [side] Side
+---
+ -  [master] Merge branch 'side'
+ *+ [side] Side
+ *  [master^] Second
++*+ [initial] Initial
+EOF
+
+V=`git version | sed -e 's/^git version //' -e 's/\./\\./g'`
+while read cmd
+do
+       case "$cmd" in
+       '' | '#'*) continue ;;
+       esac
+       test=`echo "$cmd" | sed -e 's|[/ ][/ ]*|_|g'`
+       cnt=`expr $test_count + 1`
+       pfx=`printf "%04d" $cnt`
+       expect="../t4013/diff.$test"
+       actual="$pfx-diff.$test"
+
+       test_expect_success "git $cmd" '
+               {
+                       echo "\$ git $cmd"
+                       git $cmd |
+                       sed -e "s/^\\(-*\\)$V\\(-*\\)\$/\\1g-i-t--v-e-r-s-i-o-n\2/" \
+                           -e "s/^\\(.*mixed; boundary=\"-*\\)$V\\(-*\\)\"\$/\\1g-i-t--v-e-r-s-i-o-n\2\"/"
+                       echo "\$"
+               } >"$actual" &&
+               if test -f "$expect"
+               then
+                       git diff "$expect" "$actual" &&
+                       rm -f "$actual"
+               else
+                       # this is to help developing new tests.
+                       cp "$actual" "$expect"
+                       false
+               fi
+       '
+done <<\EOF
+diff-tree initial
+diff-tree -r initial
+diff-tree -r --abbrev initial
+diff-tree -r --abbrev=4 initial
+diff-tree --root initial
+diff-tree --root --abbrev initial
+diff-tree --root -r initial
+diff-tree --root -r --abbrev initial
+diff-tree --root -r --abbrev=4 initial
+diff-tree -p initial
+diff-tree --root -p initial
+diff-tree --patch-with-stat initial
+diff-tree --root --patch-with-stat initial
+diff-tree --patch-with-raw initial
+diff-tree --root --patch-with-raw initial
+
+diff-tree --pretty initial
+diff-tree --pretty --root initial
+diff-tree --pretty -p initial
+diff-tree --pretty --stat initial
+diff-tree --pretty --summary initial
+diff-tree --pretty --stat --summary initial
+diff-tree --pretty --root -p initial
+diff-tree --pretty --root --stat initial
+# improved by Timo's patch
+diff-tree --pretty --root --summary initial
+# improved by Timo's patch
+diff-tree --pretty --root --summary -r initial
+diff-tree --pretty --root --stat --summary initial
+diff-tree --pretty --patch-with-stat initial
+diff-tree --pretty --root --patch-with-stat initial
+diff-tree --pretty --patch-with-raw initial
+diff-tree --pretty --root --patch-with-raw initial
+
+diff-tree --pretty=oneline initial
+diff-tree --pretty=oneline --root initial
+diff-tree --pretty=oneline -p initial
+diff-tree --pretty=oneline --root -p initial
+diff-tree --pretty=oneline --patch-with-stat initial
+# improved by Timo's patch
+diff-tree --pretty=oneline --root --patch-with-stat initial
+diff-tree --pretty=oneline --patch-with-raw initial
+diff-tree --pretty=oneline --root --patch-with-raw initial
+
+diff-tree --pretty side
+diff-tree --pretty -p side
+diff-tree --pretty --patch-with-stat side
+
+diff-tree master
+diff-tree -p master
+diff-tree -p -m master
+diff-tree -c master
+diff-tree -c --abbrev master
+diff-tree --cc master
+# stat only should show the diffstat with the first parent
+diff-tree -c --stat master
+diff-tree --cc --stat master
+diff-tree -c --stat --summary master
+diff-tree --cc --stat --summary master
+# stat summary should show the diffstat and summary with the first parent
+diff-tree -c --stat --summary side
+diff-tree --cc --stat --summary side
+# improved by Timo's patch
+diff-tree --cc --patch-with-stat master
+# improved by Timo's patch
+diff-tree --cc --patch-with-stat --summary master
+# this is correct
+diff-tree --cc --patch-with-stat --summary side
+
+log master
+log -p master
+log --root master
+log --root -p master
+log --patch-with-stat master
+log --root --patch-with-stat master
+log --root --patch-with-stat --summary master
+# improved by Timo's patch
+log --root -c --patch-with-stat --summary master
+# improved by Timo's patch
+log --root --cc --patch-with-stat --summary master
+log -SF master
+log -SF -p master
+
+whatchanged master
+whatchanged -p master
+whatchanged --root master
+whatchanged --root -p master
+whatchanged --patch-with-stat master
+whatchanged --root --patch-with-stat master
+whatchanged --root --patch-with-stat --summary master
+# improved by Timo's patch
+whatchanged --root -c --patch-with-stat --summary master
+# improved by Timo's patch
+whatchanged --root --cc --patch-with-stat --summary master
+whatchanged -SF master
+whatchanged -SF -p master
+
+log --patch-with-stat master -- dir/
+whatchanged --patch-with-stat master -- dir/
+log --patch-with-stat --summary master -- dir/
+whatchanged --patch-with-stat --summary master -- dir/
+
+show initial
+show --root initial
+show side
+show master
+show --stat side
+show --stat --summary side
+show --patch-with-stat side
+show --patch-with-raw side
+show --patch-with-stat --summary side
+
+format-patch --stdout initial..side
+format-patch --stdout initial..master^
+format-patch --stdout initial..master
+format-patch --attach --stdout initial..side
+format-patch --attach --stdout initial..master^
+format-patch --attach --stdout initial..master
+format-patch --inline --stdout initial..side
+format-patch --inline --stdout initial..master^
+format-patch --inline --stdout initial..master
+format-patch --inline --stdout --subject-prefix=TESTCASE initial..master
+config format.subjectprefix DIFFERENT_PREFIX
+format-patch --inline --stdout initial..master^^
+
+diff --abbrev initial..side
+diff -r initial..side
+diff --stat initial..side
+diff -r --stat initial..side
+diff initial..side
+diff --patch-with-stat initial..side
+diff --patch-with-raw initial..side
+diff --patch-with-stat -r initial..side
+diff --patch-with-raw -r initial..side
+diff --name-status dir2 dir
+EOF
+
+test_done
diff --git a/t/t4013/diff.config_format.subjectprefix_DIFFERENT_PREFIX b/t/t4013/diff.config_format.subjectprefix_DIFFERENT_PREFIX
new file mode 100644 (file)
index 0000000..78f8970
--- /dev/null
@@ -0,0 +1,2 @@
+$ git config format.subjectprefix DIFFERENT_PREFIX
+$
diff --git a/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_master b/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_master
new file mode 100644 (file)
index 0000000..3a9f78a
--- /dev/null
@@ -0,0 +1,34 @@
+$ git diff-tree --cc --patch-with-stat --summary master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --cc dir/sub
+index cead32e,7289e35..992913c
+--- a/dir/sub
++++ b/dir/sub
+@@@ -1,6 -1,4 +1,8 @@@
+  A
+  B
+ +C
+ +D
+ +E
+ +F
++ 1
++ 2
+diff --cc file0
+index b414108,f4615da..10a8a9f
+--- a/file0
++++ b/file0
+@@@ -1,6 -1,6 +1,9 @@@
+  1
+  2
+  3
+ +4
+ +5
+ +6
++ A
++ B
++ C
+$
diff --git a/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_side b/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_side
new file mode 100644 (file)
index 0000000..a61ad8c
--- /dev/null
@@ -0,0 +1,39 @@
+$ git diff-tree --cc --patch-with-stat --summary side
+c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.diff-tree_--cc_--patch-with-stat_master b/t/t4013/diff.diff-tree_--cc_--patch-with-stat_master
new file mode 100644 (file)
index 0000000..49f23b9
--- /dev/null
@@ -0,0 +1,34 @@
+$ git diff-tree --cc --patch-with-stat master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --cc dir/sub
+index cead32e,7289e35..992913c
+--- a/dir/sub
++++ b/dir/sub
+@@@ -1,6 -1,4 +1,8 @@@
+  A
+  B
+ +C
+ +D
+ +E
+ +F
++ 1
++ 2
+diff --cc file0
+index b414108,f4615da..10a8a9f
+--- a/file0
++++ b/file0
+@@@ -1,6 -1,6 +1,9 @@@
+  1
+  2
+  3
+ +4
+ +5
+ +6
++ A
++ B
++ C
+$
diff --git a/t/t4013/diff.diff-tree_--cc_--stat_--summary_master b/t/t4013/diff.diff-tree_--cc_--stat_--summary_master
new file mode 100644 (file)
index 0000000..cc6eb3b
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree --cc --stat --summary master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+$
diff --git a/t/t4013/diff.diff-tree_--cc_--stat_--summary_side b/t/t4013/diff.diff-tree_--cc_--stat_--summary_side
new file mode 100644 (file)
index 0000000..50362be
--- /dev/null
@@ -0,0 +1,8 @@
+$ git diff-tree --cc --stat --summary side
+c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+$
diff --git a/t/t4013/diff.diff-tree_--cc_--stat_master b/t/t4013/diff.diff-tree_--cc_--stat_master
new file mode 100644 (file)
index 0000000..fae7f33
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree --cc --stat master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+$
diff --git a/t/t4013/diff.diff-tree_--cc_master b/t/t4013/diff.diff-tree_--cc_master
new file mode 100644 (file)
index 0000000..5ecb4e1
--- /dev/null
@@ -0,0 +1,30 @@
+$ git diff-tree --cc master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+diff --cc dir/sub
+index cead32e,7289e35..992913c
+--- a/dir/sub
++++ b/dir/sub
+@@@ -1,6 -1,4 +1,8 @@@
+  A
+  B
+ +C
+ +D
+ +E
+ +F
++ 1
++ 2
+diff --cc file0
+index b414108,f4615da..10a8a9f
+--- a/file0
++++ b/file0
+@@@ -1,6 -1,6 +1,9 @@@
+  1
+  2
+  3
+ +4
+ +5
+ +6
++ A
++ B
++ C
+$
diff --git a/t/t4013/diff.diff-tree_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--patch-with-raw_initial
new file mode 100644 (file)
index 0000000..fc177ab
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --patch-with-raw initial
+$
diff --git a/t/t4013/diff.diff-tree_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--patch-with-stat_initial
new file mode 100644 (file)
index 0000000..bd905b1
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --patch-with-stat initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-raw_initial
new file mode 100644 (file)
index 0000000..7bb8b45
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty=oneline --patch-with-raw initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-stat_initial
new file mode 100644 (file)
index 0000000..cbdde4f
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty=oneline --patch-with-stat initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-raw_initial
new file mode 100644 (file)
index 0000000..cd79f1a
--- /dev/null
@@ -0,0 +1,33 @@
+$ git diff-tree --pretty=oneline --root --patch-with-raw initial
+444ac553ac7612cc88969031b02b3767fb8a353a Initial
+:000000 100644 0000000000000000000000000000000000000000 35d242ba79ae89ac695e26b3d4c27a8e6f028f9e A     dir/sub
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file0
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file2
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-stat_initial
new file mode 100644 (file)
index 0000000..d5c333a
--- /dev/null
@@ -0,0 +1,34 @@
+$ git diff-tree --pretty=oneline --root --patch-with-stat initial
+444ac553ac7612cc88969031b02b3767fb8a353a Initial
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--root_-p_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--root_-p_initial
new file mode 100644 (file)
index 0000000..3c5092c
--- /dev/null
@@ -0,0 +1,29 @@
+$ git diff-tree --pretty=oneline --root -p initial
+444ac553ac7612cc88969031b02b3767fb8a353a Initial
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--root_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--root_initial
new file mode 100644 (file)
index 0000000..08920ac
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree --pretty=oneline --root initial
+444ac553ac7612cc88969031b02b3767fb8a353a Initial
+:000000 040000 0000000000000000000000000000000000000000 da7a33fa77d8066d6698643940ce5860fe2d7fb3 A     dir
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file0
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file2
+$
diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_-p_initial b/t/t4013/diff.diff-tree_--pretty=oneline_-p_initial
new file mode 100644 (file)
index 0000000..94b76bf
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty=oneline -p initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_initial b/t/t4013/diff.diff-tree_--pretty=oneline_initial
new file mode 100644 (file)
index 0000000..d50970d
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty=oneline initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--pretty_--patch-with-raw_initial
new file mode 100644 (file)
index 0000000..3a85316
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty --patch-with-raw initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_initial
new file mode 100644 (file)
index 0000000..2e08239
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty --patch-with-stat initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_side b/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_side
new file mode 100644 (file)
index 0000000..4d30e7e
--- /dev/null
@@ -0,0 +1,43 @@
+$ git diff-tree --pretty --patch-with-stat side
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-raw_initial
new file mode 100644 (file)
index 0000000..a3203bd
--- /dev/null
@@ -0,0 +1,38 @@
+$ git diff-tree --pretty --root --patch-with-raw initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+:000000 100644 0000000000000000000000000000000000000000 35d242ba79ae89ac695e26b3d4c27a8e6f028f9e A     dir/sub
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file0
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file2
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-stat_initial
new file mode 100644 (file)
index 0000000..7dfa6af
--- /dev/null
@@ -0,0 +1,39 @@
+$ git diff-tree --pretty --root --patch-with-stat initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--stat_--summary_initial b/t/t4013/diff.diff-tree_--pretty_--root_--stat_--summary_initial
new file mode 100644 (file)
index 0000000..43bfce2
--- /dev/null
@@ -0,0 +1,15 @@
+$ git diff-tree --pretty --root --stat --summary initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+ create mode 100644 dir/sub
+ create mode 100644 file0
+ create mode 100644 file2
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--stat_initial b/t/t4013/diff.diff-tree_--pretty_--root_--stat_initial
new file mode 100644 (file)
index 0000000..9154aa4
--- /dev/null
@@ -0,0 +1,12 @@
+$ git diff-tree --pretty --root --stat initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--summary_-r_initial b/t/t4013/diff.diff-tree_--pretty_--root_--summary_-r_initial
new file mode 100644 (file)
index 0000000..ccdaafb
--- /dev/null
@@ -0,0 +1,11 @@
+$ git diff-tree --pretty --root --summary -r initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+ create mode 100644 dir/sub
+ create mode 100644 file0
+ create mode 100644 file2
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--summary_initial b/t/t4013/diff.diff-tree_--pretty_--root_--summary_initial
new file mode 100644 (file)
index 0000000..58e5f74
--- /dev/null
@@ -0,0 +1,11 @@
+$ git diff-tree --pretty --root --summary initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+ create mode 100644 dir/sub
+ create mode 100644 file0
+ create mode 100644 file2
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--root_-p_initial b/t/t4013/diff.diff-tree_--pretty_--root_-p_initial
new file mode 100644 (file)
index 0000000..d0411f6
--- /dev/null
@@ -0,0 +1,34 @@
+$ git diff-tree --pretty --root -p initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--root_initial b/t/t4013/diff.diff-tree_--pretty_--root_initial
new file mode 100644 (file)
index 0000000..94e32ea
--- /dev/null
@@ -0,0 +1,11 @@
+$ git diff-tree --pretty --root initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+:000000 040000 0000000000000000000000000000000000000000 da7a33fa77d8066d6698643940ce5860fe2d7fb3 A     dir
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file0
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file2
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--stat_--summary_initial b/t/t4013/diff.diff-tree_--pretty_--stat_--summary_initial
new file mode 100644 (file)
index 0000000..c22983a
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty --stat --summary initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--stat_initial b/t/t4013/diff.diff-tree_--pretty_--stat_initial
new file mode 100644 (file)
index 0000000..8fdcfb4
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty --stat initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_--summary_initial b/t/t4013/diff.diff-tree_--pretty_--summary_initial
new file mode 100644 (file)
index 0000000..9bc2c4f
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty --summary initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_-p_initial b/t/t4013/diff.diff-tree_--pretty_-p_initial
new file mode 100644 (file)
index 0000000..3c9942f
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty -p initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_-p_side b/t/t4013/diff.diff-tree_--pretty_-p_side
new file mode 100644 (file)
index 0000000..b993aa7
--- /dev/null
@@ -0,0 +1,38 @@
+$ git diff-tree --pretty -p side
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_initial b/t/t4013/diff.diff-tree_--pretty_initial
new file mode 100644 (file)
index 0000000..14715bf
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree --pretty initial
+$
diff --git a/t/t4013/diff.diff-tree_--pretty_side b/t/t4013/diff.diff-tree_--pretty_side
new file mode 100644 (file)
index 0000000..e9b6e1c
--- /dev/null
@@ -0,0 +1,11 @@
+$ git diff-tree --pretty side
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+:040000 040000 da7a33fa77d8066d6698643940ce5860fe2d7fb3 f977ed46ae6873c1c30ab878e15a4accedc3618b M     dir
+:100644 100644 01e79c32a8c99c557f0757da7cb6d65b3414466d f4615da674c09df322d6ba8d6b21ecfb1b1ba510 M     file0
+:000000 100644 0000000000000000000000000000000000000000 7289e35bff32727c08dda207511bec138fdb9ea5 A     file3
+$
diff --git a/t/t4013/diff.diff-tree_--root_--abbrev_initial b/t/t4013/diff.diff-tree_--root_--abbrev_initial
new file mode 100644 (file)
index 0000000..5aa84b2
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree --root --abbrev initial
+444ac553ac7612cc88969031b02b3767fb8a353a
+:000000 040000 0000000... da7a33f... A dir
+:000000 100644 0000000... 01e79c3... A file0
+:000000 100644 0000000... 01e79c3... A file2
+$
diff --git a/t/t4013/diff.diff-tree_--root_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--root_--patch-with-raw_initial
new file mode 100644 (file)
index 0000000..d295e47
--- /dev/null
@@ -0,0 +1,33 @@
+$ git diff-tree --root --patch-with-raw initial
+444ac553ac7612cc88969031b02b3767fb8a353a
+:000000 100644 0000000000000000000000000000000000000000 35d242ba79ae89ac695e26b3d4c27a8e6f028f9e A     dir/sub
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file0
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file2
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.diff-tree_--root_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--root_--patch-with-stat_initial
new file mode 100644 (file)
index 0000000..1562b62
--- /dev/null
@@ -0,0 +1,34 @@
+$ git diff-tree --root --patch-with-stat initial
+444ac553ac7612cc88969031b02b3767fb8a353a
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.diff-tree_--root_-p_initial b/t/t4013/diff.diff-tree_--root_-p_initial
new file mode 100644 (file)
index 0000000..3219c72
--- /dev/null
@@ -0,0 +1,29 @@
+$ git diff-tree --root -p initial
+444ac553ac7612cc88969031b02b3767fb8a353a
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.diff-tree_--root_-r_--abbrev=4_initial b/t/t4013/diff.diff-tree_--root_-r_--abbrev=4_initial
new file mode 100644 (file)
index 0000000..0c53616
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree --root -r --abbrev=4 initial
+444ac553ac7612cc88969031b02b3767fb8a353a
+:000000 100644 0000... 35d2... A       dir/sub
+:000000 100644 0000... 01e7... A       file0
+:000000 100644 0000... 01e7... A       file2
+$
diff --git a/t/t4013/diff.diff-tree_--root_-r_--abbrev_initial b/t/t4013/diff.diff-tree_--root_-r_--abbrev_initial
new file mode 100644 (file)
index 0000000..c7b460f
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree --root -r --abbrev initial
+444ac553ac7612cc88969031b02b3767fb8a353a
+:000000 100644 0000000... 35d242b... A dir/sub
+:000000 100644 0000000... 01e79c3... A file0
+:000000 100644 0000000... 01e79c3... A file2
+$
diff --git a/t/t4013/diff.diff-tree_--root_-r_initial b/t/t4013/diff.diff-tree_--root_-r_initial
new file mode 100644 (file)
index 0000000..eed435e
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree --root -r initial
+444ac553ac7612cc88969031b02b3767fb8a353a
+:000000 100644 0000000000000000000000000000000000000000 35d242ba79ae89ac695e26b3d4c27a8e6f028f9e A     dir/sub
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file0
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file2
+$
diff --git a/t/t4013/diff.diff-tree_--root_initial b/t/t4013/diff.diff-tree_--root_initial
new file mode 100644 (file)
index 0000000..ddf6b06
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree --root initial
+444ac553ac7612cc88969031b02b3767fb8a353a
+:000000 040000 0000000000000000000000000000000000000000 da7a33fa77d8066d6698643940ce5860fe2d7fb3 A     dir
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file0
+:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A     file2
+$
diff --git a/t/t4013/diff.diff-tree_-c_--abbrev_master b/t/t4013/diff.diff-tree_-c_--abbrev_master
new file mode 100644 (file)
index 0000000..b8e4aa2
--- /dev/null
@@ -0,0 +1,5 @@
+$ git diff-tree -c --abbrev master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+::100644 100644 100644 cead32e... 7289e35... 992913c... MM     dir/sub
+::100644 100644 100644 b414108... f4615da... 10a8a9f... MM     file0
+$
diff --git a/t/t4013/diff.diff-tree_-c_--stat_--summary_master b/t/t4013/diff.diff-tree_-c_--stat_--summary_master
new file mode 100644 (file)
index 0000000..ac9f641
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree -c --stat --summary master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+$
diff --git a/t/t4013/diff.diff-tree_-c_--stat_--summary_side b/t/t4013/diff.diff-tree_-c_--stat_--summary_side
new file mode 100644 (file)
index 0000000..2afcca1
--- /dev/null
@@ -0,0 +1,8 @@
+$ git diff-tree -c --stat --summary side
+c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+$
diff --git a/t/t4013/diff.diff-tree_-c_--stat_master b/t/t4013/diff.diff-tree_-c_--stat_master
new file mode 100644 (file)
index 0000000..c2fe6a9
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff-tree -c --stat master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+$
diff --git a/t/t4013/diff.diff-tree_-c_master b/t/t4013/diff.diff-tree_-c_master
new file mode 100644 (file)
index 0000000..e2d2bb2
--- /dev/null
@@ -0,0 +1,5 @@
+$ git diff-tree -c master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+::100644 100644 100644 cead32e925b1420c84c14cbf7cf755e7e45af8ad 7289e35bff32727c08dda207511bec138fdb9ea5 992913c5aa0a5476d10c49ed0f21fc0c6d1aedf3 MM   dir/sub
+::100644 100644 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 f4615da674c09df322d6ba8d6b21ecfb1b1ba510 10a8a9f3657f91a156b9f0184ed79a20adef9f7f MM   file0
+$
diff --git a/t/t4013/diff.diff-tree_-p_-m_master b/t/t4013/diff.diff-tree_-p_-m_master
new file mode 100644 (file)
index 0000000..b60bea0
--- /dev/null
@@ -0,0 +1,80 @@
+$ git diff-tree -p -m master
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+diff --git a/dir/sub b/dir/sub
+index cead32e..992913c 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -4,3 +4,5 @@ C
+ D
+ E
+ F
++1
++2
+diff --git a/file0 b/file0
+index b414108..10a8a9f 100644
+--- a/file0
++++ b/file0
+@@ -4,3 +4,6 @@
+ 4
+ 5
+ 6
++A
++B
++C
+59d314ad6f356dd08601a4cd5e530381da3e3c64
+diff --git a/dir/sub b/dir/sub
+index 7289e35..992913c 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,4 +1,8 @@
+ A
+ B
++C
++D
++E
++F
+ 1
+ 2
+diff --git a/file0 b/file0
+index f4615da..10a8a9f 100644
+--- a/file0
++++ b/file0
+@@ -1,6 +1,9 @@
+ 1
+ 2
+ 3
++4
++5
++6
+ A
+ B
+ C
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+diff --git a/file3 b/file3
+deleted file mode 100644
+index 7289e35..0000000
+--- a/file3
++++ /dev/null
+@@ -1,4 +0,0 @@
+-A
+-B
+-1
+-2
+$
diff --git a/t/t4013/diff.diff-tree_-p_initial b/t/t4013/diff.diff-tree_-p_initial
new file mode 100644 (file)
index 0000000..e20ce88
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree -p initial
+$
diff --git a/t/t4013/diff.diff-tree_-p_master b/t/t4013/diff.diff-tree_-p_master
new file mode 100644 (file)
index 0000000..b182875
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree -p master
+$
diff --git a/t/t4013/diff.diff-tree_-r_--abbrev=4_initial b/t/t4013/diff.diff-tree_-r_--abbrev=4_initial
new file mode 100644 (file)
index 0000000..c5a3aa5
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree -r --abbrev=4 initial
+$
diff --git a/t/t4013/diff.diff-tree_-r_--abbrev_initial b/t/t4013/diff.diff-tree_-r_--abbrev_initial
new file mode 100644 (file)
index 0000000..0b689b7
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree -r --abbrev initial
+$
diff --git a/t/t4013/diff.diff-tree_-r_initial b/t/t4013/diff.diff-tree_-r_initial
new file mode 100644 (file)
index 0000000..1765d83
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree -r initial
+$
diff --git a/t/t4013/diff.diff-tree_initial b/t/t4013/diff.diff-tree_initial
new file mode 100644 (file)
index 0000000..b49fc53
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree initial
+$
diff --git a/t/t4013/diff.diff-tree_master b/t/t4013/diff.diff-tree_master
new file mode 100644 (file)
index 0000000..fe9226f
--- /dev/null
@@ -0,0 +1,2 @@
+$ git diff-tree master
+$
diff --git a/t/t4013/diff.diff_--abbrev_initial..side b/t/t4013/diff.diff_--abbrev_initial..side
new file mode 100644 (file)
index 0000000..a88e66f
--- /dev/null
@@ -0,0 +1,32 @@
+$ git diff --abbrev initial..side
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.diff_--name-status_dir2_dir b/t/t4013/diff.diff_--name-status_dir2_dir
new file mode 100644 (file)
index 0000000..ef7fdb7
--- /dev/null
@@ -0,0 +1,3 @@
+$ git diff --name-status dir2 dir
+A      dir/sub
+$
diff --git a/t/t4013/diff.diff_--patch-with-raw_-r_initial..side b/t/t4013/diff.diff_--patch-with-raw_-r_initial..side
new file mode 100644 (file)
index 0000000..3590dc7
--- /dev/null
@@ -0,0 +1,36 @@
+$ git diff --patch-with-raw -r initial..side
+:100644 100644 35d242b... 7289e35... M dir/sub
+:100644 100644 01e79c3... f4615da... M file0
+:000000 100644 0000000... 7289e35... A file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.diff_--patch-with-raw_initial..side b/t/t4013/diff.diff_--patch-with-raw_initial..side
new file mode 100644 (file)
index 0000000..b21d5dc
--- /dev/null
@@ -0,0 +1,36 @@
+$ git diff --patch-with-raw initial..side
+:100644 100644 35d242b... 7289e35... M dir/sub
+:100644 100644 01e79c3... f4615da... M file0
+:000000 100644 0000000... 7289e35... A file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.diff_--patch-with-stat_-r_initial..side b/t/t4013/diff.diff_--patch-with-stat_-r_initial..side
new file mode 100644 (file)
index 0000000..9ed317a
--- /dev/null
@@ -0,0 +1,37 @@
+$ git diff --patch-with-stat -r initial..side
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.diff_--patch-with-stat_initial..side b/t/t4013/diff.diff_--patch-with-stat_initial..side
new file mode 100644 (file)
index 0000000..8b50629
--- /dev/null
@@ -0,0 +1,37 @@
+$ git diff --patch-with-stat initial..side
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.diff_--stat_initial..side b/t/t4013/diff.diff_--stat_initial..side
new file mode 100644 (file)
index 0000000..0517b5d
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff --stat initial..side
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+$
diff --git a/t/t4013/diff.diff_-r_--stat_initial..side b/t/t4013/diff.diff_-r_--stat_initial..side
new file mode 100644 (file)
index 0000000..245220d
--- /dev/null
@@ -0,0 +1,6 @@
+$ git diff -r --stat initial..side
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+$
diff --git a/t/t4013/diff.diff_-r_initial..side b/t/t4013/diff.diff_-r_initial..side
new file mode 100644 (file)
index 0000000..5bb2fe2
--- /dev/null
@@ -0,0 +1,32 @@
+$ git diff -r initial..side
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.diff_initial..side b/t/t4013/diff.diff_initial..side
new file mode 100644 (file)
index 0000000..c8adaf5
--- /dev/null
@@ -0,0 +1,32 @@
+$ git diff initial..side
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.format-patch_--attach_--stdout_initial..master b/t/t4013/diff.format-patch_--attach_--stdout_initial..master
new file mode 100644 (file)
index 0000000..cf6891f
--- /dev/null
@@ -0,0 +1,164 @@
+$ git format-patch --attach --stdout initial..master
+From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:01:00 +0000
+Subject: [PATCH] Second
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+
+This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: attachment; filename="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+
+From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:02:00 +0000
+Subject: [PATCH] Third
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: attachment; filename="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+
+From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:03:00 +0000
+Subject: [PATCH] Side
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: attachment; filename="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+$
diff --git a/t/t4013/diff.format-patch_--attach_--stdout_initial..master^ b/t/t4013/diff.format-patch_--attach_--stdout_initial..master^
new file mode 100644 (file)
index 0000000..fe02587
--- /dev/null
@@ -0,0 +1,106 @@
+$ git format-patch --attach --stdout initial..master^
+From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:01:00 +0000
+Subject: [PATCH] Second
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+
+This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: attachment; filename="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+
+From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:02:00 +0000
+Subject: [PATCH] Third
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: attachment; filename="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+$
diff --git a/t/t4013/diff.format-patch_--attach_--stdout_initial..side b/t/t4013/diff.format-patch_--attach_--stdout_initial..side
new file mode 100644 (file)
index 0000000..9ff828e
--- /dev/null
@@ -0,0 +1,59 @@
+$ git format-patch --attach --stdout initial..side
+From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:03:00 +0000
+Subject: [PATCH] Side
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: attachment; filename="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+$
diff --git a/t/t4013/diff.format-patch_--inline_--stdout_--subject-prefix=TESTCASE_initial..master b/t/t4013/diff.format-patch_--inline_--stdout_--subject-prefix=TESTCASE_initial..master
new file mode 100644 (file)
index 0000000..a8093be
--- /dev/null
@@ -0,0 +1,164 @@
+$ git format-patch --inline --stdout --subject-prefix=TESTCASE initial..master
+From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:01:00 +0000
+Subject: [TESTCASE] Second
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+
+This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+
+From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:02:00 +0000
+Subject: [TESTCASE] Third
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+
+From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:03:00 +0000
+Subject: [TESTCASE] Side
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+$
diff --git a/t/t4013/diff.format-patch_--inline_--stdout_initial..master b/t/t4013/diff.format-patch_--inline_--stdout_initial..master
new file mode 100644 (file)
index 0000000..aa110c0
--- /dev/null
@@ -0,0 +1,164 @@
+$ git format-patch --inline --stdout initial..master
+From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:01:00 +0000
+Subject: [PATCH] Second
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+
+This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+
+From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:02:00 +0000
+Subject: [PATCH] Third
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+
+From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:03:00 +0000
+Subject: [PATCH] Side
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+$
diff --git a/t/t4013/diff.format-patch_--inline_--stdout_initial..master^ b/t/t4013/diff.format-patch_--inline_--stdout_initial..master^
new file mode 100644 (file)
index 0000000..95e9ea4
--- /dev/null
@@ -0,0 +1,106 @@
+$ git format-patch --inline --stdout initial..master^
+From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:01:00 +0000
+Subject: [PATCH] Second
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+
+This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+
+From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:02:00 +0000
+Subject: [PATCH] Third
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+$
diff --git a/t/t4013/diff.format-patch_--inline_--stdout_initial..master^^ b/t/t4013/diff.format-patch_--inline_--stdout_initial..master^^
new file mode 100644 (file)
index 0000000..b8e81e1
--- /dev/null
@@ -0,0 +1,60 @@
+$ git format-patch --inline --stdout initial..master^^
+From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:01:00 +0000
+Subject: [DIFFERENT_PREFIX] Second
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+
+This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+$
diff --git a/t/t4013/diff.format-patch_--inline_--stdout_initial..side b/t/t4013/diff.format-patch_--inline_--stdout_initial..side
new file mode 100644 (file)
index 0000000..86ae923
--- /dev/null
@@ -0,0 +1,59 @@
+$ git format-patch --inline --stdout initial..side
+From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:03:00 +0000
+Subject: [PATCH] Side
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="------------g-i-t--v-e-r-s-i-o-n"
+
+This is a multi-part message in MIME format.
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/plain; charset=UTF-8; format=fixed
+Content-Transfer-Encoding: 8bit
+
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+--------------g-i-t--v-e-r-s-i-o-n
+Content-Type: text/x-patch; name="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline; filename="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff"
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+--------------g-i-t--v-e-r-s-i-o-n--
+
+
+$
diff --git a/t/t4013/diff.format-patch_--stdout_initial..master b/t/t4013/diff.format-patch_--stdout_initial..master
new file mode 100644 (file)
index 0000000..8b88ca4
--- /dev/null
@@ -0,0 +1,127 @@
+$ git format-patch --stdout initial..master
+From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:01:00 +0000
+Subject: [PATCH] Second
+
+This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+-- 
+g-i-t--v-e-r-s-i-o-n
+
+
+From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:02:00 +0000
+Subject: [PATCH] Third
+
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+-- 
+g-i-t--v-e-r-s-i-o-n
+
+
+From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:03:00 +0000
+Subject: [PATCH] Side
+
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+-- 
+g-i-t--v-e-r-s-i-o-n
+
+$
diff --git a/t/t4013/diff.format-patch_--stdout_initial..master^ b/t/t4013/diff.format-patch_--stdout_initial..master^
new file mode 100644 (file)
index 0000000..47a4b88
--- /dev/null
@@ -0,0 +1,81 @@
+$ git format-patch --stdout initial..master^
+From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:01:00 +0000
+Subject: [PATCH] Second
+
+This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+-- 
+g-i-t--v-e-r-s-i-o-n
+
+
+From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:02:00 +0000
+Subject: [PATCH] Third
+
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+-- 
+g-i-t--v-e-r-s-i-o-n
+
+$
diff --git a/t/t4013/diff.format-patch_--stdout_initial..side b/t/t4013/diff.format-patch_--stdout_initial..side
new file mode 100644 (file)
index 0000000..e765088
--- /dev/null
@@ -0,0 +1,47 @@
+$ git format-patch --stdout initial..side
+From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:03:00 +0000
+Subject: [PATCH] Side
+
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+-- 
+g-i-t--v-e-r-s-i-o-n
+
+$
diff --git a/t/t4013/diff.log_--patch-with-stat_--summary_master_--_dir_ b/t/t4013/diff.log_--patch-with-stat_--summary_master_--_dir_
new file mode 100644 (file)
index 0000000..3ceb8e7
--- /dev/null
@@ -0,0 +1,74 @@
+$ git log --patch-with-stat --summary master -- dir/
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+$
diff --git a/t/t4013/diff.log_--patch-with-stat_master b/t/t4013/diff.log_--patch-with-stat_master
new file mode 100644 (file)
index 0000000..43d7776
--- /dev/null
@@ -0,0 +1,129 @@
+$ git log --patch-with-stat master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+$
diff --git a/t/t4013/diff.log_--patch-with-stat_master_--_dir_ b/t/t4013/diff.log_--patch-with-stat_master_--_dir_
new file mode 100644 (file)
index 0000000..5187a26
--- /dev/null
@@ -0,0 +1,74 @@
+$ git log --patch-with-stat master -- dir/
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+$
diff --git a/t/t4013/diff.log_--root_--cc_--patch-with-stat_--summary_master b/t/t4013/diff.log_--root_--cc_--patch-with-stat_--summary_master
new file mode 100644 (file)
index 0000000..c964097
--- /dev/null
@@ -0,0 +1,199 @@
+$ git log --root --cc --patch-with-stat --summary master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --cc dir/sub
+index cead32e,7289e35..992913c
+--- a/dir/sub
++++ b/dir/sub
+@@@ -1,6 -1,4 +1,8 @@@
+  A
+  B
+ +C
+ +D
+ +E
+ +F
++ 1
++ 2
+diff --cc file0
+index b414108,f4615da..10a8a9f
+--- a/file0
++++ b/file0
+@@@ -1,6 -1,6 +1,9 @@@
+  1
+  2
+  3
+ +4
+ +5
+ +6
++ A
++ B
++ C
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+ create mode 100644 dir/sub
+ create mode 100644 file0
+ create mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.log_--root_--patch-with-stat_--summary_master b/t/t4013/diff.log_--root_--patch-with-stat_--summary_master
new file mode 100644 (file)
index 0000000..ad050af
--- /dev/null
@@ -0,0 +1,167 @@
+$ git log --root --patch-with-stat --summary master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+ create mode 100644 dir/sub
+ create mode 100644 file0
+ create mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.log_--root_--patch-with-stat_master b/t/t4013/diff.log_--root_--patch-with-stat_master
new file mode 100644 (file)
index 0000000..628c6c0
--- /dev/null
@@ -0,0 +1,161 @@
+$ git log --root --patch-with-stat master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.log_--root_-c_--patch-with-stat_--summary_master b/t/t4013/diff.log_--root_-c_--patch-with-stat_--summary_master
new file mode 100644 (file)
index 0000000..5d4e0f1
--- /dev/null
@@ -0,0 +1,199 @@
+$ git log --root -c --patch-with-stat --summary master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --combined dir/sub
+index cead32e,7289e35..992913c
+--- a/dir/sub
++++ b/dir/sub
+@@@ -1,6 -1,4 +1,8 @@@
+  A
+  B
+ +C
+ +D
+ +E
+ +F
++ 1
++ 2
+diff --combined file0
+index b414108,f4615da..10a8a9f
+--- a/file0
++++ b/file0
+@@@ -1,6 -1,6 +1,9 @@@
+  1
+  2
+  3
+ +4
+ +5
+ +6
++ A
++ B
++ C
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+ create mode 100644 dir/sub
+ create mode 100644 file0
+ create mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.log_--root_-p_master b/t/t4013/diff.log_--root_-p_master
new file mode 100644 (file)
index 0000000..217a2eb
--- /dev/null
@@ -0,0 +1,142 @@
+$ git log --root -p master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.log_--root_master b/t/t4013/diff.log_--root_master
new file mode 100644 (file)
index 0000000..e17ccfc
--- /dev/null
@@ -0,0 +1,34 @@
+$ git log --root master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+$
diff --git a/t/t4013/diff.log_-SF_-p_master b/t/t4013/diff.log_-SF_-p_master
new file mode 100644 (file)
index 0000000..5e32438
--- /dev/null
@@ -0,0 +1,18 @@
+$ git log -SF -p master
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+$
diff --git a/t/t4013/diff.log_-SF_master b/t/t4013/diff.log_-SF_master
new file mode 100644 (file)
index 0000000..c1599f2
--- /dev/null
@@ -0,0 +1,7 @@
+$ git log -SF master
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+$
diff --git a/t/t4013/diff.log_-p_master b/t/t4013/diff.log_-p_master
new file mode 100644 (file)
index 0000000..f8fefef
--- /dev/null
@@ -0,0 +1,115 @@
+$ git log -p master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+$
diff --git a/t/t4013/diff.log_master b/t/t4013/diff.log_master
new file mode 100644 (file)
index 0000000..e9d9e7b
--- /dev/null
@@ -0,0 +1,34 @@
+$ git log master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+$
diff --git a/t/t4013/diff.show_--patch-with-raw_side b/t/t4013/diff.show_--patch-with-raw_side
new file mode 100644 (file)
index 0000000..221b46a
--- /dev/null
@@ -0,0 +1,42 @@
+$ git show --patch-with-raw side
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+:100644 100644 35d242b... 7289e35... M dir/sub
+:100644 100644 01e79c3... f4615da... M file0
+:000000 100644 0000000... 7289e35... A file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.show_--patch-with-stat_--summary_side b/t/t4013/diff.show_--patch-with-stat_--summary_side
new file mode 100644 (file)
index 0000000..377f2b7
--- /dev/null
@@ -0,0 +1,44 @@
+$ git show --patch-with-stat --summary side
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.show_--patch-with-stat_side b/t/t4013/diff.show_--patch-with-stat_side
new file mode 100644 (file)
index 0000000..fb14c53
--- /dev/null
@@ -0,0 +1,43 @@
+$ git show --patch-with-stat side
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.show_--root_initial b/t/t4013/diff.show_--root_initial
new file mode 100644 (file)
index 0000000..8c89136
--- /dev/null
@@ -0,0 +1,34 @@
+$ git show --root initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.show_--stat_--summary_side b/t/t4013/diff.show_--stat_--summary_side
new file mode 100644 (file)
index 0000000..5bd5977
--- /dev/null
@@ -0,0 +1,13 @@
+$ git show --stat --summary side
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+$
diff --git a/t/t4013/diff.show_--stat_side b/t/t4013/diff.show_--stat_side
new file mode 100644 (file)
index 0000000..3b22327
--- /dev/null
@@ -0,0 +1,12 @@
+$ git show --stat side
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+$
diff --git a/t/t4013/diff.show_initial b/t/t4013/diff.show_initial
new file mode 100644 (file)
index 0000000..4c4066a
--- /dev/null
@@ -0,0 +1,7 @@
+$ git show initial
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+$
diff --git a/t/t4013/diff.show_master b/t/t4013/diff.show_master
new file mode 100644 (file)
index 0000000..9e6e1f2
--- /dev/null
@@ -0,0 +1,36 @@
+$ git show master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+diff --cc dir/sub
+index cead32e,7289e35..992913c
+--- a/dir/sub
++++ b/dir/sub
+@@@ -1,6 -1,4 +1,8 @@@
+  A
+  B
+ +C
+ +D
+ +E
+ +F
++ 1
++ 2
+diff --cc file0
+index b414108,f4615da..10a8a9f
+--- a/file0
++++ b/file0
+@@@ -1,6 -1,6 +1,9 @@@
+  1
+  2
+  3
+ +4
+ +5
+ +6
++ A
++ B
++ C
+$
diff --git a/t/t4013/diff.show_side b/t/t4013/diff.show_side
new file mode 100644 (file)
index 0000000..530a073
--- /dev/null
@@ -0,0 +1,38 @@
+$ git show side
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+$
diff --git a/t/t4013/diff.whatchanged_--patch-with-stat_--summary_master_--_dir_ b/t/t4013/diff.whatchanged_--patch-with-stat_--summary_master_--_dir_
new file mode 100644 (file)
index 0000000..6a467cc
--- /dev/null
@@ -0,0 +1,61 @@
+$ git whatchanged --patch-with-stat --summary master -- dir/
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+$
diff --git a/t/t4013/diff.whatchanged_--patch-with-stat_master b/t/t4013/diff.whatchanged_--patch-with-stat_master
new file mode 100644 (file)
index 0000000..1e1bbe1
--- /dev/null
@@ -0,0 +1,116 @@
+$ git whatchanged --patch-with-stat master
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+$
diff --git a/t/t4013/diff.whatchanged_--patch-with-stat_master_--_dir_ b/t/t4013/diff.whatchanged_--patch-with-stat_master_--_dir_
new file mode 100644 (file)
index 0000000..13789f1
--- /dev/null
@@ -0,0 +1,61 @@
+$ git whatchanged --patch-with-stat master -- dir/
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+$
diff --git a/t/t4013/diff.whatchanged_--root_--cc_--patch-with-stat_--summary_master b/t/t4013/diff.whatchanged_--root_--cc_--patch-with-stat_--summary_master
new file mode 100644 (file)
index 0000000..5facf25
--- /dev/null
@@ -0,0 +1,199 @@
+$ git whatchanged --root --cc --patch-with-stat --summary master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --cc dir/sub
+index cead32e,7289e35..992913c
+--- a/dir/sub
++++ b/dir/sub
+@@@ -1,6 -1,4 +1,8 @@@
+  A
+  B
+ +C
+ +D
+ +E
+ +F
++ 1
++ 2
+diff --cc file0
+index b414108,f4615da..10a8a9f
+--- a/file0
++++ b/file0
+@@@ -1,6 -1,6 +1,9 @@@
+  1
+  2
+  3
+ +4
+ +5
+ +6
++ A
++ B
++ C
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+ create mode 100644 dir/sub
+ create mode 100644 file0
+ create mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.whatchanged_--root_--patch-with-stat_--summary_master b/t/t4013/diff.whatchanged_--root_--patch-with-stat_--summary_master
new file mode 100644 (file)
index 0000000..0291153
--- /dev/null
@@ -0,0 +1,160 @@
+$ git whatchanged --root --patch-with-stat --summary master
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+ create mode 100644 dir/sub
+ create mode 100644 file0
+ create mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.whatchanged_--root_--patch-with-stat_master b/t/t4013/diff.whatchanged_--root_--patch-with-stat_master
new file mode 100644 (file)
index 0000000..9b0349c
--- /dev/null
@@ -0,0 +1,154 @@
+$ git whatchanged --root --patch-with-stat master
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.whatchanged_--root_-c_--patch-with-stat_--summary_master b/t/t4013/diff.whatchanged_--root_-c_--patch-with-stat_--summary_master
new file mode 100644 (file)
index 0000000..10f6767
--- /dev/null
@@ -0,0 +1,199 @@
+$ git whatchanged --root -c --patch-with-stat --summary master
+commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
+Merge: 9a6d494... c7a2ab9...
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:04:00 2006 +0000
+
+    Merge branch 'side'
+
+ dir/sub |    2 ++
+ file0   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --combined dir/sub
+index cead32e,7289e35..992913c
+--- a/dir/sub
++++ b/dir/sub
+@@@ -1,6 -1,4 +1,8 @@@
+  A
+  B
+ +C
+ +D
+ +E
+ +F
++ 1
++ 2
+diff --combined file0
+index b414108,f4615da..10a8a9f
+--- a/file0
++++ b/file0
+@@@ -1,6 -1,6 +1,9 @@@
+  1
+  2
+  3
+ +4
+ +5
+ +6
++ A
++ B
++ C
+
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file3   |    4 ++++
+ 3 files changed, 9 insertions(+), 0 deletions(-)
+ create mode 100644 file3
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 +++
+ 3 files changed, 8 insertions(+), 0 deletions(-)
+ create mode 100644 dir/sub
+ create mode 100644 file0
+ create mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.whatchanged_--root_-p_master b/t/t4013/diff.whatchanged_--root_-p_master
new file mode 100644 (file)
index 0000000..ebf1f06
--- /dev/null
@@ -0,0 +1,135 @@
+$ git whatchanged --root -p master
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+diff --git a/dir/sub b/dir/sub
+new file mode 100644
+index 0000000..35d242b
+--- /dev/null
++++ b/dir/sub
+@@ -0,0 +1,2 @@
++A
++B
+diff --git a/file0 b/file0
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file0
+@@ -0,0 +1,3 @@
++1
++2
++3
+diff --git a/file2 b/file2
+new file mode 100644
+index 0000000..01e79c3
+--- /dev/null
++++ b/file2
+@@ -0,0 +1,3 @@
++1
++2
++3
+$
diff --git a/t/t4013/diff.whatchanged_--root_master b/t/t4013/diff.whatchanged_--root_master
new file mode 100644 (file)
index 0000000..a405cb6
--- /dev/null
@@ -0,0 +1,42 @@
+$ git whatchanged --root master
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+:100644 100644 35d242b... 7289e35... M dir/sub
+:100644 100644 01e79c3... f4615da... M file0
+:000000 100644 0000000... 7289e35... A file3
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+:100644 100644 8422d40... cead32e... M dir/sub
+:000000 100644 0000000... b1e6722... A file1
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+
+:100644 100644 35d242b... 8422d40... M dir/sub
+:100644 100644 01e79c3... b414108... M file0
+:100644 000000 01e79c3... 0000000... D file2
+
+commit 444ac553ac7612cc88969031b02b3767fb8a353a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:00:00 2006 +0000
+
+    Initial
+
+:000000 100644 0000000... 35d242b... A dir/sub
+:000000 100644 0000000... 01e79c3... A file0
+:000000 100644 0000000... 01e79c3... A file2
+$
diff --git a/t/t4013/diff.whatchanged_-SF_-p_master b/t/t4013/diff.whatchanged_-SF_-p_master
new file mode 100644 (file)
index 0000000..f39da84
--- /dev/null
@@ -0,0 +1,18 @@
+$ git whatchanged -SF -p master
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+$
diff --git a/t/t4013/diff.whatchanged_-SF_master b/t/t4013/diff.whatchanged_-SF_master
new file mode 100644 (file)
index 0000000..0499321
--- /dev/null
@@ -0,0 +1,9 @@
+$ git whatchanged -SF master
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+:100644 100644 8422d40... cead32e... M dir/sub
+$
diff --git a/t/t4013/diff.whatchanged_-p_master b/t/t4013/diff.whatchanged_-p_master
new file mode 100644 (file)
index 0000000..f18d432
--- /dev/null
@@ -0,0 +1,102 @@
+$ git whatchanged -p master
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..7289e35 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++1
++2
+diff --git a/file0 b/file0
+index 01e79c3..f4615da 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++A
++B
++C
+diff --git a/file3 b/file3
+new file mode 100644
+index 0000000..7289e35
+--- /dev/null
++++ b/file3
+@@ -0,0 +1,4 @@
++A
++B
++1
++2
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+$
diff --git a/t/t4013/diff.whatchanged_master b/t/t4013/diff.whatchanged_master
new file mode 100644 (file)
index 0000000..cd3bcc2
--- /dev/null
@@ -0,0 +1,32 @@
+$ git whatchanged master
+commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:03:00 2006 +0000
+
+    Side
+
+:100644 100644 35d242b... 7289e35... M dir/sub
+:100644 100644 01e79c3... f4615da... M file0
+:000000 100644 0000000... 7289e35... A file3
+
+commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:02:00 2006 +0000
+
+    Third
+
+:100644 100644 8422d40... cead32e... M dir/sub
+:000000 100644 0000000... b1e6722... A file1
+
+commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
+Author: A U Thor <author@example.com>
+Date:   Mon Jun 26 00:01:00 2006 +0000
+
+    Second
+    
+    This is the second commit.
+
+:100644 100644 35d242b... 8422d40... M dir/sub
+:100644 100644 01e79c3... b414108... M file0
+:100644 000000 01e79c3... 0000000... D file2
+$
diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh
new file mode 100755 (executable)
index 0000000..0a6fe53
--- /dev/null
@@ -0,0 +1,91 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='Format-patch skipping already incorporated patches'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       for i in 1 2 3 4 5 6 7 8 9 10; do echo "$i"; done >file &&
+       cat file >elif &&
+       git add file elif &&
+       git commit -m Initial &&
+       git checkout -b side &&
+
+       for i in 1 2 5 6 A B C 7 8 9 10; do echo "$i"; done >file &&
+       chmod +x elif &&
+       git update-index file elif &&
+       git update-index --chmod=+x elif &&
+       git commit -m "Side changes #1" &&
+
+       for i in D E F; do echo "$i"; done >>file &&
+       git update-index file &&
+       git commit -m "Side changes #2" &&
+       git tag C2 &&
+
+       for i in 5 6 1 2 3 A 4 B C 7 8 9 10 D E F; do echo "$i"; done >file &&
+       git update-index file &&
+       git commit -m "Side changes #3 with \\n backslash-n in it." &&
+
+       git checkout master &&
+       git diff-tree -p C2 | git apply --index &&
+       git commit -m "Master accepts moral equivalent of #2"
+
+'
+
+test_expect_success "format-patch --ignore-if-in-upstream" '
+
+       git format-patch --stdout master..side >patch0 &&
+       cnt=`grep "^From " patch0 | wc -l` &&
+       test $cnt = 3
+
+'
+
+test_expect_success "format-patch --ignore-if-in-upstream" '
+
+       git format-patch --stdout \
+               --ignore-if-in-upstream master..side >patch1 &&
+       cnt=`grep "^From " patch1 | wc -l` &&
+       test $cnt = 2
+
+'
+
+test_expect_success "format-patch result applies" '
+
+       git checkout -b rebuild-0 master &&
+       git am -3 patch0 &&
+       cnt=`git rev-list master.. | wc -l` &&
+       test $cnt = 2
+'
+
+test_expect_success "format-patch --ignore-if-in-upstream result applies" '
+
+       git checkout -b rebuild-1 master &&
+       git am -3 patch1 &&
+       cnt=`git rev-list master.. | wc -l` &&
+       test $cnt = 2
+'
+
+test_expect_success 'commit did not screw up the log message' '
+
+       git cat-file commit side | grep "^Side .* with .* backslash-n"
+
+'
+
+test_expect_success 'format-patch did not screw up the log message' '
+
+       grep "^Subject: .*Side changes #3 with .* backslash-n" patch0 &&
+       grep "^Subject: .*Side changes #3 with .* backslash-n" patch1
+
+'
+
+test_expect_success 'replay did not screw up the log message' '
+
+       git cat-file commit rebuild-1 | grep "^Side .* with .* backslash-n"
+
+'
+
+test_done
diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh
new file mode 100755 (executable)
index 0000000..79fdff3
--- /dev/null
@@ -0,0 +1,120 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Johannes E. Schindelin
+#
+
+test_description='Test special whitespace in diff engine.
+
+'
+. ./test-lib.sh
+. ../diff-lib.sh
+
+# Ray Lehtiniemi's example
+
+cat << EOF > x
+do {
+   nothing;
+} while (0);
+EOF
+
+git update-index --add x
+
+cat << EOF > x
+do
+{
+   nothing;
+}
+while (0);
+EOF
+
+cat << EOF > expect
+diff --git a/x b/x
+index adf3937..6edc172 100644
+--- a/x
++++ b/x
+@@ -1,3 +1,5 @@
+-do {
++do
++{
+    nothing;
+-} while (0);
++}
++while (0);
+EOF
+
+git diff > out
+test_expect_success "Ray's example without options" 'git diff expect out'
+
+git diff -w > out
+test_expect_success "Ray's example with -w" 'git diff expect out'
+
+git diff -b > out
+test_expect_success "Ray's example with -b" 'git diff expect out'
+
+tr 'Q' '\015' << EOF > x
+whitespace at beginning
+whitespace change
+whitespace in the middle
+whitespace at end
+unchanged line
+CR at endQ
+EOF
+
+git update-index x
+
+cat << EOF > x
+       whitespace at beginning
+whitespace      change
+white space in the middle
+whitespace at end  
+unchanged line
+CR at end
+EOF
+
+tr 'Q' '\015' << EOF > expect
+diff --git a/x b/x
+index d99af23..8b32fb5 100644
+--- a/x
++++ b/x
+@@ -1,6 +1,6 @@
+-whitespace at beginning
+-whitespace change
+-whitespace in the middle
+-whitespace at end
++      whitespace at beginning
++whitespace     change
++white space in the middle
++whitespace at end  
+ unchanged line
+-CR at endQ
++CR at end
+EOF
+git diff > out
+test_expect_success 'another test, without options' 'git diff expect out'
+
+cat << EOF > expect
+diff --git a/x b/x
+index d99af23..8b32fb5 100644
+EOF
+git diff -w > out
+test_expect_success 'another test, with -w' 'git diff expect out'
+
+tr 'Q' '\015' << EOF > expect
+diff --git a/x b/x
+index d99af23..8b32fb5 100644
+--- a/x
++++ b/x
+@@ -1,6 +1,6 @@
+-whitespace at beginning
++      whitespace at beginning
+ whitespace change
+-whitespace in the middle
++white space in the middle
+ whitespace at end
+ unchanged line
+ CR at endQ
+EOF
+git diff -b > out
+test_expect_success 'another test, with -b' 'git diff expect out'
+
+test_done
diff --git a/t/t4016-diff-quote.sh b/t/t4016-diff-quote.sh
new file mode 100755 (executable)
index 0000000..5dbdc0c
--- /dev/null
@@ -0,0 +1,70 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Junio C Hamano
+#
+
+test_description='Quoting paths in diff output.
+'
+
+. ./test-lib.sh
+
+P0='pathname'
+P1='pathname   with HT'
+P2='pathname with SP'
+P3='pathname
+with LF'
+: >"$P1" 2>&1 && test -f "$P1" && rm -f "$P1" || {
+       echo >&2 'Filesystem does not support tabs in names'
+       test_done
+}
+
+test_expect_success setup '
+       echo P0.0 >"$P0.0" &&
+       echo P0.1 >"$P0.1" &&
+       echo P0.2 >"$P0.2" &&
+       echo P0.3 >"$P0.3" &&
+       echo P1.0 >"$P1.0" &&
+       echo P1.2 >"$P1.2" &&
+       echo P1.3 >"$P1.3" &&
+       git add . &&
+       git commit -m initial &&
+       git mv "$P0.0" "R$P0.0" &&
+       git mv "$P0.1" "R$P1.0" &&
+       git mv "$P0.2" "R$P2.0" &&
+       git mv "$P0.3" "R$P3.0" &&
+       git mv "$P1.0" "R$P0.1" &&
+       git mv "$P1.2" "R$P2.1" &&
+       git mv "$P1.3" "R$P3.1" &&
+       :
+'
+
+cat >expect <<\EOF
+ rename pathname.1 => "Rpathname\twith HT.0" (100%)
+ rename pathname.3 => "Rpathname\nwith LF.0" (100%)
+ rename "pathname\twith HT.3" => "Rpathname\nwith LF.1" (100%)
+ rename pathname.2 => Rpathname with SP.0 (100%)
+ rename "pathname\twith HT.2" => Rpathname with SP.1 (100%)
+ rename pathname.0 => Rpathname.0 (100%)
+ rename "pathname\twith HT.0" => Rpathname.1 (100%)
+EOF
+test_expect_success 'git diff --summary -M HEAD' '
+       git diff --summary -M HEAD >actual &&
+       git diff expect actual
+'
+
+cat >expect <<\EOF
+ pathname.1 => "Rpathname\twith HT.0"            |    0 
+ pathname.3 => "Rpathname\nwith LF.0"            |    0 
+ "pathname\twith HT.3" => "Rpathname\nwith LF.1" |    0 
+ pathname.2 => Rpathname with SP.0               |    0 
+ "pathname\twith HT.2" => Rpathname with SP.1    |    0 
+ pathname.0 => Rpathname.0                       |    0 
+ "pathname\twith HT.0" => Rpathname.1            |    0 
+ 7 files changed, 0 insertions(+), 0 deletions(-)
+EOF
+test_expect_success 'git diff --stat -M HEAD' '
+       git diff --stat -M HEAD >actual &&
+       git diff expect actual
+'
+
+test_done
diff --git a/t/t4017-diff-retval.sh b/t/t4017-diff-retval.sh
new file mode 100755 (executable)
index 0000000..6873190
--- /dev/null
@@ -0,0 +1,79 @@
+#!/bin/sh
+
+test_description='Return value of diffs'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+       echo 1 >a &&
+       git add . &&
+       git commit -m first &&
+       echo 2 >b &&
+       git add . &&
+       git commit -a -m second
+'
+
+test_expect_success 'git diff-tree HEAD^ HEAD' '
+       git diff-tree --exit-code HEAD^ HEAD
+       test $? = 1
+'
+test_expect_success 'git diff-tree HEAD^ HEAD -- a' '
+       git diff-tree --exit-code HEAD^ HEAD -- a
+       test $? = 0
+'
+test_expect_success 'git diff-tree HEAD^ HEAD -- b' '
+       git diff-tree --exit-code HEAD^ HEAD -- b
+       test $? = 1
+'
+test_expect_success 'echo HEAD | git diff-tree --stdin' '
+       echo $(git rev-parse HEAD) | git diff-tree --exit-code --stdin
+       test $? = 1
+'
+test_expect_success 'git diff-tree HEAD HEAD' '
+       git diff-tree --exit-code HEAD HEAD
+       test $? = 0
+'
+test_expect_success 'git diff-files' '
+       git diff-files --exit-code
+       test $? = 0
+'
+test_expect_success 'git diff-index --cached HEAD' '
+       git diff-index --exit-code --cached HEAD
+       test $? = 0
+'
+test_expect_success 'git diff-index --cached HEAD^' '
+       git diff-index --exit-code --cached HEAD^
+       test $? = 1
+'
+test_expect_success 'git diff-index --cached HEAD^' '
+       echo text >>b &&
+       echo 3 >c &&
+       git add . && {
+               git diff-index --exit-code --cached HEAD^
+               test $? = 1
+       }
+'
+test_expect_success 'git diff-tree -Stext HEAD^ HEAD -- b' '
+       git commit -m "text in b" && {
+               git diff-tree -p --exit-code -Stext HEAD^ HEAD -- b
+               test $? = 1
+       }
+'
+test_expect_success 'git diff-tree -Snot-found HEAD^ HEAD -- b' '
+       git diff-tree -p --exit-code -Snot-found HEAD^ HEAD -- b
+       test $? = 0
+'
+test_expect_success 'git diff-files' '
+       echo 3 >>c && {
+               git diff-files --exit-code
+               test $? = 1
+       }
+'
+test_expect_success 'git diff-index --cached HEAD' '
+       git update-index c && {
+               git diff-index --exit-code --cached HEAD
+               test $? = 1
+       }
+'
+
+test_done
diff --git a/t/t4017-quiet.sh b/t/t4017-quiet.sh
new file mode 100755 (executable)
index 0000000..e747e84
--- /dev/null
@@ -0,0 +1,80 @@
+#!/bin/sh
+
+test_description='Return value of diffs'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+       echo 1 >a &&
+       git add . &&
+       git commit -m first &&
+       echo 2 >b &&
+       git add . &&
+       git commit -a -m second
+'
+
+test_expect_success 'git diff-tree HEAD^ HEAD' '
+       git diff-tree --quiet HEAD^ HEAD >cnt
+       test $? = 1 && test $(wc -l <cnt) = 0
+'
+test_expect_success 'git diff-tree HEAD^ HEAD -- a' '
+       git diff-tree --quiet HEAD^ HEAD -- a >cnt
+       test $? = 0 && test $(wc -l <cnt) = 0
+'
+test_expect_success 'git diff-tree HEAD^ HEAD -- b' '
+       git diff-tree --quiet HEAD^ HEAD -- b >cnt
+       test $? = 1 && test $(wc -l <cnt) = 0
+'
+# this diff outputs one line: sha1 of the given head
+test_expect_success 'echo HEAD | git diff-tree --stdin' '
+       echo $(git rev-parse HEAD) | git diff-tree --quiet --stdin >cnt
+       test $? = 1 && test $(wc -l <cnt) = 1
+'
+test_expect_success 'git diff-tree HEAD HEAD' '
+       git diff-tree --quiet HEAD HEAD >cnt
+       test $? = 0 && test $(wc -l <cnt) = 0
+'
+test_expect_success 'git diff-files' '
+       git diff-files --quiet >cnt
+       test $? = 0 && test $(wc -l <cnt) = 0
+'
+test_expect_success 'git diff-index --cached HEAD' '
+       git diff-index --quiet --cached HEAD >cnt
+       test $? = 0 && test $(wc -l <cnt) = 0
+'
+test_expect_success 'git diff-index --cached HEAD^' '
+       git diff-index --quiet --cached HEAD^ >cnt
+       test $? = 1 && test $(wc -l <cnt) = 0
+'
+test_expect_success 'git diff-index --cached HEAD^' '
+       echo text >>b &&
+       echo 3 >c &&
+       git add . && {
+               git diff-index --quiet --cached HEAD^ >cnt
+               test $? = 1 && test $(wc -l <cnt) = 0
+       }
+'
+test_expect_success 'git diff-tree -Stext HEAD^ HEAD -- b' '
+       git commit -m "text in b" && {
+               git diff-tree --quiet -Stext HEAD^ HEAD -- b >cnt
+               test $? = 1 && test $(wc -l <cnt) = 0
+       }
+'
+test_expect_success 'git diff-tree -Snot-found HEAD^ HEAD -- b' '
+       git diff-tree --quiet -Snot-found HEAD^ HEAD -- b >cnt
+       test $? = 0 && test $(wc -l <cnt) = 0
+'
+test_expect_success 'git diff-files' '
+       echo 3 >>c && {
+               git diff-files --quiet >cnt
+               test $? = 1 && test $(wc -l <cnt) = 0
+       }
+'
+test_expect_success 'git diff-index --cached HEAD' '
+       git update-index c && {
+               git diff-index --quiet --cached HEAD >cnt
+               test $? = 1 && test $(wc -l <cnt) = 0
+       }
+'
+
+test_done
diff --git a/t/t4018-diff-funcname.sh b/t/t4018-diff-funcname.sh
new file mode 100755 (executable)
index 0000000..f9db81d
--- /dev/null
@@ -0,0 +1,60 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes E. Schindelin
+#
+
+test_description='Test custom diff function name patterns'
+
+. ./test-lib.sh
+
+LF='
+'
+
+cat > Beer.java << EOF
+public class Beer
+{
+       int special;
+       public static void main(String args[])
+       {
+               String s=" ";
+               for(int x = 99; x > 0; x--)
+               {
+                       System.out.print(x + " bottles of beer on the wall "
+                               + x + " bottles of beer\n"
+                               + "Take one down, pass it around, " + (x - 1)
+                               + " bottles of beer on the wall.\n");
+               }
+               System.out.print("Go to the store, buy some more,\n"
+                       + "99 bottles of beer on the wall.\n");
+       }
+}
+EOF
+
+sed 's/beer\\/beer,\\/' < Beer.java > Beer-correct.java
+
+test_expect_success 'default behaviour' '
+       git diff Beer.java Beer-correct.java |
+       grep "^@@.*@@ public class Beer"
+'
+
+test_expect_success 'preset java pattern' '
+       echo "*.java diff=java" >.gitattributes &&
+       git diff Beer.java Beer-correct.java |
+       grep "^@@.*@@ public static void main("
+'
+
+git config diff.java.funcname '!static
+!String
+[^     ].*s.*'
+
+test_expect_success 'custom pattern' '
+       git diff Beer.java Beer-correct.java |
+       grep "^@@.*@@ int special;$"
+'
+
+test_expect_success 'last regexp must not be negated' '
+       git config diff.java.funcname "!static" &&
+       ! git diff Beer.java Beer-correct.java
+'
+
+test_done
diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh
new file mode 100755 (executable)
index 0000000..ed3bd5b
--- /dev/null
@@ -0,0 +1,109 @@
+#!/bin/sh
+
+test_description='external diff interface test'
+
+. ./test-lib.sh
+
+_z40=0000000000000000000000000000000000000000
+
+test_expect_success setup '
+
+       test_tick &&
+       echo initial >file &&
+       git add file &&
+       git commit -m initial &&
+
+       test_tick &&
+       echo second >file &&
+       git add file &&
+       git commit -m second &&
+
+       test_tick &&
+       echo third >file
+'
+
+test_expect_success 'GIT_EXTERNAL_DIFF environment' '
+
+       GIT_EXTERNAL_DIFF=echo git diff | {
+               read path oldfile oldhex oldmode newfile newhex newmode &&
+               test "z$path" = zfile &&
+               test "z$oldmode" = z100644 &&
+               test "z$newhex" = "z$_z40" &&
+               test "z$newmode" = z100644 &&
+               oh=$(git rev-parse --verify HEAD:file) &&
+               test "z$oh" = "z$oldhex"
+       }
+
+'
+
+test_expect_success 'GIT_EXTERNAL_DIFF environment should apply only to diff' '
+
+       GIT_EXTERNAL_DIFF=echo git log -p -1 HEAD |
+       grep "^diff --git a/file b/file"
+
+'
+
+test_expect_success 'diff attribute' '
+
+       git config diff.parrot.command echo &&
+
+       echo >.gitattributes "file diff=parrot" &&
+
+       git diff | {
+               read path oldfile oldhex oldmode newfile newhex newmode &&
+               test "z$path" = zfile &&
+               test "z$oldmode" = z100644 &&
+               test "z$newhex" = "z$_z40" &&
+               test "z$newmode" = z100644 &&
+               oh=$(git rev-parse --verify HEAD:file) &&
+               test "z$oh" = "z$oldhex"
+       }
+
+'
+
+test_expect_success 'diff attribute should apply only to diff' '
+
+       git log -p -1 HEAD |
+       grep "^diff --git a/file b/file"
+
+'
+
+test_expect_success 'diff attribute' '
+
+       git config --unset diff.parrot.command &&
+       git config diff.color.command echo &&
+
+       echo >.gitattributes "file diff=color" &&
+
+       git diff | {
+               read path oldfile oldhex oldmode newfile newhex newmode &&
+               test "z$path" = zfile &&
+               test "z$oldmode" = z100644 &&
+               test "z$newhex" = "z$_z40" &&
+               test "z$newmode" = z100644 &&
+               oh=$(git rev-parse --verify HEAD:file) &&
+               test "z$oh" = "z$oldhex"
+       }
+
+'
+
+test_expect_success 'diff attribute should apply only to diff' '
+
+       git log -p -1 HEAD |
+       grep "^diff --git a/file b/file"
+
+'
+
+test_expect_success 'no diff with -diff' '
+       echo >.gitattributes "file -diff" &&
+       git diff | grep Binary
+'
+
+echo NULZbetweenZwords | tr Z '\0' > file
+
+test_expect_success 'force diff with "diff"' '
+       echo >.gitattributes "file diff" &&
+       git diff | grep -a second
+'
+
+test_done
diff --git a/t/t4021-format-patch-numbered.sh b/t/t4021-format-patch-numbered.sh
new file mode 100755 (executable)
index 0000000..43d64bb
--- /dev/null
@@ -0,0 +1,106 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Brian C Gernhardt
+#
+
+test_description='Format-patch numbering options'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       echo A > file &&
+       git add file &&
+       git commit -m First &&
+
+       echo B >> file &&
+       git commit -a -m Second &&
+
+       echo C >> file &&
+       git commit -a -m Third
+
+'
+
+# Each of these gets used multiple times.
+
+test_num_no_numbered() {
+       cnt=$(grep "^Subject: \[PATCH\]" $1 | wc -l) &&
+       test $cnt = $2
+}
+
+test_single_no_numbered() {
+       test_num_no_numbered $1 1
+}
+
+test_no_numbered() {
+       test_num_no_numbered $1 2
+}
+
+test_single_numbered() {
+       grep "^Subject: \[PATCH 1/1\]" $1
+}
+
+test_numbered() {
+       grep "^Subject: \[PATCH 1/2\]" $1 &&
+       grep "^Subject: \[PATCH 2/2\]" $1
+}
+
+test_expect_success 'Default: no numbered' '
+
+       git format-patch --stdout HEAD~2 >patch0 &&
+       test_no_numbered patch0
+
+'
+
+test_expect_success 'Use --numbered' '
+
+       git format-patch --numbered --stdout HEAD~2 >patch1 &&
+       test_numbered patch1
+
+'
+
+test_expect_success 'format.numbered = true' '
+
+       git config format.numbered true &&
+       git format-patch --stdout HEAD~2 >patch2 &&
+       test_numbered patch2
+
+'
+
+test_expect_success 'format.numbered && single patch' '
+
+       git format-patch --stdout HEAD^ > patch3 &&
+       test_single_numbered patch3
+
+'
+
+test_expect_success 'format.numbered && --no-numbered' '
+
+       git format-patch --no-numbered --stdout HEAD~2 >patch4 &&
+       test_no_numbered patch4
+
+'
+
+test_expect_success 'format.numbered = auto' '
+
+       git config format.numbered auto
+       git format-patch --stdout HEAD~2 > patch5 &&
+       test_numbered patch5
+
+'
+
+test_expect_success 'format.numbered = auto && single patch' '
+
+       git format-patch --stdout HEAD^ > patch6 &&
+       test_single_no_numbered patch6
+
+'
+
+test_expect_success 'format.numbered = auto && --no-numbered' '
+
+       git format-patch --no-numbered --stdout HEAD~2 > patch7 &&
+       test_no_numbered patch7
+
+'
+
+test_done
diff --git a/t/t4021-format-patch-signer-mime.sh b/t/t4021-format-patch-signer-mime.sh
new file mode 100755 (executable)
index 0000000..67a70fa
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+test_description='format-patch -s should force MIME encoding as needed'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       >F &&
+       git add F &&
+       git commit -m initial &&
+       echo new line >F &&
+
+       test_tick &&
+       git commit -m "This adds some lines to F" F
+
+'
+
+test_expect_success 'format normally' '
+
+       git format-patch --stdout -1 >output &&
+       ! grep Content-Type output
+
+'
+
+test_expect_success 'format with signoff without funny signer name' '
+
+       git format-patch -s --stdout -1 >output &&
+       ! grep Content-Type output
+
+'
+
+test_expect_success 'format with non ASCII signer name' '
+
+       GIT_COMMITTER_NAME="\e$B$O$^$N\e(B \e$B$U$K$*$&\e(B" \
+       git format-patch -s --stdout -1 >output &&
+       grep Content-Type output
+
+'
+
+test_done
+
diff --git a/t/t4022-diff-rewrite.sh b/t/t4022-diff-rewrite.sh
new file mode 100755 (executable)
index 0000000..6de4acb
--- /dev/null
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+test_description='rewrite diff'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       cat ../../COPYING >test &&
+       git add test &&
+       tr 'a-zA-Z' 'n-za-mN-ZA-M' <../../COPYING >test
+
+'
+
+test_expect_success 'detect rewrite' '
+
+       actual=$(git diff-files -B --summary test) &&
+       expr "$actual" : " rewrite test ([0-9]*%)$" || {
+               echo "Eh? <<$actual>>"
+               false
+       }
+
+'
+
+test_done
+
diff --git a/t/t4023-diff-rename-typechange.sh b/t/t4023-diff-rename-typechange.sh
new file mode 100755 (executable)
index 0000000..255604e
--- /dev/null
@@ -0,0 +1,86 @@
+#!/bin/sh
+
+test_description='typechange rename detection'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       rm -f foo bar &&
+       cat ../../COPYING >foo &&
+       ln -s linklink bar &&
+       git add foo bar &&
+       git commit -a -m Initial &&
+       git tag one &&
+
+       rm -f foo bar &&
+       cat ../../COPYING >bar &&
+       ln -s linklink foo &&
+       git add foo bar &&
+       git commit -a -m Second &&
+       git tag two &&
+
+       rm -f foo bar &&
+       cat ../../COPYING >foo &&
+       git add foo &&
+       git commit -a -m Third &&
+       git tag three &&
+
+       mv foo bar &&
+       ln -s linklink foo &&
+       git add foo bar &&
+       git commit -a -m Fourth &&
+       git tag four &&
+
+       # This is purely for sanity check
+
+       rm -f foo bar &&
+       cat ../../COPYING >foo &&
+       cat ../../Makefile >bar &&
+       git add foo bar &&
+       git commit -a -m Fifth &&
+       git tag five &&
+
+       rm -f foo bar &&
+       cat ../../Makefile >foo &&
+       cat ../../COPYING >bar &&
+       git add foo bar &&
+       git commit -a -m Sixth &&
+       git tag six
+
+'
+
+test_expect_success 'cross renames to be detected for regular files' '
+
+       git diff-tree five six -r --name-status -B -M | sort >actual &&
+       {
+               echo "R100      foo     bar"
+               echo "R100      bar     foo"
+       } | sort >expect &&
+       diff -u expect actual
+
+'
+
+test_expect_success 'cross renames to be detected for typechange' '
+
+       git diff-tree one two -r --name-status -B -M | sort >actual &&
+       {
+               echo "R100      foo     bar"
+               echo "R100      bar     foo"
+       } | sort >expect &&
+       diff -u expect actual
+
+'
+
+test_expect_success 'moves and renames' '
+
+       git diff-tree three four -r --name-status -B -M | sort >actual &&
+       {
+               echo "R100      foo     bar"
+               echo "T100      foo"
+       } | sort >expect &&
+       diff -u expect actual
+
+'
+
+test_done
diff --git a/t/t4100-apply-stat.sh b/t/t4100-apply-stat.sh
new file mode 100755 (executable)
index 0000000..435f65b
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git apply --stat --summary test.
+
+'
+. ./test-lib.sh
+
+test_expect_success \
+    'rename' \
+    'git apply --stat --summary <../t4100/t-apply-1.patch >current &&
+    git diff ../t4100/t-apply-1.expect current'
+
+test_expect_success \
+    'copy' \
+    'git apply --stat --summary <../t4100/t-apply-2.patch >current &&
+    git diff ../t4100/t-apply-2.expect current'
+
+test_expect_success \
+    'rewrite' \
+    'git apply --stat --summary <../t4100/t-apply-3.patch >current &&
+    git diff ../t4100/t-apply-3.expect current'
+
+test_expect_success \
+    'mode' \
+    'git apply --stat --summary <../t4100/t-apply-4.patch >current &&
+    git diff ../t4100/t-apply-4.expect current'
+
+test_expect_success \
+    'non git' \
+    'git apply --stat --summary <../t4100/t-apply-5.patch >current &&
+    git diff ../t4100/t-apply-5.expect current'
+
+test_expect_success \
+    'non git' \
+    'git apply --stat --summary <../t4100/t-apply-6.patch >current &&
+    git diff ../t4100/t-apply-6.expect current'
+
+test_expect_success \
+    'non git' \
+    'git apply --stat --summary <../t4100/t-apply-7.patch >current &&
+    git diff ../t4100/t-apply-7.expect current'
+
+test_done
diff --git a/t/t4100/t-apply-1.expect b/t/t4100/t-apply-1.expect
new file mode 100644 (file)
index 0000000..540e64d
--- /dev/null
@@ -0,0 +1,11 @@
+ Documentation/git-ssh-pull.txt |   12 ++++++------
+ Documentation/git-ssh-push.txt |   10 +++++-----
+ Documentation/git.txt          |    6 +++---
+ Makefile                       |    6 +++---
+ ssh-pull.c                     |    4 ++--
+ ssh-push.c                     |   14 +++++++-------
+ 6 files changed, 26 insertions(+), 26 deletions(-)
+ rename Documentation/{git-rpull.txt => git-ssh-pull.txt} (90%)
+ rename Documentation/{git-rpush.txt => git-ssh-push.txt} (71%)
+ rename rpull.c => ssh-pull.c (97%)
+ rename rpush.c => ssh-push.c (93%)
diff --git a/t/t4100/t-apply-1.patch b/t/t4100/t-apply-1.patch
new file mode 100644 (file)
index 0000000..90ab54f
--- /dev/null
@@ -0,0 +1,194 @@
+418aaf847a8b3ffffb4f777a2dd5262ca5ce0ef7 (from dc93841715dfa9a9cdda6f2c4a25eec831ea7aa0)
+diff --git a/Documentation/git-rpull.txt b/Documentation/git-ssh-pull.txt
+similarity index 90%
+rename from Documentation/git-rpull.txt
+rename to Documentation/git-ssh-pull.txt
+--- a/Documentation/git-rpull.txt
++++ b/Documentation/git-ssh-pull.txt
+@@ -1,21 +1,21 @@
+-git-rpull(1)
+-============
++git-ssh-pull(1)
++===============
+ v0.1, May 2005
+ NAME
+ ----
+-git-rpull - Pulls from a remote repository over ssh connection
++git-ssh-pull - Pulls from a remote repository over ssh connection
+ SYNOPSIS
+ --------
+-'git-rpull' [-c] [-t] [-a] [-d] [-v] [--recover] commit-id url
++'git-ssh-pull' [-c] [-t] [-a] [-d] [-v] [--recover] commit-id url
+ DESCRIPTION
+ -----------
+-Pulls from a remote repository over ssh connection, invoking git-rpush on
+-the other end.
++Pulls from a remote repository over ssh connection, invoking git-ssh-push
++on the other end.
+ OPTIONS
+ -------
+diff --git a/Documentation/git-rpush.txt b/Documentation/git-ssh-push.txt
+similarity index 71%
+rename from Documentation/git-rpush.txt
+rename to Documentation/git-ssh-push.txt
+--- a/Documentation/git-rpush.txt
++++ b/Documentation/git-ssh-push.txt
+@@ -1,19 +1,19 @@
+-git-rpush(1)
+-============
++git-ssh-push(1)
++===============
+ v0.1, May 2005
+ NAME
+ ----
+-git-rpush - Helper "server-side" program used by git-rpull
++git-ssh-push - Helper "server-side" program used by git-ssh-pull
+ SYNOPSIS
+ --------
+-'git-rpush'
++'git-ssh-push'
+ DESCRIPTION
+ -----------
+-Helper "server-side" program used by git-rpull.
++Helper "server-side" program used by git-ssh-pull.
+ Author
+diff --git a/Documentation/git.txt b/Documentation/git.txt
+--- a/Documentation/git.txt
++++ b/Documentation/git.txt
+@@ -148,7 +148,7 @@ link:git-resolve-script.html[git-resolve
+ link:git-tag-script.html[git-tag-script]::
+       An example script to create a tag object signed with GPG
+-link:git-rpull.html[git-rpull]::
++link:git-ssh-pull.html[git-ssh-pull]::
+       Pulls from a remote repository over ssh connection
+ Interogators:
+@@ -156,8 +156,8 @@ Interogators:
+ link:git-diff-helper.html[git-diff-helper]::
+       Generates patch format output for git-diff-*
+-link:git-rpush.html[git-rpush]::
+-      Helper "server-side" program used by git-rpull
++link:git-ssh-push.html[git-ssh-push]::
++      Helper "server-side" program used by git-ssh-pull
+diff --git a/Makefile b/Makefile
+--- a/Makefile
++++ b/Makefile
+@@ -30,7 +30,7 @@ PROG=   git-update-index git-diff-files
+       git-checkout-cache git-diff-tree git-rev-tree git-ls-files \
+       git-check-files git-ls-tree git-merge-base git-merge-cache \
+       git-unpack-file git-export git-diff-cache git-convert-cache \
+-      git-http-pull git-rpush git-rpull git-rev-list git-mktag \
++      git-http-pull git-ssh-push git-ssh-pull git-rev-list git-mktag \
+       git-diff-helper git-tar-tree git-local-pull git-write-blob \
+       git-get-tar-commit-id git-mkdelta git-apply git-stripspace
+@@ -105,8 +105,8 @@ git-diff-cache: diff-cache.c
+ git-convert-cache: convert-cache.c
+ git-http-pull: http-pull.c pull.c
+ git-local-pull: local-pull.c pull.c
+-git-rpush: rsh.c
+-git-rpull: rsh.c pull.c
++git-ssh-push: rsh.c
++git-ssh-pull: rsh.c pull.c
+ git-rev-list: rev-list.c
+ git-mktag: mktag.c
+ git-diff-helper: diff-helper.c
+diff --git a/rpull.c b/ssh-pull.c
+similarity index 97%
+rename from rpull.c
+rename to ssh-pull.c
+--- a/rpull.c
++++ b/ssh-pull.c
+@@ -64,13 +64,13 @@ int main(int argc, char **argv)
+               arg++;
+       }
+       if (argc < arg + 2) {
+-              usage("git-rpull [-c] [-t] [-a] [-v] [-d] [--recover] commit-id url");
++              usage("git-ssh-pull [-c] [-t] [-a] [-v] [-d] [--recover] commit-id url");
+               return 1;
+       }
+       commit_id = argv[arg];
+       url = argv[arg + 1];
+-      if (setup_connection(&fd_in, &fd_out, "git-rpush", url, arg, argv + 1))
++      if (setup_connection(&fd_in, &fd_out, "git-ssh-push", url, arg, argv + 1))
+               return 1;
+       if (get_version())
+diff --git a/rpush.c b/ssh-push.c
+similarity index 93%
+rename from rpush.c
+rename to ssh-push.c
+--- a/rpush.c
++++ b/ssh-push.c
+@@ -16,7 +16,7 @@ int serve_object(int fd_in, int fd_out) 
+       do {
+               size = read(fd_in, sha1 + posn, 20 - posn);
+               if (size < 0) {
+-                      perror("git-rpush: read ");
++                      perror("git-ssh-push: read ");
+                       return -1;
+               }
+               if (!size)
+@@ -30,7 +30,7 @@ int serve_object(int fd_in, int fd_out) 
+       buf = map_sha1_file(sha1, &objsize);
+       
+       if (!buf) {
+-              fprintf(stderr, "git-rpush: could not find %s\n", 
++              fprintf(stderr, "git-ssh-push: could not find %s\n", 
+                       sha1_to_hex(sha1));
+               remote = -1;
+       }
+@@ -45,9 +45,9 @@ int serve_object(int fd_in, int fd_out) 
+               size = write(fd_out, buf + posn, objsize - posn);
+               if (size <= 0) {
+                       if (!size) {
+-                              fprintf(stderr, "git-rpush: write closed");
++                              fprintf(stderr, "git-ssh-push: write closed");
+                       } else {
+-                              perror("git-rpush: write ");
++                              perror("git-ssh-push: write ");
+                       }
+                       return -1;
+               }
+@@ -71,7 +71,7 @@ void service(int fd_in, int fd_out) {
+               retval = read(fd_in, &type, 1);
+               if (retval < 1) {
+                       if (retval < 0)
+-                              perror("rpush: read ");
++                              perror("git-ssh-push: read ");
+                       return;
+               }
+               if (type == 'v' && serve_version(fd_in, fd_out))
+@@ -91,12 +91,12 @@ int main(int argc, char **argv)
+                 arg++;
+         }
+         if (argc < arg + 2) {
+-              usage("git-rpush [-c] [-t] [-a] commit-id url");
++              usage("git-ssh-push [-c] [-t] [-a] commit-id url");
+                 return 1;
+         }
+       commit_id = argv[arg];
+       url = argv[arg + 1];
+-      if (setup_connection(&fd_in, &fd_out, "git-rpull", url, arg, argv + 1))
++      if (setup_connection(&fd_in, &fd_out, "git-ssh-pull", url, arg, argv + 1))
+               return 1;
+       service(fd_in, fd_out);
diff --git a/t/t4100/t-apply-2.expect b/t/t4100/t-apply-2.expect
new file mode 100644 (file)
index 0000000..d1e6459
--- /dev/null
@@ -0,0 +1,5 @@
+ Makefile         |    2 +-
+ git-fetch-script |    5 -----
+ git-pull-script  |   34 +---------------------------------
+ 3 files changed, 2 insertions(+), 39 deletions(-)
+ copy git-pull-script => git-fetch-script (87%)
diff --git a/t/t4100/t-apply-2.patch b/t/t4100/t-apply-2.patch
new file mode 100644 (file)
index 0000000..f5c7d60
--- /dev/null
@@ -0,0 +1,72 @@
+7ef76925d9c19ef74874e1735e2436e56d0c4897 (from 6b14d7faf0bad026a81a27bac07b47691f621b8f)
+diff --git a/Makefile b/Makefile
+--- a/Makefile
++++ b/Makefile
+@@ -20,7 +20,7 @@ INSTALL=install
+ SCRIPTS=git-apply-patch-script git-merge-one-file-script git-prune-script \
+       git-pull-script git-tag-script git-resolve-script git-whatchanged \
+-      git-deltafy-script
++      git-deltafy-script git-fetch-script
+ PROG=   git-update-index git-diff-files git-init-db git-write-tree \
+       git-read-tree git-commit-tree git-cat-file git-fsck-cache \
+diff --git a/git-pull-script b/git-fetch-script
+similarity index 87%
+copy from git-pull-script
+copy to git-fetch-script
+--- a/git-pull-script
++++ b/git-fetch-script
+@@ -39,8 +39,3 @@ download_one "$merge_repo/$merge_name" "
+ echo "Getting object database"
+ download_objects "$merge_repo" "$(cat "$GIT_DIR"/MERGE_HEAD)"
+-
+-git-resolve-script \
+-      "$(cat "$GIT_DIR"/HEAD)" \
+-      "$(cat "$GIT_DIR"/MERGE_HEAD)" \
+-      "$merge_repo"
+diff --git a/git-pull-script b/git-pull-script
+--- a/git-pull-script
++++ b/git-pull-script
+@@ -6,39 +6,7 @@ merge_name=${2:-HEAD}
+ : ${GIT_DIR=.git}
+ : ${GIT_OBJECT_DIRECTORY="${SHA1_FILE_DIRECTORY-"$GIT_DIR/objects"}"}
+-download_one () {
+-      # remote_path="$1" local_file="$2"
+-      case "$1" in
+-      http://*)
+-              wget -q -O "$2" "$1" ;;
+-      /*)
+-              test -f "$1" && cat >"$2" "$1" ;;
+-      *)
+-              rsync -L "$1" "$2" ;;
+-      esac
+-}
+-
+-download_objects () {
+-      # remote_repo="$1" head_sha1="$2"
+-      case "$1" in
+-      http://*)
+-              git-http-pull -a "$2" "$1/"
+-              ;;
+-      /*)
+-              git-local-pull -l -a "$2" "$1/"
+-              ;;
+-      *)
+-              rsync -avz --ignore-existing \
+-                      "$1/objects/." "$GIT_OBJECT_DIRECTORY"/.
+-              ;;
+-      esac
+-}
+-
+-echo "Getting remote $merge_name"
+-download_one "$merge_repo/$merge_name" "$GIT_DIR"/MERGE_HEAD
+-
+-echo "Getting object database"
+-download_objects "$merge_repo" "$(cat "$GIT_DIR"/MERGE_HEAD)"
++git-fetch-script "$merge_repo" "$merge_name"
+ git-resolve-script \
+       "$(cat "$GIT_DIR"/HEAD)" \
diff --git a/t/t4100/t-apply-3.expect b/t/t4100/t-apply-3.expect
new file mode 100644 (file)
index 0000000..912a552
--- /dev/null
@@ -0,0 +1,7 @@
+ Documentation/git-ls-tree.txt |   20 +-
+ ls-tree.c                     |  459 ++++++++++++++++++++++-------------------
+ t/t3100-ls-tree-restrict.sh   |    3 
+ tree.c                        |    2 
+ tree.h                        |    1 
+ 5 files changed, 262 insertions(+), 223 deletions(-)
+ rewrite ls-tree.c (82%)
diff --git a/t/t4100/t-apply-3.patch b/t/t4100/t-apply-3.patch
new file mode 100644 (file)
index 0000000..90cdbaa
--- /dev/null
@@ -0,0 +1,567 @@
+6af1f0192ff8740fe77db7cf02c739ccfbdf119c (from 2bc2564145835996734d6ed5d1880f85b17233d6)
+diff --git a/Documentation/git-ls-tree.txt b/Documentation/git-ls-tree.txt
+--- a/Documentation/git-ls-tree.txt
++++ b/Documentation/git-ls-tree.txt
+@@ -4,23 +4,26 @@ v0.1, May 2005
+ NAME
+ ----
+-git-ls-tree - Displays a tree object in human readable form
++git-ls-tree - Lists the contents of a tree object.
+ SYNOPSIS
+ --------
+-'git-ls-tree' [-r] [-z] <tree-ish> [paths...]
++'git-ls-tree' [-d] [-r] [-z] <tree-ish> [paths...]
+ DESCRIPTION
+ -----------
+-Converts the tree object to a human readable (and script processable)
+-form.
++Lists the contents of a tree object, like what "/bin/ls -a" does
++in the current working directory.
+ OPTIONS
+ -------
+ <tree-ish>::
+       Id of a tree.
++-d::
++      show only the named tree entry itself, not its children
++
+ -r::
+       recurse into sub-trees
+@@ -28,18 +31,19 @@ OPTIONS
+       \0 line termination on output
+ paths::
+-      Optionally, restrict the output of git-ls-tree to specific
+-      paths. Directories will only list their tree blob ids.
+-      Implies -r.
++      When paths are given, shows them.  Otherwise implicitly
++      uses the root level of the tree as the sole path argument.
++
+ Output Format
+ -------------
+-        <mode>\t      <type>\t        <object>\t      <file>
++        <mode> SP <type> SP <object> TAB <file>
+ Author
+ ------
+ Written by Linus Torvalds <torvalds@osdl.org>
++Completely rewritten from scratch by Junio C Hamano <junkio@cox.net>
+ Documentation
+ --------------
+diff --git a/ls-tree.c b/ls-tree.c
+dissimilarity index 82%
+--- ls-tree.c
++++ ls-tree.c
+@@ -1,212 +1,247 @@
+-/*
+- * GIT - The information manager from hell
+- *
+- * Copyright (C) Linus Torvalds, 2005
+- */
+-#include "cache.h"
+-
+-static int line_termination = '\n';
+-static int recursive = 0;
+-
+-struct path_prefix {
+-      struct path_prefix *prev;
+-      const char *name;
+-};
+-
+-#define DEBUG(fmt, ...)       
+-
+-static int string_path_prefix(char *buff, size_t blen, struct path_prefix *prefix)
+-{
+-      int len = 0;
+-      if (prefix) {
+-              if (prefix->prev) {
+-                      len = string_path_prefix(buff,blen,prefix->prev);
+-                      buff += len;
+-                      blen -= len;
+-                      if (blen > 0) {
+-                              *buff = '/';
+-                              len++;
+-                              buff++;
+-                              blen--;
+-                      }
+-              }
+-              strncpy(buff,prefix->name,blen);
+-              return len + strlen(prefix->name);
+-      }
+-
+-      return 0;
+-}
+-
+-static void print_path_prefix(struct path_prefix *prefix)
+-{
+-      if (prefix) {
+-              if (prefix->prev) {
+-                      print_path_prefix(prefix->prev);
+-                      putchar('/');
+-              }
+-              fputs(prefix->name, stdout);
+-      }
+-}
+-
+-/*
+- * return:
+- *    -1 if prefix is *not* a subset of path
+- *     0 if prefix == path
+- *     1 if prefix is a subset of path
+- */
+-static int pathcmp(const char *path, struct path_prefix *prefix)
+-{
+-      char buff[PATH_MAX];
+-      int len,slen;
+-
+-      if (prefix == NULL)
+-              return 1;
+-
+-      len = string_path_prefix(buff, sizeof buff, prefix);
+-      slen = strlen(path);
+-
+-      if (slen < len)
+-              return -1;
+-
+-      if (strncmp(path,buff,len) == 0) {
+-              if (slen == len)
+-                      return 0;
+-              else
+-                      return 1;
+-      }
+-
+-      return -1;
+-}     
+-
+-/*
+- * match may be NULL, or a *sorted* list of paths
+- */
+-static void list_recursive(void *buffer,
+-                         const char *type,
+-                         unsigned long size,
+-                         struct path_prefix *prefix,
+-                         char **match, int matches)
+-{
+-      struct path_prefix this_prefix;
+-      this_prefix.prev = prefix;
+-
+-      if (strcmp(type, "tree"))
+-              die("expected a 'tree' node");
+-
+-      if (matches)
+-              recursive = 1;
+-
+-      while (size) {
+-              int namelen = strlen(buffer)+1;
+-              void *eltbuf = NULL;
+-              char elttype[20];
+-              unsigned long eltsize;
+-              unsigned char *sha1 = buffer + namelen;
+-              char *path = strchr(buffer, ' ') + 1;
+-              unsigned int mode;
+-              const char *matched = NULL;
+-              int mtype = -1;
+-              int mindex;
+-
+-              if (size < namelen + 20 || sscanf(buffer, "%o", &mode) != 1)
+-                      die("corrupt 'tree' file");
+-              buffer = sha1 + 20;
+-              size -= namelen + 20;
+-
+-              this_prefix.name = path;
+-              for ( mindex = 0; mindex < matches; mindex++) {
+-                      mtype = pathcmp(match[mindex],&this_prefix);
+-                      if (mtype >= 0) {
+-                              matched = match[mindex];
+-                              break;
+-                      }
+-              }
+-
+-              /*
+-               * If we're not matching, or if this is an exact match,
+-               * print out the info
+-               */
+-              if (!matches || (matched != NULL && mtype == 0)) {
+-                      printf("%06o %s %s\t", mode,
+-                             S_ISDIR(mode) ? "tree" : "blob",
+-                             sha1_to_hex(sha1));
+-                      print_path_prefix(&this_prefix);
+-                      putchar(line_termination);
+-              }
+-
+-              if (! recursive || ! S_ISDIR(mode))
+-                      continue;
+-
+-              if (matches && ! matched)
+-                      continue;
+-
+-              if (! (eltbuf = read_sha1_file(sha1, elttype, &eltsize)) ) {
+-                      error("cannot read %s", sha1_to_hex(sha1));
+-                      continue;
+-              }
+-
+-              /* If this is an exact directory match, we may have
+-               * directory files following this path. Match on them.
+-               * Otherwise, we're at a pach subcomponent, and we need
+-               * to try to match again.
+-               */
+-              if (mtype == 0)
+-                      mindex++;
+-
+-              list_recursive(eltbuf, elttype, eltsize, &this_prefix, &match[mindex], matches-mindex);
+-              free(eltbuf);
+-      }
+-}
+-
+-static int qcmp(const void *a, const void *b)
+-{
+-      return strcmp(*(char **)a, *(char **)b);
+-}
+-
+-static int list(unsigned char *sha1,char **path)
+-{
+-      void *buffer;
+-      unsigned long size;
+-      int npaths;
+-
+-      for (npaths = 0; path[npaths] != NULL; npaths++)
+-              ;
+-
+-      qsort(path,npaths,sizeof(char *),qcmp);
+-
+-      buffer = read_object_with_reference(sha1, "tree", &size, NULL);
+-      if (!buffer)
+-              die("unable to read sha1 file");
+-      list_recursive(buffer, "tree", size, NULL, path, npaths);
+-      free(buffer);
+-      return 0;
+-}
+-
+-static const char *ls_tree_usage = "git-ls-tree [-r] [-z] <key> [paths...]";
+-
+-int main(int argc, char **argv)
+-{
+-      unsigned char sha1[20];
+-
+-      while (1 < argc && argv[1][0] == '-') {
+-              switch (argv[1][1]) {
+-              case 'z':
+-                      line_termination = 0;
+-                      break;
+-              case 'r':
+-                      recursive = 1;
+-                      break;
+-              default:
+-                      usage(ls_tree_usage);
+-              }
+-              argc--; argv++;
+-      }
+-
+-      if (argc < 2)
+-              usage(ls_tree_usage);
+-      if (get_sha1(argv[1], sha1) < 0)
+-              usage(ls_tree_usage);
+-      if (list(sha1, &argv[2]) < 0)
+-              die("list failed");
+-      return 0;
+-}
++/*
++ * GIT - The information manager from hell
++ *
++ * Copyright (C) Linus Torvalds, 2005
++ */
++#include "cache.h"
++#include "blob.h"
++#include "tree.h"
++
++static int line_termination = '\n';
++#define LS_RECURSIVE 1
++#define LS_TREE_ONLY 2
++static int ls_options = 0;
++
++static struct tree_entry_list root_entry;
++
++static void prepare_root(unsigned char *sha1)
++{
++      unsigned char rsha[20];
++      unsigned long size;
++      void *buf;
++      struct tree *root_tree;
++
++      buf = read_object_with_reference(sha1, "tree", &size, rsha);
++      free(buf);
++      if (!buf)
++              die("Could not read %s", sha1_to_hex(sha1));
++
++      root_tree = lookup_tree(rsha);
++      if (!root_tree)
++              die("Could not read %s", sha1_to_hex(sha1));
++
++      /* Prepare a fake entry */
++      root_entry.directory = 1;
++      root_entry.executable = root_entry.symlink = 0;
++      root_entry.mode = S_IFDIR;
++      root_entry.name = "";
++      root_entry.item.tree = root_tree;
++      root_entry.parent = NULL;
++}
++
++static int prepare_children(struct tree_entry_list *elem)
++{
++      if (!elem->directory)
++              return -1;
++      if (!elem->item.tree->object.parsed) {
++              struct tree_entry_list *e;
++              if (parse_tree(elem->item.tree))
++                      return -1;
++              /* Set up the parent link */
++              for (e = elem->item.tree->entries; e; e = e->next)
++                      e->parent = elem;
++      }
++      return 0;
++}
++
++static struct tree_entry_list *find_entry_0(struct tree_entry_list *elem,
++                                          const char *path,
++                                          const char *path_end)
++{
++      const char *ep;
++      int len;
++
++      while (path < path_end) {
++              if (prepare_children(elem))
++                      return NULL;
++
++              /* In elem->tree->entries, find the one that has name
++               * that matches what is between path and ep.
++               */
++              elem = elem->item.tree->entries;
++
++              ep = strchr(path, '/');
++              if (!ep || path_end <= ep)
++                      ep = path_end;
++              len = ep - path;
++
++              while (elem) {
++                      if ((strlen(elem->name) == len) &&
++                          !strncmp(elem->name, path, len))
++                              break;
++                      elem = elem->next;
++              }
++              if (path_end <= ep || !elem)
++                      return elem;
++              while (*ep == '/' && ep < path_end)
++                      ep++;
++              path = ep;
++      }
++      return NULL;
++}
++
++static struct tree_entry_list *find_entry(const char *path,
++                                        const char *path_end)
++{
++      /* Find tree element, descending from root, that
++       * corresponds to the named path, lazily expanding
++       * the tree if possible.
++       */
++      if (path == path_end) {
++              /* Special.  This is the root level */
++              return &root_entry;
++      }
++      return find_entry_0(&root_entry, path, path_end);
++}
++
++static void show_entry_name(struct tree_entry_list *e)
++{
++      /* This is yucky.  The root level is there for
++       * our convenience but we really want to do a
++       * forest.
++       */
++      if (e->parent && e->parent != &root_entry) {
++              show_entry_name(e->parent);
++              putchar('/');
++      }
++      printf("%s", e->name);
++}
++
++static const char *entry_type(struct tree_entry_list *e)
++{
++      return (e->directory ? "tree" : "blob");
++}
++
++static const char *entry_hex(struct tree_entry_list *e)
++{
++      return sha1_to_hex(e->directory
++                         ? e->item.tree->object.sha1
++                         : e->item.blob->object.sha1);
++}
++
++/* forward declaration for mutually recursive routines */
++static int show_entry(struct tree_entry_list *, int);
++
++static int show_children(struct tree_entry_list *e, int level)
++{
++      if (prepare_children(e))
++              die("internal error: ls-tree show_children called with non tree");
++      e = e->item.tree->entries;
++      while (e) {
++              show_entry(e, level);
++              e = e->next;
++      }
++      return 0;
++}
++
++static int show_entry(struct tree_entry_list *e, int level)
++{
++      int err = 0; 
++
++      if (e != &root_entry) {
++              printf("%06o %s %s      ", e->mode, entry_type(e),
++                     entry_hex(e));
++              show_entry_name(e);
++              putchar(line_termination);
++      }
++
++      if (e->directory) {
++              /* If this is a directory, we have the following cases:
++               * (1) This is the top-level request (explicit path from the
++               *     command line, or "root" if there is no command line).
++               *  a. Without any flag.  We show direct children.  We do not 
++               *     recurse into them.
++               *  b. With -r.  We do recurse into children.
++               *  c. With -d.  We do not recurse into children.
++               * (2) We came here because our caller is either (1-a) or
++               *     (1-b).
++               *  a. Without any flag.  We do not show our children (which
++               *     are grandchildren for the original request).
++               *  b. With -r.  We continue to recurse into our children.
++               *  c. With -d.  We should not have come here to begin with.
++               */
++              if (level == 0 && !(ls_options & LS_TREE_ONLY))
++                      /* case (1)-a and (1)-b */
++                      err = err | show_children(e, level+1);
++              else if (level && ls_options & LS_RECURSIVE)
++                      /* case (2)-b */
++                      err = err | show_children(e, level+1);
++      }
++      return err;
++}
++
++static int list_one(const char *path, const char *path_end)
++{
++      int err = 0;
++      struct tree_entry_list *e = find_entry(path, path_end);
++      if (!e) {
++              /* traditionally ls-tree does not complain about
++               * missing path.  We may change this later to match
++               * what "/bin/ls -a" does, which is to complain.
++               */
++              return err;
++      }
++      err = err | show_entry(e, 0);
++      return err;
++}
++
++static int list(char **path)
++{
++      int i;
++      int err = 0;
++      for (i = 0; path[i]; i++) {
++              int len = strlen(path[i]);
++              while (0 <= len && path[i][len] == '/')
++                      len--;
++              err = err | list_one(path[i], path[i] + len);
++      }
++      return err;
++}
++
++static const char *ls_tree_usage =
++      "git-ls-tree [-d] [-r] [-z] <tree-ish> [path...]";
++
++int main(int argc, char **argv)
++{
++      static char *path0[] = { "", NULL };
++      char **path;
++      unsigned char sha1[20];
++
++      while (1 < argc && argv[1][0] == '-') {
++              switch (argv[1][1]) {
++              case 'z':
++                      line_termination = 0;
++                      break;
++              case 'r':
++                      ls_options |= LS_RECURSIVE;
++                      break;
++              case 'd':
++                      ls_options |= LS_TREE_ONLY;
++                      break;
++              default:
++                      usage(ls_tree_usage);
++              }
++              argc--; argv++;
++      }
++
++      if (argc < 2)
++              usage(ls_tree_usage);
++      if (get_sha1(argv[1], sha1) < 0)
++              usage(ls_tree_usage);
++
++      path = (argc == 2) ? path0 : (argv + 2);
++      prepare_root(sha1);
++      if (list(path) < 0)
++              die("list failed");
++      return 0;
++}
+diff --git a/t/t3100-ls-tree-restrict.sh b/t/t3100-ls-tree-restrict.sh
+--- a/t/t3100-ls-tree-restrict.sh
++++ b/t/t3100-ls-tree-restrict.sh
+@@ -74,8 +74,8 @@ test_expect_success \
+     'ls-tree filtered' \
+     'git-ls-tree $tree path1 path0 >current &&
+      cat >expected <<\EOF &&
+-100644 blob X path0
+ 120000 blob X path1
++100644 blob X path0
+ EOF
+      test_output'
+@@ -85,7 +85,6 @@ test_expect_success \
+      cat >expected <<\EOF &&
+ 040000 tree X path2
+ 040000 tree X path2/baz
+-100644 blob X path2/baz/b
+ 120000 blob X path2/bazbo
+ 100644 blob X path2/foo
+ EOF
+diff --git a/tree.c b/tree.c
+--- a/tree.c
++++ b/tree.c
+@@ -133,7 +133,7 @@ int parse_tree_buffer(struct tree *item,
+               }
+               if (obj)
+                       add_ref(&item->object, obj);
+-
++              entry->parent = NULL; /* needs to be filled by the user */
+               *list_p = entry;
+               list_p = &entry->next;
+       }
+diff --git a/tree.h b/tree.h
+--- a/tree.h
++++ b/tree.h
+@@ -16,6 +16,7 @@ struct tree_entry_list {
+               struct tree *tree;
+               struct blob *blob;
+       } item;
++      struct tree_entry_list *parent;
+ };
+ struct tree {
diff --git a/t/t4100/t-apply-4.expect b/t/t4100/t-apply-4.expect
new file mode 100644 (file)
index 0000000..1ec028b
--- /dev/null
@@ -0,0 +1,5 @@
+ t/t0000-basic.sh |    0 
+ t/test-lib.sh    |    0 
+ 2 files changed, 0 insertions(+), 0 deletions(-)
+ mode change 100644 => 100755 t/t0000-basic.sh
+ mode change 100644 => 100755 t/test-lib.sh
diff --git a/t/t4100/t-apply-4.patch b/t/t4100/t-apply-4.patch
new file mode 100644 (file)
index 0000000..4a56ab5
--- /dev/null
@@ -0,0 +1,7 @@
+ceede59ea90cebad52ba9c8263fef3fb6ef17593 (from 368f99d57e8ed17243f2e164431449d48bfca2fb)
+diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh
+old mode 100644
+new mode 100755
+diff --git a/t/test-lib.sh b/t/test-lib.sh
+old mode 100644
+new mode 100755
diff --git a/t/t4100/t-apply-5.expect b/t/t4100/t-apply-5.expect
new file mode 100644 (file)
index 0000000..b387df1
--- /dev/null
@@ -0,0 +1,19 @@
+ Documentation/git-rpull.txt    |   50 -------------------
+ Documentation/git-rpush.txt    |   30 ------------
+ Documentation/git-ssh-pull.txt |   50 +++++++++++++++++++
+ Documentation/git-ssh-push.txt |   30 ++++++++++++
+ Documentation/git.txt          |    6 +-
+ Makefile                       |    6 +-
+ rpull.c                        |   83 --------------------------------
+ rpush.c                        |  104 ----------------------------------------
+ ssh-pull.c                     |   83 ++++++++++++++++++++++++++++++++
+ ssh-push.c                     |  104 ++++++++++++++++++++++++++++++++++++++++
+ 10 files changed, 273 insertions(+), 273 deletions(-)
+ delete Documentation/git-rpull.txt
+ delete Documentation/git-rpush.txt
+ create Documentation/git-ssh-pull.txt
+ create Documentation/git-ssh-push.txt
+ delete rpull.c
+ delete rpush.c
+ create ssh-pull.c
+ create ssh-push.c
diff --git a/t/t4100/t-apply-5.patch b/t/t4100/t-apply-5.patch
new file mode 100644 (file)
index 0000000..5f6ddc1
--- /dev/null
@@ -0,0 +1,612 @@
+diff a/Documentation/git-rpull.txt b/Documentation/git-rpull.txt
+--- a/Documentation/git-rpull.txt
++++ /dev/null
+@@ -1,50 +0,0 @@
+-git-rpull(1)
+-============
+-v0.1, May 2005
+-
+-NAME
+-----
+-git-rpull - Pulls from a remote repository over ssh connection
+-
+-
+-
+-SYNOPSIS
+---------
+-'git-rpull' [-c] [-t] [-a] [-d] [-v] [--recover] commit-id url
+-
+-DESCRIPTION
+------------
+-Pulls from a remote repository over ssh connection, invoking git-rpush on
+-the other end.
+-
+-OPTIONS
+--------
+--c::
+-      Get the commit objects.
+--t::
+-      Get trees associated with the commit objects.
+--a::
+-      Get all the objects.
+--d::
+-      Do not check for delta base objects (use this option
+-      only when you know the remote repository is not
+-      deltified).
+---recover::
+-      Check dependency of deltified object more carefully than
+-      usual, to recover after earlier pull that was interrupted.
+--v::
+-      Report what is downloaded.
+-
+-
+-Author
+-------
+-Written by Linus Torvalds <torvalds@osdl.org>
+-
+-Documentation
+---------------
+-Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+-
+-GIT
+----
+-Part of the link:git.html[git] suite
+-
+diff a/Documentation/git-rpush.txt b/Documentation/git-rpush.txt
+--- a/Documentation/git-rpush.txt
++++ /dev/null
+@@ -1,30 +0,0 @@
+-git-rpush(1)
+-============
+-v0.1, May 2005
+-
+-NAME
+-----
+-git-rpush - Helper "server-side" program used by git-rpull
+-
+-
+-SYNOPSIS
+---------
+-'git-rpush'
+-
+-DESCRIPTION
+------------
+-Helper "server-side" program used by git-rpull.
+-
+-
+-Author
+-------
+-Written by Linus Torvalds <torvalds@osdl.org>
+-
+-Documentation
+---------------
+-Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
+-
+-GIT
+----
+-Part of the link:git.html[git] suite
+-
+diff a/Documentation/git-ssh-pull.txt b/Documentation/git-ssh-pull.txt
+--- /dev/null
++++ b/Documentation/git-ssh-pull.txt
+@@ -0,0 +1,50 @@
++git-ssh-pull(1)
++===============
++v0.1, May 2005
++
++NAME
++----
++git-ssh-pull - Pulls from a remote repository over ssh connection
++
++
++
++SYNOPSIS
++--------
++'git-ssh-pull' [-c] [-t] [-a] [-d] [-v] [--recover] commit-id url
++
++DESCRIPTION
++-----------
++Pulls from a remote repository over ssh connection, invoking git-ssh-push
++on the other end.
++
++OPTIONS
++-------
++-c::
++      Get the commit objects.
++-t::
++      Get trees associated with the commit objects.
++-a::
++      Get all the objects.
++-d::
++      Do not check for delta base objects (use this option
++      only when you know the remote repository is not
++      deltified).
++--recover::
++      Check dependency of deltified object more carefully than
++      usual, to recover after earlier pull that was interrupted.
++-v::
++      Report what is downloaded.
++
++
++Author
++------
++Written by Linus Torvalds <torvalds@osdl.org>
++
++Documentation
++--------------
++Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
++
++GIT
++---
++Part of the link:git.html[git] suite
++
+diff a/Documentation/git-ssh-push.txt b/Documentation/git-ssh-push.txt
+--- /dev/null
++++ b/Documentation/git-ssh-push.txt
+@@ -0,0 +1,30 @@
++git-ssh-push(1)
++===============
++v0.1, May 2005
++
++NAME
++----
++git-ssh-push - Helper "server-side" program used by git-ssh-pull
++
++
++SYNOPSIS
++--------
++'git-ssh-push'
++
++DESCRIPTION
++-----------
++Helper "server-side" program used by git-ssh-pull.
++
++
++Author
++------
++Written by Linus Torvalds <torvalds@osdl.org>
++
++Documentation
++--------------
++Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
++
++GIT
++---
++Part of the link:git.html[git] suite
++
+diff a/Documentation/git.txt b/Documentation/git.txt
+--- a/Documentation/git.txt
++++ b/Documentation/git.txt
+@@ -148,7 +148,7 @@ link:git-resolve-script.html[git-resolve
+ link:git-tag-script.html[git-tag-script]::
+       An example script to create a tag object signed with GPG
+-link:git-rpull.html[git-rpull]::
++link:git-ssh-pull.html[git-ssh-pull]::
+       Pulls from a remote repository over ssh connection
+ Interogators:
+@@ -156,8 +156,8 @@ Interogators:
+ link:git-diff-helper.html[git-diff-helper]::
+       Generates patch format output for git-diff-*
+-link:git-rpush.html[git-rpush]::
+-      Helper "server-side" program used by git-rpull
++link:git-ssh-push.html[git-ssh-push]::
++      Helper "server-side" program used by git-ssh-pull
+diff a/Makefile b/Makefile
+--- a/Makefile
++++ b/Makefile
+@@ -30,7 +30,7 @@ PROG=   git-update-index git-diff-files
+       git-checkout-cache git-diff-tree git-rev-tree git-ls-files \
+       git-check-files git-ls-tree git-merge-base git-merge-cache \
+       git-unpack-file git-export git-diff-cache git-convert-cache \
+-      git-http-pull git-rpush git-rpull git-rev-list git-mktag \
++      git-http-pull git-ssh-push git-ssh-pull git-rev-list git-mktag \
+       git-diff-helper git-tar-tree git-local-pull git-write-blob \
+       git-get-tar-commit-id git-mkdelta git-apply git-stripspace
+@@ -105,8 +105,8 @@ git-diff-cache: diff-cache.c
+ git-convert-cache: convert-cache.c
+ git-http-pull: http-pull.c pull.c
+ git-local-pull: local-pull.c pull.c
+-git-rpush: rsh.c
+-git-rpull: rsh.c pull.c
++git-ssh-push: rsh.c
++git-ssh-pull: rsh.c pull.c
+ git-rev-list: rev-list.c
+ git-mktag: mktag.c
+ git-diff-helper: diff-helper.c
+diff a/rpull.c b/rpull.c
+--- a/rpull.c
++++ /dev/null
+@@ -1,83 +0,0 @@
+-#include "cache.h"
+-#include "commit.h"
+-#include "rsh.h"
+-#include "pull.h"
+-
+-static int fd_in;
+-static int fd_out;
+-
+-static unsigned char remote_version = 0;
+-static unsigned char local_version = 1;
+-
+-int fetch(unsigned char *sha1)
+-{
+-      int ret;
+-      signed char remote;
+-      char type = 'o';
+-      if (has_sha1_file(sha1))
+-              return 0;
+-      write(fd_out, &type, 1);
+-      write(fd_out, sha1, 20);
+-      if (read(fd_in, &remote, 1) < 1)
+-              return -1;
+-      if (remote < 0)
+-              return remote;
+-      ret = write_sha1_from_fd(sha1, fd_in);
+-      if (!ret)
+-              pull_say("got %s\n", sha1_to_hex(sha1));
+-      return ret;
+-}
+-
+-int get_version(void)
+-{
+-      char type = 'v';
+-      write(fd_out, &type, 1);
+-      write(fd_out, &local_version, 1);
+-      if (read(fd_in, &remote_version, 1) < 1) {
+-              return error("Couldn't read version from remote end");
+-      }
+-      return 0;
+-}
+-
+-int main(int argc, char **argv)
+-{
+-      char *commit_id;
+-      char *url;
+-      int arg = 1;
+-
+-      while (arg < argc && argv[arg][0] == '-') {
+-              if (argv[arg][1] == 't') {
+-                      get_tree = 1;
+-              } else if (argv[arg][1] == 'c') {
+-                      get_history = 1;
+-              } else if (argv[arg][1] == 'd') {
+-                      get_delta = 0;
+-              } else if (!strcmp(argv[arg], "--recover")) {
+-                      get_delta = 2;
+-              } else if (argv[arg][1] == 'a') {
+-                      get_all = 1;
+-                      get_tree = 1;
+-                      get_history = 1;
+-              } else if (argv[arg][1] == 'v') {
+-                      get_verbosely = 1;
+-              }
+-              arg++;
+-      }
+-      if (argc < arg + 2) {
+-              usage("git-rpull [-c] [-t] [-a] [-v] [-d] [--recover] commit-id url");
+-              return 1;
+-      }
+-      commit_id = argv[arg];
+-      url = argv[arg + 1];
+-
+-      if (setup_connection(&fd_in, &fd_out, "git-rpush", url, arg, argv + 1))
+-              return 1;
+-
+-      if (get_version())
+-              return 1;
+-
+-      if (pull(commit_id))
+-              return 1;
+-
+-      return 0;
+-}
+diff a/rpush.c b/rpush.c
+--- a/rpush.c
++++ /dev/null
+@@ -1,104 +0,0 @@
+-#include "cache.h"
+-#include "rsh.h"
+-#include <sys/socket.h>
+-#include <errno.h>
+-
+-unsigned char local_version = 1;
+-unsigned char remote_version = 0;
+-
+-int serve_object(int fd_in, int fd_out) {
+-      ssize_t size;
+-      int posn = 0;
+-      char sha1[20];
+-      unsigned long objsize;
+-      void *buf;
+-      signed char remote;
+-      do {
+-              size = read(fd_in, sha1 + posn, 20 - posn);
+-              if (size < 0) {
+-                      perror("git-rpush: read ");
+-                      return -1;
+-              }
+-              if (!size)
+-                      return -1;
+-              posn += size;
+-      } while (posn < 20);
+-      
+-      /* fprintf(stderr, "Serving %s\n", sha1_to_hex(sha1)); */
+-      remote = 0;
+-      
+-      buf = map_sha1_file(sha1, &objsize);
+-      
+-      if (!buf) {
+-              fprintf(stderr, "git-rpush: could not find %s\n", 
+-                      sha1_to_hex(sha1));
+-              remote = -1;
+-      }
+-      
+-      write(fd_out, &remote, 1);
+-      
+-      if (remote < 0)
+-              return 0;
+-      
+-      posn = 0;
+-      do {
+-              size = write(fd_out, buf + posn, objsize - posn);
+-              if (size <= 0) {
+-                      if (!size) {
+-                              fprintf(stderr, "git-rpush: write closed");
+-                      } else {
+-                              perror("git-rpush: write ");
+-                      }
+-                      return -1;
+-              }
+-              posn += size;
+-      } while (posn < objsize);
+-      return 0;
+-}
+-
+-int serve_version(int fd_in, int fd_out)
+-{
+-      if (read(fd_in, &remote_version, 1) < 1)
+-              return -1;
+-      write(fd_out, &local_version, 1);
+-      return 0;
+-}
+-
+-void service(int fd_in, int fd_out) {
+-      char type;
+-      int retval;
+-      do {
+-              retval = read(fd_in, &type, 1);
+-              if (retval < 1) {
+-                      if (retval < 0)
+-                              perror("rpush: read ");
+-                      return;
+-              }
+-              if (type == 'v' && serve_version(fd_in, fd_out))
+-                      return;
+-              if (type == 'o' && serve_object(fd_in, fd_out))
+-                      return;
+-      } while (1);
+-}
+-
+-int main(int argc, char **argv)
+-{
+-      int arg = 1;
+-        char *commit_id;
+-        char *url;
+-      int fd_in, fd_out;
+-      while (arg < argc && argv[arg][0] == '-') {
+-                arg++;
+-        }
+-        if (argc < arg + 2) {
+-              usage("git-rpush [-c] [-t] [-a] commit-id url");
+-                return 1;
+-        }
+-      commit_id = argv[arg];
+-      url = argv[arg + 1];
+-      if (setup_connection(&fd_in, &fd_out, "git-rpull", url, arg, argv + 1))
+-              return 1;
+-
+-      service(fd_in, fd_out);
+-      return 0;
+-}
+diff a/ssh-pull.c b/ssh-pull.c
+--- /dev/null
++++ b/ssh-pull.c
+@@ -0,0 +1,83 @@
++#include "cache.h"
++#include "commit.h"
++#include "rsh.h"
++#include "pull.h"
++
++static int fd_in;
++static int fd_out;
++
++static unsigned char remote_version = 0;
++static unsigned char local_version = 1;
++
++int fetch(unsigned char *sha1)
++{
++      int ret;
++      signed char remote;
++      char type = 'o';
++      if (has_sha1_file(sha1))
++              return 0;
++      write(fd_out, &type, 1);
++      write(fd_out, sha1, 20);
++      if (read(fd_in, &remote, 1) < 1)
++              return -1;
++      if (remote < 0)
++              return remote;
++      ret = write_sha1_from_fd(sha1, fd_in);
++      if (!ret)
++              pull_say("got %s\n", sha1_to_hex(sha1));
++      return ret;
++}
++
++int get_version(void)
++{
++      char type = 'v';
++      write(fd_out, &type, 1);
++      write(fd_out, &local_version, 1);
++      if (read(fd_in, &remote_version, 1) < 1) {
++              return error("Couldn't read version from remote end");
++      }
++      return 0;
++}
++
++int main(int argc, char **argv)
++{
++      char *commit_id;
++      char *url;
++      int arg = 1;
++
++      while (arg < argc && argv[arg][0] == '-') {
++              if (argv[arg][1] == 't') {
++                      get_tree = 1;
++              } else if (argv[arg][1] == 'c') {
++                      get_history = 1;
++              } else if (argv[arg][1] == 'd') {
++                      get_delta = 0;
++              } else if (!strcmp(argv[arg], "--recover")) {
++                      get_delta = 2;
++              } else if (argv[arg][1] == 'a') {
++                      get_all = 1;
++                      get_tree = 1;
++                      get_history = 1;
++              } else if (argv[arg][1] == 'v') {
++                      get_verbosely = 1;
++              }
++              arg++;
++      }
++      if (argc < arg + 2) {
++              usage("git-ssh-pull [-c] [-t] [-a] [-v] [-d] [--recover] commit-id url");
++              return 1;
++      }
++      commit_id = argv[arg];
++      url = argv[arg + 1];
++
++      if (setup_connection(&fd_in, &fd_out, "git-ssh-push", url, arg, argv + 1))
++              return 1;
++
++      if (get_version())
++              return 1;
++
++      if (pull(commit_id))
++              return 1;
++
++      return 0;
++}
+diff a/ssh-push.c b/ssh-push.c
+--- /dev/null
++++ b/ssh-push.c
+@@ -0,0 +1,104 @@
++#include "cache.h"
++#include "rsh.h"
++#include <sys/socket.h>
++#include <errno.h>
++
++unsigned char local_version = 1;
++unsigned char remote_version = 0;
++
++int serve_object(int fd_in, int fd_out) {
++      ssize_t size;
++      int posn = 0;
++      char sha1[20];
++      unsigned long objsize;
++      void *buf;
++      signed char remote;
++      do {
++              size = read(fd_in, sha1 + posn, 20 - posn);
++              if (size < 0) {
++                      perror("git-ssh-push: read ");
++                      return -1;
++              }
++              if (!size)
++                      return -1;
++              posn += size;
++      } while (posn < 20);
++      
++      /* fprintf(stderr, "Serving %s\n", sha1_to_hex(sha1)); */
++      remote = 0;
++      
++      buf = map_sha1_file(sha1, &objsize);
++      
++      if (!buf) {
++              fprintf(stderr, "git-ssh-push: could not find %s\n", 
++                      sha1_to_hex(sha1));
++              remote = -1;
++      }
++      
++      write(fd_out, &remote, 1);
++      
++      if (remote < 0)
++              return 0;
++      
++      posn = 0;
++      do {
++              size = write(fd_out, buf + posn, objsize - posn);
++              if (size <= 0) {
++                      if (!size) {
++                              fprintf(stderr, "git-ssh-push: write closed");
++                      } else {
++                              perror("git-ssh-push: write ");
++                      }
++                      return -1;
++              }
++              posn += size;
++      } while (posn < objsize);
++      return 0;
++}
++
++int serve_version(int fd_in, int fd_out)
++{
++      if (read(fd_in, &remote_version, 1) < 1)
++              return -1;
++      write(fd_out, &local_version, 1);
++      return 0;
++}
++
++void service(int fd_in, int fd_out) {
++      char type;
++      int retval;
++      do {
++              retval = read(fd_in, &type, 1);
++              if (retval < 1) {
++                      if (retval < 0)
++                              perror("git-ssh-push: read ");
++                      return;
++              }
++              if (type == 'v' && serve_version(fd_in, fd_out))
++                      return;
++              if (type == 'o' && serve_object(fd_in, fd_out))
++                      return;
++      } while (1);
++}
++
++int main(int argc, char **argv)
++{
++      int arg = 1;
++        char *commit_id;
++        char *url;
++      int fd_in, fd_out;
++      while (arg < argc && argv[arg][0] == '-') {
++                arg++;
++        }
++        if (argc < arg + 2) {
++              usage("git-ssh-push [-c] [-t] [-a] commit-id url");
++                return 1;
++        }
++      commit_id = argv[arg];
++      url = argv[arg + 1];
++      if (setup_connection(&fd_in, &fd_out, "git-ssh-pull", url, arg, argv + 1))
++              return 1;
++
++      service(fd_in, fd_out);
++      return 0;
++}
diff --git a/t/t4100/t-apply-6.expect b/t/t4100/t-apply-6.expect
new file mode 100644 (file)
index 0000000..1c343d4
--- /dev/null
@@ -0,0 +1,5 @@
+ Makefile         |    2 +-
+ git-fetch-script |   41 +++++++++++++++++++++++++++++++++++++++++
+ git-pull-script  |   34 +---------------------------------
+ 3 files changed, 43 insertions(+), 34 deletions(-)
+ create git-fetch-script
diff --git a/t/t4100/t-apply-6.patch b/t/t4100/t-apply-6.patch
new file mode 100644 (file)
index 0000000..a72729a
--- /dev/null
@@ -0,0 +1,101 @@
+diff a/Makefile b/Makefile
+--- a/Makefile
++++ b/Makefile
+@@ -20,7 +20,7 @@ INSTALL=install
+ SCRIPTS=git-apply-patch-script git-merge-one-file-script git-prune-script \
+       git-pull-script git-tag-script git-resolve-script git-whatchanged \
+-      git-deltafy-script
++      git-deltafy-script git-fetch-script
+ PROG=   git-update-index git-diff-files git-init-db git-write-tree \
+       git-read-tree git-commit-tree git-cat-file git-fsck-cache \
+diff a/git-fetch-script b/git-fetch-script
+--- /dev/null
++++ b/git-fetch-script
+@@ -0,0 +1,41 @@
++#!/bin/sh
++#
++merge_repo=$1
++merge_name=${2:-HEAD}
++
++: ${GIT_DIR=.git}
++: ${GIT_OBJECT_DIRECTORY="${SHA1_FILE_DIRECTORY-"$GIT_DIR/objects"}"}
++
++download_one () {
++      # remote_path="$1" local_file="$2"
++      case "$1" in
++      http://*)
++              wget -q -O "$2" "$1" ;;
++      /*)
++              test -f "$1" && cat >"$2" "$1" ;;
++      *)
++              rsync -L "$1" "$2" ;;
++      esac
++}
++
++download_objects () {
++      # remote_repo="$1" head_sha1="$2"
++      case "$1" in
++      http://*)
++              git-http-pull -a "$2" "$1/"
++              ;;
++      /*)
++              git-local-pull -l -a "$2" "$1/"
++              ;;
++      *)
++              rsync -avz --ignore-existing \
++                      "$1/objects/." "$GIT_OBJECT_DIRECTORY"/.
++              ;;
++      esac
++}
++
++echo "Getting remote $merge_name"
++download_one "$merge_repo/$merge_name" "$GIT_DIR"/MERGE_HEAD
++
++echo "Getting object database"
++download_objects "$merge_repo" "$(cat "$GIT_DIR"/MERGE_HEAD)"
+diff a/git-pull-script b/git-pull-script
+--- a/git-pull-script
++++ b/git-pull-script
+@@ -6,39 +6,7 @@ merge_name=${2:-HEAD}
+ : ${GIT_DIR=.git}
+ : ${GIT_OBJECT_DIRECTORY="${SHA1_FILE_DIRECTORY-"$GIT_DIR/objects"}"}
+-download_one () {
+-      # remote_path="$1" local_file="$2"
+-      case "$1" in
+-      http://*)
+-              wget -q -O "$2" "$1" ;;
+-      /*)
+-              test -f "$1" && cat >"$2" "$1" ;;
+-      *)
+-              rsync -L "$1" "$2" ;;
+-      esac
+-}
+-
+-download_objects () {
+-      # remote_repo="$1" head_sha1="$2"
+-      case "$1" in
+-      http://*)
+-              git-http-pull -a "$2" "$1/"
+-              ;;
+-      /*)
+-              git-local-pull -l -a "$2" "$1/"
+-              ;;
+-      *)
+-              rsync -avz --ignore-existing \
+-                      "$1/objects/." "$GIT_OBJECT_DIRECTORY"/.
+-              ;;
+-      esac
+-}
+-
+-echo "Getting remote $merge_name"
+-download_one "$merge_repo/$merge_name" "$GIT_DIR"/MERGE_HEAD
+-
+-echo "Getting object database"
+-download_objects "$merge_repo" "$(cat "$GIT_DIR"/MERGE_HEAD)"
++git-fetch-script "$merge_repo" "$merge_name"
+ git-resolve-script \
+       "$(cat "$GIT_DIR"/HEAD)" \
diff --git a/t/t4100/t-apply-7.expect b/t/t4100/t-apply-7.expect
new file mode 100644 (file)
index 0000000..1283164
--- /dev/null
@@ -0,0 +1,6 @@
+ Documentation/git-ls-tree.txt |   20 +-
+ ls-tree.c                     |  333 +++++++++++++++++++++++------------------
+ t/t3100-ls-tree-restrict.sh   |    3 
+ tree.c                        |    2 
+ tree.h                        |    1 
+ 5 files changed, 199 insertions(+), 160 deletions(-)
diff --git a/t/t4100/t-apply-7.patch b/t/t4100/t-apply-7.patch
new file mode 100644 (file)
index 0000000..07c6589
--- /dev/null
@@ -0,0 +1,494 @@
+diff a/Documentation/git-ls-tree.txt b/Documentation/git-ls-tree.txt
+--- a/Documentation/git-ls-tree.txt
++++ b/Documentation/git-ls-tree.txt
+@@ -4,23 +4,26 @@ v0.1, May 2005
+ NAME
+ ----
+-git-ls-tree - Displays a tree object in human readable form
++git-ls-tree - Lists the contents of a tree object.
+ SYNOPSIS
+ --------
+-'git-ls-tree' [-r] [-z] <tree-ish> [paths...]
++'git-ls-tree' [-d] [-r] [-z] <tree-ish> [paths...]
+ DESCRIPTION
+ -----------
+-Converts the tree object to a human readable (and script processable)
+-form.
++Lists the contents of a tree object, like what "/bin/ls -a" does
++in the current working directory.
+ OPTIONS
+ -------
+ <tree-ish>::
+       Id of a tree.
++-d::
++      show only the named tree entry itself, not its children
++
+ -r::
+       recurse into sub-trees
+@@ -28,18 +31,19 @@ OPTIONS
+       \0 line termination on output
+ paths::
+-      Optionally, restrict the output of git-ls-tree to specific
+-      paths. Directories will only list their tree blob ids.
+-      Implies -r.
++      When paths are given, shows them.  Otherwise implicitly
++      uses the root level of the tree as the sole path argument.
++
+ Output Format
+ -------------
+-        <mode>\t      <type>\t        <object>\t      <file>
++        <mode> SP <type> SP <object> TAB <file>
+ Author
+ ------
+ Written by Linus Torvalds <torvalds@osdl.org>
++Completely rewritten from scratch by Junio C Hamano <junkio@cox.net>
+ Documentation
+ --------------
+diff a/ls-tree.c b/ls-tree.c
+--- a/ls-tree.c
++++ b/ls-tree.c
+@@ -4,188 +4,217 @@
+  * Copyright (C) Linus Torvalds, 2005
+  */
+ #include "cache.h"
++#include "blob.h"
++#include "tree.h"
+ static int line_termination = '\n';
+-static int recursive = 0;
++#define LS_RECURSIVE 1
++#define LS_TREE_ONLY 2
++static int ls_options = 0;
+-struct path_prefix {
+-      struct path_prefix *prev;
+-      const char *name;
+-};
+-
+-#define DEBUG(fmt, ...)       
+-
+-static int string_path_prefix(char *buff, size_t blen, struct path_prefix *prefix)
+-{
+-      int len = 0;
+-      if (prefix) {
+-              if (prefix->prev) {
+-                      len = string_path_prefix(buff,blen,prefix->prev);
+-                      buff += len;
+-                      blen -= len;
+-                      if (blen > 0) {
+-                              *buff = '/';
+-                              len++;
+-                              buff++;
+-                              blen--;
+-                      }
+-              }
+-              strncpy(buff,prefix->name,blen);
+-              return len + strlen(prefix->name);
+-      }
++static struct tree_entry_list root_entry;
+-      return 0;
++static void prepare_root(unsigned char *sha1)
++{
++      unsigned char rsha[20];
++      unsigned long size;
++      void *buf;
++      struct tree *root_tree;
++
++      buf = read_object_with_reference(sha1, "tree", &size, rsha);
++      free(buf);
++      if (!buf)
++              die("Could not read %s", sha1_to_hex(sha1));
++
++      root_tree = lookup_tree(rsha);
++      if (!root_tree)
++              die("Could not read %s", sha1_to_hex(sha1));
++
++      /* Prepare a fake entry */
++      root_entry.directory = 1;
++      root_entry.executable = root_entry.symlink = 0;
++      root_entry.mode = S_IFDIR;
++      root_entry.name = "";
++      root_entry.item.tree = root_tree;
++      root_entry.parent = NULL;
+ }
+-static void print_path_prefix(struct path_prefix *prefix)
++static int prepare_children(struct tree_entry_list *elem)
+ {
+-      if (prefix) {
+-              if (prefix->prev) {
+-                      print_path_prefix(prefix->prev);
+-                      putchar('/');
+-              }
+-              fputs(prefix->name, stdout);
++      if (!elem->directory)
++              return -1;
++      if (!elem->item.tree->object.parsed) {
++              struct tree_entry_list *e;
++              if (parse_tree(elem->item.tree))
++                      return -1;
++              /* Set up the parent link */
++              for (e = elem->item.tree->entries; e; e = e->next)
++                      e->parent = elem;
+       }
++      return 0;
+ }
+-/*
+- * return:
+- *    -1 if prefix is *not* a subset of path
+- *     0 if prefix == path
+- *     1 if prefix is a subset of path
+- */
+-static int pathcmp(const char *path, struct path_prefix *prefix)
+-{
+-      char buff[PATH_MAX];
+-      int len,slen;
++static struct tree_entry_list *find_entry_0(struct tree_entry_list *elem,
++                                          const char *path,
++                                          const char *path_end)
++{
++      const char *ep;
++      int len;
++
++      while (path < path_end) {
++              if (prepare_children(elem))
++                      return NULL;
+-      if (prefix == NULL)
+-              return 1;
++              /* In elem->tree->entries, find the one that has name
++               * that matches what is between path and ep.
++               */
++              elem = elem->item.tree->entries;
+-      len = string_path_prefix(buff, sizeof buff, prefix);
+-      slen = strlen(path);
++              ep = strchr(path, '/');
++              if (!ep || path_end <= ep)
++                      ep = path_end;
++              len = ep - path;
++
++              while (elem) {
++                      if ((strlen(elem->name) == len) &&
++                          !strncmp(elem->name, path, len))
++                              break;
++                      elem = elem->next;
++              }
++              if (path_end <= ep || !elem)
++                      return elem;
++              while (*ep == '/' && ep < path_end)
++                      ep++;
++              path = ep;
++      }
++      return NULL;
++}
+-      if (slen < len)
+-              return -1;
++static struct tree_entry_list *find_entry(const char *path,
++                                        const char *path_end)
++{
++      /* Find tree element, descending from root, that
++       * corresponds to the named path, lazily expanding
++       * the tree if possible.
++       */
++      if (path == path_end) {
++              /* Special.  This is the root level */
++              return &root_entry;
++      }
++      return find_entry_0(&root_entry, path, path_end);
++}
+-      if (strncmp(path,buff,len) == 0) {
+-              if (slen == len)
+-                      return 0;
+-              else
+-                      return 1;
++static void show_entry_name(struct tree_entry_list *e)
++{
++      /* This is yucky.  The root level is there for
++       * our convenience but we really want to do a
++       * forest.
++       */
++      if (e->parent && e->parent != &root_entry) {
++              show_entry_name(e->parent);
++              putchar('/');
+       }
++      printf("%s", e->name);
++}
+-      return -1;
+-}     
++static const char *entry_type(struct tree_entry_list *e)
++{
++      return (e->directory ? "tree" : "blob");
++}
+-/*
+- * match may be NULL, or a *sorted* list of paths
+- */
+-static void list_recursive(void *buffer,
+-                         const char *type,
+-                         unsigned long size,
+-                         struct path_prefix *prefix,
+-                         char **match, int matches)
+-{
+-      struct path_prefix this_prefix;
+-      this_prefix.prev = prefix;
+-
+-      if (strcmp(type, "tree"))
+-              die("expected a 'tree' node");
+-
+-      if (matches)
+-              recursive = 1;
+-
+-      while (size) {
+-              int namelen = strlen(buffer)+1;
+-              void *eltbuf = NULL;
+-              char elttype[20];
+-              unsigned long eltsize;
+-              unsigned char *sha1 = buffer + namelen;
+-              char *path = strchr(buffer, ' ') + 1;
+-              unsigned int mode;
+-              const char *matched = NULL;
+-              int mtype = -1;
+-              int mindex;
+-
+-              if (size < namelen + 20 || sscanf(buffer, "%o", &mode) != 1)
+-                      die("corrupt 'tree' file");
+-              buffer = sha1 + 20;
+-              size -= namelen + 20;
+-
+-              this_prefix.name = path;
+-              for ( mindex = 0; mindex < matches; mindex++) {
+-                      mtype = pathcmp(match[mindex],&this_prefix);
+-                      if (mtype >= 0) {
+-                              matched = match[mindex];
+-                              break;
+-                      }
+-              }
++static const char *entry_hex(struct tree_entry_list *e)
++{
++      return sha1_to_hex(e->directory
++                         ? e->item.tree->object.sha1
++                         : e->item.blob->object.sha1);
++}
+-              /*
+-               * If we're not matching, or if this is an exact match,
+-               * print out the info
+-               */
+-              if (!matches || (matched != NULL && mtype == 0)) {
+-                      printf("%06o %s %s\t", mode,
+-                             S_ISDIR(mode) ? "tree" : "blob",
+-                             sha1_to_hex(sha1));
+-                      print_path_prefix(&this_prefix);
+-                      putchar(line_termination);
+-              }
++/* forward declaration for mutually recursive routines */
++static int show_entry(struct tree_entry_list *, int);
+-              if (! recursive || ! S_ISDIR(mode))
+-                      continue;
++static int show_children(struct tree_entry_list *e, int level)
++{
++      if (prepare_children(e))
++              die("internal error: ls-tree show_children called with non tree");
++      e = e->item.tree->entries;
++      while (e) {
++              show_entry(e, level);
++              e = e->next;
++      }
++      return 0;
++}
+-              if (matches && ! matched)
+-                      continue;
++static int show_entry(struct tree_entry_list *e, int level)
++{
++      int err = 0; 
+-              if (! (eltbuf = read_sha1_file(sha1, elttype, &eltsize)) ) {
+-                      error("cannot read %s", sha1_to_hex(sha1));
+-                      continue;
+-              }
++      if (e != &root_entry) {
++              printf("%06o %s %s      ", e->mode, entry_type(e),
++                     entry_hex(e));
++              show_entry_name(e);
++              putchar(line_termination);
++      }
+-              /* If this is an exact directory match, we may have
+-               * directory files following this path. Match on them.
+-               * Otherwise, we're at a pach subcomponent, and we need
+-               * to try to match again.
++      if (e->directory) {
++              /* If this is a directory, we have the following cases:
++               * (1) This is the top-level request (explicit path from the
++               *     command line, or "root" if there is no command line).
++               *  a. Without any flag.  We show direct children.  We do not 
++               *     recurse into them.
++               *  b. With -r.  We do recurse into children.
++               *  c. With -d.  We do not recurse into children.
++               * (2) We came here because our caller is either (1-a) or
++               *     (1-b).
++               *  a. Without any flag.  We do not show our children (which
++               *     are grandchildren for the original request).
++               *  b. With -r.  We continue to recurse into our children.
++               *  c. With -d.  We should not have come here to begin with.
+                */
+-              if (mtype == 0)
+-                      mindex++;
+-
+-              list_recursive(eltbuf, elttype, eltsize, &this_prefix, &match[mindex], matches-mindex);
+-              free(eltbuf);
++              if (level == 0 && !(ls_options & LS_TREE_ONLY))
++                      /* case (1)-a and (1)-b */
++                      err = err | show_children(e, level+1);
++              else if (level && ls_options & LS_RECURSIVE)
++                      /* case (2)-b */
++                      err = err | show_children(e, level+1);
+       }
++      return err;
+ }
+-static int qcmp(const void *a, const void *b)
++static int list_one(const char *path, const char *path_end)
+ {
+-      return strcmp(*(char **)a, *(char **)b);
++      int err = 0;
++      struct tree_entry_list *e = find_entry(path, path_end);
++      if (!e) {
++              /* traditionally ls-tree does not complain about
++               * missing path.  We may change this later to match
++               * what "/bin/ls -a" does, which is to complain.
++               */
++              return err;
++      }
++      err = err | show_entry(e, 0);
++      return err;
+ }
+-static int list(unsigned char *sha1,char **path)
++static int list(char **path)
+ {
+-      void *buffer;
+-      unsigned long size;
+-      int npaths;
+-
+-      for (npaths = 0; path[npaths] != NULL; npaths++)
+-              ;
+-
+-      qsort(path,npaths,sizeof(char *),qcmp);
+-
+-      buffer = read_object_with_reference(sha1, "tree", &size, NULL);
+-      if (!buffer)
+-              die("unable to read sha1 file");
+-      list_recursive(buffer, "tree", size, NULL, path, npaths);
+-      free(buffer);
+-      return 0;
++      int i;
++      int err = 0;
++      for (i = 0; path[i]; i++) {
++              int len = strlen(path[i]);
++              while (0 <= len && path[i][len] == '/')
++                      len--;
++              err = err | list_one(path[i], path[i] + len);
++      }
++      return err;
+ }
+-static const char *ls_tree_usage = "git-ls-tree [-r] [-z] <key> [paths...]";
++static const char *ls_tree_usage =
++      "git-ls-tree [-d] [-r] [-z] <tree-ish> [path...]";
+ int main(int argc, char **argv)
+ {
++      static char *path0[] = { "", NULL };
++      char **path;
+       unsigned char sha1[20];
+       while (1 < argc && argv[1][0] == '-') {
+@@ -194,7 +223,10 @@ int main(int argc, char **argv)
+                       line_termination = 0;
+                       break;
+               case 'r':
+-                      recursive = 1;
++                      ls_options |= LS_RECURSIVE;
++                      break;
++              case 'd':
++                      ls_options |= LS_TREE_ONLY;
+                       break;
+               default:
+                       usage(ls_tree_usage);
+@@ -206,7 +238,10 @@ int main(int argc, char **argv)
+               usage(ls_tree_usage);
+       if (get_sha1(argv[1], sha1) < 0)
+               usage(ls_tree_usage);
+-      if (list(sha1, &argv[2]) < 0)
++
++      path = (argc == 2) ? path0 : (argv + 2);
++      prepare_root(sha1);
++      if (list(path) < 0)
+               die("list failed");
+       return 0;
+ }
+diff a/t/t3100-ls-tree-restrict.sh b/t/t3100-ls-tree-restrict.sh
+--- a/t/t3100-ls-tree-restrict.sh
++++ b/t/t3100-ls-tree-restrict.sh
+@@ -74,8 +74,8 @@ test_expect_success \
+     'ls-tree filtered' \
+     'git-ls-tree $tree path1 path0 >current &&
+      cat >expected <<\EOF &&
+-100644 blob X path0
+ 120000 blob X path1
++100644 blob X path0
+ EOF
+      test_output'
+@@ -85,7 +85,6 @@ test_expect_success \
+      cat >expected <<\EOF &&
+ 040000 tree X path2
+ 040000 tree X path2/baz
+-100644 blob X path2/baz/b
+ 120000 blob X path2/bazbo
+ 100644 blob X path2/foo
+ EOF
+diff a/tree.c b/tree.c
+--- a/tree.c
++++ b/tree.c
+@@ -133,7 +133,7 @@ int parse_tree_buffer(struct tree *item,
+               }
+               if (obj)
+                       add_ref(&item->object, obj);
+-
++              entry->parent = NULL; /* needs to be filled by the user */
+               *list_p = entry;
+               list_p = &entry->next;
+       }
+diff a/tree.h b/tree.h
+--- a/tree.h
++++ b/tree.h
+@@ -16,6 +16,7 @@ struct tree_entry_list {
+               struct tree *tree;
+               struct blob *blob;
+       } item;
++      struct tree_entry_list *parent;
+ };
+ struct tree {
diff --git a/t/t4101-apply-nonl.sh b/t/t4101-apply-nonl.sh
new file mode 100755 (executable)
index 0000000..da8abcf
--- /dev/null
@@ -0,0 +1,30 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git apply should handle files with incomplete lines.
+
+'
+. ./test-lib.sh
+
+# setup
+
+(echo a; echo b) >frotz.0
+(echo a; echo b; echo c) >frotz.1
+(echo a; echo b | tr -d '\012') >frotz.2
+(echo a; echo c; echo b | tr -d '\012') >frotz.3
+
+for i in 0 1 2 3
+do
+  for j in 0 1 2 3
+  do
+    test $i -eq $j && continue
+    cat frotz.$i >frotz
+    test_expect_success \
+        "apply diff between $i and $j" \
+       "git apply <../t4101/diff.$i-$j && diff frotz.$j frotz"
+  done
+done
+
+test_done
diff --git a/t/t4101/diff.0-1 b/t/t4101/diff.0-1
new file mode 100644 (file)
index 0000000..1010a88
--- /dev/null
@@ -0,0 +1,6 @@
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,3 @@
+ a
+ b
++c
diff --git a/t/t4101/diff.0-2 b/t/t4101/diff.0-2
new file mode 100644 (file)
index 0000000..36460a2
--- /dev/null
@@ -0,0 +1,7 @@
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,2 @@
+ a
+-b
++b
+\ No newline at end of file
diff --git a/t/t4101/diff.0-3 b/t/t4101/diff.0-3
new file mode 100644 (file)
index 0000000..b281c43
--- /dev/null
@@ -0,0 +1,8 @@
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,3 @@
+ a
+-b
++c
++b
+\ No newline at end of file
diff --git a/t/t4101/diff.1-0 b/t/t4101/diff.1-0
new file mode 100644 (file)
index 0000000..f0a2e92
--- /dev/null
@@ -0,0 +1,6 @@
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,2 @@
+ a
+ b
+-c
diff --git a/t/t4101/diff.1-2 b/t/t4101/diff.1-2
new file mode 100644 (file)
index 0000000..2a440a5
--- /dev/null
@@ -0,0 +1,8 @@
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,2 @@
+ a
+-b
+-c
++b
+\ No newline at end of file
diff --git a/t/t4101/diff.1-3 b/t/t4101/diff.1-3
new file mode 100644 (file)
index 0000000..61aff97
--- /dev/null
@@ -0,0 +1,8 @@
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,3 @@
+ a
+-b
+ c
++b
+\ No newline at end of file
diff --git a/t/t4101/diff.2-0 b/t/t4101/diff.2-0
new file mode 100644 (file)
index 0000000..c2e71ee
--- /dev/null
@@ -0,0 +1,7 @@
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,2 @@
+ a
+-b
+\ No newline at end of file
++b
diff --git a/t/t4101/diff.2-1 b/t/t4101/diff.2-1
new file mode 100644 (file)
index 0000000..a66d9fd
--- /dev/null
@@ -0,0 +1,8 @@
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,3 @@
+ a
+-b
+\ No newline at end of file
++b
++c
diff --git a/t/t4101/diff.2-3 b/t/t4101/diff.2-3
new file mode 100644 (file)
index 0000000..5633c83
--- /dev/null
@@ -0,0 +1,7 @@
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,3 @@
+ a
++c
+ b
+\ No newline at end of file
diff --git a/t/t4101/diff.3-0 b/t/t4101/diff.3-0
new file mode 100644 (file)
index 0000000..10b1a41
--- /dev/null
@@ -0,0 +1,8 @@
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,2 @@
+ a
+-c
+-b
+\ No newline at end of file
++b
diff --git a/t/t4101/diff.3-1 b/t/t4101/diff.3-1
new file mode 100644 (file)
index 0000000..c799c60
--- /dev/null
@@ -0,0 +1,8 @@
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,3 @@
+ a
++b
+ c
+-b
+\ No newline at end of file
diff --git a/t/t4101/diff.3-2 b/t/t4101/diff.3-2
new file mode 100644 (file)
index 0000000..f8d1ba6
--- /dev/null
@@ -0,0 +1,7 @@
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,2 @@
+ a
+-c
+ b
+\ No newline at end of file
diff --git a/t/t4102-apply-rename.sh b/t/t4102-apply-rename.sh
new file mode 100755 (executable)
index 0000000..d42abff
--- /dev/null
@@ -0,0 +1,62 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git apply handling copy/rename patch.
+
+'
+. ./test-lib.sh
+
+# setup
+
+cat >test-patch <<\EOF
+diff --git a/foo b/bar
+similarity index 47%
+rename from foo
+rename to bar
+--- a/foo
++++ b/bar
+@@ -1 +1 @@
+-This is foo
++This is bar
+EOF
+
+echo 'This is foo' >foo
+chmod +x foo
+
+test_expect_success setup \
+    'git update-index --add foo'
+
+test_expect_success apply \
+    'git apply --index --stat --summary --apply test-patch'
+
+if [ "$(git config --get core.filemode)" = false ]
+then
+       say 'filemode disabled on the filesystem'
+else
+       test_expect_success validate \
+           'test -f bar && ls -l bar | grep "^-..x......"'
+fi
+
+test_expect_success 'apply reverse' \
+    'git apply -R --index --stat --summary --apply test-patch &&
+     test "$(cat foo)" = "This is foo"'
+
+cat >test-patch <<\EOF
+diff --git a/foo b/bar
+similarity index 47%
+copy from foo
+copy to bar
+--- a/foo
++++ b/bar
+@@ -1 +1 @@
+-This is foo
++This is bar
+EOF
+
+test_expect_success 'apply copy' \
+    'git apply --index --stat --summary --apply test-patch &&
+     test "$(cat bar)" = "This is bar" -a "$(cat foo)" = "This is foo"'
+
+test_done
diff --git a/t/t4103-apply-binary.sh b/t/t4103-apply-binary.sh
new file mode 100755 (executable)
index 0000000..011126f
--- /dev/null
@@ -0,0 +1,115 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git apply handling binary patches
+
+'
+. ./test-lib.sh
+
+# setup
+
+cat >file1 <<EOF
+A quick brown fox jumps over the lazy dog.
+A tiny little penguin runs around in circles.
+There is a flag with Linux written on it.
+A slow black-and-white panda just sits there,
+munching on his bamboo.
+EOF
+cat file1 >file2
+cat file1 >file4
+
+git update-index --add --remove file1 file2 file4
+git-commit -m 'Initial Version' 2>/dev/null
+
+git-checkout -b binary
+tr 'x' '\0' <file1 >file3
+cat file3 >file4
+git add file2
+tr '\0' 'v' <file3 >file1
+rm -f file2
+git update-index --add --remove file1 file2 file3 file4
+git-commit -m 'Second Version'
+
+git diff-tree -p master binary >B.diff
+git diff-tree -p -C master binary >C.diff
+
+git diff-tree -p --binary master binary >BF.diff
+git diff-tree -p --binary -C master binary >CF.diff
+
+test_expect_success 'stat binary diff -- should not fail.' \
+       'git-checkout master
+        git apply --stat --summary B.diff'
+
+test_expect_success 'stat binary diff (copy) -- should not fail.' \
+       'git-checkout master
+        git apply --stat --summary C.diff'
+
+test_expect_failure 'check binary diff -- should fail.' \
+       'git-checkout master
+        git apply --check B.diff'
+
+test_expect_failure 'check binary diff (copy) -- should fail.' \
+       'git-checkout master
+        git apply --check C.diff'
+
+test_expect_failure 'check incomplete binary diff with replacement -- should fail.' \
+       'git-checkout master
+        git apply --check --allow-binary-replacement B.diff'
+
+test_expect_failure 'check incomplete binary diff with replacement (copy) -- should fail.' \
+       'git-checkout master
+        git apply --check --allow-binary-replacement C.diff'
+
+test_expect_success 'check binary diff with replacement.' \
+       'git-checkout master
+        git apply --check --allow-binary-replacement BF.diff'
+
+test_expect_success 'check binary diff with replacement (copy).' \
+       'git-checkout master
+        git apply --check --allow-binary-replacement CF.diff'
+
+# Now we start applying them.
+
+do_reset () {
+       rm -f file?
+       git-reset --hard
+       git-checkout -f master
+}
+
+test_expect_failure 'apply binary diff -- should fail.' \
+       'do_reset
+        git apply B.diff'
+
+test_expect_failure 'apply binary diff -- should fail.' \
+       'do_reset
+        git apply --index B.diff'
+
+test_expect_failure 'apply binary diff (copy) -- should fail.' \
+       'do_reset
+        git apply C.diff'
+
+test_expect_failure 'apply binary diff (copy) -- should fail.' \
+       'do_reset
+        git apply --index C.diff'
+
+test_expect_success 'apply binary diff without replacement.' \
+       'do_reset
+        git apply BF.diff'
+
+test_expect_success 'apply binary diff without replacement (copy).' \
+       'do_reset
+        git apply CF.diff'
+
+test_expect_success 'apply binary diff.' \
+       'do_reset
+        git apply --allow-binary-replacement --index BF.diff &&
+        test -z "$(git diff --name-status binary)"'
+
+test_expect_success 'apply binary diff (copy).' \
+       'do_reset
+        git apply --allow-binary-replacement --index CF.diff &&
+        test -z "$(git diff --name-status binary)"'
+
+test_done
diff --git a/t/t4104-apply-boundary.sh b/t/t4104-apply-boundary.sh
new file mode 100755 (executable)
index 0000000..64f34e3
--- /dev/null
@@ -0,0 +1,115 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git apply boundary tests
+
+'
+. ./test-lib.sh
+
+L="c d e f g h i j k l m n o p q r s t u v w x"
+
+test_expect_success setup '
+       for i in b '"$L"' y
+       do
+               echo $i
+       done >victim &&
+       cat victim >original &&
+       git update-index --add victim &&
+
+       : add to the head
+       for i in a b '"$L"' y
+       do
+               echo $i
+       done >victim &&
+       cat victim >add-a-expect &&
+       git diff victim >add-a-patch.with &&
+       git diff --unified=0 >add-a-patch.without &&
+
+       : modify at the head
+       for i in a '"$L"' y
+       do
+               echo $i
+       done >victim &&
+       cat victim >mod-a-expect &&
+       git diff victim >mod-a-patch.with &&
+       git diff --unified=0 >mod-a-patch.without &&
+
+       : remove from the head
+       for i in '"$L"' y
+       do
+               echo $i
+       done >victim &&
+       cat victim >del-a-expect &&
+       git diff victim >del-a-patch.with
+       git diff --unified=0 >del-a-patch.without &&
+
+       : add to the tail
+       for i in b '"$L"' y z
+       do
+               echo $i
+       done >victim &&
+       cat victim >add-z-expect &&
+       git diff victim >add-z-patch.with &&
+       git diff --unified=0 >add-z-patch.without &&
+
+       : modify at the tail
+       for i in a '"$L"' y
+       do
+               echo $i
+       done >victim &&
+       cat victim >mod-z-expect &&
+       git diff victim >mod-z-patch.with &&
+       git diff --unified=0 >mod-z-patch.without &&
+
+       : remove from the tail
+       for i in b '"$L"'
+       do
+               echo $i
+       done >victim &&
+       cat victim >del-z-expect &&
+       git diff victim >del-z-patch.with
+       git diff --unified=0 >del-z-patch.without &&
+
+       : done
+'
+
+for with in with without
+do
+       case "$with" in
+       with) u= ;;
+       without) u='--unidiff-zero ' ;;
+       esac
+       for kind in add-a add-z mod-a mod-z del-a del-z
+       do
+               test_expect_success "apply $kind-patch $with context" '
+                       cat original >victim &&
+                       git update-index victim &&
+                       git apply --index '"$u$kind-patch.$with"' || {
+                               cat '"$kind-patch.$with"'
+                               (exit 1)
+                       } &&
+                       git diff '"$kind"'-expect victim
+               '
+       done
+done
+
+for kind in add-a add-z mod-a mod-z del-a del-z
+do
+       rm -f $kind-ng.without
+       sed     -e "s/^diff --git /diff /" \
+               -e '/^index /d' \
+               <$kind-patch.without >$kind-ng.without
+       test_expect_success "apply non-git $kind-patch without context" '
+               cat original >victim &&
+               git update-index victim &&
+               git apply --unidiff-zero --index '"$kind-ng.without"' || {
+                       cat '"$kind-ng.without"'
+                       (exit 1)
+               } &&
+               git diff '"$kind"'-expect victim
+       '
+done
+
+test_done
diff --git a/t/t4109-apply-multifrag.sh b/t/t4109-apply-multifrag.sh
new file mode 100755 (executable)
index 0000000..bd40a21
--- /dev/null
@@ -0,0 +1,176 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+# Copyright (c) 2005 Robert Fitzsimons
+#
+
+test_description='git apply test patches with multiple fragments.
+
+'
+. ./test-lib.sh
+
+# setup
+
+cat > patch1.patch <<\EOF
+diff --git a/main.c b/main.c
+new file mode 100644
+--- /dev/null
++++ b/main.c
+@@ -0,0 +1,23 @@
++#include <stdio.h>
++
++int func(int num);
++void print_int(int num);
++
++int main() {
++      int i;
++
++      for (i = 0; i < 10; i++) {
++              print_int(func(i));
++      }
++
++      return 0;
++}
++
++int func(int num) {
++      return num * num;
++}
++
++void print_int(int num) {
++      printf("%d", num);
++}
++
+EOF
+cat > patch2.patch <<\EOF
+diff --git a/main.c b/main.c
+--- a/main.c
++++ b/main.c
+@@ -1,7 +1,9 @@
++#include <stdlib.h>
+ #include <stdio.h>
+ int func(int num);
+ void print_int(int num);
++void print_ln();
+ int main() {
+       int i;
+@@ -10,6 +12,8 @@
+               print_int(func(i));
+       }
++      print_ln();
++
+       return 0;
+ }
+@@ -21,3 +25,7 @@
+       printf("%d", num);
+ }
++void print_ln() {
++      printf("\n");
++}
++
+EOF
+cat > patch3.patch <<\EOF
+diff --git a/main.c b/main.c
+--- a/main.c
++++ b/main.c
+@@ -1,9 +1,7 @@
+-#include <stdlib.h>
+ #include <stdio.h>
+ int func(int num);
+ void print_int(int num);
+-void print_ln();
+ int main() {
+       int i;
+@@ -12,8 +10,6 @@
+               print_int(func(i));
+       }
+-      print_ln();
+-
+       return 0;
+ }
+@@ -25,7 +21,3 @@
+       printf("%d", num);
+ }
+-void print_ln() {
+-      printf("\n");
+-}
+-
+EOF
+cat > patch4.patch <<\EOF
+diff --git a/main.c b/main.c
+--- a/main.c
++++ b/main.c
+@@ -1,13 +1,14 @@
+ #include <stdio.h>
+ int func(int num);
+-void print_int(int num);
++int func2(int num);
+ int main() {
+       int i;
+       for (i = 0; i < 10; i++) {
+-              print_int(func(i));
++              printf("%d", func(i));
++              printf("%d", func3(i));
+       }
+       return 0;
+@@ -17,7 +18,7 @@
+       return num * num;
+ }
+-void print_int(int num) {
+-      printf("%d", num);
++int func2(int num) {
++      return num * num * num;
+ }
+EOF
+
+test_expect_success "S = git apply (1)" \
+    'git apply patch1.patch patch2.patch'
+mv main.c main.c.git
+
+test_expect_success "S = patch (1)" \
+    'cat patch1.patch patch2.patch | patch -p1'
+
+test_expect_success "S = cmp (1)" \
+    'cmp main.c.git main.c'
+
+rm -f main.c main.c.git
+
+test_expect_success "S = git apply (2)" \
+    'git apply patch1.patch patch2.patch patch3.patch'
+mv main.c main.c.git
+
+test_expect_success "S = patch (2)" \
+    'cat patch1.patch patch2.patch patch3.patch | patch -p1'
+
+test_expect_success "S = cmp (2)" \
+    'cmp main.c.git main.c'
+
+rm -f main.c main.c.git
+
+test_expect_success "S = git apply (3)" \
+    'git apply patch1.patch patch4.patch'
+mv main.c main.c.git
+
+test_expect_success "S = patch (3)" \
+    'cat patch1.patch patch4.patch | patch -p1'
+
+test_expect_success "S = cmp (3)" \
+    'cmp main.c.git main.c'
+
+test_done
+
diff --git a/t/t4110-apply-scan.sh b/t/t4110-apply-scan.sh
new file mode 100755 (executable)
index 0000000..db60652
--- /dev/null
@@ -0,0 +1,100 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+# Copyright (c) 2005 Robert Fitzsimons
+#
+
+test_description='git apply test for patches which require scanning forwards and backwards.
+
+'
+. ./test-lib.sh
+
+# setup
+
+cat > patch1.patch <<\EOF
+diff --git a/new.txt b/new.txt
+new file mode 100644
+--- /dev/null
++++ b/new.txt
+@@ -0,0 +1,12 @@
++a1
++a11
++a111
++a1111
++b1
++b11
++b111
++b1111
++c1
++c11
++c111
++c1111
+EOF
+cat > patch2.patch <<\EOF
+diff --git a/new.txt b/new.txt
+--- a/new.txt
++++ b/new.txt
+@@ -1,7 +1,3 @@
+-a1
+-a11
+-a111
+-a1111
+ b1
+ b11
+ b111
+EOF
+cat > patch3.patch <<\EOF
+diff --git a/new.txt b/new.txt
+--- a/new.txt
++++ b/new.txt
+@@ -6,6 +6,10 @@
+ b11
+ b111
+ b1111
++b2
++b22
++b222
++b2222
+ c1
+ c11
+ c111
+EOF
+cat > patch4.patch <<\EOF
+diff --git a/new.txt b/new.txt
+--- a/new.txt
++++ b/new.txt
+@@ -1,3 +1,7 @@
++a1
++a11
++a111
++a1111
+ b1
+ b11
+ b111
+EOF
+cat > patch5.patch <<\EOF
+diff --git a/new.txt b/new.txt
+--- a/new.txt
++++ b/new.txt
+@@ -10,3 +10,7 @@
+ c11
+ c111
+ c1111
++c2
++c22
++c222
++c2222
+EOF
+
+test_expect_success "S = git apply scan" \
+    'git apply patch1.patch patch2.patch patch3.patch patch4.patch patch5.patch'
+mv new.txt apply.txt
+
+test_expect_success "S = patch scan" \
+    'cat patch1.patch patch2.patch patch3.patch patch4.patch patch5.patch | patch'
+mv new.txt patch.txt
+
+test_expect_success "S = cmp" \
+    'cmp apply.txt patch.txt'
+
+test_done
diff --git a/t/t4112-apply-renames.sh b/t/t4112-apply-renames.sh
new file mode 100755 (executable)
index 0000000..70a1859
--- /dev/null
@@ -0,0 +1,124 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git apply should not get confused with rename/copy.
+
+'
+
+. ./test-lib.sh
+
+# setup
+
+mkdir -p klibc/arch/x86_64/include/klibc
+
+cat >klibc/arch/x86_64/include/klibc/archsetjmp.h <<\EOF
+/*
+ * arch/x86_64/include/klibc/archsetjmp.h
+ */
+
+#ifndef _KLIBC_ARCHSETJMP_H
+#define _KLIBC_ARCHSETJMP_H
+
+struct __jmp_buf {
+  unsigned long __rbx;
+  unsigned long __rsp;
+  unsigned long __rbp;
+  unsigned long __r12;
+  unsigned long __r13;
+  unsigned long __r14;
+  unsigned long __r15;
+  unsigned long __rip;
+};
+
+typedef struct __jmp_buf jmp_buf[1];
+
+#endif /* _SETJMP_H */
+EOF
+
+cat >patch <<\EOF
+diff --git a/klibc/arch/x86_64/include/klibc/archsetjmp.h b/include/arch/cris/klibc/archsetjmp.h
+similarity index 76%
+copy from klibc/arch/x86_64/include/klibc/archsetjmp.h
+copy to include/arch/cris/klibc/archsetjmp.h
+--- a/klibc/arch/x86_64/include/klibc/archsetjmp.h
++++ b/include/arch/cris/klibc/archsetjmp.h
+@@ -1,21 +1,24 @@
+ /*
+- * arch/x86_64/include/klibc/archsetjmp.h
++ * arch/cris/include/klibc/archsetjmp.h
+  */
+
+ #ifndef _KLIBC_ARCHSETJMP_H
+ #define _KLIBC_ARCHSETJMP_H
+
+ struct __jmp_buf {
+-  unsigned long __rbx;
+-  unsigned long __rsp;
+-  unsigned long __rbp;
+-  unsigned long __r12;
+-  unsigned long __r13;
+-  unsigned long __r14;
+-  unsigned long __r15;
+-  unsigned long __rip;
++  unsigned long __r0;
++  unsigned long __r1;
++  unsigned long __r2;
++  unsigned long __r3;
++  unsigned long __r4;
++  unsigned long __r5;
++  unsigned long __r6;
++  unsigned long __r7;
++  unsigned long __r8;
++  unsigned long __sp;
++  unsigned long __srp;
+ };
+
+ typedef struct __jmp_buf jmp_buf[1];
+
+-#endif /* _SETJMP_H */
++#endif /* _KLIBC_ARCHSETJMP_H */
+diff --git a/klibc/arch/x86_64/include/klibc/archsetjmp.h b/include/arch/m32r/klibc/archsetjmp.h
+similarity index 66%
+rename from klibc/arch/x86_64/include/klibc/archsetjmp.h
+rename to include/arch/m32r/klibc/archsetjmp.h
+--- a/klibc/arch/x86_64/include/klibc/archsetjmp.h
++++ b/include/arch/m32r/klibc/archsetjmp.h
+@@ -1,21 +1,21 @@
+ /*
+- * arch/x86_64/include/klibc/archsetjmp.h
++ * arch/m32r/include/klibc/archsetjmp.h
+  */
+
+ #ifndef _KLIBC_ARCHSETJMP_H
+ #define _KLIBC_ARCHSETJMP_H
+
+ struct __jmp_buf {
+-  unsigned long __rbx;
+-  unsigned long __rsp;
+-  unsigned long __rbp;
++  unsigned long __r8;
++  unsigned long __r9;
++  unsigned long __r10;
++  unsigned long __r11;
+   unsigned long __r12;
+   unsigned long __r13;
+   unsigned long __r14;
+   unsigned long __r15;
+-  unsigned long __rip;
+ };
+
+ typedef struct __jmp_buf jmp_buf[1];
+
+-#endif /* _SETJMP_H */
++#endif /* _KLIBC_ARCHSETJMP_H */
+EOF
+
+find klibc -type f -print | xargs git update-index --add --
+
+test_expect_success 'check rename/copy patch' 'git apply --check patch'
+
+test_expect_success 'apply rename/copy patch' 'git apply --index patch'
+
+test_done
diff --git a/t/t4113-apply-ending.sh b/t/t4113-apply-ending.sh
new file mode 100755 (executable)
index 0000000..1c6bec0
--- /dev/null
@@ -0,0 +1,53 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Catalin Marinas
+#
+
+test_description='git apply trying to add an ending line.
+
+'
+. ./test-lib.sh
+
+# setup
+
+cat >test-patch <<\EOF
+diff --git a/file b/file
+--- a/file
++++ b/file
+@@ -1,2 +1,3 @@
+ a
+ b
++c
+EOF
+
+echo 'a' >file
+echo 'b' >>file
+echo 'c' >>file
+
+test_expect_success setup \
+    'git update-index --add file'
+
+# test
+
+test_expect_failure 'apply at the end' \
+    'git apply --index test-patch'
+
+cat >test-patch <<\EOF
+diff a/file b/file
+--- a/file
++++ b/file
+@@ -1,2 +1,3 @@
++a
+ b
+ c
+EOF
+
+echo >file 'a
+b
+c'
+git update-index file
+
+test_expect_failure 'apply at the beginning' \
+       'git apply --index test-patch'
+
+test_done
diff --git a/t/t4114-apply-typechange.sh b/t/t4114-apply-typechange.sh
new file mode 100755 (executable)
index 0000000..5533492
--- /dev/null
@@ -0,0 +1,105 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+#
+
+test_description='git apply should not get confused with type changes.
+
+'
+
+. ./test-lib.sh
+
+test_expect_success 'setup repository and commits' '
+       echo "hello world" > foo &&
+       echo "hi planet" > bar &&
+       git update-index --add foo bar &&
+       git commit -m initial &&
+       git branch initial &&
+       rm -f foo &&
+       ln -s bar foo &&
+       git update-index foo &&
+       git commit -m "foo symlinked to bar" &&
+       git branch foo-symlinked-to-bar &&
+       rm -f foo &&
+       echo "how far is the sun?" > foo &&
+       git update-index foo &&
+       git commit -m "foo back to file" &&
+       git branch foo-back-to-file &&
+       rm -f foo &&
+       git update-index --remove foo &&
+       mkdir foo &&
+       echo "if only I knew" > foo/baz &&
+       git update-index --add foo/baz &&
+       git commit -m "foo becomes a directory" &&
+       git branch "foo-becomes-a-directory" &&
+       echo "hello world" > foo/baz &&
+       git update-index foo/baz &&
+       git commit -m "foo/baz is the original foo" &&
+       git branch foo-baz-renamed-from-foo
+       '
+
+test_expect_success 'file renamed from foo to foo/baz' '
+       git checkout -f initial &&
+       git diff-tree -M -p HEAD foo-baz-renamed-from-foo > patch &&
+       git apply --index < patch
+       '
+test_debug 'cat patch'
+
+
+test_expect_success 'file renamed from foo/baz to foo' '
+       git checkout -f foo-baz-renamed-from-foo &&
+       git diff-tree -M -p HEAD initial > patch &&
+       git apply --index < patch
+       '
+test_debug 'cat patch'
+
+
+test_expect_success 'directory becomes file' '
+       git checkout -f foo-becomes-a-directory &&
+       git diff-tree -p HEAD initial > patch &&
+       git apply --index < patch
+       '
+test_debug 'cat patch'
+
+
+test_expect_success 'file becomes directory' '
+       git checkout -f initial &&
+       git diff-tree -p HEAD foo-becomes-a-directory > patch &&
+       git apply --index < patch
+       '
+test_debug 'cat patch'
+
+
+test_expect_success 'file becomes symlink' '
+       git checkout -f initial &&
+       git diff-tree -p HEAD foo-symlinked-to-bar > patch &&
+       git apply --index < patch
+       '
+test_debug 'cat patch'
+
+
+test_expect_success 'symlink becomes file' '
+       git checkout -f foo-symlinked-to-bar &&
+       git diff-tree -p HEAD foo-back-to-file > patch &&
+       git apply --index < patch
+       '
+test_debug 'cat patch'
+
+
+test_expect_success 'symlink becomes directory' '
+       git checkout -f foo-symlinked-to-bar &&
+       git diff-tree -p HEAD foo-becomes-a-directory > patch &&
+       git apply --index < patch
+       '
+test_debug 'cat patch'
+
+
+test_expect_success 'directory becomes symlink' '
+       git checkout -f foo-becomes-a-directory &&
+       git diff-tree -p HEAD foo-symlinked-to-bar > patch &&
+       git apply --index < patch
+       '
+test_debug 'cat patch'
+
+
+test_done
diff --git a/t/t4115-apply-symlink.sh b/t/t4115-apply-symlink.sh
new file mode 100755 (executable)
index 0000000..a07ff42
--- /dev/null
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git apply symlinks and partial files
+
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       ln -s path1/path2/path3/path4/path5 link1 &&
+       git add link? &&
+       git commit -m initial &&
+
+       git branch side &&
+
+       rm -f link? &&
+
+       ln -s htap6 link1 &&
+       git update-index link? &&
+       git commit -m second &&
+
+       git diff-tree -p HEAD^ HEAD >patch  &&
+       git apply --stat --summary patch
+
+'
+
+test_expect_success 'apply symlink patch' '
+
+       git checkout side &&
+       git apply patch &&
+       git diff-files -p >patched &&
+       git diff patch patched
+
+'
+
+test_expect_success 'apply --index symlink patch' '
+
+       git checkout -f side &&
+       git apply --index patch &&
+       git diff-index --cached -p HEAD >patched &&
+       git diff patch patched
+
+'
+
+test_done
diff --git a/t/t4116-apply-reverse.sh b/t/t4116-apply-reverse.sh
new file mode 100755 (executable)
index 0000000..9ae2b3a
--- /dev/null
@@ -0,0 +1,91 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git apply in reverse
+
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       for i in a b c d e f g h i j k l m n; do echo $i; done >file1 &&
+       tr "[ijk]" '\''[\0\1\2]'\'' <file1 >file2 &&
+
+       git add file1 file2 &&
+       git commit -m initial &&
+       git tag initial &&
+
+       for i in a b c g h i J K L m o n p q; do echo $i; done >file1 &&
+       tr "[mon]" '\''[\0\1\2]'\'' <file1 >file2 &&
+
+       git commit -a -m second &&
+       git tag second &&
+
+       git diff --binary initial second >patch
+
+'
+
+test_expect_success 'apply in forward' '
+
+       T0=`git rev-parse "second^{tree}"` &&
+       git reset --hard initial &&
+       git apply --index --binary patch &&
+       T1=`git write-tree` &&
+       test "$T0" = "$T1"
+'
+
+test_expect_success 'apply in reverse' '
+
+       git reset --hard second &&
+       git apply --reverse --binary --index patch &&
+       git diff >diff &&
+       git diff /dev/null diff
+
+'
+
+test_expect_success 'setup separate repository lacking postimage' '
+
+       git tar-tree initial initial | tar xf - &&
+       (
+               cd initial && git init && git add .
+       ) &&
+
+       git tar-tree second second | tar xf - &&
+       (
+               cd second && git init && git add .
+       )
+
+'
+
+test_expect_success 'apply in forward without postimage' '
+
+       T0=`git rev-parse "second^{tree}"` &&
+       (
+               cd initial &&
+               git apply --index --binary ../patch &&
+               T1=`git write-tree` &&
+               test "$T0" = "$T1"
+       )
+'
+
+test_expect_success 'apply in reverse without postimage' '
+
+       T0=`git rev-parse "initial^{tree}"` &&
+       (
+               cd second &&
+               git apply --index --binary --reverse ../patch &&
+               T1=`git write-tree` &&
+               test "$T0" = "$T1"
+       )
+'
+
+test_expect_success 'reversing a whitespace introduction' '
+       sed "s/a/a /" < file1 > file1.new &&
+       mv file1.new file1 &&
+       git diff | git apply --reverse --whitespace=error
+'
+
+test_done
diff --git a/t/t4117-apply-reject.sh b/t/t4117-apply-reject.sh
new file mode 100755 (executable)
index 0000000..659e17c
--- /dev/null
@@ -0,0 +1,157 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git apply with rejects
+
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
+       do
+               echo $i
+       done >file1 &&
+       cat file1 >saved.file1 &&
+       git update-index --add file1 &&
+       git commit -m initial &&
+
+       for i in 1 2 A B 4 5 6 7 8 9 10 11 12 C 13 14 15 16 17 18 19 20 D 21
+       do
+               echo $i
+       done >file1 &&
+       git diff >patch.1 &&
+       cat file1 >clean &&
+
+       for i in 1 E 2 3 4 5 6 7 8 9 10 11 12 C 13 14 15 16 17 18 19 20 F 21
+       do
+               echo $i
+       done >expected &&
+
+       mv file1 file2 &&
+       git update-index --add --remove file1 file2 &&
+       git diff -M HEAD >patch.2 &&
+
+       rm -f file1 file2 &&
+       mv saved.file1 file1 &&
+       git update-index --add --remove file1 file2 &&
+
+       for i in 1 E 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 F 21
+       do
+               echo $i
+       done >file1 &&
+
+       cat file1 >saved.file1
+'
+
+test_expect_success 'apply without --reject should fail' '
+
+       if git apply patch.1
+       then
+               echo "Eh? Why?"
+               exit 1
+       fi
+
+       git diff file1 saved.file1
+'
+
+test_expect_success 'apply without --reject should fail' '
+
+       if git apply --verbose patch.1
+       then
+               echo "Eh? Why?"
+               exit 1
+       fi
+
+       git diff file1 saved.file1
+'
+
+test_expect_success 'apply with --reject should fail but update the file' '
+
+       cat saved.file1 >file1 &&
+       rm -f file1.rej file2.rej &&
+
+       if git apply --reject patch.1
+       then
+               echo "succeeds with --reject?"
+               exit 1
+       fi
+
+       git diff file1 expected &&
+
+       cat file1.rej &&
+
+       if test -f file2.rej
+       then
+               echo "file2 should not have been touched"
+               exit 1
+       fi
+'
+
+test_expect_success 'apply with --reject should fail but update the file' '
+
+       cat saved.file1 >file1 &&
+       rm -f file1.rej file2.rej file2 &&
+
+       if git apply --reject patch.2 >rejects
+       then
+               echo "succeeds with --reject?"
+               exit 1
+       fi
+
+       test -f file1 && {
+               echo "file1 still exists?"
+               exit 1
+       }
+       git diff file2 expected &&
+
+       cat file2.rej &&
+
+       if test -f file1.rej
+       then
+               echo "file2 should not have been touched"
+               exit 1
+       fi
+
+'
+
+test_expect_success 'the same test with --verbose' '
+
+       cat saved.file1 >file1 &&
+       rm -f file1.rej file2.rej file2 &&
+
+       if git apply --reject --verbose patch.2 >rejects
+       then
+               echo "succeeds with --reject?"
+               exit 1
+       fi
+
+       test -f file1 && {
+               echo "file1 still exists?"
+               exit 1
+       }
+       git diff file2 expected &&
+
+       cat file2.rej &&
+
+       if test -f file1.rej
+       then
+               echo "file2 should not have been touched"
+               exit 1
+       fi
+
+'
+
+test_expect_success 'apply cleanly with --verbose' '
+
+       git cat-file -p HEAD:file1 >file1 &&
+       rm -f file?.rej file2 &&
+
+       git apply --verbose patch.1 &&
+
+       git diff file1 clean
+'
+
+test_done
diff --git a/t/t4118-apply-empty-context.sh b/t/t4118-apply-empty-context.sh
new file mode 100755 (executable)
index 0000000..1d531ca
--- /dev/null
@@ -0,0 +1,55 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='git apply with new style GNU diff with empty context
+
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       {
+               echo; echo;
+               echo A; echo B; echo C;
+               echo;
+       } >file1 &&
+       cat file1 >file1.orig &&
+       {
+               cat file1 &&
+               echo Q | tr -d "\\012"
+       } >file2 &&
+       cat file2 >file2.orig
+       git add file1 file2 &&
+       sed -e "/^B/d" <file1.orig >file1 &&
+       sed -e "/^[BQ]/d" <file2.orig >file2 &&
+       echo Q | tr -d "\\012" >>file2 &&
+       cat file1 >file1.mods &&
+       cat file2 >file2.mods &&
+       git diff |
+       sed -e "s/^ \$//" >diff.output
+'
+
+test_expect_success 'apply --numstat' '
+
+       git apply --numstat diff.output >actual &&
+       {
+               echo "0 1       file1" &&
+               echo "0 1       file2"
+       } >expect &&
+       git diff expect actual
+
+'
+
+test_expect_success 'apply --apply' '
+
+       cat file1.orig >file1 &&
+       cat file2.orig >file2 &&
+       git update-index file1 file2 &&
+       git apply --index diff.output &&
+       git diff file1.mods file1 &&
+       git diff file2.mods file2
+'
+
+test_done
diff --git a/t/t4119-apply-config.sh b/t/t4119-apply-config.sh
new file mode 100755 (executable)
index 0000000..b540f72
--- /dev/null
@@ -0,0 +1,162 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Junio C Hamano
+#
+
+test_description='git apply --whitespace=strip and configuration file.
+
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       mkdir sub &&
+       echo A >sub/file1 &&
+       cp sub/file1 saved &&
+       git add sub/file1 &&
+       echo "B " >sub/file1 &&
+       git diff >patch.file
+'
+
+# Also handcraft GNU diff output; note this has trailing whitespace.
+cat >gpatch.file <<\EOF &&
+--- file1      2007-02-21 01:04:24.000000000 -0800
++++ file1+     2007-02-21 01:07:44.000000000 -0800
+@@ -1 +1 @@
+-A
++B 
+EOF
+
+sed -e 's|file1|sub/&|' gpatch.file >gpatch-sub.file &&
+sed -e '
+       /^--- /s|file1|a/sub/&|
+       /^+++ /s|file1|b/sub/&|
+' gpatch.file >gpatch-ab-sub.file &&
+
+check_result () {
+       if grep " " "$1"
+       then
+               echo "Eh?"
+               false
+       elif grep B "$1"
+       then
+               echo Happy
+       else
+               echo "Huh?"
+               false
+       fi
+}
+
+test_expect_success 'apply --whitespace=strip' '
+
+       rm -f sub/file1 &&
+       cp saved sub/file1 &&
+       git update-index --refresh &&
+
+       git apply --whitespace=strip patch.file &&
+       check_result sub/file1
+'
+
+test_expect_success 'apply --whitespace=strip from config' '
+
+       rm -f sub/file1 &&
+       cp saved sub/file1 &&
+       git update-index --refresh &&
+
+       git config apply.whitespace strip &&
+       git apply patch.file &&
+       check_result sub/file1
+'
+
+D=`pwd`
+
+test_expect_success 'apply --whitespace=strip in subdir' '
+
+       cd "$D" &&
+       git config --unset-all apply.whitespace
+       rm -f sub/file1 &&
+       cp saved sub/file1 &&
+       git update-index --refresh &&
+
+       cd sub &&
+       git apply --whitespace=strip ../patch.file &&
+       check_result file1
+'
+
+test_expect_success 'apply --whitespace=strip from config in subdir' '
+
+       cd "$D" &&
+       git config apply.whitespace strip &&
+       rm -f sub/file1 &&
+       cp saved sub/file1 &&
+       git update-index --refresh &&
+
+       cd sub &&
+       git apply ../patch.file &&
+       check_result file1
+'
+
+test_expect_success 'same in subdir but with traditional patch input' '
+
+       cd "$D" &&
+       git config apply.whitespace strip &&
+       rm -f sub/file1 &&
+       cp saved sub/file1 &&
+       git update-index --refresh &&
+
+       cd sub &&
+       git apply ../gpatch.file &&
+       check_result file1
+'
+
+test_expect_success 'same but with traditional patch input of depth 1' '
+
+       cd "$D" &&
+       git config apply.whitespace strip &&
+       rm -f sub/file1 &&
+       cp saved sub/file1 &&
+       git update-index --refresh &&
+
+       cd sub &&
+       git apply ../gpatch-sub.file &&
+       check_result file1
+'
+
+test_expect_success 'same but with traditional patch input of depth 2' '
+
+       cd "$D" &&
+       git config apply.whitespace strip &&
+       rm -f sub/file1 &&
+       cp saved sub/file1 &&
+       git update-index --refresh &&
+
+       cd sub &&
+       git apply ../gpatch-ab-sub.file &&
+       check_result file1
+'
+
+test_expect_success 'same but with traditional patch input of depth 1' '
+
+       cd "$D" &&
+       git config apply.whitespace strip &&
+       rm -f sub/file1 &&
+       cp saved sub/file1 &&
+       git update-index --refresh &&
+
+       git apply -p0 gpatch-sub.file &&
+       check_result sub/file1
+'
+
+test_expect_success 'same but with traditional patch input of depth 2' '
+
+       cd "$D" &&
+       git config apply.whitespace strip &&
+       rm -f sub/file1 &&
+       cp saved sub/file1 &&
+       git update-index --refresh &&
+
+       git apply gpatch-ab-sub.file &&
+       check_result sub/file1
+'
+
+test_done
diff --git a/t/t4120-apply-popt.sh b/t/t4120-apply-popt.sh
new file mode 100755 (executable)
index 0000000..83d4ba6
--- /dev/null
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Shawn O. Pearce
+#
+
+test_description='git apply -p handling.'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       mkdir sub &&
+       echo A >sub/file1 &&
+       cp sub/file1 file1 &&
+       git add sub/file1 &&
+       echo B >sub/file1 &&
+       git diff >patch.file &&
+       rm sub/file1 &&
+       rmdir sub
+'
+
+test_expect_success 'apply git diff with -p2' '
+       git apply -p2 patch.file
+'
+
+test_done
diff --git a/t/t4121-apply-diffs.sh b/t/t4121-apply-diffs.sh
new file mode 100755 (executable)
index 0000000..aff551a
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+test_description='git apply for contextually independent diffs'
+. ./test-lib.sh
+
+echo '1
+2
+3
+4
+5
+6
+7
+8' >file
+
+test_expect_success 'setup' \
+       'git add file &&
+       git commit -q -m 1 &&
+       git checkout -b test &&
+       mv file file.tmp &&
+       echo 0 >file &&
+       cat file.tmp >>file &&
+       rm file.tmp &&
+       git commit -a -q -m 2 &&
+       echo 9 >>file &&
+       git commit -a -q -m 3 &&
+       git checkout master'
+
+test_expect_success \
+       'check if contextually independent diffs for the same file apply' \
+       '( git diff test~2 test~1; git diff test~1 test~0 )| git apply'
+
+test_done
diff --git a/t/t4122-apply-symlink-inside.sh b/t/t4122-apply-symlink-inside.sh
new file mode 100755 (executable)
index 0000000..841773f
--- /dev/null
@@ -0,0 +1,55 @@
+#!/bin/sh
+
+test_description='apply to deeper directory without getting fooled with symlink'
+. ./test-lib.sh
+
+lecho () {
+       for l_
+       do
+               echo "$l_"
+       done
+}
+
+test_expect_success setup '
+
+       mkdir -p arch/i386/boot arch/x86_64 &&
+       lecho 1 2 3 4 5 >arch/i386/boot/Makefile &&
+       ln -s ../i386/boot arch/x86_64/boot &&
+       git add . &&
+       test_tick &&
+       git commit -m initial &&
+       git branch test &&
+
+       rm arch/x86_64/boot &&
+       mkdir arch/x86_64/boot &&
+       lecho 2 3 4 5 6 >arch/x86_64/boot/Makefile &&
+       git add . &&
+       test_tick &&
+       git commit -a -m second &&
+
+       git format-patch --binary -1 --stdout >test.patch
+
+'
+
+test_expect_success apply '
+
+       git checkout test &&
+       git diff --exit-code test &&
+       git diff --exit-code --cached test &&
+       git apply --index test.patch
+
+'
+
+test_expect_success 'check result' '
+
+       git diff --exit-code master &&
+       git diff --exit-code --cached master &&
+       test_tick &&
+       git commit -m replay &&
+       T1=$(git rev-parse "master^{tree}") &&
+       T2=$(git rev-parse "HEAD^{tree}") &&
+       test "z$T1" = "z$T2"
+
+'
+
+test_done
diff --git a/t/t4123-apply-shrink.sh b/t/t4123-apply-shrink.sh
new file mode 100755 (executable)
index 0000000..984157f
--- /dev/null
@@ -0,0 +1,58 @@
+#!/bin/sh
+
+test_description='apply a patch that is larger than the preimage'
+
+. ./test-lib.sh
+
+cat >F  <<\EOF
+1
+2
+3
+4
+5
+6
+7
+8
+999999
+A
+B
+C
+D
+E
+F
+G
+H
+I
+J
+
+EOF
+
+test_expect_success setup '
+
+       git add F &&
+       mv F G &&
+       sed -e "s/1/11/" -e "s/999999/9/" -e "s/H/HH/" <G >F &&
+       git diff >patch &&
+       sed -e "/^\$/d" <G >F &&
+       git add F
+
+'
+
+test_expect_success 'apply should fail gracefully' '
+
+       if git apply --index patch
+       then
+               echo Oops, should not have succeeded
+               false
+       else
+               status=$?
+               echo "Status was $status"
+               if test -f .git/index.lock
+               then
+                       echo Oops, should not have crashed
+                       false
+               fi
+       fi
+'
+
+test_done
diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh
new file mode 100755 (executable)
index 0000000..cfcdb69
--- /dev/null
@@ -0,0 +1,201 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Johannes E. Schindelin
+#
+
+test_description='git rerere
+'
+
+. ./test-lib.sh
+
+cat > a1 << EOF
+Whether 'tis nobler in the mind to suffer
+The slings and arrows of outrageous fortune,
+Or to take arms against a sea of troubles,
+And by opposing end them? To die: to sleep;
+No more; and by a sleep to say we end
+The heart-ache and the thousand natural shocks
+That flesh is heir to, 'tis a consummation
+Devoutly to be wish'd.
+EOF
+
+git add a1
+git commit -q -a -m initial
+
+git checkout -b first
+cat >> a1 << EOF
+To die, to sleep;
+To sleep: perchance to dream: ay, there's the rub;
+For in that sleep of death what dreams may come
+When we have shuffled off this mortal coil,
+Must give us pause: there's the respect
+That makes calamity of so long life;
+EOF
+git commit -q -a -m first
+
+git checkout -b second master
+git show first:a1 |
+sed -e 's/To die, t/To die! T/' > a1
+echo "* END *" >>a1
+git commit -q -a -m second
+
+test_expect_success 'nothing recorded without rerere' '
+       (rm -rf .git/rr-cache; git config rerere.enabled false) &&
+       ! git merge first &&
+       ! test -d .git/rr-cache
+'
+
+# activate rerere, old style
+test_expect_success 'conflicting merge' '
+       git reset --hard &&
+       mkdir .git/rr-cache &&
+       git config --unset rerere.enabled &&
+       ! git merge first
+'
+
+sha1=$(sed -e 's/      .*//' .git/rr-cache/MERGE_RR)
+rr=.git/rr-cache/$sha1
+test_expect_success 'recorded preimage' "grep ======= $rr/preimage"
+
+test_expect_success 'rerere.enabled works, too' '
+       rm -rf .git/rr-cache &&
+       git config rerere.enabled true &&
+       git reset --hard &&
+       ! git merge first &&
+       grep ======= $rr/preimage
+'
+
+test_expect_success 'no postimage or thisimage yet' \
+       "test ! -f $rr/postimage -a ! -f $rr/thisimage"
+
+test_expect_success 'preimage has right number of lines' '
+
+       cnt=$(sed -ne "/^<<<<<<</,/^>>>>>>>/p" $rr/preimage | wc -l) &&
+       test $cnt = 9
+
+'
+
+git show first:a1 > a1
+
+cat > expect << EOF
+--- a/a1
++++ b/a1
+@@ -6,17 +6,9 @@
+ The heart-ache and the thousand natural shocks
+ That flesh is heir to, 'tis a consummation
+ Devoutly to be wish'd.
+-<<<<<<<
+-To die! To sleep;
+-=======
+ To die, to sleep;
+->>>>>>>
+ To sleep: perchance to dream: ay, there's the rub;
+ For in that sleep of death what dreams may come
+ When we have shuffled off this mortal coil,
+ Must give us pause: there's the respect
+ That makes calamity of so long life;
+-<<<<<<<
+-=======
+-* END *
+->>>>>>>
+EOF
+git rerere diff > out
+
+test_expect_success 'rerere diff' 'git diff expect out'
+
+cat > expect << EOF
+a1
+EOF
+
+git rerere status > out
+
+test_expect_success 'rerere status' 'git diff expect out'
+
+test_expect_success 'commit succeeds' \
+       "git commit -q -a -m 'prefer first over second'"
+
+test_expect_success 'recorded postimage' "test -f $rr/postimage"
+
+test_expect_success 'another conflicting merge' '
+       git checkout -b third master &&
+       git show second^:a1 | sed "s/To die: t/To die! T/" > a1 &&
+       git commit -q -a -m third &&
+       ! git pull . first
+'
+
+git show first:a1 | sed 's/To die: t/To die! T/' > expect
+test_expect_success 'rerere kicked in' "! grep ======= a1"
+
+test_expect_success 'rerere prefers first change' 'git diff a1 expect'
+
+rm $rr/postimage
+echo "$sha1    a1" | tr '\012' '\0' > .git/rr-cache/MERGE_RR
+
+test_expect_success 'rerere clear' 'git rerere clear'
+
+test_expect_success 'clear removed the directory' "test ! -d $rr"
+
+mkdir $rr
+echo Hello > $rr/preimage
+echo World > $rr/postimage
+
+sha2=4000000000000000000000000000000000000000
+rr2=.git/rr-cache/$sha2
+mkdir $rr2
+echo Hello > $rr2/preimage
+
+almost_15_days_ago=$((60-15*86400))
+just_over_15_days_ago=$((-1-15*86400))
+almost_60_days_ago=$((60-60*86400))
+just_over_60_days_ago=$((-1-60*86400))
+
+test-chmtime =$almost_60_days_ago $rr/preimage
+test-chmtime =$almost_15_days_ago $rr2/preimage
+
+test_expect_success 'garbage collection (part1)' 'git rerere gc'
+
+test_expect_success 'young records still live' \
+       "test -f $rr/preimage && test -f $rr2/preimage"
+
+test-chmtime =$just_over_60_days_ago $rr/preimage
+test-chmtime =$just_over_15_days_ago $rr2/preimage
+
+test_expect_success 'garbage collection (part2)' 'git rerere gc'
+
+test_expect_success 'old records rest in peace' \
+       "test ! -f $rr/preimage && test ! -f $rr2/preimage"
+
+test_expect_success 'file2 added differently in two branches' '
+       git reset --hard &&
+       git checkout -b fourth &&
+       echo Hallo > file2 &&
+       git add file2 &&
+       git commit -m version1 &&
+       git checkout third &&
+       echo Bello > file2 &&
+       git add file2 &&
+       git commit -m version2 &&
+       ! git merge fourth &&
+       sha1=$(sed -e "s/       .*//" .git/rr-cache/MERGE_RR) &&
+       rr=.git/rr-cache/$sha1 &&
+       echo Cello > file2 &&
+       git add file2 &&
+       git commit -m resolution
+'
+
+test_expect_success 'resolution was recorded properly' '
+       git reset --hard HEAD~2 &&
+       git checkout -b fifth &&
+       echo Hallo > file3 &&
+       git add file3 &&
+       git commit -m version1 &&
+       git checkout third &&
+       echo Bello > file3 &&
+       git add file3 &&
+       git commit -m version2 &&
+       ! git merge fifth &&
+       git diff-files -q &&
+       test Cello = "$(cat file3)"
+'
+
+test_done
diff --git a/t/t4201-shortlog.sh b/t/t4201-shortlog.sh
new file mode 100755 (executable)
index 0000000..10a5fa9
--- /dev/null
@@ -0,0 +1,50 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Johannes E. Schindelin
+#
+
+test_description='git shortlog
+'
+
+. ./test-lib.sh
+
+echo 1 > a1
+git add a1
+tree=$(git write-tree)
+commit=$( (echo "Test"; echo) | git commit-tree $tree )
+git update-ref HEAD $commit
+
+echo 2 > a1
+git commit --quiet -m "This is a very, very long first line for the commit message to see if it is wrapped correctly" a1
+
+# test if the wrapping is still valid when replacing all i's by treble clefs.
+echo 3 > a1
+git commit --quiet -m "$(echo "This is a very, very long first line for the commit message to see if it is wrapped correctly" | sed "s/i/1234/g" | tr 1234 '\360\235\204\236')" a1
+
+# now fsck up the utf8
+git repo-config i18n.commitencoding non-utf-8
+echo 4 > a1
+git commit --quiet -m "$(echo "This is a very, very long first line for the commit message to see if it is wrapped correctly" | sed "s/i/1234/g" | tr 1234 '\370\235\204\236')" a1
+
+echo 5 > a1
+git commit --quiet -m "a                                                               12      34      56      78" a1
+
+git shortlog -w HEAD > out
+
+cat > expect << EOF
+A U Thor (5):
+      Test
+      This is a very, very long first line for the commit message to see if
+         it is wrapped correctly
+      Th𝄞s 𝄞s a very, very long f𝄞rst l𝄞ne for the comm𝄞t message to see 𝄞f
+         𝄞t 𝄞s wrapped correctly
+      Thø\9d\84\9es ø\9d\84\9es a very, very long fø\9d\84\9erst lø\9d\84\9ene for the commø\9d\84\9et
+         message to see ø\9d\84\9ef ø\9d\84\9et ø\9d\84\9es wrapped correctly
+      a                                                                12      34
+         56    78
+
+EOF
+
+test_expect_success 'shortlog wrapping' 'diff -u expect out'
+
+test_done
diff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh
new file mode 100755 (executable)
index 0000000..dca2067
--- /dev/null
@@ -0,0 +1,174 @@
+#!/bin/sh
+#
+# Copyright (C) 2005 Rene Scharfe
+#
+
+test_description='git tar-tree and git get-tar-commit-id test
+
+This test covers the topics of file contents, commit date handling and
+commit id embedding:
+
+  The contents of the repository is compared to the extracted tar
+  archive.  The repository contains simple text files, symlinks and a
+  binary file (/bin/sh).  Only paths shorter than 99 characters are
+  used.
+
+  git tar-tree applies the commit date to every file in the archive it
+  creates.  The test sets the commit date to a specific value and checks
+  if the tar archive contains that value.
+
+  When giving git tar-tree a commit id (in contrast to a tree id) it
+  embeds this commit id into the tar archive as a comment.  The test
+  checks the ability of git get-tar-commit-id to figure it out from the
+  tar file.
+
+'
+
+. ./test-lib.sh
+TAR=${TAR:-tar}
+UNZIP=${UNZIP:-unzip}
+
+SUBSTFORMAT=%H%n
+
+test_expect_success \
+    'populate workdir' \
+    'mkdir a b c &&
+     echo simple textfile >a/a &&
+     mkdir a/bin &&
+     cp /bin/sh a/bin &&
+     printf "A\$Format:%s\$O" "$SUBSTFORMAT" >a/substfile1 &&
+     printf "A not substituted O" >a/substfile2 &&
+     ln -s a a/l1 &&
+     (p=long_path_to_a_file && cd a &&
+      for depth in 1 2 3 4 5; do mkdir $p && cd $p; done &&
+      echo text >file_with_long_path) &&
+     (cd a && find .) | sort >a.lst'
+
+test_expect_success \
+    'add files to repository' \
+    'find a -type f | xargs git update-index --add &&
+     find a -type l | xargs git update-index --add &&
+     treeid=`git write-tree` &&
+     echo $treeid >treeid &&
+     git update-ref HEAD $(TZ=GMT GIT_COMMITTER_DATE="2005-05-27 22:00:00" \
+     git commit-tree $treeid </dev/null)'
+
+test_expect_success \
+    'git archive' \
+    'git archive HEAD >b.tar'
+
+test_expect_success \
+    'git tar-tree' \
+    'git tar-tree HEAD >b2.tar'
+
+test_expect_success \
+    'git archive vs. git tar-tree' \
+    'diff b.tar b2.tar'
+
+test_expect_success \
+    'validate file modification time' \
+    'TZ=GMT $TAR tvf b.tar a/a |
+     awk \{print\ \$4,\ \(length\(\$5\)\<7\)\ ?\ \$5\":00\"\ :\ \$5\} \
+     >b.mtime &&
+     echo "2005-05-27 22:00:00" >expected.mtime &&
+     diff expected.mtime b.mtime'
+
+test_expect_success \
+    'git get-tar-commit-id' \
+    'git get-tar-commit-id <b.tar >b.commitid &&
+     diff .git/$(git symbolic-ref HEAD) b.commitid'
+
+test_expect_success \
+    'extract tar archive' \
+    '(cd b && $TAR xf -) <b.tar'
+
+test_expect_success \
+    'validate filenames' \
+    '(cd b/a && find .) | sort >b.lst &&
+     diff a.lst b.lst'
+
+test_expect_success \
+    'validate file contents' \
+    'diff -r a b/a'
+
+test_expect_success \
+    'git tar-tree with prefix' \
+    'git tar-tree HEAD prefix >c.tar'
+
+test_expect_success \
+    'extract tar archive with prefix' \
+    '(cd c && $TAR xf -) <c.tar'
+
+test_expect_success \
+    'validate filenames with prefix' \
+    '(cd c/prefix/a && find .) | sort >c.lst &&
+     diff a.lst c.lst'
+
+test_expect_success \
+    'validate file contents with prefix' \
+    'diff -r a c/prefix/a'
+
+test_expect_success \
+    'create an archive with a substfiles' \
+    'echo "substfile?" export-subst >a/.gitattributes &&
+     git archive HEAD >f.tar &&
+     rm a/.gitattributes'
+
+test_expect_success \
+    'extract substfiles' \
+    '(mkdir f && cd f && $TAR xf -) <f.tar'
+
+test_expect_success \
+     'validate substfile contents' \
+     'git log --max-count=1 "--pretty=format:A${SUBSTFORMAT}O" HEAD \
+      >f/a/substfile1.expected &&
+      diff f/a/substfile1.expected f/a/substfile1 &&
+      diff a/substfile2 f/a/substfile2
+'
+
+test_expect_success \
+    'git archive --format=zip' \
+    'git archive --format=zip HEAD >d.zip'
+
+$UNZIP -v >/dev/null 2>&1
+if [ $? -eq 127 ]; then
+       echo "Skipping ZIP tests, because unzip was not found"
+       test_done
+       exit
+fi
+
+test_expect_success \
+    'extract ZIP archive' \
+    '(mkdir d && cd d && $UNZIP ../d.zip)'
+
+test_expect_success \
+    'validate filenames' \
+    '(cd d/a && find .) | sort >d.lst &&
+     diff a.lst d.lst'
+
+test_expect_success \
+    'validate file contents' \
+    'diff -r a d/a'
+
+test_expect_success \
+    'git archive --format=zip with prefix' \
+    'git archive --format=zip --prefix=prefix/ HEAD >e.zip'
+
+test_expect_success \
+    'extract ZIP archive with prefix' \
+    '(mkdir e && cd e && $UNZIP ../e.zip)'
+
+test_expect_success \
+    'validate filenames with prefix' \
+    '(cd e/prefix/a && find .) | sort >e.lst &&
+     diff a.lst e.lst'
+
+test_expect_success \
+    'validate file contents with prefix' \
+    'diff -r a e/prefix/a'
+
+test_expect_success \
+    'git archive --list outside of a git repo' \
+    'GIT_DIR=some/non-existing/directory git archive --list'
+
+test_done
diff --git a/t/t5100-mailinfo.sh b/t/t5100-mailinfo.sh
new file mode 100755 (executable)
index 0000000..9b1a745
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git mailinfo and git mailsplit test'
+
+. ./test-lib.sh
+
+test_expect_success 'split sample box' \
+       'git mailsplit -o. ../t5100/sample.mbox >last &&
+       last=`cat last` &&
+       echo total is $last &&
+       test `cat last` = 8'
+
+for mail in `echo 00*`
+do
+       test_expect_success "mailinfo $mail" \
+               "git mailinfo -u msg$mail patch$mail <$mail >info$mail &&
+               echo msg &&
+               diff ../t5100/msg$mail msg$mail &&
+               echo patch &&
+               diff ../t5100/patch$mail patch$mail &&
+               echo info &&
+               diff ../t5100/info$mail info$mail"
+done
+
+test_done
diff --git a/t/t5100/info0001 b/t/t5100/info0001
new file mode 100644 (file)
index 0000000..8c05277
--- /dev/null
@@ -0,0 +1,5 @@
+Author: A U Thor
+Email: a.u.thor@example.com
+Subject: a commit.
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+
diff --git a/t/t5100/info0002 b/t/t5100/info0002
new file mode 100644 (file)
index 0000000..49bb0fe
--- /dev/null
@@ -0,0 +1,5 @@
+Author: A U Thor
+Email: a.u.thor@example.com
+Subject: another patch
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+
diff --git a/t/t5100/info0003 b/t/t5100/info0003
new file mode 100644 (file)
index 0000000..bd0d122
--- /dev/null
@@ -0,0 +1,5 @@
+Author: A U Thor
+Email: a.u.thor@example.com
+Subject: third patch
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+
diff --git a/t/t5100/info0004 b/t/t5100/info0004
new file mode 100644 (file)
index 0000000..616c309
--- /dev/null
@@ -0,0 +1,5 @@
+Author: YOSHIFUJI Hideaki / 吉藤英明
+Email: yoshfuji@linux-ipv6.org
+Subject: GIT: Try all addresses for given remote name
+Date: Thu, 21 Jul 2005 09:10:36 -0400 (EDT)
+
diff --git a/t/t5100/info0005 b/t/t5100/info0005
new file mode 100644 (file)
index 0000000..46a46fc
--- /dev/null
@@ -0,0 +1,5 @@
+Author: David Kågedal
+Email: davidk@lysator.liu.se
+Subject: Fixed two bugs in git-cvsimport-script.
+Date: Mon, 15 Aug 2005 20:18:25 +0200
+
diff --git a/t/t5100/info0006 b/t/t5100/info0006
new file mode 100644 (file)
index 0000000..8c05277
--- /dev/null
@@ -0,0 +1,5 @@
+Author: A U Thor
+Email: a.u.thor@example.com
+Subject: a commit.
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+
diff --git a/t/t5100/info0007 b/t/t5100/info0007
new file mode 100644 (file)
index 0000000..49bb0fe
--- /dev/null
@@ -0,0 +1,5 @@
+Author: A U Thor
+Email: a.u.thor@example.com
+Subject: another patch
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+
diff --git a/t/t5100/info0008 b/t/t5100/info0008
new file mode 100644 (file)
index 0000000..e8a2951
--- /dev/null
@@ -0,0 +1,5 @@
+Author: Junio C Hamano
+Email: junio@kernel.org
+Subject: another patch
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+
diff --git a/t/t5100/msg0001 b/t/t5100/msg0001
new file mode 100644 (file)
index 0000000..b275a9a
--- /dev/null
@@ -0,0 +1,2 @@
+Here is a patch from A U Thor.
+
diff --git a/t/t5100/msg0002 b/t/t5100/msg0002
new file mode 100644 (file)
index 0000000..e2546ec
--- /dev/null
@@ -0,0 +1,21 @@
+Here is a patch from A U Thor.  This addresses the issue raised in the
+message:
+
+From: Nit Picker <nit.picker@example.net>
+Subject: foo is too old
+Message-Id: <nitpicker.12121212@example.net>
+
+Hopefully this would fix the problem stated there.
+
+
+I have included an extra blank line above, but it does not have to be
+stripped away here, along with the                                
+whitespaces at the end of the above line.  They are expected to be squashed
+when the message is made into a commit log by stripspace,
+Also, there are three blank lines after this paragraph,
+two truly blank and another full of spaces in between.
+
+            
+
+Hope this helps.
+
diff --git a/t/t5100/msg0003 b/t/t5100/msg0003
new file mode 100644 (file)
index 0000000..1ac6810
--- /dev/null
@@ -0,0 +1,9 @@
+Here is a patch from A U Thor.  This addresses the issue raised in the
+message:
+
+From: Nit Picker <nit.picker@example.net>
+Subject: foo is too old
+Message-Id: <nitpicker.12121212@example.net>
+
+Hopefully this would fix the problem stated there.
+
diff --git a/t/t5100/msg0004 b/t/t5100/msg0004
new file mode 100644 (file)
index 0000000..6f8ba3b
--- /dev/null
@@ -0,0 +1,7 @@
+Hello.
+
+Try all addresses for given remote name until it succeeds.
+Also supports IPv6.
+
+Signed-of-by: Hideaki YOSHIFUJI <yoshfuji@linux-ipv6.org>
+
diff --git a/t/t5100/msg0005 b/t/t5100/msg0005
new file mode 100644 (file)
index 0000000..dd94cd7
--- /dev/null
@@ -0,0 +1,13 @@
+The git-cvsimport-script had a copule of small bugs that prevented me
+from importing a big CVS repository.
+
+The first was that it didn't handle removed files with a multi-digit
+primary revision number.
+
+The second was that it was asking the CVS server for "F" messages,
+although they were not handled.
+
+I also updated the documentation for that script to correspond to
+actual flags.
+
+Signed-off-by: David Kågedal <davidk@lysator.liu.se>
diff --git a/t/t5100/msg0006 b/t/t5100/msg0006
new file mode 100644 (file)
index 0000000..b275a9a
--- /dev/null
@@ -0,0 +1,2 @@
+Here is a patch from A U Thor.
+
diff --git a/t/t5100/msg0007 b/t/t5100/msg0007
new file mode 100644 (file)
index 0000000..71b23c0
--- /dev/null
@@ -0,0 +1,2 @@
+Here is an empty patch from A U Thor.
+
diff --git a/t/t5100/msg0008 b/t/t5100/msg0008
new file mode 100644 (file)
index 0000000..a80ecb9
--- /dev/null
@@ -0,0 +1,4 @@
+>Here is an empty patch from A U Thor.
+
+Hey you forgot the patch!
+
diff --git a/t/t5100/patch0001 b/t/t5100/patch0001
new file mode 100644 (file)
index 0000000..8ce1551
--- /dev/null
@@ -0,0 +1,14 @@
+---
+ foo |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun  9 00:44:04 PDT 2006
++Fri Jun  9 00:44:13 PDT 2006
+-- 
+1.4.0.g6f2b
+
diff --git a/t/t5100/patch0002 b/t/t5100/patch0002
new file mode 100644 (file)
index 0000000..8ce1551
--- /dev/null
@@ -0,0 +1,14 @@
+---
+ foo |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun  9 00:44:04 PDT 2006
++Fri Jun  9 00:44:13 PDT 2006
+-- 
+1.4.0.g6f2b
+
diff --git a/t/t5100/patch0003 b/t/t5100/patch0003
new file mode 100644 (file)
index 0000000..8ce1551
--- /dev/null
@@ -0,0 +1,14 @@
+---
+ foo |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun  9 00:44:04 PDT 2006
++Fri Jun  9 00:44:13 PDT 2006
+-- 
+1.4.0.g6f2b
+
diff --git a/t/t5100/patch0004 b/t/t5100/patch0004
new file mode 100644 (file)
index 0000000..196458e
--- /dev/null
@@ -0,0 +1,93 @@
+diff --git a/connect.c b/connect.c
+--- a/connect.c
++++ b/connect.c
+@@ -96,42 +96,57 @@ static enum protocol get_protocol(const 
+       die("I don't handle protocol '%s'", name);
+ }
+-static void lookup_host(const char *host, struct sockaddr *in)
+-{
+-      struct addrinfo *res;
+-      int ret;
+-
+-      ret = getaddrinfo(host, NULL, NULL, &res);
+-      if (ret)
+-              die("Unable to look up %s (%s)", host, gai_strerror(ret));
+-      *in = *res->ai_addr;
+-      freeaddrinfo(res);
+-}
++#define STR_(s)       # s
++#define STR(s)        STR_(s)
+ static int git_tcp_connect(int fd[2], const char *prog, char *host, char *path)
+ {
+-      struct sockaddr addr;
+-      int port = DEFAULT_GIT_PORT, sockfd;
+-      char *colon;
+-
+-      colon = strchr(host, ':');
+-      if (colon) {
+-              char *end;
+-              unsigned long n = strtoul(colon+1, &end, 0);
+-              if (colon[1] && !*end) {
+-                      *colon = 0;
+-                      port = n;
++      int sockfd = -1;
++      char *colon, *end;
++      char *port = STR(DEFAULT_GIT_PORT);
++      struct addrinfo hints, *ai0, *ai;
++      int gai;
++
++      if (host[0] == '[') {
++              end = strchr(host + 1, ']');
++              if (end) {
++                      *end = 0;
++                      end++;
++                      host++;
++              } else
++                      end = host;
++      } else
++              end = host;
++      colon = strchr(end, ':');
++
++      if (colon)
++              port = colon + 1;
++
++      memset(&hints, 0, sizeof(hints));
++      hints.ai_socktype = SOCK_STREAM;
++      hints.ai_protocol = IPPROTO_TCP;
++
++      gai = getaddrinfo(host, port, &hints, &ai);
++      if (gai)
++              die("Unable to look up %s (%s)", host, gai_strerror(gai));
++
++      for (ai0 = ai; ai; ai = ai->ai_next) {
++              sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
++              if (sockfd < 0)
++                      continue;
++              if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
++                      close(sockfd);
++                      sockfd = -1;
++                      continue;
+               }
++              break;
+       }
+-      lookup_host(host, &addr);
+-      ((struct sockaddr_in *)&addr)->sin_port = htons(port);
++      freeaddrinfo(ai0);
+-      sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_IP);
+       if (sockfd < 0)
+               die("unable to create socket (%s)", strerror(errno));
+-      if (connect(sockfd, (void *)&addr, sizeof(addr)) < 0)
+-              die("unable to connect (%s)", strerror(errno));
++
+       fd[0] = sockfd;
+       fd[1] = sockfd;
+       packet_write(sockfd, "%s %s\n", prog, path);
+
+-- 
+YOSHIFUJI Hideaki @ USAGI Project  <yoshfuji@linux-ipv6.org>
+GPG-FP  : 9022 65EB 1ECF 3AD1 0BDF  80D8 4807 F894 E062 0EEA
+
diff --git a/t/t5100/patch0005 b/t/t5100/patch0005
new file mode 100644 (file)
index 0000000..7d24b24
--- /dev/null
@@ -0,0 +1,69 @@
+---
+
+ Documentation/git-cvsimport-script.txt |    9 ++++++++-
+ git-cvsimport-script                   |    4 ++--
+ 2 files changed, 10 insertions(+), 3 deletions(-)
+
+50452f9c0c2df1f04d83a26266ba704b13861632
+diff --git a/Documentation/git-cvsimport-script.txt b/Documentation/git-cvsimport-script.txt
+--- a/Documentation/git-cvsimport-script.txt
++++ b/Documentation/git-cvsimport-script.txt
+@@ -29,6 +29,10 @@ OPTIONS
+       currently, only the :local:, :ext: and :pserver: access methods 
+       are supported.
++-C <target-dir>::
++        The GIT repository to import to.  If the directory doesn't
++        exist, it will be created.  Default is the current directory.
++
+ -i::
+       Import-only: don't perform a checkout after importing.  This option
+       ensures the working directory and cache remain untouched and will
+@@ -44,7 +48,7 @@ OPTIONS
+ -p <options-for-cvsps>::
+       Additional options for cvsps.
+-      The options '-x' and '-A' are implicit and should not be used here.
++      The options '-u' and '-A' are implicit and should not be used here.
+       If you need to pass multiple options, separate them with a comma.
+@@ -57,6 +61,9 @@ OPTIONS
+ -h::
+       Print a short usage message and exit.
++-z <fuzz>::
++        Pass the timestamp fuzz factor to cvsps.
++
+ OUTPUT
+ ------
+ If '-v' is specified, the script reports what it is doing.
+diff --git a/git-cvsimport-script b/git-cvsimport-script
+--- a/git-cvsimport-script
++++ b/git-cvsimport-script
+@@ -190,7 +190,7 @@ sub conn {
+       $self->{'socketo'}->write("Root $repo\n");
+       # Trial and error says that this probably is the minimum set
+-      $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mode M Mbinary E F Checked-in Created Updated Merged Removed\n");
++      $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mode M Mbinary E Checked-in Created Updated Merged Removed\n");
+       $self->{'socketo'}->write("valid-requests\n");
+       $self->{'socketo'}->flush();
+@@ -691,7 +691,7 @@ while(<CVS>) {
+               unlink($tmpname);
+               my $mode = pmode($cvs->{'mode'});
+               push(@new,[$mode, $sha, $fn]); # may be resurrected!
+-      } elsif($state == 9 and /^\s+(\S+):\d(?:\.\d+)+->(\d(?:\.\d+)+)\(DEAD\)\s*$/) {
++      } elsif($state == 9 and /^\s+(\S+):\d+(?:\.\d+)+->(\d+(?:\.\d+)+)\(DEAD\)\s*$/) {
+               my $fn = $1;
+               $fn =~ s#^/+##;
+               push(@old,$fn);
+
+-- 
+David Kågedal
+-
+To unsubscribe from this list: send the line "unsubscribe git" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
diff --git a/t/t5100/patch0006 b/t/t5100/patch0006
new file mode 100644 (file)
index 0000000..8ce1551
--- /dev/null
@@ -0,0 +1,14 @@
+---
+ foo |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun  9 00:44:04 PDT 2006
++Fri Jun  9 00:44:13 PDT 2006
+-- 
+1.4.0.g6f2b
+
diff --git a/t/t5100/patch0007 b/t/t5100/patch0007
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/t/t5100/patch0008 b/t/t5100/patch0008
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/t/t5100/sample.mbox b/t/t5100/sample.mbox
new file mode 100644 (file)
index 0000000..070c166
--- /dev/null
@@ -0,0 +1,409 @@
+    
+       
+    
+From nobody Mon Sep 17 00:00:00 2001
+From: A U Thor <a.u.thor@example.com>
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+Subject: [PATCH] a commit.
+
+Here is a patch from A U Thor.
+
+---
+ foo |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun  9 00:44:04 PDT 2006
++Fri Jun  9 00:44:13 PDT 2006
+-- 
+1.4.0.g6f2b
+
+From nobody Mon Sep 17 00:00:00 2001
+From: A U Thor <a.u.thor@example.com>
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+Subject: [PATCH] another patch
+
+Here is a patch from A U Thor.  This addresses the issue raised in the
+message:
+
+From: Nit Picker <nit.picker@example.net>
+Subject: foo is too old
+Message-Id: <nitpicker.12121212@example.net>
+
+Hopefully this would fix the problem stated there.
+
+
+I have included an extra blank line above, but it does not have to be
+stripped away here, along with the                                
+whitespaces at the end of the above line.  They are expected to be squashed
+when the message is made into a commit log by stripspace,
+Also, there are three blank lines after this paragraph,
+two truly blank and another full of spaces in between.
+
+            
+
+Hope this helps.
+
+---
+ foo |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun  9 00:44:04 PDT 2006
++Fri Jun  9 00:44:13 PDT 2006
+-- 
+1.4.0.g6f2b
+
+From nobody Mon Sep 17 00:00:00 2001
+From: Junio C Hamano <junio@kernel.org>
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+Subject: re: [PATCH] another patch
+
+From: A U Thor <a.u.thor@example.com>
+Subject: [PATCH] third patch
+
+Here is a patch from A U Thor.  This addresses the issue raised in the
+message:
+
+From: Nit Picker <nit.picker@example.net>
+Subject: foo is too old
+Message-Id: <nitpicker.12121212@example.net>
+
+Hopefully this would fix the problem stated there.
+
+---
+ foo |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun  9 00:44:04 PDT 2006
++Fri Jun  9 00:44:13 PDT 2006
+-- 
+1.4.0.g6f2b
+
+From nobody Sat Aug 27 23:07:49 2005
+Path: news.gmane.org!not-for-mail
+Message-ID: <20050721.091036.01119516.yoshfuji@linux-ipv6.org>
+From: YOSHIFUJI Hideaki / =?iso-2022-jp?B?GyRCNUhGIzFRTEAbKEI=?= 
+       <yoshfuji@linux-ipv6.org>
+Newsgroups: gmane.comp.version-control.git
+Subject: [PATCH 1/2] GIT: Try all addresses for given remote name
+Date: Thu, 21 Jul 2005 09:10:36 -0400 (EDT)
+Lines: 99
+Organization: USAGI/WIDE Project
+Approved: news@gmane.org
+NNTP-Posting-Host: main.gmane.org
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+X-Trace: sea.gmane.org 1121951434 29350 80.91.229.2 (21 Jul 2005 13:10:34 GMT)
+X-Complaints-To: usenet@sea.gmane.org
+NNTP-Posting-Date: Thu, 21 Jul 2005 13:10:34 +0000 (UTC)
+
+Hello.
+
+Try all addresses for given remote name until it succeeds.
+Also supports IPv6.
+
+Signed-of-by: Hideaki YOSHIFUJI <yoshfuji@linux-ipv6.org>
+
+diff --git a/connect.c b/connect.c
+--- a/connect.c
++++ b/connect.c
+@@ -96,42 +96,57 @@ static enum protocol get_protocol(const 
+       die("I don't handle protocol '%s'", name);
+ }
+-static void lookup_host(const char *host, struct sockaddr *in)
+-{
+-      struct addrinfo *res;
+-      int ret;
+-
+-      ret = getaddrinfo(host, NULL, NULL, &res);
+-      if (ret)
+-              die("Unable to look up %s (%s)", host, gai_strerror(ret));
+-      *in = *res->ai_addr;
+-      freeaddrinfo(res);
+-}
++#define STR_(s)       # s
++#define STR(s)        STR_(s)
+ static int git_tcp_connect(int fd[2], const char *prog, char *host, char *path)
+ {
+-      struct sockaddr addr;
+-      int port = DEFAULT_GIT_PORT, sockfd;
+-      char *colon;
+-
+-      colon = strchr(host, ':');
+-      if (colon) {
+-              char *end;
+-              unsigned long n = strtoul(colon+1, &end, 0);
+-              if (colon[1] && !*end) {
+-                      *colon = 0;
+-                      port = n;
++      int sockfd = -1;
++      char *colon, *end;
++      char *port = STR(DEFAULT_GIT_PORT);
++      struct addrinfo hints, *ai0, *ai;
++      int gai;
++
++      if (host[0] == '[') {
++              end = strchr(host + 1, ']');
++              if (end) {
++                      *end = 0;
++                      end++;
++                      host++;
++              } else
++                      end = host;
++      } else
++              end = host;
++      colon = strchr(end, ':');
++
++      if (colon)
++              port = colon + 1;
++
++      memset(&hints, 0, sizeof(hints));
++      hints.ai_socktype = SOCK_STREAM;
++      hints.ai_protocol = IPPROTO_TCP;
++
++      gai = getaddrinfo(host, port, &hints, &ai);
++      if (gai)
++              die("Unable to look up %s (%s)", host, gai_strerror(gai));
++
++      for (ai0 = ai; ai; ai = ai->ai_next) {
++              sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
++              if (sockfd < 0)
++                      continue;
++              if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
++                      close(sockfd);
++                      sockfd = -1;
++                      continue;
+               }
++              break;
+       }
+-      lookup_host(host, &addr);
+-      ((struct sockaddr_in *)&addr)->sin_port = htons(port);
++      freeaddrinfo(ai0);
+-      sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_IP);
+       if (sockfd < 0)
+               die("unable to create socket (%s)", strerror(errno));
+-      if (connect(sockfd, (void *)&addr, sizeof(addr)) < 0)
+-              die("unable to connect (%s)", strerror(errno));
++
+       fd[0] = sockfd;
+       fd[1] = sockfd;
+       packet_write(sockfd, "%s %s\n", prog, path);
+
+-- 
+YOSHIFUJI Hideaki @ USAGI Project  <yoshfuji@linux-ipv6.org>
+GPG-FP  : 9022 65EB 1ECF 3AD1 0BDF  80D8 4807 F894 E062 0EEA
+
+From nobody Sat Aug 27 23:07:49 2005
+Path: news.gmane.org!not-for-mail
+Message-ID: <u5tacjjdpxq.fsf@lysator.liu.se>
+From: =?iso-8859-1?Q?David_K=E5gedal?= <davidk@lysator.liu.se>
+Newsgroups: gmane.comp.version-control.git
+Subject: [PATCH] Fixed two bugs in git-cvsimport-script.
+Date: Mon, 15 Aug 2005 20:18:25 +0200
+Lines: 83
+Approved: news@gmane.org
+NNTP-Posting-Host: main.gmane.org
+Mime-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+X-Trace: sea.gmane.org 1124130247 31839 80.91.229.2 (15 Aug 2005 18:24:07 GMT)
+X-Complaints-To: usenet@sea.gmane.org
+NNTP-Posting-Date: Mon, 15 Aug 2005 18:24:07 +0000 (UTC)
+Cc: "Junio C. Hamano" <junkio@cox.net>
+Original-X-From: git-owner@vger.kernel.org Mon Aug 15 20:24:05 2005
+
+The git-cvsimport-script had a copule of small bugs that prevented me
+from importing a big CVS repository.
+
+The first was that it didn't handle removed files with a multi-digit
+primary revision number.
+
+The second was that it was asking the CVS server for "F" messages,
+although they were not handled.
+
+I also updated the documentation for that script to correspond to
+actual flags.
+
+Signed-off-by: David K=E5gedal <davidk@lysator.liu.se>
+---
+
+ Documentation/git-cvsimport-script.txt |    9 ++++++++-
+ git-cvsimport-script                   |    4 ++--
+ 2 files changed, 10 insertions(+), 3 deletions(-)
+
+50452f9c0c2df1f04d83a26266ba704b13861632
+diff --git a/Documentation/git-cvsimport-script.txt b/Documentation/git=
+-cvsimport-script.txt
+--- a/Documentation/git-cvsimport-script.txt
++++ b/Documentation/git-cvsimport-script.txt
+@@ -29,6 +29,10 @@ OPTIONS
+       currently, only the :local:, :ext: and :pserver: access methods=20
+       are supported.
+=20
++-C <target-dir>::
++        The GIT repository to import to.  If the directory doesn't
++        exist, it will be created.  Default is the current directory.
++
+ -i::
+       Import-only: don't perform a checkout after importing.  This option
+       ensures the working directory and cache remain untouched and will
+@@ -44,7 +48,7 @@ OPTIONS
+=20
+ -p <options-for-cvsps>::
+       Additional options for cvsps.
+-      The options '-x' and '-A' are implicit and should not be used here.
++      The options '-u' and '-A' are implicit and should not be used here.
+=20
+       If you need to pass multiple options, separate them with a comma.
+=20
+@@ -57,6 +61,9 @@ OPTIONS
+ -h::
+       Print a short usage message and exit.
+=20
++-z <fuzz>::
++        Pass the timestamp fuzz factor to cvsps.
++
+ OUTPUT
+ ------
+ If '-v' is specified, the script reports what it is doing.
+diff --git a/git-cvsimport-script b/git-cvsimport-script
+--- a/git-cvsimport-script
++++ b/git-cvsimport-script
+@@ -190,7 +190,7 @@ sub conn {
+       $self->{'socketo'}->write("Root $repo\n");
+=20
+       # Trial and error says that this probably is the minimum set
+-      $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mo=
+de M Mbinary E F Checked-in Created Updated Merged Removed\n");
++      $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mo=
+de M Mbinary E Checked-in Created Updated Merged Removed\n");
+=20
+       $self->{'socketo'}->write("valid-requests\n");
+       $self->{'socketo'}->flush();
+@@ -691,7 +691,7 @@ while(<CVS>) {
+               unlink($tmpname);
+               my $mode =3D pmode($cvs->{'mode'});
+               push(@new,[$mode, $sha, $fn]); # may be resurrected!
+-      } elsif($state =3D=3D 9 and /^\s+(\S+):\d(?:\.\d+)+->(\d(?:\.\d+)+)\(=
+DEAD\)\s*$/) {
++      } elsif($state =3D=3D 9 and /^\s+(\S+):\d+(?:\.\d+)+->(\d+(?:\.\d+)+)=
+\(DEAD\)\s*$/) {
+               my $fn =3D $1;
+               $fn =3D~ s#^/+##;
+               push(@old,$fn);
+
+--=20
+David K=E5gedal
+-
+To unsubscribe from this list: send the line "unsubscribe git" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+From nobody Mon Sep 17 00:00:00 2001
+From: A U Thor <a.u.thor@example.com>
+References: <Pine.LNX.4.640.0001@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0002@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0003@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0004@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0005@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0006@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0007@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0008@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0009@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0010@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0011@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0012@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0013@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0014@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0015@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0016@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0017@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0018@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0019@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0020@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0021@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0022@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0023@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0024@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0025@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0026@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0027@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0028@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0029@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0030@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0031@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0032@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0033@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0034@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0035@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0036@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0037@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0038@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0039@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0040@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0041@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0042@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0043@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0044@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0045@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0046@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0047@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0048@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0049@woody.linux-foundation.org>
+ <Pine.LNX.4.640.0050@woody.linux-foundation.org>
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+Subject: [PATCH] a commit.
+
+Here is a patch from A U Thor.
+
+---
+ foo |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun  9 00:44:04 PDT 2006
++Fri Jun  9 00:44:13 PDT 2006
+-- 
+1.4.0.g6f2b
+
+From nobody Mon Sep 17 00:00:00 2001
+From: A U Thor <a.u.thor@example.com>
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+Subject: [PATCH] another patch
+
+Here is an empty patch from A U Thor.
+
+From nobody Mon Sep 17 00:00:00 2001
+From: Junio C Hamano <junio@kernel.org>
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+Subject: re: [PATCH] another patch
+
+From: A U Thor <a.u.thor@example.com>
+Subject: [PATCH] another patch
+>Here is an empty patch from A U Thor.
+
+Hey you forgot the patch!
+
diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh
new file mode 100755 (executable)
index 0000000..f1106e6
--- /dev/null
@@ -0,0 +1,271 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git-pack-object
+
+'
+. ./test-lib.sh
+
+TRASH=`pwd`
+
+test_expect_success \
+    'setup' \
+    'rm -f .git/index*
+     for i in a b c
+     do
+            dd if=/dev/zero bs=4k count=1 | tr "\\0" $i >$i &&
+            git update-index --add $i || return 1
+     done &&
+     cat c >d && echo foo >>d && git update-index --add d &&
+     tree=`git write-tree` &&
+     commit=`git commit-tree $tree </dev/null` && {
+        echo $tree &&
+        echo $commit &&
+        git ls-tree $tree | sed -e "s/.* \\([0-9a-f]*\\)       .*/\\1/"
+     } >obj-list && {
+        git diff-tree --root -p $commit &&
+        while read object
+        do
+           t=`git cat-file -t $object` &&
+           git cat-file $t $object || return 1
+        done <obj-list
+     } >expect'
+
+test_expect_success \
+    'pack without delta' \
+    'packname_1=$(git pack-objects --window=0 test-1 <obj-list)'
+
+rm -fr .git2
+mkdir .git2
+
+test_expect_success \
+    'unpack without delta' \
+    "GIT_OBJECT_DIRECTORY=.git2/objects &&
+     export GIT_OBJECT_DIRECTORY &&
+     git init &&
+     git unpack-objects -n <test-1-${packname_1}.pack &&
+     git unpack-objects <test-1-${packname_1}.pack"
+
+unset GIT_OBJECT_DIRECTORY
+cd "$TRASH/.git2"
+
+test_expect_success \
+    'check unpack without delta' \
+    '(cd ../.git && find objects -type f -print) |
+     while read path
+     do
+         cmp $path ../.git/$path || {
+            echo $path differs.
+            return 1
+        }
+     done'
+cd "$TRASH"
+
+test_expect_success \
+    'pack with REF_DELTA' \
+    'pwd &&
+     packname_2=$(git pack-objects test-2 <obj-list)'
+
+rm -fr .git2
+mkdir .git2
+
+test_expect_success \
+    'unpack with REF_DELTA' \
+    'GIT_OBJECT_DIRECTORY=.git2/objects &&
+     export GIT_OBJECT_DIRECTORY &&
+     git init &&
+     git unpack-objects -n <test-2-${packname_2}.pack &&
+     git unpack-objects <test-2-${packname_2}.pack'
+
+unset GIT_OBJECT_DIRECTORY
+cd "$TRASH/.git2"
+test_expect_success \
+    'check unpack with REF_DELTA' \
+    '(cd ../.git && find objects -type f -print) |
+     while read path
+     do
+         cmp $path ../.git/$path || {
+            echo $path differs.
+            return 1
+        }
+     done'
+cd "$TRASH"
+
+test_expect_success \
+    'pack with OFS_DELTA' \
+    'pwd &&
+     packname_3=$(git pack-objects --delta-base-offset test-3 <obj-list)'
+
+rm -fr .git2
+mkdir .git2
+
+test_expect_success \
+    'unpack with OFS_DELTA' \
+    'GIT_OBJECT_DIRECTORY=.git2/objects &&
+     export GIT_OBJECT_DIRECTORY &&
+     git init &&
+     git unpack-objects -n <test-3-${packname_3}.pack &&
+     git unpack-objects <test-3-${packname_3}.pack'
+
+unset GIT_OBJECT_DIRECTORY
+cd "$TRASH/.git2"
+test_expect_success \
+    'check unpack with OFS_DELTA' \
+    '(cd ../.git && find objects -type f -print) |
+     while read path
+     do
+         cmp $path ../.git/$path || {
+            echo $path differs.
+            return 1
+        }
+     done'
+cd "$TRASH"
+
+test_expect_success 'compare delta flavors' '
+       perl -e '\''
+               defined($_ = -s $_) or die for @ARGV;
+               exit 1 if $ARGV[0] <= $ARGV[1];
+       '\'' test-2-$packname_2.pack test-3-$packname_3.pack
+'
+
+rm -fr .git2
+mkdir .git2
+
+test_expect_success \
+    'use packed objects' \
+    'GIT_OBJECT_DIRECTORY=.git2/objects &&
+     export GIT_OBJECT_DIRECTORY &&
+     git init &&
+     cp test-1-${packname_1}.pack test-1-${packname_1}.idx .git2/objects/pack && {
+        git diff-tree --root -p $commit &&
+        while read object
+        do
+           t=`git cat-file -t $object` &&
+           git cat-file $t $object || return 1
+        done <obj-list
+    } >current &&
+    diff expect current'
+
+test_expect_success \
+    'use packed deltified (REF_DELTA) objects' \
+    'GIT_OBJECT_DIRECTORY=.git2/objects &&
+     export GIT_OBJECT_DIRECTORY &&
+     rm -f .git2/objects/pack/test-* &&
+     cp test-2-${packname_2}.pack test-2-${packname_2}.idx .git2/objects/pack && {
+        git diff-tree --root -p $commit &&
+        while read object
+        do
+           t=`git cat-file -t $object` &&
+           git cat-file $t $object || return 1
+        done <obj-list
+    } >current &&
+    diff expect current'
+
+test_expect_success \
+    'use packed deltified (OFS_DELTA) objects' \
+    'GIT_OBJECT_DIRECTORY=.git2/objects &&
+     export GIT_OBJECT_DIRECTORY &&
+     rm -f .git2/objects/pack/test-* &&
+     cp test-3-${packname_3}.pack test-3-${packname_3}.idx .git2/objects/pack && {
+        git diff-tree --root -p $commit &&
+        while read object
+        do
+           t=`git cat-file -t $object` &&
+           git cat-file $t $object || return 1
+        done <obj-list
+    } >current &&
+    diff expect current'
+
+unset GIT_OBJECT_DIRECTORY
+
+test_expect_success \
+    'verify pack' \
+    'git verify-pack   test-1-${packname_1}.idx \
+                       test-2-${packname_2}.idx \
+                       test-3-${packname_3}.idx'
+
+test_expect_success \
+    'verify-pack catches mismatched .idx and .pack files' \
+    'cat test-1-${packname_1}.idx >test-3.idx &&
+     cat test-2-${packname_2}.pack >test-3.pack &&
+     if git verify-pack test-3.idx
+     then false
+     else :;
+     fi'
+
+test_expect_success \
+    'verify-pack catches a corrupted pack signature' \
+    'cat test-1-${packname_1}.pack >test-3.pack &&
+     dd if=/dev/zero of=test-3.pack count=1 bs=1 conv=notrunc seek=2 &&
+     if git verify-pack test-3.idx
+     then false
+     else :;
+     fi'
+
+test_expect_success \
+    'verify-pack catches a corrupted pack version' \
+    'cat test-1-${packname_1}.pack >test-3.pack &&
+     dd if=/dev/zero of=test-3.pack count=1 bs=1 conv=notrunc seek=7 &&
+     if git verify-pack test-3.idx
+     then false
+     else :;
+     fi'
+
+test_expect_success \
+    'verify-pack catches a corrupted type/size of the 1st packed object data' \
+    'cat test-1-${packname_1}.pack >test-3.pack &&
+     dd if=/dev/zero of=test-3.pack count=1 bs=1 conv=notrunc seek=12 &&
+     if git verify-pack test-3.idx
+     then false
+     else :;
+     fi'
+
+test_expect_success \
+    'verify-pack catches a corrupted sum of the index file itself' \
+    'l=`wc -c <test-3.idx` &&
+     l=`expr $l - 20` &&
+     cat test-1-${packname_1}.pack >test-3.pack &&
+     dd if=/dev/zero of=test-3.idx count=20 bs=1 conv=notrunc seek=$l &&
+     if git verify-pack test-3.pack
+     then false
+     else :;
+     fi'
+
+test_expect_success \
+    'build pack index for an existing pack' \
+    'cat test-1-${packname_1}.pack >test-3.pack &&
+     git-index-pack -o tmp.idx test-3.pack &&
+     cmp tmp.idx test-1-${packname_1}.idx &&
+
+     git-index-pack test-3.pack &&
+     cmp test-3.idx test-1-${packname_1}.idx &&
+
+     cat test-2-${packname_2}.pack >test-3.pack &&
+     git-index-pack -o tmp.idx test-2-${packname_2}.pack &&
+     cmp tmp.idx test-2-${packname_2}.idx &&
+
+     git-index-pack test-3.pack &&
+     cmp test-3.idx test-2-${packname_2}.idx &&
+
+     cat test-3-${packname_3}.pack >test-3.pack &&
+     git-index-pack -o tmp.idx test-3-${packname_3}.pack &&
+     cmp tmp.idx test-3-${packname_3}.idx &&
+
+     git-index-pack test-3.pack &&
+     cmp test-3.idx test-3-${packname_3}.idx &&
+
+     :'
+
+test_expect_success \
+    'fake a SHA1 hash collision' \
+    'test -f   .git/objects/c8/2de19312b6c3695c0c18f70709a6c535682a67 &&
+     cp -f     .git/objects/9d/235ed07cd19811a6ceb342de82f190e49c9f68 \
+               .git/objects/c8/2de19312b6c3695c0c18f70709a6c535682a67'
+
+test_expect_failure \
+    'make sure index-pack detects the SHA1 collision' \
+    'git-index-pack -o bad.idx test-3.pack'
+
+test_done
diff --git a/t/t5301-sliding-window.sh b/t/t5301-sliding-window.sh
new file mode 100755 (executable)
index 0000000..073ac0c
--- /dev/null
@@ -0,0 +1,60 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Shawn Pearce
+#
+
+test_description='mmap sliding window tests'
+. ./test-lib.sh
+
+test_expect_success \
+    'setup' \
+    'rm -f .git/index*
+     for i in a b c
+     do
+         echo $i >$i &&
+         test-genrandom "$i" 32768 >>$i &&
+         git update-index --add $i || return 1
+     done &&
+     echo d >d && cat c >>d && git update-index --add d &&
+     tree=`git write-tree` &&
+     commit1=`git commit-tree $tree </dev/null` &&
+     git update-ref HEAD $commit1 &&
+     git-repack -a -d &&
+     test "`git count-objects`" = "0 objects, 0 kilobytes" &&
+     pack1=`ls .git/objects/pack/*.pack` &&
+     test -f "$pack1"'
+
+test_expect_success \
+    'verify-pack -v, defaults' \
+    'git verify-pack -v "$pack1"'
+
+test_expect_success \
+    'verify-pack -v, packedGitWindowSize == 1 page' \
+    'git config core.packedGitWindowSize 512 &&
+     git verify-pack -v "$pack1"'
+
+test_expect_success \
+    'verify-pack -v, packedGit{WindowSize,Limit} == 1 page' \
+    'git config core.packedGitWindowSize 512 &&
+     git config core.packedGitLimit 512 &&
+     git verify-pack -v "$pack1"'
+
+test_expect_success \
+    'repack -a -d, packedGit{WindowSize,Limit} == 1 page' \
+    'git config core.packedGitWindowSize 512 &&
+     git config core.packedGitLimit 512 &&
+     commit2=`git commit-tree $tree -p $commit1 </dev/null` &&
+     git update-ref HEAD $commit2 &&
+     git-repack -a -d &&
+     test "`git count-objects`" = "0 objects, 0 kilobytes" &&
+     pack2=`ls .git/objects/pack/*.pack` &&
+     test -f "$pack2"
+     test "$pack1" \!= "$pack2"'
+
+test_expect_success \
+    'verify-pack -v, defaults' \
+    'git config --unset core.packedGitWindowSize &&
+     git config --unset core.packedGitLimit &&
+     git verify-pack -v "$pack2"'
+
+test_done
diff --git a/t/t5302-pack-index.sh b/t/t5302-pack-index.sh
new file mode 100755 (executable)
index 0000000..2a2878b
--- /dev/null
@@ -0,0 +1,165 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Nicolas Pitre
+#
+
+test_description='pack index with 64-bit offsets and object CRC'
+. ./test-lib.sh
+
+test_expect_success \
+    'setup' \
+    'rm -rf .git
+     git init &&
+     i=1 &&
+        while test $i -le 100
+     do
+                i=`printf '%03i' $i`
+         echo $i >file_$i &&
+         test-genrandom "$i" 8192 >>file_$i &&
+         git update-index --add file_$i &&
+                i=`expr $i + 1` || return 1
+     done &&
+     { echo 101 && test-genrandom 100 8192; } >file_101 &&
+     git update-index --add file_101 &&
+     tree=`git write-tree` &&
+     commit=`git commit-tree $tree </dev/null` && {
+        echo $tree &&
+        git ls-tree $tree | sed -e "s/.* \\([0-9a-f]*\\)       .*/\\1/"
+     } >obj-list &&
+     git update-ref HEAD $commit'
+
+test_expect_success \
+    'pack-objects with index version 1' \
+    'pack1=$(git pack-objects --index-version=1 test-1 <obj-list) &&
+     git verify-pack -v "test-1-${pack1}.pack"'
+
+test_expect_success \
+    'pack-objects with index version 2' \
+    'pack2=$(git pack-objects --index-version=2 test-2 <obj-list) &&
+     git verify-pack -v "test-2-${pack2}.pack"'
+
+test_expect_success \
+    'both packs should be identical' \
+    'cmp "test-1-${pack1}.pack" "test-2-${pack2}.pack"'
+
+test_expect_failure \
+    'index v1 and index v2 should be different' \
+    'cmp "test-1-${pack1}.idx" "test-2-${pack2}.idx"'
+
+test_expect_success \
+    'index-pack with index version 1' \
+    'git-index-pack --index-version=1 -o 1.idx "test-1-${pack1}.pack"'
+
+test_expect_success \
+    'index-pack with index version 2' \
+    'git-index-pack --index-version=2 -o 2.idx "test-1-${pack1}.pack"'
+
+test_expect_success \
+    'index-pack results should match pack-objects ones' \
+    'cmp "test-1-${pack1}.idx" "1.idx" &&
+     cmp "test-2-${pack2}.idx" "2.idx"'
+
+test_expect_success \
+    'index v2: force some 64-bit offsets with pack-objects' \
+    'pack3=$(git pack-objects --index-version=2,0x40000 test-3 <obj-list)'
+
+have_64bits=
+if msg=$(git verify-pack -v "test-3-${pack3}.pack" 2>&1) ||
+       ! echo "$msg" | grep "pack too large .* off_t"
+then
+       have_64bits=t
+else
+       say "skipping tests concerning 64-bit offsets"
+fi
+
+test "$have_64bits" &&
+test_expect_success \
+    'index v2: verify a pack with some 64-bit offsets' \
+    'git verify-pack -v "test-3-${pack3}.pack"'
+
+test "$have_64bits" &&
+test_expect_failure \
+    '64-bit offsets: should be different from previous index v2 results' \
+    'cmp "test-2-${pack2}.idx" "test-3-${pack3}.idx"'
+
+test "$have_64bits" &&
+test_expect_success \
+    'index v2: force some 64-bit offsets with index-pack' \
+    'git-index-pack --index-version=2,0x40000 -o 3.idx "test-1-${pack1}.pack"'
+
+test "$have_64bits" &&
+test_expect_success \
+    '64-bit offsets: index-pack result should match pack-objects one' \
+    'cmp "test-3-${pack3}.idx" "3.idx"'
+
+test_expect_success \
+    '[index v1] 1) stream pack to repository' \
+    'git-index-pack --index-version=1 --stdin < "test-1-${pack1}.pack" &&
+     git prune-packed &&
+     git count-objects | ( read nr rest && test "$nr" -eq 1 ) &&
+     cmp "test-1-${pack1}.pack" ".git/objects/pack/pack-${pack1}.pack" &&
+     cmp "test-1-${pack1}.idx"  ".git/objects/pack/pack-${pack1}.idx"'
+
+test_expect_success \
+    '[index v1] 2) create a stealth corruption in a delta base reference' \
+    '# this test assumes a delta smaller than 16 bytes at the end of the pack
+     git show-index <1.idx | sort -n | tail -n 1 | (
+       read delta_offs delta_sha1 &&
+       git cat-file blob "$delta_sha1" > blob_1 &&
+       chmod +w ".git/objects/pack/pack-${pack1}.pack" &&
+       dd of=".git/objects/pack/pack-${pack1}.pack" seek=$(($delta_offs + 1)) \
+         if=".git/objects/pack/pack-${pack1}.idx" skip=$((256 * 4 + 4)) \
+         bs=1 count=20 conv=notrunc &&
+       git cat-file blob "$delta_sha1" > blob_2 )'
+
+test_expect_failure \
+    '[index v1] 3) corrupted delta happily returned wrong data' \
+    'cmp blob_1 blob_2'
+
+test_expect_failure \
+    '[index v1] 4) confirm that the pack is actually corrupted' \
+    'git fsck --full $commit'
+
+test_expect_success \
+    '[index v1] 5) pack-objects happily reuses corrupted data' \
+    'pack4=$(git pack-objects test-4 <obj-list) &&
+     test -f "test-4-${pack1}.pack"'
+
+test_expect_failure \
+    '[index v1] 6) newly created pack is BAD !' \
+    'git verify-pack -v "test-4-${pack1}.pack"'
+
+test_expect_success \
+    '[index v2] 1) stream pack to repository' \
+    'rm -f .git/objects/pack/* &&
+     git-index-pack --index-version=2 --stdin < "test-1-${pack1}.pack" &&
+     git prune-packed &&
+     git count-objects | ( read nr rest && test "$nr" -eq 1 ) &&
+     cmp "test-1-${pack1}.pack" ".git/objects/pack/pack-${pack1}.pack" &&
+     cmp "test-2-${pack1}.idx"  ".git/objects/pack/pack-${pack1}.idx"'
+
+test_expect_success \
+    '[index v2] 2) create a stealth corruption in a delta base reference' \
+    '# this test assumes a delta smaller than 16 bytes at the end of the pack
+     git show-index <1.idx | sort -n | tail -n 1 | (
+       read delta_offs delta_sha1 delta_crc &&
+       git cat-file blob "$delta_sha1" > blob_3 &&
+       chmod +w ".git/objects/pack/pack-${pack1}.pack" &&
+       dd of=".git/objects/pack/pack-${pack1}.pack" seek=$(($delta_offs + 1)) \
+         if=".git/objects/pack/pack-${pack1}.idx" skip=$((8 + 256 * 4)) \
+         bs=1 count=20 conv=notrunc &&
+       git cat-file blob "$delta_sha1" > blob_4 )'
+
+test_expect_failure \
+    '[index v2] 3) corrupted delta happily returned wrong data' \
+    'cmp blob_3 blob_4'
+
+test_expect_failure \
+    '[index v2] 4) confirm that the pack is actually corrupted' \
+    'git fsck --full $commit'
+
+test_expect_failure \
+    '[index v2] 5) pack-objects refuses to reuse corrupted data' \
+    'git pack-objects test-5 <obj-list'
+
+test_done
diff --git a/t/t5400-send-pack.sh b/t/t5400-send-pack.sh
new file mode 100755 (executable)
index 0000000..2d0c07f
--- /dev/null
@@ -0,0 +1,174 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='See why rewinding head breaks send-pack
+
+'
+. ./test-lib.sh
+
+cnt=64
+test_expect_success setup '
+       test_tick &&
+       mkdir mozart mozart/is &&
+       echo "Commit #0" >mozart/is/pink &&
+       git update-index --add mozart/is/pink &&
+       tree=$(git write-tree) &&
+       commit=$(echo "Commit #0" | git commit-tree $tree) &&
+       zero=$commit &&
+       parent=$zero &&
+       i=0 &&
+       while test $i -le $cnt
+       do
+           i=$(($i+1)) &&
+           test_tick &&
+           echo "Commit #$i" >mozart/is/pink &&
+           git update-index --add mozart/is/pink &&
+           tree=$(git write-tree) &&
+           commit=$(echo "Commit #$i" | git commit-tree $tree -p $parent) &&
+           git update-ref refs/tags/commit$i $commit &&
+           parent=$commit || return 1
+       done &&
+       git update-ref HEAD "$commit" &&
+       git-clone ./. victim &&
+       cd victim &&
+       git log &&
+       cd .. &&
+       git update-ref HEAD "$zero" &&
+       parent=$zero &&
+       i=0 &&
+       while test $i -le $cnt
+       do
+           i=$(($i+1)) &&
+           test_tick &&
+           echo "Rebase #$i" >mozart/is/pink &&
+           git update-index --add mozart/is/pink &&
+           tree=$(git write-tree) &&
+           commit=$(echo "Rebase #$i" | git commit-tree $tree -p $parent) &&
+           git update-ref refs/tags/rebase$i $commit &&
+           parent=$commit || return 1
+       done &&
+       git update-ref HEAD "$commit" &&
+       echo Rebase &&
+       git log'
+
+test_expect_success 'pack the source repository' '
+       git repack -a -d &&
+       git prune
+'
+
+test_expect_success 'pack the destination repository' '
+       cd victim &&
+       git repack -a -d &&
+       git prune &&
+       cd ..
+'
+
+test_expect_success \
+        'pushing rewound head should not barf but require --force' '
+       # should not fail but refuse to update.
+       if git-send-pack ./victim/.git/ master
+       then
+               # now it should fail with Pasky patch
+               echo >&2 Gaah, it should have failed.
+               false
+       else
+               echo >&2 Thanks, it correctly failed.
+               true
+       fi &&
+       if cmp victim/.git/refs/heads/master .git/refs/heads/master
+       then
+               # should have been left as it was!
+               false
+       else
+               true
+       fi &&
+       # this should update
+       git-send-pack --force ./victim/.git/ master &&
+       cmp victim/.git/refs/heads/master .git/refs/heads/master
+'
+
+test_expect_success \
+        'push can be used to delete a ref' '
+       cd victim &&
+       git branch extra master &&
+       cd .. &&
+       test -f victim/.git/refs/heads/extra &&
+       git-send-pack ./victim/.git/ :extra master &&
+       ! test -f victim/.git/refs/heads/extra
+'
+
+unset GIT_CONFIG GIT_CONFIG_LOCAL
+HOME=`pwd`/no-such-directory
+export HOME ;# this way we force the victim/.git/config to be used.
+
+test_expect_success \
+        'pushing with --force should be denied with denyNonFastforwards' '
+       cd victim &&
+       git config receive.denyNonFastforwards true &&
+       cd .. &&
+       git update-ref refs/heads/master master^ || return 1
+       git-send-pack --force ./victim/.git/ master && return 1
+       ! git diff .git/refs/heads/master victim/.git/refs/heads/master
+'
+
+test_expect_success \
+       'pushing does not include non-head refs' '
+       mkdir parent && cd parent &&
+       git-init && touch file && git-add file && git-commit -m add &&
+       cd .. &&
+       git-clone parent child && cd child && git-push --all &&
+       cd ../parent &&
+       git-branch -a >branches && ! grep -q origin/master branches
+'
+
+rewound_push_setup() {
+       rm -rf parent child &&
+       mkdir parent && cd parent &&
+       git-init && echo one >file && git-add file && git-commit -m one &&
+       echo two >file && git-commit -a -m two &&
+       cd .. &&
+       git-clone parent child && cd child && git-reset --hard HEAD^
+}
+
+rewound_push_succeeded() {
+       cmp ../parent/.git/refs/heads/master .git/refs/heads/master
+}
+
+rewound_push_failed() {
+       if rewound_push_succeeded
+       then
+               false
+       else
+               true
+       fi
+}
+
+test_expect_success \
+       'pushing explicit refspecs respects forcing' '
+       rewound_push_setup &&
+       if git-send-pack ../parent/.git refs/heads/master:refs/heads/master
+       then
+               false
+       else
+               true
+       fi && rewound_push_failed &&
+       git-send-pack ../parent/.git +refs/heads/master:refs/heads/master &&
+       rewound_push_succeeded
+'
+
+test_expect_success \
+       'pushing wildcard refspecs respects forcing' '
+       rewound_push_setup &&
+       if git-send-pack ../parent/.git refs/heads/*:refs/heads/*
+       then
+               false
+       else
+               true
+       fi && rewound_push_failed &&
+       git-send-pack ../parent/.git +refs/heads/*:refs/heads/* &&
+       rewound_push_succeeded
+'
+
+test_done
diff --git a/t/t5401-update-hooks.sh b/t/t5401-update-hooks.sh
new file mode 100755 (executable)
index 0000000..c5dd30d
--- /dev/null
@@ -0,0 +1,136 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Shawn O. Pearce
+#
+
+test_description='Test the update hook infrastructure.'
+. ./test-lib.sh
+
+test_expect_success setup '
+       echo This is a test. >a &&
+       git update-index --add a &&
+       tree0=$(git write-tree) &&
+       commit0=$(echo setup | git commit-tree $tree0) &&
+       echo We hope it works. >a &&
+       git update-index a &&
+       tree1=$(git write-tree) &&
+       commit1=$(echo modify | git commit-tree $tree1 -p $commit0) &&
+       git update-ref refs/heads/master $commit0 &&
+       git update-ref refs/heads/tofail $commit1 &&
+       git-clone ./. victim &&
+       GIT_DIR=victim/.git git update-ref refs/heads/tofail $commit1 &&
+       git update-ref refs/heads/master $commit1 &&
+       git update-ref refs/heads/tofail $commit0
+'
+
+cat >victim/.git/hooks/pre-receive <<'EOF'
+#!/bin/sh
+printf "$@" >>$GIT_DIR/pre-receive.args
+cat - >$GIT_DIR/pre-receive.stdin
+echo STDOUT pre-receive
+echo STDERR pre-receive >&2
+EOF
+chmod u+x victim/.git/hooks/pre-receive
+
+cat >victim/.git/hooks/update <<'EOF'
+#!/bin/sh
+echo "$@" >>$GIT_DIR/update.args
+read x; printf "$x" >$GIT_DIR/update.stdin
+echo STDOUT update $1
+echo STDERR update $1 >&2
+test "$1" = refs/heads/master || exit
+EOF
+chmod u+x victim/.git/hooks/update
+
+cat >victim/.git/hooks/post-receive <<'EOF'
+#!/bin/sh
+printf "$@" >>$GIT_DIR/post-receive.args
+cat - >$GIT_DIR/post-receive.stdin
+echo STDOUT post-receive
+echo STDERR post-receive >&2
+EOF
+chmod u+x victim/.git/hooks/post-receive
+
+cat >victim/.git/hooks/post-update <<'EOF'
+#!/bin/sh
+echo "$@" >>$GIT_DIR/post-update.args
+read x; printf "$x" >$GIT_DIR/post-update.stdin
+echo STDOUT post-update
+echo STDERR post-update >&2
+EOF
+chmod u+x victim/.git/hooks/post-update
+
+test_expect_failure push '
+       git-send-pack --force ./victim/.git master tofail >send.out 2>send.err
+'
+
+test_expect_success 'updated as expected' '
+       test $(GIT_DIR=victim/.git git rev-parse master) = $commit1 &&
+       test $(GIT_DIR=victim/.git git rev-parse tofail) = $commit1
+'
+
+test_expect_success 'hooks ran' '
+       test -f victim/.git/pre-receive.args &&
+       test -f victim/.git/pre-receive.stdin &&
+       test -f victim/.git/update.args &&
+       test -f victim/.git/update.stdin &&
+       test -f victim/.git/post-receive.args &&
+       test -f victim/.git/post-receive.stdin &&
+       test -f victim/.git/post-update.args &&
+       test -f victim/.git/post-update.stdin
+'
+
+test_expect_success 'pre-receive hook input' '
+       (echo $commit0 $commit1 refs/heads/master;
+        echo $commit1 $commit0 refs/heads/tofail
+       ) | git diff - victim/.git/pre-receive.stdin
+'
+
+test_expect_success 'update hook arguments' '
+       (echo refs/heads/master $commit0 $commit1;
+        echo refs/heads/tofail $commit1 $commit0
+       ) | git diff - victim/.git/update.args
+'
+
+test_expect_success 'post-receive hook input' '
+       echo $commit0 $commit1 refs/heads/master |
+       git diff - victim/.git/post-receive.stdin
+'
+
+test_expect_success 'post-update hook arguments' '
+       echo refs/heads/master |
+       git diff - victim/.git/post-update.args
+'
+
+test_expect_success 'all hook stdin is /dev/null' '
+       ! test -s victim/.git/update.stdin &&
+       ! test -s victim/.git/post-update.stdin
+'
+
+test_expect_success 'all *-receive hook args are empty' '
+       ! test -s victim/.git/pre-receive.args &&
+       ! test -s victim/.git/post-receive.args
+'
+
+test_expect_failure 'send-pack produced no output' '
+       test -s send.out
+'
+
+cat <<EOF >expect
+STDOUT pre-receive
+STDERR pre-receive
+STDOUT update refs/heads/master
+STDERR update refs/heads/master
+STDOUT update refs/heads/tofail
+STDERR update refs/heads/tofail
+STDOUT post-receive
+STDERR post-receive
+STDOUT post-update
+STDERR post-update
+EOF
+test_expect_success 'send-pack stderr contains hook messages' '
+       egrep ^STD send.err >actual &&
+       git diff - actual <expect
+'
+
+test_done
diff --git a/t/t5402-post-merge-hook.sh b/t/t5402-post-merge-hook.sh
new file mode 100755 (executable)
index 0000000..1c4b0b3
--- /dev/null
@@ -0,0 +1,56 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Josh England
+#
+
+test_description='Test the post-merge hook.'
+. ./test-lib.sh
+
+test_expect_success setup '
+       echo Data for commit0. >a &&
+       git update-index --add a &&
+       tree0=$(git write-tree) &&
+       commit0=$(echo setup | git commit-tree $tree0) &&
+       echo Changed data for commit1. >a &&
+       git update-index a &&
+       tree1=$(git write-tree) &&
+       commit1=$(echo modify | git commit-tree $tree1 -p $commit0) &&
+        git update-ref refs/heads/master $commit0 &&
+       git-clone ./. clone1 &&
+       GIT_DIR=clone1/.git git update-index --add a &&
+       git-clone ./. clone2 &&
+       GIT_DIR=clone2/.git git update-index --add a
+'
+
+for clone in 1 2; do
+    cat >clone${clone}/.git/hooks/post-merge <<'EOF'
+#!/bin/sh
+echo $@ >> $GIT_DIR/post-merge.args
+EOF
+    chmod u+x clone${clone}/.git/hooks/post-merge
+done
+
+test_expect_failure 'post-merge does not run for up-to-date ' '
+        GIT_DIR=clone1/.git git merge $commit0 &&
+       test -e clone1/.git/post-merge.args
+'
+
+test_expect_success 'post-merge runs as expected ' '
+        GIT_DIR=clone1/.git git merge $commit1 &&
+       test -e clone1/.git/post-merge.args
+'
+
+test_expect_success 'post-merge from normal merge receives the right argument ' '
+        grep 0 clone1/.git/post-merge.args
+'
+
+test_expect_success 'post-merge from squash merge runs as expected ' '
+        GIT_DIR=clone2/.git git merge --squash $commit1 &&
+       test -e clone2/.git/post-merge.args
+'
+
+test_expect_success 'post-merge from squash merge receives the right argument ' '
+        grep 1 clone2/.git/post-merge.args
+'
+
+test_done
diff --git a/t/t5403-post-checkout-hook.sh b/t/t5403-post-checkout-hook.sh
new file mode 100755 (executable)
index 0000000..823239a
--- /dev/null
@@ -0,0 +1,74 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Josh England
+#
+
+test_description='Test the post-checkout hook.'
+. ./test-lib.sh
+
+test_expect_success setup '
+        echo Data for commit0. >a &&
+        echo Data for commit0. >b &&
+        git update-index --add a &&
+        git update-index --add b &&
+        tree0=$(git write-tree) &&
+        commit0=$(echo setup | git commit-tree $tree0) &&
+        git update-ref refs/heads/master $commit0 &&
+        git-clone ./. clone1 &&
+        git-clone ./. clone2 &&
+        GIT_DIR=clone2/.git git branch -a new2 &&
+        echo Data for commit1. >clone2/b &&
+        GIT_DIR=clone2/.git git add clone2/b &&
+        GIT_DIR=clone2/.git git commit -m new2
+'
+
+for clone in 1 2; do
+    cat >clone${clone}/.git/hooks/post-checkout <<'EOF'
+#!/bin/sh
+echo $@ > $GIT_DIR/post-checkout.args
+EOF
+    chmod u+x clone${clone}/.git/hooks/post-checkout
+done
+
+test_expect_success 'post-checkout runs as expected ' '
+        GIT_DIR=clone1/.git git checkout master &&
+        test -e clone1/.git/post-checkout.args
+'
+
+test_expect_success 'post-checkout receives the right arguments with HEAD unchanged ' '
+        old=$(awk "{print \$1}" clone1/.git/post-checkout.args) &&
+        new=$(awk "{print \$2}" clone1/.git/post-checkout.args) &&
+        flag=$(awk "{print \$3}" clone1/.git/post-checkout.args) &&
+        test $old = $new -a $flag = 1
+'
+
+test_expect_success 'post-checkout runs as expected ' '
+        GIT_DIR=clone1/.git git checkout master &&
+        test -e clone1/.git/post-checkout.args
+'
+
+test_expect_success 'post-checkout args are correct with git checkout -b ' '
+        GIT_DIR=clone1/.git git checkout -b new1 &&
+        old=$(awk "{print \$1}" clone1/.git/post-checkout.args) &&
+        new=$(awk "{print \$2}" clone1/.git/post-checkout.args) &&
+        flag=$(awk "{print \$3}" clone1/.git/post-checkout.args) &&
+        test $old = $new -a $flag = 1
+'
+
+test_expect_success 'post-checkout receives the right args with HEAD changed ' '
+        GIT_DIR=clone2/.git git checkout new2 &&
+        old=$(awk "{print \$1}" clone2/.git/post-checkout.args) &&
+        new=$(awk "{print \$2}" clone2/.git/post-checkout.args) &&
+        flag=$(awk "{print \$3}" clone2/.git/post-checkout.args) &&
+        test $old != $new -a $flag = 1
+'
+
+test_expect_success 'post-checkout receives the right args when not switching branches ' '
+        GIT_DIR=clone2/.git git checkout master b &&
+        old=$(awk "{print \$1}" clone2/.git/post-checkout.args) &&
+        new=$(awk "{print \$2}" clone2/.git/post-checkout.args) &&
+        flag=$(awk "{print \$3}" clone2/.git/post-checkout.args) &&
+        test $old = $new -a $flag = 0
+'
+
+test_done
diff --git a/t/t5404-tracking-branches.sh b/t/t5404-tracking-branches.sh
new file mode 100755 (executable)
index 0000000..1493a92
--- /dev/null
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+test_description='tracking branch update checks for git push'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+       echo 1 >file &&
+       git add file &&
+       git commit -m 1 &&
+       git branch b1 &&
+       git branch b2 &&
+       git clone . aa &&
+       git checkout b1 &&
+       echo b1 >>file &&
+       git commit -a -m b1 &&
+       git checkout b2 &&
+       echo b2 >>file &&
+       git commit -a -m b2
+'
+
+test_expect_success 'prepare pushable branches' '
+       cd aa &&
+       b1=$(git rev-parse origin/b1) &&
+       b2=$(git rev-parse origin/b2) &&
+       git checkout -b b1 origin/b1 &&
+       echo aa-b1 >>file &&
+       git commit -a -m aa-b1 &&
+       git checkout -b b2 origin/b2 &&
+       echo aa-b2 >>file &&
+       git commit -a -m aa-b2 &&
+       git checkout master &&
+       echo aa-master >>file &&
+       git commit -a -m aa-master
+'
+
+test_expect_success 'mixed-success push returns error' '! git push'
+
+test_expect_success 'check tracking branches updated correctly after push' '
+       test "$(git rev-parse origin/master)" = "$(git rev-parse master)"
+'
+
+test_expect_success 'check tracking branches not updated for failed refs' '
+       test "$(git rev-parse origin/b1)" = "$b1" &&
+       test "$(git rev-parse origin/b2)" = "$b2"
+'
+
+test_expect_success 'deleted branches have their tracking branches removed' '
+       git push origin :b1 &&
+       test "$(git rev-parse origin/b1)" = "origin/b1"
+'
+
+test_done
diff --git a/t/t5405-send-pack-rewind.sh b/t/t5405-send-pack-rewind.sh
new file mode 100755 (executable)
index 0000000..86abc62
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+test_description='forced push to replace commit we do not have'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       >file1 && git add file1 && test_tick &&
+       git commit -m Initial &&
+
+       mkdir another && (
+               cd another &&
+               git init &&
+               git fetch .. master:master
+       ) &&
+
+       >file2 && git add file2 && test_tick &&
+       git commit -m Second
+
+'
+
+test_expect_success 'non forced push should die not segfault' '
+
+       (
+               cd another &&
+               git push .. master:master
+               test $? = 1
+       )
+
+'
+
+test_expect_success 'forced push should succeed' '
+
+       (
+               cd another &&
+               git push .. +master:master
+       )
+
+'
+
+test_done
diff --git a/t/t5406-remote-rejects.sh b/t/t5406-remote-rejects.sh
new file mode 100755 (executable)
index 0000000..46b2cb4
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+test_description='remote push rejects are reported by client'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+       mkdir .git/hooks &&
+       (echo "#!/bin/sh" ; echo "exit 1") >.git/hooks/update &&
+       chmod +x .git/hooks/update &&
+       echo 1 >file &&
+       git add file &&
+       git commit -m 1 &&
+       git clone . child &&
+       cd child &&
+       echo 2 >file &&
+       git commit -a -m 2
+'
+
+test_expect_success 'push reports error' '! git push 2>stderr'
+
+test_expect_success 'individual ref reports error' 'grep rejected stderr'
+
+test_done
diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
new file mode 100755 (executable)
index 0000000..7b6798d
--- /dev/null
@@ -0,0 +1,182 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Johannes Schindelin
+#
+
+test_description='Testing multi_ack pack fetching
+
+'
+. ./test-lib.sh
+
+# Test fetch-pack/upload-pack pair.
+
+# Some convenience functions
+
+add () {
+       name=$1
+       text="$@"
+       branch=`echo $name | sed -e 's/^\(.\).*$/\1/'`
+       parents=""
+
+       shift
+       while test $1; do
+               parents="$parents -p $1"
+               shift
+       done
+
+       echo "$text" > test.txt
+       git update-index --add test.txt
+       tree=$(git write-tree)
+       # make sure timestamps are in correct order
+       sec=$(($sec+1))
+       commit=$(echo "$text" | GIT_AUTHOR_DATE=$sec \
+               git commit-tree $tree $parents 2>>log2.txt)
+       export $name=$commit
+       echo $commit > .git/refs/heads/$branch
+       eval ${branch}TIP=$commit
+}
+
+count_objects () {
+       ls .git/objects/??/* 2>>log2.txt | wc -l | tr -d " "
+}
+
+test_expect_object_count () {
+       message=$1
+       count=$2
+
+       output="$(count_objects)"
+       test_expect_success \
+               "new object count $message" \
+               "test $count = $output"
+}
+
+pull_to_client () {
+       number=$1
+       heads=$2
+       count=$3
+       no_strict_count_check=$4
+
+       cd client
+       test_expect_success "$number pull" \
+               "git-fetch-pack -k -v .. $heads"
+       case "$heads" in *A*) echo $ATIP > .git/refs/heads/A;; esac
+       case "$heads" in *B*) echo $BTIP > .git/refs/heads/B;; esac
+       git symbolic-ref HEAD refs/heads/`echo $heads | sed -e 's/^\(.\).*$/\1/'`
+
+       test_expect_success "fsck" 'git fsck --full > fsck.txt 2>&1'
+
+       test_expect_success 'check downloaded results' \
+       'mv .git/objects/pack/pack-* . &&
+        p=`ls -1 pack-*.pack` &&
+        git unpack-objects <$p &&
+        git fsck --full'
+
+       test_expect_success "new object count after $number pull" \
+       'idx=`echo pack-*.idx` &&
+        pack_count=`git show-index <$idx | wc -l` &&
+        test $pack_count = $count'
+       test -z "$pack_count" && pack_count=0
+       if [ -z "$no_strict_count_check" ]; then
+               test_expect_success "minimal count" "test $count = $pack_count"
+       else
+               test $count != $pack_count && \
+                       echo "WARNING: $pack_count objects transmitted, only $count of which were needed"
+       fi
+       rm -f pack-*
+       cd ..
+}
+
+# Here begins the actual testing
+
+# A1 - ... - A20 - A21
+#    \
+#      B1  -   B2 - .. - B70
+
+# client pulls A20, B1. Then tracks only B. Then pulls A.
+
+(
+       mkdir client &&
+       cd client &&
+       git init 2>> log2.txt &&
+       git config transfer.unpacklimit 0
+)
+
+add A1
+
+prev=1; cur=2; while [ $cur -le 10 ]; do
+       add A$cur $(eval echo \$A$prev)
+       prev=$cur
+       cur=$(($cur+1))
+done
+
+add B1 $A1
+
+echo $ATIP > .git/refs/heads/A
+echo $BTIP > .git/refs/heads/B
+git symbolic-ref HEAD refs/heads/B
+
+pull_to_client 1st "B A" $((11*3))
+
+add A11 $A10
+
+prev=1; cur=2; while [ $cur -le 65 ]; do
+       add B$cur $(eval echo \$B$prev)
+       prev=$cur
+       cur=$(($cur+1))
+done
+
+pull_to_client 2nd "B" $((64*3))
+
+pull_to_client 3rd "A" $((1*3)) # old fails
+
+test_expect_success "clone shallow" "git-clone --depth 2 file://`pwd`/. shallow"
+
+(cd shallow; git count-objects -v) > count.shallow
+
+test_expect_success "clone shallow object count" \
+       "test \"in-pack: 18\" = \"$(grep in-pack count.shallow)\""
+
+count_output () {
+       sed -e '/^in-pack:/d' -e '/^packs:/d' -e '/: 0$/d' "$1"
+}
+
+test_expect_success "clone shallow object count (part 2)" '
+       test -z "$(count_output count.shallow)"
+'
+
+test_expect_success "fsck in shallow repo" \
+       "(cd shallow; git fsck --full)"
+
+#test_done; exit
+
+add B66 $B65
+add B67 $B66
+
+test_expect_success "pull in shallow repo" \
+       "(cd shallow; git pull .. B)"
+
+(cd shallow; git count-objects -v) > count.shallow
+test_expect_success "clone shallow object count" \
+       "test \"count: 6\" = \"$(grep count count.shallow)\""
+
+add B68 $B67
+add B69 $B68
+
+test_expect_success "deepening pull in shallow repo" \
+       "(cd shallow; git pull --depth 4 .. B)"
+
+(cd shallow; git count-objects -v) > count.shallow
+test_expect_success "clone shallow object count" \
+       "test \"count: 12\" = \"$(grep count count.shallow)\""
+
+test_expect_success "deepening fetch in shallow repo" \
+       "(cd shallow; git fetch --depth 4 .. A:A)"
+
+(cd shallow; git count-objects -v) > count.shallow
+test_expect_success "clone shallow object count" \
+       "test \"count: 18\" = \"$(grep count count.shallow)\""
+
+test_expect_failure "pull in shallow repo with missing merge base" \
+       "(cd shallow; git pull --depth 4 .. A)"
+
+test_done
diff --git a/t/t5502-quickfetch.sh b/t/t5502-quickfetch.sh
new file mode 100755 (executable)
index 0000000..16eadd6
--- /dev/null
@@ -0,0 +1,122 @@
+#!/bin/sh
+
+test_description='test quickfetch from local'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       test_tick &&
+       echo ichi >file &&
+       git add file &&
+       git commit -m initial &&
+
+       cnt=$( (
+               git count-objects | sed -e "s/ *objects,.*//"
+       ) ) &&
+       test $cnt -eq 3
+'
+
+test_expect_success 'clone without alternate' '
+
+       (
+               mkdir cloned &&
+               cd cloned &&
+               git init-db &&
+               git remote add -f origin ..
+       ) &&
+       cnt=$( (
+               cd cloned &&
+               git count-objects | sed -e "s/ *objects,.*//"
+       ) ) &&
+       test $cnt -eq 3
+'
+
+test_expect_success 'further commits in the original' '
+
+       test_tick &&
+       echo ni >file &&
+       git commit -a -m second &&
+
+       cnt=$( (
+               git count-objects | sed -e "s/ *objects,.*//"
+       ) ) &&
+       test $cnt -eq 6
+'
+
+test_expect_success 'copy commit and tree but not blob by hand' '
+
+       git rev-list --objects HEAD |
+       git pack-objects --stdout |
+       (
+               cd cloned &&
+               git unpack-objects
+       ) &&
+
+       cnt=$( (
+               cd cloned &&
+               git count-objects | sed -e "s/ *objects,.*//"
+       ) ) &&
+       test $cnt -eq 6
+
+       blob=$(git rev-parse HEAD:file | sed -e "s|..|&/|") &&
+       test -f "cloned/.git/objects/$blob" &&
+       rm -f "cloned/.git/objects/$blob" &&
+
+       cnt=$( (
+               cd cloned &&
+               git count-objects | sed -e "s/ *objects,.*//"
+       ) ) &&
+       test $cnt -eq 5
+
+'
+
+test_expect_success 'quickfetch should not leave a corrupted repository' '
+
+       (
+               cd cloned &&
+               git fetch
+       ) &&
+
+       cnt=$( (
+               cd cloned &&
+               git count-objects | sed -e "s/ *objects,.*//"
+       ) ) &&
+       test $cnt -eq 6
+
+'
+
+test_expect_success 'quickfetch should not copy from alternate' '
+
+       (
+               mkdir quickclone &&
+               cd quickclone &&
+               git init-db &&
+               (cd ../.git/objects && pwd) >.git/objects/info/alternates &&
+               git remote add origin .. &&
+               git fetch -k -k
+       ) &&
+       obj_cnt=$( (
+               cd quickclone &&
+               git count-objects | sed -e "s/ *objects,.*//"
+       ) ) &&
+       pck_cnt=$( (
+               cd quickclone &&
+               git count-objects -v | sed -n -e "/packs:/{
+                               s/packs://
+                               p
+                               q
+                       }"
+       ) ) &&
+       origin_master=$( (
+               cd quickclone &&
+               git rev-parse origin/master
+       ) ) &&
+       echo "loose objects: $obj_cnt, packfiles: $pck_cnt" &&
+       test $obj_cnt -eq 0 &&
+       test $pck_cnt -eq 0 &&
+       test z$origin_master = z$(git rev-parse master)
+
+'
+
+test_done
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
new file mode 100755 (executable)
index 0000000..636aec2
--- /dev/null
@@ -0,0 +1,100 @@
+#!/bin/sh
+
+test_description='git remote porcelain-ish'
+
+. ./test-lib.sh
+
+GIT_CONFIG=.git/config
+export GIT_CONFIG
+
+setup_repository () {
+       mkdir "$1" && (
+       cd "$1" &&
+       git init &&
+       >file &&
+       git add file &&
+       git commit -m "Initial" &&
+       git checkout -b side &&
+       >elif &&
+       git add elif &&
+       git commit -m "Second" &&
+       git checkout master
+       )
+}
+
+tokens_match () {
+       echo "$1" | tr ' ' '\012' | sort | sed -e '/^$/d' >expect &&
+       echo "$2" | tr ' ' '\012' | sort | sed -e '/^$/d' >actual &&
+       diff -u expect actual
+}
+
+check_remote_track () {
+       actual=$(git remote show "$1" | sed -n -e '$p') &&
+       shift &&
+       tokens_match "$*" "$actual"
+}
+
+check_tracking_branch () {
+       f="" &&
+       r=$(git for-each-ref "--format=%(refname)" |
+               sed -ne "s|^refs/remotes/$1/||p") &&
+       shift &&
+       tokens_match "$*" "$r"
+}
+
+test_expect_success setup '
+
+       setup_repository one &&
+       setup_repository two &&
+       (
+               cd two && git branch another
+       ) &&
+       git clone one test
+
+'
+
+test_expect_success 'remote information for the origin' '
+(
+       cd test &&
+       tokens_match origin "$(git remote)" &&
+       check_remote_track origin master side &&
+       check_tracking_branch origin HEAD master side
+)
+'
+
+test_expect_success 'add another remote' '
+(
+       cd test &&
+       git remote add -f second ../two &&
+       tokens_match "origin second" "$(git remote)" &&
+       check_remote_track origin master side &&
+       check_remote_track second master side another &&
+       check_tracking_branch second master side another &&
+       git for-each-ref "--format=%(refname)" refs/remotes |
+       sed -e "/^refs\/remotes\/origin\//d" \
+           -e "/^refs\/remotes\/second\//d" >actual &&
+       >expect &&
+       diff -u expect actual
+)
+'
+
+test_expect_success 'remove remote' '
+(
+       cd test &&
+       git remote rm second
+)
+'
+
+test_expect_success 'remove remote' '
+(
+       cd test &&
+       tokens_match origin "$(git remote)" &&
+       check_remote_track origin master side &&
+       git for-each-ref "--format=%(refname)" refs/remotes |
+       sed -e "/^refs\/remotes\/origin\//d" >actual &&
+       >expect &&
+       diff -u expect actual
+)
+'
+
+test_done
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
new file mode 100755 (executable)
index 0000000..02882c1
--- /dev/null
@@ -0,0 +1,298 @@
+#!/bin/sh
+# Copyright (c) 2006, Junio C Hamano.
+
+test_description='Per branch config variables affects "git fetch".
+
+'
+
+. ./test-lib.sh
+
+D=`pwd`
+
+test_expect_success setup '
+       echo >file original &&
+       git add file &&
+       git commit -a -m original'
+
+test_expect_success "clone and setup child repos" '
+       git clone . one &&
+       cd one &&
+       echo >file updated by one &&
+       git commit -a -m "updated by one" &&
+       cd .. &&
+       git clone . two &&
+       cd two &&
+       git config branch.master.remote one &&
+       git config remote.one.url ../one/.git/ &&
+       git config remote.one.fetch refs/heads/master:refs/heads/one &&
+       cd .. &&
+       git clone . three &&
+       cd three &&
+       git config branch.master.remote two &&
+       git config branch.master.merge refs/heads/one &&
+       mkdir -p .git/remotes &&
+       {
+               echo "URL: ../two/.git/"
+               echo "Pull: refs/heads/master:refs/heads/two"
+               echo "Pull: refs/heads/one:refs/heads/one"
+       } >.git/remotes/two &&
+       cd .. &&
+       git clone . bundle
+'
+
+test_expect_success "fetch test" '
+       cd "$D" &&
+       echo >file updated by origin &&
+       git commit -a -m "updated by origin" &&
+       cd two &&
+       git fetch &&
+       test -f .git/refs/heads/one &&
+       mine=`git rev-parse refs/heads/one` &&
+       his=`cd ../one && git rev-parse refs/heads/master` &&
+       test "z$mine" = "z$his"
+'
+
+test_expect_success "fetch test for-merge" '
+       cd "$D" &&
+       cd three &&
+       git fetch &&
+       test -f .git/refs/heads/two &&
+       test -f .git/refs/heads/one &&
+       master_in_two=`cd ../two && git rev-parse master` &&
+       one_in_two=`cd ../two && git rev-parse one` &&
+       {
+               echo "$master_in_two    not-for-merge"
+               echo "$one_in_two       "
+       } >expected &&
+       cut -f -2 .git/FETCH_HEAD >actual &&
+       diff expected actual'
+
+test_expect_success 'fetch tags when there is no tags' '
+
+    cd "$D" &&
+
+    mkdir notags &&
+    cd notags &&
+    git init &&
+
+    git fetch -t ..
+
+'
+
+test_expect_success 'fetch following tags' '
+
+       cd "$D" &&
+       git tag -a -m 'annotated' anno HEAD &&
+       git tag light HEAD &&
+
+       mkdir four &&
+       cd four &&
+       git init &&
+
+       git fetch .. :track &&
+       git show-ref --verify refs/tags/anno &&
+       git show-ref --verify refs/tags/light
+
+'
+
+test_expect_failure 'fetch must not resolve short tag name' '
+
+       cd "$D" &&
+
+       mkdir five &&
+       cd five &&
+       git init &&
+
+       git fetch .. anno:five
+
+'
+
+test_expect_failure 'fetch must not resolve short remote name' '
+
+       cd "$D" &&
+       git-update-ref refs/remotes/six/HEAD HEAD
+
+       mkdir six &&
+       cd six &&
+       git init &&
+
+       git fetch .. six:six
+
+'
+
+test_expect_success 'create bundle 1' '
+       cd "$D" &&
+       echo >file updated again by origin &&
+       git commit -a -m "tip" &&
+       git bundle create bundle1 master^..master
+'
+
+test_expect_success 'header of bundle looks right' '
+       head -n 1 "$D"/bundle1 | grep "^#" &&
+       head -n 2 "$D"/bundle1 | grep "^-[0-9a-f]\{40\} " &&
+       head -n 3 "$D"/bundle1 | grep "^[0-9a-f]\{40\} " &&
+       head -n 4 "$D"/bundle1 | grep "^$"
+'
+
+test_expect_success 'create bundle 2' '
+       cd "$D" &&
+       git bundle create bundle2 master~2..master
+'
+
+test_expect_failure 'unbundle 1' '
+       cd "$D/bundle" &&
+       git checkout -b some-branch &&
+       git fetch "$D/bundle1" master:master
+'
+
+test_expect_success 'bundle 1 has only 3 files ' '
+       cd "$D" &&
+       (
+               while read x && test -n "$x"
+               do
+                       :;
+               done
+               cat
+       ) <bundle1 >bundle.pack &&
+       git index-pack bundle.pack &&
+       verify=$(git verify-pack -v bundle.pack) &&
+       test 4 = $(echo "$verify" | wc -l)
+'
+
+test_expect_success 'unbundle 2' '
+       cd "$D/bundle" &&
+       git fetch ../bundle2 master:master &&
+       test "tip" = "$(git log -1 --pretty=oneline master | cut -b42-)"
+'
+
+test_expect_success 'bundle does not prerequisite objects' '
+       cd "$D" &&
+       touch file2 &&
+       git add file2 &&
+       git commit -m add.file2 file2 &&
+       git bundle create bundle3 -1 HEAD &&
+       (
+               while read x && test -n "$x"
+               do
+                       :;
+               done
+               cat
+       ) <bundle3 >bundle.pack &&
+       git index-pack bundle.pack &&
+       test 4 = $(git verify-pack -v bundle.pack | wc -l)
+'
+
+test_expect_success 'bundle should be able to create a full history' '
+
+       cd "$D" &&
+       git tag -a -m '1.0' v1.0 master &&
+       git bundle create bundle4 v1.0
+
+'
+
+test "$TEST_RSYNC" && {
+test_expect_success 'fetch via rsync' '
+       git pack-refs &&
+       mkdir rsynced &&
+       cd rsynced &&
+       git init &&
+       git fetch rsync://127.0.0.1$(pwd)/../.git master:refs/heads/master &&
+       git gc --prune &&
+       test $(git rev-parse master) = $(cd .. && git rev-parse master) &&
+       git fsck --full
+'
+
+test_expect_success 'push via rsync' '
+       mkdir ../rsynced2 &&
+       (cd ../rsynced2 &&
+        git init) &&
+       git push rsync://127.0.0.1$(pwd)/../rsynced2/.git master &&
+       cd ../rsynced2 &&
+       git gc --prune &&
+       test $(git rev-parse master) = $(cd .. && git rev-parse master) &&
+       git fsck --full
+'
+
+test_expect_success 'push via rsync' '
+       cd .. &&
+       mkdir rsynced3 &&
+       (cd rsynced3 &&
+        git init) &&
+       git push --all rsync://127.0.0.1$(pwd)/rsynced3/.git &&
+       cd rsynced3 &&
+       test $(git rev-parse master) = $(cd .. && git rev-parse master) &&
+       git fsck --full
+'
+}
+
+test_expect_success 'fetch with a non-applying branch.<name>.merge' '
+       git config branch.master.remote yeti &&
+       git config branch.master.merge refs/heads/bigfoot &&
+       git config remote.blub.url one &&
+       git config remote.blub.fetch "refs/heads/*:refs/remotes/one/*" &&
+       git fetch blub
+'
+
+# the strange name is: a\!'b
+test_expect_success 'quoting of a strangely named repo' '
+       ! git fetch "a\\!'\''b" > result 2>&1 &&
+       cat result &&
+       grep "fatal: '\''a\\\\!'\''b'\''" result
+'
+
+test_expect_success 'bundle should record HEAD correctly' '
+
+       cd "$D" &&
+       git bundle create bundle5 HEAD master &&
+       git bundle list-heads bundle5 >actual &&
+       for h in HEAD refs/heads/master
+       do
+               echo "$(git rev-parse --verify $h) $h"
+       done >expect &&
+       diff -u expect actual
+
+'
+
+test_expect_success 'explicit fetch should not update tracking' '
+
+       cd "$D" &&
+       git branch -f side &&
+       (
+               cd three &&
+               o=$(git rev-parse --verify refs/remotes/origin/master) &&
+               git fetch origin master &&
+               n=$(git rev-parse --verify refs/remotes/origin/master) &&
+               test "$o" = "$n" &&
+               ! git rev-parse --verify refs/remotes/origin/side
+       )
+'
+
+test_expect_success 'explicit pull should not update tracking' '
+
+       cd "$D" &&
+       git branch -f side &&
+       (
+               cd three &&
+               o=$(git rev-parse --verify refs/remotes/origin/master) &&
+               git pull origin master &&
+               n=$(git rev-parse --verify refs/remotes/origin/master) &&
+               test "$o" = "$n" &&
+               ! git rev-parse --verify refs/remotes/origin/side
+       )
+'
+
+test_expect_success 'configured fetch updates tracking' '
+
+       cd "$D" &&
+       git branch -f side &&
+       (
+               cd three &&
+               o=$(git rev-parse --verify refs/remotes/origin/master) &&
+               git fetch origin &&
+               n=$(git rev-parse --verify refs/remotes/origin/master) &&
+               test "$o" != "$n" &&
+               git rev-parse --verify refs/remotes/origin/side
+       )
+'
+
+test_done
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
new file mode 100755 (executable)
index 0000000..6ec5f7c
--- /dev/null
@@ -0,0 +1,52 @@
+#!/bin/sh
+
+test_description='git ls-remote'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       >file &&
+       git add file &&
+       test_tick &&
+       git commit -m initial &&
+       git tag mark &&
+       git show-ref --tags -d | sed -e "s/ /   /" >expected.tag &&
+       (
+               echo "$(git rev-parse HEAD)     HEAD"
+               git show-ref -d | sed -e "s/ /  /"
+       ) >expected.all &&
+
+       git remote add self $(pwd)/.git
+
+'
+
+test_expect_success 'ls-remote --tags .git' '
+
+       git ls-remote --tags .git >actual &&
+       diff -u expected.tag actual
+
+'
+
+test_expect_success 'ls-remote .git' '
+
+       git ls-remote .git >actual &&
+       diff -u expected.all actual
+
+'
+
+test_expect_success 'ls-remote --tags self' '
+
+       git ls-remote --tags self >actual &&
+       diff -u expected.tag actual
+
+'
+
+test_expect_success 'ls-remote self' '
+
+       git ls-remote self >actual &&
+       diff -u expected.all actual
+
+'
+
+test_done
diff --git a/t/t5515-fetch-merge-logic.sh b/t/t5515-fetch-merge-logic.sh
new file mode 100755 (executable)
index 0000000..31c1081
--- /dev/null
@@ -0,0 +1,161 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Santi Béjar, based on t4013 by Junio C Hamano
+#
+#
+
+test_description='Merge logic in fetch'
+
+. ./test-lib.sh
+
+LF='
+'
+
+test_expect_success setup '
+       GIT_AUTHOR_DATE="2006-06-26 00:00:00 +0000" &&
+       GIT_COMMITTER_DATE="2006-06-26 00:00:00 +0000" &&
+       export GIT_AUTHOR_DATE GIT_COMMITTER_DATE &&
+
+       echo >file original &&
+       git add file &&
+       git commit -a -m One &&
+       git tag tag-one &&
+       git tag tag-one-tree HEAD^{tree} &&
+       git branch one &&
+
+       echo two >> file &&
+       git commit -a -m Two &&
+       git tag -a -m "Tag Two" tag-two &&
+       git branch two &&
+
+       echo three >> file &&
+       git commit -a -m Three &&
+       git tag -a -m "Tag Three" tag-three &&
+       git tag -a -m "Tag Three file" tag-three-file HEAD^{tree}:file &&
+       git branch three &&
+
+       echo master >> file &&
+       git commit -a -m Master &&
+       git tag -a -m "Tag Master" tag-master &&
+
+       git checkout three &&
+
+       git clone . cloned &&
+       cd cloned &&
+       git config remote.origin.url ../.git/ &&
+
+       git config remote.config-explicit.url ../.git/ &&
+       git config remote.config-explicit.fetch refs/heads/master:remotes/rem/master &&
+       git config --add remote.config-explicit.fetch refs/heads/one:remotes/rem/one &&
+       git config --add remote.config-explicit.fetch two:remotes/rem/two &&
+       git config --add remote.config-explicit.fetch refs/heads/three:remotes/rem/three &&
+       remotes="config-explicit" &&
+
+       git config remote.config-glob.url ../.git/ &&
+       git config remote.config-glob.fetch refs/heads/*:refs/remotes/rem/* &&
+       remotes="$remotes config-glob" &&
+
+       mkdir -p .git/remotes &&
+       {
+               echo "URL: ../.git/"
+               echo "Pull: refs/heads/master:remotes/rem/master"
+               echo "Pull: refs/heads/one:remotes/rem/one"
+               echo "Pull: two:remotes/rem/two"
+               echo "Pull: refs/heads/three:remotes/rem/three"
+       } >.git/remotes/remote-explicit &&
+       remotes="$remotes remote-explicit" &&
+
+       {
+               echo "URL: ../.git/"
+               echo "Pull: refs/heads/*:refs/remotes/rem/*"
+       } >.git/remotes/remote-glob &&
+       remotes="$remotes remote-glob" &&
+
+       mkdir -p .git/branches &&
+       echo "../.git" > .git/branches/branches-default &&
+       remotes="$remotes branches-default" &&
+
+       echo "../.git#one" > .git/branches/branches-one &&
+       remotes="$remotes branches-one" &&
+
+       for remote in $remotes ; do
+               git config branch.br-$remote.remote $remote &&
+               git config branch.br-$remote-merge.remote $remote &&
+               git config branch.br-$remote-merge.merge refs/heads/three &&
+               git config branch.br-$remote-octopus.remote $remote &&
+               git config branch.br-$remote-octopus.merge refs/heads/one &&
+               git config --add branch.br-$remote-octopus.merge two
+       done
+'
+
+# Merge logic depends on branch properties and Pull: or .fetch lines
+for remote in $remotes ; do
+    for branch in "" "-merge" "-octopus" ; do
+cat <<EOF
+br-$remote$branch
+br-$remote$branch $remote
+EOF
+    done
+done > tests
+
+# Merge logic does not depend on branch properties,
+# but does depend on Pull: or fetch lines.
+# Use two branches completely unrelated from the arguments,
+# the clone default and one without branch properties
+for branch in master br-unconfig ; do
+    echo $branch
+    for remote in $remotes ; do
+       echo $branch $remote
+    done
+done >> tests
+
+# Merge logic does not depend on branch properties
+# neither in the Pull: or .fetch config
+for branch in master br-unconfig ; do
+    cat <<EOF
+$branch ../.git
+$branch ../.git one
+$branch ../.git one two
+$branch --tags ../.git
+$branch ../.git tag tag-one tag tag-three
+$branch ../.git tag tag-one-tree tag tag-three-file
+$branch ../.git one tag tag-one tag tag-three-file
+EOF
+done >> tests
+
+while read cmd
+do
+       case "$cmd" in
+       '' | '#'*) continue ;;
+       esac
+       test=`echo "$cmd" | sed -e 's|[/ ][/ ]*|_|g'`
+       cnt=`expr $test_count + 1`
+       pfx=`printf "%04d" $cnt`
+       expect="../../t5515/fetch.$test"
+       actual="$pfx-fetch.$test"
+
+       test_expect_success "$cmd" '
+               {
+                       echo "# $cmd"
+                       set x $cmd; shift
+                       git symbolic-ref HEAD refs/heads/$1 ; shift
+                       rm -f .git/FETCH_HEAD
+                       rm -f .git/refs/heads/*
+                       rm -f .git/refs/remotes/rem/*
+                       rm -f .git/refs/tags/*
+                       git fetch "$@" >/dev/null
+                       cat .git/FETCH_HEAD
+               } >"$actual" &&
+               if test -f "$expect"
+               then
+                       git diff -u "$expect" "$actual" &&
+                       rm -f "$actual"
+               else
+                       # this is to help developing new tests.
+                       cp "$actual" "$expect"
+                       false
+               fi
+       '
+done < tests
+
+test_done
diff --git a/t/t5515/fetch.br-branches-default b/t/t5515/fetch.br-branches-default
new file mode 100644 (file)
index 0000000..2e0414f
--- /dev/null
@@ -0,0 +1,8 @@
+# br-branches-default
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-default-merge b/t/t5515/fetch.br-branches-default-merge
new file mode 100644 (file)
index 0000000..ca2cc1d
--- /dev/null
@@ -0,0 +1,9 @@
+# br-branches-default-merge
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-default-merge_branches-default b/t/t5515/fetch.br-branches-default-merge_branches-default
new file mode 100644 (file)
index 0000000..7d947cd
--- /dev/null
@@ -0,0 +1,9 @@
+# br-branches-default-merge branches-default
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-default-octopus b/t/t5515/fetch.br-branches-default-octopus
new file mode 100644 (file)
index 0000000..ec39c54
--- /dev/null
@@ -0,0 +1,10 @@
+# br-branches-default-octopus
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-default-octopus_branches-default b/t/t5515/fetch.br-branches-default-octopus_branches-default
new file mode 100644 (file)
index 0000000..6bf42e2
--- /dev/null
@@ -0,0 +1,10 @@
+# br-branches-default-octopus branches-default
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-default_branches-default b/t/t5515/fetch.br-branches-default_branches-default
new file mode 100644 (file)
index 0000000..4a2bf3c
--- /dev/null
@@ -0,0 +1,8 @@
+# br-branches-default branches-default
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-one b/t/t5515/fetch.br-branches-one
new file mode 100644 (file)
index 0000000..12ac8d2
--- /dev/null
@@ -0,0 +1,8 @@
+# br-branches-one
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-one-merge b/t/t5515/fetch.br-branches-one-merge
new file mode 100644 (file)
index 0000000..b4b3b35
--- /dev/null
@@ -0,0 +1,9 @@
+# br-branches-one-merge
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-one-merge_branches-one b/t/t5515/fetch.br-branches-one-merge_branches-one
new file mode 100644 (file)
index 0000000..2ecef38
--- /dev/null
@@ -0,0 +1,9 @@
+# br-branches-one-merge branches-one
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-one-octopus b/t/t5515/fetch.br-branches-one-octopus
new file mode 100644 (file)
index 0000000..96e3029
--- /dev/null
@@ -0,0 +1,9 @@
+# br-branches-one-octopus
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-one-octopus_branches-one b/t/t5515/fetch.br-branches-one-octopus_branches-one
new file mode 100644 (file)
index 0000000..55e0bad
--- /dev/null
@@ -0,0 +1,9 @@
+# br-branches-one-octopus branches-one
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-branches-one_branches-one b/t/t5515/fetch.br-branches-one_branches-one
new file mode 100644 (file)
index 0000000..281fa09
--- /dev/null
@@ -0,0 +1,8 @@
+# br-branches-one branches-one
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-explicit b/t/t5515/fetch.br-config-explicit
new file mode 100644 (file)
index 0000000..e2fa9c8
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-explicit-merge b/t/t5515/fetch.br-config-explicit-merge
new file mode 100644 (file)
index 0000000..ec1a723
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-explicit-merge
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-explicit-merge_config-explicit b/t/t5515/fetch.br-config-explicit-merge_config-explicit
new file mode 100644 (file)
index 0000000..54f6891
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-explicit-merge config-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-explicit-octopus b/t/t5515/fetch.br-config-explicit-octopus
new file mode 100644 (file)
index 0000000..7011dfc
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-explicit-octopus
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-explicit-octopus_config-explicit b/t/t5515/fetch.br-config-explicit-octopus_config-explicit
new file mode 100644 (file)
index 0000000..bdad51f
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-explicit-octopus config-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-explicit_config-explicit b/t/t5515/fetch.br-config-explicit_config-explicit
new file mode 100644 (file)
index 0000000..1b237dd
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-explicit config-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-glob b/t/t5515/fetch.br-config-glob
new file mode 100644 (file)
index 0000000..e75ec2f
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-glob-merge b/t/t5515/fetch.br-config-glob-merge
new file mode 100644 (file)
index 0000000..ce8f739
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-glob-merge
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-glob-merge_config-glob b/t/t5515/fetch.br-config-glob-merge_config-glob
new file mode 100644 (file)
index 0000000..5817bed
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-glob-merge config-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-glob-octopus b/t/t5515/fetch.br-config-glob-octopus
new file mode 100644 (file)
index 0000000..938e532
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-glob-octopus
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-glob-octopus_config-glob b/t/t5515/fetch.br-config-glob-octopus_config-glob
new file mode 100644 (file)
index 0000000..c9225bf
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-glob-octopus config-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-config-glob_config-glob b/t/t5515/fetch.br-config-glob_config-glob
new file mode 100644 (file)
index 0000000..a6c20f9
--- /dev/null
@@ -0,0 +1,11 @@
+# br-config-glob config-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-explicit b/t/t5515/fetch.br-remote-explicit
new file mode 100644 (file)
index 0000000..83534d2
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-explicit-merge b/t/t5515/fetch.br-remote-explicit-merge
new file mode 100644 (file)
index 0000000..a9064dd
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-explicit-merge
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-explicit-merge_remote-explicit b/t/t5515/fetch.br-remote-explicit-merge_remote-explicit
new file mode 100644 (file)
index 0000000..732a37e
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-explicit-merge remote-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-explicit-octopus b/t/t5515/fetch.br-remote-explicit-octopus
new file mode 100644 (file)
index 0000000..ecf020d
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-explicit-octopus
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-explicit-octopus_remote-explicit b/t/t5515/fetch.br-remote-explicit-octopus_remote-explicit
new file mode 100644 (file)
index 0000000..af77531
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-explicit-octopus remote-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-explicit_remote-explicit b/t/t5515/fetch.br-remote-explicit_remote-explicit
new file mode 100644 (file)
index 0000000..51fae56
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-explicit remote-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-glob b/t/t5515/fetch.br-remote-glob
new file mode 100644 (file)
index 0000000..94e6ad3
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-glob-merge b/t/t5515/fetch.br-remote-glob-merge
new file mode 100644 (file)
index 0000000..09362e2
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-glob-merge
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-glob-merge_remote-glob b/t/t5515/fetch.br-remote-glob-merge_remote-glob
new file mode 100644 (file)
index 0000000..e2eabec
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-glob-merge remote-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-glob-octopus b/t/t5515/fetch.br-remote-glob-octopus
new file mode 100644 (file)
index 0000000..b08e046
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-glob-octopus
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-glob-octopus_remote-glob b/t/t5515/fetch.br-remote-glob-octopus_remote-glob
new file mode 100644 (file)
index 0000000..d4d547c
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-glob-octopus remote-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-remote-glob_remote-glob b/t/t5515/fetch.br-remote-glob_remote-glob
new file mode 100644 (file)
index 0000000..646dbc8
--- /dev/null
@@ -0,0 +1,11 @@
+# br-remote-glob remote-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig b/t/t5515/fetch.br-unconfig
new file mode 100644 (file)
index 0000000..65ce6d9
--- /dev/null
@@ -0,0 +1,11 @@
+# br-unconfig
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_--tags_.._.git b/t/t5515/fetch.br-unconfig_--tags_.._.git
new file mode 100644 (file)
index 0000000..8258c80
--- /dev/null
@@ -0,0 +1,7 @@
+# br-unconfig --tags ../.git
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_.._.git b/t/t5515/fetch.br-unconfig_.._.git
new file mode 100644 (file)
index 0000000..284bb1f
--- /dev/null
@@ -0,0 +1,2 @@
+# br-unconfig ../.git
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               ../
diff --git a/t/t5515/fetch.br-unconfig_.._.git_one b/t/t5515/fetch.br-unconfig_.._.git_one
new file mode 100644 (file)
index 0000000..11eb5a6
--- /dev/null
@@ -0,0 +1,2 @@
+# br-unconfig ../.git one
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
diff --git a/t/t5515/fetch.br-unconfig_.._.git_one_tag_tag-one_tag_tag-three-file b/t/t5515/fetch.br-unconfig_.._.git_one_tag_tag-one_tag_tag-three-file
new file mode 100644 (file)
index 0000000..f02bab2
--- /dev/null
@@ -0,0 +1,8 @@
+# br-unconfig ../.git one tag tag-one tag tag-three-file
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               tag 'tag-one' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_.._.git_one_two b/t/t5515/fetch.br-unconfig_.._.git_one_two
new file mode 100644 (file)
index 0000000..3f1be22
--- /dev/null
@@ -0,0 +1,3 @@
+# br-unconfig ../.git one two
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
diff --git a/t/t5515/fetch.br-unconfig_.._.git_tag_tag-one-tree_tag_tag-three-file b/t/t5515/fetch.br-unconfig_.._.git_tag_tag-one-tree_tag_tag-three-file
new file mode 100644 (file)
index 0000000..85de411
--- /dev/null
@@ -0,0 +1,7 @@
+# br-unconfig ../.git tag tag-one-tree tag tag-three-file
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_.._.git_tag_tag-one_tag_tag-three b/t/t5515/fetch.br-unconfig_.._.git_tag_tag-one_tag_tag-three
new file mode 100644 (file)
index 0000000..0da2337
--- /dev/null
@@ -0,0 +1,7 @@
+# br-unconfig ../.git tag tag-one tag tag-three
+8e32a6d901327a23ef831511badce7bf3bf46689               tag 'tag-one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               tag 'tag-three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_branches-default b/t/t5515/fetch.br-unconfig_branches-default
new file mode 100644 (file)
index 0000000..fc7041e
--- /dev/null
@@ -0,0 +1,8 @@
+# br-unconfig branches-default
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_branches-one b/t/t5515/fetch.br-unconfig_branches-one
new file mode 100644 (file)
index 0000000..e94cde7
--- /dev/null
@@ -0,0 +1,8 @@
+# br-unconfig branches-one
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_config-explicit b/t/t5515/fetch.br-unconfig_config-explicit
new file mode 100644 (file)
index 0000000..01a283e
--- /dev/null
@@ -0,0 +1,11 @@
+# br-unconfig config-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_config-glob b/t/t5515/fetch.br-unconfig_config-glob
new file mode 100644 (file)
index 0000000..3a556c5
--- /dev/null
@@ -0,0 +1,11 @@
+# br-unconfig config-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_remote-explicit b/t/t5515/fetch.br-unconfig_remote-explicit
new file mode 100644 (file)
index 0000000..db216df
--- /dev/null
@@ -0,0 +1,11 @@
+# br-unconfig remote-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.br-unconfig_remote-glob b/t/t5515/fetch.br-unconfig_remote-glob
new file mode 100644 (file)
index 0000000..aee65c2
--- /dev/null
@@ -0,0 +1,11 @@
+# br-unconfig remote-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master b/t/t5515/fetch.master
new file mode 100644 (file)
index 0000000..950fd07
--- /dev/null
@@ -0,0 +1,11 @@
+# master
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_--tags_.._.git b/t/t5515/fetch.master_--tags_.._.git
new file mode 100644 (file)
index 0000000..0e59950
--- /dev/null
@@ -0,0 +1,7 @@
+# master --tags ../.git
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_.._.git b/t/t5515/fetch.master_.._.git
new file mode 100644 (file)
index 0000000..66d1aad
--- /dev/null
@@ -0,0 +1,2 @@
+# master ../.git
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               ../
diff --git a/t/t5515/fetch.master_.._.git_one b/t/t5515/fetch.master_.._.git_one
new file mode 100644 (file)
index 0000000..35deddb
--- /dev/null
@@ -0,0 +1,2 @@
+# master ../.git one
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
diff --git a/t/t5515/fetch.master_.._.git_one_tag_tag-one_tag_tag-three-file b/t/t5515/fetch.master_.._.git_one_tag_tag-one_tag_tag-three-file
new file mode 100644 (file)
index 0000000..8286852
--- /dev/null
@@ -0,0 +1,8 @@
+# master ../.git one tag tag-one tag tag-three-file
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689               tag 'tag-one' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_.._.git_one_two b/t/t5515/fetch.master_.._.git_one_two
new file mode 100644 (file)
index 0000000..35ec578
--- /dev/null
@@ -0,0 +1,3 @@
+# master ../.git one two
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8               branch 'two' of ../
diff --git a/t/t5515/fetch.master_.._.git_tag_tag-one-tree_tag_tag-three-file b/t/t5515/fetch.master_.._.git_tag_tag-one-tree_tag_tag-three-file
new file mode 100644 (file)
index 0000000..2e133ef
--- /dev/null
@@ -0,0 +1,7 @@
+# master ../.git tag tag-one-tree tag tag-three-file
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_.._.git_tag_tag-one_tag_tag-three b/t/t5515/fetch.master_.._.git_tag_tag-one_tag_tag-three
new file mode 100644 (file)
index 0000000..92b18b4
--- /dev/null
@@ -0,0 +1,7 @@
+# master ../.git tag tag-one tag tag-three
+8e32a6d901327a23ef831511badce7bf3bf46689               tag 'tag-one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b               tag 'tag-three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_branches-default b/t/t5515/fetch.master_branches-default
new file mode 100644 (file)
index 0000000..603d6d2
--- /dev/null
@@ -0,0 +1,8 @@
+# master branches-default
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_branches-one b/t/t5515/fetch.master_branches-one
new file mode 100644 (file)
index 0000000..fe9bb0b
--- /dev/null
@@ -0,0 +1,8 @@
+# master branches-one
+8e32a6d901327a23ef831511badce7bf3bf46689               branch 'one' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_config-explicit b/t/t5515/fetch.master_config-explicit
new file mode 100644 (file)
index 0000000..4be97c7
--- /dev/null
@@ -0,0 +1,11 @@
+# master config-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_config-glob b/t/t5515/fetch.master_config-glob
new file mode 100644 (file)
index 0000000..cb0726f
--- /dev/null
@@ -0,0 +1,11 @@
+# master config-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_remote-explicit b/t/t5515/fetch.master_remote-explicit
new file mode 100644 (file)
index 0000000..44a1ca8
--- /dev/null
@@ -0,0 +1,11 @@
+# master remote-explicit
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f               branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5515/fetch.master_remote-glob b/t/t5515/fetch.master_remote-glob
new file mode 100644 (file)
index 0000000..724e8db
--- /dev/null
@@ -0,0 +1,11 @@
+# master remote-glob
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   branch 'master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   branch 'one' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   branch 'three' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   branch 'two' of ../
+754b754407bf032e9a2f9d5a9ad05ca79a6b228f       not-for-merge   tag 'tag-master' of ../
+8e32a6d901327a23ef831511badce7bf3bf46689       not-for-merge   tag 'tag-one' of ../
+22feea448b023a2d864ef94b013735af34d238ba       not-for-merge   tag 'tag-one-tree' of ../
+0567da4d5edd2ff4bb292a465ba9e64dcad9536b       not-for-merge   tag 'tag-three' of ../
+0e3b14047d3ee365f4f2a1b673db059c3972589c       not-for-merge   tag 'tag-three-file' of ../
+6134ee8f857693b96ff1cc98d3e2fd62b199e5a8       not-for-merge   tag 'tag-two' of ../
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
new file mode 100755 (executable)
index 0000000..9d2dc33
--- /dev/null
@@ -0,0 +1,323 @@
+#!/bin/sh
+
+test_description='fetching and pushing, with or without wildcard'
+
+. ./test-lib.sh
+
+D=`pwd`
+
+mk_empty () {
+       rm -fr testrepo &&
+       mkdir testrepo &&
+       (
+               cd testrepo &&
+               git init &&
+               mv .git/hooks .git/hooks-disabled
+       )
+}
+
+mk_test () {
+       mk_empty &&
+       (
+               for ref in "$@"
+               do
+                       git push testrepo $the_first_commit:refs/$ref || {
+                               echo "Oops, push refs/$ref failure"
+                               exit 1
+                       }
+               done &&
+               cd testrepo &&
+               for ref in "$@"
+               do
+                       r=$(git show-ref -s --verify refs/$ref) &&
+                       test "z$r" = "z$the_first_commit" || {
+                               echo "Oops, refs/$ref is wrong"
+                               exit 1
+                       }
+               done &&
+               git fsck --full
+       )
+}
+
+check_push_result () {
+       (
+               cd testrepo &&
+               it="$1" &&
+               shift
+               for ref in "$@"
+               do
+                       r=$(git show-ref -s --verify refs/$ref) &&
+                       test "z$r" = "z$it" || {
+                               echo "Oops, refs/$ref is wrong"
+                               exit 1
+                       }
+               done &&
+               git fsck --full
+       )
+}
+
+test_expect_success setup '
+
+       : >path1 &&
+       git add path1 &&
+       test_tick &&
+       git commit -a -m repo &&
+       the_first_commit=$(git show-ref -s --verify refs/heads/master) &&
+
+       : >path2 &&
+       git add path2 &&
+       test_tick &&
+       git commit -a -m second &&
+       the_commit=$(git show-ref -s --verify refs/heads/master)
+
+'
+
+test_expect_success 'fetch without wildcard' '
+       mk_empty &&
+       (
+               cd testrepo &&
+               git fetch .. refs/heads/master:refs/remotes/origin/master &&
+
+               r=$(git show-ref -s --verify refs/remotes/origin/master) &&
+               test "z$r" = "z$the_commit" &&
+
+               test 1 = $(git for-each-ref refs/remotes/origin | wc -l)
+       )
+'
+
+test_expect_success 'fetch with wildcard' '
+       mk_empty &&
+       (
+               cd testrepo &&
+               git config remote.up.url .. &&
+               git config remote.up.fetch "refs/heads/*:refs/remotes/origin/*" &&
+               git fetch up &&
+
+               r=$(git show-ref -s --verify refs/remotes/origin/master) &&
+               test "z$r" = "z$the_commit" &&
+
+               test 1 = $(git for-each-ref refs/remotes/origin | wc -l)
+       )
+'
+
+test_expect_success 'push without wildcard' '
+       mk_empty &&
+
+       git push testrepo refs/heads/master:refs/remotes/origin/master &&
+       (
+               cd testrepo &&
+               r=$(git show-ref -s --verify refs/remotes/origin/master) &&
+               test "z$r" = "z$the_commit" &&
+
+               test 1 = $(git for-each-ref refs/remotes/origin | wc -l)
+       )
+'
+
+test_expect_success 'push with wildcard' '
+       mk_empty &&
+
+       git push testrepo "refs/heads/*:refs/remotes/origin/*" &&
+       (
+               cd testrepo &&
+               r=$(git show-ref -s --verify refs/remotes/origin/master) &&
+               test "z$r" = "z$the_commit" &&
+
+               test 1 = $(git for-each-ref refs/remotes/origin | wc -l)
+       )
+'
+
+test_expect_success 'push with matching heads' '
+
+       mk_test heads/master &&
+       git push testrepo &&
+       check_push_result $the_commit heads/master
+
+'
+
+test_expect_success 'push with no ambiguity (1)' '
+
+       mk_test heads/master &&
+       git push testrepo master:master &&
+       check_push_result $the_commit heads/master
+
+'
+
+test_expect_success 'push with no ambiguity (2)' '
+
+       mk_test remotes/origin/master &&
+       git push testrepo master:origin/master &&
+       check_push_result $the_commit remotes/origin/master
+
+'
+
+test_expect_success 'push with colon-less refspec, no ambiguity' '
+
+       mk_test heads/master heads/t/master &&
+       git branch -f t/master master &&
+       git push testrepo master &&
+       check_push_result $the_commit heads/master &&
+       check_push_result $the_first_commit heads/t/master
+
+'
+
+test_expect_success 'push with weak ambiguity (1)' '
+
+       mk_test heads/master remotes/origin/master &&
+       git push testrepo master:master &&
+       check_push_result $the_commit heads/master &&
+       check_push_result $the_first_commit remotes/origin/master
+
+'
+
+test_expect_success 'push with weak ambiguity (2)' '
+
+       mk_test heads/master remotes/origin/master remotes/another/master &&
+       git push testrepo master:master &&
+       check_push_result $the_commit heads/master &&
+       check_push_result $the_first_commit remotes/origin/master remotes/another/master
+
+'
+
+test_expect_success 'push with ambiguity (1)' '
+
+       mk_test remotes/origin/master remotes/frotz/master &&
+       if git push testrepo master:master
+       then
+               echo "Oops, should have failed"
+               false
+       else
+               check_push_result $the_first_commit remotes/origin/master remotes/frotz/master
+       fi
+'
+
+test_expect_success 'push with ambiguity (2)' '
+
+       mk_test heads/frotz tags/frotz &&
+       if git push testrepo master:frotz
+       then
+               echo "Oops, should have failed"
+               false
+       else
+               check_push_result $the_first_commit heads/frotz tags/frotz
+       fi
+
+'
+
+test_expect_success 'push with colon-less refspec (1)' '
+
+       mk_test heads/frotz tags/frotz &&
+       git branch -f frotz master &&
+       git push testrepo frotz &&
+       check_push_result $the_commit heads/frotz &&
+       check_push_result $the_first_commit tags/frotz
+
+'
+
+test_expect_success 'push with colon-less refspec (2)' '
+
+       mk_test heads/frotz tags/frotz &&
+       if git show-ref --verify -q refs/heads/frotz
+       then
+               git branch -D frotz
+       fi &&
+       git tag -f frotz &&
+       git push testrepo frotz &&
+       check_push_result $the_commit tags/frotz &&
+       check_push_result $the_first_commit heads/frotz
+
+'
+
+test_expect_success 'push with colon-less refspec (3)' '
+
+       mk_test &&
+       if git show-ref --verify -q refs/tags/frotz
+       then
+               git tag -d frotz
+       fi &&
+       git branch -f frotz master &&
+       git push testrepo frotz &&
+       check_push_result $the_commit heads/frotz &&
+       test 1 = $( cd testrepo && git show-ref | wc -l )
+'
+
+test_expect_success 'push with colon-less refspec (4)' '
+
+       mk_test &&
+       if git show-ref --verify -q refs/heads/frotz
+       then
+               git branch -D frotz
+       fi &&
+       git tag -f frotz &&
+       git push testrepo frotz &&
+       check_push_result $the_commit tags/frotz &&
+       test 1 = $( cd testrepo && git show-ref | wc -l )
+
+'
+
+test_expect_success 'push with HEAD' '
+
+       mk_test heads/master &&
+       git checkout master &&
+       git push testrepo HEAD &&
+       check_push_result $the_commit heads/master
+
+'
+
+test_expect_success 'push with HEAD nonexisting at remote' '
+
+       mk_test heads/master &&
+       git checkout -b local master &&
+       git push testrepo HEAD &&
+       check_push_result $the_commit heads/local
+'
+
+test_expect_success 'push with dry-run' '
+
+       mk_test heads/master &&
+       (cd testrepo &&
+        old_commit=$(git show-ref -s --verify refs/heads/master)) &&
+       git push --dry-run testrepo &&
+       check_push_result $old_commit heads/master
+'
+
+test_expect_success 'push updates local refs' '
+
+       rm -rf parent child &&
+       mkdir parent &&
+       (cd parent && git init &&
+               echo one >foo && git add foo && git commit -m one) &&
+       git clone parent child &&
+       (cd child &&
+               echo two >foo && git commit -a -m two &&
+               git push &&
+       test $(git rev-parse master) = $(git rev-parse remotes/origin/master))
+
+'
+
+test_expect_success 'push does not update local refs on failure' '
+
+       rm -rf parent child &&
+       mkdir parent &&
+       (cd parent && git init &&
+               echo one >foo && git add foo && git commit -m one &&
+               echo exit 1 >.git/hooks/pre-receive &&
+               chmod +x .git/hooks/pre-receive) &&
+       git clone parent child &&
+       (cd child &&
+               echo two >foo && git commit -a -m two &&
+               ! git push &&
+               test $(git rev-parse master) != \
+                       $(git rev-parse remotes/origin/master))
+
+'
+
+test_expect_success 'allow deleting an invalid remote ref' '
+
+       pwd &&
+       rm -f testrepo/.git/objects/??/* &&
+       git push testrepo :refs/heads/master &&
+       (cd testrepo && ! git rev-parse --verify refs/heads/master)
+
+'
+
+test_done
diff --git a/t/t5517-push-mirror.sh b/t/t5517-push-mirror.sh
new file mode 100755 (executable)
index 0000000..ed3fec1
--- /dev/null
@@ -0,0 +1,228 @@
+#!/bin/sh
+
+test_description='pushing to a mirror repository'
+
+. ./test-lib.sh
+
+D=`pwd`
+
+invert () {
+       if "$@"; then
+               return 1
+       else
+               return 0
+       fi
+}
+
+mk_repo_pair () {
+       rm -rf master mirror &&
+       mkdir mirror &&
+       (
+               cd mirror &&
+               git init
+       ) &&
+       mkdir master &&
+       (
+               cd master &&
+               git init &&
+               git config remote.up.url ../mirror
+       )
+}
+
+
+# BRANCH tests
+test_expect_success 'push mirror creates new branches' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git push --mirror up
+       ) &&
+       master_master=$(cd master && git show-ref -s --verify refs/heads/master) &&
+       mirror_master=$(cd mirror && git show-ref -s --verify refs/heads/master) &&
+       test "$master_master" = "$mirror_master"
+
+'
+
+test_expect_success 'push mirror updates existing branches' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git push --mirror up &&
+               echo two >foo && git add foo && git commit -m two &&
+               git push --mirror up
+       ) &&
+       master_master=$(cd master && git show-ref -s --verify refs/heads/master) &&
+       mirror_master=$(cd mirror && git show-ref -s --verify refs/heads/master) &&
+       test "$master_master" = "$mirror_master"
+
+'
+
+test_expect_success 'push mirror force updates existing branches' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git push --mirror up &&
+               echo two >foo && git add foo && git commit -m two &&
+               git push --mirror up &&
+               git reset --hard HEAD^
+               git push --mirror up
+       ) &&
+       master_master=$(cd master && git show-ref -s --verify refs/heads/master) &&
+       mirror_master=$(cd mirror && git show-ref -s --verify refs/heads/master) &&
+       test "$master_master" = "$mirror_master"
+
+'
+
+test_expect_success 'push mirror removes branches' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git branch remove master &&
+               git push --mirror up &&
+               git branch -D remove
+               git push --mirror up
+       ) &&
+       (
+               cd mirror &&
+               invert git show-ref -s --verify refs/heads/remove
+       )
+
+'
+
+test_expect_success 'push mirror adds, updates and removes branches together' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git branch remove master &&
+               git push --mirror up &&
+               git branch -D remove &&
+               git branch add master &&
+               echo two >foo && git add foo && git commit -m two &&
+               git push --mirror up
+       ) &&
+       master_master=$(cd master && git show-ref -s --verify refs/heads/master) &&
+       master_add=$(cd master && git show-ref -s --verify refs/heads/add) &&
+       mirror_master=$(cd mirror && git show-ref -s --verify refs/heads/master) &&
+       mirror_add=$(cd mirror && git show-ref -s --verify refs/heads/add) &&
+       test "$master_master" = "$mirror_master" &&
+       test "$master_add" = "$mirror_add" &&
+       (
+               cd mirror &&
+               invert git show-ref -s --verify refs/heads/remove
+       )
+
+'
+
+
+# TAG tests
+test_expect_success 'push mirror creates new tags' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git tag -f tmaster master &&
+               git push --mirror up
+       ) &&
+       master_master=$(cd master && git show-ref -s --verify refs/tags/tmaster) &&
+       mirror_master=$(cd mirror && git show-ref -s --verify refs/tags/tmaster) &&
+       test "$master_master" = "$mirror_master"
+
+'
+
+test_expect_success 'push mirror updates existing tags' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git tag -f tmaster master &&
+               git push --mirror up &&
+               echo two >foo && git add foo && git commit -m two &&
+               git tag -f tmaster master &&
+               git push --mirror up
+       ) &&
+       master_master=$(cd master && git show-ref -s --verify refs/tags/tmaster) &&
+       mirror_master=$(cd mirror && git show-ref -s --verify refs/tags/tmaster) &&
+       test "$master_master" = "$mirror_master"
+
+'
+
+test_expect_success 'push mirror force updates existing tags' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git tag -f tmaster master &&
+               git push --mirror up &&
+               echo two >foo && git add foo && git commit -m two &&
+               git tag -f tmaster master &&
+               git push --mirror up &&
+               git reset --hard HEAD^
+               git tag -f tmaster master &&
+               git push --mirror up
+       ) &&
+       master_master=$(cd master && git show-ref -s --verify refs/tags/tmaster) &&
+       mirror_master=$(cd mirror && git show-ref -s --verify refs/tags/tmaster) &&
+       test "$master_master" = "$mirror_master"
+
+'
+
+test_expect_success 'push mirror removes tags' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git tag -f tremove master &&
+               git push --mirror up &&
+               git tag -d tremove
+               git push --mirror up
+       ) &&
+       (
+               cd mirror &&
+               invert git show-ref -s --verify refs/tags/tremove
+       )
+
+'
+
+test_expect_success 'push mirror adds, updates and removes tags together' '
+
+       mk_repo_pair &&
+       (
+               cd master &&
+               echo one >foo && git add foo && git commit -m one &&
+               git tag -f tmaster master &&
+               git tag -f tremove master &&
+               git push --mirror up &&
+               git tag -d tremove &&
+               git tag tadd master &&
+               echo two >foo && git add foo && git commit -m two &&
+               git tag -f tmaster master &&
+               git push --mirror up
+       ) &&
+       master_master=$(cd master && git show-ref -s --verify refs/tags/tmaster) &&
+       master_add=$(cd master && git show-ref -s --verify refs/tags/tadd) &&
+       mirror_master=$(cd mirror && git show-ref -s --verify refs/tags/tmaster) &&
+       mirror_add=$(cd mirror && git show-ref -s --verify refs/tags/tadd) &&
+       test "$master_master" = "$mirror_master" &&
+       test "$master_add" = "$mirror_add" &&
+       (
+               cd mirror &&
+               invert git show-ref -s --verify refs/tags/tremove
+       )
+
+'
+
+test_done
diff --git a/t/t5520-pull.sh b/t/t5520-pull.sh
new file mode 100755 (executable)
index 0000000..52b3a0c
--- /dev/null
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+test_description='pulling into void'
+
+. ./test-lib.sh
+
+D=`pwd`
+
+test_expect_success setup '
+
+       echo file >file &&
+       git add file &&
+       git commit -a -m original
+
+'
+
+test_expect_success 'pulling into void' '
+       mkdir cloned &&
+       cd cloned &&
+       git init &&
+       git pull ..
+'
+
+cd "$D"
+
+test_expect_success 'checking the results' '
+       test -f file &&
+       test -f cloned/file &&
+       diff file cloned/file
+'
+
+test_expect_success 'test . as a remote' '
+
+       git branch copy master &&
+       git config branch.copy.remote . &&
+       git config branch.copy.merge refs/heads/master &&
+       echo updated >file &&
+       git commit -a -m updated &&
+       git checkout copy &&
+       test `cat file` = file &&
+       git pull &&
+       test `cat file` = updated
+'
+
+test_expect_success 'the default remote . should not break explicit pull' '
+       git checkout -b second master^ &&
+       echo modified >file &&
+       git commit -a -m modified &&
+       git checkout copy &&
+       git reset --hard HEAD^ &&
+       test `cat file` = file &&
+       git pull . second &&
+       test `cat file` = modified
+'
+
+test_expect_success '--rebase' '
+       git branch to-rebase &&
+       echo modified again > file &&
+       git commit -m file file &&
+       git checkout to-rebase &&
+       echo new > file2 &&
+       git add file2 &&
+       git commit -m "new file" &&
+       git tag before-rebase &&
+       git pull --rebase . copy &&
+       test $(git rev-parse HEAD^) = $(git rev-parse copy) &&
+       test new = $(git show HEAD:file2)
+'
+
+test_expect_success 'branch.to-rebase.rebase' '
+       git reset --hard before-rebase &&
+       git config branch.to-rebase.rebase 1 &&
+       git pull . copy &&
+       test $(git rev-parse HEAD^) = $(git rev-parse copy) &&
+       test new = $(git show HEAD:file2)
+'
+
+test_done
diff --git a/t/t5530-upload-pack-error.sh b/t/t5530-upload-pack-error.sh
new file mode 100755 (executable)
index 0000000..cc8949e
--- /dev/null
@@ -0,0 +1,75 @@
+#!/bin/sh
+
+test_description='errors in upload-pack'
+
+. ./test-lib.sh
+
+D=`pwd`
+
+corrupt_repo () {
+       object_sha1=$(git rev-parse "$1") &&
+       ob=$(expr "$object_sha1" : "\(..\)") &&
+       ject=$(expr "$object_sha1" : "..\(..*\)") &&
+       rm -f ".git/objects/$ob/$ject"
+}
+
+test_expect_success 'setup and corrupt repository' '
+
+       echo file >file &&
+       git add file &&
+       git rev-parse :file &&
+       git commit -a -m original &&
+       test_tick &&
+       echo changed >file &&
+       git commit -a -m changed &&
+       corrupt_repo HEAD:file
+
+'
+
+test_expect_failure 'fsck fails' '
+
+       git fsck
+'
+
+test_expect_success 'upload-pack fails due to error in pack-objects' '
+
+       ! echo "0032want $(git rev-parse HEAD)
+00000009done
+0000" | git-upload-pack . > /dev/null 2> output.err &&
+       grep "pack-objects died" output.err
+'
+
+test_expect_success 'corrupt repo differently' '
+
+       git hash-object -w file &&
+       corrupt_repo HEAD^^{tree}
+
+'
+
+test_expect_failure 'fsck fails' '
+
+       git fsck
+'
+test_expect_success 'upload-pack fails due to error in rev-list' '
+
+       ! echo "0032want $(git rev-parse HEAD)
+00000009done
+0000" | git-upload-pack . > /dev/null 2> output.err &&
+       grep "waitpid (async) failed" output.err
+'
+
+test_expect_success 'create empty repository' '
+
+       mkdir foo &&
+       cd foo &&
+       git init
+
+'
+
+test_expect_failure 'fetch fails' '
+
+       git fetch .. master
+
+'
+
+test_done
diff --git a/t/t5600-clone-fail-cleanup.sh b/t/t5600-clone-fail-cleanup.sh
new file mode 100755 (executable)
index 0000000..1776b37
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/sh
+#
+# Copyright (C) 2006 Carl D. Worth <cworth@cworth.org>
+#
+
+test_description='test git-clone to cleanup after failure
+
+This test covers the fact that if git-clone fails, it should remove
+the directory it created, to avoid the user having to manually
+remove the directory before attempting a clone again.'
+
+. ./test-lib.sh
+
+test_expect_failure \
+    'clone of non-existent source should fail' \
+    'git-clone foo bar'
+
+test_expect_failure \
+    'failed clone should not leave a directory' \
+    'cd bar'
+
+# Need a repo to clone
+test_create_repo foo
+
+# clone doesn't like it if there is no HEAD. Is that a bug?
+(cd foo && touch file && git add file && git commit -m 'add file' >/dev/null 2>&1)
+
+# source repository given to git-clone should be relative to the
+# current path not to the target dir
+test_expect_failure \
+    'clone of non-existent (relative to $PWD) source should fail' \
+    'git-clone ../foo baz'
+
+test_expect_success \
+    'clone should work now that source exists' \
+    'git-clone foo bar'
+
+test_expect_success \
+    'successful clone must leave the directory' \
+    'cd bar'
+
+test_done
diff --git a/t/t5700-clone-reference.sh b/t/t5700-clone-reference.sh
new file mode 100755 (executable)
index 0000000..b6a5486
--- /dev/null
@@ -0,0 +1,116 @@
+#!/bin/sh
+#
+# Copyright (C) 2006 Martin Waitz <tali@admingilde.org>
+#
+
+test_description='test clone --reference'
+. ./test-lib.sh
+
+base_dir=`pwd`
+
+test_expect_success 'preparing first repository' \
+'test_create_repo A && cd A &&
+echo first > file1 &&
+git add file1 &&
+git commit -m initial'
+
+cd "$base_dir"
+
+test_expect_success 'preparing second repository' \
+'git clone A B && cd B &&
+echo second > file2 &&
+git add file2 &&
+git commit -m addition &&
+git repack -a -d &&
+git prune'
+
+cd "$base_dir"
+
+test_expect_success 'cloning with reference (-l -s)' \
+'git clone -l -s --reference B A C'
+
+cd "$base_dir"
+
+test_expect_success 'existence of info/alternates' \
+'test `wc -l <C/.git/objects/info/alternates` = 2'
+
+cd "$base_dir"
+
+test_expect_success 'pulling from reference' \
+'cd C &&
+git pull ../B master'
+
+cd "$base_dir"
+
+test_expect_success 'that reference gets used' \
+'cd C &&
+echo "0 objects, 0 kilobytes" > expected &&
+git count-objects > current &&
+diff expected current'
+
+cd "$base_dir"
+
+test_expect_success 'cloning with reference (no -l -s)' \
+'git clone --reference B file://`pwd`/A D'
+
+cd "$base_dir"
+
+test_expect_success 'existence of info/alternates' \
+'test `wc -l <D/.git/objects/info/alternates` = 1'
+
+cd "$base_dir"
+
+test_expect_success 'pulling from reference' \
+'cd D && git pull ../B master'
+
+cd "$base_dir"
+
+test_expect_success 'that reference gets used' \
+'cd D && echo "0 objects, 0 kilobytes" > expected &&
+git count-objects > current &&
+diff expected current'
+
+cd "$base_dir"
+
+test_expect_success 'updating origin' \
+'cd A &&
+echo third > file3 &&
+git add file3 &&
+git commit -m update &&
+git repack -a -d &&
+git prune'
+
+cd "$base_dir"
+
+test_expect_success 'pulling changes from origin' \
+'cd C &&
+git pull origin'
+
+cd "$base_dir"
+
+# the 2 local objects are commit and tree from the merge
+test_expect_success 'that alternate to origin gets used' \
+'cd C &&
+echo "2 objects" > expected &&
+git count-objects | cut -d, -f1 > current &&
+diff expected current'
+
+cd "$base_dir"
+
+test_expect_success 'pulling changes from origin' \
+'cd D &&
+git pull origin'
+
+cd "$base_dir"
+
+# the 5 local objects are expected; file3 blob, commit in A to add it
+# and its tree, and 2 are our tree and the merge commit.
+test_expect_success 'check objects expected to exist locally' \
+'cd D &&
+echo "5 objects" > expected &&
+git count-objects | cut -d, -f1 > current &&
+diff expected current'
+
+cd "$base_dir"
+
+test_done
diff --git a/t/t5701-clone-local.sh b/t/t5701-clone-local.sh
new file mode 100755 (executable)
index 0000000..822ac8c
--- /dev/null
@@ -0,0 +1,66 @@
+#!/bin/sh
+
+test_description='test local clone'
+. ./test-lib.sh
+
+D=`pwd`
+
+test_expect_success 'preparing origin repository' '
+       : >file && git add . && git commit -m1 &&
+       git clone --bare . a.git &&
+       git clone --bare . x &&
+       test "$(GIT_CONFIG=a.git/config git config --bool core.bare)" = true &&
+       test "$(GIT_CONFIG=x/config git config --bool core.bare)" = true
+'
+
+test_expect_success 'local clone without .git suffix' '
+       cd "$D" &&
+       git clone -l -s a b &&
+       cd b &&
+       test "$(GIT_CONFIG=.git/config git config --bool core.bare)" = false &&
+       git fetch
+'
+
+test_expect_success 'local clone with .git suffix' '
+       cd "$D" &&
+       git clone -l -s a.git c &&
+       cd c &&
+       git fetch
+'
+
+test_expect_success 'local clone from x' '
+       cd "$D" &&
+       git clone -l -s x y &&
+       cd y &&
+       git fetch
+'
+
+test_expect_success 'local clone from x.git that does not exist' '
+       cd "$D" &&
+       if git clone -l -s x.git z
+       then
+               echo "Oops, should have failed"
+               false
+       else
+               echo happy
+       fi
+'
+
+test_expect_success 'With -no-hardlinks, local will make a copy' '
+       cd "$D" &&
+       git clone --bare --no-hardlinks x w &&
+       cd w &&
+       linked=$(find objects -type f ! -links 1 | wc -l) &&
+       test 0 = $linked
+'
+
+test_expect_success 'Even without -l, local will make a hardlink' '
+       cd "$D" &&
+       rm -fr w &&
+       git clone -l --bare x w &&
+       cd w &&
+       copied=$(find objects -type f -links 1 | wc -l) &&
+       test 0 = $copied
+'
+
+test_done
diff --git a/t/t5710-info-alternate.sh b/t/t5710-info-alternate.sh
new file mode 100755 (executable)
index 0000000..699df6e
--- /dev/null
@@ -0,0 +1,106 @@
+#!/bin/sh
+#
+# Copyright (C) 2006 Martin Waitz <tali@admingilde.org>
+#
+
+test_description='test transitive info/alternate entries'
+. ./test-lib.sh
+
+# test that a file is not reachable in the current repository
+# but that it is after creating a info/alternate entry
+reachable_via() {
+       alternate="$1"
+       file="$2"
+       if git cat-file -e "HEAD:$file"; then return 1; fi
+       echo "$alternate" >> .git/objects/info/alternate
+       git cat-file -e "HEAD:$file"
+}
+
+test_valid_repo() {
+       git fsck --full > fsck.log &&
+       test `wc -l < fsck.log` = 0
+}
+
+base_dir=`pwd`
+
+test_expect_success 'preparing first repository' \
+'test_create_repo A && cd A &&
+echo "Hello World" > file1 &&
+git add file1 &&
+git commit -m "Initial commit" file1 &&
+git repack -a -d &&
+git prune'
+
+cd "$base_dir"
+
+test_expect_success 'preparing second repository' \
+'git clone -l -s A B && cd B &&
+echo "foo bar" > file2 &&
+git add file2 &&
+git commit -m "next commit" file2 &&
+git repack -a -d -l &&
+git prune'
+
+cd "$base_dir"
+
+test_expect_success 'preparing third repository' \
+'git clone -l -s B C && cd C &&
+echo "Goodbye, cruel world" > file3 &&
+git add file3 &&
+git commit -m "one more" file3 &&
+git repack -a -d -l &&
+git prune'
+
+cd "$base_dir"
+
+test_expect_failure 'creating too deep nesting' \
+'git clone -l -s C D &&
+git clone -l -s D E &&
+git clone -l -s E F &&
+git clone -l -s F G &&
+git clone -l -s G H &&
+cd H &&
+test_valid_repo'
+
+cd "$base_dir"
+
+test_expect_success 'validity of third repository' \
+'cd C &&
+test_valid_repo'
+
+cd "$base_dir"
+
+test_expect_success 'validity of fourth repository' \
+'cd D &&
+test_valid_repo'
+
+cd "$base_dir"
+
+test_expect_success 'breaking of loops' \
+"echo '$base_dir/B/.git/objects' >> '$base_dir'/A/.git/objects/info/alternates&&
+cd C &&
+test_valid_repo"
+
+cd "$base_dir"
+
+test_expect_failure 'that info/alternates is necessary' \
+'cd C &&
+rm .git/objects/info/alternates &&
+test_valid_repo'
+
+cd "$base_dir"
+
+test_expect_success 'that relative alternate is possible for current dir' \
+'cd C &&
+echo "../../../B/.git/objects" > .git/objects/info/alternates &&
+test_valid_repo'
+
+cd "$base_dir"
+
+test_expect_failure 'that relative alternate is only possible for current dir' \
+'cd D &&
+test_valid_repo'
+
+cd "$base_dir"
+
+test_done
diff --git a/t/t6000lib.sh b/t/t6000lib.sh
new file mode 100755 (executable)
index 0000000..180633e
--- /dev/null
@@ -0,0 +1,116 @@
+[ -d .git/refs/tags ] || mkdir -p .git/refs/tags
+
+:> sed.script
+
+# Answer the sha1 has associated with the tag. The tag must exist in .git or .git/refs/tags
+tag()
+{
+       _tag=$1
+       [ -f .git/refs/tags/$_tag ] || error "tag: \"$_tag\" does not exist"
+       cat .git/refs/tags/$_tag
+}
+
+# Generate a commit using the text specified to make it unique and the tree
+# named by the tag specified.
+unique_commit()
+{
+       _text=$1
+        _tree=$2
+       shift 2
+       echo $_text | git commit-tree $(tag $_tree) "$@"
+}
+
+# Save the output of a command into the tag specified. Prepend
+# a substitution script for the tag onto the front of sed.script
+save_tag()
+{
+       _tag=$1
+       [ -n "$_tag" ] || error "usage: save_tag tag commit-args ..."
+       shift 1
+       "$@" >.git/refs/tags/$_tag
+
+        echo "s/$(tag $_tag)/$_tag/g" > sed.script.tmp
+       cat sed.script >> sed.script.tmp
+       rm sed.script
+       mv sed.script.tmp sed.script
+}
+
+# Replace unhelpful sha1 hashses with their symbolic equivalents
+entag()
+{
+       sed -f sed.script
+}
+
+# Execute a command after first saving, then setting the GIT_AUTHOR_EMAIL
+# tag to a specified value. Restore the original value on return.
+as_author()
+{
+       _author=$1
+       shift 1
+        _save=$GIT_AUTHOR_EMAIL
+
+       export GIT_AUTHOR_EMAIL="$_author"
+       "$@"
+       if test -z "$_save"
+       then
+               unset GIT_AUTHOR_EMAIL
+       else
+               export GIT_AUTHOR_EMAIL="$_save"
+       fi
+}
+
+commit_date()
+{
+        _commit=$1
+       git cat-file commit $_commit | sed -n "s/^committer .*> \([0-9]*\) .*/\1/p"
+}
+
+on_committer_date()
+{
+    _date=$1
+    shift 1
+    export GIT_COMMITTER_DATE="$_date"
+    "$@"
+    unset GIT_COMMITTER_DATE
+}
+
+# Execute a command and suppress any error output.
+hide_error()
+{
+       "$@" 2>/dev/null
+}
+
+check_output()
+{
+       _name=$1
+       shift 1
+       if eval "$*" | entag > $_name.actual
+       then
+               diff $_name.expected $_name.actual
+       else
+               return 1;
+       fi
+}
+
+# Turn a reasonable test description into a reasonable test name.
+# All alphanums translated into -'s which are then compressed and stripped
+# from front and back.
+name_from_description()
+{
+        tr "'" '-' | tr '~`!@#$%^&*()_+={}[]|\;:"<>,/? ' '-' | tr -s '-' | tr '[A-Z]' '[a-z]' | sed "s/^-*//;s/-*\$//"
+}
+
+
+# Execute the test described by the first argument, by eval'ing
+# command line specified in the 2nd argument. Check the status code
+# is zero and that the output matches the stream read from
+# stdin.
+test_output_expect_success()
+{
+       _description=$1
+        _test=$2
+        [ $# -eq 2 ] || error "usage: test_output_expect_success description test <<EOF ... EOF"
+        _name=$(echo $_description | name_from_description)
+       cat > $_name.expected
+       test_expect_success "$_description" "check_output $_name \"$_test\""
+}
diff --git a/t/t6001-rev-list-graft.sh b/t/t6001-rev-list-graft.sh
new file mode 100755 (executable)
index 0000000..b2131cd
--- /dev/null
@@ -0,0 +1,113 @@
+#!/bin/sh
+
+test_description='Revision traversal vs grafts and path limiter'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       mkdir subdir &&
+       echo >fileA fileA &&
+       echo >subdir/fileB fileB &&
+       git add fileA subdir/fileB &&
+       git commit -a -m "Initial in one history." &&
+       A0=`git rev-parse --verify HEAD` &&
+
+       echo >fileA fileA modified &&
+       git commit -a -m "Second in one history." &&
+       A1=`git rev-parse --verify HEAD` &&
+
+       echo >subdir/fileB fileB modified &&
+       git commit -a -m "Third in one history." &&
+       A2=`git rev-parse --verify HEAD` &&
+
+       rm -f .git/refs/heads/master .git/index &&
+
+       echo >fileA fileA again &&
+       echo >subdir/fileB fileB again &&
+       git add fileA subdir/fileB &&
+       git commit -a -m "Initial in alternate history." &&
+       B0=`git rev-parse --verify HEAD` &&
+
+       echo >fileA fileA modified in alternate history &&
+       git commit -a -m "Second in alternate history." &&
+       B1=`git rev-parse --verify HEAD` &&
+
+       echo >subdir/fileB fileB modified in alternate history &&
+       git commit -a -m "Third in alternate history." &&
+       B2=`git rev-parse --verify HEAD` &&
+       : done
+'
+
+check () {
+       type=$1
+       shift
+
+       arg=
+       which=arg
+       rm -f test.expect
+       for a
+       do
+               if test "z$a" = z--
+               then
+                       which=expect
+                       child=
+                       continue
+               fi
+               if test "$which" = arg
+               then
+                       arg="$arg$a "
+                       continue
+               fi
+               if test "$type" = basic
+               then
+                       echo "$a"
+               else
+                       if test "z$child" != z
+                       then
+                               echo "$child $a"
+                       fi
+                       child="$a"
+               fi
+       done >test.expect
+       if test "$type" != basic && test "z$child" != z
+       then
+               echo >>test.expect $child
+       fi
+       if test $type = basic
+       then
+               git rev-list $arg >test.actual
+       elif test $type = parents
+       then
+               git rev-list --parents $arg >test.actual
+       elif test $type = parents-raw
+       then
+               git rev-list --parents --pretty=raw $arg |
+               sed -n -e 's/^commit //p' >test.actual
+       fi
+       diff test.expect test.actual
+}
+
+for type in basic parents parents-raw
+do
+       test_expect_success 'without grafts' "
+               rm -f .git/info/grafts
+               check $type $B2 -- $B2 $B1 $B0
+       "
+
+       test_expect_success 'with grafts' "
+               echo '$B0 $A2' >.git/info/grafts
+               check $type $B2 -- $B2 $B1 $B0 $A2 $A1 $A0
+       "
+
+       test_expect_success 'without grafts, with pathlimit' "
+               rm -f .git/info/grafts
+               check $type $B2 subdir -- $B2 $B0
+       "
+
+       test_expect_success 'with grafts, with pathlimit' "
+               echo '$B0 $A2' >.git/info/grafts
+               check $type $B2 subdir -- $B2 $B0 $A2 $A0
+       "
+
+done
+test_done
diff --git a/t/t6002-rev-list-bisect.sh b/t/t6002-rev-list-bisect.sh
new file mode 100755 (executable)
index 0000000..8f5de09
--- /dev/null
@@ -0,0 +1,238 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Jon Seymour
+#
+test_description='Tests git rev-list --bisect functionality'
+
+. ./test-lib.sh
+. ../t6000lib.sh # t6xxx specific functions
+
+# usage: test_bisection max-diff bisect-option head ^prune...
+#
+# e.g. test_bisection 1 --bisect l1 ^l0
+#
+test_bisection_diff()
+{
+       _max_diff=$1
+       _bisect_option=$2
+       shift 2
+       _bisection=$(git rev-list $_bisect_option "$@")
+       _list_size=$(git rev-list "$@" | wc -l)
+        _head=$1
+       shift 1
+       _bisection_size=$(git rev-list $_bisection "$@" | wc -l)
+       [ -n "$_list_size" -a -n "$_bisection_size" ] ||
+       error "test_bisection_diff failed"
+
+       # Test if bisection size is close to half of list size within
+       # tolerance.
+       #
+       _bisect_err=`expr $_list_size - $_bisection_size \* 2`
+       test "$_bisect_err" -lt 0 && _bisect_err=`expr 0 - $_bisect_err`
+       _bisect_err=`expr $_bisect_err / 2` ; # floor
+
+       test_expect_success \
+       "bisection diff $_bisect_option $_head $* <= $_max_diff" \
+       'test $_bisect_err -le $_max_diff'
+}
+
+date >path0
+git update-index --add path0
+save_tag tree git write-tree
+on_committer_date "1971-08-16 00:00:00" hide_error save_tag root unique_commit root tree
+on_committer_date "1971-08-16 00:00:01" save_tag l0 unique_commit l0 tree -p root
+on_committer_date "1971-08-16 00:00:02" save_tag l1 unique_commit l1 tree -p l0
+on_committer_date "1971-08-16 00:00:03" save_tag l2 unique_commit l2 tree -p l1
+on_committer_date "1971-08-16 00:00:04" save_tag a0 unique_commit a0 tree -p l2
+on_committer_date "1971-08-16 00:00:05" save_tag a1 unique_commit a1 tree -p a0
+on_committer_date "1971-08-16 00:00:06" save_tag b1 unique_commit b1 tree -p a0
+on_committer_date "1971-08-16 00:00:07" save_tag c1 unique_commit c1 tree -p b1
+on_committer_date "1971-08-16 00:00:08" save_tag b2 unique_commit b2 tree -p b1
+on_committer_date "1971-08-16 00:00:09" save_tag b3 unique_commit b2 tree -p b2
+on_committer_date "1971-08-16 00:00:10" save_tag c2 unique_commit c2 tree -p c1 -p b2
+on_committer_date "1971-08-16 00:00:11" save_tag c3 unique_commit c3 tree -p c2
+on_committer_date "1971-08-16 00:00:12" save_tag a2 unique_commit a2 tree -p a1
+on_committer_date "1971-08-16 00:00:13" save_tag a3 unique_commit a3 tree -p a2
+on_committer_date "1971-08-16 00:00:14" save_tag b4 unique_commit b4 tree -p b3 -p a3
+on_committer_date "1971-08-16 00:00:15" save_tag a4 unique_commit a4 tree -p a3 -p b4 -p c3
+on_committer_date "1971-08-16 00:00:16" save_tag l3 unique_commit l3 tree -p a4
+on_committer_date "1971-08-16 00:00:17" save_tag l4 unique_commit l4 tree -p l3
+on_committer_date "1971-08-16 00:00:18" save_tag l5 unique_commit l5 tree -p l4
+git update-ref HEAD $(tag l5)
+
+
+#     E
+#    / \
+#   e1  |
+#   |   |
+#   e2  |
+#   |   |
+#   e3  |
+#   |   |
+#   e4  |
+#   |   |
+#   |   f1
+#   |   |
+#   |   f2
+#   |   |
+#   |   f3
+#   |   |
+#   |   f4
+#   |   |
+#   e5  |
+#   |   |
+#   e6  |
+#   |   |
+#   e7  |
+#   |   |
+#   e8  |
+#    \ /
+#     F
+
+
+on_committer_date "1971-08-16 00:00:00" hide_error save_tag F unique_commit F tree
+on_committer_date "1971-08-16 00:00:01" save_tag e8 unique_commit e8 tree -p F
+on_committer_date "1971-08-16 00:00:02" save_tag e7 unique_commit e7 tree -p e8
+on_committer_date "1971-08-16 00:00:03" save_tag e6 unique_commit e6 tree -p e7
+on_committer_date "1971-08-16 00:00:04" save_tag e5 unique_commit e5 tree -p e6
+on_committer_date "1971-08-16 00:00:05" save_tag f4 unique_commit f4 tree -p F
+on_committer_date "1971-08-16 00:00:06" save_tag f3 unique_commit f3 tree -p f4
+on_committer_date "1971-08-16 00:00:07" save_tag f2 unique_commit f2 tree -p f3
+on_committer_date "1971-08-16 00:00:08" save_tag f1 unique_commit f1 tree -p f2
+on_committer_date "1971-08-16 00:00:09" save_tag e4 unique_commit e4 tree -p e5
+on_committer_date "1971-08-16 00:00:10" save_tag e3 unique_commit e3 tree -p e4
+on_committer_date "1971-08-16 00:00:11" save_tag e2 unique_commit e2 tree -p e3
+on_committer_date "1971-08-16 00:00:12" save_tag e1 unique_commit e1 tree -p e2
+on_committer_date "1971-08-16 00:00:13" save_tag E unique_commit E tree -p e1 -p f1
+
+on_committer_date "1971-08-16 00:00:00" hide_error save_tag U unique_commit U tree
+on_committer_date "1971-08-16 00:00:01" save_tag u0 unique_commit u0 tree -p U
+on_committer_date "1971-08-16 00:00:01" save_tag u1 unique_commit u1 tree -p u0
+on_committer_date "1971-08-16 00:00:02" save_tag u2 unique_commit u2 tree -p u0
+on_committer_date "1971-08-16 00:00:03" save_tag u3 unique_commit u3 tree -p u0
+on_committer_date "1971-08-16 00:00:04" save_tag u4 unique_commit u4 tree -p u0
+on_committer_date "1971-08-16 00:00:05" save_tag u5 unique_commit u5 tree -p u0
+on_committer_date "1971-08-16 00:00:06" save_tag V unique_commit V tree -p u1 -p u2 -p u3 -p u4 -p u5
+
+test_sequence()
+{
+       _bisect_option=$1
+
+       test_bisection_diff 0 $_bisect_option l0 ^root
+       test_bisection_diff 0 $_bisect_option l1 ^root
+       test_bisection_diff 0 $_bisect_option l2 ^root
+       test_bisection_diff 0 $_bisect_option a0 ^root
+       test_bisection_diff 0 $_bisect_option a1 ^root
+       test_bisection_diff 0 $_bisect_option a2 ^root
+       test_bisection_diff 0 $_bisect_option a3 ^root
+       test_bisection_diff 0 $_bisect_option b1 ^root
+       test_bisection_diff 0 $_bisect_option b2 ^root
+       test_bisection_diff 0 $_bisect_option b3 ^root
+       test_bisection_diff 0 $_bisect_option c1 ^root
+       test_bisection_diff 0 $_bisect_option c2 ^root
+       test_bisection_diff 0 $_bisect_option c3 ^root
+       test_bisection_diff 0 $_bisect_option E ^F
+       test_bisection_diff 0 $_bisect_option e1 ^F
+       test_bisection_diff 0 $_bisect_option e2 ^F
+       test_bisection_diff 0 $_bisect_option e3 ^F
+       test_bisection_diff 0 $_bisect_option e4 ^F
+       test_bisection_diff 0 $_bisect_option e5 ^F
+       test_bisection_diff 0 $_bisect_option e6 ^F
+       test_bisection_diff 0 $_bisect_option e7 ^F
+       test_bisection_diff 0 $_bisect_option f1 ^F
+       test_bisection_diff 0 $_bisect_option f2 ^F
+       test_bisection_diff 0 $_bisect_option f3 ^F
+       test_bisection_diff 0 $_bisect_option f4 ^F
+       test_bisection_diff 0 $_bisect_option E ^F
+
+       test_bisection_diff 1 $_bisect_option V ^U
+       test_bisection_diff 0 $_bisect_option V ^U ^u1 ^u2 ^u3
+       test_bisection_diff 0 $_bisect_option u1 ^U
+       test_bisection_diff 0 $_bisect_option u2 ^U
+       test_bisection_diff 0 $_bisect_option u3 ^U
+       test_bisection_diff 0 $_bisect_option u4 ^U
+       test_bisection_diff 0 $_bisect_option u5 ^U
+
+#
+# the following illustrates Linus' binary bug blatt idea.
+#
+# assume the bug is actually at l3, but you don't know that - all you know is that l3 is broken
+# and it wasn't broken before
+#
+# keep bisecting the list, advancing the "bad" head and accumulating "good" heads until
+# the bisection point is the head - this is the bad point.
+#
+
+test_output_expect_success "$_bisect_option l5 ^root" 'git rev-list $_bisect_option l5 ^root' <<EOF
+c3
+EOF
+
+test_output_expect_success "$_bisect_option l5 ^root ^c3" 'git rev-list $_bisect_option l5 ^root ^c3' <<EOF
+b4
+EOF
+
+test_output_expect_success "$_bisect_option l5 ^root ^c3 ^b4" 'git rev-list $_bisect_option l5 ^c3 ^b4' <<EOF
+l3
+EOF
+
+test_output_expect_success "$_bisect_option l3 ^root ^c3 ^b4" 'git rev-list $_bisect_option l3 ^root ^c3 ^b4' <<EOF
+a4
+EOF
+
+test_output_expect_success "$_bisect_option l5 ^b3 ^a3 ^b4 ^a4" 'git rev-list $_bisect_option l3 ^b3 ^a3 ^a4' <<EOF
+l3
+EOF
+
+#
+# if l3 is bad, then l4 is bad too - so advance the bad pointer by making b4 the known bad head
+#
+
+test_output_expect_success "$_bisect_option l4 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l4 ^a2 ^a3 ^a4' <<EOF
+l3
+EOF
+
+test_output_expect_success "$_bisect_option l3 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l3 ^a2 ^a3 ^a4' <<EOF
+l3
+EOF
+
+# found!
+
+#
+# as another example, let's consider a4 to be the bad head, in which case
+#
+
+test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
+c2
+EOF
+
+test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2' <<EOF
+c3
+EOF
+
+test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3' <<EOF
+a4
+EOF
+
+# found!
+
+#
+# or consider c3 to be the bad head
+#
+
+test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
+c2
+EOF
+
+test_output_expect_success "$_bisect_option c3 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option c3 ^a2 ^a3 ^b4 ^c2' <<EOF
+c3
+EOF
+
+# found!
+
+}
+
+test_sequence "--bisect"
+
+#
+#
+test_done
diff --git a/t/t6003-rev-list-topo-order.sh b/t/t6003-rev-list-topo-order.sh
new file mode 100755 (executable)
index 0000000..5daa0be
--- /dev/null
@@ -0,0 +1,408 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Jon Seymour
+#
+
+test_description='Tests git rev-list --topo-order functionality'
+
+. ./test-lib.sh
+. ../t6000lib.sh # t6xxx specific functions
+
+list_duplicates()
+{
+    "$@" | sort | uniq -d
+}
+
+date >path0
+git update-index --add path0
+save_tag tree git write-tree
+on_committer_date "1971-08-16 00:00:00" hide_error save_tag root unique_commit root tree
+on_committer_date "1971-08-16 00:00:01" save_tag l0 unique_commit l0 tree -p root
+on_committer_date "1971-08-16 00:00:02" save_tag l1 unique_commit l1 tree -p l0
+on_committer_date "1971-08-16 00:00:03" save_tag l2 unique_commit l2 tree -p l1
+on_committer_date "1971-08-16 00:00:04" save_tag a0 unique_commit a0 tree -p l2
+on_committer_date "1971-08-16 00:00:05" save_tag a1 unique_commit a1 tree -p a0
+on_committer_date "1971-08-16 00:00:06" save_tag b1 unique_commit b1 tree -p a0
+on_committer_date "1971-08-16 00:00:07" save_tag c1 unique_commit c1 tree -p b1
+on_committer_date "1971-08-16 00:00:08" as_author foobar@example.com save_tag b2 unique_commit b2 tree -p b1
+on_committer_date "1971-08-16 00:00:09" save_tag b3 unique_commit b3 tree -p b2
+on_committer_date "1971-08-16 00:00:10" save_tag c2 unique_commit c2 tree -p c1 -p b2
+on_committer_date "1971-08-16 00:00:11" save_tag c3 unique_commit c3 tree -p c2
+on_committer_date "1971-08-16 00:00:12" save_tag a2 unique_commit a2 tree -p a1
+on_committer_date "1971-08-16 00:00:13" save_tag a3 unique_commit a3 tree -p a2
+on_committer_date "1971-08-16 00:00:14" save_tag b4 unique_commit b4 tree -p b3 -p a3
+on_committer_date "1971-08-16 00:00:15" save_tag a4 unique_commit a4 tree -p a3 -p b4 -p c3
+on_committer_date "1971-08-16 00:00:16" save_tag l3 unique_commit l3 tree -p a4
+on_committer_date "1971-08-16 00:00:17" save_tag l4 unique_commit l4 tree -p l3
+on_committer_date "1971-08-16 00:00:18" save_tag l5 unique_commit l5 tree -p l4
+on_committer_date "1971-08-16 00:00:19" save_tag m1 unique_commit m1 tree -p a4 -p c3
+on_committer_date "1971-08-16 00:00:20" save_tag m2 unique_commit m2 tree -p c3 -p a4
+on_committer_date "1971-08-16 00:00:21" hide_error save_tag alt_root unique_commit alt_root tree
+on_committer_date "1971-08-16 00:00:22" save_tag r0 unique_commit r0 tree -p alt_root
+on_committer_date "1971-08-16 00:00:23" save_tag r1 unique_commit r1 tree -p r0
+on_committer_date "1971-08-16 00:00:24" save_tag l5r1 unique_commit l5r1 tree -p l5 -p r1
+on_committer_date "1971-08-16 00:00:25" save_tag r1l5 unique_commit r1l5 tree -p r1 -p l5
+
+
+#
+# note: as of 20/6, it isn't possible to create duplicate parents, so this
+# can't be tested.
+#
+#on_committer_date "1971-08-16 00:00:20" save_tag m3 unique_commit m3 tree -p c3 -p a4 -p c3
+hide_error save_tag e1 as_author e@example.com unique_commit e1 tree
+save_tag e2 as_author e@example.com unique_commit e2 tree -p e1
+save_tag f1 as_author f@example.com unique_commit f1 tree -p e1
+save_tag e3 as_author e@example.com unique_commit e3 tree -p e2
+save_tag f2 as_author f@example.com unique_commit f2 tree -p f1
+save_tag e4 as_author e@example.com unique_commit e4 tree -p e3 -p f2
+save_tag e5 as_author e@example.com unique_commit e5 tree -p e4
+save_tag f3 as_author f@example.com unique_commit f3 tree -p f2
+save_tag f4 as_author f@example.com unique_commit f4 tree -p f3
+save_tag e6 as_author e@example.com unique_commit e6 tree -p e5 -p f4
+save_tag f5 as_author f@example.com unique_commit f5 tree -p f4
+save_tag f6 as_author f@example.com unique_commit f6 tree -p f5 -p e6
+save_tag e7 as_author e@example.com unique_commit e7 tree -p e6
+save_tag e8 as_author e@example.com unique_commit e8 tree -p e7
+save_tag e9 as_author e@example.com unique_commit e9 tree -p e8
+save_tag f7 as_author f@example.com unique_commit f7 tree -p f6
+save_tag f8 as_author f@example.com unique_commit f8 tree -p f7
+save_tag f9 as_author f@example.com unique_commit f9 tree -p f8
+save_tag e10 as_author e@example.com unique_commit e1 tree -p e9 -p f8
+
+hide_error save_tag g0 unique_commit g0 tree
+save_tag g1 unique_commit g1 tree -p g0
+save_tag h1 unique_commit g2 tree -p g0
+save_tag g2 unique_commit g3 tree -p g1 -p h1
+save_tag h2 unique_commit g4 tree -p g2
+save_tag g3 unique_commit g5 tree -p g2
+save_tag g4 unique_commit g6 tree -p g3 -p h2
+
+git update-ref HEAD $(tag l5)
+
+test_output_expect_success 'rev-list has correct number of entries' 'git rev-list HEAD | wc -l | tr -d \" \"' <<EOF
+19
+EOF
+
+test_output_expect_success 'simple topo order' 'git rev-list --topo-order  HEAD' <<EOF
+l5
+l4
+l3
+a4
+c3
+c2
+c1
+b4
+a3
+a2
+a1
+b3
+b2
+b1
+a0
+l2
+l1
+l0
+root
+EOF
+
+test_output_expect_success 'two diamonds topo order (g6)' 'git rev-list --topo-order  g4' <<EOF
+g4
+h2
+g3
+g2
+h1
+g1
+g0
+EOF
+
+test_output_expect_success 'multiple heads' 'git rev-list --topo-order a3 b3 c3' <<EOF
+a3
+a2
+a1
+c3
+c2
+c1
+b3
+b2
+b1
+a0
+l2
+l1
+l0
+root
+EOF
+
+test_output_expect_success 'multiple heads, prune at a1' 'git rev-list --topo-order a3 b3 c3 ^a1' <<EOF
+a3
+a2
+c3
+c2
+c1
+b3
+b2
+b1
+EOF
+
+test_output_expect_success 'multiple heads, prune at l1' 'git rev-list --topo-order a3 b3 c3 ^l1' <<EOF
+a3
+a2
+a1
+c3
+c2
+c1
+b3
+b2
+b1
+a0
+l2
+EOF
+
+test_output_expect_success 'cross-epoch, head at l5, prune at l1' 'git rev-list --topo-order l5 ^l1' <<EOF
+l5
+l4
+l3
+a4
+c3
+c2
+c1
+b4
+a3
+a2
+a1
+b3
+b2
+b1
+a0
+l2
+EOF
+
+test_output_expect_success 'duplicated head arguments' 'git rev-list --topo-order l5 l5 ^l1' <<EOF
+l5
+l4
+l3
+a4
+c3
+c2
+c1
+b4
+a3
+a2
+a1
+b3
+b2
+b1
+a0
+l2
+EOF
+
+test_output_expect_success 'prune near topo' 'git rev-list --topo-order a4 ^c3' <<EOF
+a4
+b4
+a3
+a2
+a1
+b3
+EOF
+
+test_output_expect_success "head has no parent" 'git rev-list --topo-order  root' <<EOF
+root
+EOF
+
+test_output_expect_success "two nodes - one head, one base" 'git rev-list --topo-order  l0' <<EOF
+l0
+root
+EOF
+
+test_output_expect_success "three nodes one head, one internal, one base" 'git rev-list --topo-order  l1' <<EOF
+l1
+l0
+root
+EOF
+
+test_output_expect_success "linear prune l2 ^root" 'git rev-list --topo-order  l2 ^root' <<EOF
+l2
+l1
+l0
+EOF
+
+test_output_expect_success "linear prune l2 ^l0" 'git rev-list --topo-order  l2 ^l0' <<EOF
+l2
+l1
+EOF
+
+test_output_expect_success "linear prune l2 ^l1" 'git rev-list --topo-order  l2 ^l1' <<EOF
+l2
+EOF
+
+test_output_expect_success "linear prune l5 ^a4" 'git rev-list --topo-order  l5 ^a4' <<EOF
+l5
+l4
+l3
+EOF
+
+test_output_expect_success "linear prune l5 ^l3" 'git rev-list --topo-order  l5 ^l3' <<EOF
+l5
+l4
+EOF
+
+test_output_expect_success "linear prune l5 ^l4" 'git rev-list --topo-order  l5 ^l4' <<EOF
+l5
+EOF
+
+test_output_expect_success "max-count 10 - topo order" 'git rev-list --topo-order  --max-count=10 l5' <<EOF
+l5
+l4
+l3
+a4
+c3
+c2
+c1
+b4
+a3
+a2
+EOF
+
+test_output_expect_success "max-count 10 - non topo order" 'git rev-list --max-count=10 l5' <<EOF
+l5
+l4
+l3
+a4
+b4
+a3
+a2
+c3
+c2
+b3
+EOF
+
+test_output_expect_success '--max-age=c3, no --topo-order' "git rev-list --max-age=$(commit_date c3) l5" <<EOF
+l5
+l4
+l3
+a4
+b4
+a3
+a2
+c3
+EOF
+
+#
+# this test fails on --topo-order - a fix is required
+#
+#test_output_expect_success '--max-age=c3, --topo-order' "git rev-list --topo-order --max-age=$(commit_date c3) l5" <<EOF
+#l5
+#l4
+#l3
+#a4
+#c3
+#b4
+#a3
+#a2
+#EOF
+
+test_output_expect_success 'one specified head reachable from another a4, c3, --topo-order' "list_duplicates git rev-list --topo-order a4 c3" <<EOF
+EOF
+
+test_output_expect_success 'one specified head reachable from another c3, a4, --topo-order' "list_duplicates git rev-list --topo-order c3 a4" <<EOF
+EOF
+
+test_output_expect_success 'one specified head reachable from another a4, c3, no --topo-order' "list_duplicates git rev-list a4 c3" <<EOF
+EOF
+
+test_output_expect_success 'one specified head reachable from another c3, a4, no --topo-order' "list_duplicates git rev-list c3 a4" <<EOF
+EOF
+
+test_output_expect_success 'graph with c3 and a4 parents of head' "list_duplicates git rev-list m1" <<EOF
+EOF
+
+test_output_expect_success 'graph with a4 and c3 parents of head' "list_duplicates git rev-list m2" <<EOF
+EOF
+
+test_expect_success "head ^head --topo-order" 'git rev-list --topo-order  a3 ^a3' <<EOF
+EOF
+
+test_expect_success "head ^head no --topo-order" 'git rev-list a3 ^a3' <<EOF
+EOF
+
+test_output_expect_success 'simple topo order (l5r1)' 'git rev-list --topo-order  l5r1' <<EOF
+l5r1
+r1
+r0
+alt_root
+l5
+l4
+l3
+a4
+c3
+c2
+c1
+b4
+a3
+a2
+a1
+b3
+b2
+b1
+a0
+l2
+l1
+l0
+root
+EOF
+
+test_output_expect_success 'simple topo order (r1l5)' 'git rev-list --topo-order  r1l5' <<EOF
+r1l5
+l5
+l4
+l3
+a4
+c3
+c2
+c1
+b4
+a3
+a2
+a1
+b3
+b2
+b1
+a0
+l2
+l1
+l0
+root
+r1
+r0
+alt_root
+EOF
+
+test_output_expect_success "don't print things unreachable from one branch" "git rev-list a3 ^b3 --topo-order" <<EOF
+a3
+a2
+a1
+EOF
+
+test_output_expect_success "--topo-order a4 l3" "git rev-list --topo-order a4 l3" <<EOF
+l3
+a4
+c3
+c2
+c1
+b4
+a3
+a2
+a1
+b3
+b2
+b1
+a0
+l2
+l1
+l0
+root
+EOF
+
+#
+#
+
+test_done
diff --git a/t/t6004-rev-list-path-optim.sh b/t/t6004-rev-list-path-optim.sh
new file mode 100755 (executable)
index 0000000..80d7198
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+test_description='git rev-list trivial path optimization test'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+echo Hello > a &&
+git add a &&
+git commit -m "Initial commit" a &&
+initial=$(git rev-parse --verify HEAD)
+'
+
+test_expect_success path-optimization '
+    commit=$(echo "Unchanged tree" | git commit-tree "HEAD^{tree}" -p HEAD) &&
+    test $(git rev-list $commit | wc -l) = 2 &&
+    test $(git rev-list $commit -- . | wc -l) = 1
+'
+
+test_expect_success 'further setup' '
+       git checkout -b side &&
+       echo Irrelevant >c &&
+       git add c &&
+       git commit -m "Side makes an irrelevant commit" &&
+       echo "More Irrelevancy" >c &&
+       git add c &&
+       git commit -m "Side makes another irrelevant commit" &&
+       echo Bye >a &&
+       git add a &&
+       git commit -m "Side touches a" &&
+       side=$(git rev-parse --verify HEAD) &&
+       echo "Yet more Irrelevancy" >c &&
+       git add c &&
+       git commit -m "Side makes yet another irrelevant commit" &&
+       git checkout master &&
+       echo Another >b &&
+       git add b &&
+       git commit -m "Master touches b" &&
+       git merge side &&
+       echo Touched >b &&
+       git add b &&
+       git commit -m "Master touches b again"
+'
+
+test_expect_success 'path optimization 2' '
+       ( echo "$side"; echo "$initial" ) >expected &&
+       git rev-list HEAD -- a >actual &&
+       diff -u expected actual
+'
+
+test_done
diff --git a/t/t6005-rev-list-count.sh b/t/t6005-rev-list-count.sh
new file mode 100755 (executable)
index 0000000..0b64822
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+test_description='git rev-list --max-count and --skip test'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+    for n in 1 2 3 4 5 ; do \
+        echo $n > a ; \
+        git add a ; \
+        git commit -m "$n" ; \
+    done
+'
+
+test_expect_success 'no options' '
+    test $(git rev-list HEAD | wc -l) = 5
+'
+
+test_expect_success '--max-count' '
+    test $(git rev-list HEAD --max-count=0 | wc -l) = 0 &&
+    test $(git rev-list HEAD --max-count=3 | wc -l) = 3 &&
+    test $(git rev-list HEAD --max-count=5 | wc -l) = 5 &&
+    test $(git rev-list HEAD --max-count=10 | wc -l) = 5
+'
+
+test_expect_success '--max-count all forms' '
+    test $(git rev-list HEAD --max-count=1 | wc -l) = 1 &&
+    test $(git rev-list HEAD -1 | wc -l) = 1 &&
+    test $(git rev-list HEAD -n1 | wc -l) = 1 &&
+    test $(git rev-list HEAD -n 1 | wc -l) = 1
+'
+
+test_expect_success '--skip' '
+    test $(git rev-list HEAD --skip=0 | wc -l) = 5 &&
+    test $(git rev-list HEAD --skip=3 | wc -l) = 2 &&
+    test $(git rev-list HEAD --skip=5 | wc -l) = 0 &&
+    test $(git rev-list HEAD --skip=10 | wc -l) = 0
+'
+
+test_expect_success '--skip --max-count' '
+    test $(git rev-list HEAD --skip=0 --max-count=0 | wc -l) = 0 &&
+    test $(git rev-list HEAD --skip=0 --max-count=10 | wc -l) = 5 &&
+    test $(git rev-list HEAD --skip=3 --max-count=0 | wc -l) = 0 &&
+    test $(git rev-list HEAD --skip=3 --max-count=1 | wc -l) = 1 &&
+    test $(git rev-list HEAD --skip=3 --max-count=2 | wc -l) = 2 &&
+    test $(git rev-list HEAD --skip=3 --max-count=10 | wc -l) = 2 &&
+    test $(git rev-list HEAD --skip=5 --max-count=10 | wc -l) = 0 &&
+    test $(git rev-list HEAD --skip=10 --max-count=10 | wc -l) = 0
+'
+
+test_done
diff --git a/t/t6006-rev-list-format.sh b/t/t6006-rev-list-format.sh
new file mode 100755 (executable)
index 0000000..1e4541a
--- /dev/null
@@ -0,0 +1,142 @@
+#!/bin/sh
+
+test_description='git rev-list --pretty=format test'
+
+. ./test-lib.sh
+
+test_tick
+test_expect_success 'setup' '
+touch foo && git add foo && git-commit -m "added foo" &&
+  echo changed >foo && git-commit -a -m "changed foo"
+'
+
+# usage: test_format name format_string <expected_output
+test_format() {
+       cat >expect.$1
+       test_expect_success "format $1" "
+git rev-list --pretty=format:$2 master >output.$1 &&
+git diff expect.$1 output.$1
+"
+}
+
+test_format hash %H%n%h <<'EOF'
+commit 131a310eb913d107dd3c09a65d1651175898735d
+131a310eb913d107dd3c09a65d1651175898735d
+131a310
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+86c75cf
+EOF
+
+test_format tree %T%n%t <<'EOF'
+commit 131a310eb913d107dd3c09a65d1651175898735d
+fe722612f26da5064c32ca3843aa154bdb0b08a0
+fe72261
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+4d5fcadc293a348e88f777dc0920f11e7d71441c
+4d5fcad
+EOF
+
+test_format parents %P%n%p <<'EOF'
+commit 131a310eb913d107dd3c09a65d1651175898735d
+86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+86c75cf
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+
+
+EOF
+
+# we don't test relative here
+test_format author %an%n%ae%n%ad%n%aD%n%at <<'EOF'
+commit 131a310eb913d107dd3c09a65d1651175898735d
+A U Thor
+author@example.com
+Thu Apr 7 15:13:13 2005 -0700
+Thu, 7 Apr 2005 15:13:13 -0700
+1112911993
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+A U Thor
+author@example.com
+Thu Apr 7 15:13:13 2005 -0700
+Thu, 7 Apr 2005 15:13:13 -0700
+1112911993
+EOF
+
+test_format committer %cn%n%ce%n%cd%n%cD%n%ct <<'EOF'
+commit 131a310eb913d107dd3c09a65d1651175898735d
+C O Mitter
+committer@example.com
+Thu Apr 7 15:13:13 2005 -0700
+Thu, 7 Apr 2005 15:13:13 -0700
+1112911993
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+C O Mitter
+committer@example.com
+Thu Apr 7 15:13:13 2005 -0700
+Thu, 7 Apr 2005 15:13:13 -0700
+1112911993
+EOF
+
+test_format encoding %e <<'EOF'
+commit 131a310eb913d107dd3c09a65d1651175898735d
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+EOF
+
+test_format subject %s <<'EOF'
+commit 131a310eb913d107dd3c09a65d1651175898735d
+changed foo
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+added foo
+EOF
+
+test_format body %b <<'EOF'
+commit 131a310eb913d107dd3c09a65d1651175898735d
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+EOF
+
+test_format colors %Credfoo%Cgreenbar%Cbluebaz%Cresetxyzzy <<'EOF'
+commit 131a310eb913d107dd3c09a65d1651175898735d
+\e[31mfoo\e[32mbar\e[34mbaz\e[mxyzzy
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+\e[31mfoo\e[32mbar\e[34mbaz\e[mxyzzy
+EOF
+
+cat >commit-msg <<'EOF'
+Test printing of complex bodies
+
+This commit message is much longer than the others,
+and it will be encoded in iso8859-1. We should therefore
+include an iso8859 character: ¡bueno!
+EOF
+test_expect_success 'setup complex body' '
+git config i18n.commitencoding iso8859-1 &&
+  echo change2 >foo && git-commit -a -F commit-msg
+'
+
+test_format complex-encoding %e <<'EOF'
+commit f58db70b055c5718631e5c61528b28b12090cdea
+iso8859-1
+commit 131a310eb913d107dd3c09a65d1651175898735d
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+EOF
+
+test_format complex-subject %s <<'EOF'
+commit f58db70b055c5718631e5c61528b28b12090cdea
+Test printing of complex bodies
+commit 131a310eb913d107dd3c09a65d1651175898735d
+changed foo
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+added foo
+EOF
+
+test_format complex-body %b <<'EOF'
+commit f58db70b055c5718631e5c61528b28b12090cdea
+This commit message is much longer than the others,
+and it will be encoded in iso8859-1. We should therefore
+include an iso8859 character: ¡bueno!
+
+commit 131a310eb913d107dd3c09a65d1651175898735d
+commit 86c75cfd708a0e5868dc876ed5b8bb66c80b4873
+EOF
+
+test_done
diff --git a/t/t6007-rev-list-cherry-pick-file.sh b/t/t6007-rev-list-cherry-pick-file.sh
new file mode 100755 (executable)
index 0000000..4b8611c
--- /dev/null
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+test_description='test git rev-list --cherry-pick -- file'
+
+. ./test-lib.sh
+
+# A---B
+#  \
+#   \
+#    C
+#
+# B changes a file foo.c, adding a line of text.  C changes foo.c as
+# well as bar.c, but the change in foo.c was identical to change B.
+
+test_expect_success setup '
+       echo Hallo > foo &&
+       git add foo &&
+       test_tick &&
+       git commit -m "A" &&
+       git tag A &&
+       git checkout -b branch &&
+       echo Bello > foo &&
+       echo Cello > bar &&
+       git add foo bar &&
+       test_tick &&
+       git commit -m "C" &&
+       git tag C &&
+       git checkout master &&
+       git checkout branch foo &&
+       test_tick &&
+       git commit -m "B" &&
+       git tag B
+'
+
+test_expect_success '--cherry-pick foo comes up empty' '
+       test -z "$(git rev-list --left-right --cherry-pick B...C -- foo)"
+'
+
+test_expect_success '--cherry-pick bar does not come up empty' '
+       ! test -z "$(git rev-list --left-right --cherry-pick B...C -- bar)"
+'
+
+test_expect_success '--cherry-pick with independent, but identical branches' '
+       git symbolic-ref HEAD refs/heads/independent &&
+       rm .git/index &&
+       echo Hallo > foo &&
+       git add foo &&
+       test_tick &&
+       git commit -m "independent" &&
+       echo Bello > foo &&
+       test_tick &&
+       git commit -m "independent, too" foo &&
+       test -z "$(git rev-list --left-right --cherry-pick \
+               HEAD...master -- foo)"
+'
+
+test_done
diff --git a/t/t6008-rev-list-submodule.sh b/t/t6008-rev-list-submodule.sh
new file mode 100755 (executable)
index 0000000..88e96fb
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes E. Schindelin
+#
+
+test_description='git rev-list involving submodules that this repo has'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+       : > file &&
+       git add file &&
+       test_tick &&
+       git commit -m initial &&
+       echo 1 > file &&
+       test_tick &&
+       git commit -m second file &&
+       echo 2 > file &&
+       test_tick &&
+       git commit -m third file &&
+
+       rm .git/index &&
+
+       : > super-file &&
+       git add super-file &&
+       git submodule add . sub &&
+       git symbolic-ref HEAD refs/heads/super &&
+       test_tick &&
+       git commit -m super-initial &&
+       echo 1 > super-file &&
+       test_tick &&
+       git commit -m super-first super-file &&
+       echo 2 > super-file &&
+       test_tick &&
+       git commit -m super-second super-file
+'
+
+test_expect_success "Ilari's test" '
+       git rev-list --objects super master ^super^
+'
+
+test_done
diff --git a/t/t6010-merge-base.sh b/t/t6010-merge-base.sh
new file mode 100755 (executable)
index 0000000..96f3d35
--- /dev/null
@@ -0,0 +1,110 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='Merge base computation.
+'
+
+. ./test-lib.sh
+
+T=$(git write-tree)
+
+M=1130000000
+Z=+0000
+
+export GIT_COMMITTER_EMAIL=git@comm.iter.xz
+export GIT_COMMITTER_NAME='C O Mmiter'
+export GIT_AUTHOR_NAME='A U Thor'
+export GIT_AUTHOR_EMAIL=git@au.thor.xz
+
+doit() {
+       OFFSET=$1; shift
+       NAME=$1; shift
+       PARENTS=
+       for P
+       do
+               PARENTS="${PARENTS}-p $P "
+       done
+       GIT_COMMITTER_DATE="$(($M + $OFFSET)) $Z"
+       GIT_AUTHOR_DATE=$GIT_COMMITTER_DATE
+       export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
+       commit=$(echo $NAME | git commit-tree $T $PARENTS)
+       echo $commit >.git/refs/tags/$NAME
+       echo $commit
+}
+
+#  E---D---C---B---A
+#  \'-_         \   \
+#   \  `---------G   \
+#    \                \
+#     F----------------H
+
+# Setup...
+E=$(doit 5 E)
+D=$(doit 4 D $E)
+F=$(doit 6 F $E)
+C=$(doit 3 C $D)
+B=$(doit 2 B $C)
+A=$(doit 1 A $B)
+G=$(doit 7 G $B $E)
+H=$(doit 8 H $A $F)
+
+test_expect_success 'compute merge-base (single)' \
+    'MB=$(git merge-base G H) &&
+     expr "$(git name-rev "$MB")" : "[0-9a-f]* tags/B"'
+
+test_expect_success 'compute merge-base (all)' \
+    'MB=$(git merge-base --all G H) &&
+     expr "$(git name-rev "$MB")" : "[0-9a-f]* tags/B"'
+
+test_expect_success 'compute merge-base with show-branch' \
+    'MB=$(git show-branch --merge-base G H) &&
+     expr "$(git name-rev "$MB")" : "[0-9a-f]* tags/B"'
+
+# Setup for second test to demonstrate that relying on timestamps in a
+# distributed SCM to provide a _consistent_ partial ordering of commits
+# leads to insanity.
+#
+#               Relative
+# Structure     timestamps
+#
+#   PL  PR        +4  +4
+#  /  \/  \      /  \/  \
+# L2  C2  R2    +3  -1  +3
+# |   |   |     |   |   |
+# L1  C1  R1    +2  -2  +2
+# |   |   |     |   |   |
+# L0  C0  R0    +1  -3  +1
+#   \ |  /        \ |  /
+#     S             0
+#
+# The left and right chains of commits can be of any length and complexity as
+# long as all of the timestamps are greater than that of S.
+
+S=$(doit  0 S)
+
+C0=$(doit -3 C0 $S)
+C1=$(doit -2 C1 $C0)
+C2=$(doit -1 C2 $C1)
+
+L0=$(doit  1 L0 $S)
+L1=$(doit  2 L1 $L0)
+L2=$(doit  3 L2 $L1)
+
+R0=$(doit  1 R0 $S)
+R1=$(doit  2 R1 $R0)
+R2=$(doit  3 R2 $R1)
+
+PL=$(doit  4 PL $L2 $C2)
+PR=$(doit  4 PR $C2 $R2)
+
+test_expect_success 'compute merge-base (single)' \
+    'MB=$(git merge-base PL PR) &&
+     expr "$(git name-rev "$MB")" : "[0-9a-f]* tags/C2"'
+
+test_expect_success 'compute merge-base (all)' \
+    'MB=$(git merge-base --all PL PR) &&
+     expr "$(git name-rev "$MB")" : "[0-9a-f]* tags/C2"'
+
+test_done
diff --git a/t/t6020-merge-df.sh b/t/t6020-merge-df.sh
new file mode 100755 (executable)
index 0000000..a19d49d
--- /dev/null
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Fredrik Kuivinen
+#
+
+test_description='Test merge with directory/file conflicts'
+. ./test-lib.sh
+
+test_expect_success 'prepare repository' \
+'echo "Hello" > init &&
+git add init &&
+git commit -m "Initial commit" &&
+git branch B &&
+mkdir dir &&
+echo "foo" > dir/foo &&
+git add dir/foo &&
+git commit -m "File: dir/foo" &&
+git checkout B &&
+echo "file dir" > dir &&
+git add dir &&
+git commit -m "File: dir"'
+
+test_expect_code 1 'Merge with d/f conflicts' 'git merge "merge msg" B master'
+
+test_done
diff --git a/t/t6021-merge-criss-cross.sh b/t/t6021-merge-criss-cross.sh
new file mode 100755 (executable)
index 0000000..0ab14a6
--- /dev/null
@@ -0,0 +1,92 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Fredrik Kuivinen
+#
+
+# See http://marc.theaimsgroup.com/?l=git&m=111463358500362&w=2 for a
+# nice description of what this is about.
+
+
+test_description='Test criss-cross merge'
+. ./test-lib.sh
+
+test_expect_success 'prepare repository' \
+'echo "1
+2
+3
+4
+5
+6
+7
+8
+9" > file &&
+git add file &&
+git commit -m "Initial commit" file &&
+git branch A &&
+git branch B &&
+git checkout A &&
+echo "1
+2
+3
+4
+5
+6
+7
+8 changed in B8, branch A
+9" > file &&
+git commit -m "B8" file &&
+git checkout B &&
+echo "1
+2
+3 changed in C3, branch B
+4
+5
+6
+7
+8
+9
+" > file &&
+git commit -m "C3" file &&
+git branch C3 &&
+git merge "pre E3 merge" B A &&
+echo "1
+2
+3 changed in E3, branch B. New file size
+4
+5
+6
+7
+8 changed in B8, branch A
+9
+" > file &&
+git commit -m "E3" file &&
+git checkout A &&
+git merge "pre D8 merge" A C3 &&
+echo "1
+2
+3 changed in C3, branch B
+4
+5
+6
+7
+8 changed in D8, branch A. New file size 2
+9" > file &&
+git commit -m D8 file'
+
+test_expect_success 'Criss-cross merge' 'git merge "final merge" A B'
+
+cat > file-expect <<EOF
+1
+2
+3 changed in E3, branch B. New file size
+4
+5
+6
+7
+8 changed in D8, branch A. New file size 2
+9
+EOF
+
+test_expect_success 'Criss-cross merge result' 'cmp file file-expect'
+
+test_done
diff --git a/t/t6022-merge-rename.sh b/t/t6022-merge-rename.sh
new file mode 100755 (executable)
index 0000000..e3f7ae8
--- /dev/null
@@ -0,0 +1,344 @@
+#!/bin/sh
+
+test_description='Merge-recursive merging renames'
+. ./test-lib.sh
+
+test_expect_success setup \
+'
+cat >A <<\EOF &&
+a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+b bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+c cccccccccccccccccccccccccccccccccccccccccccccccc
+d dddddddddddddddddddddddddddddddddddddddddddddddd
+e eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+f ffffffffffffffffffffffffffffffffffffffffffffffff
+g gggggggggggggggggggggggggggggggggggggggggggggggg
+h hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh
+i iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii
+j jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
+k kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk
+l llllllllllllllllllllllllllllllllllllllllllllllll
+m mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
+n nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn
+o oooooooooooooooooooooooooooooooooooooooooooooooo
+EOF
+
+cat >M <<\EOF &&
+A AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+B BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+C CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
+D DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
+E EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
+F FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+G GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
+H HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
+I IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
+J JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ
+K KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
+L LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
+M MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
+N NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
+O OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
+EOF
+
+git add A M &&
+git commit -m "initial has A and M" &&
+git branch white &&
+git branch red &&
+git branch blue &&
+git branch yellow &&
+git branch change &&
+git branch change+rename &&
+
+sed -e "/^g /s/.*/g : master changes a line/" <A >A+ &&
+mv A+ A &&
+git commit -a -m "master updates A" &&
+
+git checkout yellow &&
+rm -f M &&
+git commit -a -m "yellow removes M" &&
+
+git checkout white &&
+sed -e "/^g /s/.*/g : white changes a line/" <A >B &&
+sed -e "/^G /s/.*/G : colored branch changes a line/" <M >N &&
+rm -f A M &&
+git update-index --add --remove A B M N &&
+git commit -m "white renames A->B, M->N" &&
+
+git checkout red &&
+sed -e "/^g /s/.*/g : red changes a line/" <A >B &&
+sed -e "/^G /s/.*/G : colored branch changes a line/" <M >N &&
+rm -f A M &&
+git update-index --add --remove A B M N &&
+git commit -m "red renames A->B, M->N" &&
+
+git checkout blue &&
+sed -e "/^g /s/.*/g : blue changes a line/" <A >C &&
+sed -e "/^G /s/.*/G : colored branch changes a line/" <M >N &&
+rm -f A M &&
+git update-index --add --remove A C M N &&
+git commit -m "blue renames A->C, M->N" &&
+
+git checkout change &&
+sed -e "/^g /s/.*/g : changed line/" <A >A+ &&
+mv A+ A &&
+git commit -q -a -m "changed" &&
+
+git checkout change+rename &&
+sed -e "/^g /s/.*/g : changed line/" <A >B &&
+rm A &&
+git update-index --add B &&
+git commit -q -a -m "changed and renamed" &&
+
+git checkout master'
+
+test_expect_success 'pull renaming branch into unrenaming one' \
+'
+       git show-branch
+       git pull . white && {
+               echo "BAD: should have conflicted"
+               return 1
+       }
+       git ls-files -s
+       test "$(git ls-files -u B | wc -l)" -eq 3 || {
+               echo "BAD: should have left stages for B"
+               return 1
+       }
+       test "$(git ls-files -s N | wc -l)" -eq 1 || {
+               echo "BAD: should have merged N"
+               return 1
+       }
+       sed -ne "/^g/{
+       p
+       q
+       }" B | grep master || {
+               echo "BAD: should have listed our change first"
+               return 1
+       }
+       test "$(git diff white N | wc -l)" -eq 0 || {
+               echo "BAD: should have taken colored branch"
+               return 1
+       }
+'
+
+test_expect_success 'pull renaming branch into another renaming one' \
+'
+       rm -f B
+       git reset --hard
+       git checkout red
+       git pull . white && {
+               echo "BAD: should have conflicted"
+               return 1
+       }
+       test "$(git ls-files -u B | wc -l)" -eq 3 || {
+               echo "BAD: should have left stages"
+               return 1
+       }
+       test "$(git ls-files -s N | wc -l)" -eq 1 || {
+               echo "BAD: should have merged N"
+               return 1
+       }
+       sed -ne "/^g/{
+       p
+       q
+       }" B | grep red || {
+               echo "BAD: should have listed our change first"
+               return 1
+       }
+       test "$(git diff white N | wc -l)" -eq 0 || {
+               echo "BAD: should have taken colored branch"
+               return 1
+       }
+'
+
+test_expect_success 'pull unrenaming branch into renaming one' \
+'
+       git reset --hard
+       git show-branch
+       git pull . master && {
+               echo "BAD: should have conflicted"
+               return 1
+       }
+       test "$(git ls-files -u B | wc -l)" -eq 3 || {
+               echo "BAD: should have left stages"
+               return 1
+       }
+       test "$(git ls-files -s N | wc -l)" -eq 1 || {
+               echo "BAD: should have merged N"
+               return 1
+       }
+       sed -ne "/^g/{
+       p
+       q
+       }" B | grep red || {
+               echo "BAD: should have listed our change first"
+               return 1
+       }
+       test "$(git diff white N | wc -l)" -eq 0 || {
+               echo "BAD: should have taken colored branch"
+               return 1
+       }
+'
+
+test_expect_success 'pull conflicting renames' \
+'
+       git reset --hard
+       git show-branch
+       git pull . blue && {
+               echo "BAD: should have conflicted"
+               return 1
+       }
+       test "$(git ls-files -u A | wc -l)" -eq 1 || {
+               echo "BAD: should have left a stage"
+               return 1
+       }
+       test "$(git ls-files -u B | wc -l)" -eq 1 || {
+               echo "BAD: should have left a stage"
+               return 1
+       }
+       test "$(git ls-files -u C | wc -l)" -eq 1 || {
+               echo "BAD: should have left a stage"
+               return 1
+       }
+       test "$(git ls-files -s N | wc -l)" -eq 1 || {
+               echo "BAD: should have merged N"
+               return 1
+       }
+       sed -ne "/^g/{
+       p
+       q
+       }" B | grep red || {
+               echo "BAD: should have listed our change first"
+               return 1
+       }
+       test "$(git diff white N | wc -l)" -eq 0 || {
+               echo "BAD: should have taken colored branch"
+               return 1
+       }
+'
+
+test_expect_success 'interference with untracked working tree file' '
+
+       git reset --hard
+       git show-branch
+       echo >A this file should not matter
+       git pull . white && {
+               echo "BAD: should have conflicted"
+               return 1
+       }
+       test -f A || {
+               echo "BAD: should have left A intact"
+               return 1
+       }
+'
+
+test_expect_success 'interference with untracked working tree file' '
+
+       git reset --hard
+       git checkout white
+       git show-branch
+       rm -f A
+       echo >A this file should not matter
+       git pull . red && {
+               echo "BAD: should have conflicted"
+               return 1
+       }
+       test -f A || {
+               echo "BAD: should have left A intact"
+               return 1
+       }
+'
+
+test_expect_success 'interference with untracked working tree file' '
+
+       git reset --hard
+       rm -f A M
+       git checkout -f master
+       git tag -f anchor
+       git show-branch
+       git pull . yellow || {
+               echo "BAD: should have cleanly merged"
+               return 1
+       }
+       test -f M && {
+               echo "BAD: should have removed M"
+               return 1
+       }
+       git reset --hard anchor
+'
+
+test_expect_success 'updated working tree file should prevent the merge' '
+
+       git reset --hard
+       rm -f A M
+       git checkout -f master
+       git tag -f anchor
+       git show-branch
+       echo >>M one line addition
+       cat M >M.saved
+       git pull . yellow && {
+               echo "BAD: should have complained"
+               return 1
+       }
+       diff M M.saved || {
+               echo "BAD: should have left M intact"
+               return 1
+       }
+       rm -f M.saved
+'
+
+test_expect_success 'updated working tree file should prevent the merge' '
+
+       git reset --hard
+       rm -f A M
+       git checkout -f master
+       git tag -f anchor
+       git show-branch
+       echo >>M one line addition
+       cat M >M.saved
+       git update-index M
+       git pull . yellow && {
+               echo "BAD: should have complained"
+               return 1
+       }
+       diff M M.saved || {
+               echo "BAD: should have left M intact"
+               return 1
+       }
+       rm -f M.saved
+'
+
+test_expect_success 'interference with untracked working tree file' '
+
+       git reset --hard
+       rm -f A M
+       git checkout -f yellow
+       git tag -f anchor
+       git show-branch
+       echo >M this file should not matter
+       git pull . master || {
+               echo "BAD: should have cleanly merged"
+               return 1
+       }
+       test -f M || {
+               echo "BAD: should have left M intact"
+               return 1
+       }
+       git ls-files -s | grep M && {
+               echo "BAD: M must be untracked in the result"
+               return 1
+       }
+       git reset --hard anchor
+'
+
+test_expect_success 'merge of identical changes in a renamed file' '
+       rm -f A M N
+       git reset --hard &&
+       git checkout change+rename &&
+       GIT_MERGE_VERBOSITY=3 git merge change | grep "^Skipped B" &&
+       git reset --hard HEAD^ &&
+       git checkout change &&
+       GIT_MERGE_VERBOSITY=3 git merge change+rename | grep "^Skipped B"
+'
+
+test_done
diff --git a/t/t6023-merge-file.sh b/t/t6023-merge-file.sh
new file mode 100755 (executable)
index 0000000..ae3b6f2
--- /dev/null
@@ -0,0 +1,142 @@
+#!/bin/sh
+
+test_description='RCS merge replacement: merge-file'
+. ./test-lib.sh
+
+cat > orig.txt << EOF
+Dominus regit me,
+et nihil mihi deerit.
+In loco pascuae ibi me collocavit,
+super aquam refectionis educavit me;
+animam meam convertit,
+deduxit me super semitas jusitiae,
+propter nomen suum.
+EOF
+
+cat > new1.txt << EOF
+Dominus regit me,
+et nihil mihi deerit.
+In loco pascuae ibi me collocavit,
+super aquam refectionis educavit me;
+animam meam convertit,
+deduxit me super semitas jusitiae,
+propter nomen suum.
+Nam et si ambulavero in medio umbrae mortis,
+non timebo mala, quoniam tu mecum es:
+virga tua et baculus tuus ipsa me consolata sunt.
+EOF
+
+cat > new2.txt << EOF
+Dominus regit me, et nihil mihi deerit.
+In loco pascuae ibi me collocavit,
+super aquam refectionis educavit me;
+animam meam convertit,
+deduxit me super semitas jusitiae,
+propter nomen suum.
+EOF
+
+cat > new3.txt << EOF
+DOMINUS regit me,
+et nihil mihi deerit.
+In loco pascuae ibi me collocavit,
+super aquam refectionis educavit me;
+animam meam convertit,
+deduxit me super semitas jusitiae,
+propter nomen suum.
+EOF
+
+cat > new4.txt << EOF
+Dominus regit me, et nihil mihi deerit.
+In loco pascuae ibi me collocavit,
+super aquam refectionis educavit me;
+animam meam convertit,
+deduxit me super semitas jusitiae,
+EOF
+printf "propter nomen suum." >> new4.txt
+
+cp new1.txt test.txt
+test_expect_success "merge without conflict" \
+       "git merge-file test.txt orig.txt new2.txt"
+
+cp new1.txt test2.txt
+test_expect_success "merge without conflict (missing LF at EOF)" \
+       "git merge-file test2.txt orig.txt new2.txt"
+
+test_expect_success "merge result added missing LF" \
+       "git diff test.txt test2.txt"
+
+cp test.txt backup.txt
+test_expect_failure "merge with conflicts" \
+       "git merge-file test.txt orig.txt new3.txt"
+
+cat > expect.txt << EOF
+<<<<<<< test.txt
+Dominus regit me, et nihil mihi deerit.
+=======
+DOMINUS regit me,
+et nihil mihi deerit.
+>>>>>>> new3.txt
+In loco pascuae ibi me collocavit,
+super aquam refectionis educavit me;
+animam meam convertit,
+deduxit me super semitas jusitiae,
+propter nomen suum.
+Nam et si ambulavero in medio umbrae mortis,
+non timebo mala, quoniam tu mecum es:
+virga tua et baculus tuus ipsa me consolata sunt.
+EOF
+
+test_expect_success "expected conflict markers" "git diff test.txt expect.txt"
+
+cp backup.txt test.txt
+test_expect_failure "merge with conflicts, using -L" \
+       "git merge-file -L 1 -L 2 test.txt orig.txt new3.txt"
+
+cat > expect.txt << EOF
+<<<<<<< 1
+Dominus regit me, et nihil mihi deerit.
+=======
+DOMINUS regit me,
+et nihil mihi deerit.
+>>>>>>> new3.txt
+In loco pascuae ibi me collocavit,
+super aquam refectionis educavit me;
+animam meam convertit,
+deduxit me super semitas jusitiae,
+propter nomen suum.
+Nam et si ambulavero in medio umbrae mortis,
+non timebo mala, quoniam tu mecum es:
+virga tua et baculus tuus ipsa me consolata sunt.
+EOF
+
+test_expect_success "expected conflict markers, with -L" \
+       "git diff test.txt expect.txt"
+
+sed "s/ tu / TU /" < new1.txt > new5.txt
+test_expect_failure "conflict in removed tail" \
+       "git merge-file -p orig.txt new1.txt new5.txt > out"
+
+cat > expect << EOF
+Dominus regit me,
+et nihil mihi deerit.
+In loco pascuae ibi me collocavit,
+super aquam refectionis educavit me;
+animam meam convertit,
+deduxit me super semitas jusitiae,
+propter nomen suum.
+<<<<<<< orig.txt
+=======
+Nam et si ambulavero in medio umbrae mortis,
+non timebo mala, quoniam TU mecum es:
+virga tua et baculus tuus ipsa me consolata sunt.
+>>>>>>> new5.txt
+EOF
+
+test_expect_success "expected conflict markers" "git diff expect out"
+
+test_expect_success 'binary files cannot be merged' '
+       ! git merge-file -p orig.txt ../test4012.png new1.txt 2> merge.err &&
+       grep "Cannot merge binary files" merge.err
+'
+
+test_done
diff --git a/t/t6023-merge-rename-nocruft.sh b/t/t6023-merge-rename-nocruft.sh
new file mode 100755 (executable)
index 0000000..65be95f
--- /dev/null
@@ -0,0 +1,139 @@
+#!/bin/sh
+
+test_description='Merge-recursive merging renames'
+. ./test-lib.sh
+
+test_expect_success setup \
+'
+cat >A <<\EOF &&
+a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+b bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+c cccccccccccccccccccccccccccccccccccccccccccccccc
+d dddddddddddddddddddddddddddddddddddddddddddddddd
+e eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+f ffffffffffffffffffffffffffffffffffffffffffffffff
+g gggggggggggggggggggggggggggggggggggggggggggggggg
+h hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh
+i iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii
+j jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
+k kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk
+l llllllllllllllllllllllllllllllllllllllllllllllll
+m mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
+n nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn
+o oooooooooooooooooooooooooooooooooooooooooooooooo
+EOF
+
+cat >M <<\EOF &&
+A AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+B BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+C CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
+D DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
+E EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
+F FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+G GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
+H HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
+I IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
+J JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ
+K KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
+L LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
+M MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
+N NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
+O OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
+EOF
+
+git add A M &&
+git commit -m "initial has A and M" &&
+git branch white &&
+git branch red &&
+git branch blue &&
+
+git checkout white &&
+sed -e "/^g /s/.*/g : white changes a line/" <A >B &&
+sed -e "/^G /s/.*/G : colored branch changes a line/" <M >N &&
+rm -f A M &&
+git update-index --add --remove A B M N &&
+git commit -m "white renames A->B, M->N" &&
+
+git checkout red &&
+echo created by red >R &&
+git update-index --add R &&
+git commit -m "red creates R" &&
+
+git checkout blue &&
+sed -e "/^o /s/.*/g : blue changes a line/" <A >B &&
+rm -f A &&
+mv B A &&
+git update-index A &&
+git commit -m "blue modify A" &&
+
+git checkout master'
+
+# This test broke in 65ac6e9c3f47807cb603af07a6a9e1a43bc119ae
+test_expect_success 'merge white into red (A->B,M->N)' \
+'
+       git checkout -b red-white red &&
+       git merge white &&
+       git write-tree >/dev/null || {
+               echo "BAD: merge did not complete"
+               return 1
+       }
+
+       test -f B || {
+               echo "BAD: B does not exist in working directory"
+               return 1
+       }
+       test -f N || {
+               echo "BAD: N does not exist in working directory"
+               return 1
+       }
+       test -f R || {
+               echo "BAD: R does not exist in working directory"
+               return 1
+       }
+
+       test -f A && {
+               echo "BAD: A still exists in working directory"
+               return 1
+       }
+       test -f M && {
+               echo "BAD: M still exists in working directory"
+               return 1
+       }
+       return 0
+'
+
+# This test broke in 8371234ecaaf6e14fe3f2082a855eff1bbd79ae9
+test_expect_success 'merge blue into white (A->B, mod A, A untracked)' \
+'
+       git checkout -b white-blue white &&
+       echo dirty >A &&
+       git merge blue &&
+       git write-tree >/dev/null || {
+               echo "BAD: merge did not complete"
+               return 1
+       }
+
+       test -f A || {
+               echo "BAD: A does not exist in working directory"
+               return 1
+       }
+       test `cat A` = dirty || {
+               echo "BAD: A content is wrong"
+               return 1
+       }
+       test -f B || {
+               echo "BAD: B does not exist in working directory"
+               return 1
+       }
+       test -f N || {
+               echo "BAD: N does not exist in working directory"
+               return 1
+       }
+       test -f M && {
+               echo "BAD: M still exists in working directory"
+               return 1
+       }
+       return 0
+'
+
+test_done
diff --git a/t/t6024-recursive-merge.sh b/t/t6024-recursive-merge.sh
new file mode 100755 (executable)
index 0000000..c154f03
--- /dev/null
@@ -0,0 +1,98 @@
+#!/bin/sh
+
+test_description='Test merge without common ancestors'
+. ./test-lib.sh
+
+# This scenario is based on a real-world repository of Shawn Pearce.
+
+# 1 - A - D - F
+#   \   X   /
+#     B   X
+#       X   \
+# 2 - C - E - G
+
+GIT_COMMITTER_DATE="2006-12-12 23:28:00 +0100"
+export GIT_COMMITTER_DATE
+
+test_expect_success "setup tests" '
+echo 1 > a1 &&
+git add a1 &&
+GIT_AUTHOR_DATE="2006-12-12 23:00:00" git commit -m 1 a1 &&
+
+git checkout -b A master &&
+echo A > a1 &&
+GIT_AUTHOR_DATE="2006-12-12 23:00:01" git commit -m A a1 &&
+
+git checkout -b B master &&
+echo B > a1 &&
+GIT_AUTHOR_DATE="2006-12-12 23:00:02" git commit -m B a1 &&
+
+git checkout -b D A &&
+git rev-parse B > .git/MERGE_HEAD &&
+echo D > a1 &&
+git update-index a1 &&
+GIT_AUTHOR_DATE="2006-12-12 23:00:03" git commit -m D &&
+
+git symbolic-ref HEAD refs/heads/other &&
+echo 2 > a1 &&
+GIT_AUTHOR_DATE="2006-12-12 23:00:04" git commit -m 2 a1 &&
+
+git checkout -b C &&
+echo C > a1 &&
+GIT_AUTHOR_DATE="2006-12-12 23:00:05" git commit -m C a1 &&
+
+git checkout -b E C &&
+git rev-parse B > .git/MERGE_HEAD &&
+echo E > a1 &&
+git update-index a1 &&
+GIT_AUTHOR_DATE="2006-12-12 23:00:06" git commit -m E &&
+
+git checkout -b G E &&
+git rev-parse A > .git/MERGE_HEAD &&
+echo G > a1 &&
+git update-index a1 &&
+GIT_AUTHOR_DATE="2006-12-12 23:00:07" git commit -m G &&
+
+git checkout -b F D &&
+git rev-parse C > .git/MERGE_HEAD &&
+echo F > a1 &&
+git update-index a1 &&
+GIT_AUTHOR_DATE="2006-12-12 23:00:08" git commit -m F
+'
+
+test_expect_failure "combined merge conflicts" "git merge -m final G"
+
+cat > expect << EOF
+<<<<<<< HEAD:a1
+F
+=======
+G
+>>>>>>> G:a1
+EOF
+
+test_expect_success "result contains a conflict" "git diff expect a1"
+
+git ls-files --stage > out
+cat > expect << EOF
+100644 da056ce14a2241509897fa68bb2b3b6e6194ef9e 1      a1
+100644 cf84443e49e1b366fac938711ddf4be2d4d1d9e9 2      a1
+100644 fd7923529855d0b274795ae3349c5e0438333979 3      a1
+EOF
+
+test_expect_success "virtual trees were processed" "git diff expect out"
+
+git reset --hard
+test_expect_success 'refuse to merge binary files' '
+       printf "\0" > binary-file &&
+       git add binary-file &&
+       git commit -m binary &&
+       git checkout G &&
+       printf "\0\0" > binary-file &&
+       git add binary-file &&
+       git commit -m binary2 &&
+       ! git merge F > merge.out 2> merge.err &&
+       grep "Cannot merge binary files: HEAD:binary-file vs. F:binary-file" \
+               merge.err
+'
+
+test_done
diff --git a/t/t6025-merge-symlinks.sh b/t/t6025-merge-symlinks.sh
new file mode 100755 (executable)
index 0000000..950c2e9
--- /dev/null
@@ -0,0 +1,62 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes Sixt
+#
+
+test_description='merging symlinks on filesystem w/o symlink support.
+
+This tests that git-merge-recursive writes merge results as plain files
+if core.symlinks is false.'
+
+. ./test-lib.sh
+
+test_expect_success \
+'setup' '
+git config core.symlinks false &&
+> file &&
+git add file &&
+git-commit -m initial &&
+git branch b-symlink &&
+git branch b-file &&
+l=$(echo -n file | git-hash-object -t blob -w --stdin) &&
+echo "120000 $l        symlink" | git update-index --index-info &&
+git-commit -m master &&
+git-checkout b-symlink &&
+l=$(echo -n file-different | git-hash-object -t blob -w --stdin) &&
+echo "120000 $l        symlink" | git update-index --index-info &&
+git-commit -m b-symlink &&
+git-checkout b-file &&
+echo plain-file > symlink &&
+git add symlink &&
+git-commit -m b-file'
+
+test_expect_failure \
+'merge master into b-symlink, which has a different symbolic link' '
+! git-checkout b-symlink ||
+git-merge master'
+
+test_expect_success \
+'the merge result must be a file' '
+test -f symlink'
+
+test_expect_failure \
+'merge master into b-file, which has a file instead of a symbolic link' '
+! (git-reset --hard &&
+git-checkout b-file) ||
+git-merge master'
+
+test_expect_success \
+'the merge result must be a file' '
+test -f symlink'
+
+test_expect_failure \
+'merge b-file, which has a file instead of a symbolic link, into master' '
+! (git-reset --hard &&
+git-checkout master) ||
+git-merge b-file'
+
+test_expect_success \
+'the merge result must be a file' '
+test -f symlink'
+
+test_done
diff --git a/t/t6026-merge-attr.sh b/t/t6026-merge-attr.sh
new file mode 100755 (executable)
index 0000000..56fc341
--- /dev/null
@@ -0,0 +1,145 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Junio C Hamano
+#
+
+test_description='per path merge controlled by merge attribute'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       for f in text binary union
+       do
+               echo Initial >$f && git add $f || break
+       done &&
+       test_tick &&
+       git commit -m Initial &&
+
+       git branch side &&
+       for f in text binary union
+       do
+               echo Master >>$f && git add $f || break
+       done &&
+       test_tick &&
+       git commit -m Master &&
+
+       git checkout side &&
+       for f in text binary union
+       do
+               echo Side >>$f && git add $f || break
+       done &&
+       test_tick &&
+       git commit -m Side &&
+
+       git tag anchor
+'
+
+test_expect_success merge '
+
+       {
+               echo "binary -merge"
+               echo "union merge=union"
+       } >.gitattributes &&
+
+       if git merge master
+       then
+               echo Gaah, should have conflicted
+               false
+       else
+               echo Ok, conflicted.
+       fi
+'
+
+test_expect_success 'check merge result in index' '
+
+       git ls-files -u | grep binary &&
+       git ls-files -u | grep text &&
+       ! (git ls-files -u | grep union)
+
+'
+
+test_expect_success 'check merge result in working tree' '
+
+       git cat-file -p HEAD:binary >binary-orig &&
+       grep "<<<<<<<" text &&
+       cmp binary-orig binary &&
+       ! grep "<<<<<<<" union &&
+       grep Master union &&
+       grep Side union
+
+'
+
+cat >./custom-merge <<\EOF
+#!/bin/sh
+
+orig="$1" ours="$2" theirs="$3" exit="$4"
+(
+       echo "orig is $orig"
+       echo "ours is $ours"
+       echo "theirs is $theirs"
+       echo "=== orig ==="
+       cat "$orig"
+       echo "=== ours ==="
+       cat "$ours"
+       echo "=== theirs ==="
+       cat "$theirs"
+) >"$ours+"
+cat "$ours+" >"$ours"
+rm -f "$ours+"
+exit "$exit"
+EOF
+chmod +x ./custom-merge
+
+test_expect_success 'custom merge backend' '
+
+       echo "* merge=union" >.gitattributes &&
+       echo "text merge=custom" >>.gitattributes &&
+
+       git reset --hard anchor &&
+       git config --replace-all \
+       merge.custom.driver "./custom-merge %O %A %B 0" &&
+       git config --replace-all \
+       merge.custom.name "custom merge driver for testing" &&
+
+       git merge master &&
+
+       cmp binary union &&
+       sed -e 1,3d text >check-1 &&
+       o=$(git-unpack-file master^:text) &&
+       a=$(git-unpack-file side^:text) &&
+       b=$(git-unpack-file master:text) &&
+       sh -c "./custom-merge $o $a $b 0" &&
+       sed -e 1,3d $a >check-2 &&
+       cmp check-1 check-2 &&
+       rm -f $o $a $b
+'
+
+test_expect_success 'custom merge backend' '
+
+       git reset --hard anchor &&
+       git config --replace-all \
+       merge.custom.driver "./custom-merge %O %A %B 1" &&
+       git config --replace-all \
+       merge.custom.name "custom merge driver for testing" &&
+
+       if git merge master
+       then
+               echo "Eh? should have conflicted"
+               false
+       else
+               echo "Ok, conflicted"
+       fi &&
+
+       cmp binary union &&
+       sed -e 1,3d text >check-1 &&
+       o=$(git-unpack-file master^:text) &&
+       a=$(git-unpack-file anchor:text) &&
+       b=$(git-unpack-file master:text) &&
+       sh -c "./custom-merge $o $a $b 0" &&
+       sed -e 1,3d $a >check-2 &&
+       cmp check-1 check-2 &&
+       rm -f $o $a $b
+'
+
+test_done
diff --git a/t/t6027-merge-binary.sh b/t/t6027-merge-binary.sh
new file mode 100755 (executable)
index 0000000..a7358f7
--- /dev/null
@@ -0,0 +1,67 @@
+#!/bin/sh
+
+test_description='ask merge-recursive to merge binary files'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       cat ../test4012.png >m &&
+       git add m &&
+       git ls-files -s | sed -e "s/ 0  / 1     /" >E1 &&
+       test_tick &&
+       git commit -m "initial" &&
+
+       git branch side &&
+       echo frotz >a &&
+       git add a &&
+       echo nitfol >>m &&
+       git add a m &&
+       git ls-files -s a >E0 &&
+       git ls-files -s m | sed -e "s/ 0        / 3     /" >E3 &&
+       test_tick &&
+       git commit -m "master adds some" &&
+
+       git checkout side &&
+       echo rezrov >>m &&
+       git add m &&
+       git ls-files -s m | sed -e "s/ 0        / 2     /" >E2 &&
+       test_tick &&
+       git commit -m "side modifies" &&
+
+       git tag anchor &&
+
+       cat E0 E1 E2 E3 >expect
+'
+
+test_expect_success resolve '
+
+       rm -f a* m* &&
+       git reset --hard anchor &&
+
+       if git merge -s resolve master
+       then
+               echo Oops, should not have succeeded
+               false
+       else
+               git ls-files -s >current
+               diff -u current expect
+       fi
+'
+
+test_expect_success recursive '
+
+       rm -f a* m* &&
+       git reset --hard anchor &&
+
+       if git merge -s recursive master
+       then
+               echo Oops, should not have succeeded
+               false
+       else
+               git ls-files -s >current
+               diff -u current expect
+       fi
+'
+
+test_done
diff --git a/t/t6028-merge-up-to-date.sh b/t/t6028-merge-up-to-date.sh
new file mode 100755 (executable)
index 0000000..f8f3e3f
--- /dev/null
@@ -0,0 +1,77 @@
+#!/bin/sh
+
+test_description='merge fast forward and up to date'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       >file &&
+       git add file &&
+       test_tick &&
+       git commit -m initial &&
+       git tag c0 &&
+
+       echo second >file &&
+       git add file &&
+       test_tick &&
+       git commit -m second &&
+       git tag c1 &&
+       git branch test
+'
+
+test_expect_success 'merge -s recursive up-to-date' '
+
+       git reset --hard c1 &&
+       test_tick &&
+       git merge -s recursive c0 &&
+       expect=$(git rev-parse c1) &&
+       current=$(git rev-parse HEAD) &&
+       test "$expect" = "$current"
+
+'
+
+test_expect_success 'merge -s recursive fast-forward' '
+
+       git reset --hard c0 &&
+       test_tick &&
+       git merge -s recursive c1 &&
+       expect=$(git rev-parse c1) &&
+       current=$(git rev-parse HEAD) &&
+       test "$expect" = "$current"
+
+'
+
+test_expect_success 'merge -s ours up-to-date' '
+
+       git reset --hard c1 &&
+       test_tick &&
+       git merge -s ours c0 &&
+       expect=$(git rev-parse c1) &&
+       current=$(git rev-parse HEAD) &&
+       test "$expect" = "$current"
+
+'
+
+test_expect_success 'merge -s ours fast-forward' '
+
+       git reset --hard c0 &&
+       test_tick &&
+       git merge -s ours c1 &&
+       expect=$(git rev-parse c0^{tree}) &&
+       current=$(git rev-parse HEAD^{tree}) &&
+       test "$expect" = "$current"
+
+'
+
+test_expect_success 'merge -s subtree up-to-date' '
+
+       git reset --hard c1 &&
+       test_tick &&
+       git merge -s subtree c0 &&
+       expect=$(git rev-parse c1) &&
+       current=$(git rev-parse HEAD) &&
+       test "$expect" = "$current"
+
+'
+
+test_done
diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh
new file mode 100755 (executable)
index 0000000..2ba4b00
--- /dev/null
@@ -0,0 +1,259 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Christian Couder
+#
+test_description='Tests git-bisect functionality'
+
+exec </dev/null
+
+. ./test-lib.sh
+
+add_line_into_file()
+{
+    _line=$1
+    _file=$2
+
+    if [ -f "$_file" ]; then
+        echo "$_line" >> $_file || return $?
+        MSG="Add <$_line> into <$_file>."
+    else
+        echo "$_line" > $_file || return $?
+        git add $_file || return $?
+        MSG="Create file <$_file> with <$_line> inside."
+    fi
+
+    test_tick
+    git-commit --quiet -m "$MSG" $_file
+}
+
+HASH1=
+HASH2=
+HASH3=
+HASH4=
+
+test_expect_success 'set up basic repo with 1 file (hello) and 4 commits' '
+     add_line_into_file "1: Hello World" hello &&
+     HASH1=$(git rev-parse --verify HEAD) &&
+     add_line_into_file "2: A new day for git" hello &&
+     HASH2=$(git rev-parse --verify HEAD) &&
+     add_line_into_file "3: Another new day for git" hello &&
+     HASH3=$(git rev-parse --verify HEAD) &&
+     add_line_into_file "4: Ciao for now" hello &&
+     HASH4=$(git rev-parse --verify HEAD)
+'
+
+test_expect_success 'bisect starts with only one bad' '
+       git bisect reset &&
+       git bisect start &&
+       git bisect bad $HASH4 &&
+       git bisect next
+'
+
+test_expect_success 'bisect does not start with only one good' '
+       git bisect reset &&
+       git bisect start &&
+       git bisect good $HASH1 || return 1
+
+       if git bisect next
+       then
+               echo Oops, should have failed.
+               false
+       else
+               :
+       fi
+'
+
+test_expect_success 'bisect start with one bad and good' '
+       git bisect reset &&
+       git bisect start &&
+       git bisect good $HASH1 &&
+       git bisect bad $HASH4 &&
+       git bisect next
+'
+
+test_expect_success 'bisect reset: back in the master branch' '
+       git bisect reset &&
+       echo "* master" > branch.expect &&
+       git branch > branch.output &&
+       cmp branch.expect branch.output
+'
+
+test_expect_success 'bisect reset: back in another branch' '
+       git checkout -b other &&
+       git bisect start &&
+       git bisect good $HASH1 &&
+       git bisect bad $HASH3 &&
+       git bisect reset &&
+       echo "  master" > branch.expect &&
+       echo "* other" >> branch.expect &&
+       git branch > branch.output &&
+       cmp branch.expect branch.output
+'
+
+test_expect_success 'bisect reset when not bisecting' '
+       git bisect reset &&
+       git branch > branch.output &&
+       cmp branch.expect branch.output
+'
+
+test_expect_success 'bisect reset removes packed refs' '
+       git bisect reset &&
+       git bisect start &&
+       git bisect good $HASH1 &&
+       git bisect bad $HASH3 &&
+       git pack-refs --all --prune &&
+       git bisect next &&
+       git bisect reset &&
+       test -z "$(git for-each-ref "refs/bisect/*")" &&
+       test -z "$(git for-each-ref "refs/heads/bisect")"
+'
+
+# $HASH1 is good, $HASH4 is bad, we skip $HASH3
+# but $HASH2 is bad,
+# so we should find $HASH2 as the first bad commit
+test_expect_success 'bisect skip: successfull result' '
+       git bisect reset &&
+       git bisect start $HASH4 $HASH1 &&
+       git bisect skip &&
+       git bisect bad > my_bisect_log.txt &&
+       grep "$HASH2 is first bad commit" my_bisect_log.txt &&
+       git bisect reset
+'
+
+# $HASH1 is good, $HASH4 is bad, we skip $HASH3 and $HASH2
+# so we should not be able to tell the first bad commit
+# among $HASH2, $HASH3 and $HASH4
+test_expect_success 'bisect skip: cannot tell between 3 commits' '
+       git bisect start $HASH4 $HASH1 &&
+       git bisect skip || return 1
+
+       if git bisect skip > my_bisect_log.txt
+       then
+               echo Oops, should have failed.
+               false
+       else
+               test $? -eq 2 &&
+               grep "first bad commit could be any of" my_bisect_log.txt &&
+               ! grep $HASH1 my_bisect_log.txt &&
+               grep $HASH2 my_bisect_log.txt &&
+               grep $HASH3 my_bisect_log.txt &&
+               grep $HASH4 my_bisect_log.txt &&
+               git bisect reset
+       fi
+'
+
+# $HASH1 is good, $HASH4 is bad, we skip $HASH3
+# but $HASH2 is good,
+# so we should not be able to tell the first bad commit
+# among $HASH3 and $HASH4
+test_expect_success 'bisect skip: cannot tell between 2 commits' '
+       git bisect start $HASH4 $HASH1 &&
+       git bisect skip || return 1
+
+       if git bisect good > my_bisect_log.txt
+       then
+               echo Oops, should have failed.
+               false
+       else
+               test $? -eq 2 &&
+               grep "first bad commit could be any of" my_bisect_log.txt &&
+               ! grep $HASH1 my_bisect_log.txt &&
+               ! grep $HASH2 my_bisect_log.txt &&
+               grep $HASH3 my_bisect_log.txt &&
+               grep $HASH4 my_bisect_log.txt &&
+               git bisect reset
+       fi
+'
+
+# We want to automatically find the commit that
+# introduced "Another" into hello.
+test_expect_success \
+    '"git bisect run" simple case' \
+    'echo "#"\!"/bin/sh" > test_script.sh &&
+     echo "grep Another hello > /dev/null" >> test_script.sh &&
+     echo "test \$? -ne 0" >> test_script.sh &&
+     chmod +x test_script.sh &&
+     git bisect start &&
+     git bisect good $HASH1 &&
+     git bisect bad $HASH4 &&
+     git bisect run ./test_script.sh > my_bisect_log.txt &&
+     grep "$HASH3 is first bad commit" my_bisect_log.txt &&
+     git bisect reset'
+
+# We want to automatically find the commit that
+# introduced "Ciao" into hello.
+test_expect_success \
+    '"git bisect run" with more complex "git bisect start"' \
+    'echo "#"\!"/bin/sh" > test_script.sh &&
+     echo "grep Ciao hello > /dev/null" >> test_script.sh &&
+     echo "test \$? -ne 0" >> test_script.sh &&
+     chmod +x test_script.sh &&
+     git bisect start $HASH4 $HASH1 &&
+     git bisect run ./test_script.sh > my_bisect_log.txt &&
+     grep "$HASH4 is first bad commit" my_bisect_log.txt &&
+     git bisect reset'
+
+# $HASH1 is good, $HASH5 is bad, we skip $HASH3
+# but $HASH4 is good,
+# so we should find $HASH5 as the first bad commit
+HASH5=
+test_expect_success 'bisect skip: add line and then a new test' '
+       add_line_into_file "5: Another new line." hello &&
+       HASH5=$(git rev-parse --verify HEAD) &&
+       git bisect start $HASH5 $HASH1 &&
+       git bisect skip &&
+       git bisect good > my_bisect_log.txt &&
+       grep "$HASH5 is first bad commit" my_bisect_log.txt &&
+       git bisect log > log_to_replay.txt &&
+       git bisect reset
+'
+
+test_expect_success 'bisect skip and bisect replay' '
+       git bisect replay log_to_replay.txt > my_bisect_log.txt &&
+       grep "$HASH5 is first bad commit" my_bisect_log.txt &&
+       git bisect reset
+'
+
+HASH6=
+test_expect_success 'bisect run & skip: cannot tell between 2' '
+       add_line_into_file "6: Yet a line." hello &&
+       HASH6=$(git rev-parse --verify HEAD) &&
+       echo "#"\!"/bin/sh" > test_script.sh &&
+       echo "tail -1 hello | grep Ciao > /dev/null && exit 125" >> test_script.sh &&
+       echo "grep line hello > /dev/null" >> test_script.sh &&
+       echo "test \$? -ne 0" >> test_script.sh &&
+       chmod +x test_script.sh &&
+       git bisect start $HASH6 $HASH1 &&
+       if git bisect run ./test_script.sh > my_bisect_log.txt
+       then
+               echo Oops, should have failed.
+               false
+       else
+               test $? -eq 2 &&
+               grep "first bad commit could be any of" my_bisect_log.txt &&
+               ! grep $HASH3 my_bisect_log.txt &&
+               ! grep $HASH6 my_bisect_log.txt &&
+               grep $HASH4 my_bisect_log.txt &&
+               grep $HASH5 my_bisect_log.txt
+       fi
+'
+
+HASH7=
+test_expect_success 'bisect run & skip: find first bad' '
+       git bisect reset &&
+       add_line_into_file "7: Should be the last line." hello &&
+       HASH7=$(git rev-parse --verify HEAD) &&
+       echo "#"\!"/bin/sh" > test_script.sh &&
+       echo "tail -1 hello | grep Ciao > /dev/null && exit 125" >> test_script.sh &&
+       echo "tail -1 hello | grep day > /dev/null && exit 125" >> test_script.sh &&
+       echo "grep Yet hello > /dev/null" >> test_script.sh &&
+       echo "test \$? -ne 0" >> test_script.sh &&
+       chmod +x test_script.sh &&
+       git bisect start $HASH7 $HASH1 &&
+       git bisect run ./test_script.sh > my_bisect_log.txt &&
+       grep "$HASH6 is first bad commit" my_bisect_log.txt
+'
+
+#
+#
+test_done
diff --git a/t/t6101-rev-parse-parents.sh b/t/t6101-rev-parse-parents.sh
new file mode 100755 (executable)
index 0000000..0724864
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Johannes Schindelin
+#
+
+test_description='Test git rev-parse with different parent options'
+
+. ./test-lib.sh
+. ../t6000lib.sh # t6xxx specific functions
+
+date >path0
+git update-index --add path0
+save_tag tree git write-tree
+hide_error save_tag start unique_commit "start" tree
+save_tag second unique_commit "second" tree -p start
+hide_error save_tag start2 unique_commit "start2" tree
+save_tag two_parents unique_commit "next" tree -p second -p start2
+save_tag final unique_commit "final" tree -p two_parents
+
+test_expect_success 'start is valid' 'git rev-parse start | grep "^[0-9a-f]\{40\}$"'
+test_expect_success 'start^0' "test $(cat .git/refs/tags/start) = $(git rev-parse start^0)"
+test_expect_success 'start^1 not valid' "if git rev-parse --verify start^1; then false; else :; fi"
+test_expect_success 'second^1 = second^' "test $(git rev-parse second^1) = $(git rev-parse second^)"
+test_expect_success 'final^1^1^1' "test $(git rev-parse start) = $(git rev-parse final^1^1^1)"
+test_expect_success 'final^1^1^1 = final^^^' "test $(git rev-parse final^1^1^1) = $(git rev-parse final^^^)"
+test_expect_success 'final^1^2' "test $(git rev-parse start2) = $(git rev-parse final^1^2)"
+test_expect_success 'final^1^2 != final^1^1' "test $(git rev-parse final^1^2) != $(git rev-parse final^1^1)"
+test_expect_success 'final^1^3 not valid' "if git rev-parse --verify final^1^3; then false; else :; fi"
+test_expect_failure '--verify start2^1' 'git rev-parse --verify start2^1'
+test_expect_success '--verify start2^0' 'git rev-parse --verify start2^0'
+
+test_expect_success 'repack for next test' 'git repack -a -d'
+test_expect_success 'short SHA-1 works' '
+       start=`git rev-parse --verify start` &&
+       echo $start &&
+       abbrv=`echo $start | sed s/.\$//` &&
+       echo $abbrv &&
+       abbrv=`git rev-parse --verify $abbrv` &&
+       echo $abbrv &&
+       test $start = $abbrv'
+
+test_done
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
new file mode 100755 (executable)
index 0000000..ae8ee11
--- /dev/null
@@ -0,0 +1,97 @@
+#!/bin/sh
+
+test_description='test describe
+
+                       B
+        .--------------o----o----o----x
+       /                   /    /
+ o----o----o----o----o----.    /
+       \        A    c        /
+        .------------o---o---o
+                     D   e
+'
+. ./test-lib.sh
+
+check_describe () {
+       expect="$1"
+       shift
+       R=$(git describe "$@") &&
+       test_expect_success "describe $*" '
+       case "$R" in
+       $expect)        echo happy ;;
+       *)      echo "Oops - $R is not $expect";
+               false ;;
+       esac
+       '
+}
+
+test_expect_success setup '
+
+       test_tick &&
+       echo one >file && git add file && git-commit -m initial &&
+       one=$(git rev-parse HEAD) &&
+
+       test_tick &&
+       echo two >file && git add file && git-commit -m second &&
+       two=$(git rev-parse HEAD) &&
+
+       test_tick &&
+       echo three >file && git add file && git-commit -m third &&
+
+       test_tick &&
+       echo A >file && git add file && git-commit -m A &&
+       test_tick &&
+       git-tag -a -m A A &&
+
+       test_tick &&
+       echo c >file && git add file && git-commit -m c &&
+       test_tick &&
+       git-tag c &&
+
+       git reset --hard $two &&
+       test_tick &&
+       echo B >side && git add side && git-commit -m B &&
+       test_tick &&
+       git-tag -a -m B B &&
+
+       test_tick &&
+       git-merge -m Merged c &&
+       merged=$(git rev-parse HEAD) &&
+
+       git reset --hard $two &&
+       test_tick &&
+       echo D >another && git add another && git-commit -m D &&
+       test_tick &&
+       git-tag -a -m D D &&
+
+       test_tick &&
+       echo DD >another && git commit -a -m another &&
+
+       test_tick &&
+       git-tag e &&
+
+       test_tick &&
+       echo DDD >another && git commit -a -m "yet another" &&
+
+       test_tick &&
+       git-merge -m Merged $merged &&
+
+       test_tick &&
+       echo X >file && echo X >side && git add file side &&
+       git-commit -m x
+
+'
+
+check_describe A-* HEAD
+check_describe A-* HEAD^
+check_describe D-* HEAD^^
+check_describe A-* HEAD^^2
+check_describe B HEAD^^2^
+
+check_describe A-* --tags HEAD
+check_describe A-* --tags HEAD^
+check_describe D-* --tags HEAD^^
+check_describe A-* --tags HEAD^^2
+check_describe B --tags HEAD^^2^
+
+test_done
diff --git a/t/t6200-fmt-merge-msg.sh b/t/t6200-fmt-merge-msg.sh
new file mode 100755 (executable)
index 0000000..526d7d1
--- /dev/null
@@ -0,0 +1,163 @@
+#!/bin/sh
+#
+# Copyright (c) 2006, Junio C Hamano
+#
+
+test_description='fmt-merge-msg test'
+
+. ./test-lib.sh
+
+datestamp=1151939923
+setdate () {
+       GIT_COMMITTER_DATE="$datestamp +0200"
+       GIT_AUTHOR_DATE="$datestamp +0200"
+       datestamp=`expr "$datestamp" + 1`
+       export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
+}
+
+test_expect_success setup '
+       echo one >one &&
+       git add one &&
+       setdate &&
+       git commit -m "Initial" &&
+
+       echo uno >one &&
+       echo dos >two &&
+       git add two &&
+       setdate &&
+       git commit -a -m "Second" &&
+
+       git checkout -b left &&
+
+       echo $datestamp >one &&
+       setdate &&
+       git commit -a -m "Common #1" &&
+
+       echo $datestamp >one &&
+       setdate &&
+       git commit -a -m "Common #2" &&
+
+       git branch right &&
+
+       echo $datestamp >two &&
+       setdate &&
+       git commit -a -m "Left #3" &&
+
+       echo $datestamp >two &&
+       setdate &&
+       git commit -a -m "Left #4" &&
+
+       echo $datestamp >two &&
+       setdate &&
+       git commit -a -m "Left #5" &&
+
+       git checkout right &&
+
+       echo $datestamp >three &&
+       git add three &&
+       setdate &&
+       git commit -a -m "Right #3" &&
+
+       echo $datestamp >three &&
+       setdate &&
+       git commit -a -m "Right #4" &&
+
+       echo $datestamp >three &&
+       setdate &&
+       git commit -a -m "Right #5" &&
+
+       git show-branch
+'
+
+cat >expected <<\EOF
+Merge branch 'left'
+EOF
+
+test_expect_success 'merge-msg test #1' '
+
+       git checkout master &&
+       git fetch . left &&
+
+       git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+       git diff actual expected
+'
+
+cat >expected <<\EOF
+Merge branch 'left' of ../trash
+EOF
+
+test_expect_success 'merge-msg test #2' '
+
+       git checkout master &&
+       git fetch ../trash left &&
+
+       git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+       git diff actual expected
+'
+
+cat >expected <<\EOF
+Merge branch 'left'
+
+* left:
+  Left #5
+  Left #4
+  Left #3
+  Common #2
+  Common #1
+EOF
+
+test_expect_success 'merge-msg test #3' '
+
+       git config merge.summary true &&
+
+       git checkout master &&
+       setdate &&
+       git fetch . left &&
+
+       git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+       git diff actual expected
+'
+
+cat >expected <<\EOF
+Merge branches 'left' and 'right'
+
+* left:
+  Left #5
+  Left #4
+  Left #3
+  Common #2
+  Common #1
+
+* right:
+  Right #5
+  Right #4
+  Right #3
+  Common #2
+  Common #1
+EOF
+
+test_expect_success 'merge-msg test #4' '
+
+       git config merge.summary true &&
+
+       git checkout master &&
+       setdate &&
+       git fetch . left right &&
+
+       git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+       git diff actual expected
+'
+
+test_expect_success 'merge-msg test #5' '
+
+       git config merge.summary yes &&
+
+       git checkout master &&
+       setdate &&
+       git fetch . left right &&
+
+       git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+       git diff actual expected
+'
+
+test_done
diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh
new file mode 100755 (executable)
index 0000000..8a23aaf
--- /dev/null
@@ -0,0 +1,212 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Andy Parkins
+#
+
+test_description='for-each-ref test'
+
+. ./test-lib.sh
+
+# Mon Jul 3 15:18:43 2006 +0000
+datestamp=1151939923
+setdate_and_increment () {
+    GIT_COMMITTER_DATE="$datestamp +0200"
+    datestamp=$(expr "$datestamp" + 1)
+    GIT_AUTHOR_DATE="$datestamp +0200"
+    datestamp=$(expr "$datestamp" + 1)
+    export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
+}
+
+test_expect_success 'Create sample commit with known timestamp' '
+       setdate_and_increment &&
+       echo "Using $datestamp" > one &&
+       git add one &&
+       git commit -m "Initial" &&
+       setdate_and_increment &&
+       git tag -a -m "Tagging at $datestamp" testtag
+'
+
+test_expect_success 'Check atom names are valid' '
+       bad=
+       for token in \
+               refname objecttype objectsize objectname tree parent \
+               numparent object type author authorname authoremail \
+               authordate committer committername committeremail \
+               committerdate tag tagger taggername taggeremail \
+               taggerdate creator creatordate subject body contents
+       do
+               git for-each-ref --format="$token=%($token)" refs/heads || {
+                       bad=$token
+                       break
+               }
+       done
+       test -z "$bad"
+'
+
+test_expect_failure 'Check invalid atoms names are errors' '
+       git-for-each-ref --format="%(INVALID)" refs/heads
+'
+
+test_expect_success 'Check format specifiers are ignored in naming date atoms' '
+       git-for-each-ref --format="%(authordate)" refs/heads &&
+       git-for-each-ref --format="%(authordate:default) %(authordate)" refs/heads &&
+       git-for-each-ref --format="%(authordate) %(authordate:default)" refs/heads &&
+       git-for-each-ref --format="%(authordate:default) %(authordate:default)" refs/heads
+'
+
+test_expect_success 'Check valid format specifiers for date fields' '
+       git-for-each-ref --format="%(authordate:default)" refs/heads &&
+       git-for-each-ref --format="%(authordate:relative)" refs/heads &&
+       git-for-each-ref --format="%(authordate:short)" refs/heads &&
+       git-for-each-ref --format="%(authordate:local)" refs/heads &&
+       git-for-each-ref --format="%(authordate:iso8601)" refs/heads &&
+       git-for-each-ref --format="%(authordate:rfc2822)" refs/heads
+'
+
+test_expect_failure 'Check invalid format specifiers are errors' '
+       git-for-each-ref --format="%(authordate:INVALID)" refs/heads
+'
+
+cat >expected <<\EOF
+'refs/heads/master' 'Mon Jul 3 17:18:43 2006 +0200' 'Mon Jul 3 17:18:44 2006 +0200'
+'refs/tags/testtag' 'Mon Jul 3 17:18:45 2006 +0200'
+EOF
+
+test_expect_success 'Check unformatted date fields output' '
+       (git for-each-ref --shell --format="%(refname) %(committerdate) %(authordate)" refs/heads &&
+       git for-each-ref --shell --format="%(refname) %(taggerdate)" refs/tags) >actual &&
+       git diff expected actual
+'
+
+test_expect_success 'Check format "default" formatted date fields output' '
+       f=default &&
+       (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
+       git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
+       git diff expected actual
+'
+
+# Don't know how to do relative check because I can't know when this script
+# is going to be run and can't fake the current time to git, and hence can't
+# provide expected output.  Instead, I'll just make sure that "relative"
+# doesn't exit in error
+#
+#cat >expected <<\EOF
+#
+#EOF
+#
+test_expect_success 'Check format "relative" date fields output' '
+       f=relative &&
+       (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
+       git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual
+'
+
+cat >expected <<\EOF
+'refs/heads/master' '2006-07-03' '2006-07-03'
+'refs/tags/testtag' '2006-07-03'
+EOF
+
+test_expect_success 'Check format "short" date fields output' '
+       f=short &&
+       (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
+       git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
+       git diff expected actual
+'
+
+cat >expected <<\EOF
+'refs/heads/master' 'Mon Jul 3 15:18:43 2006' 'Mon Jul 3 15:18:44 2006'
+'refs/tags/testtag' 'Mon Jul 3 15:18:45 2006'
+EOF
+
+test_expect_success 'Check format "local" date fields output' '
+       f=local &&
+       (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
+       git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
+       git diff expected actual
+'
+
+cat >expected <<\EOF
+'refs/heads/master' '2006-07-03 17:18:43 +0200' '2006-07-03 17:18:44 +0200'
+'refs/tags/testtag' '2006-07-03 17:18:45 +0200'
+EOF
+
+test_expect_success 'Check format "iso8601" date fields output' '
+       f=iso8601 &&
+       (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
+       git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
+       git diff expected actual
+'
+
+cat >expected <<\EOF
+'refs/heads/master' 'Mon, 3 Jul 2006 17:18:43 +0200' 'Mon, 3 Jul 2006 17:18:44 +0200'
+'refs/tags/testtag' 'Mon, 3 Jul 2006 17:18:45 +0200'
+EOF
+
+test_expect_success 'Check format "rfc2822" date fields output' '
+       f=rfc2822 &&
+       (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
+       git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
+       git diff expected actual
+'
+
+cat >expected <<\EOF
+refs/heads/master
+refs/tags/testtag
+EOF
+
+test_expect_success 'Verify ascending sort' '
+       git-for-each-ref --format="%(refname)" --sort=refname >actual &&
+       git diff expected actual
+'
+
+
+cat >expected <<\EOF
+refs/tags/testtag
+refs/heads/master
+EOF
+
+test_expect_success 'Verify descending sort' '
+       git-for-each-ref --format="%(refname)" --sort=-refname >actual &&
+       git diff expected actual
+'
+
+cat >expected <<\EOF
+'refs/heads/master'
+'refs/tags/testtag'
+EOF
+
+test_expect_success 'Quoting style: shell' '
+       git for-each-ref --shell --format="%(refname)" >actual &&
+       git diff expected actual
+'
+
+test_expect_success 'Quoting style: perl' '
+       git for-each-ref --perl --format="%(refname)" >actual &&
+       git diff expected actual
+'
+
+test_expect_success 'Quoting style: python' '
+       git for-each-ref --python --format="%(refname)" >actual &&
+       git diff expected actual
+'
+
+cat >expected <<\EOF
+"refs/heads/master"
+"refs/tags/testtag"
+EOF
+
+test_expect_success 'Quoting style: tcl' '
+       git for-each-ref --tcl --format="%(refname)" >actual &&
+       git diff expected actual
+'
+
+for i in "--perl --shell" "-s --python" "--python --tcl" "--tcl --perl"; do
+       test_expect_success "more than one quoting style: $i" "
+               git for-each-ref $i 2>&1 | (read line &&
+               case \$line in
+               \"error: more than one quoting style\"*) : happy;;
+               *) false
+               esac)
+       "
+done
+
+test_done
diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh
new file mode 100755 (executable)
index 0000000..8b43fb5
--- /dev/null
@@ -0,0 +1,121 @@
+#!/bin/sh
+
+test_description='git mv in subdirs'
+. ./test-lib.sh
+
+test_expect_success \
+    'prepare reference tree' \
+    'mkdir path0 path1 &&
+     cp ../../COPYING path0/COPYING &&
+     git add path0/COPYING &&
+     git-commit -m add -a'
+
+test_expect_success \
+    'moving the file out of subdirectory' \
+    'cd path0 && git mv COPYING ../path1/COPYING'
+
+# in path0 currently
+test_expect_success \
+    'commiting the change' \
+    'cd .. && git-commit -m move-out -a'
+
+test_expect_success \
+    'checking the commit' \
+    'git diff-tree -r -M --name-status  HEAD^ HEAD | \
+    grep -E "^R100.+path0/COPYING.+path1/COPYING"'
+
+test_expect_success \
+    'moving the file back into subdirectory' \
+    'cd path0 && git mv ../path1/COPYING COPYING'
+
+# in path0 currently
+test_expect_success \
+    'commiting the change' \
+    'cd .. && git-commit -m move-in -a'
+
+test_expect_success \
+    'checking the commit' \
+    'git diff-tree -r -M --name-status  HEAD^ HEAD | \
+    grep -E "^R100.+path1/COPYING.+path0/COPYING"'
+
+test_expect_success \
+    'adding another file' \
+    'cp ../../README path0/README &&
+     git add path0/README &&
+     git-commit -m add2 -a'
+
+test_expect_success \
+    'moving whole subdirectory' \
+    'git mv path0 path2'
+
+test_expect_success \
+    'commiting the change' \
+    'git-commit -m dir-move -a'
+
+test_expect_success \
+    'checking the commit' \
+    'git diff-tree -r -M --name-status  HEAD^ HEAD | \
+     grep -E "^R100.+path0/COPYING.+path2/COPYING" &&
+     git diff-tree -r -M --name-status  HEAD^ HEAD | \
+     grep -E "^R100.+path0/README.+path2/README"'
+
+test_expect_success \
+    'succeed when source is a prefix of destination' \
+    'git mv path2/COPYING path2/COPYING-renamed'
+
+test_expect_success \
+    'moving whole subdirectory into subdirectory' \
+    'git mv path2 path1'
+
+test_expect_success \
+    'commiting the change' \
+    'git-commit -m dir-move -a'
+
+test_expect_success \
+    'checking the commit' \
+    'git diff-tree -r -M --name-status  HEAD^ HEAD | \
+     grep -E "^R100.+path2/COPYING.+path1/path2/COPYING" &&
+     git diff-tree -r -M --name-status  HEAD^ HEAD | \
+     grep -E "^R100.+path2/README.+path1/path2/README"'
+
+test_expect_failure \
+    'do not move directory over existing directory' \
+    'mkdir path0 && mkdir path0/path2 && git mv path2 path0'
+
+test_expect_success \
+    'move into "."' \
+    'git mv path1/path2/ .'
+
+test_expect_success "Michael Cassar's test case" '
+       rm -fr .git papers partA &&
+       git init &&
+       mkdir -p papers/unsorted papers/all-papers partA &&
+       echo a > papers/unsorted/Thesis.pdf &&
+       echo b > partA/outline.txt &&
+       echo c > papers/unsorted/_another &&
+       git add papers partA &&
+       T1=`git write-tree` &&
+
+       git mv papers/unsorted/Thesis.pdf papers/all-papers/moo-blah.pdf &&
+
+       T=`git write-tree` &&
+       git ls-tree -r $T | grep partA/outline.txt || {
+               git ls-tree -r $T
+               (exit 1)
+       }
+'
+
+rm -fr papers partA path?
+
+test_expect_success "Sergey Vlasov's test case" '
+       rm -fr .git &&
+       git init &&
+       mkdir ab &&
+       date >ab.c &&
+       date >ab/d &&
+       git add ab.c ab &&
+       git commit -m 'initial' &&
+       git mv ab a
+'
+
+test_done
diff --git a/t/t7002-grep.sh b/t/t7002-grep.sh
new file mode 100755 (executable)
index 0000000..68b2b92
--- /dev/null
@@ -0,0 +1,116 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='git grep various.
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       {
+               echo foo mmap bar
+               echo foo_mmap bar
+               echo foo_mmap bar mmap
+               echo foo mmap bar_mmap
+               echo foo_mmap bar mmap baz
+       } >file &&
+       echo x x xx x >x &&
+       echo y yy >y &&
+       echo zzz > z &&
+       mkdir t &&
+       echo test >t/t &&
+       git add file x y z t/t &&
+       git commit -m initial
+'
+
+for H in HEAD ''
+do
+       case "$H" in
+       HEAD)   HC='HEAD:' L='HEAD' ;;
+       '')     HC= L='in working tree' ;;
+       esac
+
+       test_expect_success "grep -w $L" '
+               {
+                       echo ${HC}file:1:foo mmap bar
+                       echo ${HC}file:3:foo_mmap bar mmap
+                       echo ${HC}file:4:foo mmap bar_mmap
+                       echo ${HC}file:5:foo_mmap bar mmap baz
+               } >expected &&
+               git grep -n -w -e mmap $H >actual &&
+               diff expected actual
+       '
+
+       test_expect_success "grep -w $L (x)" '
+               {
+                       echo ${HC}x:1:x x xx x
+               } >expected &&
+               git grep -n -w -e "x xx* x" $H >actual &&
+               diff expected actual
+       '
+
+       test_expect_success "grep -w $L (y-1)" '
+               {
+                       echo ${HC}y:1:y yy
+               } >expected &&
+               git grep -n -w -e "^y" $H >actual &&
+               diff expected actual
+       '
+
+       test_expect_success "grep -w $L (y-2)" '
+               : >expected &&
+               if git grep -n -w -e "^y y" $H >actual
+               then
+                       echo should not have matched
+                       cat actual
+                       false
+               else
+                       diff expected actual
+               fi
+       '
+
+       test_expect_success "grep -w $L (z)" '
+               : >expected &&
+               if git grep -n -w -e "^z" $H >actual
+               then
+                       echo should not have matched
+                       cat actual
+                       false
+               else
+                       diff expected actual
+               fi
+       '
+
+       test_expect_success "grep $L (t-1)" '
+               echo "${HC}t/t:1:test" >expected &&
+               git grep -n -e test $H >actual &&
+               diff expected actual
+       '
+
+       test_expect_success "grep $L (t-2)" '
+               echo "${HC}t:1:test" >expected &&
+               (
+                       cd t &&
+                       git grep -n -e test $H
+               ) >actual &&
+               diff expected actual
+       '
+
+       test_expect_success "grep $L (t-3)" '
+               echo "${HC}t/t:1:test" >expected &&
+               (
+                       cd t &&
+                       git grep --full-name -n -e test $H
+               ) >actual &&
+               diff expected actual
+       '
+
+        test_expect_failure "grep -c $L (no /dev/null)" '
+               git grep -c test $H | grep -q "/dev/null"
+        '
+
+done
+
+test_done
diff --git a/t/t7003-filter-branch.sh b/t/t7003-filter-branch.sh
new file mode 100755 (executable)
index 0000000..5f60b22
--- /dev/null
@@ -0,0 +1,168 @@
+#!/bin/sh
+
+test_description='git-filter-branch'
+. ./test-lib.sh
+
+make_commit () {
+       lower=$(echo $1 | tr A-Z a-z)
+       echo $lower > $lower
+       git add $lower
+       test_tick
+       git commit -m $1
+       git tag $1
+}
+
+test_expect_success 'setup' '
+       make_commit A
+       make_commit B
+       git checkout -b branch B
+       make_commit D
+       make_commit E
+       git checkout master
+       make_commit C
+       git checkout branch
+       git merge C
+       git tag F
+       make_commit G
+       make_commit H
+'
+
+H=$(git rev-parse H)
+
+test_expect_success 'rewrite identically' '
+       git-filter-branch branch
+'
+test_expect_success 'result is really identical' '
+       test $H = $(git rev-parse HEAD)
+'
+
+test_expect_success 'rewrite, renaming a specific file' '
+       git-filter-branch -f --tree-filter "mv d doh || :" HEAD
+'
+
+test_expect_success 'test that the file was renamed' '
+       test d = $(git show HEAD:doh) &&
+       test -f doh &&
+       test d = $(cat doh)
+'
+
+git tag oldD HEAD~4
+test_expect_success 'rewrite one branch, keeping a side branch' '
+       git branch modD oldD &&
+       git-filter-branch -f --tree-filter "mv b boh || :" D..modD
+'
+
+test_expect_success 'common ancestor is still common (unchanged)' '
+       test "$(git merge-base modD D)" = "$(git rev-parse B)"
+'
+
+test_expect_success 'filter subdirectory only' '
+       mkdir subdir &&
+       touch subdir/new &&
+       git add subdir/new &&
+       test_tick &&
+       git commit -m "subdir" &&
+       echo H > a &&
+       test_tick &&
+       git commit -m "not subdir" a &&
+       echo A > subdir/new &&
+       test_tick &&
+       git commit -m "again subdir" subdir/new &&
+       git rm a &&
+       test_tick &&
+       git commit -m "again not subdir" &&
+       git branch sub &&
+       git-filter-branch -f --subdirectory-filter subdir refs/heads/sub
+'
+
+test_expect_success 'subdirectory filter result looks okay' '
+       test 2 = $(git rev-list sub | wc -l) &&
+       git show sub:new &&
+       ! git show sub:subdir
+'
+
+test_expect_success 'setup and filter history that requires --full-history' '
+       git checkout master &&
+       mkdir subdir &&
+       echo A > subdir/new &&
+       git add subdir/new &&
+       test_tick &&
+       git commit -m "subdir on master" subdir/new &&
+       git rm a &&
+       test_tick &&
+       git commit -m "again subdir on master" &&
+       git merge branch &&
+       git branch sub-master &&
+       git-filter-branch -f --subdirectory-filter subdir sub-master
+'
+
+test_expect_success 'subdirectory filter result looks okay' '
+       test 3 = $(git rev-list -1 --parents sub-master | wc -w) &&
+       git show sub-master^:new &&
+       git show sub-master^2:new &&
+       ! git show sub:subdir
+'
+
+test_expect_success 'use index-filter to move into a subdirectory' '
+       git branch directorymoved &&
+       git-filter-branch -f --index-filter \
+                "git ls-files -s | sed \"s-\\t-&newsubdir/-\" |
+                 GIT_INDEX_FILE=\$GIT_INDEX_FILE.new \
+                       git update-index --index-info &&
+                 mv \$GIT_INDEX_FILE.new \$GIT_INDEX_FILE" directorymoved &&
+       test -z "$(git diff HEAD directorymoved:newsubdir)"'
+
+test_expect_success 'stops when msg filter fails' '
+       old=$(git rev-parse HEAD) &&
+       ! git-filter-branch -f --msg-filter false HEAD &&
+       test $old = $(git rev-parse HEAD) &&
+       rm -rf .git-rewrite
+'
+
+test_expect_success 'author information is preserved' '
+       : > i &&
+       git add i &&
+       test_tick &&
+       GIT_AUTHOR_NAME="B V Uips" git commit -m bvuips &&
+       git branch preserved-author &&
+       git-filter-branch -f --msg-filter "cat; \
+                       test \$GIT_COMMIT != $(git rev-parse master) || \
+                       echo Hallo" \
+               preserved-author &&
+       test 1 = $(git rev-list --author="B V Uips" preserved-author | wc -l)
+'
+
+test_expect_success "remove a certain author's commits" '
+       echo i > i &&
+       test_tick &&
+       git commit -m i i &&
+       git branch removed-author &&
+       git-filter-branch -f --commit-filter "\
+               if [ \"\$GIT_AUTHOR_NAME\" = \"B V Uips\" ];\
+               then\
+                       skip_commit \"\$@\";
+               else\
+                       git commit-tree \"\$@\";\
+               fi" removed-author &&
+       cnt1=$(git rev-list master | wc -l) &&
+       cnt2=$(git rev-list removed-author | wc -l) &&
+       test $cnt1 -eq $(($cnt2 + 1)) &&
+       test 0 = $(git rev-list --author="B V Uips" removed-author | wc -l)
+'
+
+test_expect_success 'barf on invalid name' '
+       ! git filter-branch -f master xy-problem &&
+       ! git filter-branch -f HEAD^
+'
+
+test_expect_success '"map" works in commit filter' '
+       git filter-branch -f --commit-filter "\
+               parent=\$(git rev-parse \$GIT_COMMIT^) &&
+               mapped=\$(map \$parent) &&
+               actual=\$(echo \"\$@\" | sed \"s/^.*-p //\") &&
+               test \$mapped = \$actual &&
+               git commit-tree \"\$@\";" master~2..master &&
+       git rev-parse --verify master
+'
+
+test_done
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
new file mode 100755 (executable)
index 0000000..c7130c4
--- /dev/null
@@ -0,0 +1,1043 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Carlos Rica
+#
+
+test_description='git-tag
+
+Tests for operations with tags.'
+
+. ./test-lib.sh
+
+# creating and listing lightweight tags:
+
+tag_exists () {
+       git show-ref --quiet --verify refs/tags/"$1"
+}
+
+# todo: git tag -l now returns always zero, when fixed, change this test
+test_expect_success 'listing all tags in an empty tree should succeed' '
+       git tag -l &&
+       git tag
+'
+
+test_expect_success 'listing all tags in an empty tree should output nothing' '
+       test `git-tag -l | wc -l` -eq 0 &&
+       test `git-tag | wc -l` -eq 0
+'
+
+test_expect_failure 'looking for a tag in an empty tree should fail' \
+       'tag_exists mytag'
+
+test_expect_success 'creating a tag in an empty tree should fail' '
+       ! git-tag mynotag &&
+       ! tag_exists mynotag
+'
+
+test_expect_success 'creating a tag for HEAD in an empty tree should fail' '
+       ! git-tag mytaghead HEAD &&
+       ! tag_exists mytaghead
+'
+
+test_expect_success 'creating a tag for an unknown revision should fail' '
+       ! git-tag mytagnorev aaaaaaaaaaa &&
+       ! tag_exists mytagnorev
+'
+
+# commit used in the tests, test_tick is also called here to freeze the date:
+test_expect_success 'creating a tag using default HEAD should succeed' '
+       test_tick &&
+       echo foo >foo &&
+       git add foo &&
+       git commit -m Foo &&
+       git tag mytag
+'
+
+test_expect_success 'listing all tags if one exists should succeed' '
+       git-tag -l &&
+       git-tag
+'
+
+test_expect_success 'listing all tags if one exists should output that tag' '
+       test `git-tag -l` = mytag &&
+       test `git-tag` = mytag
+'
+
+# pattern matching:
+
+test_expect_success 'listing a tag using a matching pattern should succeed' \
+       'git-tag -l mytag'
+
+test_expect_success \
+       'listing a tag using a matching pattern should output that tag' \
+       'test `git-tag -l mytag` = mytag'
+
+# todo: git tag -l now returns always zero, when fixed, change this test
+test_expect_success \
+       'listing tags using a non-matching pattern should suceed' \
+       'git-tag -l xxx'
+
+test_expect_success \
+       'listing tags using a non-matching pattern should output nothing' \
+       'test `git-tag -l xxx | wc -l` -eq 0'
+
+# special cases for creating tags:
+
+test_expect_failure \
+       'trying to create a tag with the name of one existing should fail' \
+       'git tag mytag'
+
+test_expect_success \
+       'trying to create a tag with a non-valid name should fail' '
+       test `git-tag -l | wc -l` -eq 1 &&
+       ! git tag "" &&
+       ! git tag .othertag &&
+       ! git tag "other tag" &&
+       ! git tag "othertag^" &&
+       ! git tag "other~tag" &&
+       test `git-tag -l | wc -l` -eq 1
+'
+
+test_expect_success 'creating a tag using HEAD directly should succeed' '
+       git tag myhead HEAD &&
+       tag_exists myhead
+'
+
+# deleting tags:
+
+test_expect_success 'trying to delete an unknown tag should fail' '
+       ! tag_exists unknown-tag &&
+       ! git-tag -d unknown-tag
+'
+
+cat >expect <<EOF
+myhead
+mytag
+EOF
+test_expect_success \
+       'trying to delete tags without params should succeed and do nothing' '
+       git tag -l > actual && git diff expect actual &&
+       git-tag -d &&
+       git tag -l > actual && git diff expect actual
+'
+
+test_expect_success \
+       'deleting two existing tags in one command should succeed' '
+       tag_exists mytag &&
+       tag_exists myhead &&
+       git-tag -d mytag myhead &&
+       ! tag_exists mytag &&
+       ! tag_exists myhead
+'
+
+test_expect_success \
+       'creating a tag with the name of another deleted one should succeed' '
+       ! tag_exists mytag &&
+       git-tag mytag &&
+       tag_exists mytag
+'
+
+test_expect_success \
+       'trying to delete two tags, existing and not, should fail in the 2nd' '
+       tag_exists mytag &&
+       ! tag_exists myhead &&
+       ! git-tag -d mytag anothertag &&
+       ! tag_exists mytag &&
+       ! tag_exists myhead
+'
+
+test_expect_failure 'trying to delete an already deleted tag should fail' \
+       'git-tag -d mytag'
+
+# listing various tags with pattern matching:
+
+cat >expect <<EOF
+a1
+aa1
+cba
+t210
+t211
+v0.2.1
+v1.0
+v1.0.1
+v1.1.3
+EOF
+test_expect_success 'listing all tags should print them ordered' '
+       git tag v1.0.1 &&
+       git tag t211 &&
+       git tag aa1 &&
+       git tag v0.2.1 &&
+       git tag v1.1.3 &&
+       git tag cba &&
+       git tag a1 &&
+       git tag v1.0 &&
+       git tag t210 &&
+       git tag -l > actual &&
+       git diff expect actual &&
+       git tag > actual &&
+       git diff expect actual
+'
+
+cat >expect <<EOF
+a1
+aa1
+cba
+EOF
+test_expect_success \
+       'listing tags with substring as pattern must print those matching' '
+       git-tag -l "*a*" > actual &&
+       git diff expect actual
+'
+
+cat >expect <<EOF
+v0.2.1
+v1.0.1
+EOF
+test_expect_success \
+       'listing tags with a suffix as pattern must print those matching' '
+       git-tag -l "*.1" > actual &&
+       git diff expect actual
+'
+
+cat >expect <<EOF
+t210
+t211
+EOF
+test_expect_success \
+       'listing tags with a prefix as pattern must print those matching' '
+       git-tag -l "t21*" > actual &&
+       git diff expect actual
+'
+
+cat >expect <<EOF
+a1
+EOF
+test_expect_success \
+       'listing tags using a name as pattern must print that one matching' '
+       git-tag -l a1 > actual &&
+       git diff expect actual
+'
+
+cat >expect <<EOF
+v1.0
+EOF
+test_expect_success \
+       'listing tags using a name as pattern must print that one matching' '
+       git-tag -l v1.0 > actual &&
+       git diff expect actual
+'
+
+cat >expect <<EOF
+v1.0.1
+v1.1.3
+EOF
+test_expect_success \
+       'listing tags with ? in the pattern should print those matching' '
+       git-tag -l "v1.?.?" > actual &&
+       git diff expect actual
+'
+
+>expect
+test_expect_success \
+       'listing tags using v.* should print nothing because none have v.' '
+       git-tag -l "v.*" > actual &&
+       git diff expect actual
+'
+
+cat >expect <<EOF
+v0.2.1
+v1.0
+v1.0.1
+v1.1.3
+EOF
+test_expect_success \
+       'listing tags using v* should print only those having v' '
+       git-tag -l "v*" > actual &&
+       git diff expect actual
+'
+
+# creating and verifying lightweight tags:
+
+test_expect_success \
+       'a non-annotated tag created without parameters should point to HEAD' '
+       git-tag non-annotated-tag &&
+       test $(git cat-file -t non-annotated-tag) = commit &&
+       test $(git rev-parse non-annotated-tag) = $(git rev-parse HEAD)
+'
+
+test_expect_failure 'trying to verify an unknown tag should fail' \
+       'git-tag -v unknown-tag'
+
+test_expect_failure \
+       'trying to verify a non-annotated and non-signed tag should fail' \
+       'git-tag -v non-annotated-tag'
+
+test_expect_failure \
+       'trying to verify many non-annotated or unknown tags, should fail' \
+       'git-tag -v unknown-tag1 non-annotated-tag unknown-tag2'
+
+# creating annotated tags:
+
+get_tag_msg () {
+       git cat-file tag "$1" | sed -e "/BEGIN PGP/q"
+}
+
+# run test_tick before committing always gives the time in that timezone
+get_tag_header () {
+cat <<EOF
+object $2
+type $3
+tag $1
+tagger C O Mitter <committer@example.com> $4 -0700
+
+EOF
+}
+
+commit=$(git rev-parse HEAD)
+time=$test_tick
+
+get_tag_header annotated-tag $commit commit $time >expect
+echo "A message" >>expect
+test_expect_success \
+       'creating an annotated tag with -m message should succeed' '
+       git-tag -m "A message" annotated-tag &&
+       get_tag_msg annotated-tag >actual &&
+       git diff expect actual
+'
+
+cat >msgfile <<EOF
+Another message
+in a file.
+EOF
+get_tag_header file-annotated-tag $commit commit $time >expect
+cat msgfile >>expect
+test_expect_success \
+       'creating an annotated tag with -F messagefile should succeed' '
+       git-tag -F msgfile file-annotated-tag &&
+       get_tag_msg file-annotated-tag >actual &&
+       git diff expect actual
+'
+
+cat >inputmsg <<EOF
+A message from the
+standard input
+EOF
+get_tag_header stdin-annotated-tag $commit commit $time >expect
+cat inputmsg >>expect
+test_expect_success 'creating an annotated tag with -F - should succeed' '
+       git-tag -F - stdin-annotated-tag <inputmsg &&
+       get_tag_msg stdin-annotated-tag >actual &&
+       git diff expect actual
+'
+
+test_expect_success \
+       'trying to create a tag with a non-existing -F file should fail' '
+       ! test -f nonexistingfile &&
+       ! tag_exists notag &&
+       ! git-tag -F nonexistingfile notag &&
+       ! tag_exists notag
+'
+
+test_expect_success \
+       'trying to create tags giving both -m or -F options should fail' '
+       echo "message file 1" >msgfile1 &&
+       echo "message file 2" >msgfile2 &&
+       ! tag_exists msgtag &&
+       ! git-tag -m "message 1" -F msgfile1 msgtag &&
+       ! tag_exists msgtag &&
+       ! git-tag -F msgfile1 -m "message 1" msgtag &&
+       ! tag_exists msgtag &&
+       ! git-tag -m "message 1" -F msgfile1 -m "message 2" msgtag &&
+       ! tag_exists msgtag
+'
+
+# blank and empty messages:
+
+get_tag_header empty-annotated-tag $commit commit $time >expect
+test_expect_success \
+       'creating a tag with an empty -m message should succeed' '
+       git-tag -m "" empty-annotated-tag &&
+       get_tag_msg empty-annotated-tag >actual &&
+       git diff expect actual
+'
+
+>emptyfile
+get_tag_header emptyfile-annotated-tag $commit commit $time >expect
+test_expect_success \
+       'creating a tag with an empty -F messagefile should succeed' '
+       git-tag -F emptyfile emptyfile-annotated-tag &&
+       get_tag_msg emptyfile-annotated-tag >actual &&
+       git diff expect actual
+'
+
+printf '\n\n  \n\t\nLeading blank lines\n' >blanksfile
+printf '\n\t \t  \nRepeated blank lines\n' >>blanksfile
+printf '\n\n\nTrailing spaces      \t  \n' >>blanksfile
+printf '\nTrailing blank lines\n\n\t \n\n' >>blanksfile
+get_tag_header blanks-annotated-tag $commit commit $time >expect
+cat >>expect <<EOF
+Leading blank lines
+
+Repeated blank lines
+
+Trailing spaces
+
+Trailing blank lines
+EOF
+test_expect_success \
+       'extra blanks in the message for an annotated tag should be removed' '
+       git-tag -F blanksfile blanks-annotated-tag &&
+       get_tag_msg blanks-annotated-tag >actual &&
+       git diff expect actual
+'
+
+get_tag_header blank-annotated-tag $commit commit $time >expect
+test_expect_success \
+       'creating a tag with blank -m message with spaces should succeed' '
+       git-tag -m "     " blank-annotated-tag &&
+       get_tag_msg blank-annotated-tag >actual &&
+       git diff expect actual
+'
+
+echo '     ' >blankfile
+echo ''      >>blankfile
+echo '  '    >>blankfile
+get_tag_header blankfile-annotated-tag $commit commit $time >expect
+test_expect_success \
+       'creating a tag with blank -F messagefile with spaces should succeed' '
+       git-tag -F blankfile blankfile-annotated-tag &&
+       get_tag_msg blankfile-annotated-tag >actual &&
+       git diff expect actual
+'
+
+printf '      ' >blanknonlfile
+get_tag_header blanknonlfile-annotated-tag $commit commit $time >expect
+test_expect_success \
+       'creating a tag with -F file of spaces and no newline should succeed' '
+       git-tag -F blanknonlfile blanknonlfile-annotated-tag &&
+       get_tag_msg blanknonlfile-annotated-tag >actual &&
+       git diff expect actual
+'
+
+# messages with commented lines:
+
+cat >commentsfile <<EOF
+# A comment
+
+############
+The message.
+############
+One line.
+
+
+# commented lines
+# commented lines
+
+Another line.
+# comments
+
+Last line.
+EOF
+get_tag_header comments-annotated-tag $commit commit $time >expect
+cat >>expect <<EOF
+The message.
+One line.
+
+Another line.
+
+Last line.
+EOF
+test_expect_success \
+       'creating a tag using a -F messagefile with #comments should succeed' '
+       git-tag -F commentsfile comments-annotated-tag &&
+       get_tag_msg comments-annotated-tag >actual &&
+       git diff expect actual
+'
+
+get_tag_header comment-annotated-tag $commit commit $time >expect
+test_expect_success \
+       'creating a tag with a #comment in the -m message should succeed' '
+       git-tag -m "#comment" comment-annotated-tag &&
+       get_tag_msg comment-annotated-tag >actual &&
+       git diff expect actual
+'
+
+echo '#comment' >commentfile
+echo ''         >>commentfile
+echo '####'     >>commentfile
+get_tag_header commentfile-annotated-tag $commit commit $time >expect
+test_expect_success \
+       'creating a tag with #comments in the -F messagefile should succeed' '
+       git-tag -F commentfile commentfile-annotated-tag &&
+       get_tag_msg commentfile-annotated-tag >actual &&
+       git diff expect actual
+'
+
+printf '#comment' >commentnonlfile
+get_tag_header commentnonlfile-annotated-tag $commit commit $time >expect
+test_expect_success \
+       'creating a tag with a file of #comment and no newline should succeed' '
+       git-tag -F commentnonlfile commentnonlfile-annotated-tag &&
+       get_tag_msg commentnonlfile-annotated-tag >actual &&
+       git diff expect actual
+'
+
+# listing messages for annotated non-signed tags:
+
+test_expect_success \
+       'listing the one-line message of a non-signed tag should succeed' '
+       git-tag -m "A msg" tag-one-line &&
+
+       echo "tag-one-line" >expect &&
+       git-tag -l | grep "^tag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l | grep "^tag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l tag-one-line >actual &&
+       git diff expect actual &&
+
+       echo "tag-one-line    A msg" >expect &&
+       git-tag -n xxx -l | grep "^tag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n "" -l | grep "^tag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n 1 -l | grep "^tag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n -l | grep "^tag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n 1 -l tag-one-line >actual &&
+       git diff expect actual &&
+       git-tag -n 2 -l tag-one-line >actual &&
+       git diff expect actual &&
+       git-tag -n 999 -l tag-one-line >actual &&
+       git diff expect actual
+'
+
+test_expect_success \
+       'listing the zero-lines message of a non-signed tag should succeed' '
+       git-tag -m "" tag-zero-lines &&
+
+       echo "tag-zero-lines" >expect &&
+       git-tag -l | grep "^tag-zero-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l | grep "^tag-zero-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l tag-zero-lines >actual &&
+       git diff expect actual &&
+
+       echo "tag-zero-lines  " >expect &&
+       git-tag -n 1 -l | grep "^tag-zero-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n -l | grep "^tag-zero-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 1 -l tag-zero-lines >actual &&
+       git diff expect actual &&
+       git-tag -n 2 -l tag-zero-lines >actual &&
+       git diff expect actual &&
+       git-tag -n 999 -l tag-zero-lines >actual &&
+       git diff expect actual
+'
+
+echo 'tag line one' >annotagmsg
+echo 'tag line two' >>annotagmsg
+echo 'tag line three' >>annotagmsg
+test_expect_success \
+       'listing many message lines of a non-signed tag should succeed' '
+       git-tag -F annotagmsg tag-lines &&
+
+       echo "tag-lines" >expect &&
+       git-tag -l | grep "^tag-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l | grep "^tag-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l tag-lines >actual &&
+       git diff expect actual &&
+
+       echo "tag-lines       tag line one" >expect &&
+       git-tag -n 1 -l | grep "^tag-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n -l | grep "^tag-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 1 -l tag-lines >actual &&
+       git diff expect actual &&
+
+       echo "    tag line two" >>expect &&
+       git-tag -n 2 -l | grep "^ *tag.line" >actual &&
+       git diff expect actual &&
+       git-tag -n 2 -l tag-lines >actual &&
+       git diff expect actual &&
+
+       echo "    tag line three" >>expect &&
+       git-tag -n 3 -l | grep "^ *tag.line" >actual &&
+       git diff expect actual &&
+       git-tag -n 3 -l tag-lines >actual &&
+       git diff expect actual &&
+       git-tag -n 4 -l | grep "^ *tag.line" >actual &&
+       git diff expect actual &&
+       git-tag -n 4 -l tag-lines >actual &&
+       git diff expect actual &&
+       git-tag -n 99 -l | grep "^ *tag.line" >actual &&
+       git diff expect actual &&
+       git-tag -n 99 -l tag-lines >actual &&
+       git diff expect actual
+'
+
+# trying to verify annotated non-signed tags:
+
+test_expect_success \
+       'trying to verify an annotated non-signed tag should fail' '
+       tag_exists annotated-tag &&
+       ! git-tag -v annotated-tag
+'
+
+test_expect_success \
+       'trying to verify a file-annotated non-signed tag should fail' '
+       tag_exists file-annotated-tag &&
+       ! git-tag -v file-annotated-tag
+'
+
+test_expect_success \
+       'trying to verify two annotated non-signed tags should fail' '
+       tag_exists annotated-tag file-annotated-tag &&
+       ! git-tag -v annotated-tag file-annotated-tag
+'
+
+# creating and verifying signed tags:
+
+gpg --version >/dev/null
+if [ $? -eq 127 ]; then
+       echo "Skipping signed tags tests, because gpg was not found"
+       test_done
+       exit
+fi
+
+# As said here: http://www.gnupg.org/documentation/faqs.html#q6.19
+# the gpg version 1.0.6 didn't parse trust packets correctly, so for
+# that version, creation of signed tags using the generated key fails.
+case "$(gpg --version)" in
+'gpg (GnuPG) 1.0.6'*)
+       echo "Skipping signed tag tests, because a bug in 1.0.6 version"
+       test_done
+       exit
+       ;;
+esac
+
+# key generation info: gpg --homedir t/t7004 --gen-key
+# Type DSA and Elgamal, size 2048 bits, no expiration date.
+# Name and email: C O Mitter <committer@example.com>
+# No password given, to enable non-interactive operation.
+
+cp -R ../t7004 ./gpghome
+chmod 0700 gpghome
+export GNUPGHOME="$(pwd)/gpghome"
+
+get_tag_header signed-tag $commit commit $time >expect
+echo 'A signed tag message' >>expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success 'creating a signed tag with -m message should succeed' '
+       git-tag -s -m "A signed tag message" signed-tag &&
+       get_tag_msg signed-tag >actual &&
+       git diff expect actual
+'
+
+cat >sigmsgfile <<EOF
+Another signed tag
+message in a file.
+EOF
+get_tag_header file-signed-tag $commit commit $time >expect
+cat sigmsgfile >>expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with -F messagefile should succeed' '
+       git-tag -s -F sigmsgfile file-signed-tag &&
+       get_tag_msg file-signed-tag >actual &&
+       git diff expect actual
+'
+
+cat >siginputmsg <<EOF
+A signed tag message from
+the standard input
+EOF
+get_tag_header stdin-signed-tag $commit commit $time >expect
+cat siginputmsg >>expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success 'creating a signed tag with -F - should succeed' '
+       git-tag -s -F - stdin-signed-tag <siginputmsg &&
+       get_tag_msg stdin-signed-tag >actual &&
+       git diff expect actual
+'
+
+cat >fakeeditor <<'EOF'
+#!/bin/sh
+test -n "$1" && exec >"$1"
+echo A signed tag message
+echo from a fake editor.
+EOF
+chmod +x fakeeditor
+get_tag_header implied-annotate $commit commit $time >expect
+./fakeeditor >>expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success '-s implies annotated tag' '
+       GIT_EDITOR=./fakeeditor git-tag -s implied-annotate &&
+       get_tag_msg implied-annotate >actual &&
+       git diff expect actual
+'
+
+test_expect_success \
+       'trying to create a signed tag with non-existing -F file should fail' '
+       ! test -f nonexistingfile &&
+       ! tag_exists nosigtag &&
+       ! git-tag -s -F nonexistingfile nosigtag &&
+       ! tag_exists nosigtag
+'
+
+test_expect_success 'verifying a signed tag should succeed' \
+       'git-tag -v signed-tag'
+
+test_expect_success 'verifying two signed tags in one command should succeed' \
+       'git-tag -v signed-tag file-signed-tag'
+
+test_expect_success \
+       'verifying many signed and non-signed tags should fail' '
+       ! git-tag -v signed-tag annotated-tag &&
+       ! git-tag -v file-annotated-tag file-signed-tag &&
+       ! git-tag -v annotated-tag file-signed-tag file-annotated-tag &&
+       ! git-tag -v signed-tag annotated-tag file-signed-tag
+'
+
+test_expect_success 'verifying a forged tag should fail' '
+       forged=$(git cat-file tag signed-tag |
+               sed -e "s/signed-tag/forged-tag/" |
+               git mktag) &&
+       git tag forged-tag $forged &&
+       ! git-tag -v forged-tag
+'
+
+# blank and empty messages for signed tags:
+
+get_tag_header empty-signed-tag $commit commit $time >expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with an empty -m message should succeed' '
+       git-tag -s -m "" empty-signed-tag &&
+       get_tag_msg empty-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v empty-signed-tag
+'
+
+>sigemptyfile
+get_tag_header emptyfile-signed-tag $commit commit $time >expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with an empty -F messagefile should succeed' '
+       git-tag -s -F sigemptyfile emptyfile-signed-tag &&
+       get_tag_msg emptyfile-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v emptyfile-signed-tag
+'
+
+printf '\n\n  \n\t\nLeading blank lines\n' > sigblanksfile
+printf '\n\t \t  \nRepeated blank lines\n' >>sigblanksfile
+printf '\n\n\nTrailing spaces      \t  \n' >>sigblanksfile
+printf '\nTrailing blank lines\n\n\t \n\n' >>sigblanksfile
+get_tag_header blanks-signed-tag $commit commit $time >expect
+cat >>expect <<EOF
+Leading blank lines
+
+Repeated blank lines
+
+Trailing spaces
+
+Trailing blank lines
+EOF
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'extra blanks in the message for a signed tag should be removed' '
+       git-tag -s -F sigblanksfile blanks-signed-tag &&
+       get_tag_msg blanks-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v blanks-signed-tag
+'
+
+get_tag_header blank-signed-tag $commit commit $time >expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with a blank -m message should succeed' '
+       git-tag -s -m "     " blank-signed-tag &&
+       get_tag_msg blank-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v blank-signed-tag
+'
+
+echo '     ' >sigblankfile
+echo ''      >>sigblankfile
+echo '  '    >>sigblankfile
+get_tag_header blankfile-signed-tag $commit commit $time >expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with blank -F file with spaces should succeed' '
+       git-tag -s -F sigblankfile blankfile-signed-tag &&
+       get_tag_msg blankfile-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v blankfile-signed-tag
+'
+
+printf '      ' >sigblanknonlfile
+get_tag_header blanknonlfile-signed-tag $commit commit $time >expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with spaces and no newline should succeed' '
+       git-tag -s -F sigblanknonlfile blanknonlfile-signed-tag &&
+       get_tag_msg blanknonlfile-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v signed-tag
+'
+
+# messages with commented lines for signed tags:
+
+cat >sigcommentsfile <<EOF
+# A comment
+
+############
+The message.
+############
+One line.
+
+
+# commented lines
+# commented lines
+
+Another line.
+# comments
+
+Last line.
+EOF
+get_tag_header comments-signed-tag $commit commit $time >expect
+cat >>expect <<EOF
+The message.
+One line.
+
+Another line.
+
+Last line.
+EOF
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with a -F file with #comments should succeed' '
+       git-tag -s -F sigcommentsfile comments-signed-tag &&
+       get_tag_msg comments-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v comments-signed-tag
+'
+
+get_tag_header comment-signed-tag $commit commit $time >expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with #commented -m message should succeed' '
+       git-tag -s -m "#comment" comment-signed-tag &&
+       get_tag_msg comment-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v comment-signed-tag
+'
+
+echo '#comment' >sigcommentfile
+echo ''         >>sigcommentfile
+echo '####'     >>sigcommentfile
+get_tag_header commentfile-signed-tag $commit commit $time >expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with #commented -F messagefile should succeed' '
+       git-tag -s -F sigcommentfile commentfile-signed-tag &&
+       get_tag_msg commentfile-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v commentfile-signed-tag
+'
+
+printf '#comment' >sigcommentnonlfile
+get_tag_header commentnonlfile-signed-tag $commit commit $time >expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag with a #comment and no newline should succeed' '
+       git-tag -s -F sigcommentnonlfile commentnonlfile-signed-tag &&
+       get_tag_msg commentnonlfile-signed-tag >actual &&
+       git diff expect actual &&
+       git-tag -v commentnonlfile-signed-tag
+'
+
+# listing messages for signed tags:
+
+test_expect_success \
+       'listing the one-line message of a signed tag should succeed' '
+       git-tag -s -m "A message line signed" stag-one-line &&
+
+       echo "stag-one-line" >expect &&
+       git-tag -l | grep "^stag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l | grep "^stag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l stag-one-line >actual &&
+       git diff expect actual &&
+
+       echo "stag-one-line   A message line signed" >expect &&
+       git-tag -n xxx -l | grep "^stag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n "" -l | grep "^stag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n 1 -l | grep "^stag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n -l | grep "^stag-one-line" >actual &&
+       git diff expect actual &&
+       git-tag -n 1 -l stag-one-line >actual &&
+       git diff expect actual &&
+       git-tag -n 2 -l stag-one-line >actual &&
+       git diff expect actual &&
+       git-tag -n 999 -l stag-one-line >actual &&
+       git diff expect actual
+'
+
+test_expect_success \
+       'listing the zero-lines message of a signed tag should succeed' '
+       git-tag -s -m "" stag-zero-lines &&
+
+       echo "stag-zero-lines" >expect &&
+       git-tag -l | grep "^stag-zero-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l | grep "^stag-zero-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l stag-zero-lines >actual &&
+       git diff expect actual &&
+
+       echo "stag-zero-lines " >expect &&
+       git-tag -n 1 -l | grep "^stag-zero-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n -l | grep "^stag-zero-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 1 -l stag-zero-lines >actual &&
+       git diff expect actual &&
+       git-tag -n 2 -l stag-zero-lines >actual &&
+       git diff expect actual &&
+       git-tag -n 999 -l stag-zero-lines >actual &&
+       git diff expect actual
+'
+
+echo 'stag line one' >sigtagmsg
+echo 'stag line two' >>sigtagmsg
+echo 'stag line three' >>sigtagmsg
+test_expect_success \
+       'listing many message lines of a signed tag should succeed' '
+       git-tag -s -F sigtagmsg stag-lines &&
+
+       echo "stag-lines" >expect &&
+       git-tag -l | grep "^stag-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l | grep "^stag-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 0 -l stag-lines >actual &&
+       git diff expect actual &&
+
+       echo "stag-lines      stag line one" >expect &&
+       git-tag -n 1 -l | grep "^stag-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n -l | grep "^stag-lines" >actual &&
+       git diff expect actual &&
+       git-tag -n 1 -l stag-lines >actual &&
+       git diff expect actual &&
+
+       echo "    stag line two" >>expect &&
+       git-tag -n 2 -l | grep "^ *stag.line" >actual &&
+       git diff expect actual &&
+       git-tag -n 2 -l stag-lines >actual &&
+       git diff expect actual &&
+
+       echo "    stag line three" >>expect &&
+       git-tag -n 3 -l | grep "^ *stag.line" >actual &&
+       git diff expect actual &&
+       git-tag -n 3 -l stag-lines >actual &&
+       git diff expect actual &&
+       git-tag -n 4 -l | grep "^ *stag.line" >actual &&
+       git diff expect actual &&
+       git-tag -n 4 -l stag-lines >actual &&
+       git diff expect actual &&
+       git-tag -n 99 -l | grep "^ *stag.line" >actual &&
+       git diff expect actual &&
+       git-tag -n 99 -l stag-lines >actual &&
+       git diff expect actual
+'
+
+# tags pointing to objects different from commits:
+
+tree=$(git rev-parse HEAD^{tree})
+blob=$(git rev-parse HEAD:foo)
+tag=$(git rev-parse signed-tag)
+
+get_tag_header tree-signed-tag $tree tree $time >expect
+echo "A message for a tree" >>expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag pointing to a tree should succeed' '
+       git-tag -s -m "A message for a tree" tree-signed-tag HEAD^{tree} &&
+       get_tag_msg tree-signed-tag >actual &&
+       git diff expect actual
+'
+
+get_tag_header blob-signed-tag $blob blob $time >expect
+echo "A message for a blob" >>expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag pointing to a blob should succeed' '
+       git-tag -s -m "A message for a blob" blob-signed-tag HEAD:foo &&
+       get_tag_msg blob-signed-tag >actual &&
+       git diff expect actual
+'
+
+get_tag_header tag-signed-tag $tag tag $time >expect
+echo "A message for another tag" >>expect
+echo '-----BEGIN PGP SIGNATURE-----' >>expect
+test_expect_success \
+       'creating a signed tag pointing to another tag should succeed' '
+       git-tag -s -m "A message for another tag" tag-signed-tag signed-tag &&
+       get_tag_msg tag-signed-tag >actual &&
+       git diff expect actual
+'
+
+# try to sign with bad user.signingkey
+git config user.signingkey BobTheMouse
+test_expect_failure \
+       'git-tag -s fails if gpg is misconfigured' \
+       'git tag -s -m tail tag-gpg-failure'
+git config --unset user.signingkey
+
+# try to verify without gpg:
+
+rm -rf gpghome
+test_expect_failure \
+       'verify signed tag fails when public key is not present' \
+       'git-tag -v signed-tag'
+
+test_expect_failure \
+       'git-tag -a fails if tag annotation is empty' '
+       GIT_EDITOR=cat git tag -a initial-comment
+'
+
+test_expect_success \
+       'message in editor has initial comment' '
+       GIT_EDITOR=cat git tag -a initial-comment > actual
+       # check the first line --- should be empty
+       first=$(sed -e 1q <actual) &&
+       test -z "$first" &&
+       # remove commented lines from the remainder -- should be empty
+       rest=$(sed -e 1d -e '/^#/d' <actual) &&
+       test -z "$rest"
+'
+
+get_tag_header reuse $commit commit $time >expect
+echo "An annotation to be reused" >> expect
+test_expect_success \
+       'overwriting an annoted tag should use its previous body' '
+       git tag -a -m "An annotation to be reused" reuse &&
+       GIT_EDITOR=true git tag -f -a reuse &&
+       get_tag_msg reuse >actual &&
+       git diff expect actual
+'
+
+test_done
diff --git a/t/t7004/pubring.gpg b/t/t7004/pubring.gpg
new file mode 100644 (file)
index 0000000..83855fa
Binary files /dev/null and b/t/t7004/pubring.gpg differ
diff --git a/t/t7004/random_seed b/t/t7004/random_seed
new file mode 100644 (file)
index 0000000..8fed133
Binary files /dev/null and b/t/t7004/random_seed differ
diff --git a/t/t7004/secring.gpg b/t/t7004/secring.gpg
new file mode 100644 (file)
index 0000000..d831cd9
Binary files /dev/null and b/t/t7004/secring.gpg differ
diff --git a/t/t7004/trustdb.gpg b/t/t7004/trustdb.gpg
new file mode 100644 (file)
index 0000000..abace96
Binary files /dev/null and b/t/t7004/trustdb.gpg differ
diff --git a/t/t7005-editor.sh b/t/t7005-editor.sh
new file mode 100755 (executable)
index 0000000..44228b5
--- /dev/null
@@ -0,0 +1,94 @@
+#!/bin/sh
+
+test_description='GIT_EDITOR, core.editor, and stuff'
+
+. ./test-lib.sh
+
+OLD_TERM="$TERM"
+
+for i in GIT_EDITOR core_editor EDITOR VISUAL vi
+do
+       cat >e-$i.sh <<-EOF
+       echo "Edited by $i" >"\$1"
+       EOF
+       chmod +x e-$i.sh
+done
+unset vi
+mv e-vi.sh vi
+unset EDITOR VISUAL GIT_EDITOR
+
+test_expect_success setup '
+
+       msg="Hand edited" &&
+       echo "$msg" >expect &&
+       git add vi &&
+       test_tick &&
+       git commit -m "$msg" &&
+       git show -s --pretty=oneline |
+       sed -e "s/^[0-9a-f]* //" >actual &&
+       diff actual expect
+
+'
+
+TERM=dumb
+export TERM
+test_expect_success 'dumb should error out when falling back on vi' '
+
+       if git commit --amend
+       then
+               echo "Oops?"
+               exit 1
+       else
+               : happy
+       fi
+'
+
+TERM=vt100
+export TERM
+for i in vi EDITOR VISUAL core_editor GIT_EDITOR
+do
+       echo "Edited by $i" >expect
+       unset EDITOR VISUAL GIT_EDITOR
+       git config --unset-all core.editor
+       case "$i" in
+       core_editor)
+               git config core.editor ./e-core_editor.sh
+               ;;
+       [A-Z]*)
+               eval "$i=./e-$i.sh"
+               export $i
+               ;;
+       esac
+       test_expect_success "Using $i" '
+               git --exec-path=. commit --amend &&
+               git show -s --pretty=oneline |
+               sed -e "s/^[0-9a-f]* //" >actual &&
+               diff actual expect
+       '
+done
+
+unset EDITOR VISUAL GIT_EDITOR
+git config --unset-all core.editor
+for i in vi EDITOR VISUAL core_editor GIT_EDITOR
+do
+       echo "Edited by $i" >expect
+       case "$i" in
+       core_editor)
+               git config core.editor ./e-core_editor.sh
+               ;;
+       [A-Z]*)
+               eval "$i=./e-$i.sh"
+               export $i
+               ;;
+       esac
+       test_expect_success "Using $i (override)" '
+               git --exec-path=. commit --amend &&
+               git show -s --pretty=oneline |
+               sed -e "s/^[0-9a-f]* //" >actual &&
+               diff actual expect
+       '
+done
+
+TERM="$OLD_TERM"
+
+test_done
diff --git a/t/t7101-reset.sh b/t/t7101-reset.sh
new file mode 100755 (executable)
index 0000000..66d4043
--- /dev/null
@@ -0,0 +1,63 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Shawn Pearce
+#
+
+test_description='git-reset should cull empty subdirs'
+. ./test-lib.sh
+
+test_expect_success \
+    'creating initial files' \
+    'mkdir path0 &&
+     cp ../../COPYING path0/COPYING &&
+     git add path0/COPYING &&
+     git-commit -m add -a'
+
+test_expect_success \
+    'creating second files' \
+    'mkdir path1 &&
+     mkdir path1/path2 &&
+     cp ../../COPYING path1/path2/COPYING &&
+     cp ../../COPYING path1/COPYING &&
+     cp ../../COPYING COPYING &&
+     cp ../../COPYING path0/COPYING-TOO &&
+     git add path1/path2/COPYING &&
+     git add path1/COPYING &&
+     git add COPYING &&
+     git add path0/COPYING-TOO &&
+     git-commit -m change -a'
+
+test_expect_success \
+    'resetting tree HEAD^' \
+    'git-reset --hard HEAD^'
+
+test_expect_success \
+    'checking initial files exist after rewind' \
+    'test -d path0 &&
+     test -f path0/COPYING'
+
+test_expect_failure \
+    'checking lack of path1/path2/COPYING' \
+    'test -f path1/path2/COPYING'
+
+test_expect_failure \
+    'checking lack of path1/COPYING' \
+    'test -f path1/COPYING'
+
+test_expect_failure \
+    'checking lack of COPYING' \
+    'test -f COPYING'
+
+test_expect_failure \
+    'checking checking lack of path1/COPYING-TOO' \
+    'test -f path0/COPYING-TOO'
+
+test_expect_failure \
+    'checking lack of path1/path2' \
+    'test -d path1/path2'
+
+test_expect_failure \
+    'checking lack of path1' \
+    'test -d path1'
+
+test_done
diff --git a/t/t7102-reset.sh b/t/t7102-reset.sh
new file mode 100755 (executable)
index 0000000..e5c9f30
--- /dev/null
@@ -0,0 +1,431 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Carlos Rica
+#
+
+test_description='git-reset
+
+Documented tests for git-reset'
+
+. ./test-lib.sh
+
+test_expect_success 'creating initial files and commits' '
+       test_tick &&
+       echo "1st file" >first &&
+       git add first &&
+       git commit -m "create 1st file" &&
+
+       echo "2nd file" >second &&
+       git add second &&
+       git commit -m "create 2nd file" &&
+
+       echo "2nd line 1st file" >>first &&
+       git commit -a -m "modify 1st file" &&
+
+       git rm first &&
+       git mv second secondfile &&
+       git commit -a -m "remove 1st and rename 2nd" &&
+
+       echo "1st line 2nd file" >secondfile &&
+       echo "2nd line 2nd file" >>secondfile &&
+       git commit -a -m "modify 2nd file"
+'
+# git log --pretty=oneline # to see those SHA1 involved
+
+check_changes () {
+       test "$(git rev-parse HEAD)" = "$1" &&
+       git diff | git diff .diff_expect - &&
+       git diff --cached | git diff .cached_expect - &&
+       for FILE in *
+       do
+               echo $FILE':'
+               cat $FILE || return
+       done | git diff .cat_expect -
+}
+
+>.diff_expect
+>.cached_expect
+cat >.cat_expect <<EOF
+secondfile:
+1st line 2nd file
+2nd line 2nd file
+EOF
+
+test_expect_success 'giving a non existing revision should fail' '
+       ! git reset aaaaaa &&
+       ! git reset --mixed aaaaaa &&
+       ! git reset --soft aaaaaa &&
+       ! git reset --hard aaaaaa &&
+       check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+test_expect_success 'reset --soft with unmerged index should fail' '
+       touch .git/MERGE_HEAD &&
+       echo "100644 44c5b5884550c17758737edcced463447b91d42b 1 un" |
+               git update-index --index-info &&
+       ! git reset --soft HEAD &&
+       rm .git/MERGE_HEAD &&
+       git rm --cached -- un
+'
+
+test_expect_success \
+       'giving paths with options different than --mixed should fail' '
+       ! git reset --soft -- first &&
+       ! git reset --hard -- first &&
+       ! git reset --soft HEAD^ -- first &&
+       ! git reset --hard HEAD^ -- first &&
+       check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+test_expect_success 'giving unrecognized options should fail' '
+       ! git reset --other &&
+       ! git reset -o &&
+       ! git reset --mixed --other &&
+       ! git reset --mixed -o &&
+       ! git reset --soft --other &&
+       ! git reset --soft -o &&
+       ! git reset --hard --other &&
+       ! git reset --hard -o &&
+       check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+test_expect_success \
+       'trying to do reset --soft with pending merge should fail' '
+       git branch branch1 &&
+       git branch branch2 &&
+
+       git checkout branch1 &&
+       echo "3rd line in branch1" >>secondfile &&
+       git commit -a -m "change in branch1" &&
+
+       git checkout branch2 &&
+       echo "3rd line in branch2" >>secondfile &&
+       git commit -a -m "change in branch2" &&
+
+       ! git merge branch1 &&
+       ! git reset --soft &&
+
+       printf "1st line 2nd file\n2nd line 2nd file\n3rd line" >secondfile &&
+       git commit -a -m "the change in branch2" &&
+
+       git checkout master &&
+       git branch -D branch1 branch2 &&
+       check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+test_expect_success \
+       'trying to do reset --soft with pending checkout merge should fail' '
+       git branch branch3 &&
+       git branch branch4 &&
+
+       git checkout branch3 &&
+       echo "3rd line in branch3" >>secondfile &&
+       git commit -a -m "line in branch3" &&
+
+       git checkout branch4 &&
+       echo "3rd line in branch4" >>secondfile &&
+
+       git checkout -m branch3 &&
+       ! git reset --soft &&
+
+       printf "1st line 2nd file\n2nd line 2nd file\n3rd line" >secondfile &&
+       git commit -a -m "the line in branch3" &&
+
+       git checkout master &&
+       git branch -D branch3 branch4 &&
+       check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+test_expect_success \
+       'resetting to HEAD with no changes should succeed and do nothing' '
+       git reset --hard &&
+               check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+       git reset --hard HEAD &&
+               check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+       git reset --soft &&
+               check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+       git reset --soft HEAD &&
+               check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+       git reset --mixed &&
+               check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+       git reset --mixed HEAD &&
+               check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+       git reset &&
+               check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+       git reset HEAD &&
+               check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+>.diff_expect
+cat >.cached_expect <<EOF
+diff --git a/secondfile b/secondfile
+index 1bbba79..44c5b58 100644
+--- a/secondfile
++++ b/secondfile
+@@ -1 +1,2 @@
+-2nd file
++1st line 2nd file
++2nd line 2nd file
+EOF
+cat >.cat_expect <<EOF
+secondfile:
+1st line 2nd file
+2nd line 2nd file
+EOF
+test_expect_success '--soft reset only should show changes in diff --cached' '
+       git reset --soft HEAD^ &&
+       check_changes d1a4bc3abce4829628ae2dcb0d60ef3d1a78b1c4 &&
+       test "$(git rev-parse ORIG_HEAD)" = \
+                       3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+>.diff_expect
+>.cached_expect
+cat >.cat_expect <<EOF
+secondfile:
+1st line 2nd file
+2nd line 2nd file
+3rd line 2nd file
+EOF
+test_expect_success \
+       'changing files and redo the last commit should succeed' '
+       echo "3rd line 2nd file" >>secondfile &&
+       git commit -a -C ORIG_HEAD &&
+       check_changes 3d3b7be011a58ca0c179ae45d94e6c83c0b0cd0d &&
+       test "$(git rev-parse ORIG_HEAD)" = \
+                       3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+>.diff_expect
+>.cached_expect
+cat >.cat_expect <<EOF
+first:
+1st file
+2nd line 1st file
+second:
+2nd file
+EOF
+test_expect_success \
+       '--hard reset should change the files and undo commits permanently' '
+       git reset --hard HEAD~2 &&
+       check_changes ddaefe00f1da16864591c61fdc7adb5d7cd6b74e &&
+       test "$(git rev-parse ORIG_HEAD)" = \
+                       3d3b7be011a58ca0c179ae45d94e6c83c0b0cd0d
+'
+
+>.diff_expect
+cat >.cached_expect <<EOF
+diff --git a/first b/first
+deleted file mode 100644
+index 8206c22..0000000
+--- a/first
++++ /dev/null
+@@ -1,2 +0,0 @@
+-1st file
+-2nd line 1st file
+diff --git a/second b/second
+deleted file mode 100644
+index 1bbba79..0000000
+--- a/second
++++ /dev/null
+@@ -1 +0,0 @@
+-2nd file
+diff --git a/secondfile b/secondfile
+new file mode 100644
+index 0000000..44c5b58
+--- /dev/null
++++ b/secondfile
+@@ -0,0 +1,2 @@
++1st line 2nd file
++2nd line 2nd file
+EOF
+cat >.cat_expect <<EOF
+secondfile:
+1st line 2nd file
+2nd line 2nd file
+EOF
+test_expect_success \
+       'redoing changes adding them without commit them should succeed' '
+       git rm first &&
+       git mv second secondfile &&
+
+       echo "1st line 2nd file" >secondfile &&
+       echo "2nd line 2nd file" >>secondfile &&
+       git add secondfile &&
+       check_changes ddaefe00f1da16864591c61fdc7adb5d7cd6b74e
+'
+
+cat >.diff_expect <<EOF
+diff --git a/first b/first
+deleted file mode 100644
+index 8206c22..0000000
+--- a/first
++++ /dev/null
+@@ -1,2 +0,0 @@
+-1st file
+-2nd line 1st file
+diff --git a/second b/second
+deleted file mode 100644
+index 1bbba79..0000000
+--- a/second
++++ /dev/null
+@@ -1 +0,0 @@
+-2nd file
+EOF
+>.cached_expect
+cat >.cat_expect <<EOF
+secondfile:
+1st line 2nd file
+2nd line 2nd file
+EOF
+test_expect_success '--mixed reset to HEAD should unadd the files' '
+       git reset &&
+       check_changes ddaefe00f1da16864591c61fdc7adb5d7cd6b74e &&
+       test "$(git rev-parse ORIG_HEAD)" = \
+                       ddaefe00f1da16864591c61fdc7adb5d7cd6b74e
+'
+
+>.diff_expect
+>.cached_expect
+cat >.cat_expect <<EOF
+secondfile:
+1st line 2nd file
+2nd line 2nd file
+EOF
+test_expect_success 'redoing the last two commits should succeed' '
+       git add secondfile &&
+       git reset --hard ddaefe00f1da16864591c61fdc7adb5d7cd6b74e &&
+
+       git rm first &&
+       git mv second secondfile &&
+       git commit -a -m "remove 1st and rename 2nd" &&
+
+       echo "1st line 2nd file" >secondfile &&
+       echo "2nd line 2nd file" >>secondfile &&
+       git commit -a -m "modify 2nd file" &&
+       check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+>.diff_expect
+>.cached_expect
+cat >.cat_expect <<EOF
+secondfile:
+1st line 2nd file
+2nd line 2nd file
+3rd line in branch2
+EOF
+test_expect_success '--hard reset to HEAD should clear a failed merge' '
+       git branch branch1 &&
+       git branch branch2 &&
+
+       git checkout branch1 &&
+       echo "3rd line in branch1" >>secondfile &&
+       git commit -a -m "change in branch1" &&
+
+       git checkout branch2 &&
+       echo "3rd line in branch2" >>secondfile &&
+       git commit -a -m "change in branch2" &&
+
+       ! git pull . branch1 &&
+       git reset --hard &&
+       check_changes 77abb337073fb4369a7ad69ff6f5ec0e4d6b54bb
+'
+
+>.diff_expect
+>.cached_expect
+cat >.cat_expect <<EOF
+secondfile:
+1st line 2nd file
+2nd line 2nd file
+EOF
+test_expect_success \
+       '--hard reset to ORIG_HEAD should clear a fast-forward merge' '
+       git reset --hard HEAD^ &&
+       check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc &&
+
+       git pull . branch1 &&
+       git reset --hard ORIG_HEAD &&
+       check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc &&
+
+       git checkout master &&
+       git branch -D branch1 branch2 &&
+       check_changes 3ec39651e7f44ea531a5de18a9fa791c0fd370fc
+'
+
+cat > expect << EOF
+diff --git a/file1 b/file1
+index d00491f..7ed6ff8 100644
+--- a/file1
++++ b/file1
+@@ -1 +1 @@
+-1
++5
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 0cfbf08..0000000
+--- a/file2
++++ /dev/null
+@@ -1 +0,0 @@
+-2
+EOF
+cat > cached_expect << EOF
+diff --git a/file4 b/file4
+new file mode 100644
+index 0000000..b8626c4
+--- /dev/null
++++ b/file4
+@@ -0,0 +1 @@
++4
+EOF
+test_expect_success 'test --mixed <paths>' '
+       echo 1 > file1 &&
+       echo 2 > file2 &&
+       git add file1 file2 &&
+       test_tick &&
+       git commit -m files &&
+       git rm file2 &&
+       echo 3 > file3 &&
+       echo 4 > file4 &&
+       echo 5 > file1 &&
+       git add file1 file3 file4 &&
+       ! git reset HEAD -- file1 file2 file3 &&
+       git diff > output &&
+       git diff output expect &&
+       git diff --cached > output &&
+       git diff output cached_expect
+'
+
+test_expect_success 'test resetting the index at give paths' '
+
+       mkdir sub &&
+       >sub/file1 &&
+       >sub/file2 &&
+       git update-index --add sub/file1 sub/file2 &&
+       T=$(git write-tree) &&
+       ! git reset HEAD sub/file2 &&
+       U=$(git write-tree) &&
+       echo "$T" &&
+       echo "$U" &&
+       ! git diff-index --cached --exit-code "$T" &&
+       test "$T" != "$U"
+
+'
+
+test_expect_success 'resetting an unmodified path is a no-op' '
+       git reset --hard &&
+       git reset -- file1 &&
+       git diff-files --exit-code &&
+       git diff-index --cached --exit-code HEAD
+'
+
+cat > expect << EOF
+file2: needs update
+EOF
+
+test_expect_success '--mixed refreshes the index' '
+       echo 123 >> file2 &&
+       git reset --mixed HEAD > output &&
+       git diff --exit-code expect output
+'
+
+test_done
diff --git a/t/t7201-co.sh b/t/t7201-co.sh
new file mode 100755 (executable)
index 0000000..55558ab
--- /dev/null
@@ -0,0 +1,233 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+test_description='git-checkout tests.
+
+Creates master, forks renamer and side branches from it.
+Test switching across them.
+
+  ! [master] Initial A one, A two
+   * [renamer] Renamer R one->uno, M two
+    ! [side] Side M one, D two, A three
+  ---
+    + [side] Side M one, D two, A three
+   *  [renamer] Renamer R one->uno, M two
+  +*+ [master] Initial A one, A two
+
+'
+
+. ./test-lib.sh
+
+fill () {
+       for i
+       do
+               echo "$i"
+       done
+}
+
+
+test_expect_success setup '
+
+       fill 1 2 3 4 5 6 7 8 >one &&
+       fill a b c d e >two &&
+       git add one two &&
+       git commit -m "Initial A one, A two" &&
+
+       git checkout -b renamer &&
+       rm -f one &&
+       fill 1 3 4 5 6 7 8 >uno &&
+       git add uno &&
+       fill a b c d e f >two &&
+       git commit -a -m "Renamer R one->uno, M two" &&
+
+       git checkout -b side master &&
+       fill 1 2 3 4 5 6 7 >one &&
+       fill A B C D E >three &&
+       rm -f two &&
+       git update-index --add --remove one two three &&
+       git commit -m "Side M one, D two, A three" &&
+
+       git checkout master
+'
+
+test_expect_success "checkout from non-existing branch" '
+
+       git checkout -b delete-me master &&
+       rm .git/refs/heads/delete-me &&
+       test refs/heads/delete-me = "$(git symbolic-ref HEAD)" &&
+       git checkout master &&
+       test refs/heads/master = "$(git symbolic-ref HEAD)"
+'
+
+test_expect_success "checkout with dirty tree without -m" '
+
+       fill 0 1 2 3 4 5 6 7 8 >one &&
+       if git checkout side
+       then
+               echo Not happy
+               false
+       else
+               echo "happy - failed correctly"
+       fi
+
+'
+
+test_expect_success "checkout -m with dirty tree" '
+
+       git checkout -f master &&
+       git clean -f &&
+
+       fill 0 1 2 3 4 5 6 7 8 >one &&
+       git checkout -m side &&
+
+       test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&
+
+       fill "M one" "A three" "D       two" >expect.master &&
+       git diff --name-status master >current.master &&
+       diff expect.master current.master &&
+
+       fill "M one" >expect.side &&
+       git diff --name-status side >current.side &&
+       diff expect.side current.side &&
+
+       : >expect.index &&
+       git diff --cached >current.index &&
+       diff expect.index current.index
+'
+
+test_expect_success "checkout -m with dirty tree, renamed" '
+
+       git checkout -f master && git clean -f &&
+
+       fill 1 2 3 4 5 7 8 >one &&
+       if git checkout renamer
+       then
+               echo Not happy
+               false
+       else
+               echo "happy - failed correctly"
+       fi &&
+
+       git checkout -m renamer &&
+       fill 1 3 4 5 7 8 >expect &&
+       diff expect uno &&
+       ! test -f one &&
+       git diff --cached >current &&
+       ! test -s current
+
+'
+
+test_expect_success 'checkout -m with merge conflict' '
+
+       git checkout -f master && git clean -f &&
+
+       fill 1 T 3 4 5 6 S 8 >one &&
+       if git checkout renamer
+       then
+               echo Not happy
+               false
+       else
+               echo "happy - failed correctly"
+       fi &&
+
+       git checkout -m renamer &&
+
+       git diff master:one :3:uno |
+       sed -e "1,/^@@/d" -e "/^ /d" -e "s/^-/d/" -e "s/^+/a/" >current &&
+       fill d2 aT d7 aS >expect &&
+       diff current expect &&
+       git diff --cached two >current &&
+       ! test -s current
+'
+
+test_expect_success 'checkout to detach HEAD' '
+
+       git checkout -f renamer && git clean -f &&
+       git checkout renamer^ &&
+       H=$(git rev-parse --verify HEAD) &&
+       M=$(git show-ref -s --verify refs/heads/master) &&
+       test "z$H" = "z$M" &&
+       if git symbolic-ref HEAD >/dev/null 2>&1
+       then
+               echo "OOPS, HEAD is still symbolic???"
+               false
+       else
+               : happy
+       fi
+'
+
+test_expect_success 'checkout to detach HEAD with branchname^' '
+
+       git checkout -f master && git clean -f &&
+       git checkout renamer^ &&
+       H=$(git rev-parse --verify HEAD) &&
+       M=$(git show-ref -s --verify refs/heads/master) &&
+       test "z$H" = "z$M" &&
+       if git symbolic-ref HEAD >/dev/null 2>&1
+       then
+               echo "OOPS, HEAD is still symbolic???"
+               false
+       else
+               : happy
+       fi
+'
+
+test_expect_success 'checkout to detach HEAD with HEAD^0' '
+
+       git checkout -f master && git clean -f &&
+       git checkout HEAD^0 &&
+       H=$(git rev-parse --verify HEAD) &&
+       M=$(git show-ref -s --verify refs/heads/master) &&
+       test "z$H" = "z$M" &&
+       if git symbolic-ref HEAD >/dev/null 2>&1
+       then
+               echo "OOPS, HEAD is still symbolic???"
+               false
+       else
+               : happy
+       fi
+'
+
+test_expect_success 'checkout with ambiguous tag/branch names' '
+
+       git tag both side &&
+       git branch both master &&
+       git reset --hard &&
+       git checkout master &&
+
+       git checkout both &&
+       H=$(git rev-parse --verify HEAD) &&
+       M=$(git show-ref -s --verify refs/heads/master) &&
+       test "z$H" = "z$M" &&
+       name=$(git symbolic-ref HEAD 2>/dev/null) &&
+       test "z$name" = zrefs/heads/both
+
+'
+
+test_expect_success 'checkout with ambiguous tag/branch names' '
+
+       git reset --hard &&
+       git checkout master &&
+
+       git tag frotz side &&
+       git branch frotz master &&
+       git reset --hard &&
+       git checkout master &&
+
+       git checkout tags/frotz &&
+       H=$(git rev-parse --verify HEAD) &&
+       S=$(git show-ref -s --verify refs/heads/side) &&
+       test "z$H" = "z$S" &&
+       if name=$(git symbolic-ref HEAD 2>/dev/null)
+       then
+               echo "Bad -- should have detached"
+               false
+       else
+               : happy
+       fi
+
+'
+
+test_done
diff --git a/t/t7300-clean.sh b/t/t7300-clean.sh
new file mode 100755 (executable)
index 0000000..dfd1188
--- /dev/null
@@ -0,0 +1,319 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Michael Spang
+#
+
+test_description='git-clean basic tests'
+
+. ./test-lib.sh
+
+git config clean.requireForce no
+
+test_expect_success 'setup' '
+
+       mkdir -p src &&
+       touch src/part1.c Makefile &&
+       echo build >.gitignore &&
+       echo \*.o >>.gitignore &&
+       git add . &&
+       git-commit -m setup &&
+       touch src/part2.c README &&
+       git add .
+
+'
+
+test_expect_success 'git-clean' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test ! -f a.out &&
+       test ! -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean src/' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean src/ &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test -f a.out &&
+       test ! -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean src/ src/' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean src/ src/ &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test -f a.out &&
+       test ! -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean with prefix' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       (cd src/ && git-clean) &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test -f a.out &&
+       test ! -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+test_expect_success 'git-clean -d with prefix and path' '
+
+       mkdir -p build docs src/feature &&
+       touch a.out src/part3.c src/feature/file.c docs/manual.txt obj.o build/lib.so &&
+       (cd src/ && git-clean -d feature/) &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test -f a.out &&
+       test -f src/part3.c &&
+       test ! -f src/feature/file.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean symbolic link' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       ln -s docs/manual.txt src/part4.c
+       git-clean &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test ! -f a.out &&
+       test ! -f src/part3.c &&
+       test ! -f src/part4.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean with wildcard' '
+
+       touch a.clean b.clean other.c &&
+       git-clean "*.clean" &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test ! -f a.clean &&
+       test ! -f b.clean &&
+       test -f other.c
+
+'
+
+test_expect_success 'git-clean -n' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean -n &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test -f a.out &&
+       test -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean -d' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean -d &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test ! -f a.out &&
+       test ! -f src/part3.c &&
+       test ! -d docs &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean -d src/ examples/' '
+
+       mkdir -p build docs examples &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so examples/1.c &&
+       git-clean -d src/ examples/ &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test -f a.out &&
+       test ! -f src/part3.c &&
+       test ! -f examples/1.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean -x' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean -x &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test ! -f a.out &&
+       test ! -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test ! -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean -d -x' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean -d -x &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test ! -f a.out &&
+       test ! -f src/part3.c &&
+       test ! -d docs &&
+       test ! -f obj.o &&
+       test ! -d build
+
+'
+
+test_expect_success 'git-clean -X' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean -X &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test -f a.out &&
+       test -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test ! -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'git-clean -d -X' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean -d -X &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test -f a.out &&
+       test -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test ! -f obj.o &&
+       test ! -d build
+
+'
+
+test_expect_success 'clean.requireForce defaults to true' '
+
+       git config --unset clean.requireForce &&
+       ! git-clean
+
+'
+
+test_expect_success 'clean.requireForce' '
+
+       git config clean.requireForce true &&
+       ! git-clean
+
+'
+
+test_expect_success 'clean.requireForce and -n' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c docs/manual.txt obj.o build/lib.so &&
+       git-clean -n &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test -f a.out &&
+       test -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'clean.requireForce and -f' '
+
+       git-clean -f &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test ! -f a.out &&
+       test ! -f src/part3.c &&
+       test -f docs/manual.txt &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
+test_expect_success 'core.excludesfile' '
+
+       echo excludes >excludes &&
+       echo included >included &&
+       git config core.excludesfile excludes &&
+       output=$(git clean -n excludes included 2>&1) &&
+       expr "$output" : ".*included" >/dev/null &&
+       ! expr "$output" : ".*excludes" >/dev/null
+
+'
+
+test_done
diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh
new file mode 100755 (executable)
index 0000000..4fe3a41
--- /dev/null
@@ -0,0 +1,199 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Lars Hjemli
+#
+
+test_description='Basic porcelain support for submodules
+
+This test tries to verify basic sanity of the init, update and status
+subcommands of git-submodule.
+'
+
+. ./test-lib.sh
+
+#
+# Test setup:
+#  -create a repository in directory lib
+#  -add a couple of files
+#  -add directory lib to 'superproject', this creates a DIRLINK entry
+#  -add a couple of regular files to enable testing of submodule filtering
+#  -mv lib subrepo
+#  -add an entry to .gitmodules for submodule 'example'
+#
+test_expect_success 'Prepare submodule testing' '
+       : > t &&
+       git-add t &&
+       git-commit -m "initial commit" &&
+       git branch initial HEAD &&
+       mkdir lib &&
+       cd lib &&
+       git init &&
+       echo a >a &&
+       git add a &&
+       git-commit -m "submodule commit 1" &&
+       git-tag -a -m "rev-1" rev-1 &&
+       rev1=$(git rev-parse HEAD) &&
+       if test -z "$rev1"
+       then
+               echo "[OOPS] submodule git rev-parse returned nothing"
+               false
+       fi &&
+       cd .. &&
+       echo a >a &&
+       echo z >z &&
+       git add a lib z &&
+       git-commit -m "super commit 1" &&
+       mv lib .subrepo &&
+       GIT_CONFIG=.gitmodules git config submodule.example.url git://example.com/lib.git
+'
+
+test_expect_success 'status should fail for unmapped paths' '
+       if git-submodule status
+       then
+               echo "[OOPS] submodule status succeeded"
+               false
+       elif ! GIT_CONFIG=.gitmodules git config submodule.example.path lib
+       then
+               echo "[OOPS] git config failed to update .gitmodules"
+               false
+       fi
+'
+
+test_expect_success 'status should only print one line' '
+       lines=$(git-submodule status | wc -l) &&
+       test $lines = 1
+'
+
+test_expect_success 'status should initially be "missing"' '
+       git-submodule status | grep "^-$rev1"
+'
+
+test_expect_success 'init should register submodule url in .git/config' '
+       git-submodule init &&
+       url=$(git config submodule.example.url) &&
+       if test "$url" != "git://example.com/lib.git"
+       then
+               echo "[OOPS] init succeeded but submodule url is wrong"
+               false
+       elif ! git config submodule.example.url ./.subrepo
+       then
+               echo "[OOPS] init succeeded but update of url failed"
+               false
+       fi
+'
+
+test_expect_success 'update should fail when path is used by a file' '
+       echo "hello" >lib &&
+       if git-submodule update
+       then
+               echo "[OOPS] update should have failed"
+               false
+       elif test "$(cat lib)" != "hello"
+       then
+               echo "[OOPS] update failed but lib file was molested"
+               false
+       else
+               rm lib
+       fi
+'
+
+test_expect_success 'update should fail when path is used by a nonempty directory' '
+       mkdir lib &&
+       echo "hello" >lib/a &&
+       if git-submodule update
+       then
+               echo "[OOPS] update should have failed"
+               false
+       elif test "$(cat lib/a)" != "hello"
+       then
+               echo "[OOPS] update failed but lib/a was molested"
+               false
+       else
+               rm lib/a
+       fi
+'
+
+test_expect_success 'update should work when path is an empty dir' '
+       rm -rf lib &&
+       mkdir lib &&
+       git-submodule update &&
+       head=$(cd lib && git rev-parse HEAD) &&
+       if test -z "$head"
+       then
+               echo "[OOPS] Failed to obtain submodule head"
+               false
+       elif test "$head" != "$rev1"
+       then
+               echo "[OOPS] Submodule head is $head but should have been $rev1"
+               false
+       fi
+'
+
+test_expect_success 'status should be "up-to-date" after update' '
+       git-submodule status | grep "^ $rev1"
+'
+
+test_expect_success 'status should be "modified" after submodule commit' '
+       cd lib &&
+       echo b >b &&
+       git add b &&
+       git-commit -m "submodule commit 2" &&
+       rev2=$(git rev-parse HEAD) &&
+       cd .. &&
+       if test -z "$rev2"
+       then
+               echo "[OOPS] submodule git rev-parse returned nothing"
+               false
+       fi &&
+       git-submodule status | grep "^+$rev2"
+'
+
+test_expect_success 'the --cached sha1 should be rev1' '
+       git-submodule --cached status | grep "^+$rev1"
+'
+
+test_expect_success 'git diff should report the SHA1 of the new submodule commit' '
+       git-diff | grep "^+Subproject commit $rev2"
+'
+
+test_expect_success 'update should checkout rev1' '
+       git-submodule update &&
+       head=$(cd lib && git rev-parse HEAD) &&
+       if test -z "$head"
+       then
+               echo "[OOPS] submodule git rev-parse returned nothing"
+               false
+       elif test "$head" != "$rev1"
+       then
+               echo "[OOPS] init did not checkout correct head"
+               false
+       fi
+'
+
+test_expect_success 'status should be "up-to-date" after update' '
+       git-submodule status | grep "^ $rev1"
+'
+
+test_expect_success 'checkout superproject with subproject already present' '
+       git-checkout initial &&
+       git-checkout master
+'
+
+test_expect_success 'apply submodule diff' '
+       git branch second &&
+       (
+               cd lib &&
+               echo s >s &&
+               git add s &&
+               git commit -m "change subproject"
+       ) &&
+       git update-index --add lib &&
+       git-commit -m "change lib" &&
+       git-format-patch -1 --stdout >P.diff &&
+       git checkout second &&
+       git apply --index P.diff &&
+       D=$(git diff --cached master) &&
+       test -z "$D"
+'
+
+test_done
diff --git a/t/t7500-commit.sh b/t/t7500-commit.sh
new file mode 100755 (executable)
index 0000000..baed6ce
--- /dev/null
@@ -0,0 +1,141 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Steven Grimm
+#
+
+test_description='git-commit
+
+Tests for selected commit options.'
+
+. ./test-lib.sh
+
+commit_msg_is () {
+       test "`git log --pretty=format:%s%b -1`" = "$1"
+}
+
+# A sanity check to see if commit is working at all.
+test_expect_success 'a basic commit in an empty tree should succeed' '
+       echo content > foo &&
+       git add foo &&
+       git commit -m "initial commit"
+'
+
+test_expect_success 'nonexistent template file should return error' '
+       echo changes >> foo &&
+       git add foo &&
+       ! git commit --template "$PWD"/notexist
+'
+
+test_expect_success 'nonexistent template file in config should return error' '
+       git config commit.template "$PWD"/notexist &&
+       ! git commit &&
+       git config --unset commit.template
+'
+
+# From now on we'll use a template file that exists.
+TEMPLATE="$PWD"/template
+
+test_expect_success 'unedited template should not commit' '
+       echo "template line" > "$TEMPLATE" &&
+       ! git commit --template "$TEMPLATE"
+'
+
+test_expect_success 'unedited template with comments should not commit' '
+       echo "# comment in template" >> "$TEMPLATE" &&
+       ! git commit --template "$TEMPLATE"
+'
+
+test_expect_success 'a Signed-off-by line by itself should not commit' '
+       ! GIT_EDITOR=../t7500/add-signed-off git commit --template "$TEMPLATE"
+'
+
+test_expect_success 'adding comments to a template should not commit' '
+       ! GIT_EDITOR=../t7500/add-comments git commit --template "$TEMPLATE"
+'
+
+test_expect_success 'adding real content to a template should commit' '
+       GIT_EDITOR=../t7500/add-content git commit --template "$TEMPLATE" &&
+       commit_msg_is "template linecommit message"
+'
+
+test_expect_success '-t option should be short for --template' '
+       echo "short template" > "$TEMPLATE" &&
+       echo "new content" >> foo &&
+       git add foo &&
+       GIT_EDITOR=../t7500/add-content git commit -t "$TEMPLATE" &&
+       commit_msg_is "short templatecommit message"
+'
+
+test_expect_success 'config-specified template should commit' '
+       echo "new template" > "$TEMPLATE" &&
+       git config commit.template "$TEMPLATE" &&
+       echo "more content" >> foo &&
+       git add foo &&
+       GIT_EDITOR=../t7500/add-content git commit &&
+       git config --unset commit.template &&
+       commit_msg_is "new templatecommit message"
+'
+
+test_expect_success 'explicit commit message should override template' '
+       echo "still more content" >> foo &&
+       git add foo &&
+       GIT_EDITOR=../t7500/add-content git commit --template "$TEMPLATE" \
+               -m "command line msg" &&
+       commit_msg_is "command line msg"
+'
+
+test_expect_success 'commit message from file should override template' '
+       echo "content galore" >> foo &&
+       git add foo &&
+       echo "standard input msg" |
+               GIT_EDITOR=../t7500/add-content git commit \
+                       --template "$TEMPLATE" --file - &&
+       commit_msg_is "standard input msg"
+'
+
+test_expect_success 'using alternate GIT_INDEX_FILE (1)' '
+
+       cp .git/index saved-index &&
+       (
+               echo some new content >file &&
+               GIT_INDEX_FILE=.git/another_index &&
+               export GIT_INDEX_FILE &&
+               git add file &&
+               git commit -m "commit using another index" &&
+               git diff-index --exit-code HEAD &&
+               git diff-files --exit-code
+       ) &&
+       cmp .git/index saved-index >/dev/null
+
+'
+
+test_expect_success 'using alternate GIT_INDEX_FILE (2)' '
+
+       cp .git/index saved-index &&
+       (
+               rm -f .git/no-such-index &&
+               GIT_INDEX_FILE=.git/no-such-index &&
+               export GIT_INDEX_FILE &&
+               git commit -m "commit using nonexistent index" &&
+               test -z "$(git ls-files)" &&
+               test -z "$(git ls-tree HEAD)"
+
+       ) &&
+       cmp .git/index saved-index >/dev/null
+'
+
+cat > expect << EOF
+zort
+
+Signed-off-by: C O Mitter <committer@example.com>
+EOF
+
+test_expect_success '--signoff' '
+       echo "yet another content *narf*" >> foo &&
+       echo "zort" |
+               GIT_EDITOR=../t7500/add-content git commit -s -F - foo &&
+       git cat-file commit HEAD | sed "1,/^$/d" > output &&
+       diff expect output
+'
+
+test_done
diff --git a/t/t7500/add-comments b/t/t7500/add-comments
new file mode 100755 (executable)
index 0000000..a72e65c
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/sh
+echo "# this is a new comment" >> "$1"
+echo "# and so is this" >> "$1"
+exit 0
diff --git a/t/t7500/add-content b/t/t7500/add-content
new file mode 100755 (executable)
index 0000000..2fa3d86
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+echo "commit message" >> "$1"
+exit 0
diff --git a/t/t7500/add-signed-off b/t/t7500/add-signed-off
new file mode 100755 (executable)
index 0000000..e1d856a
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+echo "Signed-off-by: foo <bar@frotz>" >> "$1"
+exit 0
diff --git a/t/t7501-commit.sh b/t/t7501-commit.sh
new file mode 100755 (executable)
index 0000000..19c4b2c
--- /dev/null
@@ -0,0 +1,313 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Kristian Høgsberg <krh@redhat.com>
+#
+
+# FIXME: Test the various index usages, -i and -o, test reflog,
+# signoff, hooks
+
+test_description='git-commit'
+. ./test-lib.sh
+
+test_tick
+
+test_expect_success \
+       "initial status" \
+       "echo 'bongo bongo' >file &&
+        git-add file && \
+        git-status | grep 'Initial commit'"
+
+test_expect_failure \
+       "fail initial amend" \
+       "git-commit --amend"
+
+test_expect_success \
+       "initial commit" \
+       "git-commit -m initial"
+
+test_expect_failure \
+       "invalid options 1" \
+       "git-commit -m foo -m bar -F file"
+
+test_expect_failure \
+       "invalid options 2" \
+       "git-commit -C HEAD -m illegal"
+
+test_expect_failure \
+       "using paths with -a" \
+       "echo King of the bongo >file &&
+       git-commit -m foo -a file"
+
+test_expect_failure \
+       "using paths with --interactive" \
+       "echo bong-o-bong >file &&
+       echo 7 | git-commit -m foo --interactive file"
+
+test_expect_failure \
+       "using invalid commit with -C" \
+       "git-commit -C bogus"
+
+test_expect_failure \
+       "testing nothing to commit" \
+       "git-commit -m initial"
+
+test_expect_success \
+       "next commit" \
+       "echo 'bongo bongo bongo' >file \
+        git-commit -m next -a"
+
+test_expect_failure \
+       "commit message from non-existing file" \
+       "echo 'more bongo: bongo bongo bongo bongo' >file && \
+        git-commit -F gah -a"
+
+# Empty except stray tabs and spaces on a few lines.
+sed -e 's/@$//' >msg <<EOF
+               @
+
+  @
+Signed-off-by: hula
+EOF
+test_expect_failure \
+       "empty commit message" \
+       "git-commit -F msg -a"
+
+test_expect_success \
+       "commit message from file" \
+       "echo 'this is the commit message, coming from a file' >msg && \
+        git-commit -F msg -a"
+
+cat >editor <<\EOF
+#!/bin/sh
+sed -e "s/a file/an amend commit/g" < $1 > $1-
+mv $1- $1
+EOF
+chmod 755 editor
+
+test_expect_success \
+       "amend commit" \
+       "VISUAL=./editor git-commit --amend"
+
+test_expect_failure \
+       "passing -m and -F" \
+       "echo 'enough with the bongos' >file && \
+        git-commit -F msg -m amending ."
+
+test_expect_success \
+       "using message from other commit" \
+       "git-commit -C HEAD^ ."
+
+cat >editor <<\EOF
+#!/bin/sh
+sed -e "s/amend/older/g"  < $1 > $1-
+mv $1- $1
+EOF
+chmod 755 editor
+
+test_expect_success \
+       "editing message from other commit" \
+       "echo 'hula hula' >file && \
+        VISUAL=./editor git-commit -c HEAD^ -a"
+
+test_expect_success \
+       "message from stdin" \
+       "echo 'silly new contents' >file && \
+        echo commit message from stdin | git-commit -F - -a"
+
+test_expect_success \
+       "overriding author from command line" \
+       "echo 'gak' >file && \
+        git-commit -m 'author' --author 'Rubber Duck <rduck@convoy.org>' -a"
+
+test_expect_success \
+       "interactive add" \
+       "echo 7 | git-commit --interactive | grep 'What now'"
+
+test_expect_success \
+       "showing committed revisions" \
+       "git-rev-list HEAD >current"
+
+# We could just check the head sha1, but checking each commit makes it
+# easier to isolate bugs.
+
+cat >expected <<\EOF
+72c0dc9855b0c9dadcbfd5a31cab072e0cb774ca
+9b88fc14ce6b32e3d9ee021531a54f18a5cf38a2
+3536bbb352c3a1ef9a420f5b4242d48578b92aa7
+d381ac431806e53f3dd7ac2f1ae0534f36d738b9
+4fd44095ad6334f3ef72e4c5ec8ddf108174b54a
+402702b49136e7587daa9280e91e4bb7cb2179f7
+EOF
+
+test_expect_success \
+    'validate git-rev-list output.' \
+    'diff current expected'
+
+test_expect_success 'partial commit that involves removal (1)' '
+
+       git rm --cached file &&
+       mv file elif &&
+       git add elif &&
+       git commit -m "Partial: add elif" elif &&
+       git diff-tree --name-status HEAD^ HEAD >current &&
+       echo "A elif" >expected &&
+       diff expected current
+
+'
+
+test_expect_success 'partial commit that involves removal (2)' '
+
+       git commit -m "Partial: remove file" file &&
+       git diff-tree --name-status HEAD^ HEAD >current &&
+       echo "D file" >expected &&
+       diff expected current
+
+'
+
+test_expect_success 'partial commit that involves removal (3)' '
+
+       git rm --cached elif &&
+       echo elif >elif &&
+       git commit -m "Partial: modify elif" elif &&
+       git diff-tree --name-status HEAD^ HEAD >current &&
+       echo "M elif" >expected &&
+       diff expected current
+
+'
+
+author="The Real Author <someguy@his.email.org>"
+test_expect_success 'amend commit to fix author' '
+
+       oldtick=$GIT_AUTHOR_DATE &&
+       test_tick &&
+       git reset --hard &&
+       git cat-file -p HEAD |
+       sed -e "s/author.*/author $author $oldtick/" \
+               -e "s/^\(committer.*> \).*$/\1$GIT_COMMITTER_DATE/" > \
+               expected &&
+       git commit --amend --author="$author" &&
+       git cat-file -p HEAD > current &&
+       diff expected current
+
+'
+
+test_expect_success 'sign off (1)' '
+
+       echo 1 >positive &&
+       git add positive &&
+       git commit -s -m "thank you" &&
+       git cat-file commit HEAD | sed -e "1,/^\$/d" >actual &&
+       (
+               echo thank you
+               echo
+               git var GIT_COMMITTER_IDENT |
+               sed -e "s/>.*/>/" -e "s/^/Signed-off-by: /"
+       ) >expected &&
+       diff -u expected actual
+
+'
+
+test_expect_success 'sign off (2)' '
+
+       echo 2 >positive &&
+       git add positive &&
+       existing="Signed-off-by: Watch This <watchthis@example.com>" &&
+       git commit -s -m "thank you
+
+$existing" &&
+       git cat-file commit HEAD | sed -e "1,/^\$/d" >actual &&
+       (
+               echo thank you
+               echo
+               echo $existing
+               git var GIT_COMMITTER_IDENT |
+               sed -e "s/>.*/>/" -e "s/^/Signed-off-by: /"
+       ) >expected &&
+       diff -u expected actual
+
+'
+
+test_expect_success 'multiple -m' '
+
+       >negative &&
+       git add negative &&
+       git commit -m "one" -m "two" -m "three" &&
+       git cat-file commit HEAD | sed -e "1,/^\$/d" >actual &&
+       (
+               echo one
+               echo
+               echo two
+               echo
+               echo three
+       ) >expected &&
+       diff -u expected actual
+
+'
+
+author="The Real Author <someguy@his.email.org>"
+test_expect_success 'amend commit to fix author' '
+
+       oldtick=$GIT_AUTHOR_DATE &&
+       test_tick &&
+       git reset --hard &&
+       git cat-file -p HEAD |
+       sed -e "s/author.*/author $author $oldtick/" \
+               -e "s/^\(committer.*> \).*$/\1$GIT_COMMITTER_DATE/" > \
+               expected &&
+       git commit --amend --author="$author" &&
+       git cat-file -p HEAD > current &&
+       diff expected current
+
+'
+
+test_expect_success 'git commit <file> with dirty index' '
+       echo tacocat > elif &&
+       echo tehlulz > chz &&
+       git add chz &&
+       git commit elif -m "tacocat is a palindrome" &&
+       git show --stat | grep elif &&
+       git diff --cached | grep chz
+'
+
+test_expect_success 'same tree (single parent)' '
+
+       git reset --hard
+
+       if git commit -m empty
+       then
+               echo oops -- should have complained
+               false
+       else
+               : happy
+       fi
+
+'
+
+test_expect_success 'same tree (single parent) --allow-empty' '
+
+       git commit --allow-empty -m "forced empty" &&
+       git cat-file commit HEAD | grep forced
+
+'
+
+test_expect_success 'same tree (merge and amend merge)' '
+
+       git checkout -b side HEAD^ &&
+       echo zero >zero &&
+       git add zero &&
+       git commit -m "add zero" &&
+       git checkout master &&
+
+       git merge -s ours side -m "empty ok" &&
+       git diff HEAD^ HEAD >actual &&
+       : >expected &&
+       diff -u expected actual &&
+
+       git commit --amend -m "empty really ok" &&
+       git diff HEAD^ HEAD >actual &&
+       : >expected &&
+       diff -u expected actual
+
+'
+
+test_done
diff --git a/t/t7502-commit.sh b/t/t7502-commit.sh
new file mode 100755 (executable)
index 0000000..21ac785
--- /dev/null
@@ -0,0 +1,92 @@
+#!/bin/sh
+
+test_description='git commit porcelain-ish'
+
+. ./test-lib.sh
+
+test_expect_success 'the basics' '
+
+       echo doing partial >"commit is" &&
+       mkdir not &&
+       echo very much encouraged but we should >not/forbid &&
+       git add "commit is" not &&
+       echo update added "commit is" file >"commit is" &&
+       echo also update another >not/forbid &&
+       test_tick &&
+       git commit -a -m "initial with -a" &&
+
+       git cat-file blob HEAD:"commit is" >current.1 &&
+       git cat-file blob HEAD:not/forbid >current.2 &&
+
+       cmp current.1 "commit is" &&
+       cmp current.2 not/forbid
+
+'
+
+test_expect_success 'partial' '
+
+       echo another >"commit is" &&
+       echo another >not/forbid &&
+       test_tick &&
+       git commit -m "partial commit to handle a file" "commit is" &&
+
+       changed=$(git diff-tree --name-only HEAD^ HEAD) &&
+       test "$changed" = "commit is"
+
+'
+
+test_expect_success 'partial modification in a subdirecotry' '
+
+       test_tick &&
+       git commit -m "partial commit to subdirectory" not &&
+
+       changed=$(git diff-tree -r --name-only HEAD^ HEAD) &&
+       test "$changed" = "not/forbid"
+
+'
+
+test_expect_success 'partial removal' '
+
+       git rm not/forbid &&
+       git commit -m "partial commit to remove not/forbid" not &&
+
+       changed=$(git diff-tree -r --name-only HEAD^ HEAD) &&
+       test "$changed" = "not/forbid" &&
+       remain=$(git ls-tree -r --name-only HEAD) &&
+       test "$remain" = "commit is"
+
+'
+
+test_expect_success 'sign off' '
+
+       >positive &&
+       git add positive &&
+       git commit -s -m "thank you" &&
+       actual=$(git cat-file commit HEAD | sed -ne "s/Signed-off-by: //p") &&
+       expected=$(git var GIT_COMMITTER_IDENT | sed -e "s/>.*/>/") &&
+       test "z$actual" = "z$expected"
+
+'
+
+test_expect_success 'multiple -m' '
+
+       >negative &&
+       git add negative &&
+       git commit -m "one" -m "two" -m "three" &&
+       actual=$(git cat-file commit HEAD | sed -e "1,/^\$/d") &&
+       expected=$(echo one; echo; echo two; echo; echo three) &&
+       test "z$actual" = "z$expected"
+
+'
+
+test_expect_success 'verbose' '
+
+       echo minus >negative &&
+       git add negative &&
+       git status -v | sed -ne "/^diff --git /p" >actual &&
+       echo "diff --git a/negative b/negative" >expect &&
+       diff -u expect actual
+
+'
+
+test_done
diff --git a/t/t7502-status.sh b/t/t7502-status.sh
new file mode 100755 (executable)
index 0000000..d6ae69d
--- /dev/null
@@ -0,0 +1,91 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes E. Schindelin
+#
+
+test_description='git-status'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+       : > tracked &&
+       : > modified &&
+       mkdir dir1 &&
+       : > dir1/tracked &&
+       : > dir1/modified &&
+       mkdir dir2 &&
+       : > dir1/tracked &&
+       : > dir1/modified &&
+       git add . &&
+       test_tick &&
+       git commit -m initial &&
+       : > untracked &&
+       : > dir1/untracked &&
+       : > dir2/untracked &&
+       echo 1 > dir1/modified &&
+       echo 2 > dir2/modified &&
+       echo 3 > dir2/added &&
+       git add dir2/added
+'
+
+cat > expect << \EOF
+# On branch master
+# Changes to be committed:
+#   (use "git reset HEAD <file>..." to unstage)
+#
+#      new file:   dir2/added
+#
+# Changed but not updated:
+#   (use "git add <file>..." to update what will be committed)
+#
+#      modified:   dir1/modified
+#
+# Untracked files:
+#   (use "git add <file>..." to include in what will be committed)
+#
+#      dir1/untracked
+#      dir2/modified
+#      dir2/untracked
+#      expect
+#      output
+#      untracked
+EOF
+
+test_expect_success 'status' '
+
+       git status > output &&
+       git diff expect output
+
+'
+
+cat > expect << \EOF
+# On branch master
+# Changes to be committed:
+#   (use "git reset HEAD <file>..." to unstage)
+#
+#      new file:   ../dir2/added
+#
+# Changed but not updated:
+#   (use "git add <file>..." to update what will be committed)
+#
+#      modified:   modified
+#
+# Untracked files:
+#   (use "git add <file>..." to include in what will be committed)
+#
+#      untracked
+#      ../dir2/modified
+#      ../dir2/untracked
+#      ../expect
+#      ../output
+#      ../untracked
+EOF
+
+test_expect_success 'status with relative paths' '
+
+       (cd dir1 && git status) > output &&
+       git diff expect output
+
+'
+
+test_done
diff --git a/t/t7600-merge.sh b/t/t7600-merge.sh
new file mode 100755 (executable)
index 0000000..6424c6e
--- /dev/null
@@ -0,0 +1,440 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Lars Hjemli
+#
+
+test_description='git-merge
+
+Testing basic merge operations/option parsing.'
+
+. ./test-lib.sh
+
+cat >file <<EOF
+1
+2
+3
+4
+5
+6
+7
+8
+9
+EOF
+
+cat >file.1 <<EOF
+1 X
+2
+3
+4
+5
+6
+7
+8
+9
+EOF
+
+cat >file.5 <<EOF
+1
+2
+3
+4
+5 X
+6
+7
+8
+9
+EOF
+
+cat >file.9 <<EOF
+1
+2
+3
+4
+5
+6
+7
+8
+9 X
+EOF
+
+cat  >result.1 <<EOF
+1 X
+2
+3
+4
+5
+6
+7
+8
+9
+EOF
+
+cat >result.1-5 <<EOF
+1 X
+2
+3
+4
+5 X
+6
+7
+8
+9
+EOF
+
+cat >result.1-5-9 <<EOF
+1 X
+2
+3
+4
+5 X
+6
+7
+8
+9 X
+EOF
+
+create_merge_msgs() {
+       echo "Merge commit 'c2'" >msg.1-5 &&
+       echo "Merge commit 'c2'; commit 'c3'" >msg.1-5-9 &&
+       echo "Squashed commit of the following:" >squash.1 &&
+       echo >>squash.1 &&
+       git log --no-merges ^HEAD c1 >>squash.1 &&
+       echo "Squashed commit of the following:" >squash.1-5 &&
+       echo >>squash.1-5 &&
+       git log --no-merges ^HEAD c2 >>squash.1-5 &&
+       echo "Squashed commit of the following:" >squash.1-5-9 &&
+       echo >>squash.1-5-9 &&
+       git log --no-merges ^HEAD c2 c3 >>squash.1-5-9
+}
+
+verify_diff() {
+       if ! diff -u "$1" "$2"
+       then
+               echo "$3"
+               false
+       fi
+}
+
+verify_merge() {
+       verify_diff "$2" "$1" "[OOPS] bad merge result" &&
+       if test $(git ls-files -u | wc -l) -gt 0
+       then
+               echo "[OOPS] unmerged files"
+               false
+       fi &&
+       if ! git diff --exit-code
+       then
+               echo "[OOPS] working tree != index"
+               false
+       fi &&
+       if test -n "$3"
+       then
+               git show -s --pretty=format:%s HEAD >msg.act &&
+               verify_diff "$3" msg.act "[OOPS] bad merge message"
+       fi
+}
+
+verify_head() {
+       if test "$1" != "$(git rev-parse HEAD)"
+       then
+               echo "[OOPS] HEAD != $1"
+               false
+       fi
+}
+
+verify_parents() {
+       i=1
+       while test $# -gt 0
+       do
+               if test "$1" != "$(git rev-parse HEAD^$i)"
+               then
+                       echo "[OOPS] HEAD^$i != $1"
+                       return 1
+               fi
+               i=$(expr $i + 1)
+               shift
+       done
+}
+
+verify_mergeheads() {
+       i=1
+       if ! test -f .git/MERGE_HEAD
+       then
+               echo "[OOPS] MERGE_HEAD is missing"
+               false
+       fi &&
+       while test $# -gt 0
+       do
+               head=$(head -n $i .git/MERGE_HEAD | tail -n 1)
+               if test "$1" != "$head"
+               then
+                       echo "[OOPS] MERGE_HEAD $i != $1"
+                       return 1
+               fi
+               i=$(expr $i + 1)
+               shift
+       done
+}
+
+verify_no_mergehead() {
+       if test -f .git/MERGE_HEAD
+       then
+               echo "[OOPS] MERGE_HEAD exists"
+               false
+       fi
+}
+
+
+test_expect_success 'setup' '
+       git add file &&
+       test_tick &&
+       git commit -m "commit 0" &&
+       git tag c0 &&
+       c0=$(git rev-parse HEAD) &&
+       cp file.1 file &&
+       git add file &&
+       test_tick &&
+       git commit -m "commit 1" &&
+       git tag c1 &&
+       c1=$(git rev-parse HEAD) &&
+       git reset --hard "$c0" &&
+       cp file.5 file &&
+       git add file &&
+       test_tick &&
+       git commit -m "commit 2" &&
+       git tag c2 &&
+       c2=$(git rev-parse HEAD) &&
+       git reset --hard "$c0" &&
+       cp file.9 file &&
+       git add file &&
+       test_tick &&
+       git commit -m "commit 3" &&
+       git tag c3 &&
+       c3=$(git rev-parse HEAD)
+       git reset --hard "$c0" &&
+       create_merge_msgs
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'test option parsing' '
+       if git merge -$ c1
+       then
+               echo "[OOPS] -$ accepted"
+               false
+       fi &&
+       if git merge --no-such c1
+       then
+               echo "[OOPS] --no-such accepted"
+               false
+       fi &&
+       if git merge -s foobar c1
+       then
+               echo "[OOPS] -s foobar accepted"
+               false
+       fi &&
+       if git merge -s=foobar c1
+       then
+               echo "[OOPS] -s=foobar accepted"
+               false
+       fi &&
+       if git merge -m
+       then
+               echo "[OOPS] missing commit msg accepted"
+               false
+       fi &&
+       if git merge
+       then
+               echo "[OOPS] missing commit references accepted"
+               false
+       fi
+'
+
+test_expect_success 'merge c0 with c1' '
+       git reset --hard c0 &&
+       git merge c1 &&
+       verify_merge file result.1 &&
+       verify_head "$c1"
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2' '
+       git reset --hard c1 &&
+       test_tick &&
+       git merge c2 &&
+       verify_merge file result.1-5 msg.1-5 &&
+       verify_parents $c1 $c2
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 and c3' '
+       git reset --hard c1 &&
+       test_tick &&
+       git merge c2 c3 &&
+       verify_merge file result.1-5-9 msg.1-5-9 &&
+       verify_parents $c1 $c2 $c3
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c0 with c1 (no-commit)' '
+       git reset --hard c0 &&
+       git merge --no-commit c1 &&
+       verify_merge file result.1 &&
+       verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (no-commit)' '
+       git reset --hard c1 &&
+       git merge --no-commit c2 &&
+       verify_merge file result.1-5 &&
+       verify_head $c1 &&
+       verify_mergeheads $c2
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 and c3 (no-commit)' '
+       git reset --hard c1 &&
+       git merge --no-commit c2 c3 &&
+       verify_merge file result.1-5-9 &&
+       verify_head $c1 &&
+       verify_mergeheads $c2 $c3
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c0 with c1 (squash)' '
+       git reset --hard c0 &&
+       git merge --squash c1 &&
+       verify_merge file result.1 &&
+       verify_head $c0 &&
+       verify_no_mergehead &&
+       verify_diff squash.1 .git/SQUASH_MSG "[OOPS] bad squash message"
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (squash)' '
+       git reset --hard c1 &&
+       git merge --squash c2 &&
+       verify_merge file result.1-5 &&
+       verify_head $c1 &&
+       verify_no_mergehead &&
+       verify_diff squash.1-5 .git/SQUASH_MSG "[OOPS] bad squash message"
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 and c3 (squash)' '
+       git reset --hard c1 &&
+       git merge --squash c2 c3 &&
+       verify_merge file result.1-5-9 &&
+       verify_head $c1 &&
+       verify_no_mergehead &&
+       verify_diff squash.1-5-9 .git/SQUASH_MSG "[OOPS] bad squash message"
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (no-commit in config)' '
+       git reset --hard c1 &&
+       git config branch.master.mergeoptions "--no-commit" &&
+       git merge c2 &&
+       verify_merge file result.1-5 &&
+       verify_head $c1 &&
+       verify_mergeheads $c2
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (squash in config)' '
+       git reset --hard c1 &&
+       git config branch.master.mergeoptions "--squash" &&
+       git merge c2 &&
+       verify_merge file result.1-5 &&
+       verify_head $c1 &&
+       verify_no_mergehead &&
+       verify_diff squash.1-5 .git/SQUASH_MSG "[OOPS] bad squash message"
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'override config option -n' '
+       git reset --hard c1 &&
+       git config branch.master.mergeoptions "-n" &&
+       test_tick &&
+       git merge --summary c2 >diffstat.txt &&
+       verify_merge file result.1-5 msg.1-5 &&
+       verify_parents $c1 $c2 &&
+       if ! grep -e "^ file | \+2 +-$" diffstat.txt
+       then
+               echo "[OOPS] diffstat was not generated"
+       fi
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'override config option --summary' '
+       git reset --hard c1 &&
+       git config branch.master.mergeoptions "--summary" &&
+       test_tick &&
+       git merge -n c2 >diffstat.txt &&
+       verify_merge file result.1-5 msg.1-5 &&
+       verify_parents $c1 $c2 &&
+       if grep -e "^ file | \+2 +-$" diffstat.txt
+       then
+               echo "[OOPS] diffstat was generated"
+               false
+       fi
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (override --no-commit)' '
+       git reset --hard c1 &&
+       git config branch.master.mergeoptions "--no-commit" &&
+       test_tick &&
+       git merge --commit c2 &&
+       verify_merge file result.1-5 msg.1-5 &&
+       verify_parents $c1 $c2
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (override --squash)' '
+       git reset --hard c1 &&
+       git config branch.master.mergeoptions "--squash" &&
+       test_tick &&
+       git merge --no-squash c2 &&
+       verify_merge file result.1-5 msg.1-5 &&
+       verify_parents $c1 $c2
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c0 with c1 (no-ff)' '
+       git reset --hard c0 &&
+       test_tick &&
+       git merge --no-ff c1 &&
+       verify_merge file result.1 &&
+       verify_parents $c0 $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c0 with c1 (ff overrides no-ff)' '
+       git reset --hard c0 &&
+       git config branch.master.mergeoptions "--no-ff" &&
+       git merge --ff c1 &&
+       verify_merge file result.1 &&
+       verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_done
diff --git a/t/t8001-annotate.sh b/t/t8001-annotate.sh
new file mode 100755 (executable)
index 0000000..eabec2e
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+test_description='git annotate'
+. ./test-lib.sh
+
+PROG='git annotate'
+. ../annotate-tests.sh
+
+test_expect_success \
+    'Annotating an old revision works' \
+    '[ $(git annotate file master | awk "{print \$3}" | grep -c "^A$") -eq 2 ] && \
+     [ $(git annotate file master | awk "{print \$3}" | grep -c "^B$") -eq 2 ]'
+
+
+test_done
diff --git a/t/t8002-blame.sh b/t/t8002-blame.sh
new file mode 100755 (executable)
index 0000000..92ece30
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+test_description='git blame'
+. ./test-lib.sh
+
+PROG='git blame -c'
+. ../annotate-tests.sh
+
+test_done
diff --git a/t/t8003-blame.sh b/t/t8003-blame.sh
new file mode 100755 (executable)
index 0000000..db51b3a
--- /dev/null
@@ -0,0 +1,132 @@
+#!/bin/sh
+
+test_description='git blame corner cases'
+. ./test-lib.sh
+
+pick_fc='s/^[0-9a-f^]* *\([^ ]*\) *(\([^ ]*\) .*/\1-\2/'
+
+test_expect_success setup '
+
+       echo A A A A A >one &&
+       echo B B B B B >two &&
+       echo C C C C C >tres &&
+       echo ABC >mouse &&
+       git add one two tres mouse &&
+       test_tick &&
+       GIT_AUTHOR_NAME=Initial git commit -m Initial &&
+
+       cat one >uno &&
+       mv two dos &&
+       cat one >>tres &&
+       echo DEF >>mouse
+       git add uno dos tres mouse &&
+       test_tick &&
+       GIT_AUTHOR_NAME=Second git commit -a -m Second &&
+
+       echo GHIJK >>mouse &&
+       git add mouse &&
+       test_tick &&
+       GIT_AUTHOR_NAME=Third git commit -m Third &&
+
+       cat mouse >cow &&
+       git add cow &&
+       test_tick &&
+       GIT_AUTHOR_NAME=Fourth git commit -m Fourth &&
+
+       {
+               echo ABC
+               echo DEF
+               echo XXXX
+               echo GHIJK
+       } >cow &&
+       git add cow &&
+       test_tick &&
+       GIT_AUTHOR_NAME=Fifth git commit -m Fifth
+'
+
+test_expect_success 'straight copy without -C' '
+
+       git blame uno | grep Second
+
+'
+
+test_expect_success 'straight move without -C' '
+
+       git blame dos | grep Initial
+
+'
+
+test_expect_success 'straight copy with -C' '
+
+       git blame -C1 uno | grep Second
+
+'
+
+test_expect_success 'straight move with -C' '
+
+       git blame -C1 dos | grep Initial
+
+'
+
+test_expect_success 'straight copy with -C -C' '
+
+       git blame -C -C1 uno | grep Initial
+
+'
+
+test_expect_success 'straight move with -C -C' '
+
+       git blame -C -C1 dos | grep Initial
+
+'
+
+test_expect_success 'append without -C' '
+
+       git blame -L2 tres | grep Second
+
+'
+
+test_expect_success 'append with -C' '
+
+       git blame -L2 -C1 tres | grep Second
+
+'
+
+test_expect_success 'append with -C -C' '
+
+       git blame -L2 -C -C1 tres | grep Second
+
+'
+
+test_expect_success 'append with -C -C -C' '
+
+       git blame -L2 -C -C -C1 tres | grep Initial
+
+'
+
+test_expect_success 'blame wholesale copy' '
+
+       git blame -f -C -C1 HEAD^ -- cow | sed -e "$pick_fc" >current &&
+       {
+               echo mouse-Initial
+               echo mouse-Second
+               echo mouse-Third
+       } >expected &&
+       diff -u expected current
+
+'
+
+test_expect_success 'blame wholesale copy and more' '
+
+       git blame -f -C -C1 HEAD -- cow | sed -e "$pick_fc" >current &&
+       {
+               echo mouse-Initial
+               echo mouse-Second
+               echo cow-Fifth
+               echo mouse-Third
+       } >expected &&
+       diff -u expected current
+
+'
+
+test_done
diff --git a/t/t8004-blame.sh b/t/t8004-blame.sh
new file mode 100755 (executable)
index 0000000..ba19ac1
--- /dev/null
@@ -0,0 +1,73 @@
+#!/bin/sh
+
+# Based on a test case submitted by Björn Steinbrink.
+
+test_description='git blame on conflicted files'
+. ./test-lib.sh
+
+test_expect_success 'setup first case' '
+       # Create the old file
+       echo "Old line" > file1 &&
+       git add file1 &&
+       git commit --author "Old Line <ol@localhost>" -m file1.a &&
+
+       # Branch
+       git checkout -b foo &&
+
+       # Do an ugly move and change
+       git rm file1 &&
+       echo "New line ..."  > file2 &&
+       echo "... and more" >> file2 &&
+       git add file2 &&
+       git commit --author "U Gly <ug@localhost>" -m ugly &&
+
+       # Back to master and change something
+       git checkout master &&
+       echo "
+
+bla" >> file1 &&
+       git commit --author "Old Line <ol@localhost>" -a -m file1.b &&
+
+       # Back to foo and merge master
+       git checkout foo &&
+       if git merge master; then
+               echo needed conflict here
+               exit 1
+       else
+               echo merge failed - resolving automatically
+       fi &&
+       echo "New line ...
+... and more
+
+bla
+Even more" > file2 &&
+       git rm file1 &&
+       git commit --author "M Result <mr@localhost>" -a -m merged &&
+
+       # Back to master and change file1 again
+       git checkout master &&
+       sed s/bla/foo/ <file1 >X &&
+       rm file1 &&
+       mv X file1 &&
+       git commit --author "No Bla <nb@localhost>" -a -m replace &&
+
+       # Try to merge into foo again
+       git checkout foo &&
+       if git merge master; then
+               echo needed conflict here
+               exit 1
+       else
+               echo merge failed - test is setup
+       fi
+'
+
+test_expect_success \
+       'blame runs on unconflicted file while other file has conflicts' '
+       git blame file2
+'
+
+test_expect_success 'blame runs on conflicted file in stages 1,3' '
+       git blame file1
+'
+
+test_done
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
new file mode 100755 (executable)
index 0000000..659f9c7
--- /dev/null
@@ -0,0 +1,81 @@
+#!/bin/sh
+
+test_description='git-send-email'
+. ./test-lib.sh
+
+PROG='git send-email'
+test_expect_success \
+    'prepare reference tree' \
+    'echo "1A quick brown fox jumps over the" >file &&
+     echo "lazy dog" >>file &&
+     git add file
+     GIT_AUTHOR_NAME="A" git commit -a -m "Initial."'
+
+test_expect_success \
+    'Setup helper tool' \
+    '(echo "#!/bin/sh"
+      echo shift
+      echo for a
+      echo do
+      echo "  echo \"!\$a!\""
+      echo "done >commandline"
+      echo "cat > msgtxt"
+      ) >fake.sendmail
+     chmod +x ./fake.sendmail
+     git add fake.sendmail
+     GIT_AUTHOR_NAME="A" git commit -a -m "Second."'
+
+test_expect_success 'Extract patches' '
+    patches=`git format-patch -n HEAD^1`
+'
+
+test_expect_success 'Send patches' '
+     git send-email --from="Example <nobody@example.com>" --to=nobody@example.com --smtp-server="$(pwd)/fake.sendmail" $patches 2>errors
+'
+
+cat >expected <<\EOF
+!nobody@example.com!
+!author@example.com!
+EOF
+test_expect_success \
+    'Verify commandline' \
+    'diff commandline expected'
+
+cat >expected-show-all-headers <<\EOF
+0001-Second.patch
+(mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
+Dry-OK. Log says:
+Server: relay.example.com
+MAIL FROM:<from@example.com>
+RCPT TO:<to@example.com>,<cc@example.com>,<author@example.com>,<bcc@example.com>
+From: Example <from@example.com>
+To: to@example.com
+Cc: cc@example.com, A <author@example.com>
+Subject: [PATCH 1/1] Second.
+Date: DATE-STRING
+Message-Id: MESSAGE-ID-STRING
+X-Mailer: X-MAILER-STRING
+In-Reply-To: <unique-message-id@example.com>
+References: <unique-message-id@example.com>
+
+Result: OK
+EOF
+
+test_expect_success 'Show all headers' '
+       git send-email \
+               --dry-run \
+               --from="Example <from@example.com>" \
+               --to=to@example.com \
+               --cc=cc@example.com \
+               --bcc=bcc@example.com \
+               --in-reply-to="<unique-message-id@example.com>" \
+               --smtp-server relay.example.com \
+               $patches |
+       sed     -e "s/^\(Date:\).*/\1 DATE-STRING/" \
+               -e "s/^\(Message-Id:\).*/\1 MESSAGE-ID-STRING/" \
+               -e "s/^\(X-Mailer:\).*/\1 X-MAILER-STRING/" \
+               >actual-show-all-headers &&
+       diff -u expected-show-all-headers actual-show-all-headers
+'
+
+test_done
diff --git a/t/t9100-git-svn-basic.sh b/t/t9100-git-svn-basic.sh
new file mode 100755 (executable)
index 0000000..614cf50
--- /dev/null
@@ -0,0 +1,268 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+#
+
+test_description='git-svn basic tests'
+GIT_SVN_LC_ALL=$LC_ALL
+
+case "$LC_ALL" in
+*.UTF-8)
+       have_utf8=t
+       ;;
+*)
+       have_utf8=
+       ;;
+esac
+
+. ./lib-git-svn.sh
+
+echo 'define NO_SVN_TESTS to skip git-svn tests'
+
+test_expect_success \
+    'initialize git-svn' "
+       mkdir import &&
+       cd import &&
+       echo foo > foo &&
+       ln -s foo foo.link
+       mkdir -p dir/a/b/c/d/e &&
+       echo 'deep dir' > dir/a/b/c/d/e/file &&
+       mkdir bar &&
+       echo 'zzz' > bar/zzz &&
+       echo '#!/bin/sh' > exec.sh &&
+       chmod +x exec.sh &&
+       svn import -m 'import for git-svn' . $svnrepo >/dev/null &&
+       cd .. &&
+       rm -rf import &&
+       git-svn init $svnrepo"
+
+test_expect_success \
+    'import an SVN revision into git' \
+    'git-svn fetch'
+
+test_expect_success "checkout from svn" "svn co $svnrepo '$SVN_TREE'"
+
+name='try a deep --rmdir with a commit'
+test_expect_success "$name" "
+       git checkout -f -b mybranch remotes/git-svn &&
+       mv dir/a/b/c/d/e/file dir/file &&
+       cp dir/file file &&
+       git update-index --add --remove dir/a/b/c/d/e/file dir/file file &&
+       git commit -m '$name' &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch &&
+       svn up '$SVN_TREE' &&
+       test -d '$SVN_TREE'/dir && test ! -d '$SVN_TREE'/dir/a"
+
+
+name='detect node change from file to directory #1'
+test_expect_failure "$name" "
+       mkdir dir/new_file &&
+       mv dir/file dir/new_file/file &&
+       mv dir/new_file dir/file &&
+       git update-index --remove dir/file &&
+       git update-index --add dir/file/file &&
+       git commit -m '$name'  &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch" || true
+
+
+name='detect node change from directory to file #1'
+test_expect_failure "$name" "
+       rm -rf dir '$GIT_DIR'/index &&
+       git checkout -f -b mybranch2 remotes/git-svn &&
+       mv bar/zzz zzz &&
+       rm -rf bar &&
+       mv zzz bar &&
+       git update-index --remove -- bar/zzz &&
+       git update-index --add -- bar &&
+       git commit -m '$name' &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch2" || true
+
+
+name='detect node change from file to directory #2'
+test_expect_failure "$name" "
+       rm -f '$GIT_DIR'/index &&
+       git checkout -f -b mybranch3 remotes/git-svn &&
+       rm bar/zzz &&
+       git update-index --remove bar/zzz &&
+       mkdir bar/zzz &&
+       echo yyy > bar/zzz/yyy &&
+       git update-index --add bar/zzz/yyy &&
+       git commit -m '$name' &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch3" || true
+
+
+name='detect node change from directory to file #2'
+test_expect_failure "$name" "
+       rm -f '$GIT_DIR'/index &&
+       git checkout -f -b mybranch4 remotes/git-svn &&
+       rm -rf dir &&
+       git update-index --remove -- dir/file &&
+       touch dir &&
+       echo asdf > dir &&
+       git update-index --add -- dir &&
+       git commit -m '$name' &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch4" || true
+
+
+name='remove executable bit from a file'
+test_expect_success "$name" "
+       rm -f '$GIT_DIR'/index &&
+       git checkout -f -b mybranch5 remotes/git-svn &&
+       chmod -x exec.sh &&
+       git update-index exec.sh &&
+       git commit -m '$name' &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch5 &&
+       svn up '$SVN_TREE' &&
+       test ! -x '$SVN_TREE'/exec.sh"
+
+
+name='add executable bit back file'
+test_expect_success "$name" "
+       chmod +x exec.sh &&
+       git update-index exec.sh &&
+       git commit -m '$name' &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch5 &&
+       svn up '$SVN_TREE' &&
+       test -x '$SVN_TREE'/exec.sh"
+
+
+name='executable file becomes a symlink to bar/zzz (file)'
+test_expect_success "$name" "
+       rm exec.sh &&
+       ln -s bar/zzz exec.sh &&
+       git update-index exec.sh &&
+       git commit -m '$name' &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch5 &&
+       svn up '$SVN_TREE' &&
+       test -L '$SVN_TREE'/exec.sh"
+
+name='new symlink is added to a file that was also just made executable'
+
+test_expect_success "$name" "
+       chmod +x bar/zzz &&
+       ln -s bar/zzz exec-2.sh &&
+       git update-index --add bar/zzz exec-2.sh &&
+       git commit -m '$name' &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch5 &&
+       svn up '$SVN_TREE' &&
+       test -x '$SVN_TREE'/bar/zzz &&
+       test -L '$SVN_TREE'/exec-2.sh"
+
+name='modify a symlink to become a file'
+test_expect_success "$name" "
+       echo git help > help || true &&
+       rm exec-2.sh &&
+       cp help exec-2.sh &&
+       git update-index exec-2.sh &&
+       git commit -m '$name' &&
+       git-svn set-tree --find-copies-harder --rmdir \
+               remotes/git-svn..mybranch5 &&
+       svn up '$SVN_TREE' &&
+       test -f '$SVN_TREE'/exec-2.sh &&
+       test ! -L '$SVN_TREE'/exec-2.sh &&
+       git diff help $SVN_TREE/exec-2.sh"
+
+if test "$have_utf8" = t
+then
+       name="commit with UTF-8 message: locale: $GIT_SVN_LC_ALL"
+       LC_ALL="$GIT_SVN_LC_ALL"
+       export LC_ALL
+       test_expect_success "$name" "
+               echo '# hello' >> exec-2.sh &&
+               git update-index exec-2.sh &&
+               git commit -m 'éï∏' &&
+               git-svn set-tree HEAD"
+       unset LC_ALL
+else
+       echo "UTF-8 locale not set, test skipped ($GIT_SVN_LC_ALL)"
+fi
+
+name='test fetch functionality (svn => git) with alternate GIT_SVN_ID'
+GIT_SVN_ID=alt
+export GIT_SVN_ID
+test_expect_success "$name" \
+    "git-svn init $svnrepo && git-svn fetch &&
+     git rev-list --pretty=raw remotes/git-svn | grep ^tree | uniq > a &&
+     git rev-list --pretty=raw remotes/alt | grep ^tree | uniq > b &&
+     git diff a b"
+
+name='check imported tree checksums expected tree checksums'
+rm -f expected
+if test "$have_utf8" = t
+then
+       echo tree bf522353586b1b883488f2bc73dab0d9f774b9a9 > expected
+fi
+cat >> expected <<\EOF
+tree 83654bb36f019ae4fe77a0171f81075972087624
+tree 031b8d557afc6fea52894eaebb45bec52f1ba6d1
+tree 0b094cbff17168f24c302e297f55bfac65eb8bd3
+tree d667270a1f7b109f5eb3aaea21ede14b56bfdd6e
+tree 56a30b966619b863674f5978696f4a3594f2fca9
+tree d667270a1f7b109f5eb3aaea21ede14b56bfdd6e
+tree 8f51f74cf0163afc9ad68a4b1537288c4558b5a4
+EOF
+
+test_expect_success "$name" "git diff a expected"
+
+test_expect_failure 'exit if remote refs are ambigious' "
+        git config --add svn-remote.svn.fetch \
+                              bar:refs/remotes/git-svn &&
+        git-svn migrate
+        "
+
+test_expect_failure 'exit if init-ing a would clobber a URL' "
+        svnadmin create ${PWD}/svnrepo2 &&
+        svn mkdir -m 'mkdir bar' ${svnrepo}2/bar &&
+        git config --unset svn-remote.svn.fetch \
+                                '^bar:refs/remotes/git-svn$' &&
+        git-svn init ${svnrepo}2/bar
+        "
+
+test_expect_success \
+  'init allows us to connect to another directory in the same repo' "
+        git-svn init --minimize-url -i bar $svnrepo/bar &&
+        git config --get svn-remote.svn.fetch \
+                              '^bar:refs/remotes/bar$' &&
+        git config --get svn-remote.svn.fetch \
+                              '^:refs/remotes/git-svn$'
+        "
+
+test_expect_success 'able to dcommit to a subdirectory' "
+       git-svn fetch -i bar &&
+       git checkout -b my-bar refs/remotes/bar &&
+       echo abc > d &&
+       git update-index --add d &&
+       git commit -m '/bar/d should be in the log' &&
+       git-svn dcommit -i bar &&
+       test -z \"\`git diff refs/heads/my-bar refs/remotes/bar\`\" &&
+       mkdir newdir &&
+       echo new > newdir/dir &&
+       git update-index --add newdir/dir &&
+       git commit -m 'add a new directory' &&
+       git-svn dcommit -i bar &&
+       test -z \"\`git diff refs/heads/my-bar refs/remotes/bar\`\" &&
+       echo foo >> newdir/dir &&
+       git update-index newdir/dir &&
+       git commit -m 'modify a file in new directory' &&
+       git-svn dcommit -i bar &&
+       test -z \"\`git diff refs/heads/my-bar refs/remotes/bar\`\"
+       "
+
+test_expect_success 'able to set-tree to a subdirectory' "
+       echo cba > d &&
+       git update-index d &&
+       git commit -m 'update /bar/d' &&
+       git-svn set-tree -i bar HEAD &&
+       test -z \"\`git diff refs/heads/my-bar refs/remotes/bar\`\"
+       "
+
+test_done
diff --git a/t/t9101-git-svn-props.sh b/t/t9101-git-svn-props.sh
new file mode 100755 (executable)
index 0000000..d7a7047
--- /dev/null
@@ -0,0 +1,217 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+#
+
+test_description='git-svn property tests'
+. ./lib-git-svn.sh
+
+mkdir import
+
+a_crlf=
+a_lf=
+a_cr=
+a_ne_crlf=
+a_ne_lf=
+a_ne_cr=
+a_empty=
+a_empty_lf=
+a_empty_cr=
+a_empty_crlf=
+
+cd import
+       cat >> kw.c <<\EOF
+/* Somebody prematurely put a keyword into this file */
+/* $Id$ */
+EOF
+
+       printf "Hello\r\nWorld\r\n" > crlf
+       a_crlf=`git-hash-object -w crlf`
+       printf "Hello\rWorld\r" > cr
+       a_cr=`git-hash-object -w cr`
+       printf "Hello\nWorld\n" > lf
+       a_lf=`git-hash-object -w lf`
+
+       printf "Hello\r\nWorld" > ne_crlf
+       a_ne_crlf=`git-hash-object -w ne_crlf`
+       printf "Hello\nWorld" > ne_lf
+       a_ne_lf=`git-hash-object -w ne_lf`
+       printf "Hello\rWorld" > ne_cr
+       a_ne_cr=`git-hash-object -w ne_cr`
+
+       touch empty
+       a_empty=`git-hash-object -w empty`
+       printf "\n" > empty_lf
+       a_empty_lf=`git-hash-object -w empty_lf`
+       printf "\r" > empty_cr
+       a_empty_cr=`git-hash-object -w empty_cr`
+       printf "\r\n" > empty_crlf
+       a_empty_crlf=`git-hash-object -w empty_crlf`
+
+       svn import --no-auto-props -m 'import for git-svn' . "$svnrepo" >/dev/null
+cd ..
+
+rm -rf import
+test_expect_success 'checkout working copy from svn' "svn co $svnrepo test_wc"
+test_expect_success 'setup some commits to svn' \
+       'cd test_wc &&
+               echo Greetings >> kw.c &&
+               poke kw.c &&
+               svn commit -m "Not yet an Id" &&
+               echo Hello world >> kw.c &&
+               poke kw.c &&
+               svn commit -m "Modified file, but still not yet an Id" &&
+               svn propset svn:keywords Id kw.c &&
+               poke kw.c &&
+               svn commit -m "Propset Id" &&
+       cd ..'
+
+test_expect_success 'initialize git-svn' "git-svn init $svnrepo"
+test_expect_success 'fetch revisions from svn' 'git-svn fetch'
+
+name='test svn:keywords ignoring'
+test_expect_success "$name" \
+       'git checkout -b mybranch remotes/git-svn &&
+       echo Hi again >> kw.c &&
+       git commit -a -m "test keywords ignoring" &&
+       git-svn set-tree remotes/git-svn..mybranch &&
+       git pull . remotes/git-svn'
+
+expect='/* $Id$ */'
+got="`sed -ne 2p kw.c`"
+test_expect_success 'raw $Id$ found in kw.c' "test '$expect' = '$got'"
+
+test_expect_success "propset CR on crlf files" \
+       'cd test_wc &&
+               svn propset svn:eol-style CR empty &&
+               svn propset svn:eol-style CR crlf &&
+               svn propset svn:eol-style CR ne_crlf &&
+               svn commit -m "propset CR on crlf files" &&
+        cd ..'
+
+test_expect_success 'fetch and pull latest from svn and checkout a new wc' \
+       "git-svn fetch &&
+        git pull . remotes/git-svn &&
+        svn co $svnrepo new_wc"
+
+for i in crlf ne_crlf lf ne_lf cr ne_cr empty_cr empty_lf empty empty_crlf
+do
+       test_expect_success "Comparing $i" "cmp $i new_wc/$i"
+done
+
+
+cd test_wc
+       printf '$Id$\rHello\rWorld\r' > cr
+       printf '$Id$\rHello\rWorld' > ne_cr
+       a_cr=`printf '$Id$\r\nHello\r\nWorld\r\n' | git-hash-object --stdin`
+       a_ne_cr=`printf '$Id$\r\nHello\r\nWorld' | git-hash-object --stdin`
+       test_expect_success 'Set CRLF on cr files' \
+       'svn propset svn:eol-style CRLF cr &&
+        svn propset svn:eol-style CRLF ne_cr &&
+        svn propset svn:keywords Id cr &&
+        svn propset svn:keywords Id ne_cr &&
+        svn commit -m "propset CRLF on cr files"'
+cd ..
+test_expect_success 'fetch and pull latest from svn' \
+       'git-svn fetch && git pull . remotes/git-svn'
+
+b_cr="`git-hash-object cr`"
+b_ne_cr="`git-hash-object ne_cr`"
+
+test_expect_success 'CRLF + $Id$' "test '$a_cr' = '$b_cr'"
+test_expect_success 'CRLF + $Id$ (no newline)' "test '$a_ne_cr' = '$b_ne_cr'"
+
+cat > show-ignore.expect <<\EOF
+
+# /
+/no-such-file*
+
+# /deeply/
+/deeply/no-such-file*
+
+# /deeply/nested/
+/deeply/nested/no-such-file*
+
+# /deeply/nested/directory/
+/deeply/nested/directory/no-such-file*
+EOF
+
+test_expect_success 'test show-ignore' "
+       cd test_wc &&
+       mkdir -p deeply/nested/directory &&
+       touch deeply/nested/directory/.keep &&
+       svn add deeply &&
+       svn up &&
+       svn propset -R svn:ignore 'no-such-file*' .
+       svn commit -m 'propset svn:ignore'
+       cd .. &&
+       git-svn show-ignore > show-ignore.got &&
+       cmp show-ignore.expect show-ignore.got
+       "
+
+cat >create-ignore.expect <<\EOF
+/no-such-file*
+EOF
+
+cat >create-ignore-index.expect <<\EOF
+100644 8c52e5dfcd0a8b6b6bcfe6b41b89bcbf493718a5 0      .gitignore
+100644 8c52e5dfcd0a8b6b6bcfe6b41b89bcbf493718a5 0      deeply/.gitignore
+100644 8c52e5dfcd0a8b6b6bcfe6b41b89bcbf493718a5 0      deeply/nested/.gitignore
+100644 8c52e5dfcd0a8b6b6bcfe6b41b89bcbf493718a5 0      deeply/nested/directory/.gitignore
+EOF
+
+test_expect_success 'test create-ignore' "
+       git-svn fetch && git pull . remotes/git-svn &&
+       git-svn create-ignore &&
+       cmp ./.gitignore create-ignore.expect &&
+       cmp ./deeply/.gitignore create-ignore.expect &&
+       cmp ./deeply/nested/.gitignore create-ignore.expect &&
+       cmp ./deeply/nested/directory/.gitignore create-ignore.expect &&
+       git ls-files -s | grep gitignore | cmp - create-ignore-index.expect
+       "
+
+cat >prop.expect <<\EOF
+no-such-file*
+
+EOF
+cat >prop2.expect <<\EOF
+8
+EOF
+
+# This test can be improved: since all the svn:ignore contain the same
+# pattern, it can pass even though the propget did not execute on the
+# right directory.
+test_expect_success 'test propget' "
+       git-svn propget svn:ignore . | cmp - prop.expect &&
+       cd deeply &&
+       git-svn propget svn:ignore . | cmp - ../prop.expect &&
+       git-svn propget svn:entry:committed-rev nested/directory/.keep \
+         | cmp - ../prop2.expect &&
+       git-svn propget svn:ignore .. | cmp - ../prop.expect &&
+       git-svn propget svn:ignore nested/ | cmp - ../prop.expect &&
+       git-svn propget svn:ignore ./nested | cmp - ../prop.expect &&
+       git-svn propget svn:ignore .././deeply/nested | cmp - ../prop.expect
+       "
+
+cat >prop.expect <<\EOF
+Properties on '.':
+  svn:entry:committed-date
+  svn:entry:committed-rev
+  svn:entry:last-author
+  svn:entry:uuid
+  svn:ignore
+EOF
+cat >prop2.expect <<\EOF
+Properties on 'nested/directory/.keep':
+  svn:entry:committed-date
+  svn:entry:committed-rev
+  svn:entry:last-author
+  svn:entry:uuid
+EOF
+
+test_expect_success 'test proplist' "
+       git-svn proplist . | cmp - prop.expect &&
+       git-svn proplist nested/directory/.keep | cmp - prop2.expect
+       "
+
+test_done
diff --git a/t/t9102-git-svn-deep-rmdir.sh b/t/t9102-git-svn-deep-rmdir.sh
new file mode 100755 (executable)
index 0000000..4e08083
--- /dev/null
@@ -0,0 +1,30 @@
+#!/bin/sh
+test_description='git-svn rmdir'
+. ./lib-git-svn.sh
+
+test_expect_success 'initialize repo' "
+       mkdir import &&
+       cd import &&
+       mkdir -p deeply/nested/directory/number/1 &&
+       mkdir -p deeply/nested/directory/number/2 &&
+       echo foo > deeply/nested/directory/number/1/file &&
+       echo foo > deeply/nested/directory/number/2/another &&
+       svn import -m 'import for git-svn' . $svnrepo &&
+       cd ..
+       "
+
+test_expect_success 'mirror via git-svn' "
+       git-svn init $svnrepo &&
+       git-svn fetch &&
+       git checkout -f -b test-rmdir remotes/git-svn
+       "
+
+test_expect_success 'Try a commit on rmdir' "
+       git rm -f deeply/nested/directory/number/2/another &&
+       git commit -a -m 'remove another' &&
+       git-svn set-tree --rmdir HEAD &&
+       svn ls -R $svnrepo | grep ^deeply/nested/directory/number/1
+       "
+
+
+test_done
diff --git a/t/t9104-git-svn-follow-parent.sh b/t/t9104-git-svn-follow-parent.sh
new file mode 100755 (executable)
index 0000000..7ba7630
--- /dev/null
@@ -0,0 +1,171 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+#
+
+test_description='git-svn fetching'
+. ./lib-git-svn.sh
+
+test_expect_success 'initialize repo' "
+       mkdir import &&
+       cd import &&
+       mkdir -p trunk &&
+       echo hello > trunk/readme &&
+       svn import -m 'initial' . $svnrepo &&
+       cd .. &&
+       svn co $svnrepo wc &&
+       cd wc &&
+       echo world >> trunk/readme &&
+       poke trunk/readme &&
+       svn commit -m 'another commit' &&
+       svn up &&
+       svn mv trunk thunk &&
+       echo goodbye >> thunk/readme &&
+       poke thunk/readme &&
+       svn commit -m 'bye now' &&
+       cd ..
+       "
+
+test_expect_success 'init and fetch a moved directory' "
+       git-svn init --minimize-url -i thunk $svnrepo/thunk &&
+       git-svn fetch -i thunk &&
+       test \"\`git rev-parse --verify refs/remotes/thunk@2\`\" \
+           = \"\`git rev-parse --verify refs/remotes/thunk~1\`\" &&
+        test \"\`git cat-file blob refs/remotes/thunk:readme |\
+                 sed -n -e '3p'\`\" = goodbye &&
+       test -z \"\`git config --get svn-remote.svn.fetch \
+                '^trunk:refs/remotes/thunk@2$'\`\"
+       "
+
+test_expect_success 'init and fetch from one svn-remote' "
+        git config svn-remote.svn.url $svnrepo &&
+        git config --add svn-remote.svn.fetch \
+          trunk:refs/remotes/svn/trunk &&
+        git config --add svn-remote.svn.fetch \
+          thunk:refs/remotes/svn/thunk &&
+        git-svn fetch -i svn/thunk &&
+       test \"\`git rev-parse --verify refs/remotes/svn/trunk\`\" \
+           = \"\`git rev-parse --verify refs/remotes/svn/thunk~1\`\" &&
+        test \"\`git cat-file blob refs/remotes/svn/thunk:readme |\
+                 sed -n -e '3p'\`\" = goodbye
+        "
+
+test_expect_success 'follow deleted parent' "
+        (svn cp -m 'resurrecting trunk as junk' \
+               $svnrepo/trunk@2 $svnrepo/junk ||
+         svn cp -m 'resurrecting trunk as junk' \
+               -r2 $svnrepo/trunk $svnrepo/junk) &&
+        git config --add svn-remote.svn.fetch \
+          junk:refs/remotes/svn/junk &&
+        git-svn fetch -i svn/thunk &&
+        git-svn fetch -i svn/junk &&
+        test -z \"\`git diff svn/junk svn/trunk\`\" &&
+        test \"\`git merge-base svn/junk svn/trunk\`\" \
+           = \"\`git rev-parse svn/trunk\`\"
+        "
+
+test_expect_success 'follow larger parent' "
+        mkdir -p import/trunk/thunk/bump/thud &&
+        echo hi > import/trunk/thunk/bump/thud/file &&
+        svn import -m 'import a larger parent' import $svnrepo/larger-parent &&
+        svn cp -m 'hi' $svnrepo/larger-parent $svnrepo/another-larger &&
+        git-svn init --minimize-url -i larger \
+          $svnrepo/another-larger/trunk/thunk/bump/thud &&
+        git-svn fetch -i larger &&
+        git rev-parse --verify refs/remotes/larger &&
+        git rev-parse --verify \
+           refs/remotes/larger-parent/trunk/thunk/bump/thud &&
+        test \"\`git merge-base \
+                 refs/remotes/larger-parent/trunk/thunk/bump/thud \
+                 refs/remotes/larger\`\" = \
+             \"\`git rev-parse refs/remotes/larger\`\"
+        true
+        "
+
+test_expect_success 'follow higher-level parent' "
+        svn mkdir -m 'follow higher-level parent' $svnrepo/blob &&
+        svn co $svnrepo/blob blob &&
+        cd blob &&
+                echo hi > hi &&
+                svn add hi &&
+                svn commit -m 'hihi' &&
+                cd ..
+        svn mkdir -m 'new glob at top level' $svnrepo/glob &&
+        svn mv -m 'move blob down a level' $svnrepo/blob $svnrepo/glob/blob &&
+        git-svn init --minimize-url -i blob $svnrepo/glob/blob &&
+        git-svn fetch -i blob
+        "
+
+test_expect_success 'follow deleted directory' "
+       svn mv -m 'bye!' $svnrepo/glob/blob/hi $svnrepo/glob/blob/bye &&
+       svn rm -m 'remove glob' $svnrepo/glob &&
+       git-svn init --minimize-url -i glob $svnrepo/glob &&
+       git-svn fetch -i glob &&
+       test \"\`git cat-file blob refs/remotes/glob:blob/bye\`\" = hi &&
+       test \"\`git ls-tree refs/remotes/glob | wc -l \`\" -eq 1
+       "
+
+# ref: r9270 of the Subversion repository: (http://svn.collab.net/repos/svn)
+# in trunk/subversion/bindings/swig/perl
+test_expect_success 'follow-parent avoids deleting relevant info' "
+       mkdir -p import/trunk/subversion/bindings/swig/perl/t &&
+       for i in a b c ; do \
+         echo \$i > import/trunk/subversion/bindings/swig/perl/\$i.pm &&
+         echo _\$i > import/trunk/subversion/bindings/swig/perl/t/\$i.t; \
+       done &&
+         echo 'bad delete test' > \
+          import/trunk/subversion/bindings/swig/perl/t/larger-parent &&
+         echo 'bad delete test 2' > \
+          import/trunk/subversion/bindings/swig/perl/another-larger &&
+       cd import &&
+         svn import -m 'r9270 test' . $svnrepo/r9270 &&
+       cd .. &&
+       svn co $svnrepo/r9270/trunk/subversion/bindings/swig/perl r9270 &&
+       cd r9270 &&
+         svn mkdir native &&
+         svn mv t native/t &&
+         for i in a b c; do svn mv \$i.pm native/\$i.pm; done &&
+         echo z >> native/t/c.t &&
+         poke native/t/c.t &&
+         svn commit -m 'reorg test' &&
+       cd .. &&
+       git-svn init --minimize-url -i r9270-t \
+         $svnrepo/r9270/trunk/subversion/bindings/swig/perl/native/t &&
+       git-svn fetch -i r9270-t &&
+       test \`git rev-list r9270-t | wc -l\` -eq 2 &&
+       test \"\`git ls-tree --name-only r9270-t~1\`\" = \
+            \"\`git ls-tree --name-only r9270-t\`\"
+       "
+
+test_expect_success "track initial change if it was only made to parent" "
+       svn cp -m 'wheee!' $svnrepo/r9270/trunk $svnrepo/r9270/drunk &&
+       git-svn init --minimize-url -i r9270-d \
+         $svnrepo/r9270/drunk/subversion/bindings/swig/perl/native/t &&
+       git-svn fetch -i r9270-d &&
+       test \`git rev-list r9270-d | wc -l\` -eq 3 &&
+       test \"\`git ls-tree --name-only r9270-t\`\" = \
+            \"\`git ls-tree --name-only r9270-d\`\" &&
+       test \"\`git rev-parse r9270-t\`\" = \
+            \"\`git rev-parse r9270-d~1\`\"
+       "
+
+test_expect_success "track multi-parent paths" "
+       svn cp -m 'resurrect /glob' $svnrepo/r9270 $svnrepo/glob &&
+       git-svn multi-fetch &&
+       test \`git cat-file commit refs/remotes/glob | \
+              grep '^parent ' | wc -l\` -eq 2
+       "
+
+test_expect_success "multi-fetch continues to work" "
+       git-svn multi-fetch
+       "
+
+test_expect_success "multi-fetch works off a 'clean' repository" "
+       rm -r $GIT_DIR/svn $GIT_DIR/refs/remotes $GIT_DIR/logs &&
+       mkdir $GIT_DIR/svn &&
+       git-svn multi-fetch
+       "
+
+test_debug 'gitk --all &'
+
+test_done
diff --git a/t/t9105-git-svn-commit-diff.sh b/t/t9105-git-svn-commit-diff.sh
new file mode 100755 (executable)
index 0000000..318e172
--- /dev/null
@@ -0,0 +1,43 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+test_description='git-svn commit-diff'
+. ./lib-git-svn.sh
+
+test_expect_success 'initialize repo' "
+       mkdir import &&
+       cd import &&
+       echo hello > readme &&
+       svn import -m 'initial' . $svnrepo &&
+       cd .. &&
+       echo hello > readme &&
+       git update-index --add readme &&
+       git commit -a -m 'initial' &&
+       echo world >> readme &&
+       git commit -a -m 'another'
+       "
+
+head=`git rev-parse --verify HEAD^0`
+prev=`git rev-parse --verify HEAD^1`
+
+# the internals of the commit-diff command are the same as the regular
+# commit, so only a basic test of functionality is needed since we've
+# already tested commit extensively elsewhere
+
+test_expect_success 'test the commit-diff command' "
+       test -n '$prev' && test -n '$head' &&
+       git-svn commit-diff -r1 '$prev' '$head' '$svnrepo' &&
+       svn co $svnrepo wc &&
+       cmp readme wc/readme
+       "
+
+test_expect_success 'commit-diff to a sub-directory (with git-svn config)' "
+       svn import -m 'sub-directory' import $svnrepo/subdir &&
+       git-svn init --minimize-url $svnrepo/subdir &&
+       git-svn fetch &&
+       git-svn commit-diff -r3 '$prev' '$head' &&
+       svn cat $svnrepo/subdir/readme > readme.2 &&
+       cmp readme readme.2
+       "
+
+test_done
diff --git a/t/t9106-git-svn-commit-diff-clobber.sh b/t/t9106-git-svn-commit-diff-clobber.sh
new file mode 100755 (executable)
index 0000000..79b7968
--- /dev/null
@@ -0,0 +1,99 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+test_description='git-svn commit-diff clobber'
+. ./lib-git-svn.sh
+
+test_expect_success 'initialize repo' "
+       mkdir import &&
+       cd import &&
+       echo initial > file &&
+       svn import -m 'initial' . $svnrepo &&
+       cd .. &&
+       echo initial > file &&
+       git update-index --add file &&
+       git commit -a -m 'initial'
+       "
+test_expect_success 'commit change from svn side' "
+       svn co $svnrepo t.svn &&
+       cd t.svn &&
+       echo second line from svn >> file &&
+       poke file &&
+       svn commit -m 'second line from svn' &&
+       cd .. &&
+       rm -rf t.svn
+       "
+
+test_expect_failure 'commit conflicting change from git' "
+       echo second line from git >> file &&
+       git commit -a -m 'second line from git' &&
+       git-svn commit-diff -r1 HEAD~1 HEAD $svnrepo
+       " || true
+
+test_expect_success 'commit complementing change from git' "
+       git reset --hard HEAD~1 &&
+       echo second line from svn >> file &&
+       git commit -a -m 'second line from svn' &&
+       echo third line from git >> file &&
+       git commit -a -m 'third line from git' &&
+       git-svn commit-diff -r2 HEAD~1 HEAD $svnrepo
+       "
+
+test_expect_failure 'dcommit fails to commit because of conflict' "
+       git-svn init $svnrepo &&
+       git-svn fetch &&
+       git reset --hard refs/remotes/git-svn &&
+       svn co $svnrepo t.svn &&
+       cd t.svn &&
+       echo fourth line from svn >> file &&
+       poke file &&
+       svn commit -m 'fourth line from svn' &&
+       cd .. &&
+       rm -rf t.svn &&
+       echo 'fourth line from git' >> file &&
+       git commit -a -m 'fourth line from git' &&
+       git-svn dcommit
+       " || true
+
+test_expect_success 'dcommit does the svn equivalent of an index merge' "
+       git reset --hard refs/remotes/git-svn &&
+       echo 'index merge' > file2 &&
+       git update-index --add file2 &&
+       git commit -a -m 'index merge' &&
+       echo 'more changes' >> file2 &&
+       git update-index file2 &&
+       git commit -a -m 'more changes' &&
+       git-svn dcommit
+       "
+
+test_expect_success 'commit another change from svn side' "
+       svn co $svnrepo t.svn &&
+       cd t.svn &&
+               echo third line from svn >> file &&
+               poke file &&
+               svn commit -m 'third line from svn' &&
+       cd .. &&
+       rm -rf t.svn
+       "
+
+test_expect_failure 'multiple dcommit from git-svn will not clobber svn' "
+       git reset --hard refs/remotes/git-svn &&
+       echo new file >> new-file &&
+       git update-index --add new-file &&
+       git commit -a -m 'new file' &&
+       echo clobber > file &&
+       git commit -a -m 'clobber' &&
+       git svn dcommit
+       " || true
+
+
+test_expect_success 'check that rebase really failed' 'test -d .dotest'
+
+test_expect_success 'resolve, continue the rebase and dcommit' "
+       echo clobber and I really mean it > file &&
+       git update-index file &&
+       git rebase --continue &&
+       git svn dcommit
+       "
+
+test_done
diff --git a/t/t9106-git-svn-dcommit-clobber-series.sh b/t/t9106-git-svn-dcommit-clobber-series.sh
new file mode 100755 (executable)
index 0000000..7452546
--- /dev/null
@@ -0,0 +1,63 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Eric Wong
+test_description='git-svn dcommit clobber series'
+. ./lib-git-svn.sh
+
+test_expect_success 'initialize repo' "
+       mkdir import &&
+       cd import &&
+       awk 'BEGIN { for (i = 1; i < 64; i++) { print i } }' > file
+       svn import -m 'initial' . $svnrepo &&
+       cd .. &&
+       git svn init $svnrepo &&
+       git svn fetch &&
+       test -e file
+       "
+
+test_expect_success '(supposedly) non-conflicting change from SVN' "
+       test x\"\`sed -n -e 58p < file\`\" = x58 &&
+       test x\"\`sed -n -e 61p < file\`\" = x61 &&
+       svn co $svnrepo tmp &&
+       cd tmp &&
+               perl -i -p -e 's/^58\$/5588/' file &&
+               perl -i -p -e 's/^61\$/6611/' file &&
+               poke file &&
+               test x\"\`sed -n -e 58p < file\`\" = x5588 &&
+               test x\"\`sed -n -e 61p < file\`\" = x6611 &&
+               svn commit -m '58 => 5588, 61 => 6611' &&
+               cd ..
+       "
+
+test_expect_success 'some unrelated changes to git' "
+       echo hi > life &&
+       git update-index --add life &&
+       git commit -m hi-life &&
+       echo bye >> life &&
+       git commit -m bye-life life
+       "
+
+test_expect_success 'change file but in unrelated area' "
+       test x\"\`sed -n -e 4p < file\`\" = x4 &&
+       test x\"\`sed -n -e 7p < file\`\" = x7 &&
+       perl -i -p -e 's/^4\$/4444/' file &&
+       perl -i -p -e 's/^7\$/7777/' file &&
+       test x\"\`sed -n -e 4p < file\`\" = x4444 &&
+       test x\"\`sed -n -e 7p < file\`\" = x7777 &&
+       git commit -m '4 => 4444, 7 => 7777' file &&
+       git svn dcommit &&
+       svn up tmp &&
+       cd tmp &&
+               test x\"\`sed -n -e 4p < file\`\" = x4444 &&
+               test x\"\`sed -n -e 7p < file\`\" = x7777 &&
+               test x\"\`sed -n -e 58p < file\`\" = x5588 &&
+               test x\"\`sed -n -e 61p < file\`\" = x6611
+       "
+
+test_expect_failure 'attempt to dcommit with a dirty index' '
+       echo foo >>file &&
+       git add file &&
+       git svn dcommit
+'
+
+test_done
diff --git a/t/t9107-git-svn-migrate.sh b/t/t9107-git-svn-migrate.sh
new file mode 100755 (executable)
index 0000000..67fdf70
--- /dev/null
@@ -0,0 +1,111 @@
+#!/bin/sh
+# Copyright (c) 2006 Eric Wong
+test_description='git-svn metadata migrations from previous versions'
+. ./lib-git-svn.sh
+
+test_expect_success 'setup old-looking metadata' "
+       cp $GIT_DIR/config $GIT_DIR/config-old-git-svn &&
+       mkdir import &&
+       cd import &&
+               for i in trunk branches/a branches/b \
+                        tags/0.1 tags/0.2 tags/0.3; do
+                       mkdir -p \$i && \
+                       echo hello >> \$i/README || exit 1
+               done && \
+               svn import -m test . $svnrepo
+               cd .. &&
+       git-svn init $svnrepo &&
+       git-svn fetch &&
+       mv $GIT_DIR/svn/* $GIT_DIR/ &&
+       mv $GIT_DIR/svn/.metadata $GIT_DIR/ &&
+       rmdir $GIT_DIR/svn &&
+       git update-ref refs/heads/git-svn-HEAD refs/remotes/git-svn &&
+       git update-ref refs/heads/svn-HEAD refs/remotes/git-svn &&
+       git update-ref -d refs/remotes/git-svn refs/remotes/git-svn
+       "
+
+head=`git rev-parse --verify refs/heads/git-svn-HEAD^0`
+test_expect_success 'git-svn-HEAD is a real HEAD' "test -n '$head'"
+
+test_expect_success 'initialize old-style (v0) git-svn layout' "
+       mkdir -p $GIT_DIR/git-svn/info $GIT_DIR/svn/info &&
+       echo $svnrepo > $GIT_DIR/git-svn/info/url &&
+       echo $svnrepo > $GIT_DIR/svn/info/url &&
+       git-svn migrate &&
+       ! test -d $GIT_DIR/git-svn &&
+       git rev-parse --verify refs/remotes/git-svn^0 &&
+       git rev-parse --verify refs/remotes/svn^0 &&
+       test \`git config --get svn-remote.svn.url\` = '$svnrepo' &&
+       test \`git config --get svn-remote.svn.fetch\` = \
+             ':refs/remotes/git-svn'
+       "
+
+test_expect_success 'initialize a multi-repository repo' "
+       git-svn init $svnrepo -T trunk -t tags -b branches &&
+       git config --get-all svn-remote.svn.fetch > fetch.out &&
+       grep '^trunk:refs/remotes/trunk$' fetch.out &&
+       test -n \"\`git config --get svn-remote.svn.branches \
+                   '^branches/\*:refs/remotes/\*$'\`\" &&
+       test -n \"\`git config --get svn-remote.svn.tags \
+                   '^tags/\*:refs/remotes/tags/\*$'\`\" &&
+       git config --unset svn-remote.svn.branches \
+                               '^branches/\*:refs/remotes/\*$' &&
+       git config --unset svn-remote.svn.tags \
+                               '^tags/\*:refs/remotes/tags/\*$' &&
+       git config --add svn-remote.svn.fetch 'branches/a:refs/remotes/a' &&
+       git config --add svn-remote.svn.fetch 'branches/b:refs/remotes/b' &&
+       for i in tags/0.1 tags/0.2 tags/0.3; do
+               git config --add svn-remote.svn.fetch \
+                                \$i:refs/remotes/\$i || exit 1; done
+       "
+
+# refs should all be different, but the trees should all be the same:
+test_expect_success 'multi-fetch works on partial urls + paths' "
+       git-svn multi-fetch &&
+       for i in trunk a b tags/0.1 tags/0.2 tags/0.3; do
+               git rev-parse --verify refs/remotes/\$i^0 >> refs.out || exit 1;
+           done &&
+       test -z \"\`sort < refs.out | uniq -d\`\" &&
+       for i in trunk a b tags/0.1 tags/0.2 tags/0.3; do
+         for j in trunk a b tags/0.1 tags/0.2 tags/0.3; do
+               if test \$j != \$i; then continue; fi
+           test -z \"\`git diff refs/remotes/\$i \
+                                refs/remotes/\$j\`\" ||exit 1; done; done
+       "
+
+test_expect_success 'migrate --minimize on old inited layout' "
+       git config --unset-all svn-remote.svn.fetch &&
+       git config --unset-all svn-remote.svn.url &&
+       rm -rf $GIT_DIR/svn &&
+       for i in \`cat fetch.out\`; do
+               path=\`expr \$i : '\\([^:]*\\):.*$'\`
+               ref=\`expr \$i : '[^:]*:refs/remotes/\\(.*\\)$'\`
+               if test -z \"\$ref\"; then continue; fi
+               if test -n \"\$path\"; then path=\"/\$path\"; fi
+               ( mkdir -p $GIT_DIR/svn/\$ref/info/ &&
+               echo $svnrepo\$path > $GIT_DIR/svn/\$ref/info/url ) || exit 1;
+       done &&
+       git-svn migrate --minimize &&
+       test -z \"\`git config -l |grep -v '^svn-remote\.git-svn\.'\`\" &&
+       git config --get-all svn-remote.svn.fetch > fetch.out &&
+       grep '^trunk:refs/remotes/trunk$' fetch.out &&
+       grep '^branches/a:refs/remotes/a$' fetch.out &&
+       grep '^branches/b:refs/remotes/b$' fetch.out &&
+       grep '^tags/0\.1:refs/remotes/tags/0\.1$' fetch.out &&
+       grep '^tags/0\.2:refs/remotes/tags/0\.2$' fetch.out &&
+       grep '^tags/0\.3:refs/remotes/tags/0\.3$' fetch.out
+       grep '^:refs/remotes/git-svn' fetch.out
+       "
+
+test_expect_success  ".rev_db auto-converted to .rev_db.UUID" "
+       git-svn fetch -i trunk &&
+       expect=$GIT_DIR/svn/trunk/.rev_db.* &&
+       test -n \"\$expect\" &&
+       mv \$expect $GIT_DIR/svn/trunk/.rev_db &&
+       git-svn fetch -i trunk &&
+       test -L $GIT_DIR/svn/trunk/.rev_db &&
+       test -f \$expect &&
+       cmp \$expect $GIT_DIR/svn/trunk/.rev_db
+       "
+
+test_done
diff --git a/t/t9108-git-svn-glob.sh b/t/t9108-git-svn-glob.sh
new file mode 100755 (executable)
index 0000000..db4344c
--- /dev/null
@@ -0,0 +1,86 @@
+#!/bin/sh
+# Copyright (c) 2007 Eric Wong
+test_description='git-svn globbing refspecs'
+. ./lib-git-svn.sh
+
+cat > expect.end <<EOF
+the end
+hi
+start a new branch
+initial
+EOF
+
+test_expect_success 'test refspec globbing' "
+       mkdir -p trunk/src/a trunk/src/b trunk/doc &&
+       echo 'hello world' > trunk/src/a/readme &&
+       echo 'goodbye world' > trunk/src/b/readme &&
+       svn import -m 'initial' trunk $svnrepo/trunk &&
+       svn co $svnrepo tmp &&
+       cd tmp &&
+               mkdir branches tags &&
+               svn add branches tags &&
+               svn cp trunk branches/start &&
+               svn commit -m 'start a new branch' &&
+               svn up &&
+               echo 'hi' >> branches/start/src/b/readme &&
+               poke branches/start/src/b/readme &&
+               echo 'hey' >> branches/start/src/a/readme &&
+               poke branches/start/src/a/readme &&
+               svn commit -m 'hi' &&
+               svn up &&
+               svn cp branches/start tags/end &&
+               echo 'bye' >> tags/end/src/b/readme &&
+               poke tags/end/src/b/readme &&
+               echo 'aye' >> tags/end/src/a/readme &&
+               poke tags/end/src/a/readme &&
+               svn commit -m 'the end' &&
+               echo 'byebye' >> tags/end/src/b/readme &&
+               poke tags/end/src/b/readme &&
+               svn commit -m 'nothing to see here'
+               cd .. &&
+       git config --add svn-remote.svn.url $svnrepo &&
+       git config --add svn-remote.svn.fetch \
+                        'trunk/src/a:refs/remotes/trunk' &&
+       git config --add svn-remote.svn.branches \
+                        'branches/*/src/a:refs/remotes/branches/*' &&
+       git config --add svn-remote.svn.tags\
+                        'tags/*/src/a:refs/remotes/tags/*' &&
+       git-svn multi-fetch &&
+       git log --pretty=oneline refs/remotes/tags/end | \
+           sed -e 's/^.\{41\}//' > output.end &&
+       cmp expect.end output.end &&
+       test \"\`git rev-parse refs/remotes/tags/end~1\`\" = \
+               \"\`git rev-parse refs/remotes/branches/start\`\" &&
+       test \"\`git rev-parse refs/remotes/branches/start~2\`\" = \
+               \"\`git rev-parse refs/remotes/trunk\`\"
+       "
+
+echo try to try > expect.two
+echo nothing to see here >> expect.two
+cat expect.end >> expect.two
+
+test_expect_success 'test left-hand-side only globbing' "
+       git config --add svn-remote.two.url $svnrepo &&
+       git config --add svn-remote.two.fetch trunk:refs/remotes/two/trunk &&
+       git config --add svn-remote.two.branches \
+                        'branches/*:refs/remotes/two/branches/*' &&
+       git config --add svn-remote.two.tags \
+                        'tags/*:refs/remotes/two/tags/*' &&
+       cd tmp &&
+               echo 'try try' >> tags/end/src/b/readme &&
+               poke tags/end/src/b/readme &&
+               svn commit -m 'try to try'
+               cd .. &&
+       git-svn fetch two &&
+       test \`git rev-list refs/remotes/two/tags/end | wc -l\` -eq 6 &&
+       test \`git rev-list refs/remotes/two/branches/start | wc -l\` -eq 3 &&
+       test \`git rev-parse refs/remotes/two/branches/start~2\` = \
+            \`git rev-parse refs/remotes/two/trunk\` &&
+       test \`git rev-parse refs/remotes/two/tags/end~3\` = \
+            \`git rev-parse refs/remotes/two/branches/start\` &&
+       git log --pretty=oneline refs/remotes/two/tags/end | \
+           sed -e 's/^.\{41\}//' > output.two &&
+       cmp expect.two output.two
+       "
+
+test_done
diff --git a/t/t9110-git-svn-use-svm-props.sh b/t/t9110-git-svn-use-svm-props.sh
new file mode 100755 (executable)
index 0000000..6235af4
--- /dev/null
@@ -0,0 +1,52 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Eric Wong
+#
+
+test_description='git-svn useSvmProps test'
+
+. ./lib-git-svn.sh
+
+test_expect_success 'load svm repo' "
+       svnadmin load -q $rawsvnrepo < ../t9110/svm.dump &&
+       git-svn init --minimize-url -R arr -i bar $svnrepo/mirror/arr &&
+       git-svn init --minimize-url -R argh -i dir $svnrepo/mirror/argh &&
+       git-svn init --minimize-url -R argh -i e \
+         $svnrepo/mirror/argh/a/b/c/d/e &&
+       git config svn.useSvmProps true &&
+       git-svn fetch --all
+       "
+
+uuid=161ce429-a9dd-4828-af4a-52023f968c89
+
+bar_url=http://mayonaise/svnrepo/bar
+test_expect_success 'verify metadata for /bar' "
+       git cat-file commit refs/remotes/bar | \
+          grep '^git-svn-id: $bar_url@12 $uuid$' &&
+       git cat-file commit refs/remotes/bar~1 | \
+          grep '^git-svn-id: $bar_url@11 $uuid$' &&
+       git cat-file commit refs/remotes/bar~2 | \
+          grep '^git-svn-id: $bar_url@10 $uuid$' &&
+       git cat-file commit refs/remotes/bar~3 | \
+          grep '^git-svn-id: $bar_url@9 $uuid$' &&
+       git cat-file commit refs/remotes/bar~4 | \
+          grep '^git-svn-id: $bar_url@6 $uuid$' &&
+       git cat-file commit refs/remotes/bar~5 | \
+          grep '^git-svn-id: $bar_url@1 $uuid$'
+       "
+
+e_url=http://mayonaise/svnrepo/dir/a/b/c/d/e
+test_expect_success 'verify metadata for /dir/a/b/c/d/e' "
+       git cat-file commit refs/remotes/e | \
+          grep '^git-svn-id: $e_url@1 $uuid$'
+       "
+
+dir_url=http://mayonaise/svnrepo/dir
+test_expect_success 'verify metadata for /dir' "
+       git cat-file commit refs/remotes/dir | \
+          grep '^git-svn-id: $dir_url@2 $uuid$' &&
+       git cat-file commit refs/remotes/dir~1 | \
+          grep '^git-svn-id: $dir_url@1 $uuid$'
+       "
+
+test_done
diff --git a/t/t9110/svm.dump b/t/t9110/svm.dump
new file mode 100644 (file)
index 0000000..cc799c2
--- /dev/null
@@ -0,0 +1,511 @@
+SVN-fs-dump-format-version: 2
+
+UUID: de5973c6-545d-41da-aded-c265f9039e74
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2007-02-17T06:54:59.793104Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 200
+Content-length: 200
+
+K 7
+svn:log
+V 40
+SVM: initializing mirror for /mirror/arr
+K 10
+svn:author
+V 3
+svm
+K 11
+svm:headrev
+V 39
+161ce429-a9dd-4828-af4a-52023f968c89:0
+
+K 8
+svn:date
+V 27
+2007-02-17T06:55:00.121647Z
+PROPS-END
+
+Node-path: 
+Node-kind: dir
+Node-action: change
+Prop-content-length: 44
+Content-length: 44
+
+K 10
+svm:mirror
+V 12
+/mirror/arr
+
+PROPS-END
+
+
+Node-path: mirror
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: mirror/arr
+Node-kind: dir
+Node-action: add
+Prop-content-length: 116
+Content-length: 116
+
+K 10
+svm:source
+V 29
+http://mayonaise/svnrepo!/bar
+K 8
+svm:uuid
+V 36
+161ce429-a9dd-4828-af4a-52023f968c89
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 182
+Content-length: 182
+
+K 7
+svn:log
+V 18
+import for git-svn
+K 10
+svn:author
+V 7
+svnsync
+K 11
+svm:headrev
+V 39
+161ce429-a9dd-4828-af4a-52023f968c89:1
+
+K 8
+svn:date
+V 27
+2007-02-17T05:10:52.108847Z
+PROPS-END
+
+Node-path: mirror/arr
+Node-kind: dir
+Node-action: change
+Prop-content-length: 116
+Content-length: 116
+
+K 10
+svm:source
+V 29
+http://mayonaise/svnrepo!/bar
+K 8
+svm:uuid
+V 36
+161ce429-a9dd-4828-af4a-52023f968c89
+PROPS-END
+
+
+Node-path: mirror/arr/zzz
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 4
+Text-content-md5: 33b02bc15ce9557d2dd8484d58f95ac4
+Content-length: 14
+
+PROPS-END
+zzz
+
+
+Revision-number: 3
+Prop-content-length: 230
+Content-length: 230
+
+K 7
+svn:log
+V 66
+new symlink is added to a file that was also just made executable
+
+K 10
+svn:author
+V 7
+svnsync
+K 11
+svm:headrev
+V 39
+161ce429-a9dd-4828-af4a-52023f968c89:6
+
+K 8
+svn:date
+V 27
+2007-02-17T05:11:01.686891Z
+PROPS-END
+
+Node-path: mirror/arr/zzz
+Node-kind: file
+Node-action: change
+Prop-content-length: 36
+Text-content-length: 4
+Text-content-md5: 33b02bc15ce9557d2dd8484d58f95ac4
+Content-length: 40
+
+K 14
+svn:executable
+V 1
+*
+PROPS-END
+zzz
+
+
+Revision-number: 4
+Prop-content-length: 192
+Content-length: 192
+
+K 7
+svn:log
+V 28
+/bar/d should be in the log
+
+K 10
+svn:author
+V 7
+svnsync
+K 11
+svm:headrev
+V 39
+161ce429-a9dd-4828-af4a-52023f968c89:9
+
+K 8
+svn:date
+V 27
+2007-02-17T05:11:07.686552Z
+PROPS-END
+
+Node-path: mirror/arr/d
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 4
+Text-content-md5: 0bee89b07a248e27c83fc3d5951213c1
+Content-length: 14
+
+PROPS-END
+abc
+
+
+Revision-number: 5
+Prop-content-length: 185
+Content-length: 185
+
+K 7
+svn:log
+V 20
+add a new directory
+
+K 10
+svn:author
+V 7
+svnsync
+K 11
+svm:headrev
+V 40
+161ce429-a9dd-4828-af4a-52023f968c89:10
+
+K 8
+svn:date
+V 27
+2007-02-17T05:11:08.405953Z
+PROPS-END
+
+Node-path: mirror/arr/newdir
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: mirror/arr/newdir/dir
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 4
+Text-content-md5: 9cd599a3523898e6a12e13ec787da50a
+Content-length: 14
+
+PROPS-END
+new
+
+
+Revision-number: 6
+Prop-content-length: 196
+Content-length: 196
+
+K 7
+svn:log
+V 31
+modify a file in new directory
+
+K 10
+svn:author
+V 7
+svnsync
+K 11
+svm:headrev
+V 40
+161ce429-a9dd-4828-af4a-52023f968c89:11
+
+K 8
+svn:date
+V 27
+2007-02-17T05:11:09.126645Z
+PROPS-END
+
+Node-path: mirror/arr/newdir/dir
+Node-kind: file
+Node-action: change
+Text-content-length: 8
+Text-content-md5: a950e20332358e523a5e9d571e47fa64
+Content-length: 8
+
+new
+foo
+
+
+Revision-number: 7
+Prop-content-length: 179
+Content-length: 179
+
+K 7
+svn:log
+V 14
+update /bar/d
+
+K 10
+svn:author
+V 7
+svnsync
+K 11
+svm:headrev
+V 40
+161ce429-a9dd-4828-af4a-52023f968c89:12
+
+K 8
+svn:date
+V 27
+2007-02-17T05:11:09.846221Z
+PROPS-END
+
+Node-path: mirror/arr/d
+Node-kind: file
+Node-action: change
+Text-content-length: 4
+Text-content-md5: 7abb78de7f2756ca8b511cbc879fd5e7
+Content-length: 4
+
+cba
+
+
+Revision-number: 8
+Prop-content-length: 201
+Content-length: 201
+
+K 7
+svn:log
+V 41
+SVM: initializing mirror for /mirror/argh
+K 10
+svn:author
+V 3
+svm
+K 11
+svm:headrev
+V 39
+161ce429-a9dd-4828-af4a-52023f968c89:0
+
+K 8
+svn:date
+V 27
+2007-02-17T06:56:03.703677Z
+PROPS-END
+
+Node-path: 
+Node-kind: dir
+Node-action: change
+Prop-content-length: 57
+Content-length: 57
+
+K 10
+svm:mirror
+V 25
+/mirror/argh
+/mirror/arr
+
+PROPS-END
+
+
+Node-path: mirror/argh
+Node-kind: dir
+Node-action: add
+Prop-content-length: 116
+Content-length: 116
+
+K 10
+svm:source
+V 29
+http://mayonaise/svnrepo!/dir
+K 8
+svm:uuid
+V 36
+161ce429-a9dd-4828-af4a-52023f968c89
+PROPS-END
+
+
+Revision-number: 9
+Prop-content-length: 182
+Content-length: 182
+
+K 7
+svn:log
+V 18
+import for git-svn
+K 10
+svn:author
+V 7
+svnsync
+K 11
+svm:headrev
+V 39
+161ce429-a9dd-4828-af4a-52023f968c89:1
+
+K 8
+svn:date
+V 27
+2007-02-17T05:10:52.108847Z
+PROPS-END
+
+Node-path: mirror/argh
+Node-kind: dir
+Node-action: change
+Prop-content-length: 116
+Content-length: 116
+
+K 10
+svm:source
+V 29
+http://mayonaise/svnrepo!/dir
+K 8
+svm:uuid
+V 36
+161ce429-a9dd-4828-af4a-52023f968c89
+PROPS-END
+
+
+Node-path: mirror/argh/a
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: mirror/argh/a/b
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: mirror/argh/a/b/c
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: mirror/argh/a/b/c/d
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: mirror/argh/a/b/c/d/e
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: mirror/argh/a/b/c/d/e/file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 9
+Text-content-md5: 3fd46fe46fcdcf062c802ca60dc826d5
+Content-length: 19
+
+PROPS-END
+deep dir
+
+
+Revision-number: 10
+Prop-content-length: 197
+Content-length: 197
+
+K 7
+svn:log
+V 33
+try a deep --rmdir with a commit
+
+K 10
+svn:author
+V 7
+svnsync
+K 11
+svm:headrev
+V 39
+161ce429-a9dd-4828-af4a-52023f968c89:2
+
+K 8
+svn:date
+V 27
+2007-02-17T05:10:54.847015Z
+PROPS-END
+
+Node-path: mirror/argh/file
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 9
+Node-copyfrom-path: mirror/argh/a/b/c/d/e/file
+Text-content-length: 9
+Text-content-md5: 3fd46fe46fcdcf062c802ca60dc826d5
+Content-length: 9
+
+deep dir
+
+
+Node-path: mirror/argh/a
+Node-action: delete
+
+
diff --git a/t/t9111-git-svn-use-svnsync-props.sh b/t/t9111-git-svn-use-svnsync-props.sh
new file mode 100755 (executable)
index 0000000..ec7dedd
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Eric Wong
+#
+
+test_description='git-svn useSvnsyncProps test'
+
+. ./lib-git-svn.sh
+
+test_expect_success 'load svnsync repo' "
+       svnadmin load -q $rawsvnrepo < ../t9111/svnsync.dump &&
+       git-svn init --minimize-url -R arr -i bar $svnrepo/bar &&
+       git-svn init --minimize-url -R argh -i dir $svnrepo/dir &&
+       git-svn init --minimize-url -R argh -i e $svnrepo/dir/a/b/c/d/e &&
+       git config svn.useSvnsyncProps true &&
+       git-svn fetch --all
+       "
+
+uuid=161ce429-a9dd-4828-af4a-52023f968c89
+
+bar_url=http://mayonaise/svnrepo/bar
+test_expect_success 'verify metadata for /bar' "
+       git cat-file commit refs/remotes/bar | \
+          grep '^git-svn-id: $bar_url@12 $uuid$' &&
+       git cat-file commit refs/remotes/bar~1 | \
+          grep '^git-svn-id: $bar_url@11 $uuid$' &&
+       git cat-file commit refs/remotes/bar~2 | \
+          grep '^git-svn-id: $bar_url@10 $uuid$' &&
+       git cat-file commit refs/remotes/bar~3 | \
+          grep '^git-svn-id: $bar_url@9 $uuid$' &&
+       git cat-file commit refs/remotes/bar~4 | \
+          grep '^git-svn-id: $bar_url@6 $uuid$' &&
+       git cat-file commit refs/remotes/bar~5 | \
+          grep '^git-svn-id: $bar_url@1 $uuid$'
+       "
+
+e_url=http://mayonaise/svnrepo/dir/a/b/c/d/e
+test_expect_success 'verify metadata for /dir/a/b/c/d/e' "
+       git cat-file commit refs/remotes/e | \
+          grep '^git-svn-id: $e_url@1 $uuid$'
+       "
+
+dir_url=http://mayonaise/svnrepo/dir
+test_expect_success 'verify metadata for /dir' "
+       git cat-file commit refs/remotes/dir | \
+          grep '^git-svn-id: $dir_url@2 $uuid$' &&
+       git cat-file commit refs/remotes/dir~1 | \
+          grep '^git-svn-id: $dir_url@1 $uuid$'
+       "
+
+test_done
diff --git a/t/t9111/svnsync.dump b/t/t9111/svnsync.dump
new file mode 100644 (file)
index 0000000..499fa95
--- /dev/null
@@ -0,0 +1,560 @@
+SVN-fs-dump-format-version: 2
+
+UUID: b4bfe35e-f256-4096-874c-08c5639ecad7
+
+Revision-number: 0
+Prop-content-length: 240
+Content-length: 240
+
+K 18
+svn:sync-from-uuid
+V 36
+161ce429-a9dd-4828-af4a-52023f968c89
+K 10
+svn:author
+V 7
+svnsync
+K 24
+svn:sync-last-merged-rev
+V 2
+12
+K 8
+svn:date
+V 27
+2007-02-17T05:10:52.017552Z
+K 17
+svn:sync-from-url
+V 24
+http://mayonaise/svnrepo
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 120
+Content-length: 120
+
+K 7
+svn:log
+V 18
+import for git-svn
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:10:52.108847Z
+PROPS-END
+
+Node-path: bar
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: bar/zzz
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 4
+Text-content-md5: 33b02bc15ce9557d2dd8484d58f95ac4
+Content-length: 14
+
+PROPS-END
+zzz
+
+
+Node-path: dir
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: dir/a
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: dir/a/b
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: dir/a/b/c
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: dir/a/b/c/d
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: dir/a/b/c/d/e
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: dir/a/b/c/d/e/file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 9
+Text-content-md5: 3fd46fe46fcdcf062c802ca60dc826d5
+Content-length: 19
+
+PROPS-END
+deep dir
+
+
+Node-path: exec.sh
+Node-kind: file
+Node-action: add
+Prop-content-length: 35
+Text-content-length: 10
+Text-content-md5: 3e2b31c72181b87149ff995e7202c0e3
+Content-length: 45
+
+K 14
+svn:executable
+V 0
+
+PROPS-END
+#!/bin/sh
+
+
+Node-path: foo
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 4
+Text-content-md5: d3b07384d113edec49eaa6238ad5ff00
+Content-length: 14
+
+PROPS-END
+foo
+
+
+Node-path: foo.link
+Node-kind: file
+Node-action: add
+Prop-content-length: 33
+Text-content-length: 8
+Text-content-md5: 1043146e49ef02cab12eef865cb34ff3
+Content-length: 41
+
+K 11
+svn:special
+V 1
+*
+PROPS-END
+link foo
+
+Revision-number: 2
+Prop-content-length: 135
+Content-length: 135
+
+K 7
+svn:log
+V 33
+try a deep --rmdir with a commit
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:10:54.847015Z
+PROPS-END
+
+Node-path: dir/file
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: dir/a/b/c/d/e/file
+Text-content-length: 9
+Text-content-md5: 3fd46fe46fcdcf062c802ca60dc826d5
+Content-length: 9
+
+deep dir
+
+
+Node-path: dir/a
+Node-action: delete
+
+
+Node-path: file
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: dir/a/b/c/d/e/file
+Text-content-length: 9
+Text-content-md5: 3fd46fe46fcdcf062c802ca60dc826d5
+Content-length: 9
+
+deep dir
+
+
+Revision-number: 3
+Prop-content-length: 136
+Content-length: 136
+
+K 7
+svn:log
+V 34
+remove executable bit from a file
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:10:58.232691Z
+PROPS-END
+
+Node-path: exec.sh
+Node-kind: file
+Node-action: change
+Prop-content-length: 10
+Text-content-length: 10
+Text-content-md5: 3e2b31c72181b87149ff995e7202c0e3
+Content-length: 20
+
+PROPS-END
+#!/bin/sh
+
+
+Revision-number: 4
+Prop-content-length: 131
+Content-length: 131
+
+K 7
+svn:log
+V 29
+add executable bit back file
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:10:59.666560Z
+PROPS-END
+
+Node-path: exec.sh
+Node-kind: file
+Node-action: change
+Prop-content-length: 36
+Text-content-length: 10
+Text-content-md5: 3e2b31c72181b87149ff995e7202c0e3
+Content-length: 46
+
+K 14
+svn:executable
+V 1
+*
+PROPS-END
+#!/bin/sh
+
+
+Revision-number: 5
+Prop-content-length: 154
+Content-length: 154
+
+K 7
+svn:log
+V 52
+executable file becomes a symlink to bar/zzz (file)
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:11:00.676495Z
+PROPS-END
+
+Node-path: exec.sh
+Node-kind: file
+Node-action: change
+Prop-content-length: 33
+Text-content-length: 12
+Text-content-md5: f138693371665cc117742508761d684d
+Content-length: 45
+
+K 11
+svn:special
+V 1
+*
+PROPS-END
+link bar/zzz
+
+Revision-number: 6
+Prop-content-length: 168
+Content-length: 168
+
+K 7
+svn:log
+V 66
+new symlink is added to a file that was also just made executable
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:11:01.686891Z
+PROPS-END
+
+Node-path: bar/zzz
+Node-kind: file
+Node-action: change
+Prop-content-length: 36
+Text-content-length: 4
+Text-content-md5: 33b02bc15ce9557d2dd8484d58f95ac4
+Content-length: 40
+
+K 14
+svn:executable
+V 1
+*
+PROPS-END
+zzz
+
+
+Node-path: exec-2.sh
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 5
+Node-copyfrom-path: exec.sh
+Text-content-length: 12
+Text-content-md5: f138693371665cc117742508761d684d
+Content-length: 12
+
+link bar/zzz
+
+Revision-number: 7
+Prop-content-length: 136
+Content-length: 136
+
+K 7
+svn:log
+V 34
+modify a symlink to become a file
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:11:02.677035Z
+PROPS-END
+
+Node-path: exec-2.sh
+Node-kind: file
+Node-action: change
+Prop-content-length: 10
+Text-content-length: 9
+Text-content-md5: 8e92eff9e911886cede27d420f89c735
+Content-length: 19
+
+PROPS-END
+git help
+
+
+Revision-number: 8
+Prop-content-length: 109
+Content-length: 109
+
+K 7
+svn:log
+V 8
+éï∏
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:11:03.676862Z
+PROPS-END
+
+Node-path: exec-2.sh
+Node-kind: file
+Node-action: change
+Text-content-length: 17
+Text-content-md5: 49881954063cf26ca48c212396a957ca
+Content-length: 17
+
+git help
+# hello
+
+
+Revision-number: 9
+Prop-content-length: 130
+Content-length: 130
+
+K 7
+svn:log
+V 28
+/bar/d should be in the log
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:11:07.686552Z
+PROPS-END
+
+Node-path: bar/d
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 4
+Text-content-md5: 0bee89b07a248e27c83fc3d5951213c1
+Content-length: 14
+
+PROPS-END
+abc
+
+
+Revision-number: 10
+Prop-content-length: 122
+Content-length: 122
+
+K 7
+svn:log
+V 20
+add a new directory
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:11:08.405953Z
+PROPS-END
+
+Node-path: bar/newdir
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: bar/newdir/dir
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 4
+Text-content-md5: 9cd599a3523898e6a12e13ec787da50a
+Content-length: 14
+
+PROPS-END
+new
+
+
+Revision-number: 11
+Prop-content-length: 133
+Content-length: 133
+
+K 7
+svn:log
+V 31
+modify a file in new directory
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:11:09.126645Z
+PROPS-END
+
+Node-path: bar/newdir/dir
+Node-kind: file
+Node-action: change
+Text-content-length: 8
+Text-content-md5: a950e20332358e523a5e9d571e47fa64
+Content-length: 8
+
+new
+foo
+
+
+Revision-number: 12
+Prop-content-length: 116
+Content-length: 116
+
+K 7
+svn:log
+V 14
+update /bar/d
+
+K 10
+svn:author
+V 7
+svnsync
+K 8
+svn:date
+V 27
+2007-02-17T05:11:09.846221Z
+PROPS-END
+
+Node-path: bar/d
+Node-kind: file
+Node-action: change
+Text-content-length: 4
+Text-content-md5: 7abb78de7f2756ca8b511cbc879fd5e7
+Content-length: 4
+
+cba
diff --git a/t/t9112-git-svn-md5less-file.sh b/t/t9112-git-svn-md5less-file.sh
new file mode 100755 (executable)
index 0000000..08313bb
--- /dev/null
@@ -0,0 +1,45 @@
+test_description='test that git handles an svn repository with missing md5sums'
+
+. ./lib-git-svn.sh
+
+# Loading a node from a svn dumpfile without a Text-Content-Length
+# field causes svn to neglect to store or report an md5sum.  (it will
+# calculate one if you had put Text-Content-Length: 0).  This showed
+# up in a repository creted with cvs2svn.
+
+cat > dumpfile.svn <<EOF
+SVN-fs-dump-format-version: 1
+
+Revision-number: 1
+Prop-content-length: 98
+Content-length: 98
+
+K 7
+svn:log
+V 0
+
+K 10
+svn:author
+V 4
+test
+K 8
+svn:date
+V 27
+2007-05-06T12:37:01.153339Z
+PROPS-END
+
+Node-path: md5less-file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+EOF
+
+test_expect_success 'load svn dumpfile' "svnadmin load $rawsvnrepo < dumpfile.svn"
+
+test_expect_success 'initialize git-svn' "git-svn init $svnrepo"
+test_expect_success 'fetch revisions from svn' 'git-svn fetch'
+test_done
diff --git a/t/t9113-git-svn-dcommit-new-file.sh b/t/t9113-git-svn-dcommit-new-file.sh
new file mode 100755 (executable)
index 0000000..9ef0db9
--- /dev/null
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Eric Wong
+#
+
+# Don't run this test by default unless the user really wants it
+# I don't like the idea of taking a port and possibly leaving a
+# daemon running on a users system if the test fails.
+# Not all git users will need to interact with SVN.
+test -z "$SVNSERVE_PORT" && exit 0
+
+test_description='git-svn dcommit new files over svn:// test'
+
+. ./lib-git-svn.sh
+
+start_svnserve () {
+       svnserve --listen-port $SVNSERVE_PORT \
+                --root $rawsvnrepo \
+                --listen-once \
+                --listen-host 127.0.0.1 &
+}
+
+test_expect_success 'start tracking an empty repo' "
+       svn mkdir -m 'empty dir' $svnrepo/empty-dir &&
+       echo anon-access = write >> $rawsvnrepo/conf/svnserve.conf &&
+       start_svnserve &&
+       git svn init svn://127.0.0.1:$SVNSERVE_PORT &&
+       git svn fetch
+       "
+
+test_expect_success 'create files in new directory with dcommit' "
+       mkdir git-new-dir &&
+       echo hello > git-new-dir/world &&
+       git update-index --add git-new-dir/world &&
+       git commit -m hello &&
+       start_svnserve &&
+       git svn dcommit
+       "
+
+test_done
diff --git a/t/t9114-git-svn-dcommit-merge.sh b/t/t9114-git-svn-dcommit-merge.sh
new file mode 100755 (executable)
index 0000000..225060b
--- /dev/null
@@ -0,0 +1,94 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Eric Wong
+# Based on a script by Joakim Tjernlund <joakim.tjernlund@transmode.se>
+
+test_description='git-svn dcommit handles merges'
+
+. ./lib-git-svn.sh
+
+big_text_block () {
+cat << EOF
+#
+# (C) Copyright 2000 - 2005
+# Wolfgang Denk, DENX Software Engineering, wd@denx.de.
+#
+# See file CREDITS for list of people who contributed to this
+# project.
+#
+# 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
+#
+EOF
+}
+
+test_expect_success 'setup svn repository' "
+       svn co $svnrepo mysvnwork &&
+       mkdir -p mysvnwork/trunk &&
+       cd mysvnwork &&
+               big_text_block >> trunk/README &&
+               svn add trunk &&
+               svn ci -m 'first commit' trunk &&
+               cd ..
+       "
+
+test_expect_success 'setup git mirror and merge' "
+       git svn init $svnrepo -t tags -T trunk -b branches &&
+       git svn fetch &&
+       git checkout --track -b svn remotes/trunk &&
+       git checkout -b merge &&
+       echo new file > new_file &&
+       git add new_file &&
+       git commit -a -m 'New file' &&
+       echo hello >> README &&
+       git commit -a -m 'hello' &&
+       echo add some stuff >> new_file &&
+       git commit -a -m 'add some stuff' &&
+       git checkout svn &&
+       mv -f README tmp &&
+       echo friend > README &&
+       cat tmp >> README &&
+       git commit -a -m 'friend' &&
+       git pull . merge
+       "
+
+test_debug 'gitk --all & sleep 1'
+
+test_expect_success 'verify pre-merge ancestry' "
+       test x\`git rev-parse --verify refs/heads/svn^2\` = \
+            x\`git rev-parse --verify refs/heads/merge\` &&
+       git cat-file commit refs/heads/svn^ | grep '^friend$'
+       "
+
+test_expect_success 'git svn dcommit merges' "
+       git svn dcommit
+       "
+
+test_debug 'gitk --all & sleep 1'
+
+test_expect_success 'verify post-merge ancestry' "
+       test x\`git rev-parse --verify refs/heads/svn\` = \
+            x\`git rev-parse --verify refs/remotes/trunk \` &&
+       test x\`git rev-parse --verify refs/heads/svn^2\` = \
+            x\`git rev-parse --verify refs/heads/merge\` &&
+       git cat-file commit refs/heads/svn^ | grep '^friend$'
+       "
+
+test_expect_success 'verify merge commit message' "
+       git rev-list --pretty=raw -1 refs/heads/svn | \
+         grep \"    Merge branch 'merge' into svn\"
+       "
+
+test_done
diff --git a/t/t9115-git-svn-dcommit-funky-renames.sh b/t/t9115-git-svn-dcommit-funky-renames.sh
new file mode 100755 (executable)
index 0000000..182299c
--- /dev/null
@@ -0,0 +1,54 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Eric Wong
+
+
+test_description='git-svn dcommit can commit renames of files with ugly names'
+
+. ./lib-git-svn.sh
+
+test_expect_success 'load repository with strange names' "
+       svnadmin load -q $rawsvnrepo < ../t9115/funky-names.dump &&
+       start_httpd
+       "
+
+test_expect_success 'init and fetch repository' "
+       git svn init $svnrepo &&
+       git svn fetch &&
+       git reset --hard git-svn
+       "
+
+test_expect_success 'create file in existing ugly and empty dir' '
+       mkdir "#{bad_directory_name}" &&
+       echo hi > "#{bad_directory_name}/ foo" &&
+       git update-index --add "#{bad_directory_name}/ foo" &&
+       git commit -m "new file in ugly parent" &&
+       git svn dcommit
+       '
+
+test_expect_success 'rename ugly file' '
+       git mv "#{bad_directory_name}/ foo" "file name with feces" &&
+       git commit -m "rename ugly file" &&
+       git svn dcommit
+       '
+
+test_expect_success 'rename pretty file' '
+       echo :x > pretty &&
+       git update-index --add pretty &&
+       git commit -m "pretty :x" &&
+       git svn dcommit &&
+       mkdir regular_dir_name &&
+       git mv pretty regular_dir_name/pretty &&
+       git commit -m "moved pretty file" &&
+       git svn dcommit
+       '
+
+test_expect_success 'rename pretty file into ugly one' '
+       git mv regular_dir_name/pretty "#{bad_directory_name}/ booboo" &&
+       git commit -m booboo &&
+       git svn dcommit
+       '
+
+stop_httpd
+
+test_done
diff --git a/t/t9115/funky-names.dump b/t/t9115/funky-names.dump
new file mode 100644 (file)
index 0000000..42422f7
--- /dev/null
@@ -0,0 +1,103 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 819c44fe-2bcc-4066-88e4-985e2bc0b418
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2007-07-12T07:54:26.062914Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 152
+Content-length: 152
+
+K 7
+svn:log
+V 44
+what will those wacky people think of next?
+
+K 10
+svn:author
+V 12
+normalperson
+K 8
+svn:date
+V 27
+2007-07-12T08:00:05.011573Z
+PROPS-END
+
+Node-path:  leading space
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path:  leading space file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 5
+Text-content-md5: e4fa20c67542cdc21271e08d329397ab
+Content-length: 15
+
+PROPS-END
+ugly
+
+
+Node-path: #{bad_directory_name}
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: #{cool_name}
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 18
+Text-content-md5: 87dac40ca337dfa3dcc8911388c3ddda
+Content-length: 28
+
+PROPS-END
+strange name here
+
+
+Node-path: dir name with spaces
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: file name with spaces
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 7
+Text-content-md5: c1f10cfd640618484a2a475c11410fd3
+Content-length: 17
+
+PROPS-END
+spaces
+
+
+Node-path: regular_dir_name
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
diff --git a/t/t9116-git-svn-log.sh b/t/t9116-git-svn-log.sh
new file mode 100755 (executable)
index 0000000..902ed41
--- /dev/null
@@ -0,0 +1,128 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Eric Wong
+#
+
+test_description='git-svn log tests'
+. ./lib-git-svn.sh
+
+test_expect_success 'setup repository and import' "
+       mkdir import &&
+       cd import &&
+               for i in trunk branches/a branches/b \
+                        tags/0.1 tags/0.2 tags/0.3; do
+                       mkdir -p \$i && \
+                       echo hello >> \$i/README || exit 1
+               done && \
+               svn import -m test . $svnrepo
+               cd .. &&
+       git-svn init $svnrepo -T trunk -b branches -t tags &&
+       git-svn fetch &&
+       git reset --hard trunk &&
+       echo bye >> README &&
+       git commit -a -m bye &&
+       git svn dcommit &&
+       git reset --hard a &&
+       echo why >> FEEDME &&
+       git update-index --add FEEDME &&
+       git commit -m feedme &&
+       git svn dcommit &&
+       git reset --hard trunk &&
+       echo aye >> README &&
+       git commit -a -m aye &&
+       git svn dcommit &&
+       git reset --hard b &&
+       echo spy >> README &&
+       git commit -a -m spy &&
+       echo try >> README &&
+       git commit -a -m try &&
+       git svn dcommit
+       "
+
+test_expect_success 'run log' "
+       git reset --hard a &&
+       git svn log -r2 trunk | grep ^r2 &&
+       git svn log -r4 trunk | grep ^r4 &&
+       git svn log -r3 | grep ^r3
+       "
+
+test_expect_success 'run log against a from trunk' "
+       git reset --hard trunk &&
+       git svn log -r3 a | grep ^r3
+       "
+
+printf 'r1 \nr2 \nr4 \n' > expected-range-r1-r2-r4
+
+test_expect_success 'test ascending revision range' "
+       git reset --hard trunk &&
+       git svn log -r 1:4 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r1-r2-r4 -
+       "
+
+printf 'r4 \nr2 \nr1 \n' > expected-range-r4-r2-r1
+
+test_expect_success 'test descending revision range' "
+       git reset --hard trunk &&
+       git svn log -r 4:1 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r4-r2-r1 -
+       "
+
+printf 'r1 \nr2 \n' > expected-range-r1-r2
+
+test_expect_success 'test ascending revision range with unreachable revision' "
+       git reset --hard trunk &&
+       git svn log -r 1:3 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r1-r2 -
+       "
+
+printf 'r2 \nr1 \n' > expected-range-r2-r1
+
+test_expect_success 'test descending revision range with unreachable revision' "
+       git reset --hard trunk &&
+       git svn log -r 3:1 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r2-r1 -
+       "
+
+printf 'r2 \n' > expected-range-r2
+
+test_expect_success 'test ascending revision range with unreachable upper boundary revision and 1 commit' "
+       git reset --hard trunk &&
+       git svn log -r 2:3 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r2 -
+       "
+
+test_expect_success 'test descending revision range with unreachable upper boundary revision and 1 commit' "
+       git reset --hard trunk &&
+       git svn log -r 3:2 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r2 -
+       "
+
+printf 'r4 \n' > expected-range-r4
+
+test_expect_success 'test ascending revision range with unreachable lower boundary revision and 1 commit' "
+       git reset --hard trunk &&
+       git svn log -r 3:4 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r4 -
+       "
+
+test_expect_success 'test descending revision range with unreachable lower boundary revision and 1 commit' "
+       git reset --hard trunk &&
+       git svn log -r 4:3 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r4 -
+       "
+
+printf -- '------------------------------------------------------------------------\n' > expected-separator
+
+test_expect_success 'test ascending revision range with unreachable boundary revisions and no commits' "
+       git reset --hard trunk &&
+       git svn log -r 5:6 | diff -u expected-separator -
+       "
+
+test_expect_success 'test descending revision range with unreachable boundary revisions and no commits' "
+       git reset --hard trunk &&
+       git svn log -r 6:5 | diff -u expected-separator -
+       "
+
+test_expect_success 'test ascending revision range with unreachable boundary revisions and 1 commit' "
+       git reset --hard trunk &&
+       git svn log -r 3:5 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r4 -
+       "
+
+test_expect_success 'test descending revision range with unreachable boundary revisions and 1 commit' "
+       git reset --hard trunk &&
+       git svn log -r 5:3 | grep '^r[0-9]' | cut -d'|' -f1 | diff -u expected-range-r4 -
+       "
+
+test_done
diff --git a/t/t9117-git-svn-init-clone.sh b/t/t9117-git-svn-init-clone.sh
new file mode 100755 (executable)
index 0000000..d482b40
--- /dev/null
@@ -0,0 +1,55 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Eric Wong
+#
+
+test_description='git-svn init/clone tests'
+
+. ./lib-git-svn.sh
+
+# setup, run inside tmp so we don't have any conflicts with $svnrepo
+set -e
+rm -r .git
+mkdir tmp
+cd tmp
+
+test_expect_success 'setup svnrepo' "
+       mkdir project project/trunk project/branches project/tags &&
+       echo foo > project/trunk/foo &&
+       svn import -m '$test_description' project $svnrepo/project &&
+       rm -rf project
+       "
+
+test_expect_success 'basic clone' "
+       test ! -d trunk &&
+       git svn clone $svnrepo/project/trunk &&
+       test -d trunk/.git/svn &&
+       test -e trunk/foo &&
+       rm -rf trunk
+       "
+
+test_expect_success 'clone to target directory' "
+       test ! -d target &&
+       git svn clone $svnrepo/project/trunk target &&
+       test -d target/.git/svn &&
+       test -e target/foo &&
+       rm -rf target
+       "
+
+test_expect_success 'clone with --stdlayout' "
+       test ! -d project &&
+       git svn clone -s $svnrepo/project &&
+       test -d project/.git/svn &&
+       test -e project/foo &&
+       rm -rf project
+       "
+
+test_expect_success 'clone to target directory with --stdlayout' "
+       test ! -d target &&
+       git svn clone -s $svnrepo/project target &&
+       test -d target/.git/svn &&
+       test -e target/foo &&
+       rm -rf target
+       "
+
+test_done
diff --git a/t/t9118-git-svn-funky-branch-names.sh b/t/t9118-git-svn-funky-branch-names.sh
new file mode 100755 (executable)
index 0000000..640bb06
--- /dev/null
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Eric Wong
+#
+
+test_description='git-svn funky branch names'
+. ./lib-git-svn.sh
+
+test_expect_success 'setup svnrepo' "
+       mkdir project project/trunk project/branches project/tags &&
+       echo foo > project/trunk/foo &&
+       svn import -m '$test_description' project \"$svnrepo/pr ject\" &&
+       rm -rf project &&
+       svn cp -m 'fun' \"$svnrepo/pr ject/trunk\" \
+                       \"$svnrepo/pr ject/branches/fun plugin\" &&
+       svn cp -m 'more fun!' \"$svnrepo/pr ject/branches/fun plugin\" \
+                             \"$svnrepo/pr ject/branches/more fun plugin!\" &&
+       start_httpd
+       "
+
+test_expect_success 'test clone with funky branch names' "
+       git svn clone -s \"$svnrepo/pr ject\" project &&
+       cd project &&
+               git rev-parse 'refs/remotes/fun%20plugin' &&
+               git rev-parse 'refs/remotes/more%20fun%20plugin!' &&
+       cd ..
+       "
+
+test_expect_success 'test dcommit to funky branch' "
+       cd project &&
+       git reset --hard 'refs/remotes/more%20fun%20plugin!' &&
+       echo hello >> foo &&
+       git commit -m 'hello' -- foo &&
+       git svn dcommit &&
+       cd ..
+       "
+
+stop_httpd
+
+test_done
diff --git a/t/t9119-git-svn-info.sh b/t/t9119-git-svn-info.sh
new file mode 100755 (executable)
index 0000000..439bd93
--- /dev/null
@@ -0,0 +1,368 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 David D. Kilzer
+
+test_description='git-svn info'
+
+. ./lib-git-svn.sh
+
+ptouch() {
+       perl -w -e '
+               use strict;
+               die "ptouch requires exactly 2 arguments" if @ARGV != 2;
+               die "$ARGV[0] does not exist" if ! -e $ARGV[0];
+               my @s = stat $ARGV[0];
+               utime $s[8], $s[9], $ARGV[1];
+       ' "$1" "$2"
+}
+
+test_expect_success 'setup repository and import' "
+       mkdir info &&
+       cd info &&
+               echo FIRST > A &&
+               echo one > file &&
+               ln -s file symlink-file &&
+               mkdir directory &&
+               touch directory/.placeholder &&
+               ln -s directory symlink-directory &&
+               svn import -m 'initial' . $svnrepo &&
+       cd .. &&
+       mkdir gitwc &&
+       cd gitwc &&
+               git-svn init $svnrepo &&
+               git-svn fetch &&
+       cd .. &&
+       svn co $svnrepo svnwc &&
+       ptouch svnwc/file gitwc/file &&
+       ptouch svnwc/directory gitwc/directory &&
+       ptouch svnwc/symlink-file gitwc/symlink-file &&
+       ptouch svnwc/symlink-directory gitwc/symlink-directory
+       "
+
+test_expect_success 'info' "
+       (cd svnwc; svn info) > expected.info &&
+       (cd gitwc; git-svn info) > actual.info &&
+       git-diff expected.info actual.info
+       "
+
+test_expect_success 'info --url' '
+       test $(cd gitwc; git-svn info --url) = $svnrepo
+       '
+
+test_expect_success 'info .' "
+       (cd svnwc; svn info .) > expected.info-dot &&
+       (cd gitwc; git-svn info .) > actual.info-dot &&
+       git-diff expected.info-dot actual.info-dot
+       "
+
+test_expect_success 'info --url .' '
+       test $(cd gitwc; git-svn info --url .) = $svnrepo
+       '
+
+test_expect_success 'info file' "
+       (cd svnwc; svn info file) > expected.info-file &&
+       (cd gitwc; git-svn info file) > actual.info-file &&
+       git-diff expected.info-file actual.info-file
+       "
+
+test_expect_success 'info --url file' '
+       test $(cd gitwc; git-svn info --url file) = "$svnrepo/file"
+       '
+
+test_expect_success 'info directory' "
+       (cd svnwc; svn info directory) > expected.info-directory &&
+       (cd gitwc; git-svn info directory) > actual.info-directory &&
+       git-diff expected.info-directory actual.info-directory
+       "
+
+test_expect_success 'info --url directory' '
+       test $(cd gitwc; git-svn info --url directory) = "$svnrepo/directory"
+       '
+
+test_expect_success 'info symlink-file' "
+       (cd svnwc; svn info symlink-file) > expected.info-symlink-file &&
+       (cd gitwc; git-svn info symlink-file) > actual.info-symlink-file &&
+       git-diff expected.info-symlink-file actual.info-symlink-file
+       "
+
+test_expect_success 'info --url symlink-file' '
+       test $(cd gitwc; git-svn info --url symlink-file) \
+            = "$svnrepo/symlink-file"
+       '
+
+test_expect_success 'info symlink-directory' "
+       (cd svnwc; svn info symlink-directory) \
+               > expected.info-symlink-directory &&
+       (cd gitwc; git-svn info symlink-directory) \
+               > actual.info-symlink-directory &&
+       git-diff expected.info-symlink-directory actual.info-symlink-directory
+       "
+
+test_expect_success 'info --url symlink-directory' '
+       test $(cd gitwc; git-svn info --url symlink-directory) \
+            = "$svnrepo/symlink-directory"
+       '
+
+test_expect_success 'info added-file' "
+       echo two > gitwc/added-file &&
+       cd gitwc &&
+               git add added-file &&
+       cd .. &&
+       cp gitwc/added-file svnwc/added-file &&
+       ptouch gitwc/added-file svnwc/added-file &&
+       cd svnwc &&
+               svn add added-file > /dev/null &&
+       cd .. &&
+       (cd svnwc; svn info added-file) > expected.info-added-file &&
+       (cd gitwc; git-svn info added-file) > actual.info-added-file &&
+       git-diff expected.info-added-file actual.info-added-file
+       "
+
+test_expect_success 'info --url added-file' '
+       test $(cd gitwc; git-svn info --url added-file) \
+            = "$svnrepo/added-file"
+       '
+
+test_expect_success 'info added-directory' "
+       mkdir gitwc/added-directory svnwc/added-directory &&
+       ptouch gitwc/added-directory svnwc/added-directory &&
+       touch gitwc/added-directory/.placeholder &&
+       cd svnwc &&
+               svn add added-directory > /dev/null &&
+       cd .. &&
+       cd gitwc &&
+               git add added-directory &&
+       cd .. &&
+       (cd svnwc; svn info added-directory) \
+               > expected.info-added-directory &&
+       (cd gitwc; git-svn info added-directory) \
+               > actual.info-added-directory &&
+       git-diff expected.info-added-directory actual.info-added-directory
+       "
+
+test_expect_success 'info --url added-directory' '
+       test $(cd gitwc; git-svn info --url added-directory) \
+            = "$svnrepo/added-directory"
+       '
+
+test_expect_success 'info added-symlink-file' "
+       cd gitwc &&
+               ln -s added-file added-symlink-file &&
+               git add added-symlink-file &&
+       cd .. &&
+       cd svnwc &&
+               ln -s added-file added-symlink-file &&
+               svn add added-symlink-file > /dev/null &&
+       cd .. &&
+       ptouch gitwc/added-symlink-file svnwc/added-symlink-file &&
+       (cd svnwc; svn info added-symlink-file) \
+               > expected.info-added-symlink-file &&
+       (cd gitwc; git-svn info added-symlink-file) \
+               > actual.info-added-symlink-file &&
+       git-diff expected.info-added-symlink-file \
+                actual.info-added-symlink-file
+       "
+
+test_expect_success 'info --url added-symlink-file' '
+       test $(cd gitwc; git-svn info --url added-symlink-file) \
+            = "$svnrepo/added-symlink-file"
+       '
+
+test_expect_success 'info added-symlink-directory' "
+       cd gitwc &&
+               ln -s added-directory added-symlink-directory &&
+               git add added-symlink-directory &&
+       cd .. &&
+       cd svnwc &&
+               ln -s added-directory added-symlink-directory &&
+               svn add added-symlink-directory > /dev/null &&
+       cd .. &&
+       ptouch gitwc/added-symlink-directory svnwc/added-symlink-directory &&
+       (cd svnwc; svn info added-symlink-directory) \
+               > expected.info-added-symlink-directory &&
+       (cd gitwc; git-svn info added-symlink-directory) \
+               > actual.info-added-symlink-directory &&
+       git-diff expected.info-added-symlink-directory \
+                actual.info-added-symlink-directory
+       "
+
+test_expect_success 'info --url added-symlink-directory' '
+       test $(cd gitwc; git-svn info --url added-symlink-directory) \
+            = "$svnrepo/added-symlink-directory"
+       '
+
+# The next few tests replace the "Text Last Updated" value with a
+# placeholder since git doesn't have a way to know the date that a
+# now-deleted file was last checked out locally.  Internally it
+# simply reuses the Last Changed Date.
+
+test_expect_success 'info deleted-file' "
+       cd gitwc &&
+               git rm -f file > /dev/null &&
+       cd .. &&
+       cd svnwc &&
+               svn rm --force file > /dev/null &&
+       cd .. &&
+       (cd svnwc; svn info file) |
+       sed -e 's/^\(Text Last Updated:\).*/\1 TEXT-LAST-UPDATED-STRING/' \
+               > expected.info-deleted-file &&
+       (cd gitwc; git-svn info file) |
+       sed -e 's/^\(Text Last Updated:\).*/\1 TEXT-LAST-UPDATED-STRING/' \
+               > actual.info-deleted-file &&
+       git-diff expected.info-deleted-file actual.info-deleted-file
+       "
+
+test_expect_success 'info --url file (deleted)' '
+       test $(cd gitwc; git-svn info --url file) \
+            = "$svnrepo/file"
+       '
+
+test_expect_success 'info deleted-directory' "
+       cd gitwc &&
+               git rm -r -f directory > /dev/null &&
+       cd .. &&
+       cd svnwc &&
+               svn rm --force directory > /dev/null &&
+       cd .. &&
+       (cd svnwc; svn info directory) |
+       sed -e 's/^\(Text Last Updated:\).*/\1 TEXT-LAST-UPDATED-STRING/' \
+               > expected.info-deleted-directory &&
+       (cd gitwc; git-svn info directory) |
+       sed -e 's/^\(Text Last Updated:\).*/\1 TEXT-LAST-UPDATED-STRING/' \
+               > actual.info-deleted-directory &&
+       git-diff expected.info-deleted-directory actual.info-deleted-directory
+       "
+
+test_expect_success 'info --url directory (deleted)' '
+       test $(cd gitwc; git-svn info --url directory) \
+            = "$svnrepo/directory"
+       '
+
+test_expect_success 'info deleted-symlink-file' "
+       cd gitwc &&
+               git rm -f symlink-file > /dev/null &&
+       cd .. &&
+       cd svnwc &&
+               svn rm --force symlink-file > /dev/null &&
+       cd .. &&
+       (cd svnwc; svn info symlink-file) |
+       sed -e 's/^\(Text Last Updated:\).*/\1 TEXT-LAST-UPDATED-STRING/' \
+               > expected.info-deleted-symlink-file &&
+       (cd gitwc; git-svn info symlink-file) |
+       sed -e 's/^\(Text Last Updated:\).*/\1 TEXT-LAST-UPDATED-STRING/' \
+               > actual.info-deleted-symlink-file &&
+       git-diff expected.info-deleted-symlink-file \
+                actual.info-deleted-symlink-file
+       "
+
+test_expect_success 'info --url symlink-file (deleted)' '
+       test $(cd gitwc; git-svn info --url symlink-file) \
+            = "$svnrepo/symlink-file"
+       '
+
+test_expect_success 'info deleted-symlink-directory' "
+       cd gitwc &&
+               git rm -f symlink-directory > /dev/null &&
+       cd .. &&
+       cd svnwc &&
+               svn rm --force symlink-directory > /dev/null &&
+       cd .. &&
+       (cd svnwc; svn info symlink-directory) |
+       sed -e 's/^\(Text Last Updated:\).*/\1 TEXT-LAST-UPDATED-STRING/' \
+                > expected.info-deleted-symlink-directory &&
+       (cd gitwc; git-svn info symlink-directory) |
+       sed -e 's/^\(Text Last Updated:\).*/\1 TEXT-LAST-UPDATED-STRING/' \
+                > actual.info-deleted-symlink-directory &&
+       git-diff expected.info-deleted-symlink-directory \
+                actual.info-deleted-symlink-directory
+       "
+
+test_expect_success 'info --url symlink-directory (deleted)' '
+       test $(cd gitwc; git-svn info --url symlink-directory) \
+            = "$svnrepo/symlink-directory"
+       '
+
+# NOTE: git does not have the concept of replaced objects,
+# so we can't test for files in that state.
+
+test_expect_success 'info unknown-file' "
+       echo two > gitwc/unknown-file &&
+       cp gitwc/unknown-file svnwc/unknown-file &&
+       ptouch gitwc/unknown-file svnwc/unknown-file &&
+       (cd svnwc; svn info unknown-file) 2> expected.info-unknown-file &&
+       (cd gitwc; git-svn info unknown-file) 2> actual.info-unknown-file &&
+       git-diff expected.info-unknown-file actual.info-unknown-file
+       "
+
+test_expect_success 'info --url unknown-file' '
+       test -z $(cd gitwc; git-svn info --url unknown-file \
+                       2> ../actual.info--url-unknown-file) &&
+       git-diff expected.info-unknown-file actual.info--url-unknown-file
+       '
+
+test_expect_success 'info unknown-directory' "
+       mkdir gitwc/unknown-directory svnwc/unknown-directory &&
+       ptouch gitwc/unknown-directory svnwc/unknown-directory &&
+       touch gitwc/unknown-directory/.placeholder &&
+       (cd svnwc; svn info unknown-directory) \
+               2> expected.info-unknown-directory &&
+       (cd gitwc; git-svn info unknown-directory) \
+               2> actual.info-unknown-directory &&
+       git-diff expected.info-unknown-directory actual.info-unknown-directory
+       "
+
+test_expect_success 'info --url unknown-directory' '
+       test -z $(cd gitwc; git-svn info --url unknown-directory \
+                       2> ../actual.info--url-unknown-directory) &&
+       git-diff expected.info-unknown-directory \
+                actual.info--url-unknown-directory
+       '
+
+test_expect_success 'info unknown-symlink-file' "
+       cd gitwc &&
+               ln -s unknown-file unknown-symlink-file &&
+       cd .. &&
+       cd svnwc &&
+               ln -s unknown-file unknown-symlink-file &&
+       cd .. &&
+       ptouch gitwc/unknown-symlink-file svnwc/unknown-symlink-file &&
+       (cd svnwc; svn info unknown-symlink-file) \
+               2> expected.info-unknown-symlink-file &&
+       (cd gitwc; git-svn info unknown-symlink-file) \
+               2> actual.info-unknown-symlink-file &&
+       git-diff expected.info-unknown-symlink-file \
+                actual.info-unknown-symlink-file
+       "
+
+test_expect_success 'info --url unknown-symlink-file' '
+       test -z $(cd gitwc; git-svn info --url unknown-symlink-file \
+                       2> ../actual.info--url-unknown-symlink-file) &&
+       git-diff expected.info-unknown-symlink-file \
+                actual.info--url-unknown-symlink-file
+       '
+
+test_expect_success 'info unknown-symlink-directory' "
+       cd gitwc &&
+               ln -s unknown-directory unknown-symlink-directory &&
+       cd .. &&
+       cd svnwc &&
+               ln -s unknown-directory unknown-symlink-directory &&
+       cd .. &&
+       ptouch gitwc/unknown-symlink-directory \
+              svnwc/unknown-symlink-directory &&
+       (cd svnwc; svn info unknown-symlink-directory) \
+               2> expected.info-unknown-symlink-directory &&
+       (cd gitwc; git-svn info unknown-symlink-directory) \
+               2> actual.info-unknown-symlink-directory &&
+       git-diff expected.info-unknown-symlink-directory \
+                actual.info-unknown-symlink-directory
+       "
+
+test_expect_success 'info --url unknown-symlink-directory' '
+       test -z $(cd gitwc; git-svn info --url unknown-symlink-directory \
+                       2> ../actual.info--url-unknown-symlink-directory) &&
+       git-diff expected.info-unknown-symlink-directory \
+                actual.info--url-unknown-symlink-directory
+       '
+
+test_done
diff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh
new file mode 100755 (executable)
index 0000000..910c584
--- /dev/null
@@ -0,0 +1,249 @@
+#!/bin/sh
+#
+# Copyright (c) Robin Rosenberg
+#
+test_description='CVS export comit. '
+
+. ./test-lib.sh
+
+cvs >/dev/null 2>&1
+if test $? -ne 1
+then
+    test_expect_success 'skipping git-cvsexportcommit tests, cvs not found' :
+    test_done
+    exit
+fi
+
+CVSROOT=$(pwd)/cvsroot
+CVSWORK=$(pwd)/cvswork
+GIT_DIR=$(pwd)/.git
+export CVSROOT CVSWORK GIT_DIR
+
+rm -rf "$CVSROOT" "$CVSWORK"
+mkdir "$CVSROOT" &&
+cvs init &&
+cvs -Q co -d "$CVSWORK" . &&
+echo >empty &&
+git add empty &&
+git commit -q -a -m "Initial" 2>/dev/null ||
+exit 1
+
+check_entries () {
+       # $1 == directory, $2 == expected
+       grep '^/' "$1/CVS/Entries" | sort | cut -d/ -f2,3,5 >actual
+       if test -z "$2"
+       then
+               >expected
+       else
+               printf '%s\n' "$2" | tr '|' '\012' >expected
+       fi
+       diff -u expected actual
+}
+
+test_expect_success \
+    'New file' \
+    'mkdir A B C D E F &&
+     echo hello1 >A/newfile1.txt &&
+     echo hello2 >B/newfile2.txt &&
+     cp ../test9200a.png C/newfile3.png &&
+     cp ../test9200a.png D/newfile4.png &&
+     git add A/newfile1.txt &&
+     git add B/newfile2.txt &&
+     git add C/newfile3.png &&
+     git add D/newfile4.png &&
+     git commit -a -m "Test: New file" &&
+     id=$(git rev-list --max-count=1 HEAD) &&
+     (cd "$CVSWORK" &&
+     git cvsexportcommit -c $id &&
+     check_entries A "newfile1.txt/1.1/" &&
+     check_entries B "newfile2.txt/1.1/" &&
+     check_entries C "newfile3.png/1.1/-kb" &&
+     check_entries D "newfile4.png/1.1/-kb" &&
+     diff A/newfile1.txt ../A/newfile1.txt &&
+     diff B/newfile2.txt ../B/newfile2.txt &&
+     diff C/newfile3.png ../C/newfile3.png &&
+     diff D/newfile4.png ../D/newfile4.png
+     )'
+
+test_expect_success \
+    'Remove two files, add two and update two' \
+    'echo Hello1 >>A/newfile1.txt &&
+     rm -f B/newfile2.txt &&
+     rm -f C/newfile3.png &&
+     echo Hello5  >E/newfile5.txt &&
+     cp ../test9200b.png D/newfile4.png &&
+     cp ../test9200a.png F/newfile6.png &&
+     git add E/newfile5.txt &&
+     git add F/newfile6.png &&
+     git commit -a -m "Test: Remove, add and update" &&
+     id=$(git rev-list --max-count=1 HEAD) &&
+     (cd "$CVSWORK" &&
+     git cvsexportcommit -c $id &&
+     check_entries A "newfile1.txt/1.2/" &&
+     check_entries B "" &&
+     check_entries C "" &&
+     check_entries D "newfile4.png/1.2/-kb" &&
+     check_entries E "newfile5.txt/1.1/" &&
+     check_entries F "newfile6.png/1.1/-kb" &&
+     diff A/newfile1.txt ../A/newfile1.txt &&
+     diff D/newfile4.png ../D/newfile4.png &&
+     diff E/newfile5.txt ../E/newfile5.txt &&
+     diff F/newfile6.png ../F/newfile6.png
+     )'
+
+# Should fail (but only on the git-cvsexportcommit stage)
+test_expect_success \
+    'Fail to change binary more than one generation old' \
+    'cat F/newfile6.png >>D/newfile4.png &&
+     git commit -a -m "generatiion 1" &&
+     cat F/newfile6.png >>D/newfile4.png &&
+     git commit -a -m "generation 2" &&
+     id=$(git rev-list --max-count=1 HEAD) &&
+     (cd "$CVSWORK" &&
+     ! git cvsexportcommit -c $id
+     )'
+
+#test_expect_success \
+#    'Fail to remove binary file more than one generation old' \
+#    'git reset --hard HEAD^ &&
+#     cat F/newfile6.png >>D/newfile4.png &&
+#     git commit -a -m "generation 2 (again)" &&
+#     rm -f D/newfile4.png &&
+#     git commit -a -m "generation 3" &&
+#     id=$(git rev-list --max-count=1 HEAD) &&
+#     (cd "$CVSWORK" &&
+#     ! git cvsexportcommit -c $id
+#     )'
+
+# We reuse the state from two tests back here
+
+# This test is here because a patch for only binary files will
+# fail with gnu patch, so cvsexportcommit must handle that.
+test_expect_success \
+    'Remove only binary files' \
+    'git reset --hard HEAD^^ &&
+     rm -f D/newfile4.png &&
+     git commit -a -m "test: remove only a binary file" &&
+     id=$(git rev-list --max-count=1 HEAD) &&
+     (cd "$CVSWORK" &&
+     git cvsexportcommit -c $id &&
+     check_entries A "newfile1.txt/1.2/" &&
+     check_entries B "" &&
+     check_entries C "" &&
+     check_entries D "" &&
+     check_entries E "newfile5.txt/1.1/" &&
+     check_entries F "newfile6.png/1.1/-kb" &&
+     diff A/newfile1.txt ../A/newfile1.txt &&
+     diff E/newfile5.txt ../E/newfile5.txt &&
+     diff F/newfile6.png ../F/newfile6.png
+     )'
+
+test_expect_success \
+    'Remove only a text file' \
+    'rm -f A/newfile1.txt &&
+     git commit -a -m "test: remove only a binary file" &&
+     id=$(git rev-list --max-count=1 HEAD) &&
+     (cd "$CVSWORK" &&
+     git cvsexportcommit -c $id &&
+     check_entries A "" &&
+     check_entries B "" &&
+     check_entries C "" &&
+     check_entries D "" &&
+     check_entries E "newfile5.txt/1.1/" &&
+     check_entries F "newfile6.png/1.1/-kb" &&
+     diff E/newfile5.txt ../E/newfile5.txt &&
+     diff F/newfile6.png ../F/newfile6.png
+     )'
+
+test_expect_success \
+     'New file with spaces in file name' \
+     'mkdir "G g" &&
+      echo ok then >"G g/with spaces.txt" &&
+      git add "G g/with spaces.txt" && \
+      cp ../test9200a.png "G g/with spaces.png" && \
+      git add "G g/with spaces.png" &&
+      git commit -a -m "With spaces" &&
+      id=$(git rev-list --max-count=1 HEAD) &&
+      (cd "$CVSWORK" &&
+      git-cvsexportcommit -c $id &&
+      check_entries "G g" "with spaces.png/1.1/-kb|with spaces.txt/1.1/"
+      )'
+
+test_expect_success \
+     'Update file with spaces in file name' \
+     'echo Ok then >>"G g/with spaces.txt" &&
+      cat ../test9200a.png >>"G g/with spaces.png" && \
+      git add "G g/with spaces.png" &&
+      git commit -a -m "Update with spaces" &&
+      id=$(git rev-list --max-count=1 HEAD) &&
+      (cd "$CVSWORK" &&
+      git-cvsexportcommit -c $id
+      check_entries "G g" "with spaces.png/1.2/-kb|with spaces.txt/1.2/"
+      )'
+
+# Some filesystems mangle pathnames with UTF-8 characters --
+# check and skip
+if p="Å/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/å/ä/ö" &&
+       mkdir -p "tst/$p" &&
+       date >"tst/$p/day" &&
+       found=$(find tst -type f -print) &&
+       test "z$found" = "ztst/$p/day" &&
+       rm -fr tst
+then
+
+# This test contains UTF-8 characters
+test_expect_success \
+     'File with non-ascii file name' \
+     'mkdir -p Å/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/å/ä/ö &&
+      echo Foo >Å/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/å/ä/ö/gårdetsågårdet.txt &&
+      git add Å/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/å/ä/ö/gårdetsågårdet.txt &&
+      cp ../test9200a.png Å/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/å/ä/ö/gårdetsågårdet.png &&
+      git add Å/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/å/ä/ö/gårdetsågårdet.png &&
+      git commit -a -m "Går det så går det" && \
+      id=$(git rev-list --max-count=1 HEAD) &&
+      (cd "$CVSWORK" &&
+      git-cvsexportcommit -v -c $id &&
+      check_entries \
+      "Å/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/å/ä/ö" \
+      "gårdetsågårdet.png/1.1/-kb|gårdetsågårdet.txt/1.1/"
+      )'
+
+fi
+
+rm -fr tst
+
+test_expect_success \
+     'Mismatching patch should fail' \
+     'date >>"E/newfile5.txt" &&
+      git add "E/newfile5.txt" &&
+      git commit -a -m "Update one" &&
+      date >>"E/newfile5.txt" &&
+      git add "E/newfile5.txt" &&
+      git commit -a -m "Update two" &&
+      id=$(git rev-list --max-count=1 HEAD) &&
+      (cd "$CVSWORK" &&
+      ! git-cvsexportcommit -c $id
+      )'
+
+case "$(git repo-config --bool core.filemode)" in
+false)
+       ;;
+*)
+test_expect_success \
+     'Retain execute bit' \
+     'mkdir G &&
+      echo executeon >G/on &&
+      chmod +x G/on &&
+      echo executeoff >G/off &&
+      git add G/on &&
+      git add G/off &&
+      git commit -a -m "Execute test" &&
+      (cd "$CVSWORK" &&
+      git-cvsexportcommit -c HEAD
+      test -x G/on &&
+      ! test -x G/off
+      )'
+       ;;
+esac
+
+test_done
diff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh
new file mode 100755 (executable)
index 0000000..0595041
--- /dev/null
@@ -0,0 +1,919 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Shawn Pearce
+#
+
+test_description='test git-fast-import utility'
+. ./test-lib.sh
+. ../diff-lib.sh ;# test-lib chdir's into trash
+
+file2_data='file2
+second line of EOF'
+
+file3_data='EOF
+in 3rd file
+ END'
+
+file4_data=abcd
+file4_len=4
+
+file5_data='an inline file.
+  we should see it later.'
+
+file6_data='#!/bin/sh
+echo "$@"'
+
+###
+### series A
+###
+
+test_tick
+cat >input <<INPUT_END
+blob
+mark :2
+data <<EOF
+$file2_data
+EOF
+
+blob
+mark :3
+data <<END
+$file3_data
+END
+
+blob
+mark :4
+data $file4_len
+$file4_data
+commit refs/heads/master
+mark :5
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+initial
+COMMIT
+
+M 644 :2 file2
+M 644 :3 file3
+M 755 :4 file4
+
+INPUT_END
+test_expect_success \
+    'A: create pack from stdin' \
+    'git-fast-import --export-marks=marks.out <input &&
+        git whatchanged master'
+test_expect_success \
+       'A: verify pack' \
+       'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done'
+
+cat >expect <<EOF
+author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+
+initial
+EOF
+test_expect_success \
+       'A: verify commit' \
+       'git cat-file commit master | sed 1d >actual &&
+       git diff expect actual'
+
+cat >expect <<EOF
+100644 blob file2
+100644 blob file3
+100755 blob file4
+EOF
+test_expect_success \
+       'A: verify tree' \
+       'git cat-file -p master^{tree} | sed "s/ [0-9a-f]*      / /" >actual &&
+        git diff expect actual'
+
+echo "$file2_data" >expect
+test_expect_success \
+       'A: verify file2' \
+       'git cat-file blob master:file2 >actual && git diff expect actual'
+
+echo "$file3_data" >expect
+test_expect_success \
+       'A: verify file3' \
+       'git cat-file blob master:file3 >actual && git diff expect actual'
+
+printf "$file4_data" >expect
+test_expect_success \
+       'A: verify file4' \
+       'git cat-file blob master:file4 >actual && git diff expect actual'
+
+cat >expect <<EOF
+:2 `git rev-parse --verify master:file2`
+:3 `git rev-parse --verify master:file3`
+:4 `git rev-parse --verify master:file4`
+:5 `git rev-parse --verify master^0`
+EOF
+test_expect_success \
+       'A: verify marks output' \
+       'git diff expect marks.out'
+
+test_expect_success \
+       'A: verify marks import' \
+       'git-fast-import \
+               --import-marks=marks.out \
+               --export-marks=marks.new \
+               </dev/null &&
+       git diff -u expect marks.new'
+
+test_tick
+cat >input <<INPUT_END
+commit refs/heads/verify--import-marks
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+recreate from :5
+COMMIT
+
+from :5
+M 755 :2 copy-of-file2
+
+INPUT_END
+test_expect_success \
+       'A: verify marks import does not crash' \
+       'git-fast-import --import-marks=marks.out <input &&
+        git whatchanged verify--import-marks'
+test_expect_success \
+       'A: verify pack' \
+       'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done'
+cat >expect <<EOF
+:000000 100755 0000000000000000000000000000000000000000 7123f7f44e39be127c5eb701e5968176ee9d78b1 A     copy-of-file2
+EOF
+git diff-tree -M -r master verify--import-marks >actual
+test_expect_success \
+       'A: verify diff' \
+       'compare_diff_raw expect actual &&
+        test `git rev-parse --verify master:file2` \
+           = `git rev-parse --verify verify--import-marks:copy-of-file2`'
+
+###
+### series B
+###
+
+test_tick
+cat >input <<INPUT_END
+commit refs/heads/branch
+mark :1
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+corrupt
+COMMIT
+
+from refs/heads/master
+M 755 0000000000000000000000000000000000000001 zero1
+
+INPUT_END
+test_expect_failure \
+    'B: fail on invalid blob sha1' \
+    'git-fast-import <input'
+rm -f .git/objects/pack_* .git/objects/index_*
+
+cat >input <<INPUT_END
+commit .badbranchname
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+corrupt
+COMMIT
+
+from refs/heads/master
+
+INPUT_END
+test_expect_failure \
+    'B: fail on invalid branch name ".badbranchname"' \
+    'git-fast-import <input'
+rm -f .git/objects/pack_* .git/objects/index_*
+
+cat >input <<INPUT_END
+commit bad[branch]name
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+corrupt
+COMMIT
+
+from refs/heads/master
+
+INPUT_END
+test_expect_failure \
+    'B: fail on invalid branch name "bad[branch]name"' \
+    'git-fast-import <input'
+rm -f .git/objects/pack_* .git/objects/index_*
+
+cat >input <<INPUT_END
+commit TEMP_TAG
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+tag base
+COMMIT
+
+from refs/heads/master
+
+INPUT_END
+test_expect_success \
+    'B: accept branch name "TEMP_TAG"' \
+    'git-fast-import <input &&
+        test -f .git/TEMP_TAG &&
+        test `git rev-parse master` = `git rev-parse TEMP_TAG^`'
+rm -f .git/TEMP_TAG
+
+###
+### series C
+###
+
+newf=`echo hi newf | git-hash-object -w --stdin`
+oldf=`git rev-parse --verify master:file2`
+test_tick
+cat >input <<INPUT_END
+commit refs/heads/branch
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+second
+COMMIT
+
+from refs/heads/master
+M 644 $oldf file2/oldf
+M 755 $newf file2/newf
+D file3
+
+INPUT_END
+test_expect_success \
+    'C: incremental import create pack from stdin' \
+    'git-fast-import <input &&
+        git whatchanged branch'
+test_expect_success \
+       'C: verify pack' \
+       'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done'
+test_expect_success \
+       'C: validate reuse existing blob' \
+       'test $newf = `git rev-parse --verify branch:file2/newf`
+        test $oldf = `git rev-parse --verify branch:file2/oldf`'
+
+cat >expect <<EOF
+parent `git rev-parse --verify master^0`
+author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+
+second
+EOF
+test_expect_success \
+       'C: verify commit' \
+       'git cat-file commit branch | sed 1d >actual &&
+        git diff expect actual'
+
+cat >expect <<EOF
+:000000 100755 0000000000000000000000000000000000000000 f1fb5da718392694d0076d677d6d0e364c79b0bc A     file2/newf
+:100644 100644 7123f7f44e39be127c5eb701e5968176ee9d78b1 7123f7f44e39be127c5eb701e5968176ee9d78b1 R100  file2   file2/oldf
+:100644 000000 0d92e9f3374ae2947c23aa477cbc68ce598135f1 0000000000000000000000000000000000000000 D     file3
+EOF
+git diff-tree -M -r master branch >actual
+test_expect_success \
+       'C: validate rename result' \
+       'compare_diff_raw expect actual'
+
+###
+### series D
+###
+
+test_tick
+cat >input <<INPUT_END
+commit refs/heads/branch
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+third
+COMMIT
+
+from refs/heads/branch^0
+M 644 inline newdir/interesting
+data <<EOF
+$file5_data
+EOF
+
+M 755 inline newdir/exec.sh
+data <<EOF
+$file6_data
+EOF
+
+INPUT_END
+test_expect_success \
+    'D: inline data in commit' \
+    'git-fast-import <input &&
+        git whatchanged branch'
+test_expect_success \
+       'D: verify pack' \
+       'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done'
+
+cat >expect <<EOF
+:000000 100755 0000000000000000000000000000000000000000 35a59026a33beac1569b1c7f66f3090ce9c09afc A     newdir/exec.sh
+:000000 100644 0000000000000000000000000000000000000000 046d0371e9220107917db0d0e030628de8a1de9b A     newdir/interesting
+EOF
+git diff-tree -M -r branch^ branch >actual
+test_expect_success \
+       'D: validate new files added' \
+       'compare_diff_raw expect actual'
+
+echo "$file5_data" >expect
+test_expect_success \
+       'D: verify file5' \
+       'git cat-file blob branch:newdir/interesting >actual &&
+        git diff expect actual'
+
+echo "$file6_data" >expect
+test_expect_success \
+       'D: verify file6' \
+       'git cat-file blob branch:newdir/exec.sh >actual &&
+        git diff expect actual'
+
+###
+### series E
+###
+
+cat >input <<INPUT_END
+commit refs/heads/branch
+author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> Tue Feb 6 11:22:18 2007 -0500
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> Tue Feb 6 12:35:02 2007 -0500
+data <<COMMIT
+RFC 2822 type date
+COMMIT
+
+from refs/heads/branch^0
+
+INPUT_END
+test_expect_failure \
+    'E: rfc2822 date, --date-format=raw' \
+    'git-fast-import --date-format=raw <input'
+test_expect_success \
+    'E: rfc2822 date, --date-format=rfc2822' \
+    'git-fast-import --date-format=rfc2822 <input'
+test_expect_success \
+       'E: verify pack' \
+       'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done'
+
+cat >expect <<EOF
+author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> 1170778938 -0500
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1170783302 -0500
+
+RFC 2822 type date
+EOF
+test_expect_success \
+       'E: verify commit' \
+       'git cat-file commit branch | sed 1,2d >actual &&
+       git diff expect actual'
+
+###
+### series F
+###
+
+old_branch=`git rev-parse --verify branch^0`
+test_tick
+cat >input <<INPUT_END
+commit refs/heads/branch
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+losing things already?
+COMMIT
+
+from refs/heads/branch~1
+
+reset refs/heads/other
+from refs/heads/branch
+
+INPUT_END
+test_expect_success \
+    'F: non-fast-forward update skips' \
+    'if git-fast-import <input
+        then
+               echo BAD gfi did not fail
+               return 1
+        else
+               if test $old_branch = `git rev-parse --verify branch^0`
+               then
+                       : branch unaffected and failure returned
+                       return 0
+               else
+                       echo BAD gfi changed branch $old_branch
+                       return 1
+               fi
+        fi
+       '
+test_expect_success \
+       'F: verify pack' \
+       'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done'
+
+cat >expect <<EOF
+tree `git rev-parse branch~1^{tree}`
+parent `git rev-parse branch~1`
+author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+
+losing things already?
+EOF
+test_expect_success \
+       'F: verify other commit' \
+       'git cat-file commit other >actual &&
+       git diff expect actual'
+
+###
+### series G
+###
+
+old_branch=`git rev-parse --verify branch^0`
+test_tick
+cat >input <<INPUT_END
+commit refs/heads/branch
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+losing things already?
+COMMIT
+
+from refs/heads/branch~1
+
+INPUT_END
+test_expect_success \
+    'G: non-fast-forward update forced' \
+    'git-fast-import --force <input'
+test_expect_success \
+       'G: verify pack' \
+       'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done'
+test_expect_success \
+       'G: branch changed, but logged' \
+       'test $old_branch != `git rev-parse --verify branch^0` &&
+        test $old_branch = `git rev-parse --verify branch@{1}`'
+
+###
+### series H
+###
+
+test_tick
+cat >input <<INPUT_END
+commit refs/heads/H
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+third
+COMMIT
+
+from refs/heads/branch^0
+M 644 inline i-will-die
+data <<EOF
+this file will never exist.
+EOF
+
+deleteall
+M 644 inline h/e/l/lo
+data <<EOF
+$file5_data
+EOF
+
+INPUT_END
+test_expect_success \
+    'H: deletall, add 1' \
+    'git-fast-import <input &&
+        git whatchanged H'
+test_expect_success \
+       'H: verify pack' \
+       'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done'
+
+cat >expect <<EOF
+:100755 000000 f1fb5da718392694d0076d677d6d0e364c79b0bc 0000000000000000000000000000000000000000 D     file2/newf
+:100644 000000 7123f7f44e39be127c5eb701e5968176ee9d78b1 0000000000000000000000000000000000000000 D     file2/oldf
+:100755 000000 85df50785d62d3b05ab03d9cbf7e4a0b49449730 0000000000000000000000000000000000000000 D     file4
+:100644 100644 fcf778cda181eaa1cbc9e9ce3a2e15ee9f9fe791 fcf778cda181eaa1cbc9e9ce3a2e15ee9f9fe791 R100  newdir/interesting      h/e/l/lo
+:100755 000000 e74b7d465e52746be2b4bae983670711e6e66657 0000000000000000000000000000000000000000 D     newdir/exec.sh
+EOF
+git diff-tree -M -r H^ H >actual
+test_expect_success \
+       'H: validate old files removed, new files added' \
+       'compare_diff_raw expect actual'
+
+echo "$file5_data" >expect
+test_expect_success \
+       'H: verify file' \
+       'git cat-file blob H:h/e/l/lo >actual &&
+        git diff expect actual'
+
+###
+### series I
+###
+
+cat >input <<INPUT_END
+commit refs/heads/export-boundary
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+we have a border.  its only 40 characters wide.
+COMMIT
+
+from refs/heads/branch
+
+INPUT_END
+test_expect_success \
+    'I: export-pack-edges' \
+    'git-fast-import --export-pack-edges=edges.list <input'
+
+cat >expect <<EOF
+.git/objects/pack/pack-.pack: `git rev-parse --verify export-boundary`
+EOF
+test_expect_success \
+       'I: verify edge list' \
+       'sed -e s/pack-.*pack/pack-.pack/ edges.list >actual &&
+        git diff expect actual'
+
+###
+### series J
+###
+
+cat >input <<INPUT_END
+commit refs/heads/J
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+create J
+COMMIT
+
+from refs/heads/branch
+
+reset refs/heads/J
+
+commit refs/heads/J
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+initialize J
+COMMIT
+
+INPUT_END
+test_expect_success \
+    'J: reset existing branch creates empty commit' \
+    'git-fast-import <input'
+test_expect_success \
+       'J: branch has 1 commit, empty tree' \
+       'test 1 = `git rev-list J | wc -l` &&
+        test 0 = `git ls-tree J | wc -l`'
+
+###
+### series K
+###
+
+cat >input <<INPUT_END
+commit refs/heads/K
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+create K
+COMMIT
+
+from refs/heads/branch
+
+commit refs/heads/K
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+redo K
+COMMIT
+
+from refs/heads/branch^1
+
+INPUT_END
+test_expect_success \
+    'K: reinit branch with from' \
+    'git-fast-import <input'
+test_expect_success \
+    'K: verify K^1 = branch^1' \
+    'test `git rev-parse --verify branch^1` \
+               = `git rev-parse --verify K^1`'
+
+###
+### series L
+###
+
+cat >input <<INPUT_END
+blob
+mark :1
+data <<EOF
+some data
+EOF
+
+blob
+mark :2
+data <<EOF
+other data
+EOF
+
+commit refs/heads/L
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+create L
+COMMIT
+
+M 644 :1 b.
+M 644 :1 b/other
+M 644 :1 ba
+
+commit refs/heads/L
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+update L
+COMMIT
+
+M 644 :2 b.
+M 644 :2 b/other
+M 644 :2 ba
+INPUT_END
+
+cat >expect <<EXPECT_END
+:100644 100644 4268632... 55d3a52... M b.
+:040000 040000 0ae5cac... 443c768... M b
+:100644 100644 4268632... 55d3a52... M ba
+EXPECT_END
+
+test_expect_success \
+    'L: verify internal tree sorting' \
+       'git-fast-import <input &&
+        git diff-tree --abbrev --raw L^ L >output &&
+        git diff expect output'
+
+###
+### series M
+###
+
+test_tick
+cat >input <<INPUT_END
+commit refs/heads/M1
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+file rename
+COMMIT
+
+from refs/heads/branch^0
+R file2/newf file2/n.e.w.f
+
+INPUT_END
+
+cat >expect <<EOF
+:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc R100  file2/newf      file2/n.e.w.f
+EOF
+test_expect_success \
+       'M: rename file in same subdirectory' \
+       'git-fast-import <input &&
+        git diff-tree -M -r M1^ M1 >actual &&
+        compare_diff_raw expect actual'
+
+cat >input <<INPUT_END
+commit refs/heads/M2
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+file rename
+COMMIT
+
+from refs/heads/branch^0
+R file2/newf i/am/new/to/you
+
+INPUT_END
+
+cat >expect <<EOF
+:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc R100  file2/newf      i/am/new/to/you
+EOF
+test_expect_success \
+       'M: rename file to new subdirectory' \
+       'git-fast-import <input &&
+        git diff-tree -M -r M2^ M2 >actual &&
+        compare_diff_raw expect actual'
+
+cat >input <<INPUT_END
+commit refs/heads/M3
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+file rename
+COMMIT
+
+from refs/heads/M2^0
+R i other/sub
+
+INPUT_END
+
+cat >expect <<EOF
+:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc R100  i/am/new/to/you other/sub/am/new/to/you
+EOF
+test_expect_success \
+       'M: rename subdirectory to new subdirectory' \
+       'git-fast-import <input &&
+        git diff-tree -M -r M3^ M3 >actual &&
+        compare_diff_raw expect actual'
+
+###
+### series N
+###
+
+test_tick
+cat >input <<INPUT_END
+commit refs/heads/N1
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+file copy
+COMMIT
+
+from refs/heads/branch^0
+C file2/newf file2/n.e.w.f
+
+INPUT_END
+
+cat >expect <<EOF
+:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc C100  file2/newf      file2/n.e.w.f
+EOF
+test_expect_success \
+       'N: copy file in same subdirectory' \
+       'git-fast-import <input &&
+        git diff-tree -C --find-copies-harder -r N1^ N1 >actual &&
+        compare_diff_raw expect actual'
+
+cat >input <<INPUT_END
+commit refs/heads/N2
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+clean directory copy
+COMMIT
+
+from refs/heads/branch^0
+C file2 file3
+
+commit refs/heads/N2
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+modify directory copy
+COMMIT
+
+M 644 inline file3/file5
+data <<EOF
+$file5_data
+EOF
+
+INPUT_END
+
+cat >expect <<EOF
+:100644 100644 fcf778cda181eaa1cbc9e9ce3a2e15ee9f9fe791 fcf778cda181eaa1cbc9e9ce3a2e15ee9f9fe791 C100  newdir/interesting      file3/file5
+:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc C100  file2/newf      file3/newf
+:100644 100644 7123f7f44e39be127c5eb701e5968176ee9d78b1 7123f7f44e39be127c5eb701e5968176ee9d78b1 C100  file2/oldf      file3/oldf
+EOF
+test_expect_success \
+       'N: copy then modify subdirectory' \
+       'git-fast-import <input &&
+        git diff-tree -C --find-copies-harder -r N2^^ N2 >actual &&
+        compare_diff_raw expect actual'
+
+cat >input <<INPUT_END
+commit refs/heads/N3
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+dirty directory copy
+COMMIT
+
+from refs/heads/branch^0
+M 644 inline file2/file5
+data <<EOF
+$file5_data
+EOF
+
+C file2 file3
+D file2/file5
+
+INPUT_END
+
+test_expect_success \
+       'N: copy dirty subdirectory' \
+       'git-fast-import <input &&
+        test `git-rev-parse N2^{tree}` = `git-rev-parse N3^{tree}`'
+
+###
+### series O
+###
+
+cat >input <<INPUT_END
+#we will
+commit refs/heads/O1
+# -- ignore all of this text
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+# $GIT_COMMITTER_NAME has inserted here for his benefit.
+data <<COMMIT
+dirty directory copy
+COMMIT
+
+# don't forget the import blank line!
+#
+# yes, we started from our usual base of branch^0.
+# i like branch^0.
+from refs/heads/branch^0
+# and we need to reuse file2/file5 from N3 above.
+M 644 inline file2/file5
+# otherwise the tree will be different
+data <<EOF
+$file5_data
+EOF
+
+# don't forget to copy file2 to file3
+C file2 file3
+#
+# or to delete file5 from file2.
+D file2/file5
+# are we done yet?
+
+INPUT_END
+
+test_expect_success \
+       'O: comments are all skipped' \
+       'git-fast-import <input &&
+        test `git-rev-parse N3` = `git-rev-parse O1`'
+
+cat >input <<INPUT_END
+commit refs/heads/O2
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+dirty directory copy
+COMMIT
+from refs/heads/branch^0
+M 644 inline file2/file5
+data <<EOF
+$file5_data
+EOF
+C file2 file3
+D file2/file5
+
+INPUT_END
+
+test_expect_success \
+       'O: blank lines not necessary after data commands' \
+       'git-fast-import <input &&
+        test `git-rev-parse N3` = `git-rev-parse O2`'
+
+test_expect_success \
+       'O: repack before next test' \
+       'git repack -a -d'
+
+cat >input <<INPUT_END
+commit refs/heads/O3
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+zstring
+COMMIT
+commit refs/heads/O3
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+zof
+COMMIT
+checkpoint
+commit refs/heads/O3
+mark :5
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+zempty
+COMMIT
+checkpoint
+commit refs/heads/O3
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+zcommits
+COMMIT
+reset refs/tags/O3-2nd
+from :5
+INPUT_END
+
+cat >expect <<INPUT_END
+string
+of
+empty
+commits
+INPUT_END
+test_expect_success \
+       'O: blank lines not necessary after other commands' \
+       'git-fast-import <input &&
+        test 8 = `find .git/objects/pack -type f | wc -l` &&
+        test `git rev-parse refs/tags/O3-2nd` = `git rev-parse O3^` &&
+        git log --reverse --pretty=oneline O3 | sed s/^.*z// >actual &&
+        git diff expect actual'
+
+cat >input <<INPUT_END
+commit refs/heads/O4
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+zstring
+COMMIT
+commit refs/heads/O4
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+zof
+COMMIT
+progress Two commits down, 2 to go!
+commit refs/heads/O4
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+zempty
+COMMIT
+progress Three commits down, 1 to go!
+commit refs/heads/O4
+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+data <<COMMIT
+zcommits
+COMMIT
+progress I'm done!
+INPUT_END
+test_expect_success \
+       'O: progress outputs as requested by input' \
+       'git-fast-import <input >actual &&
+        grep "progress " <input >expect &&
+        git diff expect actual'
+
+test_done
diff --git a/t/t9301-fast-export.sh b/t/t9301-fast-export.sh
new file mode 100755 (executable)
index 0000000..f09bfb1
--- /dev/null
@@ -0,0 +1,123 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes E. Schindelin
+#
+
+test_description='git-fast-export'
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+
+       echo Wohlauf > file &&
+       git add file &&
+       test_tick &&
+       git commit -m initial &&
+       echo die Luft > file &&
+       echo geht frisch > file2 &&
+       git add file file2 &&
+       test_tick &&
+       git commit -m second &&
+       echo und > file2 &&
+       test_tick &&
+       git commit -m third file2 &&
+       test_tick &&
+       git tag rein &&
+       git checkout -b wer HEAD^ &&
+       echo lange > file2
+       test_tick &&
+       git commit -m sitzt file2 &&
+       test_tick &&
+       git tag -a -m valentin muss &&
+       git merge -s ours master
+
+'
+
+test_expect_success 'fast-export | fast-import' '
+
+       MASTER=$(git rev-parse --verify master) &&
+       REIN=$(git rev-parse --verify rein) &&
+       WER=$(git rev-parse --verify wer) &&
+       MUSS=$(git rev-parse --verify muss) &&
+       mkdir new &&
+       git --git-dir=new/.git init &&
+       git fast-export --all |
+       (cd new &&
+        git fast-import &&
+        test $MASTER = $(git rev-parse --verify refs/heads/master) &&
+        test $REIN = $(git rev-parse --verify refs/tags/rein) &&
+        test $WER = $(git rev-parse --verify refs/heads/wer) &&
+        test $MUSS = $(git rev-parse --verify refs/tags/muss))
+
+'
+
+test_expect_success 'fast-export master~2..master' '
+
+       git fast-export master~2..master |
+               sed "s/master/partial/" |
+               (cd new &&
+                git fast-import &&
+                test $MASTER != $(git rev-parse --verify refs/heads/partial) &&
+                git diff master..partial &&
+                git diff master^..partial^ &&
+                ! git rev-parse partial~2)
+
+'
+
+test_expect_success 'iso-8859-1' '
+
+       git config i18n.commitencoding ISO-8859-1 &&
+       # use author and committer name in ISO-8859-1 to match it.
+       . ../t3901-8859-1.txt &&
+       test_tick &&
+       echo rosten >file &&
+       git commit -s -m den file &&
+       git fast-export wer^..wer |
+               sed "s/wer/i18n/" |
+               (cd new &&
+                git fast-import &&
+                git cat-file commit i18n | grep "Áéí óú")
+
+'
+
+cat > signed-tag-import << EOF
+tag sign-your-name
+from $(git rev-parse HEAD)
+tagger C O Mitter <committer@example.com> 1112911993 -0700
+data 210
+A message for a sign
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.5 (GNU/Linux)
+
+fakedsignaturefakedsignaturefakedsignaturefakedsignaturfakedsign
+aturefakedsignaturefake=
+=/59v
+-----END PGP SIGNATURE-----
+EOF
+
+test_expect_success 'set up faked signed tag' '
+
+       cat signed-tag-import | git fast-import
+
+'
+
+test_expect_success 'signed-tags=abort' '
+
+       ! git fast-export --signed-tags=abort sign-your-name
+
+'
+
+test_expect_success 'signed-tags=verbatim' '
+
+       git fast-export --signed-tags=verbatim sign-your-name > output &&
+       grep PGP output
+
+'
+
+test_expect_success 'signed-tags=strip' '
+
+       git fast-export --signed-tags=strip sign-your-name > output &&
+       ! grep PGP output
+
+'
+
+test_done
diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh
new file mode 100755 (executable)
index 0000000..641303e
--- /dev/null
@@ -0,0 +1,408 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Frank Lichtenheld
+#
+
+test_description='git-cvsserver access
+
+tests read access to a git repository with the
+cvs CLI client via git-cvsserver server'
+
+. ./test-lib.sh
+
+cvs >/dev/null 2>&1
+if test $? -ne 1
+then
+    test_expect_success 'skipping git-cvsserver tests, cvs not found' :
+    test_done
+    exit
+fi
+perl -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {
+    test_expect_success 'skipping git-cvsserver tests, Perl SQLite interface unavailable' :
+    test_done
+    exit
+}
+
+unset GIT_DIR GIT_CONFIG
+WORKDIR=$(pwd)
+SERVERDIR=$(pwd)/gitcvs.git
+git_config="$SERVERDIR/config"
+CVSROOT=":fork:$SERVERDIR"
+CVSWORK="$(pwd)/cvswork"
+CVS_SERVER=git-cvsserver
+export CVSROOT CVS_SERVER
+
+rm -rf "$CVSWORK" "$SERVERDIR"
+echo >empty &&
+  git add empty &&
+  git commit -q -m "First Commit" &&
+  git clone -q --local --bare "$WORKDIR/.git" "$SERVERDIR" >/dev/null 2>&1 &&
+  GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true &&
+  GIT_DIR="$SERVERDIR" git config gitcvs.logfile "$SERVERDIR/gitcvs.log" ||
+  exit 1
+
+# note that cvs doesn't accept absolute pathnames
+# as argument to co -d
+test_expect_success 'basic checkout' \
+  'GIT_CONFIG="$git_config" cvs -Q co -d cvswork master &&
+   test "$(echo $(grep -v ^D cvswork/CVS/Entries|cut -d/ -f2,3,5))" = "empty/1.1/"'
+
+#------------------------
+# PSERVER AUTHENTICATION
+#------------------------
+
+cat >request-anonymous  <<EOF
+BEGIN AUTH REQUEST
+$SERVERDIR
+anonymous
+
+END AUTH REQUEST
+EOF
+
+cat >request-git  <<EOF
+BEGIN AUTH REQUEST
+$SERVERDIR
+git
+
+END AUTH REQUEST
+EOF
+
+cat >login-anonymous <<EOF
+BEGIN VERIFICATION REQUEST
+$SERVERDIR
+anonymous
+
+END VERIFICATION REQUEST
+EOF
+
+cat >login-git <<EOF
+BEGIN VERIFICATION REQUEST
+$SERVERDIR
+git
+
+END VERIFICATION REQUEST
+EOF
+
+test_expect_success 'pserver authentication' \
+  'cat request-anonymous | git-cvsserver pserver >log 2>&1 &&
+   tail -n1 log | grep -q "^I LOVE YOU$"'
+
+test_expect_success 'pserver authentication failure (non-anonymous user)' \
+  'if cat request-git | git-cvsserver pserver >log 2>&1
+   then
+       false
+   else
+       true
+   fi &&
+   tail -n1 log | grep -q "^I HATE YOU$"'
+
+test_expect_success 'pserver authentication (login)' \
+  'cat login-anonymous | git-cvsserver pserver >log 2>&1 &&
+   tail -n1 log | grep -q "^I LOVE YOU$"'
+
+test_expect_success 'pserver authentication failure (login/non-anonymous user)' \
+  'if cat login-git | git-cvsserver pserver >log 2>&1
+   then
+       false
+   else
+       true
+   fi &&
+   tail -n1 log | grep -q "^I HATE YOU$"'
+
+
+# misuse pserver authentication for testing of req_Root
+
+cat >request-relative  <<EOF
+BEGIN AUTH REQUEST
+gitcvs.git
+anonymous
+
+END AUTH REQUEST
+EOF
+
+cat >request-conflict  <<EOF
+BEGIN AUTH REQUEST
+$SERVERDIR
+anonymous
+
+END AUTH REQUEST
+Root $WORKDIR
+EOF
+
+test_expect_success 'req_Root failure (relative pathname)' \
+  'if cat request-relative | git-cvsserver pserver >log 2>&1
+   then
+       echo unexpected success
+       false
+   else
+       true
+   fi &&
+   tail log | grep -q "^error 1 Root must be an absolute pathname$"'
+
+test_expect_success 'req_Root failure (conflicting roots)' \
+  'cat request-conflict | git-cvsserver pserver >log 2>&1 &&
+   tail log | grep -q "^error 1 Conflicting roots specified$"'
+
+test_expect_success 'req_Root (strict paths)' \
+  'cat request-anonymous | git-cvsserver --strict-paths pserver $SERVERDIR >log 2>&1 &&
+   tail -n1 log | grep -q "^I LOVE YOU$"'
+
+test_expect_failure 'req_Root failure (strict-paths)' \
+  'cat request-anonymous | git-cvsserver --strict-paths pserver $WORKDIR >log 2>&1'
+
+test_expect_success 'req_Root (w/o strict-paths)' \
+  'cat request-anonymous | git-cvsserver pserver $WORKDIR/ >log 2>&1 &&
+   tail -n1 log | grep -q "^I LOVE YOU$"'
+
+test_expect_failure 'req_Root failure (w/o strict-paths)' \
+  'cat request-anonymous | git-cvsserver pserver $WORKDIR/gitcvs >log 2>&1'
+
+cat >request-base  <<EOF
+BEGIN AUTH REQUEST
+/gitcvs.git
+anonymous
+
+END AUTH REQUEST
+Root /gitcvs.git
+EOF
+
+test_expect_success 'req_Root (base-path)' \
+  'cat request-base | git-cvsserver --strict-paths --base-path $WORKDIR/ pserver $SERVERDIR >log 2>&1 &&
+   tail -n1 log | grep -q "^I LOVE YOU$"'
+
+test_expect_failure 'req_Root failure (base-path)' \
+  'cat request-anonymous | git-cvsserver --strict-paths --base-path $WORKDIR pserver $SERVERDIR >log 2>&1'
+
+GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled false || exit 1
+
+test_expect_success 'req_Root (export-all)' \
+  'cat request-anonymous | git-cvsserver --export-all pserver $WORKDIR >log 2>&1 &&
+   tail -n1 log | grep -q "^I LOVE YOU$"'
+
+test_expect_failure 'req_Root failure (export-all w/o whitelist)' \
+  'cat request-anonymous | git-cvsserver --export-all pserver >log 2>&1 ||
+   false'
+
+test_expect_success 'req_Root (everything together)' \
+  'cat request-base | git-cvsserver --export-all --strict-paths --base-path $WORKDIR/ pserver $SERVERDIR >log 2>&1 &&
+   tail -n1 log | grep -q "^I LOVE YOU$"'
+
+GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true || exit 1
+
+#--------------
+# CONFIG TESTS
+#--------------
+
+test_expect_success 'gitcvs.enabled = false' \
+  'GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled false &&
+   if GIT_CONFIG="$git_config" cvs -Q co -d cvswork2 master >cvs.log 2>&1
+   then
+     echo unexpected cvs success
+     false
+   else
+     true
+   fi &&
+   cat cvs.log | grep -q "GITCVS emulation disabled" &&
+   test ! -d cvswork2'
+
+rm -fr cvswork2
+test_expect_success 'gitcvs.ext.enabled = true' \
+  'GIT_DIR="$SERVERDIR" git config --bool gitcvs.ext.enabled true &&
+   GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled false &&
+   GIT_CONFIG="$git_config" cvs -Q co -d cvswork2 master >cvs.log 2>&1 &&
+   diff -q cvswork cvswork2'
+
+rm -fr cvswork2
+test_expect_success 'gitcvs.ext.enabled = false' \
+  'GIT_DIR="$SERVERDIR" git config --bool gitcvs.ext.enabled false &&
+   GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true &&
+   if GIT_CONFIG="$git_config" cvs -Q co -d cvswork2 master >cvs.log 2>&1
+   then
+     echo unexpected cvs success
+     false
+   else
+     true
+   fi &&
+   cat cvs.log | grep -q "GITCVS emulation disabled" &&
+   test ! -d cvswork2'
+
+rm -fr cvswork2
+test_expect_success 'gitcvs.dbname' \
+  'GIT_DIR="$SERVERDIR" git config --bool gitcvs.ext.enabled true &&
+   GIT_DIR="$SERVERDIR" git config gitcvs.dbname %Ggitcvs.%a.%m.sqlite &&
+   GIT_CONFIG="$git_config" cvs -Q co -d cvswork2 master >cvs.log 2>&1 &&
+   diff -q cvswork cvswork2 &&
+   test -f "$SERVERDIR/gitcvs.ext.master.sqlite" &&
+   cmp "$SERVERDIR/gitcvs.master.sqlite" "$SERVERDIR/gitcvs.ext.master.sqlite"'
+
+rm -fr cvswork2
+test_expect_success 'gitcvs.ext.dbname' \
+  'GIT_DIR="$SERVERDIR" git config --bool gitcvs.ext.enabled true &&
+   GIT_DIR="$SERVERDIR" git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
+   GIT_DIR="$SERVERDIR" git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
+   GIT_CONFIG="$git_config" cvs -Q co -d cvswork2 master >cvs.log 2>&1 &&
+   diff -q cvswork cvswork2 &&
+   test -f "$SERVERDIR/gitcvs1.ext.master.sqlite" &&
+   test ! -f "$SERVERDIR/gitcvs2.ext.master.sqlite" &&
+   cmp "$SERVERDIR/gitcvs.master.sqlite" "$SERVERDIR/gitcvs1.ext.master.sqlite"'
+
+
+#------------
+# CVS UPDATE
+#------------
+
+rm -fr "$SERVERDIR"
+cd "$WORKDIR" &&
+git clone -q --local --bare "$WORKDIR/.git" "$SERVERDIR" >/dev/null 2>&1 &&
+GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true &&
+GIT_DIR="$SERVERDIR" git config gitcvs.logfile "$SERVERDIR/gitcvs.log" ||
+exit 1
+
+test_expect_success 'cvs update (create new file)' \
+  'echo testfile1 >testfile1 &&
+   git add testfile1 &&
+   git commit -q -m "Add testfile1" &&
+   git push gitcvs.git >/dev/null &&
+   cd cvswork &&
+   GIT_CONFIG="$git_config" cvs -Q update &&
+   test "$(echo $(grep testfile1 CVS/Entries|cut -d/ -f2,3,5))" = "testfile1/1.1/" &&
+   diff -q testfile1 ../testfile1'
+
+cd "$WORKDIR"
+test_expect_success 'cvs update (update existing file)' \
+  'echo line 2 >>testfile1 &&
+   git add testfile1 &&
+   git commit -q -m "Append to testfile1" &&
+   git push gitcvs.git >/dev/null &&
+   cd cvswork &&
+   GIT_CONFIG="$git_config" cvs -Q update &&
+   test "$(echo $(grep testfile1 CVS/Entries|cut -d/ -f2,3,5))" = "testfile1/1.2/" &&
+   diff -q testfile1 ../testfile1'
+
+cd "$WORKDIR"
+#TODO: cvsserver doesn't support update w/o -d
+test_expect_failure "cvs update w/o -d doesn't create subdir (TODO)" \
+  'mkdir test &&
+   echo >test/empty &&
+   git add test &&
+   git commit -q -m "Single Subdirectory" &&
+   git push gitcvs.git >/dev/null &&
+   cd cvswork &&
+   GIT_CONFIG="$git_config" cvs -Q update &&
+   test ! -d test'
+
+cd "$WORKDIR"
+test_expect_success 'cvs update (subdirectories)' \
+  '(for dir in A A/B A/B/C A/D E; do
+      mkdir $dir &&
+      echo "test file in $dir" >"$dir/file_in_$(echo $dir|sed -e "s#/# #g")"  &&
+      git add $dir;
+   done) &&
+   git commit -q -m "deep sub directory structure" &&
+   git push gitcvs.git >/dev/null &&
+   cd cvswork &&
+   GIT_CONFIG="$git_config" cvs -Q update -d &&
+   (for dir in A A/B A/B/C A/D E; do
+      filename="file_in_$(echo $dir|sed -e "s#/# #g")" &&
+      if test "$(echo $(grep -v ^D $dir/CVS/Entries|cut -d/ -f2,3,5))" = "$filename/1.1/" &&
+           diff -q "$dir/$filename" "../$dir/$filename"; then
+        :
+      else
+        echo >failure
+      fi
+    done) &&
+   test ! -f failure'
+
+cd "$WORKDIR"
+test_expect_success 'cvs update (delete file)' \
+  'git rm testfile1 &&
+   git commit -q -m "Remove testfile1" &&
+   git push gitcvs.git >/dev/null &&
+   cd cvswork &&
+   GIT_CONFIG="$git_config" cvs -Q update &&
+   test -z "$(grep testfile1 CVS/Entries)" &&
+   test ! -f testfile1'
+
+cd "$WORKDIR"
+test_expect_success 'cvs update (re-add deleted file)' \
+  'echo readded testfile >testfile1 &&
+   git add testfile1 &&
+   git commit -q -m "Re-Add testfile1" &&
+   git push gitcvs.git >/dev/null &&
+   cd cvswork &&
+   GIT_CONFIG="$git_config" cvs -Q update &&
+   test "$(echo $(grep testfile1 CVS/Entries|cut -d/ -f2,3,5))" = "testfile1/1.4/" &&
+   diff -q testfile1 ../testfile1'
+
+cd "$WORKDIR"
+test_expect_success 'cvs update (merge)' \
+  'echo Line 0 >expected &&
+   for i in 1 2 3 4 5 6 7
+   do
+     echo Line $i >>merge
+     echo Line $i >>expected
+   done &&
+   echo Line 8 >>expected &&
+   git add merge &&
+   git commit -q -m "Merge test (pre-merge)" &&
+   git push gitcvs.git >/dev/null &&
+   cd cvswork &&
+   GIT_CONFIG="$git_config" cvs -Q update &&
+   test "$(echo $(grep merge CVS/Entries|cut -d/ -f2,3,5))" = "merge/1.1/" &&
+   diff -q merge ../merge &&
+   ( echo Line 0; cat merge ) >merge.tmp &&
+   mv merge.tmp merge &&
+   cd "$WORKDIR" &&
+   echo Line 8 >>merge &&
+   git add merge &&
+   git commit -q -m "Merge test (merge)" &&
+   git push gitcvs.git >/dev/null &&
+   cd cvswork &&
+   sleep 1 && touch merge &&
+   GIT_CONFIG="$git_config" cvs -Q update &&
+   diff -q merge ../expected'
+
+cd "$WORKDIR"
+
+cat >expected.C <<EOF
+<<<<<<< merge.mine
+Line 0
+=======
+LINE 0
+>>>>>>> merge.3
+EOF
+
+for i in 1 2 3 4 5 6 7 8
+do
+  echo Line $i >>expected.C
+done
+
+test_expect_success 'cvs update (conflict merge)' \
+  '( echo LINE 0; cat merge ) >merge.tmp &&
+   mv merge.tmp merge &&
+   git add merge &&
+   git commit -q -m "Merge test (conflict)" &&
+   git push gitcvs.git >/dev/null &&
+   cd cvswork &&
+   GIT_CONFIG="$git_config" cvs -Q update &&
+   diff -q merge ../expected.C'
+
+cd "$WORKDIR"
+test_expect_success 'cvs update (-C)' \
+  'cd cvswork &&
+   GIT_CONFIG="$git_config" cvs -Q update -C &&
+   diff -q merge ../merge'
+
+cd "$WORKDIR"
+test_expect_success 'cvs update (merge no-op)' \
+   'echo Line 9 >>merge &&
+    cp merge cvswork/merge &&
+    git add merge &&
+    git commit -q -m "Merge test (no-op)" &&
+    git push gitcvs.git >/dev/null &&
+    cd cvswork &&
+    sleep 1 && touch merge &&
+    GIT_CONFIG="$git_config" cvs -Q update &&
+    diff -q merge ../merge'
+
+test_done
diff --git a/t/t9500-gitweb-standalone-no-errors.sh b/t/t9500-gitweb-standalone-no-errors.sh
new file mode 100755 (executable)
index 0000000..35fff3d
--- /dev/null
@@ -0,0 +1,583 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Jakub Narebski
+#
+
+test_description='gitweb as standalone script (basic tests).
+
+This test runs gitweb (git web interface) as CGI script from
+commandline, and checks that it would not write any errors
+or warnings to log.'
+
+gitweb_init () {
+       cat >gitweb_config.perl <<EOF
+#!/usr/bin/perl
+
+# gitweb configuration for tests
+
+our \$version = "current";
+our \$GIT = "git";
+our \$projectroot = "$(pwd)";
+our \$project_maxdepth = 8;
+our \$home_link_str = "projects";
+our \$site_name = "[localhost]";
+our \$site_header = "";
+our \$site_footer = "";
+our \$home_text = "indextext.html";
+our @stylesheets = ("file:///$(pwd)/../../gitweb/gitweb.css");
+our \$logo = "file:///$(pwd)/../../gitweb/git-logo.png";
+our \$favicon = "file:///$(pwd)/../../gitweb/git-favicon.png";
+our \$projects_list = "";
+our \$export_ok = "";
+our \$strict_export = "";
+
+EOF
+
+       cat >.git/description <<EOF
+$0 test repository
+EOF
+}
+
+gitweb_run () {
+       export GATEWAY_INTERFACE="CGI/1.1"
+       export HTTP_ACCEPT="*/*"
+       export REQUEST_METHOD="GET"
+       export QUERY_STRING=""$1""
+       export PATH_INFO=""$2""
+
+       export GITWEB_CONFIG=$(pwd)/gitweb_config.perl
+
+       # some of git commands write to STDERR on error, but this is not
+       # written to web server logs, so we are not interested in that:
+       # we are interested only in properly formatted errors/warnings
+       rm -f gitweb.log &&
+       perl -- $(pwd)/../../gitweb/gitweb.perl \
+               >/dev/null 2>gitweb.log &&
+       if grep -q -s "^[[]" gitweb.log >/dev/null; then false; else true; fi
+
+       # gitweb.log is left for debugging
+}
+
+safe_chmod () {
+       chmod "$1" "$2" &&
+       if [ "$(git config --get core.filemode)" = false ]
+       then
+               git update-index --chmod="$1" "$2"
+       fi
+}
+
+. ./test-lib.sh
+
+perl -MEncode -e 'decode_utf8("", Encode::FB_CROAK)' >/dev/null 2>&1 || {
+    test_expect_success 'skipping gitweb tests, perl version is too old' :
+    test_done
+    exit
+}
+
+gitweb_init
+
+# ----------------------------------------------------------------------
+# no commits (empty, just initialized repository)
+
+test_expect_success \
+       'no commits: projects_list (implicit)' \
+       'gitweb_run'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'no commits: projects_index' \
+       'gitweb_run "a=project_index"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'no commits: .git summary (implicit)' \
+       'gitweb_run "p=.git"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'no commits: .git commit (implicit HEAD)' \
+       'gitweb_run "p=.git;a=commit"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'no commits: .git commitdiff (implicit HEAD)' \
+       'gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'no commits: .git tree (implicit HEAD)' \
+       'gitweb_run "p=.git;a=tree"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'no commits: .git heads' \
+       'gitweb_run "p=.git;a=heads"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'no commits: .git tags' \
+       'gitweb_run "p=.git;a=tags"'
+test_debug 'cat gitweb.log'
+
+
+# ----------------------------------------------------------------------
+# initial commit
+
+test_expect_success \
+       'Make initial commit' \
+       'echo "Not an empty file." > file &&
+        git add file &&
+        git commit -a -m "Initial commit." &&
+        git branch b'
+
+test_expect_success \
+       'projects_list (implicit)' \
+       'gitweb_run'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'projects_index' \
+       'gitweb_run "a=project_index"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git summary (implicit)' \
+       'gitweb_run "p=.git"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git commit (implicit HEAD)' \
+       'gitweb_run "p=.git;a=commit"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git commitdiff (implicit HEAD, root commit)' \
+       'gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git commitdiff_plain (implicit HEAD, root commit)' \
+       'gitweb_run "p=.git;a=commitdiff_plain"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git commit (HEAD)' \
+       'gitweb_run "p=.git;a=commit;h=HEAD"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git tree (implicit HEAD)' \
+       'gitweb_run "p=.git;a=tree"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git blob (file)' \
+       'gitweb_run "p=.git;a=blob;f=file"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git blob_plain (file)' \
+       'gitweb_run "p=.git;a=blob_plain;f=file"'
+test_debug 'cat gitweb.log'
+
+# ----------------------------------------------------------------------
+# nonexistent objects
+
+test_expect_success \
+       '.git commit (non-existent)' \
+       'gitweb_run "p=.git;a=commit;h=non-existent"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git commitdiff (non-existent)' \
+       'gitweb_run "p=.git;a=commitdiff;h=non-existent"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git commitdiff (non-existent vs HEAD)' \
+       'gitweb_run "p=.git;a=commitdiff;hp=non-existent;h=HEAD"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git tree (0000000000000000000000000000000000000000)' \
+       'gitweb_run "p=.git;a=tree;h=0000000000000000000000000000000000000000"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git tag (0000000000000000000000000000000000000000)' \
+       'gitweb_run "p=.git;a=tag;h=0000000000000000000000000000000000000000"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git blob (non-existent)' \
+       'gitweb_run "p=.git;a=blob;f=non-existent"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       '.git blob_plain (non-existent)' \
+       'gitweb_run "p=.git;a=blob_plain;f=non-existent"'
+test_debug 'cat gitweb.log'
+
+
+# ----------------------------------------------------------------------
+# commitdiff testing (implicit, one implicit tree-ish)
+
+test_expect_success \
+       'commitdiff(0): root' \
+       'gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): file added' \
+       'echo "New file" > new_file &&
+        git add new_file &&
+        git commit -a -m "File added." &&
+        gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): mode change' \
+       'safe_chmod +x new_file &&
+        git commit -a -m "Mode changed." &&
+        gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): file renamed' \
+       'git mv new_file renamed_file &&
+        git commit -a -m "File renamed." &&
+        gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): file to symlink' \
+       'rm renamed_file &&
+        ln -s file renamed_file &&
+        git commit -a -m "File to symlink." &&
+        gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): file deleted' \
+       'git rm renamed_file &&
+        rm -f renamed_file &&
+        git commit -a -m "File removed." &&
+        gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): file copied / new file' \
+       'cp file file2 &&
+        git add file2 &&
+        git commit -a -m "File copied." &&
+        gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): mode change and modified' \
+       'echo "New line" >> file2 &&
+        safe_chmod +x file2 &&
+        git commit -a -m "Mode change and modification." &&
+        gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): renamed and modified' \
+       'cat >file2<<EOF &&
+Dominus regit me,
+et nihil mihi deerit.
+In loco pascuae ibi me collocavit,
+super aquam refectionis educavit me;
+animam meam convertit,
+deduxit me super semitas jusitiae,
+propter nomen suum.
+EOF
+        git commit -a -m "File added." &&
+        git mv file2 file3 &&
+        echo "Propter nomen suum." >> file3 &&
+        git commit -a -m "File rename and modification." &&
+        gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): renamed, mode change and modified' \
+       'git mv file3 file2 &&
+        echo "Propter nomen suum." >> file2 &&
+        safe_chmod +x file2 &&
+        git commit -a -m "File rename, mode change and modification." &&
+        gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+# ----------------------------------------------------------------------
+# commitdiff testing (taken from t4114-apply-typechange.sh)
+
+test_expect_success 'setup typechange commits' '
+       echo "hello world" > foo &&
+       echo "hi planet" > bar &&
+       git update-index --add foo bar &&
+       git commit -m initial &&
+       git branch initial &&
+       rm -f foo &&
+       ln -s bar foo &&
+       git update-index foo &&
+       git commit -m "foo symlinked to bar" &&
+       git branch foo-symlinked-to-bar &&
+       rm -f foo &&
+       echo "how far is the sun?" > foo &&
+       git update-index foo &&
+       git commit -m "foo back to file" &&
+       git branch foo-back-to-file &&
+       rm -f foo &&
+       git update-index --remove foo &&
+       mkdir foo &&
+       echo "if only I knew" > foo/baz &&
+       git update-index --add foo/baz &&
+       git commit -m "foo becomes a directory" &&
+       git branch "foo-becomes-a-directory" &&
+       echo "hello world" > foo/baz &&
+       git update-index foo/baz &&
+       git commit -m "foo/baz is the original foo" &&
+       git branch foo-baz-renamed-from-foo
+       '
+
+test_expect_success \
+       'commitdiff(2): file renamed from foo to foo/baz' \
+       'gitweb_run "p=.git;a=commitdiff;hp=initial;h=foo-baz-renamed-from-foo"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(2): file renamed from foo/baz to foo' \
+       'gitweb_run "p=.git;a=commitdiff;hp=foo-baz-renamed-from-foo;h=initial"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(2): directory becomes file' \
+       'gitweb_run "p=.git;a=commitdiff;hp=foo-becomes-a-directory;h=initial"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(2): file becomes directory' \
+       'gitweb_run "p=.git;a=commitdiff;hp=initial;h=foo-becomes-a-directory"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(2): file becomes symlink' \
+       'gitweb_run "p=.git;a=commitdiff;hp=initial;h=foo-symlinked-to-bar"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(2): symlink becomes file' \
+       'gitweb_run "p=.git;a=commitdiff;hp=foo-symlinked-to-bar;h=foo-back-to-file"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(2): symlink becomes directory' \
+       'gitweb_run "p=.git;a=commitdiff;hp=foo-symlinked-to-bar;h=foo-becomes-a-directory"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(2): directory becomes symlink' \
+       'gitweb_run "p=.git;a=commitdiff;hp=foo-becomes-a-directory;h=foo-symlinked-to-bar"'
+test_debug 'cat gitweb.log'
+
+# ----------------------------------------------------------------------
+# commit, commitdiff: merge, large
+test_expect_success \
+       'Create a merge' \
+       'git checkout b &&
+        echo "Branch" >> b &&
+        git add b &&
+        git commit -a -m "On branch" &&
+        git checkout master &&
+        git pull . b'
+
+test_expect_success \
+       'commit(0): merge commit' \
+       'gitweb_run "p=.git;a=commit"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(0): merge commit' \
+       'gitweb_run "p=.git;a=commitdiff"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'Prepare large commit' \
+       'git checkout b &&
+        echo "To be changed" > 01-change &&
+        echo "To be renamed" > 02-pure-rename-from &&
+        echo "To be deleted" > 03-delete &&
+        echo "To be renamed and changed" > 04-rename-from &&
+        echo "To have mode changed" > 05-mode-change &&
+        echo "File to symlink" > 06-file-or-symlink &&
+        echo "To be changed and have mode changed" > 07-change-mode-change     &&
+        git add 0* &&
+        git commit -a -m "Prepare large commit" &&
+        echo "Changed" > 01-change &&
+        git mv 02-pure-rename-from 02-pure-rename-to &&
+        git rm 03-delete && rm -f 03-delete &&
+        echo "A new file" > 03-new &&
+        git add 03-new &&
+        git mv 04-rename-from 04-rename-to &&
+        echo "Changed" >> 04-rename-to &&
+        safe_chmod +x 05-mode-change &&
+        rm -f 06-file-or-symlink && ln -s 01-change 06-file-or-symlink &&
+        echo "Changed and have mode changed" > 07-change-mode-change   &&
+        safe_chmod +x 07-change-mode-change &&
+        git commit -a -m "Large commit" &&
+        git checkout master'
+
+test_expect_success \
+       'commit(1): large commit' \
+       'gitweb_run "p=.git;a=commit;h=b"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'commitdiff(1): large commit' \
+       'gitweb_run "p=.git;a=commitdiff;h=b"'
+test_debug 'cat gitweb.log'
+
+# ----------------------------------------------------------------------
+# tags testing
+
+test_expect_success \
+       'tags: list of different types of tags' \
+       'git checkout master &&
+        git tag -a -m "Tag commit object" tag-commit HEAD &&
+        git tag -a -m "" tag-commit-nomessage HEAD &&
+        git tag -a -m "Tag tag object" tag-tag tag-commit &&
+        git tag -a -m "Tag tree object" tag-tree HEAD^{tree} &&
+        git tag -a -m "Tag blob object" tag-blob HEAD:file &&
+        git tag lightweight/tag-commit HEAD &&
+        git tag lightweight/tag-tag tag-commit &&
+        git tag lightweight/tag-tree HEAD^{tree} &&
+        git tag lightweight/tag-blob HEAD:file &&
+        gitweb_run "p=.git;a=tags"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'tag: Tag to commit object' \
+       'gitweb_run "p=.git;a=tag;h=tag-commit"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'tag: on lightweight tag (invalid)' \
+       'gitweb_run "p=.git;a=tag;h=lightweight/tag-commit"'
+test_debug 'cat gitweb.log'
+
+# ----------------------------------------------------------------------
+# logs
+
+test_expect_success \
+       'logs: log (implicit HEAD)' \
+       'gitweb_run "p=.git;a=log"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'logs: shortlog (implicit HEAD)' \
+       'gitweb_run "p=.git;a=shortlog"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'logs: history (implicit HEAD, file)' \
+       'gitweb_run "p=.git;a=history;f=file"'
+test_debug 'cat gitweb.log'
+
+# ----------------------------------------------------------------------
+# feed generation
+
+test_expect_success \
+       'feeds: OPML' \
+       'gitweb_run "a=opml"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'feed: RSS' \
+       'gitweb_run "p=.git;a=rss"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'feed: Atom' \
+       'gitweb_run "p=.git;a=atom"'
+test_debug 'cat gitweb.log'
+
+# ----------------------------------------------------------------------
+# encoding/decoding
+
+test_expect_success \
+       'encode(commit): utf8' \
+       '. ../t3901-utf8.txt &&
+        echo "UTF-8" >> file &&
+        git add file &&
+        git commit -F ../t3900/1-UTF-8.txt &&
+        gitweb_run "p=.git;a=commit"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'encode(commit): iso-8859-1' \
+       '. ../t3901-8859-1.txt &&
+        echo "ISO-8859-1" >> file &&
+        git add file &&
+        git config i18n.commitencoding ISO-8859-1 &&
+        git commit -F ../t3900/ISO-8859-1.txt &&
+        git config --unset i18n.commitencoding &&
+        gitweb_run "p=.git;a=commit"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'encode(log): utf-8 and iso-8859-1' \
+       'gitweb_run "p=.git;a=log"'
+test_debug 'cat gitweb.log'
+
+# ----------------------------------------------------------------------
+# extra options
+
+test_expect_success \
+       'opt: log --no-merges' \
+       'gitweb_run "p=.git;a=log;opt=--no-merges"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'opt: atom --no-merges' \
+       'gitweb_run "p=.git;a=log;opt=--no-merges"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'opt: "file" history --no-merges' \
+       'gitweb_run "p=.git;a=history;f=file;opt=--no-merges"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'opt: log --no-such-option (invalid option)' \
+       'gitweb_run "p=.git;a=log;opt=--no-such-option"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'opt: tree --no-merges (invalid option for action)' \
+       'gitweb_run "p=.git;a=tree;opt=--no-merges"'
+test_debug 'cat gitweb.log'
+
+# ----------------------------------------------------------------------
+# gitweb config and repo config
+
+cat >>gitweb_config.perl <<EOF
+
+\$feature{'blame'}{'override'} = 1;
+\$feature{'snapshot'}{'override'} = 1;
+EOF
+
+test_expect_success \
+       'config override: tree view, features disabled in repo config' \
+       'git config gitweb.blame no &&
+        git config gitweb.snapshot none &&
+        gitweb_run "p=.git;a=tree"'
+test_debug 'cat gitweb.log'
+
+test_expect_success \
+       'config override: tree view, features enabled in repo config' \
+       'git config gitweb.blame yes &&
+        git config gitweb.snapshot "zip,tgz, tbz2" &&
+        gitweb_run "p=.git;a=tree"'
+test_debug 'cat gitweb.log'
+
+test_done
diff --git a/t/t9600-cvsimport.sh b/t/t9600-cvsimport.sh
new file mode 100755 (executable)
index 0000000..7706430
--- /dev/null
@@ -0,0 +1,150 @@
+#!/bin/sh
+
+test_description='git-cvsimport basic tests'
+. ./test-lib.sh
+
+if ! type cvs >/dev/null 2>&1
+then
+       say 'skipping cvsimport tests, cvs not found'
+       test_done
+       exit
+fi
+
+cvsps_version=`cvsps -h 2>&1 | sed -ne 's/cvsps version //p'`
+case "$cvsps_version" in
+2.1)
+       ;;
+'')
+       say 'skipping cvsimport tests, cvsps not found'
+       test_done
+       exit
+       ;;
+*)
+       say 'skipping cvsimport tests, cvsps too old'
+       test_done
+       exit
+       ;;
+esac
+
+CVSROOT=$(pwd)/cvsroot
+export CVSROOT
+# for clean cvsps cache
+HOME=$(pwd)
+export HOME
+
+test_expect_success 'setup cvsroot' 'cvs init'
+
+test_expect_success 'setup a cvs module' '
+
+       mkdir $CVSROOT/module &&
+       cvs co -d module-cvs module &&
+       cd module-cvs &&
+       cat <<EOF >o_fortuna &&
+O Fortuna
+velut luna
+statu variabilis,
+
+semper crescis
+aut decrescis;
+vita detestabilis
+
+nunc obdurat
+et tunc curat
+ludo mentis aciem,
+
+egestatem,
+potestatem
+dissolvit ut glaciem.
+EOF
+       cvs add o_fortuna &&
+       cat <<EOF >message &&
+add "O Fortuna" lyrics
+
+These public domain lyrics make an excellent sample text.
+EOF
+       cvs commit -F message &&
+       cd ..
+'
+
+test_expect_success 'import a trivial module' '
+
+       git cvsimport -a -z 0 -C module-git module &&
+       git diff module-cvs/o_fortuna module-git/o_fortuna
+
+'
+
+test_expect_success 'pack refs' 'cd module-git && git gc && cd ..'
+
+test_expect_success 'update cvs module' '
+
+       cd module-cvs &&
+       cat <<EOF >o_fortuna &&
+O Fortune,
+like the moon
+you are changeable,
+
+ever waxing
+and waning;
+hateful life
+
+first oppresses
+and then soothes
+as fancy takes it;
+
+poverty
+and power
+it melts them like ice.
+EOF
+       cat <<EOF >message &&
+translate to English
+
+My Latin is terrible.
+EOF
+       cvs commit -F message &&
+       cd ..
+'
+
+test_expect_success 'update git module' '
+
+       cd module-git &&
+       git cvsimport -a -z 0 module &&
+       git merge origin &&
+       cd .. &&
+       git diff module-cvs/o_fortuna module-git/o_fortuna
+
+'
+
+test_expect_success 'update cvs module' '
+
+       cd module-cvs &&
+               echo 1 >tick &&
+               cvs add tick &&
+               cvs commit -m 1
+       cd ..
+
+'
+
+test_expect_success 'cvsimport.module config works' '
+
+       cd module-git &&
+               git config cvsimport.module module &&
+               git cvsimport -a -z0 &&
+               git merge origin &&
+       cd .. &&
+       git diff module-cvs/tick module-git/tick
+
+'
+
+test_expect_success 'import from a CVS working tree' '
+
+       cvs co -d import-from-wt module &&
+       cd import-from-wt &&
+               git cvsimport -a -z0 &&
+               echo 1 >expect &&
+               git log -1 --pretty=format:%s%n >actual &&
+               git diff actual expect &&
+       cd ..
+
+'
+
+test_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
new file mode 100644 (file)
index 0000000..90b6844
--- /dev/null
@@ -0,0 +1,340 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+# For repeatability, reset the environment to known value.
+LANG=C
+LC_ALL=C
+PAGER=cat
+TZ=UTC
+export LANG LC_ALL PAGER TZ
+EDITOR=:
+VISUAL=:
+unset GIT_EDITOR
+unset AUTHOR_DATE
+unset AUTHOR_EMAIL
+unset AUTHOR_NAME
+unset COMMIT_AUTHOR_EMAIL
+unset COMMIT_AUTHOR_NAME
+unset EMAIL
+unset GIT_ALTERNATE_OBJECT_DIRECTORIES
+unset GIT_AUTHOR_DATE
+GIT_AUTHOR_EMAIL=author@example.com
+GIT_AUTHOR_NAME='A U Thor'
+unset GIT_COMMITTER_DATE
+GIT_COMMITTER_EMAIL=committer@example.com
+GIT_COMMITTER_NAME='C O Mitter'
+unset GIT_DIFF_OPTS
+unset GIT_DIR
+unset GIT_WORK_TREE
+unset GIT_EXTERNAL_DIFF
+unset GIT_INDEX_FILE
+unset GIT_OBJECT_DIRECTORY
+unset SHA1_FILE_DIRECTORIES
+unset SHA1_FILE_DIRECTORY
+GIT_MERGE_VERBOSITY=5
+export GIT_MERGE_VERBOSITY
+export GIT_AUTHOR_EMAIL GIT_AUTHOR_NAME
+export GIT_COMMITTER_EMAIL GIT_COMMITTER_NAME
+export EDITOR VISUAL
+
+# Protect ourselves from common misconfiguration to export
+# CDPATH into the environment
+unset CDPATH
+
+case $(echo $GIT_TRACE |tr "[A-Z]" "[a-z]") in
+       1|2|true)
+               echo "* warning: Some tests will not work if GIT_TRACE" \
+                       "is set as to trace on STDERR ! *"
+               echo "* warning: Please set GIT_TRACE to something" \
+                       "other than 1, 2 or true ! *"
+               ;;
+esac
+
+# Each test should start with something like this, after copyright notices:
+#
+# test_description='Description of this test...
+# This test checks if command xyzzy does the right thing...
+# '
+# . ./test-lib.sh
+
+[ "x$TERM" != "xdumb" ] &&
+       [ -t 1 ] &&
+       tput bold >/dev/null 2>&1 &&
+       tput setaf 1 >/dev/null 2>&1 &&
+       tput sgr0 >/dev/null 2>&1 &&
+       color=t
+
+while test "$#" -ne 0
+do
+       case "$1" in
+       -d|--d|--de|--deb|--debu|--debug)
+               debug=t; shift ;;
+       -i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
+               immediate=t; shift ;;
+       -h|--h|--he|--hel|--help)
+               help=t; shift ;;
+       -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
+               verbose=t; shift ;;
+       -q|--q|--qu|--qui|--quie|--quiet)
+               quiet=t; shift ;;
+       --no-color)
+           color=; shift ;;
+       --no-python)
+               # noop now...
+               shift ;;
+       *)
+               break ;;
+       esac
+done
+
+if test -n "$color"; then
+       say_color () {
+               case "$1" in
+                       error) tput bold; tput setaf 1;; # bold red
+                       skip)  tput bold; tput setaf 2;; # bold green
+                       pass)  tput setaf 2;;            # green
+                       info)  tput setaf 3;;            # brown
+                       *) test -n "$quiet" && return;;
+               esac
+               shift
+               echo "* $*"
+               tput sgr0
+       }
+else
+       say_color() {
+               test -z "$1" && test -n "$quiet" && return
+               shift
+               echo "* $*"
+       }
+fi
+
+error () {
+       say_color error "error: $*"
+       trap - exit
+       exit 1
+}
+
+say () {
+       say_color info "$*"
+}
+
+test "${test_description}" != "" ||
+error "Test script did not set test_description."
+
+if test "$help" = "t"
+then
+       echo "$test_description"
+       exit 0
+fi
+
+exec 5>&1
+if test "$verbose" = "t"
+then
+       exec 4>&2 3>&1
+else
+       exec 4>/dev/null 3>/dev/null
+fi
+
+test_failure=0
+test_count=0
+
+trap 'echo >&5 "FATAL: Unexpected exit with code $?"; exit 1' exit
+
+test_tick () {
+       if test -z "${test_tick+set}"
+       then
+               test_tick=1112911993
+       else
+               test_tick=$(($test_tick + 60))
+       fi
+       GIT_COMMITTER_DATE="$test_tick -0700"
+       GIT_AUTHOR_DATE="$test_tick -0700"
+       export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
+}
+
+# You are not expected to call test_ok_ and test_failure_ directly, use
+# the text_expect_* functions instead.
+
+test_ok_ () {
+       test_count=$(expr "$test_count" + 1)
+       say_color "" "  ok $test_count: $@"
+}
+
+test_failure_ () {
+       test_count=$(expr "$test_count" + 1)
+       test_failure=$(expr "$test_failure" + 1);
+       say_color error "FAIL $test_count: $1"
+       shift
+       echo "$@" | sed -e 's/^/        /'
+       test "$immediate" = "" || { trap - exit; exit 1; }
+}
+
+
+test_debug () {
+       test "$debug" = "" || eval "$1"
+}
+
+test_run_ () {
+       eval >&3 2>&4 "$1"
+       eval_ret="$?"
+       return 0
+}
+
+test_skip () {
+       this_test=$(expr "./$0" : '.*/\(t[0-9]*\)-[^/]*$')
+       this_test="$this_test.$(expr "$test_count" + 1)"
+       to_skip=
+       for skp in $GIT_SKIP_TESTS
+       do
+               case "$this_test" in
+               $skp)
+                       to_skip=t
+               esac
+       done
+       case "$to_skip" in
+       t)
+               say_color skip >&3 "skipping test: $@"
+               test_count=$(expr "$test_count" + 1)
+               say_color skip "skip $test_count: $1"
+               : true
+               ;;
+       *)
+               false
+               ;;
+       esac
+}
+
+test_expect_failure () {
+       test "$#" = 2 ||
+       error "bug in the test script: not 2 parameters to test-expect-failure"
+       if ! test_skip "$@"
+       then
+               say >&3 "expecting failure: $2"
+               test_run_ "$2"
+               if [ "$?" = 0 -a "$eval_ret" != 0 -a "$eval_ret" -lt 129 ]
+               then
+                       test_ok_ "$1"
+               else
+                       test_failure_ "$@"
+               fi
+       fi
+       echo >&3 ""
+}
+
+test_expect_success () {
+       test "$#" = 2 ||
+       error "bug in the test script: not 2 parameters to test-expect-success"
+       if ! test_skip "$@"
+       then
+               say >&3 "expecting success: $2"
+               test_run_ "$2"
+               if [ "$?" = 0 -a "$eval_ret" = 0 ]
+               then
+                       test_ok_ "$1"
+               else
+                       test_failure_ "$@"
+               fi
+       fi
+       echo >&3 ""
+}
+
+test_expect_code () {
+       test "$#" = 3 ||
+       error "bug in the test script: not 3 parameters to test-expect-code"
+       if ! test_skip "$@"
+       then
+               say >&3 "expecting exit code $1: $3"
+               test_run_ "$3"
+               if [ "$?" = 0 -a "$eval_ret" = "$1" ]
+               then
+                       test_ok_ "$2"
+               else
+                       test_failure_ "$@"
+               fi
+       fi
+       echo >&3 ""
+}
+
+# Most tests can use the created repository, but some amy need to create more.
+# Usage: test_create_repo <directory>
+test_create_repo () {
+       test "$#" = 1 ||
+       error "bug in the test script: not 1 parameter to test-create-repo"
+       owd=`pwd`
+       repo="$1"
+       mkdir "$repo"
+       cd "$repo" || error "Cannot setup test environment"
+       "$GIT_EXEC_PATH/git" init --template=$GIT_EXEC_PATH/templates/blt/ >/dev/null 2>&1 ||
+       error "cannot run git init -- have you built things yet?"
+       mv .git/hooks .git/hooks-disabled
+       cd "$owd"
+}
+
+test_done () {
+       trap - exit
+       case "$test_failure" in
+       0)
+               # We could:
+               # cd .. && rm -fr trash
+               # but that means we forbid any tests that use their own
+               # subdirectory from calling test_done without coming back
+               # to where they started from.
+               # The Makefile provided will clean this test area so
+               # we will leave things as they are.
+
+               say_color pass "passed all $test_count test(s)"
+               exit 0 ;;
+
+       *)
+               say_color error "failed $test_failure among $test_count test(s)"
+               exit 1 ;;
+
+       esac
+}
+
+# Test the binaries we have just built.  The tests are kept in
+# t/ subdirectory and are run in trash subdirectory.
+PATH=$(pwd)/..:$PATH
+GIT_EXEC_PATH=$(pwd)/..
+GIT_TEMPLATE_DIR=$(pwd)/../templates/blt
+GIT_CONFIG=.git/config
+export PATH GIT_EXEC_PATH GIT_TEMPLATE_DIR GIT_CONFIG
+
+GITPERLLIB=$(pwd)/../perl/blib/lib:$(pwd)/../perl/blib/arch/auto/Git
+export GITPERLLIB
+test -d ../templates/blt || {
+       error "You haven't built things yet, have you?"
+}
+
+if ! test -x ../test-chmtime; then
+       echo >&2 'You need to build test-chmtime:'
+       echo >&2 'Run "make test-chmtime" in the source (toplevel) directory'
+       exit 1
+fi
+
+# Test repository
+test=trash
+rm -fr "$test"
+test_create_repo $test
+cd "$test"
+
+this_test=$(expr "./$0" : '.*/\(t[0-9]*\)-[^/]*$')
+for skp in $GIT_SKIP_TESTS
+do
+       to_skip=
+       for skp in $GIT_SKIP_TESTS
+       do
+               case "$this_test" in
+               $skp)
+                       to_skip=t
+               esac
+       done
+       case "$to_skip" in
+       t)
+               say_color skip >&3 "skipping test $this_test altogether"
+               say_color skip "skip all tests in $this_test"
+               test_done
+       esac
+done
diff --git a/t/test4012.png b/t/test4012.png
new file mode 100644 (file)
index 0000000..7b181d1
Binary files /dev/null and b/t/test4012.png differ
diff --git a/t/test9200a.png b/t/test9200a.png
new file mode 100644 (file)
index 0000000..7b181d1
Binary files /dev/null and b/t/test9200a.png differ
diff --git a/t/test9200b.png b/t/test9200b.png
new file mode 100644 (file)
index 0000000..ac22ccb
Binary files /dev/null and b/t/test9200b.png differ
diff --git a/tag.c b/tag.c
new file mode 100644 (file)
index 0000000..f62bcdd
--- /dev/null
+++ b/tag.c
@@ -0,0 +1,116 @@
+#include "cache.h"
+#include "tag.h"
+#include "commit.h"
+#include "tree.h"
+#include "blob.h"
+
+const char *tag_type = "tag";
+
+struct object *deref_tag(struct object *o, const char *warn, int warnlen)
+{
+       while (o && o->type == OBJ_TAG)
+               o = parse_object(((struct tag *)o)->tagged->sha1);
+       if (!o && warn) {
+               if (!warnlen)
+                       warnlen = strlen(warn);
+               error("missing object referenced by '%.*s'", warnlen, warn);
+       }
+       return o;
+}
+
+struct tag *lookup_tag(const unsigned char *sha1)
+{
+       struct object *obj = lookup_object(sha1);
+       if (!obj)
+               return create_object(sha1, OBJ_TAG, alloc_tag_node());
+       if (!obj->type)
+               obj->type = OBJ_TAG;
+        if (obj->type != OBJ_TAG) {
+                error("Object %s is a %s, not a tag",
+                      sha1_to_hex(sha1), typename(obj->type));
+                return NULL;
+        }
+        return (struct tag *) obj;
+}
+
+int parse_tag_buffer(struct tag *item, void *data, unsigned long size)
+{
+       int typelen, taglen;
+       unsigned char sha1[20];
+       const char *type_line, *tag_line, *sig_line;
+       char type[20];
+
+        if (item->object.parsed)
+                return 0;
+        item->object.parsed = 1;
+
+       if (size < 64)
+               return -1;
+       if (memcmp("object ", data, 7) || get_sha1_hex((char *) data + 7, sha1))
+               return -1;
+
+       type_line = (char *) data + 48;
+       if (memcmp("\ntype ", type_line-1, 6))
+               return -1;
+
+       tag_line = strchr(type_line, '\n');
+       if (!tag_line || memcmp("tag ", ++tag_line, 4))
+               return -1;
+
+       sig_line = strchr(tag_line, '\n');
+       if (!sig_line)
+               return -1;
+       sig_line++;
+
+       typelen = tag_line - type_line - strlen("type \n");
+       if (typelen >= 20)
+               return -1;
+       memcpy(type, type_line + 5, typelen);
+       type[typelen] = '\0';
+       taglen = sig_line - tag_line - strlen("tag \n");
+       item->tag = xmemdupz(tag_line + 4, taglen);
+
+       if (!strcmp(type, blob_type)) {
+               item->tagged = &lookup_blob(sha1)->object;
+       } else if (!strcmp(type, tree_type)) {
+               item->tagged = &lookup_tree(sha1)->object;
+       } else if (!strcmp(type, commit_type)) {
+               item->tagged = &lookup_commit(sha1)->object;
+       } else if (!strcmp(type, tag_type)) {
+               item->tagged = &lookup_tag(sha1)->object;
+       } else {
+               error("Unknown type %s", type);
+               item->tagged = NULL;
+       }
+
+       if (item->tagged && track_object_refs) {
+               struct object_refs *refs = alloc_object_refs(1);
+               refs->ref[0] = item->tagged;
+               set_object_refs(&item->object, refs);
+       }
+
+       return 0;
+}
+
+int parse_tag(struct tag *item)
+{
+       enum object_type type;
+       void *data;
+       unsigned long size;
+       int ret;
+
+       if (item->object.parsed)
+               return 0;
+       data = read_sha1_file(item->object.sha1, &type, &size);
+       if (!data)
+               return error("Could not read %s",
+                            sha1_to_hex(item->object.sha1));
+       if (type != OBJ_TAG) {
+               free(data);
+               return error("Object %s not a tag",
+                            sha1_to_hex(item->object.sha1));
+       }
+       ret = parse_tag_buffer(item, data, size);
+       free(data);
+       return ret;
+}
diff --git a/tag.h b/tag.h
new file mode 100644 (file)
index 0000000..7a0cb00
--- /dev/null
+++ b/tag.h
@@ -0,0 +1,20 @@
+#ifndef TAG_H
+#define TAG_H
+
+#include "object.h"
+
+extern const char *tag_type;
+
+struct tag {
+       struct object object;
+       struct object *tagged;
+       char *tag;
+       char *signature; /* not actually implemented */
+};
+
+extern struct tag *lookup_tag(const unsigned char *sha1);
+extern int parse_tag_buffer(struct tag *item, void *data, unsigned long size);
+extern int parse_tag(struct tag *item);
+extern struct object *deref_tag(struct object *, const char *, int);
+
+#endif /* TAG_H */
diff --git a/tar.h b/tar.h
new file mode 100644 (file)
index 0000000..3467705
--- /dev/null
+++ b/tar.h
@@ -0,0 +1,25 @@
+#define TYPEFLAG_AUTO          '\0'
+#define TYPEFLAG_REG           '0'
+#define TYPEFLAG_LNK           '2'
+#define TYPEFLAG_DIR           '5'
+#define TYPEFLAG_GLOBAL_HEADER 'g'
+#define TYPEFLAG_EXT_HEADER    'x'
+
+struct ustar_header {
+       char name[100];         /*   0 */
+       char mode[8];           /* 100 */
+       char uid[8];            /* 108 */
+       char gid[8];            /* 116 */
+       char size[12];          /* 124 */
+       char mtime[12];         /* 136 */
+       char chksum[8];         /* 148 */
+       char typeflag[1];       /* 156 */
+       char linkname[100];     /* 157 */
+       char magic[6];          /* 257 */
+       char version[2];        /* 263 */
+       char uname[32];         /* 265 */
+       char gname[32];         /* 297 */
+       char devmajor[8];       /* 329 */
+       char devminor[8];       /* 337 */
+       char prefix[155];       /* 345 */
+};
diff --git a/templates/.gitignore b/templates/.gitignore
new file mode 100644 (file)
index 0000000..6759ecb
--- /dev/null
@@ -0,0 +1,2 @@
+blt
+boilerplates.made
diff --git a/templates/Makefile b/templates/Makefile
new file mode 100644 (file)
index 0000000..ebd3a62
--- /dev/null
@@ -0,0 +1,51 @@
+# make and install sample templates
+
+ifndef V
+       QUIET = @
+endif
+
+INSTALL ?= install
+TAR ?= tar
+RM ?= rm -f
+prefix ?= $(HOME)
+template_dir ?= $(prefix)/share/git-core/templates
+# DESTDIR=
+
+# Shell quote (do not use $(call) to accommodate ancient setups);
+DESTDIR_SQ = $(subst ','\'',$(DESTDIR))
+template_dir_SQ = $(subst ','\'',$(template_dir))
+
+all: boilerplates.made custom
+
+# Put templates that can be copied straight from the source
+# in a file direc--tory--file in the source.  They will be
+# just copied to the destination.
+
+bpsrc = $(filter-out %~,$(wildcard *--*))
+boilerplates.made : $(bpsrc)
+       $(QUIET)ls *--* 2>/dev/null | \
+       while read boilerplate; \
+       do \
+               case "$$boilerplate" in *~) continue ;; esac && \
+               dst=`echo "$$boilerplate" | sed -e 's|^this|.|;s|--|/|g'` && \
+               dir=`expr "$$dst" : '\(.*\)/'` && \
+               mkdir -p blt/$$dir && \
+               case "$$boilerplate" in \
+               *--) ;; \
+               *) cp $$boilerplate blt/$$dst ;; \
+               esac || exit; \
+       done && \
+       date >$@
+
+# If you need build-tailored templates, build them into blt/
+# directory yourself here.
+custom:
+       $(QUIET): no custom templates yet
+
+clean:
+       $(RM) -r blt boilerplates.made
+
+install: all
+       $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(template_dir_SQ)'
+       (cd blt && $(TAR) cf - .) | \
+       (cd '$(DESTDIR_SQ)$(template_dir_SQ)' && $(TAR) xf -)
diff --git a/templates/branches-- b/templates/branches--
new file mode 100644 (file)
index 0000000..fae8870
--- /dev/null
@@ -0,0 +1 @@
+: this is just to ensure the directory exists.
diff --git a/templates/hooks--applypatch-msg b/templates/hooks--applypatch-msg
new file mode 100644 (file)
index 0000000..02de1ef
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message taken by
+# applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.  The hook is
+# allowed to edit the commit message file.
+#
+# To enable this hook, make this file executable.
+
+. git-sh-setup
+test -x "$GIT_DIR/hooks/commit-msg" &&
+       exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
+:
diff --git a/templates/hooks--commit-msg b/templates/hooks--commit-msg
new file mode 100644 (file)
index 0000000..c5cdb9d
--- /dev/null
@@ -0,0 +1,21 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message.
+# Called by git-commit with one argument, the name of the file
+# that has the commit message.  The hook should exit with non-zero
+# status after issuing an appropriate message if it wants to stop the
+# commit.  The hook is allowed to edit the commit message file.
+#
+# To enable this hook, make this file executable.
+
+# Uncomment the below to add a Signed-off-by line to the message.
+# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
+
+# This example catches duplicate Signed-off-by lines.
+
+test "" = "$(grep '^Signed-off-by: ' "$1" |
+        sort | uniq -c | sed -e '/^[   ]*1[    ]/d')" || {
+       echo >&2 Duplicate Signed-off-by lines.
+       exit 1
+}
diff --git a/templates/hooks--post-commit b/templates/hooks--post-commit
new file mode 100644 (file)
index 0000000..8be6f34
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/sh
+#
+# An example hook script that is called after a successful
+# commit is made.
+#
+# To enable this hook, make this file executable.
+
+: Nothing
diff --git a/templates/hooks--post-receive b/templates/hooks--post-receive
new file mode 100644 (file)
index 0000000..b70c8fd
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/sh
+#
+# An example hook script for the post-receive event
+#
+# This script is run after receive-pack has accepted a pack and the
+# repository has been updated.  It is passed arguments in through stdin
+# in the form
+#  <oldrev> <newrev> <refname>
+# For example:
+#  aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
+#
+# see contrib/hooks/ for an sample, or uncomment the next line (on debian)
+#
+
+
+#. /usr/share/doc/git-core/contrib/hooks/post-receive-email
diff --git a/templates/hooks--post-update b/templates/hooks--post-update
new file mode 100644 (file)
index 0000000..bcba893
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/sh
+#
+# An example hook script to prepare a packed repository for use over
+# dumb transports.
+#
+# To enable this hook, make this file executable by "chmod +x post-update".
+
+exec git-update-server-info
diff --git a/templates/hooks--pre-applypatch b/templates/hooks--pre-applypatch
new file mode 100644 (file)
index 0000000..eeccc93
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed
+# by applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.
+#
+# To enable this hook, make this file executable.
+
+. git-sh-setup
+test -x "$GIT_DIR/hooks/pre-commit" &&
+       exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
+:
diff --git a/templates/hooks--pre-commit b/templates/hooks--pre-commit
new file mode 100644 (file)
index 0000000..7092bae
--- /dev/null
@@ -0,0 +1,70 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by git-commit with no arguments.  The hook should
+# exit with non-zero status after issuing an appropriate message if
+# it wants to stop the commit.
+#
+# To enable this hook, make this file executable.
+
+# This is slightly modified from Andrew Morton's Perfect Patch.
+# Lines you introduce should not have trailing whitespace.
+# Also check for an indentation that has SP before a TAB.
+
+if git-rev-parse --verify HEAD 2>/dev/null
+then
+       git-diff-index -p -M --cached HEAD --
+else
+       # NEEDSWORK: we should produce a diff with an empty tree here
+       # if we want to do the same verification for the initial import.
+       :
+fi |
+perl -e '
+    my $found_bad = 0;
+    my $filename;
+    my $reported_filename = "";
+    my $lineno;
+    sub bad_line {
+       my ($why, $line) = @_;
+       if (!$found_bad) {
+           print STDERR "*\n";
+           print STDERR "* You have some suspicious patch lines:\n";
+           print STDERR "*\n";
+           $found_bad = 1;
+       }
+       if ($reported_filename ne $filename) {
+           print STDERR "* In $filename\n";
+           $reported_filename = $filename;
+       }
+       print STDERR "* $why (line $lineno)\n";
+       print STDERR "$filename:$lineno:$line\n";
+    }
+    while (<>) {
+       if (m|^diff --git a/(.*) b/\1$|) {
+           $filename = $1;
+           next;
+       }
+       if (/^@@ -\S+ \+(\d+)/) {
+           $lineno = $1 - 1;
+           next;
+       }
+       if (/^ /) {
+           $lineno++;
+           next;
+       }
+       if (s/^\+//) {
+           $lineno++;
+           chomp;
+           if (/\s$/) {
+               bad_line("trailing whitespace", $_);
+           }
+           if (/^\s* \t/) {
+               bad_line("indent SP followed by a TAB", $_);
+           }
+           if (/^(?:[<>=]){7}/) {
+               bad_line("unresolved merge conflict", $_);
+           }
+       }
+    }
+    exit($found_bad);
+'
diff --git a/templates/hooks--pre-rebase b/templates/hooks--pre-rebase
new file mode 100644 (file)
index 0000000..981c454
--- /dev/null
@@ -0,0 +1,150 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+publish=next
+basebranch="$1"
+if test "$#" = 2
+then
+       topic="refs/heads/$2"
+else
+       topic=`git symbolic-ref HEAD`
+fi
+
+case "$basebranch,$topic" in
+master,refs/heads/??/*)
+       ;;
+*)
+       exit 0 ;# we do not interrupt others.
+       ;;
+esac
+
+# Now we are dealing with a topic branch being rebased
+# on top of master.  Is it OK to rebase it?
+
+# Is topic fully merged to master?
+not_in_master=`git-rev-list --pretty=oneline ^master "$topic"`
+if test -z "$not_in_master"
+then
+       echo >&2 "$topic is fully merged to master; better remove it."
+       exit 1 ;# we could allow it, but there is no point.
+fi
+
+# Is topic ever merged to next?  If so you should not be rebasing it.
+only_next_1=`git-rev-list ^master "^$topic" ${publish} | sort`
+only_next_2=`git-rev-list ^master           ${publish} | sort`
+if test "$only_next_1" = "$only_next_2"
+then
+       not_in_topic=`git-rev-list "^$topic" master`
+       if test -z "$not_in_topic"
+       then
+               echo >&2 "$topic is already up-to-date with master"
+               exit 1 ;# we could allow it, but there is no point.
+       else
+               exit 0
+       fi
+else
+       not_in_next=`git-rev-list --pretty=oneline ^${publish} "$topic"`
+       perl -e '
+               my $topic = $ARGV[0];
+               my $msg = "* $topic has commits already merged to public branch:\n";
+               my (%not_in_next) = map {
+                       /^([0-9a-f]+) /;
+                       ($1 => 1);
+               } split(/\n/, $ARGV[1]);
+               for my $elem (map {
+                               /^([0-9a-f]+) (.*)$/;
+                               [$1 => $2];
+                       } split(/\n/, $ARGV[2])) {
+                       if (!exists $not_in_next{$elem->[0]}) {
+                               if ($msg) {
+                                       print STDERR $msg;
+                                       undef $msg;
+                               }
+                               print STDERR " $elem->[1]\n";
+                       }
+               }
+       ' "$topic" "$not_in_next" "$not_in_master"
+       exit 1
+fi
+
+exit 0
+
+################################################################
+
+This sample hook safeguards topic branches that have been
+published from being rewound.
+
+The workflow assumed here is:
+
+ * Once a topic branch forks from "master", "master" is never
+   merged into it again (either directly or indirectly).
+
+ * Once a topic branch is fully cooked and merged into "master",
+   it is deleted.  If you need to build on top of it to correct
+   earlier mistakes, a new topic branch is created by forking at
+   the tip of the "master".  This is not strictly necessary, but
+   it makes it easier to keep your history simple.
+
+ * Whenever you need to test or publish your changes to topic
+   branches, merge them into "next" branch.
+
+The script, being an example, hardcodes the publish branch name
+to be "next", but it is trivial to make it configurable via
+$GIT_DIR/config mechanism.
+
+With this workflow, you would want to know:
+
+(1) ... if a topic branch has ever been merged to "next".  Young
+    topic branches can have stupid mistakes you would rather
+    clean up before publishing, and things that have not been
+    merged into other branches can be easily rebased without
+    affecting other people.  But once it is published, you would
+    not want to rewind it.
+
+(2) ... if a topic branch has been fully merged to "master".
+    Then you can delete it.  More importantly, you should not
+    build on top of it -- other people may already want to
+    change things related to the topic as patches against your
+    "master", so if you need further changes, it is better to
+    fork the topic (perhaps with the same name) afresh from the
+    tip of "master".
+
+Let's look at this example:
+
+                  o---o---o---o---o---o---o---o---o---o "next"
+                 /       /           /           /
+                /   a---a---b A     /           /
+               /   /               /           /
+              /   /   c---c---c---c B         /
+             /   /   /             \         /
+            /   /   /   b---b C     \       /
+           /   /   /   /             \     /
+    ---o---o---o---o---o---o---o---o---o---o---o "master"
+
+
+A, B and C are topic branches.
+
+ * A has one fix since it was merged up to "next".
+
+ * B has finished.  It has been fully merged up to "master" and "next",
+   and is ready to be deleted.
+
+ * C has not merged to "next" at all.
+
+We would want to allow C to be rebased, refuse A, and encourage
+B to be deleted.
+
+To compute (1):
+
+       git-rev-list ^master ^topic next
+       git-rev-list ^master        next
+
+       if these match, topic has not merged in next at all.
+
+To compute (2):
+
+       git-rev-list master..topic
+
+       if this is empty, it is fully merged to "master".
diff --git a/templates/hooks--update b/templates/hooks--update
new file mode 100644 (file)
index 0000000..bd93dd1
--- /dev/null
@@ -0,0 +1,107 @@
+#!/bin/sh
+#
+# An example hook script to blocks unannotated tags from entering.
+# Called by git-receive-pack with arguments: refname sha1-old sha1-new
+#
+# To enable this hook, make this file executable by "chmod +x update".
+#
+# Config
+# ------
+# hooks.allowunannotated
+#   This boolean sets whether unannotated tags will be allowed into the
+#   repository.  By default they won't be.
+# hooks.allowdeletetag
+#   This boolean sets whether deleting tags will be allowed in the
+#   repository.  By default they won't be.
+# hooks.allowdeletebranch
+#   This boolean sets whether deleting branches will be allowed in the
+#   repository.  By default they won't be.
+#
+
+# --- Command line
+refname="$1"
+oldrev="$2"
+newrev="$3"
+
+# --- Safety check
+if [ -z "$GIT_DIR" ]; then
+       echo "Don't run this script from the command line." >&2
+       echo " (if you want, you could supply GIT_DIR then run" >&2
+       echo "  $0 <ref> <oldrev> <newrev>)" >&2
+       exit 1
+fi
+
+if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
+       echo "Usage: $0 <ref> <oldrev> <newrev>" >&2
+       exit 1
+fi
+
+# --- Config
+allowunannotated=$(git-repo-config --bool hooks.allowunannotated)
+allowdeletebranch=$(git-repo-config --bool hooks.allowdeletebranch)
+allowdeletetag=$(git-repo-config --bool hooks.allowdeletetag)
+
+# check for no description
+projectdesc=$(sed -e '1q' "$GIT_DIR/description")
+if [ -z "$projectdesc" -o "$projectdesc" = "Unnamed repository; edit this file to name it for gitweb." ]; then
+       echo "*** Project description file hasn't been set" >&2
+       exit 1
+fi
+
+# --- Check types
+# if $newrev is 0000...0000, it's a commit to delete a ref.
+if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
+       newrev_type=delete
+else
+       newrev_type=$(git-cat-file -t $newrev)
+fi
+
+case "$refname","$newrev_type" in
+       refs/tags/*,commit)
+               # un-annotated tag
+               short_refname=${refname##refs/tags/}
+               if [ "$allowunannotated" != "true" ]; then
+                       echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
+                       echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
+                       exit 1
+               fi
+               ;;
+       refs/tags/*,delete)
+               # delete tag
+               if [ "$allowdeletetag" != "true" ]; then
+                       echo "*** Deleting a tag is not allowed in this repository" >&2
+                       exit 1
+               fi
+               ;;
+       refs/tags/*,tag)
+               # annotated tag
+               ;;
+       refs/heads/*,commit)
+               # branch
+               ;;
+       refs/heads/*,delete)
+               # delete branch
+               if [ "$allowdeletebranch" != "true" ]; then
+                       echo "*** Deleting a branch is not allowed in this repository" >&2
+                       exit 1
+               fi
+               ;;
+       refs/remotes/*,commit)
+               # tracking branch
+               ;;
+       refs/remotes/*,delete)
+               # delete tracking branch
+               if [ "$allowdeletebranch" != "true" ]; then
+                       echo "*** Deleting a tracking branch is not allowed in this repository" >&2
+                       exit 1
+               fi
+               ;;
+       *)
+               # Anything else (is there anything else?)
+               echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
+               exit 1
+               ;;
+esac
+
+# --- Finished
+exit 0
diff --git a/templates/info--exclude b/templates/info--exclude
new file mode 100644 (file)
index 0000000..2c87b72
--- /dev/null
@@ -0,0 +1,6 @@
+# git-ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/templates/this--description b/templates/this--description
new file mode 100644 (file)
index 0000000..c6f25e8
--- /dev/null
@@ -0,0 +1 @@
+Unnamed repository; edit this file to name it for gitweb.
diff --git a/test-absolute-path.c b/test-absolute-path.c
new file mode 100644 (file)
index 0000000..c959ea2
--- /dev/null
@@ -0,0 +1,11 @@
+#include "cache.h"
+
+int main(int argc, char **argv)
+{
+       while (argc > 1) {
+               puts(make_absolute_path(argv[1]));
+               argc--;
+               argv++;
+       }
+       return 0;
+}
diff --git a/test-chmtime.c b/test-chmtime.c
new file mode 100644 (file)
index 0000000..90da448
--- /dev/null
@@ -0,0 +1,61 @@
+#include "git-compat-util.h"
+#include <utime.h>
+
+static const char usage_str[] = "(+|=|=+|=-|-)<seconds> <file>...";
+
+int main(int argc, const char *argv[])
+{
+       int i;
+       int set_eq;
+       long int set_time;
+       char *test;
+       const char *timespec;
+
+       if (argc < 3)
+               goto usage;
+
+       timespec = argv[1];
+       set_eq = (*timespec == '=') ? 1 : 0;
+       if (set_eq) {
+               timespec++;
+               if (*timespec == '+') {
+                       set_eq = 2; /* relative "in the future" */
+                       timespec++;
+               }
+       }
+       set_time = strtol(timespec, &test, 10);
+       if (*test) {
+               fprintf(stderr, "Not a base-10 integer: %s\n", argv[1] + 1);
+               goto usage;
+       }
+       if ((set_eq && set_time < 0) || set_eq == 2) {
+               time_t now = time(NULL);
+               set_time += now;
+       }
+
+       for (i = 2; i < argc; i++) {
+               struct stat sb;
+               struct utimbuf utb;
+
+               if (stat(argv[i], &sb) < 0) {
+                       fprintf(stderr, "Failed to stat %s: %s\n",
+                               argv[i], strerror(errno));
+                       return -1;
+               }
+
+               utb.actime = sb.st_atime;
+               utb.modtime = set_eq ? set_time : sb.st_mtime + set_time;
+
+               if (utime(argv[i], &utb) < 0) {
+                       fprintf(stderr, "Failed to modify time on %s: %s\n",
+                               argv[i], strerror(errno));
+                       return -1;
+               }
+       }
+
+       return 0;
+
+usage:
+       fprintf(stderr, "Usage: %s %s\n", argv[0], usage_str);
+       return -1;
+}
diff --git a/test-date.c b/test-date.c
new file mode 100644 (file)
index 0000000..62e8f23
--- /dev/null
@@ -0,0 +1,20 @@
+#include "cache.h"
+
+int main(int argc, char **argv)
+{
+       int i;
+
+       for (i = 1; i < argc; i++) {
+               char result[100];
+               time_t t;
+
+               memcpy(result, "bad", 4);
+               parse_date(argv[i], result, sizeof(result));
+               t = strtoul(result, NULL, 0);
+               printf("%s -> %s -> %s", argv[i], result, ctime(&t));
+
+               t = approxidate(argv[i]);
+               printf("%s -> %s\n", argv[i], ctime(&t));
+       }
+       return 0;
+}
diff --git a/test-delta.c b/test-delta.c
new file mode 100644 (file)
index 0000000..3d885ff
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * test-delta.c: test code to exercise diff-delta.c and patch-delta.c
+ *
+ * (C) 2005 Nicolas Pitre <nico@cam.org>
+ *
+ * This code is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#include "git-compat-util.h"
+#include "delta.h"
+#include "cache.h"
+
+static const char usage_str[] =
+       "test-delta (-d|-p) <from_file> <data_file> <out_file>";
+
+int main(int argc, char *argv[])
+{
+       int fd;
+       struct stat st;
+       void *from_buf, *data_buf, *out_buf;
+       unsigned long from_size, data_size, out_size;
+
+       if (argc != 5 || (strcmp(argv[1], "-d") && strcmp(argv[1], "-p"))) {
+               fprintf(stderr, "Usage: %s\n", usage_str);
+               return 1;
+       }
+
+       fd = open(argv[2], O_RDONLY);
+       if (fd < 0 || fstat(fd, &st)) {
+               perror(argv[2]);
+               return 1;
+       }
+       from_size = st.st_size;
+       from_buf = mmap(NULL, from_size, PROT_READ, MAP_PRIVATE, fd, 0);
+       if (from_buf == MAP_FAILED) {
+               perror(argv[2]);
+               close(fd);
+               return 1;
+       }
+       close(fd);
+
+       fd = open(argv[3], O_RDONLY);
+       if (fd < 0 || fstat(fd, &st)) {
+               perror(argv[3]);
+               return 1;
+       }
+       data_size = st.st_size;
+       data_buf = mmap(NULL, data_size, PROT_READ, MAP_PRIVATE, fd, 0);
+       if (data_buf == MAP_FAILED) {
+               perror(argv[3]);
+               close(fd);
+               return 1;
+       }
+       close(fd);
+
+       if (argv[1][1] == 'd')
+               out_buf = diff_delta(from_buf, from_size,
+                                    data_buf, data_size,
+                                    &out_size, 0);
+       else
+               out_buf = patch_delta(from_buf, from_size,
+                                     data_buf, data_size,
+                                     &out_size);
+       if (!out_buf) {
+               fprintf(stderr, "delta operation failed (returned NULL)\n");
+               return 1;
+       }
+
+       fd = open (argv[4], O_WRONLY|O_CREAT|O_TRUNC, 0666);
+       if (fd < 0 || write_in_full(fd, out_buf, out_size) != out_size) {
+               perror(argv[4]);
+               return 1;
+       }
+
+       return 0;
+}
diff --git a/test-genrandom.c b/test-genrandom.c
new file mode 100644 (file)
index 0000000..8cefe6c
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Simple random data generator used to create reproducible test files.
+ * This is inspired from POSIX.1-2001 implementation example for rand().
+ * Copyright (C) 2007 by Nicolas Pitre, licensed under the GPL version 2.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(int argc, char *argv[])
+{
+       unsigned long count, next = 0;
+       unsigned char *c;
+
+       if (argc < 2 || argc > 3) {
+               fprintf( stderr, "Usage: %s <seed_string> [<size>]", argv[0]);
+               return 1;
+       }
+
+       c = (unsigned char *) argv[1];
+       do {
+               next = next * 11 + *c;
+       } while (*c++);
+
+       count = (argc == 3) ? strtoul(argv[2], NULL, 0) : -1L;
+
+       while (count--) {
+               next = next * 1103515245 + 12345;
+               if (putchar((next >> 16) & 0xff) == EOF)
+                       return -1;
+       }
+
+       return 0;
+}
diff --git a/test-match-trees.c b/test-match-trees.c
new file mode 100644 (file)
index 0000000..a3c4688
--- /dev/null
@@ -0,0 +1,24 @@
+#include "cache.h"
+#include "tree.h"
+
+int main(int ac, char **av)
+{
+       unsigned char hash1[20], hash2[20], shifted[20];
+       struct tree *one, *two;
+
+       if (get_sha1(av[1], hash1))
+               die("cannot parse %s as an object name", av[1]);
+       if (get_sha1(av[2], hash2))
+               die("cannot parse %s as an object name", av[2]);
+       one = parse_tree_indirect(hash1);
+       if (!one)
+               die("not a treeish %s", av[1]);
+       two = parse_tree_indirect(hash2);
+       if (!two)
+               die("not a treeish %s", av[2]);
+
+       shift_tree(one->object.sha1, two->object.sha1, shifted, -1);
+       printf("shifted: %s\n", sha1_to_hex(shifted));
+
+       exit(0);
+}
diff --git a/test-parse-options.c b/test-parse-options.c
new file mode 100644 (file)
index 0000000..4d3e2ec
--- /dev/null
@@ -0,0 +1,36 @@
+#include "cache.h"
+#include "parse-options.h"
+
+static int boolean = 0;
+static int integer = 0;
+static char *string = NULL;
+
+int main(int argc, const char **argv)
+{
+       const char *usage[] = {
+               "test-parse-options <options>",
+               NULL
+       };
+       struct option options[] = {
+               OPT_BOOLEAN('b', "boolean", &boolean, "get a boolean"),
+               OPT_INTEGER('i', "integer", &integer, "get a integer"),
+               OPT_INTEGER('j', NULL, &integer, "get a integer, too"),
+               OPT_GROUP("string options"),
+               OPT_STRING('s', "string", &string, "string", "get a string"),
+               OPT_STRING(0, "string2", &string, "str", "get another string"),
+               OPT_STRING(0, "st", &string, "st", "get another string (pervert ordering)"),
+               OPT_END(),
+       };
+       int i;
+
+       argc = parse_options(argc, argv, options, usage, 0);
+
+       printf("boolean: %d\n", boolean);
+       printf("integer: %d\n", integer);
+       printf("string: %s\n", string ? string : "(not set)");
+
+       for (i = 0; i < argc; i++)
+               printf("arg %02d: %s\n", i, argv[i]);
+
+       return 0;
+}
diff --git a/test-sha1.c b/test-sha1.c
new file mode 100644 (file)
index 0000000..78d7e98
--- /dev/null
@@ -0,0 +1,47 @@
+#include "cache.h"
+
+int main(int ac, char **av)
+{
+       SHA_CTX ctx;
+       unsigned char sha1[20];
+       unsigned bufsz = 8192;
+       char *buffer;
+
+       if (ac == 2)
+               bufsz = strtoul(av[1], NULL, 10) * 1024 * 1024;
+
+       if (!bufsz)
+               bufsz = 8192;
+
+       while ((buffer = malloc(bufsz)) == NULL) {
+               fprintf(stderr, "bufsz %u is too big, halving...\n", bufsz);
+               bufsz /= 2;
+               if (bufsz < 1024)
+                       die("OOPS");
+       }
+
+       SHA1_Init(&ctx);
+
+       while (1) {
+               ssize_t sz, this_sz;
+               char *cp = buffer;
+               unsigned room = bufsz;
+               this_sz = 0;
+               while (room) {
+                       sz = xread(0, cp, room);
+                       if (sz == 0)
+                               break;
+                       if (sz < 0)
+                               die("test-sha1: %s", strerror(errno));
+                       this_sz += sz;
+                       cp += sz;
+                       room -= sz;
+               }
+               if (this_sz == 0)
+                       break;
+               SHA1_Update(&ctx, buffer, this_sz);
+       }
+       SHA1_Final(sha1, &ctx);
+       puts(sha1_to_hex(sha1));
+       exit(0);
+}
diff --git a/test-sha1.sh b/test-sha1.sh
new file mode 100755 (executable)
index 0000000..640856a
--- /dev/null
@@ -0,0 +1,83 @@
+#!/bin/sh
+
+dd if=/dev/zero bs=1048576 count=100 2>/dev/null |
+/usr/bin/time ./test-sha1 >/dev/null
+
+while read expect cnt pfx
+do
+       case "$expect" in '#'*) continue ;; esac
+       actual=`
+               {
+                       test -z "$pfx" || echo "$pfx"
+                       dd if=/dev/zero bs=1048576 count=$cnt 2>/dev/null |
+                       tr '[\0]' '[g]'
+               } | ./test-sha1 $cnt
+       `
+       if test "$expect" = "$actual"
+       then
+               echo "OK: $expect $cnt $pfx"
+       else
+               echo >&2 "OOPS: $cnt"
+               echo >&2 "expect: $expect"
+               echo >&2 "actual: $actual"
+               exit 1
+       fi
+done <<EOF
+da39a3ee5e6b4b0d3255bfef95601890afd80709 0
+3f786850e387550fdab836ed7e6dc881de23001b 0 a
+5277cbb45a15902137d332d97e89cf8136545485 0 ab
+03cfd743661f07975fa2f1220c5194cbaff48451 0 abc
+3330b4373640f9e4604991e73c7e86bfd8da2dc3 0 abcd
+ec11312386ad561674f724b8cca7cf1796e26d1d 0 abcde
+bdc37c074ec4ee6050d68bc133c6b912f36474df 0 abcdef
+69bca99b923859f2dc486b55b87f49689b7358c7 0 abcdefg
+e414af7161c9554089f4106d6f1797ef14a73666 0 abcdefgh
+0707f2970043f9f7c22029482db27733deaec029 0 abcdefghi
+a4dd8aa74a5636728fe52451636e2e17726033aa 1
+9986b45e2f4d7086372533bb6953a8652fa3644a 1 frotz
+23d8d4f788e8526b4877548a32577543cbaaf51f 10
+8cd23f822ab44c7f481b8c92d591f6d1fcad431c 10 frotz
+f3b5604a4e604899c1233edb3bf1cc0ede4d8c32 512
+b095bd837a371593048136e429e9ac4b476e1bb3 512 frotz
+08fa81d6190948de5ccca3966340cc48c10cceac 1200 xyzzy
+e33a291f42c30a159733dd98b8b3e4ff34158ca0 4090 4G
+#a3bf783bc20caa958f6cb24dd140a7b21984838d 9999 nitfol
+EOF
+
+exit
+
+# generating test vectors
+# inputs are number of megabytes followed by some random string to prefix.
+
+while read cnt pfx
+do
+       actual=`
+               {
+                       test -z "$pfx" || echo "$pfx"
+                       dd if=/dev/zero bs=1048576 count=$cnt 2>/dev/null |
+                       tr '[\0]' '[g]'
+               } | sha1sum |
+               sed -e 's/ .*//'
+       `
+       echo "$actual $cnt $pfx"
+done <<EOF
+0
+0 a
+0 ab
+0 abc
+0 abcd
+0 abcde
+0 abcdef
+0 abcdefg
+0 abcdefgh
+0 abcdefghi
+1
+1 frotz
+10
+10 frotz
+512
+512 frotz
+1200 xyzzy
+4090 4G
+9999 nitfol
+EOF
diff --git a/trace.c b/trace.c
new file mode 100644 (file)
index 0000000..4713f91
--- /dev/null
+++ b/trace.c
@@ -0,0 +1,127 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org>
+ * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net>
+ * Copyright (C) 2004 Theodore Y. Ts'o <tytso@mit.edu>
+ * Copyright (C) 2006 Mike McCormack
+ * Copyright (C) 2006 Christian Couder
+ *
+ *  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
+ */
+
+#include "cache.h"
+#include "quote.h"
+
+/* Get a trace file descriptor from GIT_TRACE env variable. */
+static int get_trace_fd(int *need_close)
+{
+       char *trace = getenv("GIT_TRACE");
+
+       if (!trace || !strcmp(trace, "") ||
+           !strcmp(trace, "0") || !strcasecmp(trace, "false"))
+               return 0;
+       if (!strcmp(trace, "1") || !strcasecmp(trace, "true"))
+               return STDERR_FILENO;
+       if (strlen(trace) == 1 && isdigit(*trace))
+               return atoi(trace);
+       if (is_absolute_path(trace)) {
+               int fd = open(trace, O_WRONLY | O_APPEND | O_CREAT, 0666);
+               if (fd == -1) {
+                       fprintf(stderr,
+                               "Could not open '%s' for tracing: %s\n"
+                               "Defaulting to tracing on stderr...\n",
+                               trace, strerror(errno));
+                       return STDERR_FILENO;
+               }
+               *need_close = 1;
+               return fd;
+       }
+
+       fprintf(stderr, "What does '%s' for GIT_TRACE means ?\n", trace);
+       fprintf(stderr, "If you want to trace into a file, "
+               "then please set GIT_TRACE to an absolute pathname "
+               "(starting with /).\n");
+       fprintf(stderr, "Defaulting to tracing on stderr...\n");
+
+       return STDERR_FILENO;
+}
+
+static const char err_msg[] = "Could not trace into fd given by "
+       "GIT_TRACE environment variable";
+
+void trace_printf(const char *fmt, ...)
+{
+       struct strbuf buf;
+       va_list ap;
+       int fd, len, need_close = 0;
+
+       fd = get_trace_fd(&need_close);
+       if (!fd)
+               return;
+
+       strbuf_init(&buf, 64);
+       va_start(ap, fmt);
+       len = vsnprintf(buf.buf, strbuf_avail(&buf), fmt, ap);
+       va_end(ap);
+       if (len >= strbuf_avail(&buf)) {
+               strbuf_grow(&buf, len - strbuf_avail(&buf) + 128);
+               va_start(ap, fmt);
+               len = vsnprintf(buf.buf, strbuf_avail(&buf), fmt, ap);
+               va_end(ap);
+               if (len >= strbuf_avail(&buf))
+                       die("broken vsnprintf");
+       }
+       strbuf_setlen(&buf, len);
+
+       write_or_whine_pipe(fd, buf.buf, buf.len, err_msg);
+       strbuf_release(&buf);
+
+       if (need_close)
+               close(fd);
+}
+
+void trace_argv_printf(const char **argv, const char *fmt, ...)
+{
+       struct strbuf buf;
+       va_list ap;
+       int fd, len, need_close = 0;
+
+       fd = get_trace_fd(&need_close);
+       if (!fd)
+               return;
+
+       strbuf_init(&buf, 64);
+       va_start(ap, fmt);
+       len = vsnprintf(buf.buf, strbuf_avail(&buf), fmt, ap);
+       va_end(ap);
+       if (len >= strbuf_avail(&buf)) {
+               strbuf_grow(&buf, len - strbuf_avail(&buf) + 128);
+               va_start(ap, fmt);
+               len = vsnprintf(buf.buf, strbuf_avail(&buf), fmt, ap);
+               va_end(ap);
+               if (len >= strbuf_avail(&buf))
+                       die("broken vsnprintf");
+       }
+       strbuf_setlen(&buf, len);
+
+       sq_quote_argv(&buf, argv, 0);
+       strbuf_addch(&buf, '\n');
+       write_or_whine_pipe(fd, buf.buf, buf.len, err_msg);
+       strbuf_release(&buf);
+
+       if (need_close)
+               close(fd);
+}
diff --git a/transport.c b/transport.c
new file mode 100644 (file)
index 0000000..58e66f6
--- /dev/null
@@ -0,0 +1,813 @@
+#include "cache.h"
+#include "transport.h"
+#include "run-command.h"
+#ifndef NO_CURL
+#include "http.h"
+#endif
+#include "pkt-line.h"
+#include "fetch-pack.h"
+#include "send-pack.h"
+#include "walker.h"
+#include "bundle.h"
+#include "dir.h"
+#include "refs.h"
+
+/* rsync support */
+
+/*
+ * We copy packed-refs and refs/ into a temporary file, then read the
+ * loose refs recursively (sorting whenever possible), and then inserting
+ * those packed refs that are not yet in the list (not validating, but
+ * assuming that the file is sorted).
+ *
+ * Appears refactoring this from refs.c is too cumbersome.
+ */
+
+static int str_cmp(const void *a, const void *b)
+{
+       const char *s1 = a;
+       const char *s2 = b;
+
+       return strcmp(s1, s2);
+}
+
+/* path->buf + name_offset is expected to point to "refs/" */
+
+static int read_loose_refs(struct strbuf *path, int name_offset,
+               struct ref **tail)
+{
+       DIR *dir = opendir(path->buf);
+       struct dirent *de;
+       struct {
+               char **entries;
+               int nr, alloc;
+       } list;
+       int i, pathlen;
+
+       if (!dir)
+               return -1;
+
+       memset (&list, 0, sizeof(list));
+
+       while ((de = readdir(dir))) {
+               if (de->d_name[0] == '.' && (de->d_name[1] == '\0' ||
+                               (de->d_name[1] == '.' &&
+                                de->d_name[2] == '\0')))
+                       continue;
+               ALLOC_GROW(list.entries, list.nr + 1, list.alloc);
+               list.entries[list.nr++] = xstrdup(de->d_name);
+       }
+       closedir(dir);
+
+       /* sort the list */
+
+       qsort(list.entries, list.nr, sizeof(char *), str_cmp);
+
+       pathlen = path->len;
+       strbuf_addch(path, '/');
+
+       for (i = 0; i < list.nr; i++, strbuf_setlen(path, pathlen + 1)) {
+               strbuf_addstr(path, list.entries[i]);
+               if (read_loose_refs(path, name_offset, tail)) {
+                       int fd = open(path->buf, O_RDONLY);
+                       char buffer[40];
+                       struct ref *next;
+
+                       if (fd < 0)
+                               continue;
+                       next = alloc_ref(path->len - name_offset + 1);
+                       if (read_in_full(fd, buffer, 40) != 40 ||
+                                       get_sha1_hex(buffer, next->old_sha1)) {
+                               close(fd);
+                               free(next);
+                               continue;
+                       }
+                       close(fd);
+                       strcpy(next->name, path->buf + name_offset);
+                       (*tail)->next = next;
+                       *tail = next;
+               }
+       }
+       strbuf_setlen(path, pathlen);
+
+       for (i = 0; i < list.nr; i++)
+               free(list.entries[i]);
+       free(list.entries);
+
+       return 0;
+}
+
+/* insert the packed refs for which no loose refs were found */
+
+static void insert_packed_refs(const char *packed_refs, struct ref **list)
+{
+       FILE *f = fopen(packed_refs, "r");
+       static char buffer[PATH_MAX];
+
+       if (!f)
+               return;
+
+       for (;;) {
+               int cmp = cmp, len;
+
+               if (!fgets(buffer, sizeof(buffer), f)) {
+                       fclose(f);
+                       return;
+               }
+
+               if (hexval(buffer[0]) > 0xf)
+                       continue;
+               len = strlen(buffer);
+               if (buffer[len - 1] == '\n')
+                       buffer[--len] = '\0';
+               if (len < 41)
+                       continue;
+               while ((*list)->next &&
+                               (cmp = strcmp(buffer + 41,
+                                     (*list)->next->name)) > 0)
+                       list = &(*list)->next;
+               if (!(*list)->next || cmp < 0) {
+                       struct ref *next = alloc_ref(len - 40);
+                       buffer[40] = '\0';
+                       if (get_sha1_hex(buffer, next->old_sha1)) {
+                               warning ("invalid SHA-1: %s", buffer);
+                               free(next);
+                               continue;
+                       }
+                       strcpy(next->name, buffer + 41);
+                       next->next = (*list)->next;
+                       (*list)->next = next;
+                       list = &(*list)->next;
+               }
+       }
+}
+
+static struct ref *get_refs_via_rsync(struct transport *transport)
+{
+       struct strbuf buf = STRBUF_INIT, temp_dir = STRBUF_INIT;
+       struct ref dummy, *tail = &dummy;
+       struct child_process rsync;
+       const char *args[5];
+       int temp_dir_len;
+
+       /* copy the refs to the temporary directory */
+
+       strbuf_addstr(&temp_dir, git_path("rsync-refs-XXXXXX"));
+       if (!mkdtemp(temp_dir.buf))
+               die ("Could not make temporary directory");
+       temp_dir_len = temp_dir.len;
+
+       strbuf_addstr(&buf, transport->url);
+       strbuf_addstr(&buf, "/refs");
+
+       memset(&rsync, 0, sizeof(rsync));
+       rsync.argv = args;
+       rsync.stdout_to_stderr = 1;
+       args[0] = "rsync";
+       args[1] = (transport->verbose > 0) ? "-rv" : "-r";
+       args[2] = buf.buf;
+       args[3] = temp_dir.buf;
+       args[4] = NULL;
+
+       if (run_command(&rsync))
+               die ("Could not run rsync to get refs");
+
+       strbuf_reset(&buf);
+       strbuf_addstr(&buf, transport->url);
+       strbuf_addstr(&buf, "/packed-refs");
+
+       args[2] = buf.buf;
+
+       if (run_command(&rsync))
+               die ("Could not run rsync to get refs");
+
+       /* read the copied refs */
+
+       strbuf_addstr(&temp_dir, "/refs");
+       read_loose_refs(&temp_dir, temp_dir_len + 1, &tail);
+       strbuf_setlen(&temp_dir, temp_dir_len);
+
+       tail = &dummy;
+       strbuf_addstr(&temp_dir, "/packed-refs");
+       insert_packed_refs(temp_dir.buf, &tail);
+       strbuf_setlen(&temp_dir, temp_dir_len);
+
+       if (remove_dir_recursively(&temp_dir, 0))
+               warning ("Error removing temporary directory %s.",
+                               temp_dir.buf);
+
+       strbuf_release(&buf);
+       strbuf_release(&temp_dir);
+
+       return dummy.next;
+}
+
+static int fetch_objs_via_rsync(struct transport *transport,
+                                int nr_objs, struct ref **to_fetch)
+{
+       struct strbuf buf = STRBUF_INIT;
+       struct child_process rsync;
+       const char *args[8];
+       int result;
+
+       strbuf_addstr(&buf, transport->url);
+       strbuf_addstr(&buf, "/objects/");
+
+       memset(&rsync, 0, sizeof(rsync));
+       rsync.argv = args;
+       rsync.stdout_to_stderr = 1;
+       args[0] = "rsync";
+       args[1] = (transport->verbose > 0) ? "-rv" : "-r";
+       args[2] = "--ignore-existing";
+       args[3] = "--exclude";
+       args[4] = "info";
+       args[5] = buf.buf;
+       args[6] = get_object_directory();
+       args[7] = NULL;
+
+       /* NEEDSWORK: handle one level of alternates */
+       result = run_command(&rsync);
+
+       strbuf_release(&buf);
+
+       return result;
+}
+
+static int write_one_ref(const char *name, const unsigned char *sha1,
+               int flags, void *data)
+{
+       struct strbuf *buf = data;
+       int len = buf->len;
+       FILE *f;
+
+       /* when called via for_each_ref(), flags is non-zero */
+       if (flags && prefixcmp(name, "refs/heads/") &&
+                       prefixcmp(name, "refs/tags/"))
+               return 0;
+
+       strbuf_addstr(buf, name);
+       if (safe_create_leading_directories(buf->buf) ||
+                       !(f = fopen(buf->buf, "w")) ||
+                       fprintf(f, "%s\n", sha1_to_hex(sha1)) < 0 ||
+                       fclose(f))
+               return error("problems writing temporary file %s", buf->buf);
+       strbuf_setlen(buf, len);
+       return 0;
+}
+
+static int write_refs_to_temp_dir(struct strbuf *temp_dir,
+               int refspec_nr, const char **refspec)
+{
+       int i;
+
+       for (i = 0; i < refspec_nr; i++) {
+               unsigned char sha1[20];
+               char *ref;
+
+               if (dwim_ref(refspec[i], strlen(refspec[i]), sha1, &ref) != 1)
+                       return error("Could not get ref %s", refspec[i]);
+
+               if (write_one_ref(ref, sha1, 0, temp_dir)) {
+                       free(ref);
+                       return -1;
+               }
+               free(ref);
+       }
+       return 0;
+}
+
+static int rsync_transport_push(struct transport *transport,
+               int refspec_nr, const char **refspec, int flags)
+{
+       struct strbuf buf = STRBUF_INIT, temp_dir = STRBUF_INIT;
+       int result = 0, i;
+       struct child_process rsync;
+       const char *args[10];
+
+       if (flags & TRANSPORT_PUSH_MIRROR)
+               return error("rsync transport does not support mirror mode");
+
+       /* first push the objects */
+
+       strbuf_addstr(&buf, transport->url);
+       strbuf_addch(&buf, '/');
+
+       memset(&rsync, 0, sizeof(rsync));
+       rsync.argv = args;
+       rsync.stdout_to_stderr = 1;
+       i = 0;
+       args[i++] = "rsync";
+       args[i++] = "-a";
+       if (flags & TRANSPORT_PUSH_DRY_RUN)
+               args[i++] = "--dry-run";
+       if (transport->verbose > 0)
+               args[i++] = "-v";
+       args[i++] = "--ignore-existing";
+       args[i++] = "--exclude";
+       args[i++] = "info";
+       args[i++] = get_object_directory();
+       args[i++] = buf.buf;
+       args[i++] = NULL;
+
+       if (run_command(&rsync))
+               return error("Could not push objects to %s", transport->url);
+
+       /* copy the refs to the temporary directory; they could be packed. */
+
+       strbuf_addstr(&temp_dir, git_path("rsync-refs-XXXXXX"));
+       if (!mkdtemp(temp_dir.buf))
+               die ("Could not make temporary directory");
+       strbuf_addch(&temp_dir, '/');
+
+       if (flags & TRANSPORT_PUSH_ALL) {
+               if (for_each_ref(write_one_ref, &temp_dir))
+                       return -1;
+       } else if (write_refs_to_temp_dir(&temp_dir, refspec_nr, refspec))
+               return -1;
+
+       i = 2;
+       if (flags & TRANSPORT_PUSH_DRY_RUN)
+               args[i++] = "--dry-run";
+       if (!(flags & TRANSPORT_PUSH_FORCE))
+               args[i++] = "--ignore-existing";
+       args[i++] = temp_dir.buf;
+       args[i++] = transport->url;
+       args[i++] = NULL;
+       if (run_command(&rsync))
+               result = error("Could not push to %s", transport->url);
+
+       if (remove_dir_recursively(&temp_dir, 0))
+               warning ("Could not remove temporary directory %s.",
+                               temp_dir.buf);
+
+       strbuf_release(&buf);
+       strbuf_release(&temp_dir);
+
+       return result;
+}
+
+/* Generic functions for using commit walkers */
+
+#ifndef NO_CURL /* http fetch is the only user */
+static int fetch_objs_via_walker(struct transport *transport,
+                                int nr_objs, struct ref **to_fetch)
+{
+       char *dest = xstrdup(transport->url);
+       struct walker *walker = transport->data;
+       char **objs = xmalloc(nr_objs * sizeof(*objs));
+       int i;
+
+       walker->get_all = 1;
+       walker->get_tree = 1;
+       walker->get_history = 1;
+       walker->get_verbosely = transport->verbose >= 0;
+       walker->get_recover = 0;
+
+       for (i = 0; i < nr_objs; i++)
+               objs[i] = xstrdup(sha1_to_hex(to_fetch[i]->old_sha1));
+
+       if (walker_fetch(walker, nr_objs, objs, NULL, NULL))
+               die("Fetch failed.");
+
+       for (i = 0; i < nr_objs; i++)
+               free(objs[i]);
+       free(objs);
+       free(dest);
+       return 0;
+}
+#endif /* NO_CURL */
+
+static int disconnect_walker(struct transport *transport)
+{
+       struct walker *walker = transport->data;
+       if (walker)
+               walker_free(walker);
+       return 0;
+}
+
+#ifndef NO_CURL
+static int curl_transport_push(struct transport *transport, int refspec_nr, const char **refspec, int flags)
+{
+       const char **argv;
+       int argc;
+       int err;
+
+       if (flags & TRANSPORT_PUSH_MIRROR)
+               return error("http transport does not support mirror mode");
+
+       argv = xmalloc((refspec_nr + 12) * sizeof(char *));
+       argv[0] = "http-push";
+       argc = 1;
+       if (flags & TRANSPORT_PUSH_ALL)
+               argv[argc++] = "--all";
+       if (flags & TRANSPORT_PUSH_FORCE)
+               argv[argc++] = "--force";
+       if (flags & TRANSPORT_PUSH_DRY_RUN)
+               argv[argc++] = "--dry-run";
+       if (flags & TRANSPORT_PUSH_VERBOSE)
+               argv[argc++] = "--verbose";
+       argv[argc++] = transport->url;
+       while (refspec_nr--)
+               argv[argc++] = *refspec++;
+       argv[argc] = NULL;
+       err = run_command_v_opt(argv, RUN_GIT_CMD);
+       switch (err) {
+       case -ERR_RUN_COMMAND_FORK:
+               error("unable to fork for %s", argv[0]);
+       case -ERR_RUN_COMMAND_EXEC:
+               error("unable to exec %s", argv[0]);
+               break;
+       case -ERR_RUN_COMMAND_WAITPID:
+       case -ERR_RUN_COMMAND_WAITPID_WRONG_PID:
+       case -ERR_RUN_COMMAND_WAITPID_SIGNAL:
+       case -ERR_RUN_COMMAND_WAITPID_NOEXIT:
+               error("%s died with strange error", argv[0]);
+       }
+       return !!err;
+}
+
+static int missing__target(int code, int result)
+{
+       return  /* file:// URL -- do we ever use one??? */
+               (result == CURLE_FILE_COULDNT_READ_FILE) ||
+               /* http:// and https:// URL */
+               (code == 404 && result == CURLE_HTTP_RETURNED_ERROR) ||
+               /* ftp:// URL */
+               (code == 550 && result == CURLE_FTP_COULDNT_RETR_FILE)
+               ;
+}
+
+#define missing_target(a) missing__target((a)->http_code, (a)->curl_result)
+
+static struct ref *get_refs_via_curl(struct transport *transport)
+{
+       struct buffer buffer;
+       char *data, *start, *mid;
+       char *ref_name;
+       char *refs_url;
+       int i = 0;
+
+       struct active_request_slot *slot;
+       struct slot_results results;
+
+       struct ref *refs = NULL;
+       struct ref *ref = NULL;
+       struct ref *last_ref = NULL;
+
+       data = xmalloc(4096);
+       buffer.size = 4096;
+       buffer.posn = 0;
+       buffer.buffer = data;
+
+       refs_url = xmalloc(strlen(transport->url) + 11);
+       sprintf(refs_url, "%s/info/refs", transport->url);
+
+       http_init();
+
+       slot = get_active_slot();
+       slot->results = &results;
+       curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer);
+       curl_easy_setopt(slot->curl, CURLOPT_URL, refs_url);
+       curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL);
+       if (transport->remote->http_proxy)
+               curl_easy_setopt(slot->curl, CURLOPT_PROXY,
+                                transport->remote->http_proxy);
+
+       if (start_active_slot(slot)) {
+               run_active_slot(slot);
+               if (results.curl_result != CURLE_OK) {
+                       if (missing_target(&results)) {
+                               free(buffer.buffer);
+                               return NULL;
+                       } else {
+                               free(buffer.buffer);
+                               error("%s", curl_errorstr);
+                               return NULL;
+                       }
+               }
+       } else {
+               free(buffer.buffer);
+               error("Unable to start request");
+               return NULL;
+       }
+
+       http_cleanup();
+
+       data = buffer.buffer;
+       start = NULL;
+       mid = data;
+       while (i < buffer.posn) {
+               if (!start)
+                       start = &data[i];
+               if (data[i] == '\t')
+                       mid = &data[i];
+               if (data[i] == '\n') {
+                       data[i] = 0;
+                       ref_name = mid + 1;
+                       ref = xmalloc(sizeof(struct ref) +
+                                     strlen(ref_name) + 1);
+                       memset(ref, 0, sizeof(struct ref));
+                       strcpy(ref->name, ref_name);
+                       get_sha1_hex(start, ref->old_sha1);
+                       if (!refs)
+                               refs = ref;
+                       if (last_ref)
+                               last_ref->next = ref;
+                       last_ref = ref;
+                       start = NULL;
+               }
+               i++;
+       }
+
+       free(buffer.buffer);
+
+       return refs;
+}
+
+static int fetch_objs_via_curl(struct transport *transport,
+                                int nr_objs, struct ref **to_fetch)
+{
+       if (!transport->data)
+               transport->data = get_http_walker(transport->url);
+       return fetch_objs_via_walker(transport, nr_objs, to_fetch);
+}
+
+#endif
+
+struct bundle_transport_data {
+       int fd;
+       struct bundle_header header;
+};
+
+static struct ref *get_refs_from_bundle(struct transport *transport)
+{
+       struct bundle_transport_data *data = transport->data;
+       struct ref *result = NULL;
+       int i;
+
+       if (data->fd > 0)
+               close(data->fd);
+       data->fd = read_bundle_header(transport->url, &data->header);
+       if (data->fd < 0)
+               die ("Could not read bundle '%s'.", transport->url);
+       for (i = 0; i < data->header.references.nr; i++) {
+               struct ref_list_entry *e = data->header.references.list + i;
+               struct ref *ref = alloc_ref(strlen(e->name) + 1);
+               hashcpy(ref->old_sha1, e->sha1);
+               strcpy(ref->name, e->name);
+               ref->next = result;
+               result = ref;
+       }
+       return result;
+}
+
+static int fetch_refs_from_bundle(struct transport *transport,
+                              int nr_heads, struct ref **to_fetch)
+{
+       struct bundle_transport_data *data = transport->data;
+       return unbundle(&data->header, data->fd);
+}
+
+static int close_bundle(struct transport *transport)
+{
+       struct bundle_transport_data *data = transport->data;
+       if (data->fd > 0)
+               close(data->fd);
+       free(data);
+       return 0;
+}
+
+struct git_transport_data {
+       unsigned thin : 1;
+       unsigned keep : 1;
+       int depth;
+       const char *uploadpack;
+       const char *receivepack;
+};
+
+static int set_git_option(struct transport *connection,
+                         const char *name, const char *value)
+{
+       struct git_transport_data *data = connection->data;
+       if (!strcmp(name, TRANS_OPT_UPLOADPACK)) {
+               data->uploadpack = value;
+               return 0;
+       } else if (!strcmp(name, TRANS_OPT_RECEIVEPACK)) {
+               data->receivepack = value;
+               return 0;
+       } else if (!strcmp(name, TRANS_OPT_THIN)) {
+               data->thin = !!value;
+               return 0;
+       } else if (!strcmp(name, TRANS_OPT_KEEP)) {
+               data->keep = !!value;
+               return 0;
+       } else if (!strcmp(name, TRANS_OPT_DEPTH)) {
+               if (!value)
+                       data->depth = 0;
+               else
+                       data->depth = atoi(value);
+               return 0;
+       }
+       return 1;
+}
+
+static struct ref *get_refs_via_connect(struct transport *transport)
+{
+       struct git_transport_data *data = transport->data;
+       struct ref *refs;
+       int fd[2];
+       char *dest = xstrdup(transport->url);
+       struct child_process *conn = git_connect(fd, dest, data->uploadpack, 0);
+
+       get_remote_heads(fd[0], &refs, 0, NULL, 0);
+       packet_flush(fd[1]);
+
+       finish_connect(conn);
+
+       free(dest);
+
+       return refs;
+}
+
+static int fetch_refs_via_pack(struct transport *transport,
+                              int nr_heads, struct ref **to_fetch)
+{
+       struct git_transport_data *data = transport->data;
+       char **heads = xmalloc(nr_heads * sizeof(*heads));
+       char **origh = xmalloc(nr_heads * sizeof(*origh));
+       struct ref *refs;
+       char *dest = xstrdup(transport->url);
+       struct fetch_pack_args args;
+       int i;
+
+       memset(&args, 0, sizeof(args));
+       args.uploadpack = data->uploadpack;
+       args.keep_pack = data->keep;
+       args.lock_pack = 1;
+       args.use_thin_pack = data->thin;
+       args.verbose = transport->verbose > 0;
+       args.depth = data->depth;
+
+       for (i = 0; i < nr_heads; i++)
+               origh[i] = heads[i] = xstrdup(to_fetch[i]->name);
+       refs = fetch_pack(&args, dest, nr_heads, heads, &transport->pack_lockfile);
+
+       for (i = 0; i < nr_heads; i++)
+               free(origh[i]);
+       free(origh);
+       free(heads);
+       free_refs(refs);
+       free(dest);
+       return (refs ? 0 : -1);
+}
+
+static int git_transport_push(struct transport *transport, int refspec_nr, const char **refspec, int flags)
+{
+       struct git_transport_data *data = transport->data;
+       struct send_pack_args args;
+
+       args.receivepack = data->receivepack;
+       args.send_all = !!(flags & TRANSPORT_PUSH_ALL);
+       args.send_mirror = !!(flags & TRANSPORT_PUSH_MIRROR);
+       args.force_update = !!(flags & TRANSPORT_PUSH_FORCE);
+       args.use_thin_pack = data->thin;
+       args.verbose = !!(flags & TRANSPORT_PUSH_VERBOSE);
+       args.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN);
+
+       return send_pack(&args, transport->url, transport->remote, refspec_nr, refspec);
+}
+
+static int disconnect_git(struct transport *transport)
+{
+       free(transport->data);
+       return 0;
+}
+
+static int is_local(const char *url)
+{
+       const char *colon = strchr(url, ':');
+       const char *slash = strchr(url, '/');
+       return !colon || (slash && slash < colon);
+}
+
+static int is_file(const char *url)
+{
+       struct stat buf;
+       if (stat(url, &buf))
+               return 0;
+       return S_ISREG(buf.st_mode);
+}
+
+struct transport *transport_get(struct remote *remote, const char *url)
+{
+       struct transport *ret = xcalloc(1, sizeof(*ret));
+
+       ret->remote = remote;
+       ret->url = url;
+
+       if (!prefixcmp(url, "rsync://")) {
+               ret->get_refs_list = get_refs_via_rsync;
+               ret->fetch = fetch_objs_via_rsync;
+               ret->push = rsync_transport_push;
+
+       } else if (!prefixcmp(url, "http://")
+               || !prefixcmp(url, "https://")
+               || !prefixcmp(url, "ftp://")) {
+#ifdef NO_CURL
+               error("git was compiled without libcurl support.");
+#else
+               ret->get_refs_list = get_refs_via_curl;
+               ret->fetch = fetch_objs_via_curl;
+               ret->push = curl_transport_push;
+#endif
+               ret->disconnect = disconnect_walker;
+
+       } else if (is_local(url) && is_file(url)) {
+               struct bundle_transport_data *data = xcalloc(1, sizeof(*data));
+               ret->data = data;
+               ret->get_refs_list = get_refs_from_bundle;
+               ret->fetch = fetch_refs_from_bundle;
+               ret->disconnect = close_bundle;
+
+       } else {
+               struct git_transport_data *data = xcalloc(1, sizeof(*data));
+               ret->data = data;
+               ret->set_option = set_git_option;
+               ret->get_refs_list = get_refs_via_connect;
+               ret->fetch = fetch_refs_via_pack;
+               ret->push = git_transport_push;
+               ret->disconnect = disconnect_git;
+
+               data->thin = 1;
+               data->uploadpack = "git-upload-pack";
+               if (remote && remote->uploadpack)
+                       data->uploadpack = remote->uploadpack;
+               data->receivepack = "git-receive-pack";
+               if (remote && remote->receivepack)
+                       data->receivepack = remote->receivepack;
+       }
+
+       return ret;
+}
+
+int transport_set_option(struct transport *transport,
+                        const char *name, const char *value)
+{
+       if (transport->set_option)
+               return transport->set_option(transport, name, value);
+       return 1;
+}
+
+int transport_push(struct transport *transport,
+                  int refspec_nr, const char **refspec, int flags)
+{
+       if (!transport->push)
+               return 1;
+       return transport->push(transport, refspec_nr, refspec, flags);
+}
+
+const struct ref *transport_get_remote_refs(struct transport *transport)
+{
+       if (!transport->remote_refs)
+               transport->remote_refs = transport->get_refs_list(transport);
+       return transport->remote_refs;
+}
+
+int transport_fetch_refs(struct transport *transport, struct ref *refs)
+{
+       int rc;
+       int nr_heads = 0, nr_alloc = 0;
+       struct ref **heads = NULL;
+       struct ref *rm;
+
+       for (rm = refs; rm; rm = rm->next) {
+               if (rm->peer_ref &&
+                   !hashcmp(rm->peer_ref->old_sha1, rm->old_sha1))
+                       continue;
+               ALLOC_GROW(heads, nr_heads + 1, nr_alloc);
+               heads[nr_heads++] = rm;
+       }
+
+       rc = transport->fetch(transport, nr_heads, heads);
+       free(heads);
+       return rc;
+}
+
+void transport_unlock_pack(struct transport *transport)
+{
+       if (transport->pack_lockfile) {
+               unlink(transport->pack_lockfile);
+               free(transport->pack_lockfile);
+               transport->pack_lockfile = NULL;
+       }
+}
+
+int transport_disconnect(struct transport *transport)
+{
+       int ret = 0;
+       if (transport->disconnect)
+               ret = transport->disconnect(transport);
+       free(transport);
+       return ret;
+}
diff --git a/transport.h b/transport.h
new file mode 100644 (file)
index 0000000..6fb4526
--- /dev/null
@@ -0,0 +1,72 @@
+#ifndef TRANSPORT_H
+#define TRANSPORT_H
+
+#include "cache.h"
+#include "remote.h"
+
+struct transport {
+       struct remote *remote;
+       const char *url;
+       void *data;
+       const struct ref *remote_refs;
+
+       /**
+        * Returns 0 if successful, positive if the option is not
+        * recognized or is inapplicable, and negative if the option
+        * is applicable but the value is invalid.
+        **/
+       int (*set_option)(struct transport *connection, const char *name,
+                         const char *value);
+
+       struct ref *(*get_refs_list)(struct transport *transport);
+       int (*fetch)(struct transport *transport, int refs_nr, struct ref **refs);
+       int (*push)(struct transport *connection, int refspec_nr, const char **refspec, int flags);
+
+       int (*disconnect)(struct transport *connection);
+       char *pack_lockfile;
+       signed verbose : 2;
+};
+
+#define TRANSPORT_PUSH_ALL 1
+#define TRANSPORT_PUSH_FORCE 2
+#define TRANSPORT_PUSH_DRY_RUN 4
+#define TRANSPORT_PUSH_MIRROR 8
+#define TRANSPORT_PUSH_VERBOSE 16
+
+/* Returns a transport suitable for the url */
+struct transport *transport_get(struct remote *, const char *);
+
+/* Transport options which apply to git:// and scp-style URLs */
+
+/* The program to use on the remote side to send a pack */
+#define TRANS_OPT_UPLOADPACK "uploadpack"
+
+/* The program to use on the remote side to receive a pack */
+#define TRANS_OPT_RECEIVEPACK "receivepack"
+
+/* Transfer the data as a thin pack if not null */
+#define TRANS_OPT_THIN "thin"
+
+/* Keep the pack that was transferred if not null */
+#define TRANS_OPT_KEEP "keep"
+
+/* Limit the depth of the fetch if not null */
+#define TRANS_OPT_DEPTH "depth"
+
+/**
+ * Returns 0 if the option was used, non-zero otherwise. Prints a
+ * message to stderr if the option is not used.
+ **/
+int transport_set_option(struct transport *transport, const char *name,
+                        const char *value);
+
+int transport_push(struct transport *connection,
+                  int refspec_nr, const char **refspec, int flags);
+
+const struct ref *transport_get_remote_refs(struct transport *transport);
+
+int transport_fetch_refs(struct transport *transport, struct ref *refs);
+void transport_unlock_pack(struct transport *transport);
+int transport_disconnect(struct transport *transport);
+
+#endif
diff --git a/tree-diff.c b/tree-diff.c
new file mode 100644 (file)
index 0000000..aa0a100
--- /dev/null
@@ -0,0 +1,443 @@
+/*
+ * Helper functions for tree diff generation
+ */
+#include "cache.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "tree.h"
+
+static char *malloc_base(const char *base, int baselen, const char *path, int pathlen)
+{
+       char *newbase = xmalloc(baselen + pathlen + 2);
+       memcpy(newbase, base, baselen);
+       memcpy(newbase + baselen, path, pathlen);
+       memcpy(newbase + baselen + pathlen, "/", 2);
+       return newbase;
+}
+
+static void show_entry(struct diff_options *opt, const char *prefix, struct tree_desc *desc,
+                      const char *base, int baselen);
+
+static int compare_tree_entry(struct tree_desc *t1, struct tree_desc *t2, const char *base, int baselen, struct diff_options *opt)
+{
+       unsigned mode1, mode2;
+       const char *path1, *path2;
+       const unsigned char *sha1, *sha2;
+       int cmp, pathlen1, pathlen2;
+
+       sha1 = tree_entry_extract(t1, &path1, &mode1);
+       sha2 = tree_entry_extract(t2, &path2, &mode2);
+
+       pathlen1 = tree_entry_len(path1, sha1);
+       pathlen2 = tree_entry_len(path2, sha2);
+       cmp = base_name_compare(path1, pathlen1, mode1, path2, pathlen2, mode2);
+       if (cmp < 0) {
+               show_entry(opt, "-", t1, base, baselen);
+               return -1;
+       }
+       if (cmp > 0) {
+               show_entry(opt, "+", t2, base, baselen);
+               return 1;
+       }
+       if (!DIFF_OPT_TST(opt, FIND_COPIES_HARDER) && !hashcmp(sha1, sha2) && mode1 == mode2)
+               return 0;
+
+       /*
+        * If the filemode has changed to/from a directory from/to a regular
+        * file, we need to consider it a remove and an add.
+        */
+       if (S_ISDIR(mode1) != S_ISDIR(mode2)) {
+               show_entry(opt, "-", t1, base, baselen);
+               show_entry(opt, "+", t2, base, baselen);
+               return 0;
+       }
+
+       if (DIFF_OPT_TST(opt, RECURSIVE) && S_ISDIR(mode1)) {
+               int retval;
+               char *newbase = malloc_base(base, baselen, path1, pathlen1);
+               if (DIFF_OPT_TST(opt, TREE_IN_RECURSIVE))
+                       opt->change(opt, mode1, mode2,
+                                   sha1, sha2, base, path1);
+               retval = diff_tree_sha1(sha1, sha2, newbase, opt);
+               free(newbase);
+               return retval;
+       }
+
+       opt->change(opt, mode1, mode2, sha1, sha2, base, path1);
+       return 0;
+}
+
+/*
+ * Is a tree entry interesting given the pathspec we have?
+ *
+ * Return:
+ *  - 2 for "yes, and all subsequent entries will be"
+ *  - 1 for yes
+ *  - zero for no
+ *  - negative for "no, and no subsequent entries will be either"
+ */
+static int tree_entry_interesting(struct tree_desc *desc, const char *base, int baselen, struct diff_options *opt)
+{
+       const char *path;
+       const unsigned char *sha1;
+       unsigned mode;
+       int i;
+       int pathlen;
+       int never_interesting = -1;
+
+       if (!opt->nr_paths)
+               return 1;
+
+       sha1 = tree_entry_extract(desc, &path, &mode);
+
+       pathlen = tree_entry_len(path, sha1);
+
+       for (i = 0; i < opt->nr_paths; i++) {
+               const char *match = opt->paths[i];
+               int matchlen = opt->pathlens[i];
+               int m = -1; /* signals that we haven't called strncmp() */
+
+               if (baselen >= matchlen) {
+                       /* If it doesn't match, move along... */
+                       if (strncmp(base, match, matchlen))
+                               continue;
+
+                       /*
+                        * The base is a subdirectory of a path which
+                        * was specified, so all of them are interesting.
+                        */
+                       return 2;
+               }
+
+               /* Does the base match? */
+               if (strncmp(base, match, baselen))
+                       continue;
+
+               match += baselen;
+               matchlen -= baselen;
+
+               if (never_interesting) {
+                       /*
+                        * We have not seen any match that sorts later
+                        * than the current path.
+                        */
+
+                       /*
+                        * Does match sort strictly earlier than path
+                        * with their common parts?
+                        */
+                       m = strncmp(match, path,
+                                   (matchlen < pathlen) ? matchlen : pathlen);
+                       if (m < 0)
+                               continue;
+
+                       /*
+                        * If we come here even once, that means there is at
+                        * least one pathspec that would sort equal to or
+                        * later than the path we are currently looking at.
+                        * In other words, if we have never reached this point
+                        * after iterating all pathspecs, it means all
+                        * pathspecs are either outside of base, or inside the
+                        * base but sorts strictly earlier than the current
+                        * one.  In either case, they will never match the
+                        * subsequent entries.  In such a case, we initialized
+                        * the variable to -1 and that is what will be
+                        * returned, allowing the caller to terminate early.
+                        */
+                       never_interesting = 0;
+               }
+
+               if (pathlen > matchlen)
+                       continue;
+
+               if (matchlen > pathlen) {
+                       if (match[pathlen] != '/')
+                               continue;
+                       if (!S_ISDIR(mode))
+                               continue;
+               }
+
+               if (m == -1)
+                       /*
+                        * we cheated and did not do strncmp(), so we do
+                        * that here.
+                        */
+                       m = strncmp(match, path, pathlen);
+
+               /*
+                * If common part matched earlier then it is a hit,
+                * because we rejected the case where path is not a
+                * leading directory and is shorter than match.
+                */
+               if (!m)
+                       return 1;
+       }
+       return never_interesting; /* No matches */
+}
+
+/* A whole sub-tree went away or appeared */
+static void show_tree(struct diff_options *opt, const char *prefix, struct tree_desc *desc, const char *base, int baselen)
+{
+       int all_interesting = 0;
+       while (desc->size) {
+               int show;
+
+               if (all_interesting)
+                       show = 1;
+               else {
+                       show = tree_entry_interesting(desc, base, baselen,
+                                                     opt);
+                       if (show == 2)
+                               all_interesting = 1;
+               }
+               if (show < 0)
+                       break;
+               if (show)
+                       show_entry(opt, prefix, desc, base, baselen);
+               update_tree_entry(desc);
+       }
+}
+
+/* A file entry went away or appeared */
+static void show_entry(struct diff_options *opt, const char *prefix, struct tree_desc *desc,
+                      const char *base, int baselen)
+{
+       unsigned mode;
+       const char *path;
+       const unsigned char *sha1 = tree_entry_extract(desc, &path, &mode);
+
+       if (DIFF_OPT_TST(opt, RECURSIVE) && S_ISDIR(mode)) {
+               enum object_type type;
+               int pathlen = tree_entry_len(path, sha1);
+               char *newbase = malloc_base(base, baselen, path, pathlen);
+               struct tree_desc inner;
+               void *tree;
+               unsigned long size;
+
+               tree = read_sha1_file(sha1, &type, &size);
+               if (!tree || type != OBJ_TREE)
+                       die("corrupt tree sha %s", sha1_to_hex(sha1));
+
+               init_tree_desc(&inner, tree, size);
+               show_tree(opt, prefix, &inner, newbase, baselen + 1 + pathlen);
+
+               free(tree);
+               free(newbase);
+       } else {
+               opt->add_remove(opt, prefix[0], mode, sha1, base, path);
+       }
+}
+
+static void skip_uninteresting(struct tree_desc *t, const char *base, int baselen, struct diff_options *opt)
+{
+       int all_interesting = 0;
+       while (t->size) {
+               int show;
+
+               if (all_interesting)
+                       show = 1;
+               else {
+                       show = tree_entry_interesting(t, base, baselen, opt);
+                       if (show == 2)
+                               all_interesting = 1;
+               }
+               if (!show) {
+                       update_tree_entry(t);
+                       continue;
+               }
+               /* Skip it all? */
+               if (show < 0)
+                       t->size = 0;
+               return;
+       }
+}
+
+int diff_tree(struct tree_desc *t1, struct tree_desc *t2, const char *base, struct diff_options *opt)
+{
+       int baselen = strlen(base);
+
+       for (;;) {
+               if (DIFF_OPT_TST(opt, QUIET) && DIFF_OPT_TST(opt, HAS_CHANGES))
+                       break;
+               if (opt->nr_paths) {
+                       skip_uninteresting(t1, base, baselen, opt);
+                       skip_uninteresting(t2, base, baselen, opt);
+               }
+               if (!t1->size) {
+                       if (!t2->size)
+                               break;
+                       show_entry(opt, "+", t2, base, baselen);
+                       update_tree_entry(t2);
+                       continue;
+               }
+               if (!t2->size) {
+                       show_entry(opt, "-", t1, base, baselen);
+                       update_tree_entry(t1);
+                       continue;
+               }
+               switch (compare_tree_entry(t1, t2, base, baselen, opt)) {
+               case -1:
+                       update_tree_entry(t1);
+                       continue;
+               case 0:
+                       update_tree_entry(t1);
+                       /* Fallthrough */
+               case 1:
+                       update_tree_entry(t2);
+                       continue;
+               }
+               die("git-diff-tree: internal error");
+       }
+       return 0;
+}
+
+/*
+ * Does it look like the resulting diff might be due to a rename?
+ *  - single entry
+ *  - not a valid previous file
+ */
+static inline int diff_might_be_rename(void)
+{
+       return diff_queued_diff.nr == 1 &&
+               !DIFF_FILE_VALID(diff_queued_diff.queue[0]->one);
+}
+
+static void try_to_follow_renames(struct tree_desc *t1, struct tree_desc *t2, const char *base, struct diff_options *opt)
+{
+       struct diff_options diff_opts;
+       struct diff_queue_struct *q = &diff_queued_diff;
+       struct diff_filepair *choice;
+       const char *paths[1];
+       int i;
+
+       /* Remove the file creation entry from the diff queue, and remember it */
+       choice = q->queue[0];
+       q->nr = 0;
+
+       diff_setup(&diff_opts);
+       DIFF_OPT_SET(&diff_opts, RECURSIVE);
+       diff_opts.detect_rename = DIFF_DETECT_RENAME;
+       diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+       diff_opts.single_follow = opt->paths[0];
+       diff_opts.break_opt = opt->break_opt;
+       paths[0] = NULL;
+       diff_tree_setup_paths(paths, &diff_opts);
+       if (diff_setup_done(&diff_opts) < 0)
+               die("unable to set up diff options to follow renames");
+       diff_tree(t1, t2, base, &diff_opts);
+       diffcore_std(&diff_opts);
+
+       /* Go through the new set of filepairing, and see if we find a more interesting one */
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filepair *p = q->queue[i];
+
+               /*
+                * Found a source? Not only do we use that for the new
+                * diff_queued_diff, we will also use that as the path in
+                * the future!
+                */
+               if ((p->status == 'R' || p->status == 'C') && !strcmp(p->two->path, opt->paths[0])) {
+                       /* Switch the file-pairs around */
+                       q->queue[i] = choice;
+                       choice = p;
+
+                       /* Update the path we use from now on.. */
+                       opt->paths[0] = xstrdup(p->one->path);
+                       diff_tree_setup_paths(opt->paths, opt);
+                       break;
+               }
+       }
+
+       /*
+        * Then, discard all the non-relevane file pairs...
+        */
+       for (i = 0; i < q->nr; i++) {
+               struct diff_filepair *p = q->queue[i];
+               diff_free_filepair(p);
+       }
+
+       /*
+        * .. and re-instate the one we want (which might be either the
+        * original one, or the rename/copy we found)
+        */
+       q->queue[0] = choice;
+       q->nr = 1;
+}
+
+int diff_tree_sha1(const unsigned char *old, const unsigned char *new, const char *base, struct diff_options *opt)
+{
+       void *tree1, *tree2;
+       struct tree_desc t1, t2;
+       unsigned long size1, size2;
+       int retval;
+
+       tree1 = read_object_with_reference(old, tree_type, &size1, NULL);
+       if (!tree1)
+               die("unable to read source tree (%s)", sha1_to_hex(old));
+       tree2 = read_object_with_reference(new, tree_type, &size2, NULL);
+       if (!tree2)
+               die("unable to read destination tree (%s)", sha1_to_hex(new));
+       init_tree_desc(&t1, tree1, size1);
+       init_tree_desc(&t2, tree2, size2);
+       retval = diff_tree(&t1, &t2, base, opt);
+       if (DIFF_OPT_TST(opt, FOLLOW_RENAMES) && diff_might_be_rename()) {
+               init_tree_desc(&t1, tree1, size1);
+               init_tree_desc(&t2, tree2, size2);
+               try_to_follow_renames(&t1, &t2, base, opt);
+       }
+       free(tree1);
+       free(tree2);
+       return retval;
+}
+
+int diff_root_tree_sha1(const unsigned char *new, const char *base, struct diff_options *opt)
+{
+       int retval;
+       void *tree;
+       unsigned long size;
+       struct tree_desc empty, real;
+
+       tree = read_object_with_reference(new, tree_type, &size, NULL);
+       if (!tree)
+               die("unable to read root tree (%s)", sha1_to_hex(new));
+       init_tree_desc(&real, tree, size);
+
+       init_tree_desc(&empty, "", 0);
+       retval = diff_tree(&empty, &real, base, opt);
+       free(tree);
+       return retval;
+}
+
+static int count_paths(const char **paths)
+{
+       int i = 0;
+       while (*paths++)
+               i++;
+       return i;
+}
+
+void diff_tree_release_paths(struct diff_options *opt)
+{
+       free(opt->pathlens);
+}
+
+void diff_tree_setup_paths(const char **p, struct diff_options *opt)
+{
+       opt->nr_paths = 0;
+       opt->pathlens = NULL;
+       opt->paths = NULL;
+
+       if (p) {
+               int i;
+
+               opt->paths = p;
+               opt->nr_paths = count_paths(p);
+               if (opt->nr_paths == 0) {
+                       opt->pathlens = NULL;
+                       return;
+               }
+               opt->pathlens = xmalloc(opt->nr_paths * sizeof(int));
+               for (i=0; i < opt->nr_paths; i++)
+                       opt->pathlens[i] = strlen(p[i]);
+       }
+}
diff --git a/tree-walk.c b/tree-walk.c
new file mode 100644 (file)
index 0000000..8d4b673
--- /dev/null
@@ -0,0 +1,208 @@
+#include "cache.h"
+#include "tree-walk.h"
+#include "tree.h"
+
+static const char *get_mode(const char *str, unsigned int *modep)
+{
+       unsigned char c;
+       unsigned int mode = 0;
+
+       while ((c = *str++) != ' ') {
+               if (c < '0' || c > '7')
+                       return NULL;
+               mode = (mode << 3) + (c - '0');
+       }
+       *modep = mode;
+       return str;
+}
+
+static void decode_tree_entry(struct tree_desc *desc, const void *buf, unsigned long size)
+{
+       const char *path;
+       unsigned int mode, len;
+
+       path = get_mode(buf, &mode);
+       if (!path)
+               die("corrupt tree file");
+       len = strlen(path) + 1;
+
+       /* Initialize the descriptor entry */
+       desc->entry.path = path;
+       desc->entry.mode = mode;
+       desc->entry.sha1 = (const unsigned char *)(path + len);
+}
+
+void init_tree_desc(struct tree_desc *desc, const void *buffer, unsigned long size)
+{
+       desc->buffer = buffer;
+       desc->size = size;
+       if (size)
+               decode_tree_entry(desc, buffer, size);
+}
+
+void *fill_tree_descriptor(struct tree_desc *desc, const unsigned char *sha1)
+{
+       unsigned long size = 0;
+       void *buf = NULL;
+
+       if (sha1) {
+               buf = read_object_with_reference(sha1, tree_type, &size, NULL);
+               if (!buf)
+                       die("unable to read tree %s", sha1_to_hex(sha1));
+       }
+       init_tree_desc(desc, buf, size);
+       return buf;
+}
+
+static int entry_compare(struct name_entry *a, struct name_entry *b)
+{
+       return base_name_compare(
+                       a->path, tree_entry_len(a->path, a->sha1), a->mode,
+                       b->path, tree_entry_len(b->path, b->sha1), b->mode);
+}
+
+static void entry_clear(struct name_entry *a)
+{
+       memset(a, 0, sizeof(*a));
+}
+
+static void entry_extract(struct tree_desc *t, struct name_entry *a)
+{
+       *a = t->entry;
+}
+
+void update_tree_entry(struct tree_desc *desc)
+{
+       const void *buf = desc->buffer;
+       const unsigned char *end = desc->entry.sha1 + 20;
+       unsigned long size = desc->size;
+       unsigned long len = end - (const unsigned char *)buf;
+
+       if (size < len)
+               die("corrupt tree file");
+       buf = end;
+       size -= len;
+       desc->buffer = buf;
+       desc->size = size;
+       if (size)
+               decode_tree_entry(desc, buf, size);
+}
+
+int tree_entry(struct tree_desc *desc, struct name_entry *entry)
+{
+       if (!desc->size)
+               return 0;
+
+       *entry = desc->entry;
+       update_tree_entry(desc);
+       return 1;
+}
+
+void traverse_trees(int n, struct tree_desc *t, const char *base, traverse_callback_t callback)
+{
+       struct name_entry *entry = xmalloc(n*sizeof(*entry));
+
+       for (;;) {
+               unsigned long mask = 0;
+               int i, last;
+
+               last = -1;
+               for (i = 0; i < n; i++) {
+                       if (!t[i].size)
+                               continue;
+                       entry_extract(t+i, entry+i);
+                       if (last >= 0) {
+                               int cmp = entry_compare(entry+i, entry+last);
+
+                               /*
+                                * Is the new name bigger than the old one?
+                                * Ignore it
+                                */
+                               if (cmp > 0)
+                                       continue;
+                               /*
+                                * Is the new name smaller than the old one?
+                                * Ignore all old ones
+                                */
+                               if (cmp < 0)
+                                       mask = 0;
+                       }
+                       mask |= 1ul << i;
+                       last = i;
+               }
+               if (!mask)
+                       break;
+
+               /*
+                * Update the tree entries we've walked, and clear
+                * all the unused name-entries.
+                */
+               for (i = 0; i < n; i++) {
+                       if (mask & (1ul << i)) {
+                               update_tree_entry(t+i);
+                               continue;
+                       }
+                       entry_clear(entry + i);
+               }
+               callback(n, mask, entry, base);
+       }
+       free(entry);
+}
+
+static int find_tree_entry(struct tree_desc *t, const char *name, unsigned char *result, unsigned *mode)
+{
+       int namelen = strlen(name);
+       while (t->size) {
+               const char *entry;
+               const unsigned char *sha1;
+               int entrylen, cmp;
+
+               sha1 = tree_entry_extract(t, &entry, mode);
+               update_tree_entry(t);
+               entrylen = tree_entry_len(entry, sha1);
+               if (entrylen > namelen)
+                       continue;
+               cmp = memcmp(name, entry, entrylen);
+               if (cmp > 0)
+                       continue;
+               if (cmp < 0)
+                       break;
+               if (entrylen == namelen) {
+                       hashcpy(result, sha1);
+                       return 0;
+               }
+               if (name[entrylen] != '/')
+                       continue;
+               if (!S_ISDIR(*mode))
+                       break;
+               if (++entrylen == namelen) {
+                       hashcpy(result, sha1);
+                       return 0;
+               }
+               return get_tree_entry(sha1, name + entrylen, result, mode);
+       }
+       return -1;
+}
+
+int get_tree_entry(const unsigned char *tree_sha1, const char *name, unsigned char *sha1, unsigned *mode)
+{
+       int retval;
+       void *tree;
+       unsigned long size;
+       struct tree_desc t;
+       unsigned char root[20];
+
+       tree = read_object_with_reference(tree_sha1, tree_type, &size, root);
+       if (!tree)
+               return -1;
+
+       if (name[0] == '\0') {
+               hashcpy(sha1, root);
+               return 0;
+       }
+
+       init_tree_desc(&t, tree, size);
+       retval = find_tree_entry(&t, name, sha1, mode);
+       free(tree);
+       return retval;
+}
diff --git a/tree-walk.h b/tree-walk.h
new file mode 100644 (file)
index 0000000..db0fbdc
--- /dev/null
@@ -0,0 +1,42 @@
+#ifndef TREE_WALK_H
+#define TREE_WALK_H
+
+struct name_entry {
+       const unsigned char *sha1;
+       const char *path;
+       unsigned int mode;
+};
+
+struct tree_desc {
+       const void *buffer;
+       struct name_entry entry;
+       unsigned int size;
+};
+
+static inline const unsigned char *tree_entry_extract(struct tree_desc *desc, const char **pathp, unsigned int *modep)
+{
+       *pathp = desc->entry.path;
+       *modep = canon_mode(desc->entry.mode);
+       return desc->entry.sha1;
+}
+
+static inline int tree_entry_len(const char *name, const unsigned char *sha1)
+{
+       return (const char *)sha1 - name - 1;
+}
+
+void update_tree_entry(struct tree_desc *);
+void init_tree_desc(struct tree_desc *desc, const void *buf, unsigned long size);
+
+/* Helper function that does both of the above and returns true for success */
+int tree_entry(struct tree_desc *, struct name_entry *);
+
+void *fill_tree_descriptor(struct tree_desc *desc, const unsigned char *sha1);
+
+typedef void (*traverse_callback_t)(int n, unsigned long mask, struct name_entry *entry, const char *base);
+
+void traverse_trees(int n, struct tree_desc *t, const char *base, traverse_callback_t callback);
+
+int get_tree_entry(const unsigned char *, const char *, unsigned char *, unsigned *);
+
+#endif
diff --git a/tree.c b/tree.c
new file mode 100644 (file)
index 0000000..8c0819f
--- /dev/null
+++ b/tree.c
@@ -0,0 +1,301 @@
+#include "cache.h"
+#include "cache-tree.h"
+#include "tree.h"
+#include "blob.h"
+#include "commit.h"
+#include "tag.h"
+#include "tree-walk.h"
+
+const char *tree_type = "tree";
+
+static int read_one_entry_opt(const unsigned char *sha1, const char *base, int baselen, const char *pathname, unsigned mode, int stage, int opt)
+{
+       int len;
+       unsigned int size;
+       struct cache_entry *ce;
+
+       if (S_ISDIR(mode))
+               return READ_TREE_RECURSIVE;
+
+       len = strlen(pathname);
+       size = cache_entry_size(baselen + len);
+       ce = xcalloc(1, size);
+
+       ce->ce_mode = create_ce_mode(mode);
+       ce->ce_flags = create_ce_flags(baselen + len, stage);
+       memcpy(ce->name, base, baselen);
+       memcpy(ce->name + baselen, pathname, len+1);
+       hashcpy(ce->sha1, sha1);
+       return add_cache_entry(ce, opt);
+}
+
+static int read_one_entry(const unsigned char *sha1, const char *base, int baselen, const char *pathname, unsigned mode, int stage)
+{
+       return read_one_entry_opt(sha1, base, baselen, pathname, mode, stage,
+                                 ADD_CACHE_OK_TO_ADD|ADD_CACHE_SKIP_DFCHECK);
+}
+
+/*
+ * This is used when the caller knows there is no existing entries at
+ * the stage that will conflict with the entry being added.
+ */
+static int read_one_entry_quick(const unsigned char *sha1, const char *base, int baselen, const char *pathname, unsigned mode, int stage)
+{
+       return read_one_entry_opt(sha1, base, baselen, pathname, mode, stage,
+                                 ADD_CACHE_JUST_APPEND);
+}
+
+static int match_tree_entry(const char *base, int baselen, const char *path, unsigned int mode, const char **paths)
+{
+       const char *match;
+       int pathlen;
+
+       if (!paths)
+               return 1;
+       pathlen = strlen(path);
+       while ((match = *paths++) != NULL) {
+               int matchlen = strlen(match);
+
+               if (baselen >= matchlen) {
+                       /* If it doesn't match, move along... */
+                       if (strncmp(base, match, matchlen))
+                               continue;
+                       /* The base is a subdirectory of a path which was specified. */
+                       return 1;
+               }
+
+               /* Does the base match? */
+               if (strncmp(base, match, baselen))
+                       continue;
+
+               match += baselen;
+               matchlen -= baselen;
+
+               if (pathlen > matchlen)
+                       continue;
+
+               if (matchlen > pathlen) {
+                       if (match[pathlen] != '/')
+                               continue;
+                       if (!S_ISDIR(mode))
+                               continue;
+               }
+
+               if (strncmp(path, match, pathlen))
+                       continue;
+
+               return 1;
+       }
+       return 0;
+}
+
+int read_tree_recursive(struct tree *tree,
+                       const char *base, int baselen,
+                       int stage, const char **match,
+                       read_tree_fn_t fn)
+{
+       struct tree_desc desc;
+       struct name_entry entry;
+
+       if (parse_tree(tree))
+               return -1;
+
+       init_tree_desc(&desc, tree->buffer, tree->size);
+
+       while (tree_entry(&desc, &entry)) {
+               if (!match_tree_entry(base, baselen, entry.path, entry.mode, match))
+                       continue;
+
+               switch (fn(entry.sha1, base, baselen, entry.path, entry.mode, stage)) {
+               case 0:
+                       continue;
+               case READ_TREE_RECURSIVE:
+                       break;;
+               default:
+                       return -1;
+               }
+               if (S_ISDIR(entry.mode)) {
+                       int retval;
+                       char *newbase;
+                       unsigned int pathlen = tree_entry_len(entry.path, entry.sha1);
+
+                       newbase = xmalloc(baselen + 1 + pathlen);
+                       memcpy(newbase, base, baselen);
+                       memcpy(newbase + baselen, entry.path, pathlen);
+                       newbase[baselen + pathlen] = '/';
+                       retval = read_tree_recursive(lookup_tree(entry.sha1),
+                                                    newbase,
+                                                    baselen + pathlen + 1,
+                                                    stage, match, fn);
+                       free(newbase);
+                       if (retval)
+                               return -1;
+                       continue;
+               }
+       }
+       return 0;
+}
+
+static int cmp_cache_name_compare(const void *a_, const void *b_)
+{
+       const struct cache_entry *ce1, *ce2;
+
+       ce1 = *((const struct cache_entry **)a_);
+       ce2 = *((const struct cache_entry **)b_);
+       return cache_name_compare(ce1->name, ntohs(ce1->ce_flags),
+                                 ce2->name, ntohs(ce2->ce_flags));
+}
+
+int read_tree(struct tree *tree, int stage, const char **match)
+{
+       read_tree_fn_t fn = NULL;
+       int i, err;
+
+       /*
+        * Currently the only existing callers of this function all
+        * call it with stage=1 and after making sure there is nothing
+        * at that stage; we could always use read_one_entry_quick().
+        *
+        * But when we decide to straighten out git-read-tree not to
+        * use unpack_trees() in some cases, this will probably start
+        * to matter.
+        */
+
+       /*
+        * See if we have cache entry at the stage.  If so,
+        * do it the original slow way, otherwise, append and then
+        * sort at the end.
+        */
+       for (i = 0; !fn && i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (ce_stage(ce) == stage)
+                       fn = read_one_entry;
+       }
+
+       if (!fn)
+               fn = read_one_entry_quick;
+       err = read_tree_recursive(tree, "", 0, stage, match, fn);
+       if (fn == read_one_entry || err)
+               return err;
+
+       /*
+        * Sort the cache entry -- we need to nuke the cache tree, though.
+        */
+       cache_tree_free(&active_cache_tree);
+       qsort(active_cache, active_nr, sizeof(active_cache[0]),
+             cmp_cache_name_compare);
+       return 0;
+}
+
+struct tree *lookup_tree(const unsigned char *sha1)
+{
+       struct object *obj = lookup_object(sha1);
+       if (!obj)
+               return create_object(sha1, OBJ_TREE, alloc_tree_node());
+       if (!obj->type)
+               obj->type = OBJ_TREE;
+       if (obj->type != OBJ_TREE) {
+               error("Object %s is a %s, not a tree",
+                     sha1_to_hex(sha1), typename(obj->type));
+               return NULL;
+       }
+       return (struct tree *) obj;
+}
+
+/*
+ * NOTE! Tree refs to external git repositories
+ * (ie gitlinks) do not count as real references.
+ *
+ * You don't have to have those repositories
+ * available at all, much less have the objects
+ * accessible from the current repository.
+ */
+static void track_tree_refs(struct tree *item)
+{
+       int n_refs = 0, i;
+       struct object_refs *refs;
+       struct tree_desc desc;
+       struct name_entry entry;
+
+       /* Count how many entries there are.. */
+       init_tree_desc(&desc, item->buffer, item->size);
+       while (tree_entry(&desc, &entry)) {
+               if (S_ISGITLINK(entry.mode))
+                       continue;
+               n_refs++;
+       }
+
+       /* Allocate object refs and walk it again.. */
+       i = 0;
+       refs = alloc_object_refs(n_refs);
+       init_tree_desc(&desc, item->buffer, item->size);
+       while (tree_entry(&desc, &entry)) {
+               struct object *obj;
+
+               if (S_ISGITLINK(entry.mode))
+                       continue;
+               if (S_ISDIR(entry.mode))
+                       obj = &lookup_tree(entry.sha1)->object;
+               else if (S_ISREG(entry.mode) || S_ISLNK(entry.mode))
+                       obj = &lookup_blob(entry.sha1)->object;
+               else {
+                       warning("in tree %s: entry %s has bad mode %.6o\n",
+                            sha1_to_hex(item->object.sha1), entry.path, entry.mode);
+                       obj = lookup_unknown_object(entry.sha1);
+               }
+               refs->ref[i++] = obj;
+       }
+       set_object_refs(&item->object, refs);
+}
+
+int parse_tree_buffer(struct tree *item, void *buffer, unsigned long size)
+{
+       if (item->object.parsed)
+               return 0;
+       item->object.parsed = 1;
+       item->buffer = buffer;
+       item->size = size;
+
+       if (track_object_refs)
+               track_tree_refs(item);
+       return 0;
+}
+
+int parse_tree(struct tree *item)
+{
+        enum object_type type;
+        void *buffer;
+        unsigned long size;
+
+       if (item->object.parsed)
+               return 0;
+       buffer = read_sha1_file(item->object.sha1, &type, &size);
+       if (!buffer)
+               return error("Could not read %s",
+                            sha1_to_hex(item->object.sha1));
+       if (type != OBJ_TREE) {
+               free(buffer);
+               return error("Object %s not a tree",
+                            sha1_to_hex(item->object.sha1));
+       }
+       return parse_tree_buffer(item, buffer, size);
+}
+
+struct tree *parse_tree_indirect(const unsigned char *sha1)
+{
+       struct object *obj = parse_object(sha1);
+       do {
+               if (!obj)
+                       return NULL;
+               if (obj->type == OBJ_TREE)
+                       return (struct tree *) obj;
+               else if (obj->type == OBJ_COMMIT)
+                       obj = &(((struct commit *) obj)->tree->object);
+               else if (obj->type == OBJ_TAG)
+                       obj = ((struct tag *) obj)->tagged;
+               else
+                       return NULL;
+               if (!obj->parsed)
+                       parse_object(obj->sha1);
+       } while (1);
+}
diff --git a/tree.h b/tree.h
new file mode 100644 (file)
index 0000000..dd25c53
--- /dev/null
+++ b/tree.h
@@ -0,0 +1,33 @@
+#ifndef TREE_H
+#define TREE_H
+
+#include "object.h"
+
+extern const char *tree_type;
+
+struct tree {
+       struct object object;
+       void *buffer;
+       unsigned long size;
+};
+
+struct tree *lookup_tree(const unsigned char *sha1);
+
+int parse_tree_buffer(struct tree *item, void *buffer, unsigned long size);
+
+int parse_tree(struct tree *tree);
+
+/* Parses and returns the tree in the given ent, chasing tags and commits. */
+struct tree *parse_tree_indirect(const unsigned char *sha1);
+
+#define READ_TREE_RECURSIVE 1
+typedef int (*read_tree_fn_t)(const unsigned char *, const char *, int, const char *, unsigned int, int);
+
+extern int read_tree_recursive(struct tree *tree,
+                              const char *base, int baselen,
+                              int stage, const char **match,
+                              read_tree_fn_t fn);
+
+extern int read_tree(struct tree *tree, int stage, const char **paths);
+
+#endif /* TREE_H */
diff --git a/unpack-file.c b/unpack-file.c
new file mode 100644 (file)
index 0000000..65c66eb
--- /dev/null
@@ -0,0 +1,38 @@
+#include "cache.h"
+#include "blob.h"
+
+static char *create_temp_file(unsigned char *sha1)
+{
+       static char path[50];
+       void *buf;
+       enum object_type type;
+       unsigned long size;
+       int fd;
+
+       buf = read_sha1_file(sha1, &type, &size);
+       if (!buf || type != OBJ_BLOB)
+               die("unable to read blob object %s", sha1_to_hex(sha1));
+
+       strcpy(path, ".merge_file_XXXXXX");
+       fd = xmkstemp(path);
+       if (write_in_full(fd, buf, size) != size)
+               die("unable to write temp-file");
+       close(fd);
+       return path;
+}
+
+int main(int argc, char **argv)
+{
+       unsigned char sha1[20];
+
+       if (argc != 2)
+               usage("git-unpack-file <sha1>");
+       if (get_sha1(argv[1], sha1))
+               die("Not a valid object name %s", argv[1]);
+
+       setup_git_directory();
+       git_config(git_default_config);
+
+       puts(create_temp_file(sha1));
+       return 0;
+}
diff --git a/unpack-trees.c b/unpack-trees.c
new file mode 100644 (file)
index 0000000..e9eb795
--- /dev/null
@@ -0,0 +1,928 @@
+#include "cache.h"
+#include "dir.h"
+#include "tree.h"
+#include "tree-walk.h"
+#include "cache-tree.h"
+#include "unpack-trees.h"
+#include "progress.h"
+#include "refs.h"
+
+#define DBRT_DEBUG 1
+
+struct tree_entry_list {
+       struct tree_entry_list *next;
+       unsigned int mode;
+       const char *name;
+       const unsigned char *sha1;
+};
+
+static struct tree_entry_list *create_tree_entry_list(struct tree_desc *desc)
+{
+       struct name_entry one;
+       struct tree_entry_list *ret = NULL;
+       struct tree_entry_list **list_p = &ret;
+
+       while (tree_entry(desc, &one)) {
+               struct tree_entry_list *entry;
+
+               entry = xmalloc(sizeof(struct tree_entry_list));
+               entry->name = one.path;
+               entry->sha1 = one.sha1;
+               entry->mode = one.mode;
+               entry->next = NULL;
+
+               *list_p = entry;
+               list_p = &entry->next;
+       }
+       return ret;
+}
+
+static int entcmp(const char *name1, int dir1, const char *name2, int dir2)
+{
+       int len1 = strlen(name1);
+       int len2 = strlen(name2);
+       int len = len1 < len2 ? len1 : len2;
+       int ret = memcmp(name1, name2, len);
+       unsigned char c1, c2;
+       if (ret)
+               return ret;
+       c1 = name1[len];
+       c2 = name2[len];
+       if (!c1 && dir1)
+               c1 = '/';
+       if (!c2 && dir2)
+               c2 = '/';
+       ret = (c1 < c2) ? -1 : (c1 > c2) ? 1 : 0;
+       if (c1 && c2 && !ret)
+               ret = len1 - len2;
+       return ret;
+}
+
+static inline void remove_entry(int remove)
+{
+       if (remove >= 0)
+               remove_cache_entry_at(remove);
+}
+
+static int unpack_trees_rec(struct tree_entry_list **posns, int len,
+                           const char *base, struct unpack_trees_options *o,
+                           struct tree_entry_list *df_conflict_list)
+{
+       int remove;
+       int baselen = strlen(base);
+       int src_size = len + 1;
+       int retval = 0;
+
+       do {
+               int i;
+               const char *first;
+               int firstdir = 0;
+               int pathlen;
+               unsigned ce_size;
+               struct tree_entry_list **subposns;
+               struct cache_entry **src;
+               int any_files = 0;
+               int any_dirs = 0;
+               char *cache_name;
+               int ce_stage;
+
+               /* Find the first name in the input. */
+
+               first = NULL;
+               cache_name = NULL;
+
+               /* Check the cache */
+               if (o->merge && o->pos < active_nr) {
+                       /* This is a bit tricky: */
+                       /* If the index has a subdirectory (with
+                        * contents) as the first name, it'll get a
+                        * filename like "foo/bar". But that's after
+                        * "foo", so the entry in trees will get
+                        * handled first, at which point we'll go into
+                        * "foo", and deal with "bar" from the index,
+                        * because the base will be "foo/". The only
+                        * way we can actually have "foo/bar" first of
+                        * all the things is if the trees don't
+                        * contain "foo" at all, in which case we'll
+                        * handle "foo/bar" without going into the
+                        * directory, but that's fine (and will return
+                        * an error anyway, with the added unknown
+                        * file case.
+                        */
+
+                       cache_name = active_cache[o->pos]->name;
+                       if (strlen(cache_name) > baselen &&
+                           !memcmp(cache_name, base, baselen)) {
+                               cache_name += baselen;
+                               first = cache_name;
+                       } else {
+                               cache_name = NULL;
+                       }
+               }
+
+#if DBRT_DEBUG > 1
+               if (first)
+                       printf("index %s\n", first);
+#endif
+               for (i = 0; i < len; i++) {
+                       if (!posns[i] || posns[i] == df_conflict_list)
+                               continue;
+#if DBRT_DEBUG > 1
+                       printf("%d %s\n", i + 1, posns[i]->name);
+#endif
+                       if (!first || entcmp(first, firstdir,
+                                            posns[i]->name,
+                                            S_ISDIR(posns[i]->mode)) > 0) {
+                               first = posns[i]->name;
+                               firstdir = S_ISDIR(posns[i]->mode);
+                       }
+               }
+               /* No name means we're done */
+               if (!first)
+                       goto leave_directory;
+
+               pathlen = strlen(first);
+               ce_size = cache_entry_size(baselen + pathlen);
+
+               src = xcalloc(src_size, sizeof(struct cache_entry *));
+
+               subposns = xcalloc(len, sizeof(struct tree_list_entry *));
+
+               remove = -1;
+               if (cache_name && !strcmp(cache_name, first)) {
+                       any_files = 1;
+                       src[0] = active_cache[o->pos];
+                       remove = o->pos;
+               }
+
+               for (i = 0; i < len; i++) {
+                       struct cache_entry *ce;
+
+                       if (!posns[i] ||
+                           (posns[i] != df_conflict_list &&
+                            strcmp(first, posns[i]->name))) {
+                               continue;
+                       }
+
+                       if (posns[i] == df_conflict_list) {
+                               src[i + o->merge] = o->df_conflict_entry;
+                               continue;
+                       }
+
+                       if (S_ISDIR(posns[i]->mode)) {
+                               struct tree *tree = lookup_tree(posns[i]->sha1);
+                               struct tree_desc t;
+                               any_dirs = 1;
+                               parse_tree(tree);
+                               init_tree_desc(&t, tree->buffer, tree->size);
+                               subposns[i] = create_tree_entry_list(&t);
+                               posns[i] = posns[i]->next;
+                               src[i + o->merge] = o->df_conflict_entry;
+                               continue;
+                       }
+
+                       if (!o->merge)
+                               ce_stage = 0;
+                       else if (i + 1 < o->head_idx)
+                               ce_stage = 1;
+                       else if (i + 1 > o->head_idx)
+                               ce_stage = 3;
+                       else
+                               ce_stage = 2;
+
+                       ce = xcalloc(1, ce_size);
+                       ce->ce_mode = create_ce_mode(posns[i]->mode);
+                       ce->ce_flags = create_ce_flags(baselen + pathlen,
+                                                      ce_stage);
+                       memcpy(ce->name, base, baselen);
+                       memcpy(ce->name + baselen, first, pathlen + 1);
+
+                       any_files = 1;
+
+                       hashcpy(ce->sha1, posns[i]->sha1);
+                       src[i + o->merge] = ce;
+                       subposns[i] = df_conflict_list;
+                       posns[i] = posns[i]->next;
+               }
+               if (any_files) {
+                       if (o->merge) {
+                               int ret;
+
+#if DBRT_DEBUG > 1
+                               printf("%s:\n", first);
+                               for (i = 0; i < src_size; i++) {
+                                       printf(" %d ", i);
+                                       if (src[i])
+                                               printf("%s\n", sha1_to_hex(src[i]->sha1));
+                                       else
+                                               printf("\n");
+                               }
+#endif
+                               ret = o->fn(src, o, remove);
+
+#if DBRT_DEBUG > 1
+                               printf("Added %d entries\n", ret);
+#endif
+                               o->pos += ret;
+                       } else {
+                               remove_entry(remove);
+                               for (i = 0; i < src_size; i++) {
+                                       if (src[i]) {
+                                               add_cache_entry(src[i], ADD_CACHE_OK_TO_ADD|ADD_CACHE_SKIP_DFCHECK);
+                                       }
+                               }
+                       }
+               }
+               if (any_dirs) {
+                       char *newbase = xmalloc(baselen + 2 + pathlen);
+                       memcpy(newbase, base, baselen);
+                       memcpy(newbase + baselen, first, pathlen);
+                       newbase[baselen + pathlen] = '/';
+                       newbase[baselen + pathlen + 1] = '\0';
+                       if (unpack_trees_rec(subposns, len, newbase, o,
+                                            df_conflict_list)) {
+                               retval = -1;
+                               goto leave_directory;
+                       }
+                       free(newbase);
+               }
+               free(subposns);
+               free(src);
+       } while (1);
+
+ leave_directory:
+       return retval;
+}
+
+/* Unlink the last component and attempt to remove leading
+ * directories, in case this unlink is the removal of the
+ * last entry in the directory -- empty directories are removed.
+ */
+static void unlink_entry(char *name, char *last_symlink)
+{
+       char *cp, *prev;
+
+       if (has_symlink_leading_path(name, last_symlink))
+               return;
+       if (unlink(name))
+               return;
+       prev = NULL;
+       while (1) {
+               int status;
+               cp = strrchr(name, '/');
+               if (prev)
+                       *prev = '/';
+               if (!cp)
+                       break;
+
+               *cp = 0;
+               status = rmdir(name);
+               if (status) {
+                       *cp = '/';
+                       break;
+               }
+               prev = cp;
+       }
+}
+
+static struct checkout state;
+static void check_updates(struct cache_entry **src, int nr,
+                       struct unpack_trees_options *o)
+{
+       unsigned short mask = htons(CE_UPDATE);
+       unsigned cnt = 0, total = 0;
+       struct progress *progress = NULL;
+       char last_symlink[PATH_MAX];
+
+       if (o->update && o->verbose_update) {
+               for (total = cnt = 0; cnt < nr; cnt++) {
+                       struct cache_entry *ce = src[cnt];
+                       if (!ce->ce_mode || ce->ce_flags & mask)
+                               total++;
+               }
+
+               progress = start_progress_delay("Checking out files",
+                                               total, 50, 2);
+               cnt = 0;
+       }
+
+       *last_symlink = '\0';
+       while (nr--) {
+               struct cache_entry *ce = *src++;
+
+               if (!ce->ce_mode || ce->ce_flags & mask)
+                       display_progress(progress, ++cnt);
+               if (!ce->ce_mode) {
+                       if (o->update)
+                               unlink_entry(ce->name, last_symlink);
+                       continue;
+               }
+               if (ce->ce_flags & mask) {
+                       ce->ce_flags &= ~mask;
+                       if (o->update) {
+                               checkout_entry(ce, &state, NULL);
+                               *last_symlink = '\0';
+                       }
+               }
+       }
+       stop_progress(&progress);
+}
+
+int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options *o)
+{
+       struct tree_entry_list **posns;
+       int i;
+       struct tree_entry_list df_conflict_list;
+       static struct cache_entry *dfc;
+
+       memset(&df_conflict_list, 0, sizeof(df_conflict_list));
+       df_conflict_list.next = &df_conflict_list;
+       memset(&state, 0, sizeof(state));
+       state.base_dir = "";
+       state.force = 1;
+       state.quiet = 1;
+       state.refresh_cache = 1;
+
+       o->merge_size = len;
+
+       if (!dfc)
+               dfc = xcalloc(1, sizeof(struct cache_entry) + 1);
+       o->df_conflict_entry = dfc;
+
+       if (len) {
+               posns = xmalloc(len * sizeof(struct tree_entry_list *));
+               for (i = 0; i < len; i++)
+                       posns[i] = create_tree_entry_list(t+i);
+
+               if (unpack_trees_rec(posns, len, o->prefix ? o->prefix : "",
+                                    o, &df_conflict_list))
+                       return -1;
+       }
+
+       if (o->trivial_merges_only && o->nontrivial_merge)
+               die("Merge requires file-level merging");
+
+       check_updates(active_cache, active_nr, o);
+       return 0;
+}
+
+/* Here come the merge functions */
+
+static void reject_merge(struct cache_entry *ce)
+{
+       die("Entry '%s' would be overwritten by merge. Cannot merge.",
+           ce->name);
+}
+
+static int same(struct cache_entry *a, struct cache_entry *b)
+{
+       if (!!a != !!b)
+               return 0;
+       if (!a && !b)
+               return 1;
+       return a->ce_mode == b->ce_mode &&
+              !hashcmp(a->sha1, b->sha1);
+}
+
+
+/*
+ * When a CE gets turned into an unmerged entry, we
+ * want it to be up-to-date
+ */
+static void verify_uptodate(struct cache_entry *ce,
+               struct unpack_trees_options *o)
+{
+       struct stat st;
+
+       if (o->index_only || o->reset)
+               return;
+
+       if (!lstat(ce->name, &st)) {
+               unsigned changed = ce_match_stat(ce, &st, CE_MATCH_IGNORE_VALID);
+               if (!changed)
+                       return;
+               /*
+                * NEEDSWORK: the current default policy is to allow
+                * submodule to be out of sync wrt the supermodule
+                * index.  This needs to be tightened later for
+                * submodules that are marked to be automatically
+                * checked out.
+                */
+               if (S_ISGITLINK(ntohl(ce->ce_mode)))
+                       return;
+               errno = 0;
+       }
+       if (errno == ENOENT)
+               return;
+       die("Entry '%s' not uptodate. Cannot merge.", ce->name);
+}
+
+static void invalidate_ce_path(struct cache_entry *ce)
+{
+       if (ce)
+               cache_tree_invalidate_path(active_cache_tree, ce->name);
+}
+
+/*
+ * Check that checking out ce->sha1 in subdir ce->name is not
+ * going to overwrite any working files.
+ *
+ * Currently, git does not checkout subprojects during a superproject
+ * checkout, so it is not going to overwrite anything.
+ */
+static int verify_clean_submodule(struct cache_entry *ce, const char *action,
+                                     struct unpack_trees_options *o)
+{
+       return 0;
+}
+
+static int verify_clean_subdirectory(struct cache_entry *ce, const char *action,
+                                     struct unpack_trees_options *o)
+{
+       /*
+        * we are about to extract "ce->name"; we would not want to lose
+        * anything in the existing directory there.
+        */
+       int namelen;
+       int pos, i;
+       struct dir_struct d;
+       char *pathbuf;
+       int cnt = 0;
+       unsigned char sha1[20];
+
+       if (S_ISGITLINK(ntohl(ce->ce_mode)) &&
+           resolve_gitlink_ref(ce->name, "HEAD", sha1) == 0) {
+               /* If we are not going to update the submodule, then
+                * we don't care.
+                */
+               if (!hashcmp(sha1, ce->sha1))
+                       return 0;
+               return verify_clean_submodule(ce, action, o);
+       }
+
+       /*
+        * First let's make sure we do not have a local modification
+        * in that directory.
+        */
+       namelen = strlen(ce->name);
+       pos = cache_name_pos(ce->name, namelen);
+       if (0 <= pos)
+               return cnt; /* we have it as nondirectory */
+       pos = -pos - 1;
+       for (i = pos; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               int len = ce_namelen(ce);
+               if (len < namelen ||
+                   strncmp(ce->name, ce->name, namelen) ||
+                   ce->name[namelen] != '/')
+                       break;
+               /*
+                * ce->name is an entry in the subdirectory.
+                */
+               if (!ce_stage(ce)) {
+                       verify_uptodate(ce, o);
+                       ce->ce_mode = 0;
+               }
+               cnt++;
+       }
+
+       /*
+        * Then we need to make sure that we do not lose a locally
+        * present file that is not ignored.
+        */
+       pathbuf = xmalloc(namelen + 2);
+       memcpy(pathbuf, ce->name, namelen);
+       strcpy(pathbuf+namelen, "/");
+
+       memset(&d, 0, sizeof(d));
+       if (o->dir)
+               d.exclude_per_dir = o->dir->exclude_per_dir;
+       i = read_directory(&d, ce->name, pathbuf, namelen+1, NULL);
+       if (i)
+               die("Updating '%s' would lose untracked files in it",
+                   ce->name);
+       free(pathbuf);
+       return cnt;
+}
+
+/*
+ * We do not want to remove or overwrite a working tree file that
+ * is not tracked, unless it is ignored.
+ */
+static void verify_absent(struct cache_entry *ce, const char *action,
+               struct unpack_trees_options *o)
+{
+       struct stat st;
+
+       if (o->index_only || o->reset || !o->update)
+               return;
+
+       if (has_symlink_leading_path(ce->name, NULL))
+               return;
+
+       if (!lstat(ce->name, &st)) {
+               int cnt;
+
+               if (o->dir && excluded(o->dir, ce->name))
+                       /*
+                        * ce->name is explicitly excluded, so it is Ok to
+                        * overwrite it.
+                        */
+                       return;
+               if (S_ISDIR(st.st_mode)) {
+                       /*
+                        * We are checking out path "foo" and
+                        * found "foo/." in the working tree.
+                        * This is tricky -- if we have modified
+                        * files that are in "foo/" we would lose
+                        * it.
+                        */
+                       cnt = verify_clean_subdirectory(ce, action, o);
+
+                       /*
+                        * If this removed entries from the index,
+                        * what that means is:
+                        *
+                        * (1) the caller unpack_trees_rec() saw path/foo
+                        * in the index, and it has not removed it because
+                        * it thinks it is handling 'path' as blob with
+                        * D/F conflict;
+                        * (2) we will return "ok, we placed a merged entry
+                        * in the index" which would cause o->pos to be
+                        * incremented by one;
+                        * (3) however, original o->pos now has 'path/foo'
+                        * marked with "to be removed".
+                        *
+                        * We need to increment it by the number of
+                        * deleted entries here.
+                        */
+                       o->pos += cnt;
+                       return;
+               }
+
+               /*
+                * The previous round may already have decided to
+                * delete this path, which is in a subdirectory that
+                * is being replaced with a blob.
+                */
+               cnt = cache_name_pos(ce->name, strlen(ce->name));
+               if (0 <= cnt) {
+                       struct cache_entry *ce = active_cache[cnt];
+                       if (!ce_stage(ce) && !ce->ce_mode)
+                               return;
+               }
+
+               die("Untracked working tree file '%s' "
+                   "would be %s by merge.", ce->name, action);
+       }
+}
+
+static int merged_entry(struct cache_entry *merge, struct cache_entry *old,
+               struct unpack_trees_options *o)
+{
+       merge->ce_flags |= htons(CE_UPDATE);
+       if (old) {
+               /*
+                * See if we can re-use the old CE directly?
+                * That way we get the uptodate stat info.
+                *
+                * This also removes the UPDATE flag on
+                * a match.
+                */
+               if (same(old, merge)) {
+                       *merge = *old;
+               } else {
+                       verify_uptodate(old, o);
+                       invalidate_ce_path(old);
+               }
+       }
+       else {
+               verify_absent(merge, "overwritten", o);
+               invalidate_ce_path(merge);
+       }
+
+       merge->ce_flags &= ~htons(CE_STAGEMASK);
+       add_cache_entry(merge, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE);
+       return 1;
+}
+
+static int deleted_entry(struct cache_entry *ce, struct cache_entry *old,
+               struct unpack_trees_options *o)
+{
+       if (old)
+               verify_uptodate(old, o);
+       else
+               verify_absent(ce, "removed", o);
+       ce->ce_mode = 0;
+       add_cache_entry(ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE);
+       invalidate_ce_path(ce);
+       return 1;
+}
+
+static int keep_entry(struct cache_entry *ce, struct unpack_trees_options *o)
+{
+       add_cache_entry(ce, ADD_CACHE_OK_TO_ADD);
+       return 1;
+}
+
+#if DBRT_DEBUG
+static void show_stage_entry(FILE *o,
+                            const char *label, const struct cache_entry *ce)
+{
+       if (!ce)
+               fprintf(o, "%s (missing)\n", label);
+       else
+               fprintf(o, "%s%06o %s %d\t%s\n",
+                       label,
+                       ntohl(ce->ce_mode),
+                       sha1_to_hex(ce->sha1),
+                       ce_stage(ce),
+                       ce->name);
+}
+#endif
+
+int threeway_merge(struct cache_entry **stages,
+               struct unpack_trees_options *o,
+               int remove)
+{
+       struct cache_entry *index;
+       struct cache_entry *head;
+       struct cache_entry *remote = stages[o->head_idx + 1];
+       int count;
+       int head_match = 0;
+       int remote_match = 0;
+
+       int df_conflict_head = 0;
+       int df_conflict_remote = 0;
+
+       int any_anc_missing = 0;
+       int no_anc_exists = 1;
+       int i;
+
+       for (i = 1; i < o->head_idx; i++) {
+               if (!stages[i] || stages[i] == o->df_conflict_entry)
+                       any_anc_missing = 1;
+               else
+                       no_anc_exists = 0;
+       }
+
+       index = stages[0];
+       head = stages[o->head_idx];
+
+       if (head == o->df_conflict_entry) {
+               df_conflict_head = 1;
+               head = NULL;
+       }
+
+       if (remote == o->df_conflict_entry) {
+               df_conflict_remote = 1;
+               remote = NULL;
+       }
+
+       /* First, if there's a #16 situation, note that to prevent #13
+        * and #14.
+        */
+       if (!same(remote, head)) {
+               for (i = 1; i < o->head_idx; i++) {
+                       if (same(stages[i], head)) {
+                               head_match = i;
+                       }
+                       if (same(stages[i], remote)) {
+                               remote_match = i;
+                       }
+               }
+       }
+
+       /* We start with cases where the index is allowed to match
+        * something other than the head: #14(ALT) and #2ALT, where it
+        * is permitted to match the result instead.
+        */
+       /* #14, #14ALT, #2ALT */
+       if (remote && !df_conflict_head && head_match && !remote_match) {
+               if (index && !same(index, remote) && !same(index, head))
+                       reject_merge(index);
+               return merged_entry(remote, index, o);
+       }
+       /*
+        * If we have an entry in the index cache, then we want to
+        * make sure that it matches head.
+        */
+       if (index && !same(index, head)) {
+               reject_merge(index);
+       }
+
+       if (head) {
+               /* #5ALT, #15 */
+               if (same(head, remote))
+                       return merged_entry(head, index, o);
+               /* #13, #3ALT */
+               if (!df_conflict_remote && remote_match && !head_match)
+                       return merged_entry(head, index, o);
+       }
+
+       /* #1 */
+       if (!head && !remote && any_anc_missing) {
+               remove_entry(remove);
+               return 0;
+       }
+
+       /* Under the new "aggressive" rule, we resolve mostly trivial
+        * cases that we historically had git-merge-one-file resolve.
+        */
+       if (o->aggressive) {
+               int head_deleted = !head && !df_conflict_head;
+               int remote_deleted = !remote && !df_conflict_remote;
+               struct cache_entry *ce = NULL;
+
+               if (index)
+                       ce = index;
+               else if (head)
+                       ce = head;
+               else if (remote)
+                       ce = remote;
+               else {
+                       for (i = 1; i < o->head_idx; i++) {
+                               if (stages[i] && stages[i] != o->df_conflict_entry) {
+                                       ce = stages[i];
+                                       break;
+                               }
+                       }
+               }
+
+               /*
+                * Deleted in both.
+                * Deleted in one and unchanged in the other.
+                */
+               if ((head_deleted && remote_deleted) ||
+                   (head_deleted && remote && remote_match) ||
+                   (remote_deleted && head && head_match)) {
+                       remove_entry(remove);
+                       if (index)
+                               return deleted_entry(index, index, o);
+                       else if (ce && !head_deleted)
+                               verify_absent(ce, "removed", o);
+                       return 0;
+               }
+               /*
+                * Added in both, identically.
+                */
+               if (no_anc_exists && head && remote && same(head, remote))
+                       return merged_entry(head, index, o);
+
+       }
+
+       /* Below are "no merge" cases, which require that the index be
+        * up-to-date to avoid the files getting overwritten with
+        * conflict resolution files.
+        */
+       if (index) {
+               verify_uptodate(index, o);
+       }
+
+       remove_entry(remove);
+       o->nontrivial_merge = 1;
+
+       /* #2, #3, #4, #6, #7, #9, #10, #11. */
+       count = 0;
+       if (!head_match || !remote_match) {
+               for (i = 1; i < o->head_idx; i++) {
+                       if (stages[i] && stages[i] != o->df_conflict_entry) {
+                               keep_entry(stages[i], o);
+                               count++;
+                               break;
+                       }
+               }
+       }
+#if DBRT_DEBUG
+       else {
+               fprintf(stderr, "read-tree: warning #16 detected\n");
+               show_stage_entry(stderr, "head   ", stages[head_match]);
+               show_stage_entry(stderr, "remote ", stages[remote_match]);
+       }
+#endif
+       if (head) { count += keep_entry(head, o); }
+       if (remote) { count += keep_entry(remote, o); }
+       return count;
+}
+
+/*
+ * Two-way merge.
+ *
+ * The rule is to "carry forward" what is in the index without losing
+ * information across a "fast forward", favoring a successful merge
+ * over a merge failure when it makes sense.  For details of the
+ * "carry forward" rule, please see <Documentation/git-read-tree.txt>.
+ *
+ */
+int twoway_merge(struct cache_entry **src,
+               struct unpack_trees_options *o,
+               int remove)
+{
+       struct cache_entry *current = src[0];
+       struct cache_entry *oldtree = src[1];
+       struct cache_entry *newtree = src[2];
+
+       if (o->merge_size != 2)
+               return error("Cannot do a twoway merge of %d trees",
+                            o->merge_size);
+
+       if (oldtree == o->df_conflict_entry)
+               oldtree = NULL;
+       if (newtree == o->df_conflict_entry)
+               newtree = NULL;
+
+       if (current) {
+               if ((!oldtree && !newtree) || /* 4 and 5 */
+                   (!oldtree && newtree &&
+                    same(current, newtree)) || /* 6 and 7 */
+                   (oldtree && newtree &&
+                    same(oldtree, newtree)) || /* 14 and 15 */
+                   (oldtree && newtree &&
+                    !same(oldtree, newtree) && /* 18 and 19 */
+                    same(current, newtree))) {
+                       return keep_entry(current, o);
+               }
+               else if (oldtree && !newtree && same(current, oldtree)) {
+                       /* 10 or 11 */
+                       remove_entry(remove);
+                       return deleted_entry(oldtree, current, o);
+               }
+               else if (oldtree && newtree &&
+                        same(current, oldtree) && !same(current, newtree)) {
+                       /* 20 or 21 */
+                       return merged_entry(newtree, current, o);
+               }
+               else {
+                       /* all other failures */
+                       remove_entry(remove);
+                       if (oldtree)
+                               reject_merge(oldtree);
+                       if (current)
+                               reject_merge(current);
+                       if (newtree)
+                               reject_merge(newtree);
+                       return -1;
+               }
+       }
+       else if (newtree)
+               return merged_entry(newtree, current, o);
+       remove_entry(remove);
+       return deleted_entry(oldtree, current, o);
+}
+
+/*
+ * Bind merge.
+ *
+ * Keep the index entries at stage0, collapse stage1 but make sure
+ * stage0 does not have anything there.
+ */
+int bind_merge(struct cache_entry **src,
+               struct unpack_trees_options *o,
+               int remove)
+{
+       struct cache_entry *old = src[0];
+       struct cache_entry *a = src[1];
+
+       if (o->merge_size != 1)
+               return error("Cannot do a bind merge of %d trees\n",
+                            o->merge_size);
+       if (a && old)
+               die("Entry '%s' overlaps.  Cannot bind.", a->name);
+       if (!a)
+               return keep_entry(old, o);
+       else
+               return merged_entry(a, NULL, o);
+}
+
+/*
+ * One-way merge.
+ *
+ * The rule is:
+ * - take the stat information from stage0, take the data from stage1
+ */
+int oneway_merge(struct cache_entry **src,
+               struct unpack_trees_options *o,
+               int remove)
+{
+       struct cache_entry *old = src[0];
+       struct cache_entry *a = src[1];
+
+       if (o->merge_size != 1)
+               return error("Cannot do a oneway merge of %d trees",
+                            o->merge_size);
+
+       if (!a) {
+               remove_entry(remove);
+               return deleted_entry(old, old, o);
+       }
+       if (old && same(old, a)) {
+               if (o->reset) {
+                       struct stat st;
+                       if (lstat(old->name, &st) ||
+                           ce_match_stat(old, &st, CE_MATCH_IGNORE_VALID))
+                               old->ce_flags |= htons(CE_UPDATE);
+               }
+               return keep_entry(old, o);
+       }
+       return merged_entry(a, old, o);
+}
diff --git a/unpack-trees.h b/unpack-trees.h
new file mode 100644 (file)
index 0000000..5517faa
--- /dev/null
@@ -0,0 +1,38 @@
+#ifndef UNPACK_TREES_H
+#define UNPACK_TREES_H
+
+struct unpack_trees_options;
+
+typedef int (*merge_fn_t)(struct cache_entry **src,
+               struct unpack_trees_options *options,
+               int remove);
+
+struct unpack_trees_options {
+       int reset;
+       int merge;
+       int update;
+       int index_only;
+       int nontrivial_merge;
+       int trivial_merges_only;
+       int verbose_update;
+       int aggressive;
+       const char *prefix;
+       int pos;
+       struct dir_struct *dir;
+       merge_fn_t fn;
+
+       int head_idx;
+       int merge_size;
+
+       struct cache_entry *df_conflict_entry;
+};
+
+extern int unpack_trees(unsigned n, struct tree_desc *t,
+               struct unpack_trees_options *options);
+
+int threeway_merge(struct cache_entry **stages, struct unpack_trees_options *o, int);
+int twoway_merge(struct cache_entry **src, struct unpack_trees_options *o, int);
+int bind_merge(struct cache_entry **src, struct unpack_trees_options *o, int);
+int oneway_merge(struct cache_entry **src, struct unpack_trees_options *o, int);
+
+#endif
diff --git a/update-server-info.c b/update-server-info.c
new file mode 100644 (file)
index 0000000..0b6c383
--- /dev/null
@@ -0,0 +1,25 @@
+#include "cache.h"
+
+static const char update_server_info_usage[] =
+"git-update-server-info [--force]";
+
+int main(int ac, char **av)
+{
+       int i;
+       int force = 0;
+       for (i = 1; i < ac; i++) {
+               if (av[i][0] == '-') {
+                       if (!strcmp("--force", av[i]) ||
+                           !strcmp("-f", av[i]))
+                               force = 1;
+                       else
+                               usage(update_server_info_usage);
+               }
+       }
+       if (i != ac)
+               usage(update_server_info_usage);
+
+       setup_git_directory();
+
+       return !!update_server_info(force);
+}
diff --git a/upload-pack.c b/upload-pack.c
new file mode 100644 (file)
index 0000000..7e04311
--- /dev/null
@@ -0,0 +1,631 @@
+#include "cache.h"
+#include "refs.h"
+#include "pkt-line.h"
+#include "sideband.h"
+#include "tag.h"
+#include "object.h"
+#include "commit.h"
+#include "exec_cmd.h"
+#include "diff.h"
+#include "revision.h"
+#include "list-objects.h"
+#include "run-command.h"
+
+static const char upload_pack_usage[] = "git-upload-pack [--strict] [--timeout=nn] <dir>";
+
+/* bits #0..7 in revision.h, #8..10 in commit.c */
+#define THEY_HAVE      (1u << 11)
+#define OUR_REF                (1u << 12)
+#define WANTED         (1u << 13)
+#define COMMON_KNOWN   (1u << 14)
+#define REACHABLE      (1u << 15)
+
+#define SHALLOW                (1u << 16)
+#define NOT_SHALLOW    (1u << 17)
+#define CLIENT_SHALLOW (1u << 18)
+
+static unsigned long oldest_have;
+
+static int multi_ack, nr_our_refs;
+static int use_thin_pack, use_ofs_delta, no_progress;
+static struct object_array have_obj;
+static struct object_array want_obj;
+static unsigned int timeout;
+/* 0 for no sideband,
+ * otherwise maximum packet size (up to 65520 bytes).
+ */
+static int use_sideband;
+
+static void reset_timeout(void)
+{
+       alarm(timeout);
+}
+
+static int strip(char *line, int len)
+{
+       if (len && line[len-1] == '\n')
+               line[--len] = 0;
+       return len;
+}
+
+static ssize_t send_client_data(int fd, const char *data, ssize_t sz)
+{
+       if (use_sideband)
+               return send_sideband(1, fd, data, sz, use_sideband);
+       if (fd == 3)
+               /* emergency quit */
+               fd = 2;
+       if (fd == 2) {
+               /* XXX: are we happy to lose stuff here? */
+               xwrite(fd, data, sz);
+               return sz;
+       }
+       return safe_write(fd, data, sz);
+}
+
+static FILE *pack_pipe = NULL;
+static void show_commit(struct commit *commit)
+{
+       if (commit->object.flags & BOUNDARY)
+               fputc('-', pack_pipe);
+       if (fputs(sha1_to_hex(commit->object.sha1), pack_pipe) < 0)
+               die("broken output pipe");
+       fputc('\n', pack_pipe);
+       fflush(pack_pipe);
+       free(commit->buffer);
+       commit->buffer = NULL;
+}
+
+static void show_object(struct object_array_entry *p)
+{
+       /* An object with name "foo\n0000000..." can be used to
+        * confuse downstream git-pack-objects very badly.
+        */
+       const char *ep = strchr(p->name, '\n');
+       if (ep) {
+               fprintf(pack_pipe, "%s %.*s\n", sha1_to_hex(p->item->sha1),
+                      (int) (ep - p->name),
+                      p->name);
+       }
+       else
+               fprintf(pack_pipe, "%s %s\n",
+                               sha1_to_hex(p->item->sha1), p->name);
+}
+
+static void show_edge(struct commit *commit)
+{
+       fprintf(pack_pipe, "-%s\n", sha1_to_hex(commit->object.sha1));
+}
+
+static int do_rev_list(int fd, void *create_full_pack)
+{
+       int i;
+       struct rev_info revs;
+
+       pack_pipe = fdopen(fd, "w");
+       if (create_full_pack)
+               use_thin_pack = 0; /* no point doing it */
+       init_revisions(&revs, NULL);
+       revs.tag_objects = 1;
+       revs.tree_objects = 1;
+       revs.blob_objects = 1;
+       if (use_thin_pack)
+               revs.edge_hint = 1;
+
+       if (create_full_pack) {
+               const char *args[] = {"rev-list", "--all", NULL};
+               setup_revisions(2, args, &revs, NULL);
+       } else {
+               for (i = 0; i < want_obj.nr; i++) {
+                       struct object *o = want_obj.objects[i].item;
+                       /* why??? */
+                       o->flags &= ~UNINTERESTING;
+                       add_pending_object(&revs, o, NULL);
+               }
+               for (i = 0; i < have_obj.nr; i++) {
+                       struct object *o = have_obj.objects[i].item;
+                       o->flags |= UNINTERESTING;
+                       add_pending_object(&revs, o, NULL);
+               }
+               setup_revisions(0, NULL, &revs, NULL);
+       }
+       prepare_revision_walk(&revs);
+       mark_edges_uninteresting(revs.commits, &revs, show_edge);
+       traverse_commit_list(&revs, show_commit, show_object);
+       return 0;
+}
+
+static void create_pack_file(void)
+{
+       struct async rev_list;
+       struct child_process pack_objects;
+       int create_full_pack = (nr_our_refs == want_obj.nr && !have_obj.nr);
+       char data[8193], progress[128];
+       char abort_msg[] = "aborting due to possible repository "
+               "corruption on the remote side.";
+       int buffered = -1;
+       ssize_t sz;
+       const char *argv[10];
+       int arg = 0;
+
+       rev_list.proc = do_rev_list;
+       /* .data is just a boolean: any non-NULL value will do */
+       rev_list.data = create_full_pack ? &rev_list : NULL;
+       if (start_async(&rev_list))
+               die("git-upload-pack: unable to fork git-rev-list");
+
+       argv[arg++] = "pack-objects";
+       argv[arg++] = "--stdout";
+       if (!no_progress)
+               argv[arg++] = "--progress";
+       if (use_ofs_delta)
+               argv[arg++] = "--delta-base-offset";
+       argv[arg++] = NULL;
+
+       memset(&pack_objects, 0, sizeof(pack_objects));
+       pack_objects.in = rev_list.out; /* start_command closes it */
+       pack_objects.out = -1;
+       pack_objects.err = -1;
+       pack_objects.git_cmd = 1;
+       pack_objects.argv = argv;
+
+       if (start_command(&pack_objects))
+               die("git-upload-pack: unable to fork git-pack-objects");
+
+       /* We read from pack_objects.err to capture stderr output for
+        * progress bar, and pack_objects.out to capture the pack data.
+        */
+
+       while (1) {
+               struct pollfd pfd[2];
+               int pe, pu, pollsize;
+
+               reset_timeout();
+
+               pollsize = 0;
+               pe = pu = -1;
+
+               if (0 <= pack_objects.out) {
+                       pfd[pollsize].fd = pack_objects.out;
+                       pfd[pollsize].events = POLLIN;
+                       pu = pollsize;
+                       pollsize++;
+               }
+               if (0 <= pack_objects.err) {
+                       pfd[pollsize].fd = pack_objects.err;
+                       pfd[pollsize].events = POLLIN;
+                       pe = pollsize;
+                       pollsize++;
+               }
+
+               if (!pollsize)
+                       break;
+
+               if (poll(pfd, pollsize, -1) < 0) {
+                       if (errno != EINTR) {
+                               error("poll failed, resuming: %s",
+                                     strerror(errno));
+                               sleep(1);
+                       }
+                       continue;
+               }
+               if (0 <= pu && (pfd[pu].revents & (POLLIN|POLLHUP))) {
+                       /* Data ready; we keep the last byte to ourselves
+                        * in case we detect broken rev-list, so that we
+                        * can leave the stream corrupted.  This is
+                        * unfortunate -- unpack-objects would happily
+                        * accept a valid packdata with trailing garbage,
+                        * so appending garbage after we pass all the
+                        * pack data is not good enough to signal
+                        * breakage to downstream.
+                        */
+                       char *cp = data;
+                       ssize_t outsz = 0;
+                       if (0 <= buffered) {
+                               *cp++ = buffered;
+                               outsz++;
+                       }
+                       sz = xread(pack_objects.out, cp,
+                                 sizeof(data) - outsz);
+                       if (0 < sz)
+                                       ;
+                       else if (sz == 0) {
+                               close(pack_objects.out);
+                               pack_objects.out = -1;
+                       }
+                       else
+                               goto fail;
+                       sz += outsz;
+                       if (1 < sz) {
+                               buffered = data[sz-1] & 0xFF;
+                               sz--;
+                       }
+                       else
+                               buffered = -1;
+                       sz = send_client_data(1, data, sz);
+                       if (sz < 0)
+                               goto fail;
+               }
+               if (0 <= pe && (pfd[pe].revents & (POLLIN|POLLHUP))) {
+                       /* Status ready; we ship that in the side-band
+                        * or dump to the standard error.
+                        */
+                       sz = xread(pack_objects.err, progress,
+                                 sizeof(progress));
+                       if (0 < sz)
+                               send_client_data(2, progress, sz);
+                       else if (sz == 0) {
+                               close(pack_objects.err);
+                               pack_objects.err = -1;
+                       }
+                       else
+                               goto fail;
+               }
+       }
+
+       if (finish_command(&pack_objects)) {
+               error("git-upload-pack: git-pack-objects died with error.");
+               goto fail;
+       }
+       if (finish_async(&rev_list))
+               goto fail;      /* error was already reported */
+
+       /* flush the data */
+       if (0 <= buffered) {
+               data[0] = buffered;
+               sz = send_client_data(1, data, 1);
+               if (sz < 0)
+                       goto fail;
+               fprintf(stderr, "flushed.\n");
+       }
+       if (use_sideband)
+               packet_flush(1);
+       return;
+
+ fail:
+       send_client_data(3, abort_msg, sizeof(abort_msg));
+       die("git-upload-pack: %s", abort_msg);
+}
+
+static int got_sha1(char *hex, unsigned char *sha1)
+{
+       struct object *o;
+       int we_knew_they_have = 0;
+
+       if (get_sha1_hex(hex, sha1))
+               die("git-upload-pack: expected SHA1 object, got '%s'", hex);
+       if (!has_sha1_file(sha1))
+               return -1;
+
+       o = lookup_object(sha1);
+       if (!(o && o->parsed))
+               o = parse_object(sha1);
+       if (!o)
+               die("oops (%s)", sha1_to_hex(sha1));
+       if (o->type == OBJ_COMMIT) {
+               struct commit_list *parents;
+               struct commit *commit = (struct commit *)o;
+               if (o->flags & THEY_HAVE)
+                       we_knew_they_have = 1;
+               else
+                       o->flags |= THEY_HAVE;
+               if (!oldest_have || (commit->date < oldest_have))
+                       oldest_have = commit->date;
+               for (parents = commit->parents;
+                    parents;
+                    parents = parents->next)
+                       parents->item->object.flags |= THEY_HAVE;
+       }
+       if (!we_knew_they_have) {
+               add_object_array(o, NULL, &have_obj);
+               return 1;
+       }
+       return 0;
+}
+
+static int reachable(struct commit *want)
+{
+       struct commit_list *work = NULL;
+
+       insert_by_date(want, &work);
+       while (work) {
+               struct commit_list *list = work->next;
+               struct commit *commit = work->item;
+               free(work);
+               work = list;
+
+               if (commit->object.flags & THEY_HAVE) {
+                       want->object.flags |= COMMON_KNOWN;
+                       break;
+               }
+               if (!commit->object.parsed)
+                       parse_object(commit->object.sha1);
+               if (commit->object.flags & REACHABLE)
+                       continue;
+               commit->object.flags |= REACHABLE;
+               if (commit->date < oldest_have)
+                       continue;
+               for (list = commit->parents; list; list = list->next) {
+                       struct commit *parent = list->item;
+                       if (!(parent->object.flags & REACHABLE))
+                               insert_by_date(parent, &work);
+               }
+       }
+       want->object.flags |= REACHABLE;
+       clear_commit_marks(want, REACHABLE);
+       free_commit_list(work);
+       return (want->object.flags & COMMON_KNOWN);
+}
+
+static int ok_to_give_up(void)
+{
+       int i;
+
+       if (!have_obj.nr)
+               return 0;
+
+       for (i = 0; i < want_obj.nr; i++) {
+               struct object *want = want_obj.objects[i].item;
+
+               if (want->flags & COMMON_KNOWN)
+                       continue;
+               want = deref_tag(want, "a want line", 0);
+               if (!want || want->type != OBJ_COMMIT) {
+                       /* no way to tell if this is reachable by
+                        * looking at the ancestry chain alone, so
+                        * leave a note to ourselves not to worry about
+                        * this object anymore.
+                        */
+                       want_obj.objects[i].item->flags |= COMMON_KNOWN;
+                       continue;
+               }
+               if (!reachable((struct commit *)want))
+                       return 0;
+       }
+       return 1;
+}
+
+static int get_common_commits(void)
+{
+       static char line[1000];
+       unsigned char sha1[20];
+       char hex[41], last_hex[41];
+       int len;
+
+       track_object_refs = 0;
+       save_commit_buffer = 0;
+
+       for(;;) {
+               len = packet_read_line(0, line, sizeof(line));
+               reset_timeout();
+
+               if (!len) {
+                       if (have_obj.nr == 0 || multi_ack)
+                               packet_write(1, "NAK\n");
+                       continue;
+               }
+               len = strip(line, len);
+               if (!prefixcmp(line, "have ")) {
+                       switch (got_sha1(line+5, sha1)) {
+                       case -1: /* they have what we do not */
+                               if (multi_ack && ok_to_give_up())
+                                       packet_write(1, "ACK %s continue\n",
+                                                    sha1_to_hex(sha1));
+                               break;
+                       default:
+                               memcpy(hex, sha1_to_hex(sha1), 41);
+                               if (multi_ack) {
+                                       const char *msg = "ACK %s continue\n";
+                                       packet_write(1, msg, hex);
+                                       memcpy(last_hex, hex, 41);
+                               }
+                               else if (have_obj.nr == 1)
+                                       packet_write(1, "ACK %s\n", hex);
+                               break;
+                       }
+                       continue;
+               }
+               if (!strcmp(line, "done")) {
+                       if (have_obj.nr > 0) {
+                               if (multi_ack)
+                                       packet_write(1, "ACK %s\n", last_hex);
+                               return 0;
+                       }
+                       packet_write(1, "NAK\n");
+                       return -1;
+               }
+               die("git-upload-pack: expected SHA1 list, got '%s'", line);
+       }
+}
+
+static void receive_needs(void)
+{
+       struct object_array shallows = {0, 0, NULL};
+       static char line[1000];
+       int len, depth = 0;
+
+       for (;;) {
+               struct object *o;
+               unsigned char sha1_buf[20];
+               len = packet_read_line(0, line, sizeof(line));
+               reset_timeout();
+               if (!len)
+                       break;
+
+               if (!prefixcmp(line, "shallow ")) {
+                       unsigned char sha1[20];
+                       struct object *object;
+                       use_thin_pack = 0;
+                       if (get_sha1(line + 8, sha1))
+                               die("invalid shallow line: %s", line);
+                       object = parse_object(sha1);
+                       if (!object)
+                               die("did not find object for %s", line);
+                       object->flags |= CLIENT_SHALLOW;
+                       add_object_array(object, NULL, &shallows);
+                       continue;
+               }
+               if (!prefixcmp(line, "deepen ")) {
+                       char *end;
+                       use_thin_pack = 0;
+                       depth = strtol(line + 7, &end, 0);
+                       if (end == line + 7 || depth <= 0)
+                               die("Invalid deepen: %s", line);
+                       continue;
+               }
+               if (prefixcmp(line, "want ") ||
+                   get_sha1_hex(line+5, sha1_buf))
+                       die("git-upload-pack: protocol error, "
+                           "expected to get sha, not '%s'", line);
+               if (strstr(line+45, "multi_ack"))
+                       multi_ack = 1;
+               if (strstr(line+45, "thin-pack"))
+                       use_thin_pack = 1;
+               if (strstr(line+45, "ofs-delta"))
+                       use_ofs_delta = 1;
+               if (strstr(line+45, "side-band-64k"))
+                       use_sideband = LARGE_PACKET_MAX;
+               else if (strstr(line+45, "side-band"))
+                       use_sideband = DEFAULT_PACKET_MAX;
+               if (strstr(line+45, "no-progress"))
+                       no_progress = 1;
+
+               /* We have sent all our refs already, and the other end
+                * should have chosen out of them; otherwise they are
+                * asking for nonsense.
+                *
+                * Hmph.  We may later want to allow "want" line that
+                * asks for something like "master~10" (symbolic)...
+                * would it make sense?  I don't know.
+                */
+               o = lookup_object(sha1_buf);
+               if (!o || !(o->flags & OUR_REF))
+                       die("git-upload-pack: not our ref %s", line+5);
+               if (!(o->flags & WANTED)) {
+                       o->flags |= WANTED;
+                       add_object_array(o, NULL, &want_obj);
+               }
+       }
+       if (depth == 0 && shallows.nr == 0)
+               return;
+       if (depth > 0) {
+               struct commit_list *result, *backup;
+               int i;
+               backup = result = get_shallow_commits(&want_obj, depth,
+                       SHALLOW, NOT_SHALLOW);
+               while (result) {
+                       struct object *object = &result->item->object;
+                       if (!(object->flags & (CLIENT_SHALLOW|NOT_SHALLOW))) {
+                               packet_write(1, "shallow %s",
+                                               sha1_to_hex(object->sha1));
+                               register_shallow(object->sha1);
+                       }
+                       result = result->next;
+               }
+               free_commit_list(backup);
+               for (i = 0; i < shallows.nr; i++) {
+                       struct object *object = shallows.objects[i].item;
+                       if (object->flags & NOT_SHALLOW) {
+                               struct commit_list *parents;
+                               packet_write(1, "unshallow %s",
+                                       sha1_to_hex(object->sha1));
+                               object->flags &= ~CLIENT_SHALLOW;
+                               /* make sure the real parents are parsed */
+                               unregister_shallow(object->sha1);
+                               object->parsed = 0;
+                               parse_commit((struct commit *)object);
+                               parents = ((struct commit *)object)->parents;
+                               while (parents) {
+                                       add_object_array(&parents->item->object,
+                                                       NULL, &want_obj);
+                                       parents = parents->next;
+                               }
+                       }
+                       /* make sure commit traversal conforms to client */
+                       register_shallow(object->sha1);
+               }
+               packet_flush(1);
+       } else
+               if (shallows.nr > 0) {
+                       int i;
+                       for (i = 0; i < shallows.nr; i++)
+                               register_shallow(shallows.objects[i].item->sha1);
+               }
+       free(shallows.objects);
+}
+
+static int send_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
+{
+       static const char *capabilities = "multi_ack thin-pack side-band"
+               " side-band-64k ofs-delta shallow no-progress";
+       struct object *o = parse_object(sha1);
+
+       if (!o)
+               die("git-upload-pack: cannot find object %s:", sha1_to_hex(sha1));
+
+       if (capabilities)
+               packet_write(1, "%s %s%c%s\n", sha1_to_hex(sha1), refname,
+                       0, capabilities);
+       else
+               packet_write(1, "%s %s\n", sha1_to_hex(sha1), refname);
+       capabilities = NULL;
+       if (!(o->flags & OUR_REF)) {
+               o->flags |= OUR_REF;
+               nr_our_refs++;
+       }
+       if (o->type == OBJ_TAG) {
+               o = deref_tag(o, refname, 0);
+               packet_write(1, "%s %s^{}\n", sha1_to_hex(o->sha1), refname);
+       }
+       return 0;
+}
+
+static void upload_pack(void)
+{
+       reset_timeout();
+       head_ref(send_ref, NULL);
+       for_each_ref(send_ref, NULL);
+       packet_flush(1);
+       receive_needs();
+       if (want_obj.nr) {
+               get_common_commits();
+               create_pack_file();
+       }
+}
+
+int main(int argc, char **argv)
+{
+       char *dir;
+       int i;
+       int strict = 0;
+
+       for (i = 1; i < argc; i++) {
+               char *arg = argv[i];
+
+               if (arg[0] != '-')
+                       break;
+               if (!strcmp(arg, "--strict")) {
+                       strict = 1;
+                       continue;
+               }
+               if (!prefixcmp(arg, "--timeout=")) {
+                       timeout = atoi(arg+10);
+                       continue;
+               }
+               if (!strcmp(arg, "--")) {
+                       i++;
+                       break;
+               }
+       }
+
+       if (i != argc-1)
+               usage(upload_pack_usage);
+       dir = argv[i];
+
+       if (!enter_repo(dir, strict))
+               die("'%s': unable to chdir or not a git archive", dir);
+       if (is_repository_shallow())
+               die("attempt to fetch/clone from a shallow repository");
+       upload_pack();
+       return 0;
+}
diff --git a/usage.c b/usage.c
new file mode 100644 (file)
index 0000000..a5fc4ec
--- /dev/null
+++ b/usage.c
@@ -0,0 +1,96 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "git-compat-util.h"
+
+static void report(const char *prefix, const char *err, va_list params)
+{
+       char msg[256];
+       vsnprintf(msg, sizeof(msg), err, params);
+       fprintf(stderr, "%s%s\n", prefix, msg);
+}
+
+static NORETURN void usage_builtin(const char *err)
+{
+       fprintf(stderr, "usage: %s\n", err);
+       exit(129);
+}
+
+static NORETURN void die_builtin(const char *err, va_list params)
+{
+       report("fatal: ", err, params);
+       exit(128);
+}
+
+static void error_builtin(const char *err, va_list params)
+{
+       report("error: ", err, params);
+}
+
+static void warn_builtin(const char *warn, va_list params)
+{
+       report("warning: ", warn, params);
+}
+
+/* If we are in a dlopen()ed .so write to a global variable would segfault
+ * (ugh), so keep things static. */
+static void (*usage_routine)(const char *err) NORETURN = usage_builtin;
+static void (*die_routine)(const char *err, va_list params) NORETURN = die_builtin;
+static void (*error_routine)(const char *err, va_list params) = error_builtin;
+static void (*warn_routine)(const char *err, va_list params) = warn_builtin;
+
+void set_usage_routine(void (*routine)(const char *err) NORETURN)
+{
+       usage_routine = routine;
+}
+
+void set_die_routine(void (*routine)(const char *err, va_list params) NORETURN)
+{
+       die_routine = routine;
+}
+
+void set_error_routine(void (*routine)(const char *err, va_list params))
+{
+       error_routine = routine;
+}
+
+void set_warn_routine(void (*routine)(const char *warn, va_list params))
+{
+       warn_routine = routine;
+}
+
+
+void usage(const char *err)
+{
+       usage_routine(err);
+}
+
+void die(const char *err, ...)
+{
+       va_list params;
+
+       va_start(params, err);
+       die_routine(err, params);
+       va_end(params);
+}
+
+int error(const char *err, ...)
+{
+       va_list params;
+
+       va_start(params, err);
+       error_routine(err, params);
+       va_end(params);
+       return -1;
+}
+
+void warning(const char *warn, ...)
+{
+       va_list params;
+
+       va_start(params, warn);
+       warn_routine(warn, params);
+       va_end(params);
+}
diff --git a/utf8.c b/utf8.c
new file mode 100644 (file)
index 0000000..9efcdb9
--- /dev/null
+++ b/utf8.c
@@ -0,0 +1,356 @@
+#include "git-compat-util.h"
+#include "utf8.h"
+
+/* This code is originally from http://www.cl.cam.ac.uk/~mgk25/ucs/ */
+
+typedef unsigned int ucs_char_t;  /* assuming 32bit int */
+
+struct interval {
+  int first;
+  int last;
+};
+
+/* auxiliary function for binary search in interval table */
+static int bisearch(ucs_char_t ucs, const struct interval *table, int max)
+{
+       int min = 0;
+       int mid;
+
+       if (ucs < table[0].first || ucs > table[max].last)
+               return 0;
+       while (max >= min) {
+               mid = (min + max) / 2;
+               if (ucs > table[mid].last)
+                       min = mid + 1;
+               else if (ucs < table[mid].first)
+                       max = mid - 1;
+               else
+                       return 1;
+       }
+
+       return 0;
+}
+
+/* The following two functions define the column width of an ISO 10646
+ * character as follows:
+ *
+ *    - The null character (U+0000) has a column width of 0.
+ *
+ *    - Other C0/C1 control characters and DEL will lead to a return
+ *      value of -1.
+ *
+ *    - Non-spacing and enclosing combining characters (general
+ *      category code Mn or Me in the Unicode database) have a
+ *      column width of 0.
+ *
+ *    - SOFT HYPHEN (U+00AD) has a column width of 1.
+ *
+ *    - Other format characters (general category code Cf in the Unicode
+ *      database) and ZERO WIDTH SPACE (U+200B) have a column width of 0.
+ *
+ *    - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF)
+ *      have a column width of 0.
+ *
+ *    - Spacing characters in the East Asian Wide (W) or East Asian
+ *      Full-width (F) category as defined in Unicode Technical
+ *      Report #11 have a column width of 2.
+ *
+ *    - All remaining characters (including all printable
+ *      ISO 8859-1 and WGL4 characters, Unicode control characters,
+ *      etc.) have a column width of 1.
+ *
+ * This implementation assumes that ucs_char_t characters are encoded
+ * in ISO 10646.
+ */
+
+static int git_wcwidth(ucs_char_t ch)
+{
+       /*
+        * Sorted list of non-overlapping intervals of non-spacing characters,
+        * generated by
+        *   "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c".
+        */
+       static const struct interval combining[] = {
+               { 0x0300, 0x0357 }, { 0x035D, 0x036F }, { 0x0483, 0x0486 },
+               { 0x0488, 0x0489 }, { 0x0591, 0x05A1 }, { 0x05A3, 0x05B9 },
+               { 0x05BB, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 },
+               { 0x05C4, 0x05C4 }, { 0x0600, 0x0603 }, { 0x0610, 0x0615 },
+               { 0x064B, 0x0658 }, { 0x0670, 0x0670 }, { 0x06D6, 0x06E4 },
+               { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED }, { 0x070F, 0x070F },
+               { 0x0711, 0x0711 }, { 0x0730, 0x074A }, { 0x07A6, 0x07B0 },
+               { 0x0901, 0x0902 }, { 0x093C, 0x093C }, { 0x0941, 0x0948 },
+               { 0x094D, 0x094D }, { 0x0951, 0x0954 }, { 0x0962, 0x0963 },
+               { 0x0981, 0x0981 }, { 0x09BC, 0x09BC }, { 0x09C1, 0x09C4 },
+               { 0x09CD, 0x09CD }, { 0x09E2, 0x09E3 }, { 0x0A01, 0x0A02 },
+               { 0x0A3C, 0x0A3C }, { 0x0A41, 0x0A42 }, { 0x0A47, 0x0A48 },
+               { 0x0A4B, 0x0A4D }, { 0x0A70, 0x0A71 }, { 0x0A81, 0x0A82 },
+               { 0x0ABC, 0x0ABC }, { 0x0AC1, 0x0AC5 }, { 0x0AC7, 0x0AC8 },
+               { 0x0ACD, 0x0ACD }, { 0x0AE2, 0x0AE3 }, { 0x0B01, 0x0B01 },
+               { 0x0B3C, 0x0B3C }, { 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 },
+               { 0x0B4D, 0x0B4D }, { 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 },
+               { 0x0BC0, 0x0BC0 }, { 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 },
+               { 0x0C46, 0x0C48 }, { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 },
+               { 0x0CBC, 0x0CBC }, { 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 },
+               { 0x0CCC, 0x0CCD }, { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D },
+               { 0x0DCA, 0x0DCA }, { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 },
+               { 0x0E31, 0x0E31 }, { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E },
+               { 0x0EB1, 0x0EB1 }, { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC },
+               { 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 },
+               { 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E },
+               { 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 },
+               { 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 },
+               { 0x1032, 0x1032 }, { 0x1036, 0x1037 }, { 0x1039, 0x1039 },
+               { 0x1058, 0x1059 }, { 0x1160, 0x11FF }, { 0x1712, 0x1714 },
+               { 0x1732, 0x1734 }, { 0x1752, 0x1753 }, { 0x1772, 0x1773 },
+               { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD }, { 0x17C6, 0x17C6 },
+               { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD }, { 0x180B, 0x180D },
+               { 0x18A9, 0x18A9 }, { 0x1920, 0x1922 }, { 0x1927, 0x1928 },
+               { 0x1932, 0x1932 }, { 0x1939, 0x193B }, { 0x200B, 0x200F },
+               { 0x202A, 0x202E }, { 0x2060, 0x2063 }, { 0x206A, 0x206F },
+               { 0x20D0, 0x20EA }, { 0x302A, 0x302F }, { 0x3099, 0x309A },
+               { 0xFB1E, 0xFB1E }, { 0xFE00, 0xFE0F }, { 0xFE20, 0xFE23 },
+               { 0xFEFF, 0xFEFF }, { 0xFFF9, 0xFFFB }, { 0x1D167, 0x1D169 },
+               { 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B },
+               { 0x1D1AA, 0x1D1AD }, { 0xE0001, 0xE0001 },
+               { 0xE0020, 0xE007F }, { 0xE0100, 0xE01EF }
+       };
+
+       /* test for 8-bit control characters */
+       if (ch == 0)
+               return 0;
+       if (ch < 32 || (ch >= 0x7f && ch < 0xa0))
+               return -1;
+
+       /* binary search in table of non-spacing characters */
+       if (bisearch(ch, combining, sizeof(combining)
+                               / sizeof(struct interval) - 1))
+               return 0;
+
+       /*
+        * If we arrive here, ch is neither a combining nor a C0/C1
+        * control character.
+        */
+
+       return 1 +
+               (ch >= 0x1100 &&
+                    /* Hangul Jamo init. consonants */
+                (ch <= 0x115f ||
+                 ch == 0x2329 || ch == 0x232a ||
+                  /* CJK ... Yi */
+                 (ch >= 0x2e80 && ch <= 0xa4cf &&
+                  ch != 0x303f) ||
+                 /* Hangul Syllables */
+                 (ch >= 0xac00 && ch <= 0xd7a3) ||
+                 /* CJK Compatibility Ideographs */
+                 (ch >= 0xf900 && ch <= 0xfaff) ||
+                 /* CJK Compatibility Forms */
+                 (ch >= 0xfe30 && ch <= 0xfe6f) ||
+                 /* Fullwidth Forms */
+                 (ch >= 0xff00 && ch <= 0xff60) ||
+                 (ch >= 0xffe0 && ch <= 0xffe6) ||
+                 (ch >= 0x20000 && ch <= 0x2fffd) ||
+                 (ch >= 0x30000 && ch <= 0x3fffd)));
+}
+
+/*
+ * This function returns the number of columns occupied by the character
+ * pointed to by the variable start. The pointer is updated to point at
+ * the next character. If it was not valid UTF-8, the pointer is set to NULL.
+ */
+int utf8_width(const char **start)
+{
+       unsigned char *s = (unsigned char *)*start;
+       ucs_char_t ch;
+
+       if (*s < 0x80) {
+               /* 0xxxxxxx */
+               ch = *s;
+               *start += 1;
+       } else if ((s[0] & 0xe0) == 0xc0) {
+               /* 110XXXXx 10xxxxxx */
+               if ((s[1] & 0xc0) != 0x80 ||
+                               /* overlong? */
+                               (s[0] & 0xfe) == 0xc0)
+                       goto invalid;
+               ch = ((s[0] & 0x1f) << 6) | (s[1] & 0x3f);
+               *start += 2;
+       } else if ((s[0] & 0xf0) == 0xe0) {
+               /* 1110XXXX 10Xxxxxx 10xxxxxx */
+               if ((s[1] & 0xc0) != 0x80 ||
+                               (s[2] & 0xc0) != 0x80 ||
+                               /* overlong? */
+                               (s[0] == 0xe0 && (s[1] & 0xe0) == 0x80) ||
+                               /* surrogate? */
+                               (s[0] == 0xed && (s[1] & 0xe0) == 0xa0) ||
+                               /* U+FFFE or U+FFFF? */
+                               (s[0] == 0xef && s[1] == 0xbf &&
+                                (s[2] & 0xfe) == 0xbe))
+                       goto invalid;
+               ch = ((s[0] & 0x0f) << 12) |
+                       ((s[1] & 0x3f) << 6) | (s[2] & 0x3f);
+               *start += 3;
+       } else if ((s[0] & 0xf8) == 0xf0) {
+               /* 11110XXX 10XXxxxx 10xxxxxx 10xxxxxx */
+               if ((s[1] & 0xc0) != 0x80 ||
+                               (s[2] & 0xc0) != 0x80 ||
+                               (s[3] & 0xc0) != 0x80 ||
+                               /* overlong? */
+                               (s[0] == 0xf0 && (s[1] & 0xf0) == 0x80) ||
+                               /* > U+10FFFF? */
+                               (s[0] == 0xf4 && s[1] > 0x8f) || s[0] > 0xf4)
+                       goto invalid;
+               ch = ((s[0] & 0x07) << 18) | ((s[1] & 0x3f) << 12) |
+                       ((s[2] & 0x3f) << 6) | (s[3] & 0x3f);
+               *start += 4;
+       } else {
+invalid:
+               *start = NULL;
+               return 0;
+       }
+
+       return git_wcwidth(ch);
+}
+
+int is_utf8(const char *text)
+{
+       while (*text) {
+               if (*text == '\n' || *text == '\t' || *text == '\r') {
+                       text++;
+                       continue;
+               }
+               utf8_width(&text);
+               if (!text)
+                       return 0;
+       }
+       return 1;
+}
+
+static void print_spaces(int count)
+{
+       static const char s[] = "                    ";
+       while (count >= sizeof(s)) {
+               fwrite(s, sizeof(s) - 1, 1, stdout);
+               count -= sizeof(s) - 1;
+       }
+       fwrite(s, count, 1, stdout);
+}
+
+/*
+ * Wrap the text, if necessary. The variable indent is the indent for the
+ * first line, indent2 is the indent for all other lines.
+ * If indent is negative, assume that already -indent columns have been
+ * consumed (and no extra indent is necessary for the first line).
+ */
+int print_wrapped_text(const char *text, int indent, int indent2, int width)
+{
+       int w = indent, assume_utf8 = is_utf8(text);
+       const char *bol = text, *space = NULL;
+
+       if (indent < 0) {
+               w = -indent;
+               space = text;
+       }
+
+       for (;;) {
+               char c = *text;
+               if (!c || isspace(c)) {
+                       if (w < width || !space) {
+                               const char *start = bol;
+                               if (space)
+                                       start = space;
+                               else
+                                       print_spaces(indent);
+                               fwrite(start, text - start, 1, stdout);
+                               if (!c)
+                                       return w;
+                               else if (c == '\t')
+                                       w |= 0x07;
+                               space = text;
+                               w++;
+                               text++;
+                       }
+                       else {
+                               putchar('\n');
+                               text = bol = space + isspace(*space);
+                               space = NULL;
+                               w = indent = indent2;
+                       }
+                       continue;
+               }
+               if (assume_utf8)
+                       w += utf8_width(&text);
+               else {
+                       w++;
+                       text++;
+               }
+       }
+}
+
+int is_encoding_utf8(const char *name)
+{
+       if (!name)
+               return 1;
+       if (!strcasecmp(name, "utf-8") || !strcasecmp(name, "utf8"))
+               return 1;
+       return 0;
+}
+
+/*
+ * Given a buffer and its encoding, return it re-encoded
+ * with iconv.  If the conversion fails, returns NULL.
+ */
+#ifndef NO_ICONV
+#ifdef OLD_ICONV
+       typedef const char * iconv_ibp;
+#else
+       typedef char * iconv_ibp;
+#endif
+char *reencode_string(const char *in, const char *out_encoding, const char *in_encoding)
+{
+       iconv_t conv;
+       size_t insz, outsz, outalloc;
+       char *out, *outpos;
+       iconv_ibp cp;
+
+       if (!in_encoding)
+               return NULL;
+       conv = iconv_open(out_encoding, in_encoding);
+       if (conv == (iconv_t) -1)
+               return NULL;
+       insz = strlen(in);
+       outsz = insz;
+       outalloc = outsz + 1; /* for terminating NUL */
+       out = xmalloc(outalloc);
+       outpos = out;
+       cp = (iconv_ibp)in;
+
+       while (1) {
+               size_t cnt = iconv(conv, &cp, &insz, &outpos, &outsz);
+
+               if (cnt == -1) {
+                       size_t sofar;
+                       if (errno != E2BIG) {
+                               free(out);
+                               iconv_close(conv);
+                               return NULL;
+                       }
+                       /* insz has remaining number of bytes.
+                        * since we started outsz the same as insz,
+                        * it is likely that insz is not enough for
+                        * converting the rest.
+                        */
+                       sofar = outpos - out;
+                       outalloc = sofar + insz * 2 + 32;
+                       out = xrealloc(out, outalloc);
+                       outpos = out + sofar;
+                       outsz = outalloc - sofar - 1;
+               }
+               else {
+                       *outpos = '\0';
+                       break;
+               }
+       }
+       iconv_close(conv);
+       return out;
+}
+#endif
diff --git a/utf8.h b/utf8.h
new file mode 100644 (file)
index 0000000..15db6f1
--- /dev/null
+++ b/utf8.h
@@ -0,0 +1,16 @@
+#ifndef GIT_UTF8_H
+#define GIT_UTF8_H
+
+int utf8_width(const char **start);
+int is_utf8(const char *text);
+int is_encoding_utf8(const char *name);
+
+int print_wrapped_text(const char *text, int indent, int indent2, int len);
+
+#ifndef NO_ICONV
+char *reencode_string(const char *in, const char *out_encoding, const char *in_encoding);
+#else
+#define reencode_string(a,b,c) NULL
+#endif
+
+#endif
diff --git a/var.c b/var.c
new file mode 100644 (file)
index 0000000..4127031
--- /dev/null
+++ b/var.c
@@ -0,0 +1,74 @@
+/*
+ * GIT - The information manager from hell
+ *
+ * Copyright (C) Eric Biederman, 2005
+ */
+#include "cache.h"
+
+static const char var_usage[] = "git-var [-l | <variable>]";
+
+struct git_var {
+       const char *name;
+       const char *(*read)(int);
+};
+static struct git_var git_vars[] = {
+       { "GIT_COMMITTER_IDENT", git_committer_info },
+       { "GIT_AUTHOR_IDENT",   git_author_info },
+       { "", NULL },
+};
+
+static void list_vars(void)
+{
+       struct git_var *ptr;
+       for(ptr = git_vars; ptr->read; ptr++) {
+               printf("%s=%s\n", ptr->name, ptr->read(0));
+       }
+}
+
+static const char *read_var(const char *var)
+{
+       struct git_var *ptr;
+       const char *val;
+       val = NULL;
+       for(ptr = git_vars; ptr->read; ptr++) {
+               if (strcmp(var, ptr->name) == 0) {
+                       val = ptr->read(1);
+                       break;
+               }
+       }
+       return val;
+}
+
+static int show_config(const char *var, const char *value)
+{
+       if (value)
+               printf("%s=%s\n", var, value);
+       else
+               printf("%s\n", var);
+       return git_default_config(var, value);
+}
+
+int main(int argc, char **argv)
+{
+       const char *val;
+       if (argc != 2) {
+               usage(var_usage);
+       }
+
+       setup_git_directory();
+       val = NULL;
+
+       if (strcmp(argv[1], "-l") == 0) {
+               git_config(show_config);
+               list_vars();
+               return 0;
+       }
+       git_config(git_default_config);
+       val = read_var(argv[1]);
+       if (!val)
+               usage(var_usage);
+
+       printf("%s\n", val);
+
+       return 0;
+}
diff --git a/walker.c b/walker.c
new file mode 100644 (file)
index 0000000..397b80d
--- /dev/null
+++ b/walker.c
@@ -0,0 +1,317 @@
+#include "cache.h"
+#include "walker.h"
+#include "commit.h"
+#include "tree.h"
+#include "tree-walk.h"
+#include "tag.h"
+#include "blob.h"
+#include "refs.h"
+
+static unsigned char current_commit_sha1[20];
+
+void walker_say(struct walker *walker, const char *fmt, const char *hex)
+{
+       if (walker->get_verbosely)
+               fprintf(stderr, fmt, hex);
+}
+
+static void report_missing(const struct object *obj)
+{
+       char missing_hex[41];
+       strcpy(missing_hex, sha1_to_hex(obj->sha1));;
+       fprintf(stderr, "Cannot obtain needed %s %s\n",
+               obj->type ? typename(obj->type): "object", missing_hex);
+       if (!is_null_sha1(current_commit_sha1))
+               fprintf(stderr, "while processing commit %s.\n",
+                       sha1_to_hex(current_commit_sha1));
+}
+
+static int process(struct walker *walker, struct object *obj);
+
+static int process_tree(struct walker *walker, struct tree *tree)
+{
+       struct tree_desc desc;
+       struct name_entry entry;
+
+       if (parse_tree(tree))
+               return -1;
+
+       init_tree_desc(&desc, tree->buffer, tree->size);
+       while (tree_entry(&desc, &entry)) {
+               struct object *obj = NULL;
+
+               /* submodule commits are not stored in the superproject */
+               if (S_ISGITLINK(entry.mode))
+                       continue;
+               if (S_ISDIR(entry.mode)) {
+                       struct tree *tree = lookup_tree(entry.sha1);
+                       if (tree)
+                               obj = &tree->object;
+               }
+               else {
+                       struct blob *blob = lookup_blob(entry.sha1);
+                       if (blob)
+                               obj = &blob->object;
+               }
+               if (!obj || process(walker, obj))
+                       return -1;
+       }
+       free(tree->buffer);
+       tree->buffer = NULL;
+       tree->size = 0;
+       return 0;
+}
+
+#define COMPLETE       (1U << 0)
+#define SEEN           (1U << 1)
+#define TO_SCAN                (1U << 2)
+
+static struct commit_list *complete = NULL;
+
+static int process_commit(struct walker *walker, struct commit *commit)
+{
+       if (parse_commit(commit))
+               return -1;
+
+       while (complete && complete->item->date >= commit->date) {
+               pop_most_recent_commit(&complete, COMPLETE);
+       }
+
+       if (commit->object.flags & COMPLETE)
+               return 0;
+
+       hashcpy(current_commit_sha1, commit->object.sha1);
+
+       walker_say(walker, "walk %s\n", sha1_to_hex(commit->object.sha1));
+
+       if (walker->get_tree) {
+               if (process(walker, &commit->tree->object))
+                       return -1;
+               if (!walker->get_all)
+                       walker->get_tree = 0;
+       }
+       if (walker->get_history) {
+               struct commit_list *parents = commit->parents;
+               for (; parents; parents = parents->next) {
+                       if (process(walker, &parents->item->object))
+                               return -1;
+               }
+       }
+       return 0;
+}
+
+static int process_tag(struct walker *walker, struct tag *tag)
+{
+       if (parse_tag(tag))
+               return -1;
+       return process(walker, tag->tagged);
+}
+
+static struct object_list *process_queue = NULL;
+static struct object_list **process_queue_end = &process_queue;
+
+static int process_object(struct walker *walker, struct object *obj)
+{
+       if (obj->type == OBJ_COMMIT) {
+               if (process_commit(walker, (struct commit *)obj))
+                       return -1;
+               return 0;
+       }
+       if (obj->type == OBJ_TREE) {
+               if (process_tree(walker, (struct tree *)obj))
+                       return -1;
+               return 0;
+       }
+       if (obj->type == OBJ_BLOB) {
+               return 0;
+       }
+       if (obj->type == OBJ_TAG) {
+               if (process_tag(walker, (struct tag *)obj))
+                       return -1;
+               return 0;
+       }
+       return error("Unable to determine requirements "
+                    "of type %s for %s",
+                    typename(obj->type), sha1_to_hex(obj->sha1));
+}
+
+static int process(struct walker *walker, struct object *obj)
+{
+       if (obj->flags & SEEN)
+               return 0;
+       obj->flags |= SEEN;
+
+       if (has_sha1_file(obj->sha1)) {
+               /* We already have it, so we should scan it now. */
+               obj->flags |= TO_SCAN;
+       }
+       else {
+               if (obj->flags & COMPLETE)
+                       return 0;
+               walker->prefetch(walker, obj->sha1);
+       }
+
+       object_list_insert(obj, process_queue_end);
+       process_queue_end = &(*process_queue_end)->next;
+       return 0;
+}
+
+static int loop(struct walker *walker)
+{
+       struct object_list *elem;
+
+       while (process_queue) {
+               struct object *obj = process_queue->item;
+               elem = process_queue;
+               process_queue = elem->next;
+               free(elem);
+               if (!process_queue)
+                       process_queue_end = &process_queue;
+
+               /* If we are not scanning this object, we placed it in
+                * the queue because we needed to fetch it first.
+                */
+               if (! (obj->flags & TO_SCAN)) {
+                       if (walker->fetch(walker, obj->sha1)) {
+                               report_missing(obj);
+                               return -1;
+                       }
+               }
+               if (!obj->type)
+                       parse_object(obj->sha1);
+               if (process_object(walker, obj))
+                       return -1;
+       }
+       return 0;
+}
+
+static int interpret_target(struct walker *walker, char *target, unsigned char *sha1)
+{
+       if (!get_sha1_hex(target, sha1))
+               return 0;
+       if (!check_ref_format(target)) {
+               if (!walker->fetch_ref(walker, target, sha1)) {
+                       return 0;
+               }
+       }
+       return -1;
+}
+
+static int mark_complete(const char *path, const unsigned char *sha1, int flag, void *cb_data)
+{
+       struct commit *commit = lookup_commit_reference_gently(sha1, 1);
+       if (commit) {
+               commit->object.flags |= COMPLETE;
+               insert_by_date(commit, &complete);
+       }
+       return 0;
+}
+
+int walker_targets_stdin(char ***target, const char ***write_ref)
+{
+       int targets = 0, targets_alloc = 0;
+       struct strbuf buf;
+       *target = NULL; *write_ref = NULL;
+       strbuf_init(&buf, 0);
+       while (1) {
+               char *rf_one = NULL;
+               char *tg_one;
+
+               if (strbuf_getline(&buf, stdin, '\n') == EOF)
+                       break;
+               tg_one = buf.buf;
+               rf_one = strchr(tg_one, '\t');
+               if (rf_one)
+                       *rf_one++ = 0;
+
+               if (targets >= targets_alloc) {
+                       targets_alloc = targets_alloc ? targets_alloc * 2 : 64;
+                       *target = xrealloc(*target, targets_alloc * sizeof(**target));
+                       *write_ref = xrealloc(*write_ref, targets_alloc * sizeof(**write_ref));
+               }
+               (*target)[targets] = xstrdup(tg_one);
+               (*write_ref)[targets] = rf_one ? xstrdup(rf_one) : NULL;
+               targets++;
+       }
+       strbuf_release(&buf);
+       return targets;
+}
+
+void walker_targets_free(int targets, char **target, const char **write_ref)
+{
+       while (targets--) {
+               free(target[targets]);
+               if (write_ref && write_ref[targets])
+                       free((char *) write_ref[targets]);
+       }
+}
+
+int walker_fetch(struct walker *walker, int targets, char **target,
+                const char **write_ref, const char *write_ref_log_details)
+{
+       struct ref_lock **lock = xcalloc(targets, sizeof(struct ref_lock *));
+       unsigned char *sha1 = xmalloc(targets * 20);
+       char *msg;
+       int ret;
+       int i;
+
+       save_commit_buffer = 0;
+       track_object_refs = 0;
+
+       for (i = 0; i < targets; i++) {
+               if (!write_ref || !write_ref[i])
+                       continue;
+
+               lock[i] = lock_ref_sha1(write_ref[i], NULL);
+               if (!lock[i]) {
+                       error("Can't lock ref %s", write_ref[i]);
+                       goto unlock_and_fail;
+               }
+       }
+
+       if (!walker->get_recover)
+               for_each_ref(mark_complete, NULL);
+
+       for (i = 0; i < targets; i++) {
+               if (interpret_target(walker, target[i], &sha1[20 * i])) {
+                       error("Could not interpret %s as something to pull", target[i]);
+                       goto unlock_and_fail;
+               }
+               if (process(walker, lookup_unknown_object(&sha1[20 * i])))
+                       goto unlock_and_fail;
+       }
+
+       if (loop(walker))
+               goto unlock_and_fail;
+
+       if (write_ref_log_details) {
+               msg = xmalloc(strlen(write_ref_log_details) + 12);
+               sprintf(msg, "fetch from %s", write_ref_log_details);
+       } else {
+               msg = NULL;
+       }
+       for (i = 0; i < targets; i++) {
+               if (!write_ref || !write_ref[i])
+                       continue;
+               ret = write_ref_sha1(lock[i], &sha1[20 * i], msg ? msg : "fetch (unknown)");
+               lock[i] = NULL;
+               if (ret)
+                       goto unlock_and_fail;
+       }
+       free(msg);
+
+       return 0;
+
+unlock_and_fail:
+       for (i = 0; i < targets; i++)
+               if (lock[i])
+                       unlock_ref(lock[i]);
+
+       return -1;
+}
+
+void walker_free(struct walker *walker)
+{
+       walker->cleanup(walker);
+       free(walker);
+}
diff --git a/walker.h b/walker.h
new file mode 100644 (file)
index 0000000..ea2c363
--- /dev/null
+++ b/walker.h
@@ -0,0 +1,37 @@
+#ifndef WALKER_H
+#define WALKER_H
+
+struct walker {
+       void *data;
+       int (*fetch_ref)(struct walker *, char *ref, unsigned char *sha1);
+       void (*prefetch)(struct walker *, unsigned char *sha1);
+       int (*fetch)(struct walker *, unsigned char *sha1);
+       void (*cleanup)(struct walker *);
+       int get_tree;
+       int get_history;
+       int get_all;
+       int get_verbosely;
+       int get_recover;
+
+       int corrupt_object_found;
+};
+
+/* Report what we got under get_verbosely */
+void walker_say(struct walker *walker, const char *, const char *);
+
+/* Load pull targets from stdin */
+int walker_targets_stdin(char ***target, const char ***write_ref);
+
+/* Free up loaded targets */
+void walker_targets_free(int targets, char **target, const char **write_ref);
+
+/* If write_ref is set, the ref filename to write the target value to. */
+/* If write_ref_log_details is set, additional text will appear in the ref log. */
+int walker_fetch(struct walker *impl, int targets, char **target,
+                const char **write_ref, const char *write_ref_log_details);
+
+void walker_free(struct walker *walker);
+
+struct walker *get_http_walker(const char *url);
+
+#endif /* WALKER_H */
diff --git a/windows/git-gui.sh b/windows/git-gui.sh
deleted file mode 100644 (file)
index 98f32c0..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-# Tcl ignores the next line -*- tcl -*- \
-exec wish "$0" -- "$@"
-
-if { $argc >=2 && [lindex $argv 0] == "--working-dir" } {
-       cd [lindex $argv 1]
-       set argv [lrange $argv 2 end]
-       incr argc -2
-}
-
-set gitguidir [file dirname [info script]]
-regsub -all ";" $gitguidir "\\;" gitguidir
-set env(PATH) "$gitguidir;$env(PATH)"
-unset gitguidir
-
-source [file join [file dirname [info script]] git-gui.tcl]
diff --git a/write_or_die.c b/write_or_die.c
new file mode 100644 (file)
index 0000000..e125e11
--- /dev/null
@@ -0,0 +1,112 @@
+#include "cache.h"
+
+/*
+ * Some cases use stdio, but want to flush after the write
+ * to get error handling (and to get better interactive
+ * behaviour - not buffering excessively).
+ *
+ * Of course, if the flush happened within the write itself,
+ * we've already lost the error code, and cannot report it any
+ * more. So we just ignore that case instead (and hope we get
+ * the right error code on the flush).
+ *
+ * If the file handle is stdout, and stdout is a file, then skip the
+ * flush entirely since it's not needed.
+ */
+void maybe_flush_or_die(FILE *f, const char *desc)
+{
+       static int skip_stdout_flush = -1;
+       struct stat st;
+       char *cp;
+
+       if (f == stdout) {
+               if (skip_stdout_flush < 0) {
+                       cp = getenv("GIT_FLUSH");
+                       if (cp)
+                               skip_stdout_flush = (atoi(cp) == 0);
+                       else if ((fstat(fileno(stdout), &st) == 0) &&
+                                S_ISREG(st.st_mode))
+                               skip_stdout_flush = 1;
+                       else
+                               skip_stdout_flush = 0;
+               }
+               if (skip_stdout_flush && !ferror(f))
+                       return;
+       }
+       if (fflush(f)) {
+               if (errno == EPIPE)
+                       exit(0);
+               die("write failure on %s: %s", desc, strerror(errno));
+       }
+}
+
+int read_in_full(int fd, void *buf, size_t count)
+{
+       char *p = buf;
+       ssize_t total = 0;
+
+       while (count > 0) {
+               ssize_t loaded = xread(fd, p, count);
+               if (loaded <= 0)
+                       return total ? total : loaded;
+               count -= loaded;
+               p += loaded;
+               total += loaded;
+       }
+
+       return total;
+}
+
+int write_in_full(int fd, const void *buf, size_t count)
+{
+       const char *p = buf;
+       ssize_t total = 0;
+
+       while (count > 0) {
+               ssize_t written = xwrite(fd, p, count);
+               if (written < 0)
+                       return -1;
+               if (!written) {
+                       errno = ENOSPC;
+                       return -1;
+               }
+               count -= written;
+               p += written;
+               total += written;
+       }
+
+       return total;
+}
+
+void write_or_die(int fd, const void *buf, size_t count)
+{
+       if (write_in_full(fd, buf, count) < 0) {
+               if (errno == EPIPE)
+                       exit(0);
+               die("write error (%s)", strerror(errno));
+       }
+}
+
+int write_or_whine_pipe(int fd, const void *buf, size_t count, const char *msg)
+{
+       if (write_in_full(fd, buf, count) < 0) {
+               if (errno == EPIPE)
+                       exit(0);
+               fprintf(stderr, "%s: write error (%s)\n",
+                       msg, strerror(errno));
+               return 0;
+       }
+
+       return 1;
+}
+
+int write_or_whine(int fd, const void *buf, size_t count, const char *msg)
+{
+       if (write_in_full(fd, buf, count) < 0) {
+               fprintf(stderr, "%s: write error (%s)\n",
+                       msg, strerror(errno));
+               return 0;
+       }
+
+       return 1;
+}
diff --git a/wt-status.c b/wt-status.c
new file mode 100644 (file)
index 0000000..02dbb75
--- /dev/null
@@ -0,0 +1,402 @@
+#include "cache.h"
+#include "wt-status.h"
+#include "color.h"
+#include "object.h"
+#include "dir.h"
+#include "commit.h"
+#include "diff.h"
+#include "revision.h"
+#include "diffcore.h"
+
+int wt_status_use_color = 0;
+static char wt_status_colors[][COLOR_MAXLEN] = {
+       "",         /* WT_STATUS_HEADER: normal */
+       "\033[32m", /* WT_STATUS_UPDATED: green */
+       "\033[31m", /* WT_STATUS_CHANGED: red */
+       "\033[31m", /* WT_STATUS_UNTRACKED: red */
+};
+
+static const char use_add_msg[] =
+"use \"git add <file>...\" to update what will be committed";
+static const char use_add_rm_msg[] =
+"use \"git add/rm <file>...\" to update what will be committed";
+static const char use_add_to_include_msg[] =
+"use \"git add <file>...\" to include in what will be committed";
+
+static int parse_status_slot(const char *var, int offset)
+{
+       if (!strcasecmp(var+offset, "header"))
+               return WT_STATUS_HEADER;
+       if (!strcasecmp(var+offset, "updated")
+               || !strcasecmp(var+offset, "added"))
+               return WT_STATUS_UPDATED;
+       if (!strcasecmp(var+offset, "changed"))
+               return WT_STATUS_CHANGED;
+       if (!strcasecmp(var+offset, "untracked"))
+               return WT_STATUS_UNTRACKED;
+       die("bad config variable '%s'", var);
+}
+
+static const char* color(int slot)
+{
+       return wt_status_use_color ? wt_status_colors[slot] : "";
+}
+
+void wt_status_prepare(struct wt_status *s)
+{
+       unsigned char sha1[20];
+       const char *head;
+
+       memset(s, 0, sizeof(*s));
+       head = resolve_ref("HEAD", sha1, 0, NULL);
+       s->branch = head ? xstrdup(head) : NULL;
+       s->reference = "HEAD";
+       s->fp = stdout;
+       s->index_file = get_index_file();
+}
+
+static void wt_status_print_cached_header(struct wt_status *s)
+{
+       const char *c = color(WT_STATUS_HEADER);
+       color_fprintf_ln(s->fp, c, "# Changes to be committed:");
+       if (s->reference) {
+               color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+       } else {
+               color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
+       }
+       color_fprintf_ln(s->fp, c, "#");
+}
+
+static void wt_status_print_header(struct wt_status *s,
+                                  const char *main, const char *sub)
+{
+       const char *c = color(WT_STATUS_HEADER);
+       color_fprintf_ln(s->fp, c, "# %s:", main);
+       color_fprintf_ln(s->fp, c, "#   (%s)", sub);
+       color_fprintf_ln(s->fp, c, "#");
+}
+
+static void wt_status_print_trailer(struct wt_status *s)
+{
+       color_fprintf_ln(s->fp, color(WT_STATUS_HEADER), "#");
+}
+
+static char *quote_path(const char *in, int len,
+                       struct strbuf *out, const char *prefix)
+{
+       if (len < 0)
+               len = strlen(in);
+
+       strbuf_grow(out, len);
+       strbuf_setlen(out, 0);
+       if (prefix) {
+               int off = 0;
+               while (prefix[off] && off < len && prefix[off] == in[off])
+                       if (prefix[off] == '/') {
+                               prefix += off + 1;
+                               in += off + 1;
+                               len -= off + 1;
+                               off = 0;
+                       } else
+                               off++;
+
+               for (; *prefix; prefix++)
+                       if (*prefix == '/')
+                               strbuf_addstr(out, "../");
+       }
+
+       for ( ; len > 0; in++, len--) {
+               int ch = *in;
+
+               switch (ch) {
+               case '\n':
+                       strbuf_addstr(out, "\\n");
+                       break;
+               case '\r':
+                       strbuf_addstr(out, "\\r");
+                       break;
+               default:
+                       strbuf_addch(out, ch);
+                       continue;
+               }
+       }
+
+       return out->buf;
+}
+
+static void wt_status_print_filepair(struct wt_status *s,
+                                    int t, struct diff_filepair *p)
+{
+       const char *c = color(t);
+       const char *one, *two;
+       struct strbuf onebuf, twobuf;
+
+       strbuf_init(&onebuf, 0);
+       strbuf_init(&twobuf, 0);
+       one = quote_path(p->one->path, -1, &onebuf, s->prefix);
+       two = quote_path(p->two->path, -1, &twobuf, s->prefix);
+
+       color_fprintf(s->fp, color(WT_STATUS_HEADER), "#\t");
+       switch (p->status) {
+       case DIFF_STATUS_ADDED:
+               color_fprintf(s->fp, c, "new file:   %s", one);
+               break;
+       case DIFF_STATUS_COPIED:
+               color_fprintf(s->fp, c, "copied:     %s -> %s", one, two);
+               break;
+       case DIFF_STATUS_DELETED:
+               color_fprintf(s->fp, c, "deleted:    %s", one);
+               break;
+       case DIFF_STATUS_MODIFIED:
+               color_fprintf(s->fp, c, "modified:   %s", one);
+               break;
+       case DIFF_STATUS_RENAMED:
+               color_fprintf(s->fp, c, "renamed:    %s -> %s", one, two);
+               break;
+       case DIFF_STATUS_TYPE_CHANGED:
+               color_fprintf(s->fp, c, "typechange: %s", one);
+               break;
+       case DIFF_STATUS_UNKNOWN:
+               color_fprintf(s->fp, c, "unknown:    %s", one);
+               break;
+       case DIFF_STATUS_UNMERGED:
+               color_fprintf(s->fp, c, "unmerged:   %s", one);
+               break;
+       default:
+               die("bug: unhandled diff status %c", p->status);
+       }
+       fprintf(s->fp, "\n");
+       strbuf_release(&onebuf);
+       strbuf_release(&twobuf);
+}
+
+static void wt_status_print_updated_cb(struct diff_queue_struct *q,
+               struct diff_options *options,
+               void *data)
+{
+       struct wt_status *s = data;
+       int shown_header = 0;
+       int i;
+       for (i = 0; i < q->nr; i++) {
+               if (q->queue[i]->status == 'U')
+                       continue;
+               if (!shown_header) {
+                       wt_status_print_cached_header(s);
+                       s->commitable = 1;
+                       shown_header = 1;
+               }
+               wt_status_print_filepair(s, WT_STATUS_UPDATED, q->queue[i]);
+       }
+       if (shown_header)
+               wt_status_print_trailer(s);
+}
+
+static void wt_status_print_changed_cb(struct diff_queue_struct *q,
+                        struct diff_options *options,
+                        void *data)
+{
+       struct wt_status *s = data;
+       int i;
+       if (q->nr) {
+               const char *msg = use_add_msg;
+               s->workdir_dirty = 1;
+               for (i = 0; i < q->nr; i++)
+                       if (q->queue[i]->status == DIFF_STATUS_DELETED) {
+                               msg = use_add_rm_msg;
+                               break;
+                       }
+               wt_status_print_header(s, "Changed but not updated", msg);
+       }
+       for (i = 0; i < q->nr; i++)
+               wt_status_print_filepair(s, WT_STATUS_CHANGED, q->queue[i]);
+       if (q->nr)
+               wt_status_print_trailer(s);
+}
+
+static void wt_read_cache(struct wt_status *s)
+{
+       discard_cache();
+       read_cache_from(s->index_file);
+}
+
+static void wt_status_print_initial(struct wt_status *s)
+{
+       int i;
+       struct strbuf buf;
+
+       strbuf_init(&buf, 0);
+       wt_read_cache(s);
+       if (active_nr) {
+               s->commitable = 1;
+               wt_status_print_cached_header(s);
+       }
+       for (i = 0; i < active_nr; i++) {
+               color_fprintf(s->fp, color(WT_STATUS_HEADER), "#\t");
+               color_fprintf_ln(s->fp, color(WT_STATUS_UPDATED), "new file: %s",
+                               quote_path(active_cache[i]->name, -1,
+                                          &buf, s->prefix));
+       }
+       if (active_nr)
+               wt_status_print_trailer(s);
+       strbuf_release(&buf);
+}
+
+static void wt_status_print_updated(struct wt_status *s)
+{
+       struct rev_info rev;
+       init_revisions(&rev, NULL);
+       setup_revisions(0, NULL, &rev, s->reference);
+       rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK;
+       rev.diffopt.format_callback = wt_status_print_updated_cb;
+       rev.diffopt.format_callback_data = s;
+       rev.diffopt.detect_rename = 1;
+       rev.diffopt.rename_limit = 100;
+       rev.diffopt.break_opt = 0;
+       wt_read_cache(s);
+       run_diff_index(&rev, 1);
+}
+
+static void wt_status_print_changed(struct wt_status *s)
+{
+       struct rev_info rev;
+       init_revisions(&rev, "");
+       setup_revisions(0, NULL, &rev, NULL);
+       rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK;
+       rev.diffopt.format_callback = wt_status_print_changed_cb;
+       rev.diffopt.format_callback_data = s;
+       wt_read_cache(s);
+       run_diff_files(&rev, 0);
+}
+
+static void wt_status_print_untracked(struct wt_status *s)
+{
+       struct dir_struct dir;
+       int i;
+       int shown_header = 0;
+       struct strbuf buf;
+
+       strbuf_init(&buf, 0);
+       memset(&dir, 0, sizeof(dir));
+
+       if (!s->untracked) {
+               dir.show_other_directories = 1;
+               dir.hide_empty_directories = 1;
+       }
+       setup_standard_excludes(&dir);
+
+       read_directory(&dir, ".", "", 0, NULL);
+       for(i = 0; i < dir.nr; i++) {
+               /* check for matching entry, which is unmerged; lifted from
+                * builtin-ls-files:show_other_files */
+               struct dir_entry *ent = dir.entries[i];
+               int pos = cache_name_pos(ent->name, ent->len);
+               struct cache_entry *ce;
+               if (0 <= pos)
+                       die("bug in wt_status_print_untracked");
+               pos = -pos - 1;
+               if (pos < active_nr) {
+                       ce = active_cache[pos];
+                       if (ce_namelen(ce) == ent->len &&
+                           !memcmp(ce->name, ent->name, ent->len))
+                               continue;
+               }
+               if (!shown_header) {
+                       s->workdir_untracked = 1;
+                       wt_status_print_header(s, "Untracked files",
+                                              use_add_to_include_msg);
+                       shown_header = 1;
+               }
+               color_fprintf(s->fp, color(WT_STATUS_HEADER), "#\t");
+               color_fprintf_ln(s->fp, color(WT_STATUS_UNTRACKED), "%s",
+                               quote_path(ent->name, ent->len,
+                                       &buf, s->prefix));
+       }
+       strbuf_release(&buf);
+}
+
+static void wt_status_print_verbose(struct wt_status *s)
+{
+       struct rev_info rev;
+       int saved_stdout;
+
+       fflush(s->fp);
+
+       /* Sigh, the entire diff machinery is hardcoded to output to
+        * stdout.  Do the dup-dance...*/
+       saved_stdout = dup(STDOUT_FILENO);
+       if (saved_stdout < 0 ||dup2(fileno(s->fp), STDOUT_FILENO) < 0)
+               die("couldn't redirect stdout\n");
+
+       init_revisions(&rev, NULL);
+       setup_revisions(0, NULL, &rev, s->reference);
+       rev.diffopt.output_format |= DIFF_FORMAT_PATCH;
+       rev.diffopt.detect_rename = 1;
+       wt_read_cache(s);
+       run_diff_index(&rev, 1);
+
+       fflush(stdout);
+
+       if (dup2(saved_stdout, STDOUT_FILENO) < 0)
+               die("couldn't restore stdout\n");
+       close(saved_stdout);
+}
+
+void wt_status_print(struct wt_status *s)
+{
+       unsigned char sha1[20];
+       s->is_initial = get_sha1(s->reference, sha1) ? 1 : 0;
+
+       if (s->branch) {
+               const char *on_what = "On branch ";
+               const char *branch_name = s->branch;
+               if (!prefixcmp(branch_name, "refs/heads/"))
+                       branch_name += 11;
+               else if (!strcmp(branch_name, "HEAD")) {
+                       branch_name = "";
+                       on_what = "Not currently on any branch.";
+               }
+               color_fprintf_ln(s->fp, color(WT_STATUS_HEADER),
+                       "# %s%s", on_what, branch_name);
+       }
+
+       if (s->is_initial) {
+               color_fprintf_ln(s->fp, color(WT_STATUS_HEADER), "#");
+               color_fprintf_ln(s->fp, color(WT_STATUS_HEADER), "# Initial commit");
+               color_fprintf_ln(s->fp, color(WT_STATUS_HEADER), "#");
+               wt_status_print_initial(s);
+       }
+       else {
+               wt_status_print_updated(s);
+       }
+
+       wt_status_print_changed(s);
+       wt_status_print_untracked(s);
+
+       if (s->verbose && !s->is_initial)
+               wt_status_print_verbose(s);
+       if (!s->commitable) {
+               if (s->amend)
+                       fprintf(s->fp, "# No changes\n");
+               else if (s->workdir_dirty)
+                       printf("no changes added to commit (use \"git add\" and/or \"git commit -a\")\n");
+               else if (s->workdir_untracked)
+                       printf("nothing added to commit but untracked files present (use \"git add\" to track)\n");
+               else if (s->is_initial)
+                       printf("nothing to commit (create/copy files and use \"git add\" to track)\n");
+               else
+                       printf("nothing to commit (working directory clean)\n");
+       }
+}
+
+int git_status_config(const char *k, const char *v)
+{
+       if (!strcmp(k, "status.color") || !strcmp(k, "color.status")) {
+               wt_status_use_color = git_config_colorbool(k, v, -1);
+               return 0;
+       }
+       if (!prefixcmp(k, "status.color.") || !prefixcmp(k, "color.status.")) {
+               int slot = parse_status_slot(k, 13);
+               color_parse(v, k, wt_status_colors[slot]);
+       }
+       return git_default_config(k, v);
+}
diff --git a/wt-status.h b/wt-status.h
new file mode 100644 (file)
index 0000000..225fb4d
--- /dev/null
@@ -0,0 +1,34 @@
+#ifndef STATUS_H
+#define STATUS_H
+
+#include <stdio.h>
+
+enum color_wt_status {
+       WT_STATUS_HEADER,
+       WT_STATUS_UPDATED,
+       WT_STATUS_CHANGED,
+       WT_STATUS_UNTRACKED,
+};
+
+struct wt_status {
+       int is_initial;
+       char *branch;
+       const char *reference;
+       int verbose;
+       int amend;
+       int untracked;
+       /* These are computed during processing of the individual sections */
+       int commitable;
+       int workdir_dirty;
+       int workdir_untracked;
+       const char *index_file;
+       FILE *fp;
+       const char *prefix;
+};
+
+int git_status_config(const char *var, const char *value);
+int wt_status_use_color;
+void wt_status_prepare(struct wt_status *s);
+void wt_status_print(struct wt_status *s);
+
+#endif /* STATUS_H */
diff --git a/xdiff-interface.c b/xdiff-interface.c
new file mode 100644 (file)
index 0000000..be866d1
--- /dev/null
@@ -0,0 +1,202 @@
+#include "cache.h"
+#include "xdiff-interface.h"
+
+static int parse_num(char **cp_p, int *num_p)
+{
+       char *cp = *cp_p;
+       int num = 0;
+       int read_some;
+
+       while ('0' <= *cp && *cp <= '9')
+               num = num * 10 + *cp++ - '0';
+       if (!(read_some = cp - *cp_p))
+               return -1;
+       *cp_p = cp;
+       *num_p = num;
+       return 0;
+}
+
+int parse_hunk_header(char *line, int len,
+                     int *ob, int *on,
+                     int *nb, int *nn)
+{
+       char *cp;
+       cp = line + 4;
+       if (parse_num(&cp, ob)) {
+       bad_line:
+               return error("malformed diff output: %s", line);
+       }
+       if (*cp == ',') {
+               cp++;
+               if (parse_num(&cp, on))
+                       goto bad_line;
+       }
+       else
+               *on = 1;
+       if (*cp++ != ' ' || *cp++ != '+')
+               goto bad_line;
+       if (parse_num(&cp, nb))
+               goto bad_line;
+       if (*cp == ',') {
+               cp++;
+               if (parse_num(&cp, nn))
+                       goto bad_line;
+       }
+       else
+               *nn = 1;
+       return -!!memcmp(cp, " @@", 3);
+}
+
+static void consume_one(void *priv_, char *s, unsigned long size)
+{
+       struct xdiff_emit_state *priv = priv_;
+       char *ep;
+       while (size) {
+               unsigned long this_size;
+               ep = memchr(s, '\n', size);
+               this_size = (ep == NULL) ? size : (ep - s + 1);
+               priv->consume(priv, s, this_size);
+               size -= this_size;
+               s += this_size;
+       }
+}
+
+int xdiff_outf(void *priv_, mmbuffer_t *mb, int nbuf)
+{
+       struct xdiff_emit_state *priv = priv_;
+       int i;
+
+       for (i = 0; i < nbuf; i++) {
+               if (mb[i].ptr[mb[i].size-1] != '\n') {
+                       /* Incomplete line */
+                       priv->remainder = xrealloc(priv->remainder,
+                                                  priv->remainder_size +
+                                                  mb[i].size);
+                       memcpy(priv->remainder + priv->remainder_size,
+                              mb[i].ptr, mb[i].size);
+                       priv->remainder_size += mb[i].size;
+                       continue;
+               }
+
+               /* we have a complete line */
+               if (!priv->remainder) {
+                       consume_one(priv, mb[i].ptr, mb[i].size);
+                       continue;
+               }
+               priv->remainder = xrealloc(priv->remainder,
+                                          priv->remainder_size +
+                                          mb[i].size);
+               memcpy(priv->remainder + priv->remainder_size,
+                      mb[i].ptr, mb[i].size);
+               consume_one(priv, priv->remainder,
+                           priv->remainder_size + mb[i].size);
+               free(priv->remainder);
+               priv->remainder = NULL;
+               priv->remainder_size = 0;
+       }
+       if (priv->remainder) {
+               consume_one(priv, priv->remainder, priv->remainder_size);
+               free(priv->remainder);
+               priv->remainder = NULL;
+               priv->remainder_size = 0;
+       }
+       return 0;
+}
+
+int read_mmfile(mmfile_t *ptr, const char *filename)
+{
+       struct stat st;
+       FILE *f;
+       size_t sz;
+
+       if (stat(filename, &st))
+               return error("Could not stat %s", filename);
+       if ((f = fopen(filename, "rb")) == NULL)
+               return error("Could not open %s", filename);
+       sz = xsize_t(st.st_size);
+       ptr->ptr = xmalloc(sz);
+       if (fread(ptr->ptr, sz, 1, f) != 1)
+               return error("Could not read %s", filename);
+       fclose(f);
+       ptr->size = sz;
+       return 0;
+}
+
+#define FIRST_FEW_BYTES 8000
+int buffer_is_binary(const char *ptr, unsigned long size)
+{
+       if (FIRST_FEW_BYTES < size)
+               size = FIRST_FEW_BYTES;
+       return !!memchr(ptr, 0, size);
+}
+
+struct ff_regs {
+       int nr;
+       struct ff_reg {
+               regex_t re;
+               int negate;
+       } *array;
+};
+
+static long ff_regexp(const char *line, long len,
+               char *buffer, long buffer_size, void *priv)
+{
+       char *line_buffer = xstrndup(line, len); /* make NUL terminated */
+       struct ff_regs *regs = priv;
+       regmatch_t pmatch[2];
+       int result = 0, i;
+
+       for (i = 0; i < regs->nr; i++) {
+               struct ff_reg *reg = regs->array + i;
+               if (reg->negate ^ !!regexec(&reg->re,
+                                       line_buffer, 2, pmatch, 0)) {
+                       free(line_buffer);
+                       return -1;
+               }
+       }
+       i = pmatch[1].rm_so >= 0 ? 1 : 0;
+       line += pmatch[i].rm_so;
+       result = pmatch[i].rm_eo - pmatch[i].rm_so;
+       if (result > buffer_size)
+               result = buffer_size;
+       else
+               while (result > 0 && (isspace(line[result - 1]) ||
+                                       line[result - 1] == '\n'))
+                       result--;
+       memcpy(buffer, line, result);
+       free(line_buffer);
+       return result;
+}
+
+void xdiff_set_find_func(xdemitconf_t *xecfg, const char *value)
+{
+       int i;
+       struct ff_regs *regs;
+
+       xecfg->find_func = ff_regexp;
+       regs = xecfg->find_func_priv = xmalloc(sizeof(struct ff_regs));
+       for (i = 0, regs->nr = 1; value[i]; i++)
+               if (value[i] == '\n')
+                       regs->nr++;
+       regs->array = xmalloc(regs->nr * sizeof(struct ff_reg));
+       for (i = 0; i < regs->nr; i++) {
+               struct ff_reg *reg = regs->array + i;
+               const char *ep = strchr(value, '\n'), *expression;
+               char *buffer = NULL;
+
+               reg->negate = (*value == '!');
+               if (reg->negate && i == regs->nr - 1)
+                       die("Last expression must not be negated: %s", value);
+               if (*value == '!')
+                       value++;
+               if (ep)
+                       expression = buffer = xstrndup(value, ep - value);
+               else
+                       expression = value;
+               if (regcomp(&reg->re, expression, 0))
+                       die("Invalid regexp to look for hunk header: %s", expression);
+               if (buffer)
+                       free(buffer);
+               value = ep + 1;
+       }
+}
diff --git a/xdiff-interface.h b/xdiff-interface.h
new file mode 100644 (file)
index 0000000..fb742db
--- /dev/null
@@ -0,0 +1,25 @@
+#ifndef XDIFF_INTERFACE_H
+#define XDIFF_INTERFACE_H
+
+#include "xdiff/xdiff.h"
+
+struct xdiff_emit_state;
+
+typedef void (*xdiff_emit_consume_fn)(void *, char *, unsigned long);
+
+struct xdiff_emit_state {
+       xdiff_emit_consume_fn consume;
+       char *remainder;
+       unsigned long remainder_size;
+};
+
+int xdiff_outf(void *priv_, mmbuffer_t *mb, int nbuf);
+int parse_hunk_header(char *line, int len,
+                     int *ob, int *on,
+                     int *nb, int *nn);
+int read_mmfile(mmfile_t *ptr, const char *filename);
+int buffer_is_binary(const char *ptr, unsigned long size);
+
+extern void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line);
+
+#endif
diff --git a/xdiff/xdiff.h b/xdiff/xdiff.h
new file mode 100644 (file)
index 0000000..c00ddaa
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003  Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#if !defined(XDIFF_H)
+#define XDIFF_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* #ifdef __cplusplus */
+
+
+#define XDF_NEED_MINIMAL (1 << 1)
+#define XDF_IGNORE_WHITESPACE (1 << 2)
+#define XDF_IGNORE_WHITESPACE_CHANGE (1 << 3)
+#define XDF_IGNORE_WHITESPACE_AT_EOL (1 << 4)
+#define XDF_WHITESPACE_FLAGS (XDF_IGNORE_WHITESPACE | XDF_IGNORE_WHITESPACE_CHANGE | XDF_IGNORE_WHITESPACE_AT_EOL)
+
+#define XDL_PATCH_NORMAL '-'
+#define XDL_PATCH_REVERSE '+'
+#define XDL_PATCH_MODEMASK ((1 << 8) - 1)
+#define XDL_PATCH_IGNOREBSPACE (1 << 8)
+
+#define XDL_EMIT_FUNCNAMES (1 << 0)
+#define XDL_EMIT_COMMON (1 << 1)
+
+#define XDL_MMB_READONLY (1 << 0)
+
+#define XDL_MMF_ATOMIC (1 << 0)
+
+#define XDL_BDOP_INS 1
+#define XDL_BDOP_CPY 2
+#define XDL_BDOP_INSB 3
+
+#define XDL_MERGE_MINIMAL 0
+#define XDL_MERGE_EAGER 1
+#define XDL_MERGE_ZEALOUS 2
+
+typedef struct s_mmfile {
+       char *ptr;
+       long size;
+} mmfile_t;
+
+typedef struct s_mmbuffer {
+       char *ptr;
+       long size;
+} mmbuffer_t;
+
+typedef struct s_xpparam {
+       unsigned long flags;
+} xpparam_t;
+
+typedef struct s_xdemitcb {
+       void *priv;
+       int (*outf)(void *, mmbuffer_t *, int);
+} xdemitcb_t;
+
+typedef long (*find_func_t)(const char *line, long line_len, char *buffer, long buffer_size, void *priv);
+
+typedef struct s_xdemitconf {
+       long ctxlen;
+       unsigned long flags;
+       find_func_t find_func;
+       void *find_func_priv;
+} xdemitconf_t;
+
+typedef struct s_bdiffparam {
+       long bsize;
+} bdiffparam_t;
+
+
+#define xdl_malloc(x) malloc(x)
+#define xdl_free(ptr) free(ptr)
+#define xdl_realloc(ptr,x) realloc(ptr,x)
+
+void *xdl_mmfile_first(mmfile_t *mmf, long *size);
+void *xdl_mmfile_next(mmfile_t *mmf, long *size);
+long xdl_mmfile_size(mmfile_t *mmf);
+
+int xdl_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
+            xdemitconf_t const *xecfg, xdemitcb_t *ecb);
+
+int xdl_merge(mmfile_t *orig, mmfile_t *mf1, const char *name1,
+               mmfile_t *mf2, const char *name2,
+               xpparam_t const *xpp, int level, mmbuffer_t *result);
+
+#ifdef __cplusplus
+}
+#endif /* #ifdef __cplusplus */
+
+#endif /* #if !defined(XDIFF_H) */
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
new file mode 100644 (file)
index 0000000..1bad846
--- /dev/null
@@ -0,0 +1,565 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003 Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#include "xinclude.h"
+
+
+
+#define XDL_MAX_COST_MIN 256
+#define XDL_HEUR_MIN_COST 256
+#define XDL_LINE_MAX (long)((1UL << (8 * sizeof(long) - 1)) - 1)
+#define XDL_SNAKE_CNT 20
+#define XDL_K_HEUR 4
+
+
+
+typedef struct s_xdpsplit {
+       long i1, i2;
+       int min_lo, min_hi;
+} xdpsplit_t;
+
+
+
+
+static long xdl_split(unsigned long const *ha1, long off1, long lim1,
+                     unsigned long const *ha2, long off2, long lim2,
+                     long *kvdf, long *kvdb, int need_min, xdpsplit_t *spl,
+                     xdalgoenv_t *xenv);
+static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1, long chg2);
+
+
+
+
+
+/*
+ * See "An O(ND) Difference Algorithm and its Variations", by Eugene Myers.
+ * Basically considers a "box" (off1, off2, lim1, lim2) and scan from both
+ * the forward diagonal starting from (off1, off2) and the backward diagonal
+ * starting from (lim1, lim2). If the K values on the same diagonal crosses
+ * returns the furthest point of reach. We might end up having to expensive
+ * cases using this algorithm is full, so a little bit of heuristic is needed
+ * to cut the search and to return a suboptimal point.
+ */
+static long xdl_split(unsigned long const *ha1, long off1, long lim1,
+                     unsigned long const *ha2, long off2, long lim2,
+                     long *kvdf, long *kvdb, int need_min, xdpsplit_t *spl,
+                     xdalgoenv_t *xenv) {
+       long dmin = off1 - lim2, dmax = lim1 - off2;
+       long fmid = off1 - off2, bmid = lim1 - lim2;
+       long odd = (fmid - bmid) & 1;
+       long fmin = fmid, fmax = fmid;
+       long bmin = bmid, bmax = bmid;
+       long ec, d, i1, i2, prev1, best, dd, v, k;
+
+       /*
+        * Set initial diagonal values for both forward and backward path.
+        */
+       kvdf[fmid] = off1;
+       kvdb[bmid] = lim1;
+
+       for (ec = 1;; ec++) {
+               int got_snake = 0;
+
+               /*
+                * We need to extent the diagonal "domain" by one. If the next
+                * values exits the box boundaries we need to change it in the
+                * opposite direction because (max - min) must be a power of two.
+                * Also we initialize the external K value to -1 so that we can
+                * avoid extra conditions check inside the core loop.
+                */
+               if (fmin > dmin)
+                       kvdf[--fmin - 1] = -1;
+               else
+                       ++fmin;
+               if (fmax < dmax)
+                       kvdf[++fmax + 1] = -1;
+               else
+                       --fmax;
+
+               for (d = fmax; d >= fmin; d -= 2) {
+                       if (kvdf[d - 1] >= kvdf[d + 1])
+                               i1 = kvdf[d - 1] + 1;
+                       else
+                               i1 = kvdf[d + 1];
+                       prev1 = i1;
+                       i2 = i1 - d;
+                       for (; i1 < lim1 && i2 < lim2 && ha1[i1] == ha2[i2]; i1++, i2++);
+                       if (i1 - prev1 > xenv->snake_cnt)
+                               got_snake = 1;
+                       kvdf[d] = i1;
+                       if (odd && bmin <= d && d <= bmax && kvdb[d] <= i1) {
+                               spl->i1 = i1;
+                               spl->i2 = i2;
+                               spl->min_lo = spl->min_hi = 1;
+                               return ec;
+                       }
+               }
+
+               /*
+                * We need to extent the diagonal "domain" by one. If the next
+                * values exits the box boundaries we need to change it in the
+                * opposite direction because (max - min) must be a power of two.
+                * Also we initialize the external K value to -1 so that we can
+                * avoid extra conditions check inside the core loop.
+                */
+               if (bmin > dmin)
+                       kvdb[--bmin - 1] = XDL_LINE_MAX;
+               else
+                       ++bmin;
+               if (bmax < dmax)
+                       kvdb[++bmax + 1] = XDL_LINE_MAX;
+               else
+                       --bmax;
+
+               for (d = bmax; d >= bmin; d -= 2) {
+                       if (kvdb[d - 1] < kvdb[d + 1])
+                               i1 = kvdb[d - 1];
+                       else
+                               i1 = kvdb[d + 1] - 1;
+                       prev1 = i1;
+                       i2 = i1 - d;
+                       for (; i1 > off1 && i2 > off2 && ha1[i1 - 1] == ha2[i2 - 1]; i1--, i2--);
+                       if (prev1 - i1 > xenv->snake_cnt)
+                               got_snake = 1;
+                       kvdb[d] = i1;
+                       if (!odd && fmin <= d && d <= fmax && i1 <= kvdf[d]) {
+                               spl->i1 = i1;
+                               spl->i2 = i2;
+                               spl->min_lo = spl->min_hi = 1;
+                               return ec;
+                       }
+               }
+
+               if (need_min)
+                       continue;
+
+               /*
+                * If the edit cost is above the heuristic trigger and if
+                * we got a good snake, we sample current diagonals to see
+                * if some of the, have reached an "interesting" path. Our
+                * measure is a function of the distance from the diagonal
+                * corner (i1 + i2) penalized with the distance from the
+                * mid diagonal itself. If this value is above the current
+                * edit cost times a magic factor (XDL_K_HEUR) we consider
+                * it interesting.
+                */
+               if (got_snake && ec > xenv->heur_min) {
+                       for (best = 0, d = fmax; d >= fmin; d -= 2) {
+                               dd = d > fmid ? d - fmid: fmid - d;
+                               i1 = kvdf[d];
+                               i2 = i1 - d;
+                               v = (i1 - off1) + (i2 - off2) - dd;
+
+                               if (v > XDL_K_HEUR * ec && v > best &&
+                                   off1 + xenv->snake_cnt <= i1 && i1 < lim1 &&
+                                   off2 + xenv->snake_cnt <= i2 && i2 < lim2) {
+                                       for (k = 1; ha1[i1 - k] == ha2[i2 - k]; k++)
+                                               if (k == xenv->snake_cnt) {
+                                                       best = v;
+                                                       spl->i1 = i1;
+                                                       spl->i2 = i2;
+                                                       break;
+                                               }
+                               }
+                       }
+                       if (best > 0) {
+                               spl->min_lo = 1;
+                               spl->min_hi = 0;
+                               return ec;
+                       }
+
+                       for (best = 0, d = bmax; d >= bmin; d -= 2) {
+                               dd = d > bmid ? d - bmid: bmid - d;
+                               i1 = kvdb[d];
+                               i2 = i1 - d;
+                               v = (lim1 - i1) + (lim2 - i2) - dd;
+
+                               if (v > XDL_K_HEUR * ec && v > best &&
+                                   off1 < i1 && i1 <= lim1 - xenv->snake_cnt &&
+                                   off2 < i2 && i2 <= lim2 - xenv->snake_cnt) {
+                                       for (k = 0; ha1[i1 + k] == ha2[i2 + k]; k++)
+                                               if (k == xenv->snake_cnt - 1) {
+                                                       best = v;
+                                                       spl->i1 = i1;
+                                                       spl->i2 = i2;
+                                                       break;
+                                               }
+                               }
+                       }
+                       if (best > 0) {
+                               spl->min_lo = 0;
+                               spl->min_hi = 1;
+                               return ec;
+                       }
+               }
+
+               /*
+                * Enough is enough. We spent too much time here and now we collect
+                * the furthest reaching path using the (i1 + i2) measure.
+                */
+               if (ec >= xenv->mxcost) {
+                       long fbest, fbest1, bbest, bbest1;
+
+                       fbest = fbest1 = -1;
+                       for (d = fmax; d >= fmin; d -= 2) {
+                               i1 = XDL_MIN(kvdf[d], lim1);
+                               i2 = i1 - d;
+                               if (lim2 < i2)
+                                       i1 = lim2 + d, i2 = lim2;
+                               if (fbest < i1 + i2) {
+                                       fbest = i1 + i2;
+                                       fbest1 = i1;
+                               }
+                       }
+
+                       bbest = bbest1 = XDL_LINE_MAX;
+                       for (d = bmax; d >= bmin; d -= 2) {
+                               i1 = XDL_MAX(off1, kvdb[d]);
+                               i2 = i1 - d;
+                               if (i2 < off2)
+                                       i1 = off2 + d, i2 = off2;
+                               if (i1 + i2 < bbest) {
+                                       bbest = i1 + i2;
+                                       bbest1 = i1;
+                               }
+                       }
+
+                       if ((lim1 + lim2) - bbest < fbest - (off1 + off2)) {
+                               spl->i1 = fbest1;
+                               spl->i2 = fbest - fbest1;
+                               spl->min_lo = 1;
+                               spl->min_hi = 0;
+                       } else {
+                               spl->i1 = bbest1;
+                               spl->i2 = bbest - bbest1;
+                               spl->min_lo = 0;
+                               spl->min_hi = 1;
+                       }
+                       return ec;
+               }
+       }
+}
+
+
+/*
+ * Rule: "Divide et Impera". Recursively split the box in sub-boxes by calling
+ * the box splitting function. Note that the real job (marking changed lines)
+ * is done in the two boundary reaching checks.
+ */
+int xdl_recs_cmp(diffdata_t *dd1, long off1, long lim1,
+                diffdata_t *dd2, long off2, long lim2,
+                long *kvdf, long *kvdb, int need_min, xdalgoenv_t *xenv) {
+       unsigned long const *ha1 = dd1->ha, *ha2 = dd2->ha;
+
+       /*
+        * Shrink the box by walking through each diagonal snake (SW and NE).
+        */
+       for (; off1 < lim1 && off2 < lim2 && ha1[off1] == ha2[off2]; off1++, off2++);
+       for (; off1 < lim1 && off2 < lim2 && ha1[lim1 - 1] == ha2[lim2 - 1]; lim1--, lim2--);
+
+       /*
+        * If one dimension is empty, then all records on the other one must
+        * be obviously changed.
+        */
+       if (off1 == lim1) {
+               char *rchg2 = dd2->rchg;
+               long *rindex2 = dd2->rindex;
+
+               for (; off2 < lim2; off2++)
+                       rchg2[rindex2[off2]] = 1;
+       } else if (off2 == lim2) {
+               char *rchg1 = dd1->rchg;
+               long *rindex1 = dd1->rindex;
+
+               for (; off1 < lim1; off1++)
+                       rchg1[rindex1[off1]] = 1;
+       } else {
+               long ec;
+               xdpsplit_t spl;
+               spl.i1 = spl.i2 = 0;
+
+               /*
+                * Divide ...
+                */
+               if ((ec = xdl_split(ha1, off1, lim1, ha2, off2, lim2, kvdf, kvdb,
+                                   need_min, &spl, xenv)) < 0) {
+
+                       return -1;
+               }
+
+               /*
+                * ... et Impera.
+                */
+               if (xdl_recs_cmp(dd1, off1, spl.i1, dd2, off2, spl.i2,
+                                kvdf, kvdb, spl.min_lo, xenv) < 0 ||
+                   xdl_recs_cmp(dd1, spl.i1, lim1, dd2, spl.i2, lim2,
+                                kvdf, kvdb, spl.min_hi, xenv) < 0) {
+
+                       return -1;
+               }
+       }
+
+       return 0;
+}
+
+
+int xdl_do_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
+               xdfenv_t *xe) {
+       long ndiags;
+       long *kvd, *kvdf, *kvdb;
+       xdalgoenv_t xenv;
+       diffdata_t dd1, dd2;
+
+       if (xdl_prepare_env(mf1, mf2, xpp, xe) < 0) {
+
+               return -1;
+       }
+
+       /*
+        * Allocate and setup K vectors to be used by the differential algorithm.
+        * One is to store the forward path and one to store the backward path.
+        */
+       ndiags = xe->xdf1.nreff + xe->xdf2.nreff + 3;
+       if (!(kvd = (long *) xdl_malloc((2 * ndiags + 2) * sizeof(long)))) {
+
+               xdl_free_env(xe);
+               return -1;
+       }
+       kvdf = kvd;
+       kvdb = kvdf + ndiags;
+       kvdf += xe->xdf2.nreff + 1;
+       kvdb += xe->xdf2.nreff + 1;
+
+       xenv.mxcost = xdl_bogosqrt(ndiags);
+       if (xenv.mxcost < XDL_MAX_COST_MIN)
+               xenv.mxcost = XDL_MAX_COST_MIN;
+       xenv.snake_cnt = XDL_SNAKE_CNT;
+       xenv.heur_min = XDL_HEUR_MIN_COST;
+
+       dd1.nrec = xe->xdf1.nreff;
+       dd1.ha = xe->xdf1.ha;
+       dd1.rchg = xe->xdf1.rchg;
+       dd1.rindex = xe->xdf1.rindex;
+       dd2.nrec = xe->xdf2.nreff;
+       dd2.ha = xe->xdf2.ha;
+       dd2.rchg = xe->xdf2.rchg;
+       dd2.rindex = xe->xdf2.rindex;
+
+       if (xdl_recs_cmp(&dd1, 0, dd1.nrec, &dd2, 0, dd2.nrec,
+                        kvdf, kvdb, (xpp->flags & XDF_NEED_MINIMAL) != 0, &xenv) < 0) {
+
+               xdl_free(kvd);
+               xdl_free_env(xe);
+               return -1;
+       }
+
+       xdl_free(kvd);
+
+       return 0;
+}
+
+
+static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1, long chg2) {
+       xdchange_t *xch;
+
+       if (!(xch = (xdchange_t *) xdl_malloc(sizeof(xdchange_t))))
+               return NULL;
+
+       xch->next = xscr;
+       xch->i1 = i1;
+       xch->i2 = i2;
+       xch->chg1 = chg1;
+       xch->chg2 = chg2;
+
+       return xch;
+}
+
+
+int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
+       long ix, ixo, ixs, ixref, grpsiz, nrec = xdf->nrec;
+       char *rchg = xdf->rchg, *rchgo = xdfo->rchg;
+       xrecord_t **recs = xdf->recs;
+
+       /*
+        * This is the same of what GNU diff does. Move back and forward
+        * change groups for a consistent and pretty diff output. This also
+        * helps in finding joinable change groups and reduce the diff size.
+        */
+       for (ix = ixo = 0;;) {
+               /*
+                * Find the first changed line in the to-be-compacted file.
+                * We need to keep track of both indexes, so if we find a
+                * changed lines group on the other file, while scanning the
+                * to-be-compacted file, we need to skip it properly. Note
+                * that loops that are testing for changed lines on rchg* do
+                * not need index bounding since the array is prepared with
+                * a zero at position -1 and N.
+                */
+               for (; ix < nrec && !rchg[ix]; ix++)
+                       while (rchgo[ixo++]);
+               if (ix == nrec)
+                       break;
+
+               /*
+                * Record the start of a changed-group in the to-be-compacted file
+                * and find the end of it, on both to-be-compacted and other file
+                * indexes (ix and ixo).
+                */
+               ixs = ix;
+               for (ix++; rchg[ix]; ix++);
+               for (; rchgo[ixo]; ixo++);
+
+               do {
+                       grpsiz = ix - ixs;
+
+                       /*
+                        * If the line before the current change group, is equal to
+                        * the last line of the current change group, shift backward
+                        * the group.
+                        */
+                       while (ixs > 0 && recs[ixs - 1]->ha == recs[ix - 1]->ha &&
+                              xdl_recmatch(recs[ixs - 1]->ptr, recs[ixs - 1]->size, recs[ix - 1]->ptr, recs[ix - 1]->size, flags)) {
+                               rchg[--ixs] = 1;
+                               rchg[--ix] = 0;
+
+                               /*
+                                * This change might have joined two change groups,
+                                * so we try to take this scenario in account by moving
+                                * the start index accordingly (and so the other-file
+                                * end-of-group index).
+                                */
+                               for (; rchg[ixs - 1]; ixs--);
+                               while (rchgo[--ixo]);
+                       }
+
+                       /*
+                        * Record the end-of-group position in case we are matched
+                        * with a group of changes in the other file (that is, the
+                        * change record before the enf-of-group index in the other
+                        * file is set).
+                        */
+                       ixref = rchgo[ixo - 1] ? ix: nrec;
+
+                       /*
+                        * If the first line of the current change group, is equal to
+                        * the line next of the current change group, shift forward
+                        * the group.
+                        */
+                       while (ix < nrec && recs[ixs]->ha == recs[ix]->ha &&
+                              xdl_recmatch(recs[ixs]->ptr, recs[ixs]->size, recs[ix]->ptr, recs[ix]->size, flags)) {
+                               rchg[ixs++] = 0;
+                               rchg[ix++] = 1;
+
+                               /*
+                                * This change might have joined two change groups,
+                                * so we try to take this scenario in account by moving
+                                * the start index accordingly (and so the other-file
+                                * end-of-group index). Keep tracking the reference
+                                * index in case we are shifting together with a
+                                * corresponding group of changes in the other file.
+                                */
+                               for (; rchg[ix]; ix++);
+                               while (rchgo[++ixo])
+                                       ixref = ix;
+                       }
+               } while (grpsiz != ix - ixs);
+
+               /*
+                * Try to move back the possibly merged group of changes, to match
+                * the recorded postion in the other file.
+                */
+               while (ixref < ix) {
+                       rchg[--ixs] = 1;
+                       rchg[--ix] = 0;
+                       while (rchgo[--ixo]);
+               }
+       }
+
+       return 0;
+}
+
+
+int xdl_build_script(xdfenv_t *xe, xdchange_t **xscr) {
+       xdchange_t *cscr = NULL, *xch;
+       char *rchg1 = xe->xdf1.rchg, *rchg2 = xe->xdf2.rchg;
+       long i1, i2, l1, l2;
+
+       /*
+        * Trivial. Collects "groups" of changes and creates an edit script.
+        */
+       for (i1 = xe->xdf1.nrec, i2 = xe->xdf2.nrec; i1 >= 0 || i2 >= 0; i1--, i2--)
+               if (rchg1[i1 - 1] || rchg2[i2 - 1]) {
+                       for (l1 = i1; rchg1[i1 - 1]; i1--);
+                       for (l2 = i2; rchg2[i2 - 1]; i2--);
+
+                       if (!(xch = xdl_add_change(cscr, i1, i2, l1 - i1, l2 - i2))) {
+                               xdl_free_script(cscr);
+                               return -1;
+                       }
+                       cscr = xch;
+               }
+
+       *xscr = cscr;
+
+       return 0;
+}
+
+
+void xdl_free_script(xdchange_t *xscr) {
+       xdchange_t *xch;
+
+       while ((xch = xscr) != NULL) {
+               xscr = xscr->next;
+               xdl_free(xch);
+       }
+}
+
+
+int xdl_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
+            xdemitconf_t const *xecfg, xdemitcb_t *ecb) {
+       xdchange_t *xscr;
+       xdfenv_t xe;
+
+       if (xdl_do_diff(mf1, mf2, xpp, &xe) < 0) {
+
+               return -1;
+       }
+       if (xdl_change_compact(&xe.xdf1, &xe.xdf2, xpp->flags) < 0 ||
+           xdl_change_compact(&xe.xdf2, &xe.xdf1, xpp->flags) < 0 ||
+           xdl_build_script(&xe, &xscr) < 0) {
+
+               xdl_free_env(&xe);
+               return -1;
+       }
+       if (xscr) {
+               if (xdl_emit_diff(&xe, xscr, ecb, xecfg) < 0) {
+
+                       xdl_free_script(xscr);
+                       xdl_free_env(&xe);
+                       return -1;
+               }
+               xdl_free_script(xscr);
+       }
+       xdl_free_env(&xe);
+
+       return 0;
+}
diff --git a/xdiff/xdiffi.h b/xdiff/xdiffi.h
new file mode 100644 (file)
index 0000000..3e099dc
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003  Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#if !defined(XDIFFI_H)
+#define XDIFFI_H
+
+
+typedef struct s_diffdata {
+       long nrec;
+       unsigned long const *ha;
+       long *rindex;
+       char *rchg;
+} diffdata_t;
+
+typedef struct s_xdalgoenv {
+       long mxcost;
+       long snake_cnt;
+       long heur_min;
+} xdalgoenv_t;
+
+typedef struct s_xdchange {
+       struct s_xdchange *next;
+       long i1, i2;
+       long chg1, chg2;
+} xdchange_t;
+
+
+
+int xdl_recs_cmp(diffdata_t *dd1, long off1, long lim1,
+                diffdata_t *dd2, long off2, long lim2,
+                long *kvdf, long *kvdb, int need_min, xdalgoenv_t *xenv);
+int xdl_do_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
+               xdfenv_t *xe);
+int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags);
+int xdl_build_script(xdfenv_t *xe, xdchange_t **xscr);
+void xdl_free_script(xdchange_t *xscr);
+int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb,
+                 xdemitconf_t const *xecfg);
+
+#endif /* #if !defined(XDIFFI_H) */
diff --git a/xdiff/xemit.c b/xdiff/xemit.c
new file mode 100644 (file)
index 0000000..d3d9c84
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003 Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#include "xinclude.h"
+
+
+
+
+static long xdl_get_rec(xdfile_t *xdf, long ri, char const **rec);
+static int xdl_emit_record(xdfile_t *xdf, long ri, char const *pre, xdemitcb_t *ecb);
+static xdchange_t *xdl_get_hunk(xdchange_t *xscr, xdemitconf_t const *xecfg);
+
+
+
+
+static long xdl_get_rec(xdfile_t *xdf, long ri, char const **rec) {
+
+       *rec = xdf->recs[ri]->ptr;
+
+       return xdf->recs[ri]->size;
+}
+
+
+static int xdl_emit_record(xdfile_t *xdf, long ri, char const *pre, xdemitcb_t *ecb) {
+       long size, psize = strlen(pre);
+       char const *rec;
+
+       size = xdl_get_rec(xdf, ri, &rec);
+       if (xdl_emit_diffrec(rec, size, pre, psize, ecb) < 0) {
+
+               return -1;
+       }
+
+       return 0;
+}
+
+
+/*
+ * Starting at the passed change atom, find the latest change atom to be included
+ * inside the differential hunk according to the specified configuration.
+ */
+static xdchange_t *xdl_get_hunk(xdchange_t *xscr, xdemitconf_t const *xecfg) {
+       xdchange_t *xch, *xchp;
+
+       for (xchp = xscr, xch = xscr->next; xch; xchp = xch, xch = xch->next)
+               if (xch->i1 - (xchp->i1 + xchp->chg1) > 2 * xecfg->ctxlen)
+                       break;
+
+       return xchp;
+}
+
+
+static long def_ff(const char *rec, long len, char *buf, long sz, void *priv)
+{
+       if (len > 0 &&
+                       (isalpha((unsigned char)*rec) || /* identifier? */
+                        *rec == '_' || /* also identifier? */
+                        *rec == '$')) { /* identifiers from VMS and other esoterico */
+               if (len > sz)
+                       len = sz;
+               while (0 < len && isspace((unsigned char)rec[len - 1]))
+                       len--;
+               memcpy(buf, rec, len);
+               return len;
+       }
+       return -1;
+}
+
+static void xdl_find_func(xdfile_t *xf, long i, char *buf, long sz, long *ll,
+               find_func_t ff, void *ff_priv) {
+
+       /*
+        * Be quite stupid about this for now.  Find a line in the old file
+        * before the start of the hunk (and context) which starts with a
+        * plausible character.
+        */
+
+       const char *rec;
+       long len;
+
+       while (i-- > 0) {
+               len = xdl_get_rec(xf, i, &rec);
+               if ((*ll = ff(rec, len, buf, sz, ff_priv)) >= 0)
+                       return;
+       }
+       *ll = 0;
+}
+
+
+static int xdl_emit_common(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb,
+                           xdemitconf_t const *xecfg) {
+       xdfile_t *xdf = &xe->xdf1;
+       const char *rchg = xdf->rchg;
+       long ix;
+
+       for (ix = 0; ix < xdf->nrec; ix++) {
+               if (rchg[ix])
+                       continue;
+               if (xdl_emit_record(xdf, ix, "", ecb))
+                       return -1;
+       }
+       return 0;
+}
+
+int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb,
+                 xdemitconf_t const *xecfg) {
+       long s1, s2, e1, e2, lctx;
+       xdchange_t *xch, *xche;
+       char funcbuf[80];
+       long funclen = 0;
+       find_func_t ff = xecfg->find_func ?  xecfg->find_func : def_ff;
+
+       if (xecfg->flags & XDL_EMIT_COMMON)
+               return xdl_emit_common(xe, xscr, ecb, xecfg);
+
+       for (xch = xche = xscr; xch; xch = xche->next) {
+               xche = xdl_get_hunk(xch, xecfg);
+
+               s1 = XDL_MAX(xch->i1 - xecfg->ctxlen, 0);
+               s2 = XDL_MAX(xch->i2 - xecfg->ctxlen, 0);
+
+               lctx = xecfg->ctxlen;
+               lctx = XDL_MIN(lctx, xe->xdf1.nrec - (xche->i1 + xche->chg1));
+               lctx = XDL_MIN(lctx, xe->xdf2.nrec - (xche->i2 + xche->chg2));
+
+               e1 = xche->i1 + xche->chg1 + lctx;
+               e2 = xche->i2 + xche->chg2 + lctx;
+
+               /*
+                * Emit current hunk header.
+                */
+
+               if (xecfg->flags & XDL_EMIT_FUNCNAMES) {
+                       xdl_find_func(&xe->xdf1, s1, funcbuf,
+                                     sizeof(funcbuf), &funclen,
+                                     ff, xecfg->find_func_priv);
+               }
+               if (xdl_emit_hunk_hdr(s1 + 1, e1 - s1, s2 + 1, e2 - s2,
+                                     funcbuf, funclen, ecb) < 0)
+                       return -1;
+
+               /*
+                * Emit pre-context.
+                */
+               for (; s1 < xch->i1; s1++)
+                       if (xdl_emit_record(&xe->xdf1, s1, " ", ecb) < 0)
+                               return -1;
+
+               for (s1 = xch->i1, s2 = xch->i2;; xch = xch->next) {
+                       /*
+                        * Merge previous with current change atom.
+                        */
+                       for (; s1 < xch->i1 && s2 < xch->i2; s1++, s2++)
+                               if (xdl_emit_record(&xe->xdf1, s1, " ", ecb) < 0)
+                                       return -1;
+
+                       /*
+                        * Removes lines from the first file.
+                        */
+                       for (s1 = xch->i1; s1 < xch->i1 + xch->chg1; s1++)
+                               if (xdl_emit_record(&xe->xdf1, s1, "-", ecb) < 0)
+                                       return -1;
+
+                       /*
+                        * Adds lines from the second file.
+                        */
+                       for (s2 = xch->i2; s2 < xch->i2 + xch->chg2; s2++)
+                               if (xdl_emit_record(&xe->xdf2, s2, "+", ecb) < 0)
+                                       return -1;
+
+                       if (xch == xche)
+                               break;
+                       s1 = xch->i1 + xch->chg1;
+                       s2 = xch->i2 + xch->chg2;
+               }
+
+               /*
+                * Emit post-context.
+                */
+               for (s1 = xche->i1 + xche->chg1; s1 < e1; s1++)
+                       if (xdl_emit_record(&xe->xdf1, s1, " ", ecb) < 0)
+                               return -1;
+       }
+
+       return 0;
+}
diff --git a/xdiff/xemit.h b/xdiff/xemit.h
new file mode 100644 (file)
index 0000000..440a739
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003  Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#if !defined(XEMIT_H)
+#define XEMIT_H
+
+
+
+int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb,
+                 xdemitconf_t const *xecfg);
+
+
+
+#endif /* #if !defined(XEMIT_H) */
diff --git a/xdiff/xinclude.h b/xdiff/xinclude.h
new file mode 100644 (file)
index 0000000..526ccb3
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003  Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#if !defined(XINCLUDE_H)
+#define XINCLUDE_H
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <limits.h>
+
+#include "xmacros.h"
+#include "xdiff.h"
+#include "xtypes.h"
+#include "xutils.h"
+#include "xprepare.h"
+#include "xdiffi.h"
+#include "xemit.h"
+
+
+#endif /* #if !defined(XINCLUDE_H) */
diff --git a/xdiff/xmacros.h b/xdiff/xmacros.h
new file mode 100644 (file)
index 0000000..8ef232c
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003  Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#if !defined(XMACROS_H)
+#define XMACROS_H
+
+
+
+
+#define XDL_MIN(a, b) ((a) < (b) ? (a): (b))
+#define XDL_MAX(a, b) ((a) > (b) ? (a): (b))
+#define XDL_ABS(v) ((v) >= 0 ? (v): -(v))
+#define XDL_ISDIGIT(c) ((c) >= '0' && (c) <= '9')
+#define XDL_ADDBITS(v,b)       ((v) + ((v) >> (b)))
+#define XDL_MASKBITS(b)                ((1UL << (b)) - 1)
+#define XDL_HASHLONG(v,b)      (XDL_ADDBITS((unsigned long)(v), b) & XDL_MASKBITS(b))
+#define XDL_PTRFREE(p) do { if (p) { xdl_free(p); (p) = NULL; } } while (0)
+#define XDL_LE32_PUT(p, v) \
+do { \
+       unsigned char *__p = (unsigned char *) (p); \
+       *__p++ = (unsigned char) (v); \
+       *__p++ = (unsigned char) ((v) >> 8); \
+       *__p++ = (unsigned char) ((v) >> 16); \
+       *__p = (unsigned char) ((v) >> 24); \
+} while (0)
+#define XDL_LE32_GET(p, v) \
+do { \
+       unsigned char const *__p = (unsigned char const *) (p); \
+       (v) = (unsigned long) __p[0] | ((unsigned long) __p[1]) << 8 | \
+               ((unsigned long) __p[2]) << 16 | ((unsigned long) __p[3]) << 24; \
+} while (0)
+
+
+#endif /* #if !defined(XMACROS_H) */
diff --git a/xdiff/xmerge.c b/xdiff/xmerge.c
new file mode 100644 (file)
index 0000000..b83b334
--- /dev/null
@@ -0,0 +1,426 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003-2006 Davide Libenzi, Johannes E. Schindelin
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#include "xinclude.h"
+
+typedef struct s_xdmerge {
+       struct s_xdmerge *next;
+       /*
+        * 0 = conflict,
+        * 1 = no conflict, take first,
+        * 2 = no conflict, take second.
+        */
+       int mode;
+       long i1, i2;
+       long chg1, chg2;
+} xdmerge_t;
+
+static int xdl_append_merge(xdmerge_t **merge, int mode,
+               long i1, long chg1, long i2, long chg2)
+{
+       xdmerge_t *m = *merge;
+       if (m && (i1 <= m->i1 + m->chg1 || i2 <= m->i2 + m->chg2)) {
+               if (mode != m->mode)
+                       m->mode = 0;
+               m->chg1 = i1 + chg1 - m->i1;
+               m->chg2 = i2 + chg2 - m->i2;
+       } else {
+               m = xdl_malloc(sizeof(xdmerge_t));
+               if (!m)
+                       return -1;
+               m->next = NULL;
+               m->mode = mode;
+               m->i1 = i1;
+               m->chg1 = chg1;
+               m->i2 = i2;
+               m->chg2 = chg2;
+               if (*merge)
+                       (*merge)->next = m;
+               *merge = m;
+       }
+       return 0;
+}
+
+static int xdl_cleanup_merge(xdmerge_t *c)
+{
+       int count = 0;
+       xdmerge_t *next_c;
+
+       /* were there conflicts? */
+       for (; c; c = next_c) {
+               if (c->mode == 0)
+                       count++;
+               next_c = c->next;
+               free(c);
+       }
+       return count;
+}
+
+static int xdl_merge_cmp_lines(xdfenv_t *xe1, int i1, xdfenv_t *xe2, int i2,
+               int line_count, long flags)
+{
+       int i;
+       xrecord_t **rec1 = xe1->xdf2.recs + i1;
+       xrecord_t **rec2 = xe2->xdf2.recs + i2;
+
+       for (i = 0; i < line_count; i++) {
+               int result = xdl_recmatch(rec1[i]->ptr, rec1[i]->size,
+                       rec2[i]->ptr, rec2[i]->size, flags);
+               if (!result)
+                       return -1;
+       }
+       return 0;
+}
+
+static int xdl_recs_copy(xdfenv_t *xe, int i, int count, int add_nl, char *dest)
+{
+       xrecord_t **recs = xe->xdf2.recs + i;
+       int size = 0;
+
+       if (count < 1)
+               return 0;
+
+       for (i = 0; i < count; size += recs[i++]->size)
+               if (dest)
+                       memcpy(dest + size, recs[i]->ptr, recs[i]->size);
+       if (add_nl) {
+               i = recs[count - 1]->size;
+               if (i == 0 || recs[count - 1]->ptr[i - 1] != '\n') {
+                       if (dest)
+                               dest[size] = '\n';
+                       size++;
+               }
+       }
+       return size;
+}
+
+static int xdl_fill_merge_buffer(xdfenv_t *xe1, const char *name1,
+               xdfenv_t *xe2, const char *name2, xdmerge_t *m, char *dest)
+{
+       const int marker_size = 7;
+       int marker1_size = (name1 ? strlen(name1) + 1 : 0);
+       int marker2_size = (name2 ? strlen(name2) + 1 : 0);
+       int conflict_marker_size = 3 * (marker_size + 1)
+               + marker1_size + marker2_size;
+       int size, i1, j;
+
+       for (size = i1 = 0; m; m = m->next) {
+               if (m->mode == 0) {
+                       size += xdl_recs_copy(xe1, i1, m->i1 - i1, 0,
+                                       dest ? dest + size : NULL);
+                       if (dest) {
+                               for (j = 0; j < marker_size; j++)
+                                       dest[size++] = '<';
+                               if (marker1_size) {
+                                       dest[size] = ' ';
+                                       memcpy(dest + size + 1, name1,
+                                                       marker1_size - 1);
+                                       size += marker1_size;
+                               }
+                               dest[size++] = '\n';
+                       } else
+                               size += conflict_marker_size;
+                       size += xdl_recs_copy(xe1, m->i1, m->chg1, 1,
+                                       dest ? dest + size : NULL);
+                       if (dest) {
+                               for (j = 0; j < marker_size; j++)
+                                       dest[size++] = '=';
+                               dest[size++] = '\n';
+                       }
+                       size += xdl_recs_copy(xe2, m->i2, m->chg2, 1,
+                                       dest ? dest + size : NULL);
+                       if (dest) {
+                               for (j = 0; j < marker_size; j++)
+                                       dest[size++] = '>';
+                               if (marker2_size) {
+                                       dest[size] = ' ';
+                                       memcpy(dest + size + 1, name2,
+                                                       marker2_size - 1);
+                                       size += marker2_size;
+                               }
+                               dest[size++] = '\n';
+                       }
+               } else if (m->mode == 1)
+                       size += xdl_recs_copy(xe1, i1, m->i1 + m->chg1 - i1, 0,
+                                       dest ? dest + size : NULL);
+               else if (m->mode == 2)
+                       size += xdl_recs_copy(xe2, m->i2 - m->i1 + i1,
+                                       m->i1 + m->chg2 - i1, 0,
+                                       dest ? dest + size : NULL);
+               else
+                       continue;
+               i1 = m->i1 + m->chg1;
+       }
+       size += xdl_recs_copy(xe1, i1, xe1->xdf2.nrec - i1, 0,
+                       dest ? dest + size : NULL);
+       return size;
+}
+
+/*
+ * Sometimes, changes are not quite identical, but differ in only a few
+ * lines. Try hard to show only these few lines as conflicting.
+ */
+static int xdl_refine_conflicts(xdfenv_t *xe1, xdfenv_t *xe2, xdmerge_t *m,
+               xpparam_t const *xpp)
+{
+       for (; m; m = m->next) {
+               mmfile_t t1, t2;
+               xdfenv_t xe;
+               xdchange_t *xscr, *x;
+               int i1 = m->i1, i2 = m->i2;
+
+               /* let's handle just the conflicts */
+               if (m->mode)
+                       continue;
+
+               /* no sense refining a conflict when one side is empty */
+               if (m->chg1 == 0 || m->chg2 == 0)
+                       continue;
+
+               /*
+                * This probably does not work outside git, since
+                * we have a very simple mmfile structure.
+                */
+               t1.ptr = (char *)xe1->xdf2.recs[m->i1]->ptr;
+               t1.size = xe1->xdf2.recs[m->i1 + m->chg1 - 1]->ptr
+                       + xe1->xdf2.recs[m->i1 + m->chg1 - 1]->size - t1.ptr;
+               t2.ptr = (char *)xe2->xdf2.recs[m->i2]->ptr;
+               t2.size = xe2->xdf2.recs[m->i2 + m->chg2 - 1]->ptr
+                       + xe2->xdf2.recs[m->i2 + m->chg2 - 1]->size - t2.ptr;
+               if (xdl_do_diff(&t1, &t2, xpp, &xe) < 0)
+                       return -1;
+               if (xdl_change_compact(&xe.xdf1, &xe.xdf2, xpp->flags) < 0 ||
+                   xdl_change_compact(&xe.xdf2, &xe.xdf1, xpp->flags) < 0 ||
+                   xdl_build_script(&xe, &xscr) < 0) {
+                       xdl_free_env(&xe);
+                       return -1;
+               }
+               if (!xscr) {
+                       /* If this happens, the changes are identical. */
+                       xdl_free_env(&xe);
+                       m->mode = 4;
+                       continue;
+               }
+               x = xscr;
+               m->i1 = xscr->i1 + i1;
+               m->chg1 = xscr->chg1;
+               m->i2 = xscr->i2 + i2;
+               m->chg2 = xscr->chg2;
+               while (xscr->next) {
+                       xdmerge_t *m2 = xdl_malloc(sizeof(xdmerge_t));
+                       if (!m2) {
+                               xdl_free_env(&xe);
+                               xdl_free_script(x);
+                               return -1;
+                       }
+                       xscr = xscr->next;
+                       m2->next = m->next;
+                       m->next = m2;
+                       m = m2;
+                       m->mode = 0;
+                       m->i1 = xscr->i1 + i1;
+                       m->chg1 = xscr->chg1;
+                       m->i2 = xscr->i2 + i2;
+                       m->chg2 = xscr->chg2;
+               }
+               xdl_free_env(&xe);
+               xdl_free_script(x);
+       }
+       return 0;
+}
+
+/*
+ * level == 0: mark all overlapping changes as conflict
+ * level == 1: mark overlapping changes as conflict only if not identical
+ * level == 2: analyze non-identical changes for minimal conflict set
+ *
+ * returns < 0 on error, == 0 for no conflicts, else number of conflicts
+ */
+static int xdl_do_merge(xdfenv_t *xe1, xdchange_t *xscr1, const char *name1,
+               xdfenv_t *xe2, xdchange_t *xscr2, const char *name2,
+               int level, xpparam_t const *xpp, mmbuffer_t *result) {
+       xdmerge_t *changes, *c;
+       int i1, i2, chg1, chg2;
+
+       c = changes = NULL;
+
+       while (xscr1 && xscr2) {
+               if (!changes)
+                       changes = c;
+               if (xscr1->i1 + xscr1->chg1 < xscr2->i1) {
+                       i1 = xscr1->i2;
+                       i2 = xscr2->i2 - xscr2->i1 + xscr1->i1;
+                       chg1 = xscr1->chg2;
+                       chg2 = xscr1->chg1;
+                       if (xdl_append_merge(&c, 1, i1, chg1, i2, chg2)) {
+                               xdl_cleanup_merge(changes);
+                               return -1;
+                       }
+                       xscr1 = xscr1->next;
+                       continue;
+               }
+               if (xscr2->i1 + xscr2->chg1 < xscr1->i1) {
+                       i1 = xscr1->i2 - xscr1->i1 + xscr2->i1;
+                       i2 = xscr2->i2;
+                       chg1 = xscr2->chg1;
+                       chg2 = xscr2->chg2;
+                       if (xdl_append_merge(&c, 2, i1, chg1, i2, chg2)) {
+                               xdl_cleanup_merge(changes);
+                               return -1;
+                       }
+                       xscr2 = xscr2->next;
+                       continue;
+               }
+               if (level < 1 || xscr1->i1 != xscr2->i1 ||
+                               xscr1->chg1 != xscr2->chg1 ||
+                               xscr1->chg2 != xscr2->chg2 ||
+                               xdl_merge_cmp_lines(xe1, xscr1->i2,
+                                       xe2, xscr2->i2,
+                                       xscr1->chg2, xpp->flags)) {
+                       /* conflict */
+                       int off = xscr1->i1 - xscr2->i1;
+                       int ffo = off + xscr1->chg1 - xscr2->chg1;
+
+                       i1 = xscr1->i2;
+                       i2 = xscr2->i2;
+                       if (off > 0)
+                               i1 -= off;
+                       else
+                               i2 += off;
+                       chg1 = xscr1->i2 + xscr1->chg2 - i1;
+                       chg2 = xscr2->i2 + xscr2->chg2 - i2;
+                       if (ffo > 0)
+                               chg2 += ffo;
+                       else
+                               chg1 -= ffo;
+                       if (xdl_append_merge(&c, 0, i1, chg1, i2, chg2)) {
+                               xdl_cleanup_merge(changes);
+                               return -1;
+                       }
+               }
+
+               i1 = xscr1->i1 + xscr1->chg1;
+               i2 = xscr2->i1 + xscr2->chg1;
+
+               if (i1 >= i2)
+                       xscr2 = xscr2->next;
+               if (i2 >= i1)
+                       xscr1 = xscr1->next;
+       }
+       while (xscr1) {
+               if (!changes)
+                       changes = c;
+               i1 = xscr1->i2;
+               i2 = xscr1->i1 + xe2->xdf2.nrec - xe2->xdf1.nrec;
+               chg1 = xscr1->chg2;
+               chg2 = xscr1->chg1;
+               if (xdl_append_merge(&c, 1, i1, chg1, i2, chg2)) {
+                       xdl_cleanup_merge(changes);
+                       return -1;
+               }
+               xscr1 = xscr1->next;
+       }
+       while (xscr2) {
+               if (!changes)
+                       changes = c;
+               i1 = xscr2->i1 + xe1->xdf2.nrec - xe1->xdf1.nrec;
+               i2 = xscr2->i2;
+               chg1 = xscr2->chg1;
+               chg2 = xscr2->chg2;
+               if (xdl_append_merge(&c, 2, i1, chg1, i2, chg2)) {
+                       xdl_cleanup_merge(changes);
+                       return -1;
+               }
+               xscr2 = xscr2->next;
+       }
+       if (!changes)
+               changes = c;
+       /* refine conflicts */
+       if (level > 1 && xdl_refine_conflicts(xe1, xe2, changes, xpp) < 0) {
+               xdl_cleanup_merge(changes);
+               return -1;
+       }
+       /* output */
+       if (result) {
+               int size = xdl_fill_merge_buffer(xe1, name1, xe2, name2,
+                       changes, NULL);
+               result->ptr = xdl_malloc(size);
+               if (!result->ptr) {
+                       xdl_cleanup_merge(changes);
+                       return -1;
+               }
+               result->size = size;
+               xdl_fill_merge_buffer(xe1, name1, xe2, name2, changes,
+                               result->ptr);
+       }
+       return xdl_cleanup_merge(changes);
+}
+
+int xdl_merge(mmfile_t *orig, mmfile_t *mf1, const char *name1,
+               mmfile_t *mf2, const char *name2,
+               xpparam_t const *xpp, int level, mmbuffer_t *result) {
+       xdchange_t *xscr1, *xscr2;
+       xdfenv_t xe1, xe2;
+       int status;
+
+       result->ptr = NULL;
+       result->size = 0;
+
+       if (xdl_do_diff(orig, mf1, xpp, &xe1) < 0 ||
+                       xdl_do_diff(orig, mf2, xpp, &xe2) < 0) {
+               return -1;
+       }
+       if (xdl_change_compact(&xe1.xdf1, &xe1.xdf2, xpp->flags) < 0 ||
+           xdl_change_compact(&xe1.xdf2, &xe1.xdf1, xpp->flags) < 0 ||
+           xdl_build_script(&xe1, &xscr1) < 0) {
+               xdl_free_env(&xe1);
+               return -1;
+       }
+       if (xdl_change_compact(&xe2.xdf1, &xe2.xdf2, xpp->flags) < 0 ||
+           xdl_change_compact(&xe2.xdf2, &xe2.xdf1, xpp->flags) < 0 ||
+           xdl_build_script(&xe2, &xscr2) < 0) {
+               xdl_free_env(&xe2);
+               return -1;
+       }
+       status = 0;
+       if (xscr1 || xscr2) {
+               if (!xscr1) {
+                       result->ptr = xdl_malloc(mf2->size);
+                       memcpy(result->ptr, mf2->ptr, mf2->size);
+                       result->size = mf2->size;
+               } else if (!xscr2) {
+                       result->ptr = xdl_malloc(mf1->size);
+                       memcpy(result->ptr, mf1->ptr, mf1->size);
+                       result->size = mf1->size;
+               } else {
+                       status = xdl_do_merge(&xe1, xscr1, name1,
+                                             &xe2, xscr2, name2,
+                                             level, xpp, result);
+               }
+               xdl_free_script(xscr1);
+               xdl_free_script(xscr2);
+       }
+       xdl_free_env(&xe1);
+       xdl_free_env(&xe2);
+
+       return status;
+}
diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c
new file mode 100644 (file)
index 0000000..e87ab57
--- /dev/null
@@ -0,0 +1,468 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003  Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#include "xinclude.h"
+
+
+
+#define XDL_KPDIS_RUN 4
+#define XDL_MAX_EQLIMIT 1024
+
+
+
+typedef struct s_xdlclass {
+       struct s_xdlclass *next;
+       unsigned long ha;
+       char const *line;
+       long size;
+       long idx;
+} xdlclass_t;
+
+typedef struct s_xdlclassifier {
+       unsigned int hbits;
+       long hsize;
+       xdlclass_t **rchash;
+       chastore_t ncha;
+       long count;
+       long flags;
+} xdlclassifier_t;
+
+
+
+
+static int xdl_init_classifier(xdlclassifier_t *cf, long size, long flags);
+static void xdl_free_classifier(xdlclassifier_t *cf);
+static int xdl_classify_record(xdlclassifier_t *cf, xrecord_t **rhash, unsigned int hbits,
+                              xrecord_t *rec);
+static int xdl_prepare_ctx(mmfile_t *mf, long narec, xpparam_t const *xpp,
+                          xdlclassifier_t *cf, xdfile_t *xdf);
+static void xdl_free_ctx(xdfile_t *xdf);
+static int xdl_clean_mmatch(char const *dis, long i, long s, long e);
+static int xdl_cleanup_records(xdfile_t *xdf1, xdfile_t *xdf2);
+static int xdl_trim_ends(xdfile_t *xdf1, xdfile_t *xdf2);
+static int xdl_optimize_ctxs(xdfile_t *xdf1, xdfile_t *xdf2);
+
+
+
+
+static int xdl_init_classifier(xdlclassifier_t *cf, long size, long flags) {
+       long i;
+
+       cf->flags = flags;
+
+       cf->hbits = xdl_hashbits((unsigned int) size);
+       cf->hsize = 1 << cf->hbits;
+
+       if (xdl_cha_init(&cf->ncha, sizeof(xdlclass_t), size / 4 + 1) < 0) {
+
+               return -1;
+       }
+       if (!(cf->rchash = (xdlclass_t **) xdl_malloc(cf->hsize * sizeof(xdlclass_t *)))) {
+
+               xdl_cha_free(&cf->ncha);
+               return -1;
+       }
+       for (i = 0; i < cf->hsize; i++)
+               cf->rchash[i] = NULL;
+
+       cf->count = 0;
+
+       return 0;
+}
+
+
+static void xdl_free_classifier(xdlclassifier_t *cf) {
+
+       xdl_free(cf->rchash);
+       xdl_cha_free(&cf->ncha);
+}
+
+
+static int xdl_classify_record(xdlclassifier_t *cf, xrecord_t **rhash, unsigned int hbits,
+                              xrecord_t *rec) {
+       long hi;
+       char const *line;
+       xdlclass_t *rcrec;
+
+       line = rec->ptr;
+       hi = (long) XDL_HASHLONG(rec->ha, cf->hbits);
+       for (rcrec = cf->rchash[hi]; rcrec; rcrec = rcrec->next)
+               if (rcrec->ha == rec->ha &&
+                               xdl_recmatch(rcrec->line, rcrec->size,
+                                       rec->ptr, rec->size, cf->flags))
+                       break;
+
+       if (!rcrec) {
+               if (!(rcrec = xdl_cha_alloc(&cf->ncha))) {
+
+                       return -1;
+               }
+               rcrec->idx = cf->count++;
+               rcrec->line = line;
+               rcrec->size = rec->size;
+               rcrec->ha = rec->ha;
+               rcrec->next = cf->rchash[hi];
+               cf->rchash[hi] = rcrec;
+       }
+
+       rec->ha = (unsigned long) rcrec->idx;
+
+       hi = (long) XDL_HASHLONG(rec->ha, hbits);
+       rec->next = rhash[hi];
+       rhash[hi] = rec;
+
+       return 0;
+}
+
+
+static int xdl_prepare_ctx(mmfile_t *mf, long narec, xpparam_t const *xpp,
+                          xdlclassifier_t *cf, xdfile_t *xdf) {
+       unsigned int hbits;
+       long i, nrec, hsize, bsize;
+       unsigned long hav;
+       char const *blk, *cur, *top, *prev;
+       xrecord_t *crec;
+       xrecord_t **recs, **rrecs;
+       xrecord_t **rhash;
+       unsigned long *ha;
+       char *rchg;
+       long *rindex;
+
+       if (xdl_cha_init(&xdf->rcha, sizeof(xrecord_t), narec / 4 + 1) < 0) {
+
+               return -1;
+       }
+       if (!(recs = (xrecord_t **) xdl_malloc(narec * sizeof(xrecord_t *)))) {
+
+               xdl_cha_free(&xdf->rcha);
+               return -1;
+       }
+
+       hbits = xdl_hashbits((unsigned int) narec);
+       hsize = 1 << hbits;
+       if (!(rhash = (xrecord_t **) xdl_malloc(hsize * sizeof(xrecord_t *)))) {
+
+               xdl_free(recs);
+               xdl_cha_free(&xdf->rcha);
+               return -1;
+       }
+       for (i = 0; i < hsize; i++)
+               rhash[i] = NULL;
+
+       nrec = 0;
+       if ((cur = blk = xdl_mmfile_first(mf, &bsize)) != NULL) {
+               for (top = blk + bsize;;) {
+                       if (cur >= top) {
+                               if (!(cur = blk = xdl_mmfile_next(mf, &bsize)))
+                                       break;
+                               top = blk + bsize;
+                       }
+                       prev = cur;
+                       hav = xdl_hash_record(&cur, top, xpp->flags);
+                       if (nrec >= narec) {
+                               narec *= 2;
+                               if (!(rrecs = (xrecord_t **) xdl_realloc(recs, narec * sizeof(xrecord_t *)))) {
+
+                                       xdl_free(rhash);
+                                       xdl_free(recs);
+                                       xdl_cha_free(&xdf->rcha);
+                                       return -1;
+                               }
+                               recs = rrecs;
+                       }
+                       if (!(crec = xdl_cha_alloc(&xdf->rcha))) {
+
+                               xdl_free(rhash);
+                               xdl_free(recs);
+                               xdl_cha_free(&xdf->rcha);
+                               return -1;
+                       }
+                       crec->ptr = prev;
+                       crec->size = (long) (cur - prev);
+                       crec->ha = hav;
+                       recs[nrec++] = crec;
+
+                       if (xdl_classify_record(cf, rhash, hbits, crec) < 0) {
+
+                               xdl_free(rhash);
+                               xdl_free(recs);
+                               xdl_cha_free(&xdf->rcha);
+                               return -1;
+                       }
+               }
+       }
+
+       if (!(rchg = (char *) xdl_malloc((nrec + 2) * sizeof(char)))) {
+
+               xdl_free(rhash);
+               xdl_free(recs);
+               xdl_cha_free(&xdf->rcha);
+               return -1;
+       }
+       memset(rchg, 0, (nrec + 2) * sizeof(char));
+
+       if (!(rindex = (long *) xdl_malloc((nrec + 1) * sizeof(long)))) {
+
+               xdl_free(rchg);
+               xdl_free(rhash);
+               xdl_free(recs);
+               xdl_cha_free(&xdf->rcha);
+               return -1;
+       }
+       if (!(ha = (unsigned long *) xdl_malloc((nrec + 1) * sizeof(unsigned long)))) {
+
+               xdl_free(rindex);
+               xdl_free(rchg);
+               xdl_free(rhash);
+               xdl_free(recs);
+               xdl_cha_free(&xdf->rcha);
+               return -1;
+       }
+
+       xdf->nrec = nrec;
+       xdf->recs = recs;
+       xdf->hbits = hbits;
+       xdf->rhash = rhash;
+       xdf->rchg = rchg + 1;
+       xdf->rindex = rindex;
+       xdf->nreff = 0;
+       xdf->ha = ha;
+       xdf->dstart = 0;
+       xdf->dend = nrec - 1;
+
+       return 0;
+}
+
+
+static void xdl_free_ctx(xdfile_t *xdf) {
+
+       xdl_free(xdf->rhash);
+       xdl_free(xdf->rindex);
+       xdl_free(xdf->rchg - 1);
+       xdl_free(xdf->ha);
+       xdl_free(xdf->recs);
+       xdl_cha_free(&xdf->rcha);
+}
+
+
+int xdl_prepare_env(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
+                   xdfenv_t *xe) {
+       long enl1, enl2;
+       xdlclassifier_t cf;
+
+       enl1 = xdl_guess_lines(mf1) + 1;
+       enl2 = xdl_guess_lines(mf2) + 1;
+
+       if (xdl_init_classifier(&cf, enl1 + enl2 + 1, xpp->flags) < 0) {
+
+               return -1;
+       }
+
+       if (xdl_prepare_ctx(mf1, enl1, xpp, &cf, &xe->xdf1) < 0) {
+
+               xdl_free_classifier(&cf);
+               return -1;
+       }
+       if (xdl_prepare_ctx(mf2, enl2, xpp, &cf, &xe->xdf2) < 0) {
+
+               xdl_free_ctx(&xe->xdf1);
+               xdl_free_classifier(&cf);
+               return -1;
+       }
+
+       xdl_free_classifier(&cf);
+
+       if (xdl_optimize_ctxs(&xe->xdf1, &xe->xdf2) < 0) {
+
+               xdl_free_ctx(&xe->xdf2);
+               xdl_free_ctx(&xe->xdf1);
+               return -1;
+       }
+
+       return 0;
+}
+
+
+void xdl_free_env(xdfenv_t *xe) {
+
+       xdl_free_ctx(&xe->xdf2);
+       xdl_free_ctx(&xe->xdf1);
+}
+
+
+static int xdl_clean_mmatch(char const *dis, long i, long s, long e) {
+       long r, rdis0, rpdis0, rdis1, rpdis1;
+
+       /*
+        * Scans the lines before 'i' to find a run of lines that either
+        * have no match (dis[j] == 0) or have multiple matches (dis[j] > 1).
+        * Note that we always call this function with dis[i] > 1, so the
+        * current line (i) is already a multimatch line.
+        */
+       for (r = 1, rdis0 = 0, rpdis0 = 1; (i - r) >= s; r++) {
+               if (!dis[i - r])
+                       rdis0++;
+               else if (dis[i - r] == 2)
+                       rpdis0++;
+               else
+                       break;
+       }
+       /*
+        * If the run before the line 'i' found only multimatch lines, we
+        * return 0 and hence we don't make the current line (i) discarded.
+        * We want to discard multimatch lines only when they appear in the
+        * middle of runs with nomatch lines (dis[j] == 0).
+        */
+       if (rdis0 == 0)
+               return 0;
+       for (r = 1, rdis1 = 0, rpdis1 = 1; (i + r) <= e; r++) {
+               if (!dis[i + r])
+                       rdis1++;
+               else if (dis[i + r] == 2)
+                       rpdis1++;
+               else
+                       break;
+       }
+       /*
+        * If the run after the line 'i' found only multimatch lines, we
+        * return 0 and hence we don't make the current line (i) discarded.
+        */
+       if (rdis1 == 0)
+               return 0;
+       rdis1 += rdis0;
+       rpdis1 += rpdis0;
+
+       return rpdis1 * XDL_KPDIS_RUN < (rpdis1 + rdis1);
+}
+
+
+/*
+ * Try to reduce the problem complexity, discard records that have no
+ * matches on the other file. Also, lines that have multiple matches
+ * might be potentially discarded if they happear in a run of discardable.
+ */
+static int xdl_cleanup_records(xdfile_t *xdf1, xdfile_t *xdf2) {
+       long i, nm, rhi, nreff, mlim;
+       unsigned long hav;
+       xrecord_t **recs;
+       xrecord_t *rec;
+       char *dis, *dis1, *dis2;
+
+       if (!(dis = (char *) xdl_malloc(xdf1->nrec + xdf2->nrec + 2))) {
+
+               return -1;
+       }
+       memset(dis, 0, xdf1->nrec + xdf2->nrec + 2);
+       dis1 = dis;
+       dis2 = dis1 + xdf1->nrec + 1;
+
+       if ((mlim = xdl_bogosqrt(xdf1->nrec)) > XDL_MAX_EQLIMIT)
+               mlim = XDL_MAX_EQLIMIT;
+       for (i = xdf1->dstart, recs = &xdf1->recs[xdf1->dstart]; i <= xdf1->dend; i++, recs++) {
+               hav = (*recs)->ha;
+               rhi = (long) XDL_HASHLONG(hav, xdf2->hbits);
+               for (nm = 0, rec = xdf2->rhash[rhi]; rec; rec = rec->next)
+                       if (rec->ha == hav && ++nm == mlim)
+                               break;
+               dis1[i] = (nm == 0) ? 0: (nm >= mlim) ? 2: 1;
+       }
+
+       if ((mlim = xdl_bogosqrt(xdf2->nrec)) > XDL_MAX_EQLIMIT)
+               mlim = XDL_MAX_EQLIMIT;
+       for (i = xdf2->dstart, recs = &xdf2->recs[xdf2->dstart]; i <= xdf2->dend; i++, recs++) {
+               hav = (*recs)->ha;
+               rhi = (long) XDL_HASHLONG(hav, xdf1->hbits);
+               for (nm = 0, rec = xdf1->rhash[rhi]; rec; rec = rec->next)
+                       if (rec->ha == hav && ++nm == mlim)
+                               break;
+               dis2[i] = (nm == 0) ? 0: (nm >= mlim) ? 2: 1;
+       }
+
+       for (nreff = 0, i = xdf1->dstart, recs = &xdf1->recs[xdf1->dstart];
+            i <= xdf1->dend; i++, recs++) {
+               if (dis1[i] == 1 ||
+                   (dis1[i] == 2 && !xdl_clean_mmatch(dis1, i, xdf1->dstart, xdf1->dend))) {
+                       xdf1->rindex[nreff] = i;
+                       xdf1->ha[nreff] = (*recs)->ha;
+                       nreff++;
+               } else
+                       xdf1->rchg[i] = 1;
+       }
+       xdf1->nreff = nreff;
+
+       for (nreff = 0, i = xdf2->dstart, recs = &xdf2->recs[xdf2->dstart];
+            i <= xdf2->dend; i++, recs++) {
+               if (dis2[i] == 1 ||
+                   (dis2[i] == 2 && !xdl_clean_mmatch(dis2, i, xdf2->dstart, xdf2->dend))) {
+                       xdf2->rindex[nreff] = i;
+                       xdf2->ha[nreff] = (*recs)->ha;
+                       nreff++;
+               } else
+                       xdf2->rchg[i] = 1;
+       }
+       xdf2->nreff = nreff;
+
+       xdl_free(dis);
+
+       return 0;
+}
+
+
+/*
+ * Early trim initial and terminal matching records.
+ */
+static int xdl_trim_ends(xdfile_t *xdf1, xdfile_t *xdf2) {
+       long i, lim;
+       xrecord_t **recs1, **recs2;
+
+       recs1 = xdf1->recs;
+       recs2 = xdf2->recs;
+       for (i = 0, lim = XDL_MIN(xdf1->nrec, xdf2->nrec); i < lim;
+            i++, recs1++, recs2++)
+               if ((*recs1)->ha != (*recs2)->ha)
+                       break;
+
+       xdf1->dstart = xdf2->dstart = i;
+
+       recs1 = xdf1->recs + xdf1->nrec - 1;
+       recs2 = xdf2->recs + xdf2->nrec - 1;
+       for (lim -= i, i = 0; i < lim; i++, recs1--, recs2--)
+               if ((*recs1)->ha != (*recs2)->ha)
+                       break;
+
+       xdf1->dend = xdf1->nrec - i - 1;
+       xdf2->dend = xdf2->nrec - i - 1;
+
+       return 0;
+}
+
+
+static int xdl_optimize_ctxs(xdfile_t *xdf1, xdfile_t *xdf2) {
+
+       if (xdl_trim_ends(xdf1, xdf2) < 0 ||
+           xdl_cleanup_records(xdf1, xdf2) < 0) {
+
+               return -1;
+       }
+
+       return 0;
+}
diff --git a/xdiff/xprepare.h b/xdiff/xprepare.h
new file mode 100644 (file)
index 0000000..8fb06a5
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003  Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#if !defined(XPREPARE_H)
+#define XPREPARE_H
+
+
+
+int xdl_prepare_env(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
+                   xdfenv_t *xe);
+void xdl_free_env(xdfenv_t *xe);
+
+
+
+#endif /* #if !defined(XPREPARE_H) */
diff --git a/xdiff/xtypes.h b/xdiff/xtypes.h
new file mode 100644 (file)
index 0000000..2511aef
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003  Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#if !defined(XTYPES_H)
+#define XTYPES_H
+
+
+
+typedef struct s_chanode {
+       struct s_chanode *next;
+       long icurr;
+} chanode_t;
+
+typedef struct s_chastore {
+       chanode_t *head, *tail;
+       long isize, nsize;
+       chanode_t *ancur;
+       chanode_t *sncur;
+       long scurr;
+} chastore_t;
+
+typedef struct s_xrecord {
+       struct s_xrecord *next;
+       char const *ptr;
+       long size;
+       unsigned long ha;
+} xrecord_t;
+
+typedef struct s_xdfile {
+       chastore_t rcha;
+       long nrec;
+       unsigned int hbits;
+       xrecord_t **rhash;
+       long dstart, dend;
+       xrecord_t **recs;
+       char *rchg;
+       long *rindex;
+       long nreff;
+       unsigned long *ha;
+} xdfile_t;
+
+typedef struct s_xdfenv {
+       xdfile_t xdf1, xdf2;
+} xdfenv_t;
+
+
+
+#endif /* #if !defined(XTYPES_H) */
diff --git a/xdiff/xutils.c b/xdiff/xutils.c
new file mode 100644 (file)
index 0000000..d7974d1
--- /dev/null
@@ -0,0 +1,380 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003 Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#include "xinclude.h"
+
+
+
+#define XDL_GUESS_NLINES 256
+
+
+
+
+long xdl_bogosqrt(long n) {
+       long i;
+
+       /*
+        * Classical integer square root approximation using shifts.
+        */
+       for (i = 1; n > 0; n >>= 2)
+               i <<= 1;
+
+       return i;
+}
+
+
+int xdl_emit_diffrec(char const *rec, long size, char const *pre, long psize,
+                    xdemitcb_t *ecb) {
+       int i = 2;
+       mmbuffer_t mb[3];
+
+       mb[0].ptr = (char *) pre;
+       mb[0].size = psize;
+       mb[1].ptr = (char *) rec;
+       mb[1].size = size;
+       if (size > 0 && rec[size - 1] != '\n') {
+               mb[2].ptr = (char *) "\n\\ No newline at end of file\n";
+               mb[2].size = strlen(mb[2].ptr);
+               i++;
+       }
+       if (ecb->outf(ecb->priv, mb, i) < 0) {
+
+               return -1;
+       }
+
+       return 0;
+}
+
+void *xdl_mmfile_first(mmfile_t *mmf, long *size)
+{
+       *size = mmf->size;
+       return mmf->ptr;
+}
+
+
+void *xdl_mmfile_next(mmfile_t *mmf, long *size)
+{
+       return NULL;
+}
+
+
+long xdl_mmfile_size(mmfile_t *mmf)
+{
+       return mmf->size;
+}
+
+
+int xdl_cha_init(chastore_t *cha, long isize, long icount) {
+
+       cha->head = cha->tail = NULL;
+       cha->isize = isize;
+       cha->nsize = icount * isize;
+       cha->ancur = cha->sncur = NULL;
+       cha->scurr = 0;
+
+       return 0;
+}
+
+
+void xdl_cha_free(chastore_t *cha) {
+       chanode_t *cur, *tmp;
+
+       for (cur = cha->head; (tmp = cur) != NULL;) {
+               cur = cur->next;
+               xdl_free(tmp);
+       }
+}
+
+
+void *xdl_cha_alloc(chastore_t *cha) {
+       chanode_t *ancur;
+       void *data;
+
+       if (!(ancur = cha->ancur) || ancur->icurr == cha->nsize) {
+               if (!(ancur = (chanode_t *) xdl_malloc(sizeof(chanode_t) + cha->nsize))) {
+
+                       return NULL;
+               }
+               ancur->icurr = 0;
+               ancur->next = NULL;
+               if (cha->tail)
+                       cha->tail->next = ancur;
+               if (!cha->head)
+                       cha->head = ancur;
+               cha->tail = ancur;
+               cha->ancur = ancur;
+       }
+
+       data = (char *) ancur + sizeof(chanode_t) + ancur->icurr;
+       ancur->icurr += cha->isize;
+
+       return data;
+}
+
+
+void *xdl_cha_first(chastore_t *cha) {
+       chanode_t *sncur;
+
+       if (!(cha->sncur = sncur = cha->head))
+               return NULL;
+
+       cha->scurr = 0;
+
+       return (char *) sncur + sizeof(chanode_t) + cha->scurr;
+}
+
+
+void *xdl_cha_next(chastore_t *cha) {
+       chanode_t *sncur;
+
+       if (!(sncur = cha->sncur))
+               return NULL;
+       cha->scurr += cha->isize;
+       if (cha->scurr == sncur->icurr) {
+               if (!(sncur = cha->sncur = sncur->next))
+                       return NULL;
+               cha->scurr = 0;
+       }
+
+       return (char *) sncur + sizeof(chanode_t) + cha->scurr;
+}
+
+
+long xdl_guess_lines(mmfile_t *mf) {
+       long nl = 0, size, tsize = 0;
+       char const *data, *cur, *top;
+
+       if ((cur = data = xdl_mmfile_first(mf, &size)) != NULL) {
+               for (top = data + size; nl < XDL_GUESS_NLINES;) {
+                       if (cur >= top) {
+                               tsize += (long) (cur - data);
+                               if (!(cur = data = xdl_mmfile_next(mf, &size)))
+                                       break;
+                               top = data + size;
+                       }
+                       nl++;
+                       if (!(cur = memchr(cur, '\n', top - cur)))
+                               cur = top;
+                       else
+                               cur++;
+               }
+               tsize += (long) (cur - data);
+       }
+
+       if (nl && tsize)
+               nl = xdl_mmfile_size(mf) / (tsize / nl);
+
+       return nl + 1;
+}
+
+int xdl_recmatch(const char *l1, long s1, const char *l2, long s2, long flags)
+{
+       int i1, i2;
+
+       if (flags & XDF_IGNORE_WHITESPACE) {
+               for (i1 = i2 = 0; i1 < s1 && i2 < s2; ) {
+                       if (isspace(l1[i1]))
+                               while (isspace(l1[i1]) && i1 < s1)
+                                       i1++;
+                       if (isspace(l2[i2]))
+                               while (isspace(l2[i2]) && i2 < s2)
+                                       i2++;
+                       if (i1 < s1 && i2 < s2 && l1[i1++] != l2[i2++])
+                               return 0;
+               }
+               return (i1 >= s1 && i2 >= s2);
+       } else if (flags & XDF_IGNORE_WHITESPACE_CHANGE) {
+               for (i1 = i2 = 0; i1 < s1 && i2 < s2; ) {
+                       if (isspace(l1[i1])) {
+                               if (!isspace(l2[i2]))
+                                       return 0;
+                               while (isspace(l1[i1]) && i1 < s1)
+                                       i1++;
+                               while (isspace(l2[i2]) && i2 < s2)
+                                       i2++;
+                       } else if (l1[i1++] != l2[i2++])
+                               return 0;
+               }
+               return (i1 >= s1 && i2 >= s2);
+       } else if (flags & XDF_IGNORE_WHITESPACE_AT_EOL) {
+               for (i1 = i2 = 0; i1 < s1 && i2 < s2; ) {
+                       if (l1[i1] != l2[i2]) {
+                               while (i1 < s1 && isspace(l1[i1]))
+                                       i1++;
+                               while (i2 < s2 && isspace(l2[i2]))
+                                       i2++;
+                               if (i1 < s1 || i2 < s2)
+                                       return 0;
+                               return 1;
+                       }
+                       i1++;
+                       i2++;
+               }
+               return i1 >= s1 && i2 >= s2;
+       } else
+               return s1 == s2 && !memcmp(l1, l2, s1);
+}
+
+static unsigned long xdl_hash_record_with_whitespace(char const **data,
+               char const *top, long flags) {
+       unsigned long ha = 5381;
+       char const *ptr = *data;
+
+       for (; ptr < top && *ptr != '\n'; ptr++) {
+               if (isspace(*ptr)) {
+                       const char *ptr2 = ptr;
+                       while (ptr + 1 < top && isspace(ptr[1])
+                                       && ptr[1] != '\n')
+                               ptr++;
+                       if (flags & XDF_IGNORE_WHITESPACE_CHANGE
+                                       && ptr[1] != '\n') {
+                               ha += (ha << 5);
+                               ha ^= (unsigned long) ' ';
+                       }
+                       if (flags & XDF_IGNORE_WHITESPACE_AT_EOL
+                                       && ptr[1] != '\n') {
+                               while (ptr2 != ptr + 1) {
+                                       ha += (ha << 5);
+                                       ha ^= (unsigned long) *ptr2;
+                                       ptr2++;
+                               }
+                       }
+                       continue;
+               }
+               ha += (ha << 5);
+               ha ^= (unsigned long) *ptr;
+       }
+       *data = ptr < top ? ptr + 1: ptr;
+
+       return ha;
+}
+
+
+unsigned long xdl_hash_record(char const **data, char const *top, long flags) {
+       unsigned long ha = 5381;
+       char const *ptr = *data;
+
+       if (flags & XDF_WHITESPACE_FLAGS)
+               return xdl_hash_record_with_whitespace(data, top, flags);
+
+       for (; ptr < top && *ptr != '\n'; ptr++) {
+               ha += (ha << 5);
+               ha ^= (unsigned long) *ptr;
+       }
+       *data = ptr < top ? ptr + 1: ptr;
+
+       return ha;
+}
+
+
+unsigned int xdl_hashbits(unsigned int size) {
+       unsigned int val = 1, bits = 0;
+
+       for (; val < size && bits < CHAR_BIT * sizeof(unsigned int); val <<= 1, bits++);
+       return bits ? bits: 1;
+}
+
+
+int xdl_num_out(char *out, long val) {
+       char *ptr, *str = out;
+       char buf[32];
+
+       ptr = buf + sizeof(buf) - 1;
+       *ptr = '\0';
+       if (val < 0) {
+               *--ptr = '-';
+               val = -val;
+       }
+       for (; val && ptr > buf; val /= 10)
+               *--ptr = "0123456789"[val % 10];
+       if (*ptr)
+               for (; *ptr; ptr++, str++)
+                       *str = *ptr;
+       else
+               *str++ = '0';
+       *str = '\0';
+
+       return str - out;
+}
+
+
+long xdl_atol(char const *str, char const **next) {
+       long val, base;
+       char const *top;
+
+       for (top = str; XDL_ISDIGIT(*top); top++);
+       if (next)
+               *next = top;
+       for (val = 0, base = 1, top--; top >= str; top--, base *= 10)
+               val += base * (long)(*top - '0');
+       return val;
+}
+
+
+int xdl_emit_hunk_hdr(long s1, long c1, long s2, long c2,
+                     const char *func, long funclen, xdemitcb_t *ecb) {
+       int nb = 0;
+       mmbuffer_t mb;
+       char buf[128];
+
+       memcpy(buf, "@@ -", 4);
+       nb += 4;
+
+       nb += xdl_num_out(buf + nb, c1 ? s1: s1 - 1);
+
+       if (c1 != 1) {
+               memcpy(buf + nb, ",", 1);
+               nb += 1;
+
+               nb += xdl_num_out(buf + nb, c1);
+       }
+
+       memcpy(buf + nb, " +", 2);
+       nb += 2;
+
+       nb += xdl_num_out(buf + nb, c2 ? s2: s2 - 1);
+
+       if (c2 != 1) {
+               memcpy(buf + nb, ",", 1);
+               nb += 1;
+
+               nb += xdl_num_out(buf + nb, c2);
+       }
+
+       memcpy(buf + nb, " @@", 3);
+       nb += 3;
+       if (func && funclen) {
+               buf[nb++] = ' ';
+               if (funclen > sizeof(buf) - nb - 1)
+                       funclen = sizeof(buf) - nb - 1;
+               memcpy(buf + nb, func, funclen);
+               nb += funclen;
+       }
+       buf[nb++] = '\n';
+
+       mb.ptr = buf;
+       mb.size = nb;
+       if (ecb->outf(ecb->priv, &mb, 1) < 0)
+               return -1;
+
+       return 0;
+}
diff --git a/xdiff/xutils.h b/xdiff/xutils.h
new file mode 100644 (file)
index 0000000..d5de829
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ *  LibXDiff by Davide Libenzi ( File Differential Library )
+ *  Copyright (C) 2003  Davide Libenzi
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *  Davide Libenzi <davidel@xmailserver.org>
+ *
+ */
+
+#if !defined(XUTILS_H)
+#define XUTILS_H
+
+
+
+long xdl_bogosqrt(long n);
+int xdl_emit_diffrec(char const *rec, long size, char const *pre, long psize,
+                    xdemitcb_t *ecb);
+int xdl_cha_init(chastore_t *cha, long isize, long icount);
+void xdl_cha_free(chastore_t *cha);
+void *xdl_cha_alloc(chastore_t *cha);
+void *xdl_cha_first(chastore_t *cha);
+void *xdl_cha_next(chastore_t *cha);
+long xdl_guess_lines(mmfile_t *mf);
+int xdl_recmatch(const char *l1, long s1, const char *l2, long s2, long flags);
+unsigned long xdl_hash_record(char const **data, char const *top, long flags);
+unsigned int xdl_hashbits(unsigned int size);
+int xdl_num_out(char *out, long val);
+long xdl_atol(char const *str, char const **next);
+int xdl_emit_hunk_hdr(long s1, long c1, long s2, long c2,
+                     const char *func, long funclen, xdemitcb_t *ecb);
+
+
+
+#endif /* #if !defined(XUTILS_H) */